Search code examples
c#expressionexpression-trees

Combining expressions c#


I need to concatenate two expressions (with or statement)

My code:

var items = new List<Item>
{
    new Item { Color = "Black", Categories = new List<string> { "cat1", "cat2" } },
    new Item { Color = "Red", Categories = new List<string> { "cat3" } },
    new Item { Color = "White", Categories = new List<string> { "cat1" } }
};

var categories = new List<string> { "cat2", "cat3" };

Expression<Func<Item, bool>> func1 = (x1) => x1.Color == "Black";
Expression<Func<Item, bool>> func2 = (x2) => x2.Categories.Any(y => categories.Where(z => z == y).Any());
Expression<Func<Item, bool>> fullExpression = Expression.Lambda<Func<Item, bool>>(
        Expression.Or(func1.Body, func2.Body), func1.Parameters.Single());

var result = items.AsQueryable().Where(fullExpression);
// result should be like this
// items.Where(x => (x.Color == "Black") || x.Categories.Any(y => categories.Where(z => z == y).Any()))

I get run-time error variable 'x2' of type 'Item' referenced from scope '', but it is not defined'

I also was trying to build an expression with ExpressionVisitor.

Here is ExpressionVisitor:

internal class ParameterReplacer : ExpressionVisitor
{
    private readonly ParameterExpression _parameter;

    internal ParameterReplacer(ParameterExpression parameter)
    {
        _parameter = parameter;
    }

    protected override Expression VisitParameter(ParameterExpression node)
    {
        return base.VisitParameter(_parameter);
    }
}

Here how I use it in code:

Expression<Func<Item, bool>> func1 = (x1) => x1.Color == "Black";
Expression<Func<Item, bool>> func2 = (x2) => x2.Categories.Any(y => categories.Select(z => z == y).Any());
var paramExpr = Expression.Parameter(typeof(Item));
var exprBody = Expression.Or(func1.Body, func2.Body);
exprBody = (BinaryExpression)new ParameterReplacer(paramExpr).Visit(exprBody);
var finalExpr = Expression.Lambda<Func<Item, bool>>(exprBody, paramExpr);
var result = items.AsQueryable().Where(finalExpr);

In this case during creating ParameterReplacer I'm getting error

System.InvalidOperationException: 'The operands for operator 'Equal' do not match the parameters of method 'op_Equality'.'

What did I do wrong?


Solution

  • Ian Newson is entirely right but if you want code, here you go :)

    Using these two classes you can combine the two predicates. I didn't come up with it but improved/adjusted it a bit and made it use the type Predicate instead of Func along with some newer language features (the original was quite old, sadly I don't remember where I found it).

    internal class SubstExpressionVisitor : ExpressionVisitor
    {
        private readonly Dictionary<Expression, Expression> _subst = new Dictionary<Expression, Expression>();
    
        protected override Expression VisitParameter(ParameterExpression node)
        {
            if (_subst.TryGetValue(node, out Expression newValue))
            {
                return newValue;
            }
    
            return node;
        }
    
        public Expression this[Expression original]
        {
            get => _subst[original];
            set => _subst[original] = value;
        }
    }
    
    public static class PredicateBuilder
    {
        // you don't seem to need this but it's included for completeness sake
        public static Expression<Predicate<T>> And<T>(this Expression<Predicate<T>> a, Expression<Predicate<T>> b)
        {
            if (a == null)
                throw new ArgumentNullException(nameof(a));
    
            if (b == null)
                throw new ArgumentNullException(nameof(b));
    
            ParameterExpression p = a.Parameters[0];
    
            SubstExpressionVisitor visitor = new SubstExpressionVisitor();
            visitor[b.Parameters[0]] = p;
    
            Expression body = Expression.AndAlso(a.Body, visitor.Visit(b.Body));
            return Expression.Lambda<Predicate<T>>(body, p);
        }
    
        public static Expression<Predicate<T>> Or<T>(this Expression<Predicate<T>> a, Expression<Predicate<T>> b)
        {
            if (a == null)
                throw new ArgumentNullException(nameof(a));
    
            if (b == null)
                throw new ArgumentNullException(nameof(b));
    
            ParameterExpression p = a.Parameters[0];
    
            SubstExpressionVisitor visitor = new SubstExpressionVisitor();
            visitor[b.Parameters[0]] = p;
    
            Expression body = Expression.OrElse(a.Body, visitor.Visit(b.Body));
            return Expression.Lambda<Predicate<T>>(body, p);
        }
    }
    

    You can use it like this:

    Expression<Predicate<Item>> func1 = (x1) => x1.Color == "Black";
    Expression<Predicate<Item>> func2 = (x2) => x2.Categories.Any(y => categories.Select(z => z == y).Any());
    
    Expression<Predicate<Item>> finalExpr = func1.Or(func2);
    

    You might want to keep in mind that my Or is using OrElse internally so the second expression won't be evaluated if the one before is evaluated to true (OrElse is like ||, not |). The same goes for And with AndAlso (AndAlso is like &&, not &).
    Another thing to note is that you can easily replace Predicate<T> with Func<T, bool> if you have to use Func for some reason :)