Extension method returning lambda expression through compare

前端 未结 3 1989
情话喂你
情话喂你 2021-02-15 16:48

I\'m in the process of creating a more elaborate filtering system for this huge project of ours. One of the main predicates is being able to pass comparations through a string p

相关标签:
3条回答
  • 2021-02-15 17:30

    One of the at-first-glance-magical features of the C# compiler can do the hard work for you. You probably know you can do this:

    Func<decimal, bool> totalCostIsUnder50 = d => d < 50m;
    

    that is, use a lambda expression to assign a Func. But did you know you can also do this:

    Expression<Func<decimal, bool>> totalCostIsUnder50Expression = d => d < 50m;
    

    that is, use a lambda expression to assign an Expression that expresses a Func? It's pretty neat.

    Given you say

    The comparison builder is not the issue, that's the easy bit. The hard part is actually returning the expression

    I'm assuming you can fill in the blanks here; suppose we pass in `"<50" to:

    Expression<Func<decimal, bool>> TotalCostCheckerBuilder(string criterion)
    {
        // Split criterion into operator and value
    
        // when operator is < do this:
        return d => d < value;
    
        // when operator is > do this:
        return d => d > value;
    
        // and so on
    }
    

    Finally, to compose your Expressions together with && (and still have an Expression), do this:

    var andExpression = Expression.And(firstExpression, secondExpression);
    
    0 讨论(0)
  • 2021-02-15 17:30

    The hard part is actually returning the expression.

    Translate strings into more structured constructions like enums and classes to define properties, operators and filters:

    Enum Parameter
        TotalCost
        Required
    End Enum
    
    Enum Comparator
        Less
        More
        Equals
    End Enum
    
    Class Criterion
        Public ReadOnly Parameter As Parameter
        Public ReadOnly Comparator As Comparator
        Public ReadOnly Value As Double
    
        Public Sub New(Parameter As Parameter, Comparator As Comparator, Value As Double)
            Me.Parameter = Parameter
            Me.Comparator = Comparator
            Me.Value = Value
        End Sub
    End Class
    

    Then a function to create expression is defined:

    Function CreateExpression(Criteria As IEnumerable(Of Criterion)) As Expression(Of Func(Of Field, Boolean))
        Dim FullExpression = PredicateBuilder.True(Of Field)()
    
        For Each Criterion In Criteria
            Dim Value = Criterion.Value
    
            Dim TotalCostExpressions As New Dictionary(Of Comparator, Expression(Of Func(Of Field, Boolean))) From {
                {Comparator.Less, Function(Field) Field.TotalCost < Value},
                {Comparator.More, Function(Field) Field.TotalCost > Value},
                {Comparator.Equals, Function(Field) Field.TotalCost = Value}
            }
    
            Dim RequiredExpressions As New Dictionary(Of Comparator, Expression(Of Func(Of Field, Boolean))) From {
                {Comparator.Less, Function(Field) Field.Required < Value},
                {Comparator.More, Function(Field) Field.Required > Value},
                {Comparator.Equals, Function(Field) Field.Required = Value}
            }
    
            Dim Expressions As New Dictionary(Of Parameter, IDictionary(Of Comparator, Expression(Of Func(Of Field, Boolean)))) From {
                {Parameter.TotalCost, TotalCostExpressions},
                {Parameter.Required, RequiredExpressions}}
    
            Dim Expression = Expressions(Criterion.Parameter)(Criterion.Comparator)
    
            FullExpression = Expression.And(Expression)
        Next
    
        Return FullExpression
    End Function
    

    PredicateBuilder taken here is needed to combine two expressions with AND operator.

    Usage:

    Function Usage() As Integer
    
        Dim Criteria = {
            New Criterion(Parameter.TotalCost, Comparator.Less, 50),
            New Criterion(Parameter.Required, Comparator.More, 5),
            New Criterion(Parameter.Required, Comparator.Less, 10)}
    
        Dim Expression = CreateExpression(Criteria)
    End Function
    

    It will create expression exactly like provided in an example

    field => field.TotalCost < 50 && field.Required > 5 && field.Required < 10
    
    0 讨论(0)
  • 2021-02-15 17:32

    To generate expression, that would be translated to SQL (eSQL) you should generate Expression manually. Here is example for GreaterThan filter creating, other filters can be made with similar technique.

    static Expression<Func<T, bool>> CreateGreaterThanExpression<T>(Expression<Func<T, decimal>> fieldExtractor, decimal value)
    {
        var xPar = Expression.Parameter(typeof(T), "x");
        var x = new ParameterRebinder(xPar);
        var getter = (MemberExpression)x.Visit(fieldExtractor.Body);
        var resultBody = Expression.GreaterThan(getter, Expression.Constant(value, typeof(decimal)));
        return Expression.Lambda<Func<T, bool>>(resultBody, xPar);
    }
    
    private sealed class ParameterRebinder : ExpressionVisitor
    {
        private readonly ParameterExpression _parameter;
    
        public ParameterRebinder(ParameterExpression parameter)
        { this._parameter = parameter; }
    
        protected override Expression VisitParameter(ParameterExpression p)
        { return base.VisitParameter(this._parameter); }
    }
    

    Here is the example of usage. (Assume, that we have StackEntites EF context with entity set TestEnitities of TestEntity entities)

    static void Main(string[] args)
    {
        using (var ents = new StackEntities())
        {
            var filter = CreateGreaterThanExpression<TestEnitity>(x => x.SortProperty, 3);
            var items = ents.TestEnitities.Where(filter).ToArray();
        }
    }
    

    Update: For your creation of complex expression you may use code like this: (Assume have already made CreateLessThanExpression and CreateBetweenExpression functions)

    static Expression<Func<T, bool>> CreateFilterFromString<T>(Expression<Func<T, decimal>> fieldExtractor, string text)
    {
        var greaterOrLessRegex = new Regex(@"^\s*(?<sign>\>|\<)\s*(?<number>\d+(\.\d+){0,1})\s*$");
        var match = greaterOrLessRegex.Match(text);
        if (match.Success)
        {
            var number = decimal.Parse(match.Result("${number}"));
            var sign = match.Result("${sign}");
            switch (sign)
            {
                case ">":
                    return CreateGreaterThanExpression(fieldExtractor, number);
                case "<":
                    return CreateLessThanExpression(fieldExtractor, number);
                default:
                    throw new Exception("Bad Sign!");
            }
        }
    
        var betweenRegex = new Regex(@"^\s*(?<number1>\d+(\.\d+){0,1})\s*-\s*(?<number2>\d+(\.\d+){0,1})\s*$");
        match = betweenRegex.Match(text);
        if (match.Success)
        {
            var number1 = decimal.Parse(match.Result("${number1}"));
            var number2 = decimal.Parse(match.Result("${number2}"));
            return CreateBetweenExpression(fieldExtractor, number1, number2);
        }
        throw new Exception("Bad filter Format!");
    }
    
    0 讨论(0)
提交回复
热议问题