Search code examples
c#expressionexpression-trees

How to combine expressions Expression<Func<T1, T2, bool>> to a single Expression<Func<T2, bool>>


I have a list of conditions composed of two Funcs:

   public Func<TConfiguration, string> ConfigurationField { get;}
   public Func<TNumbering, string> NumberingField { get; }

For each condition, expression would look like this:

Expression<Func<TNumbering, TConfiguration, bool>>  (n, c) => criteria.ConfigurationField(c) != criteria.NumberingField(n)

I need to chain the list of these expressions with OrElse.

I tried doing something like:

BinaryExpression expression = null;

        foreach (var criteria in SelectionCriteria)
        {
            Expression<Func<TNumbering, TConfiguration, bool>> exp = (n, c) => criteria.ConfigurationField(c) != criteria.NumberingField(n);
            expression = expression == null ? exp : Expression.OrElse(expression, exp);
        }
        if (expression == null) return Result.Failure("Expression not defined"));
        var lambda = Expression.Lambda<Func<TConfiguration, bool>>(expression);
        numberingsToRemove = numberings.Where(_ => configurations.All(lambda));

However, compiler doesn't like it, says there is no implicit conversion between Expression.Lambda<Func<TConfiguration, bool>> and Binary expression.

If I use

 expression = expression == null ? Expression.OrElse(exp, exp) : Expression.OrElse(expression, exp);

I get

The binary operator OrElse is not defined for the types 'System.Func<TNumbering,TConfiguration,System.Boolean> and 'System.Func<TNumbering,TConfiguration,System.Boolean>.

I am new to building expressions, can somebody point me in the right direction how to do this?


Solution

  • Your Expression<Func<TNumbering, TConfiguration, bool>> is a generic type whose open generic type is Expression<TDelegate>, where TDelegate is some delegate type; in this case Func<TNumbering, TConfiguration, bool>.

    Expression<TDelegate> inherits from LambdaExpression, which represents a C# (or VB.NET) lambda expression.

    Just like you couldn't write the following code:

    var result = 
        (n, c) => criteria.ConfigurationField(c) != criteria.NumberingField(n) ||
        (n1, c1) => criteria.ConfigurationField(c1) != criteria.NumberingField(n1);
    

    trying to combine two LambdaExpressions with OrElse would throw an exception at runtime.

    Your code isn't even compiling, because expression is typed as BinaryExpression, meaning an expression corresponding to this:

    criteria.ConfigurationField(c) != criteria.NumberingField(n) ||
    criteria.ConfigurationField(c1) != criteria.NumberingField(n1)
    

    into which you're trying to put a full Expression<TDelegate>, which includes (for example) the parameter list.


    Every LambdaExpression has a Body property, which extracts from an expression corresponding to this:

    (n, c) => criteria.ConfigurationField(c) != criteria.NumberingField(n)
    

    the body of the LambdaExpression, or an expression corresponding to this:

    criteria.ConfigurationField(c) != criteria.NumberingField(n)
    

    which, in theory, you could then combine into a BinaryExpression corresponding to this:

    criteria.ConfigurationField(c) != criteria.NumberingField(n) ||
    criteria.ConfigurationField(c1) != criteria.NumberingField(n1)
    

    But this wouldn't work either, because each iteration introduces multiple new parameters, all of which you'd have to pass in to the final lambda.


    It's possible to work around this problem, but I would suggest first and foremost you map each element in SelectionCriteria to an expression corresponding to the criteria evaluation, using the factory methods at System.Linq.Expressions.Expression. You could then combine those expressions into a BinaryExpression which you could then wrap up in a LambdaExpression or even an Expression.

    It might look something like this (making some assumptions):

    class Criteria<TConfiguration, TNumbering> {
        public Func<TConfiguration, string> ConfigurationField { get;}
        public Func<TNumbering, string> NumberingField { get; }
    }
    
    // using static System.Linq.Expressions.Expression;
    
    var SelectionCritera = new List<Criteria>();
    
    /*
     * populate list here
     */
    
    var configParam = Parameter(typeof(TConfiguration));
    var numberingParam = Parameter(typeof(TNumbering));
    var expressions =
        SelectionCriteria.Select(criteria => {
            var criteriaExpr = Constant(criteria);
    
            return NotEqual(                   // !=
                Invoke(                        // ( ... )
                    PropertyOrField(           // .ConfigurationField
                        criteriaExpr,          // criteria
                        "ConfigurationField"
                    ),
                    configParam                // c
                ),
                Invoke(                        // ( ... )
                    PropertyOrField(           // .NumberingField
                        criteriaExpr,          // criteria
                        "NumberingField"
                    ),
                    numberingParam             // n
                )
            );  
        })
        .ToList();
    
    if (!expressions.Any) { return Result.Failure("Expression not defined")); }
    
    // Combine all the subexpressions using ||
    var body = expressions.Aggregate((prev, next) => OrElse(prev, next));
    
    // Create a LambdaExpression
    var lmbd = Lambda<Func<TConfiguration, TNumbering, bool>>(body, configParam, numberingParam);
    
    // Create a .NET method from the LambdaExpression
    var mthd = lmbd.Compile();
    
    // Apply the method to each config/numbering pair
    var result = (
        from config in configs
        from numbering in numbering
        select (config, numbering)
    ).All(x => mthd(config, numbering));