Combining two expressions (Expression<Func<T, bool>>)

129,375

Solution 1

Well, you can use Expression.AndAlso / OrElse etc to combine logical expressions, but the problem is the parameters; are you working with the same ParameterExpression in expr1 and expr2? If so, it is easier:

var body = Expression.AndAlso(expr1.Body, expr2.Body);
var lambda = Expression.Lambda<Func<T,bool>>(body, expr1.Parameters[0]);

This also works well to negate a single operation:

static Expression<Func<T, bool>> Not<T>(
    this Expression<Func<T, bool>> expr)
{
    return Expression.Lambda<Func<T, bool>>(
        Expression.Not(expr.Body), expr.Parameters[0]);
}

Otherwise, depending on the LINQ provider, you might be able to combine them with Invoke:

// OrElse is very similar...
static Expression<Func<T, bool>> AndAlso<T>(
    this Expression<Func<T, bool>> left,
    Expression<Func<T, bool>> right)
{
    var param = Expression.Parameter(typeof(T), "x");
    var body = Expression.AndAlso(
            Expression.Invoke(left, param),
            Expression.Invoke(right, param)
        );
    var lambda = Expression.Lambda<Func<T, bool>>(body, param);
    return lambda;
}

Somewhere, I have got some code that re-writes an expression-tree replacing nodes to remove the need for Invoke, but it is quite lengthy (and I can't remember where I left it...)


Generalized version that picks the simplest route:

static Expression<Func<T, bool>> AndAlso<T>(
    this Expression<Func<T, bool>> expr1,
    Expression<Func<T, bool>> expr2)
{
    // need to detect whether they use the same
    // parameter instance; if not, they need fixing
    ParameterExpression param = expr1.Parameters[0];
    if (ReferenceEquals(param, expr2.Parameters[0]))
    {
        // simple version
        return Expression.Lambda<Func<T, bool>>(
            Expression.AndAlso(expr1.Body, expr2.Body), param);
    }
    // otherwise, keep expr1 "as is" and invoke expr2
    return Expression.Lambda<Func<T, bool>>(
        Expression.AndAlso(
            expr1.Body,
            Expression.Invoke(expr2, param)), param);
}

Starting from .NET 4.0, there is the ExpressionVisitor class which allows you to build expressions that are EF safe.

    public static Expression<Func<T, bool>> AndAlso<T>(
        this Expression<Func<T, bool>> expr1,
        Expression<Func<T, bool>> expr2)
    {
        var parameter = Expression.Parameter(typeof (T));

        var leftVisitor = new ReplaceExpressionVisitor(expr1.Parameters[0], parameter);
        var left = leftVisitor.Visit(expr1.Body);

        var rightVisitor = new ReplaceExpressionVisitor(expr2.Parameters[0], parameter);
        var right = rightVisitor.Visit(expr2.Body);

        return Expression.Lambda<Func<T, bool>>(
            Expression.AndAlso(left, right), parameter);
    }



    private class ReplaceExpressionVisitor
        : ExpressionVisitor
    {
        private readonly Expression _oldValue;
        private readonly Expression _newValue;

        public ReplaceExpressionVisitor(Expression oldValue, Expression newValue)
        {
            _oldValue = oldValue;
            _newValue = newValue;
        }

        public override Expression Visit(Expression node)
        {
            if (node == _oldValue)
                return _newValue;
            return base.Visit(node);
        }
    }

Solution 2

You can use Expression.AndAlso / OrElse to combine logical expressions, but you have to make sure the ParameterExpressions are the same.

I was having trouble with EF and the PredicateBuilder so I made my own without resorting to Invoke, that I could use like this:

var filterC = filterA.And(filterb);

Source code for my PredicateBuilder:

public static class PredicateBuilder {

    public static Expression<Func<T, bool>> And<T>(this Expression<Func<T, bool>> a, Expression<Func<T, bool>> b) {    

        ParameterExpression p = a.Parameters[0];

        SubstExpressionVisitor visitor = new SubstExpressionVisitor();
        visitor.subst[b.Parameters[0]] = p;

        Expression body = Expression.AndAlso(a.Body, visitor.Visit(b.Body));
        return Expression.Lambda<Func<T, bool>>(body, p);
    }

    public static Expression<Func<T, bool>> Or<T>(this Expression<Func<T, bool>> a, Expression<Func<T, bool>> b) {    

        ParameterExpression p = a.Parameters[0];

        SubstExpressionVisitor visitor = new SubstExpressionVisitor();
        visitor.subst[b.Parameters[0]] = p;

        Expression body = Expression.OrElse(a.Body, visitor.Visit(b.Body));
        return Expression.Lambda<Func<T, bool>>(body, p);
    }   
}

And the utility class to substitute the parameters in a lambda:

internal class SubstExpressionVisitor : System.Linq.Expressions.ExpressionVisitor {
        public Dictionary<Expression, Expression> subst = new Dictionary<Expression, Expression>();

        protected override Expression VisitParameter(ParameterExpression node) {
            Expression newValue;
            if (subst.TryGetValue(node, out newValue)) {
                return newValue;
            }
            return node;
        }
    }

Solution 3

If you provider does not support Invoke and you need to combine two expression, you can use an ExpressionVisitor to replace the parameter in the second expression by the parameter in the first expression.

class ParameterUpdateVisitor : ExpressionVisitor
{
    private ParameterExpression _oldParameter;
    private ParameterExpression _newParameter;

    public ParameterUpdateVisitor(ParameterExpression oldParameter, ParameterExpression newParameter)
    {
        _oldParameter = oldParameter;
        _newParameter = newParameter;
    }

    protected override Expression VisitParameter(ParameterExpression node)
    {
        if (object.ReferenceEquals(node, _oldParameter))
            return _newParameter;

        return base.VisitParameter(node);
    }
}

static Expression<Func<T, bool>> UpdateParameter<T>(
    Expression<Func<T, bool>> expr,
    ParameterExpression newParameter)
{
    var visitor = new ParameterUpdateVisitor(expr.Parameters[0], newParameter);
    var body = visitor.Visit(expr.Body);

    return Expression.Lambda<Func<T, bool>>(body, newParameter);
}

[TestMethod]
public void ExpressionText()
{
    string text = "test";

    Expression<Func<Coco, bool>> expr1 = p => p.Item1.Contains(text);
    Expression<Func<Coco, bool>> expr2 = q => q.Item2.Contains(text);
    Expression<Func<Coco, bool>> expr3 = UpdateParameter(expr2, expr1.Parameters[0]);

    var expr4 = Expression.Lambda<Func<Recording, bool>>(
        Expression.OrElse(expr1.Body, expr3.Body), expr1.Parameters[0]);

    var func = expr4.Compile();

    Assert.IsTrue(func(new Coco { Item1 = "caca", Item2 = "test pipi" }));
}

Solution 4

Nothing new here but married this answer with this answer and slightly refactored it so that even I understand what's going on:

public static class ExpressionExtensions
{
    public static Expression<Func<T, bool>> AndAlso<T>(this Expression<Func<T, bool>> expr1, Expression<Func<T, bool>> expr2)
    {
        ParameterExpression parameter1 = expr1.Parameters[0];
        var visitor = new ReplaceParameterVisitor(expr2.Parameters[0], parameter1);
        var body2WithParam1 = visitor.Visit(expr2.Body);
        return Expression.Lambda<Func<T, bool>>(Expression.AndAlso(expr1.Body, body2WithParam1), parameter1);
    }

    private class ReplaceParameterVisitor : ExpressionVisitor
    {
        private ParameterExpression _oldParameter;
        private ParameterExpression _newParameter;

        public ReplaceParameterVisitor(ParameterExpression oldParameter, ParameterExpression newParameter)
        {
            _oldParameter = oldParameter;
            _newParameter = newParameter;
        }

        protected override Expression VisitParameter(ParameterExpression node)
        {
            if (ReferenceEquals(node, _oldParameter))
                return _newParameter;

            return base.VisitParameter(node);
        }
    }
}

Solution 5

I combined some beautiful answers here to make it possible to easily support more Expression operators.

This is based on the answer of @Dejan but now it's quite easy to add the OR as well. I chose not to make the Combine function public, but you could do that to be even more flexible.

public static class ExpressionExtensions
{
    public static Expression<Func<T, bool>> AndAlso<T>(this Expression<Func<T, bool>> leftExpression,
        Expression<Func<T, bool>> rightExpression) =>
        Combine(leftExpression, rightExpression, Expression.AndAlso);

    public static Expression<Func<T, bool>> Or<T>(this Expression<Func<T, bool>> leftExpression,
        Expression<Func<T, bool>> rightExpression) =>
        Combine(leftExpression, rightExpression, Expression.Or);

    public static Expression<Func<T, bool>> Combine<T>(Expression<Func<T, bool>> leftExpression, Expression<Func<T, bool>> rightExpression, Func<Expression, Expression, BinaryExpression> combineOperator)
    {
        var leftParameter = leftExpression.Parameters[0];
        var rightParameter = rightExpression.Parameters[0];

        var visitor = new ReplaceParameterVisitor(rightParameter, leftParameter);

        var leftBody = leftExpression.Body;
        var rightBody = visitor.Visit(rightExpression.Body);

        return Expression.Lambda<Func<T, bool>>(combineOperator(leftBody, rightBody), leftParameter);
    }

    private class ReplaceParameterVisitor : ExpressionVisitor
    {
        private readonly ParameterExpression _oldParameter;
        private readonly ParameterExpression _newParameter;

        public ReplaceParameterVisitor(ParameterExpression oldParameter, ParameterExpression newParameter)
        {
            _oldParameter = oldParameter;
            _newParameter = newParameter;
        }

        protected override Expression VisitParameter(ParameterExpression node)
        {
            return ReferenceEquals(node, _oldParameter) ? _newParameter : base.VisitParameter(node);
        }
    }
}

Usage is not changed and still like this:

Expression<Func<Result, bool>> noFilterExpression = item => filters == null;

Expression<Func<Result, bool>> laptopFilterExpression = item => item.x == ...
Expression<Func<Result, bool>> dateFilterExpression = item => item.y == ...

var combinedFilterExpression = noFilterExpression.Or(laptopFilterExpression.AndAlso(dateFilterExpression));
    
efQuery.Where(combinedFilterExpression);

(This is an example based on my actual code, but read it as pseudo-code)

Share:
129,375

Related videos on Youtube

theh2o
Author by

theh2o

Updated on March 05, 2022

Comments

  • theh2o
    theh2o about 2 years

    I have two expressions of type Expression<Func<T, bool>> and I want to take to OR, AND or NOT of these and get a new expression of the same type

    Expression<Func<T, bool>> expr1;
    Expression<Func<T, bool>> expr2;
    
    ...
    
    //how to do this (the code below will obviously not work)
    Expression<Func<T, bool>> andExpression = expr AND expr2
    
  • andy
    andy almost 15 years
    Hey Marc, I tried out your first suggestion, in the your first code block above, but when I pass in the "lambda" expression<func<T,bool>> result in a Where method, I get an error saying the parameter is out of scope? any idea? cheers
  • Marc Gravell
    Marc Gravell almost 15 years
    @Andy - yes (see the first sentence) - you will get this if you are using different parameter instances in the two versions... I'll update with another option (but which doesn't work for all providers; LINQ-to-Objects and LINQ-to-SQL will be fine, but EF won't be...)
  • Marc Gravell
    Marc Gravell almost 15 years
    D'oh! I've already covered this in the second version... it can be simplified a little bit, though (will update)
  • Nicolas Fall
    Nicolas Fall over 14 years
    +1 the generalized version works like a charm, I used And instead of andalso, I thought linq to sql doesn't support andalso?
  • Mikhaël Bois
    Mikhaël Bois over 13 years
    you are a legend. I just tried it and the generalised version works excellently! (I tested it with NH 3.0 alpha 2)
  • Nicolas Fall
    Nicolas Fall about 13 years
    hmm getting the invoke not supported for EF4, is there a version that would work of yours for this marc?
  • Marc Gravell
    Marc Gravell about 13 years
    @Maslow - here's a rewriter that can inline the trees to save Invoke: stackoverflow.com/questions/1717444/…
  • Nicolas Fall
    Nicolas Fall about 13 years
    @Marc - thanks a ton, I imagine there are other places in the code where this was needed, but it looks so far like just chaining .Where(cond1).FirstOrDefault(cond2) is working in the place that brought me back here, not sure about scalability or performance.
  • VulgarBinary
    VulgarBinary over 11 years
    This solution was the only one that allowed me to have x => x.Property == Value combined with arg => arg.Property2 == Value. Major props, a little terse and confusing but it works so I'm not going to complain. Kudos Adam :-)
  • Shaun Wilson
    Shaun Wilson about 11 years
    This solved my particular problem where the other solution resulted in the same exception. Thanks.
  • Aaron Stainback
    Aaron Stainback almost 10 years
    This is a great solution.
  • Aaron Stainback
    Aaron Stainback almost 10 years
    This is a great solution.
  • Chris McAtackney
    Chris McAtackney almost 10 years
    Adam, this solved a very annoying problem I was having using the SharePoint Client Object model's Linq provider - thanks for posting it.
  • Aron
    Aron over 9 years
    -1 for using Expression.Invoke instead of using an Expression Tree Visitor to substitute the parameters.
  • Marc Gravell
    Marc Gravell over 9 years
    @Aron now look at the date: the .NET framework visitor (ExpressionVisitor) did not exist back then; I have a related example on stackoverflow from a similar date where it implements the visitor manually: it is a lot of code.
  • Aron
    Aron over 9 years
    @MarcGravell ah...I didn't realise that the Visitor was a new thing for .net 4. Do you mind if I update your answer for new users?
  • Marc Gravell
    Marc Gravell over 9 years
    @Aron by all means, go ahead
  • Prokurors
    Prokurors about 9 years
    @Aron Hi, I tried Your update with ExpressionVisitor and I get Expression of type 'System.Func2[(TSource),System.Boolean]' cannot be used for return type 'System.Boolean` what could be cause for that?
  • Zain Shaikh
    Zain Shaikh over 8 years
    Thanks @Marc, your solution helped a lot :) I needed this for OrElse condition so I modified your code for my needs. thanks again :)
  • Romain Vergnory
    Romain Vergnory almost 8 years
    this cannot be used in Linq to SQL for instance
  • Nishanth Shaan
    Nishanth Shaan almost 8 years
    hi @MarcGravell how to do it when we use two different 'ParameterExpression' please it would be really uselful for me
  • Marc Gravell
    Marc Gravell almost 8 years
    @Nishanth just use a Dictionary<Expression,Expression> and some TryGetValue and it should work for any number of swaps
  • Nishanth Shaan
    Nishanth Shaan almost 8 years
    Thanks, @MarcGravell but, I have followed the tuto codeproject.com/Tips/582450/… But wat i need is to be able to query the child entities also.. is it possible? what I can do now is db.Jobs.Include(j => j.SubCategory).Where(MyExpressionBuilder.Build<Job>(filters)‌​).ToList(); what i want to do : db.Jobs.Include(j => j.SubCategory). .Where(MyExpressionBuilder.Build<Job>(filters) && MyExpressionBuilder.Build<Subcategory>(filters2)).ToList(); plsConsultTheArticle forDetails, any idea please?
  • tokyo0709
    tokyo0709 over 7 years
    This worked for me! I had searched for a variety of solutions as well as predicate builder and nothing worked until this. Thank you!
  • Alexander Derck
    Alexander Derck over 7 years
    @MarcGravell Awesome piece of code! It's a bit faster to use expr1.Parameters[0] as the parameterexpression though, then you don't have to visit the left expression :-)
  • Tolga Evcimen
    Tolga Evcimen about 7 years
    This is a wonderful piece of code. I couldn't find a place to adjust the code, copy-paste and that's it :)
  • johnny 5
    johnny 5 over 6 years
    @MarkGravell, I'm using your first solution to combine my expressions, and everything is working fine even in entityframework, So what would the benefits of using the last solution be?
  • Dave Cousineau
    Dave Cousineau over 6 years
    @johnny5 the first (simpler) solution should only work if the parameters within the lambda bodies are the same parameters. given x => x > 5 and y => y < 100 you need to make z => z > 5 && z < 100, not z => x > 5 && y < 100. if x and y are already the same parameter somehow, then that's no problem. if they aren't then one expression needs to be rewritten with the parameter of the other, (or both rewritten with a new one), which is what the ReplaceExpressionVisitor allows. (note that the parameters need to be the same objects not just have the same names).
  • johnny 5
    johnny 5 over 6 years
    @DaveCousineau Yeah I found this out that hard when when it came back to bite me. But just to add to that point event if you have x => x > 5 and separate expression x => x < 100, those x's are local to the expression and if you try to use them even you will still get an exception
  • Andreas
    Andreas over 5 years
    Thank you very much
  • canton7
    canton7 over 4 years
    Note that you can optimize this somewhat: you don't need to rewrite both expr1 and expr2, but you can instead rewrite just expr2 and replace expr2.Parameters[0] with expr1.Parameters[0], leaving expr1 as-is
  • Kevin M. Lapio
    Kevin M. Lapio about 4 years
    I was having difficulty grasping the concept, and your melding of a couple other answers helped it click for me. Thanks!
  • user3071284
    user3071284 almost 4 years
    @MarcGravell Thank you for this. Do you know if the first code block (2 lines) is EF Core safe? It's reporting at compile time that it's not translatable, but I can't see why.
  • Marc Gravell
    Marc Gravell almost 4 years
    @user3071284 because parsing expression trees is hard, so sometimes we need to help them out; try the expression-visitor version at the bottom
  • ilyas varol
    ilyas varol about 2 years
    thanks, I integrated it to my code, but I didn't try if it works or not :)