How do I combine LINQ expressions into one?
Asked Answered
D

4

23

I've got a form with multiple fields on it (company name, postcode, etc.) which allows a user to search for companies in a database. If the user enters values in more than one field then I need to search on all of those fields. I am using LINQ to query the database.

So far I have managed to write a function which will look at their input and turn it into a List of expressions. I now want to turn that List into a single expression which I can then execute via the LINQ provider.

My initial attempt was as follows

private Expression<Func<Company, bool>> Combine(IList<Expression<Func<Company, bool>>> expressions)
    {
        if (expressions.Count == 0)
        {
            return null;
        }
        if (expressions.Count == 1)
        {
            return expressions[0];
        }
        Expression<Func<Company, bool>> combined = expressions[0];
        expressions.Skip(1).ToList().ForEach(expr => combined = Expression.And(combined, expr));
        return combined;
    }

However this fails with an exception message along the lines of "The binary operator And is not defined for...". Does anyone have any ideas what I need to do to combine these expressions?

EDIT: Corrected the line where I had forgotten to assign the result of and'ing the expressions together to a variable. Thanks for pointing that out folks.

Dicho answered 17/12, 2009 at 15:27 Comment(1)
If you need just logical 'And' it's better to use AndAlso to avoid unncecessary evaluations.Sapp
J
10

EDIT: Jason's answer is now fuller than mine was in terms of the expression tree stuff, so I've removed that bit. However, I wanted to leave this:

I assume you're using these for a Where clause... why not just call Where with each expression in turn? That should have the same effect:

var query = ...;
foreach (var condition in conditions)
{
    query = query.Where(condition);
}
Jacinthe answered 17/12, 2009 at 15:32 Comment(3)
@Jon Skeet: combined will be typed as an Expression; you need to do some work to return it as an Expression<Func<Company, bool>>.Gruchot
I agree your first code is easier to understand, so I will make this the correct answer. However I am actually going to use the second snippet as this is exactly what I need - I was making things far too complex, thanks Jon.Dicho
Ironically I was editing while both of these comments were written - but as it was this second snippet that was used, I think I'll leave it as it is :)Jacinthe
G
29

You can use Enumerable.Aggregate combined with Expression.AndAlso. Here's a generic version:

Expression<Func<T, bool>> AndAll<T>(
    IEnumerable<Expression<Func<T, bool>>> expressions) {

    if(expressions == null) {
        throw new ArgumentNullException("expressions");
    }
    if(expressions.Count() == 0) {
        return t => true;
    }
    Type delegateType = typeof(Func<,>)
                            .GetGenericTypeDefinition()
                            .MakeGenericType(new[] {
                                typeof(T),
                                typeof(bool) 
                            }
                        );
    var combined = expressions
                       .Cast<Expression>()
                       .Aggregate((e1, e2) => Expression.AndAlso(e1, e2));
    return (Expression<Func<T,bool>>)Expression.Lambda(delegateType, combined);
}

Your current code is never assigning to combined:

expr => Expression.And(combined, expr);

returns a new Expression that is the result of bitwise anding combined and expr but it does not mutate combined.

Gruchot answered 17/12, 2009 at 15:33 Comment(4)
+1 for a technically great answer, thanks. I've accepted Jon's as it appears simpler and also his use of Where is actually what I should be doing.Dicho
@gilles27: Yeah, if you're only using it for the predicate in a Where clause, then Jon's answer is the way to go. If you ever need a more general version, my version will help you. :-)Gruchot
I tried this but got the runtime error "System.ArgumentException: Incorrect number of parameters supplied for lambda declaration"Blakeley
stannius: Use the answer of Bike Hsu because it works compared to this one. This answer totally ignores that there is a parameter (v) that is defined in the two lambdas separately and referenced in the expressions. You have to add the parameter to the new lambda too...but you have to remap the usages inside the lambdas. If you do not code that tries to use that lambda (.Compile() call, nHibernate, EF Linq2SQL) is in trouble. Remapping may be done by reconstructing the lambda as @Jarekczek suggests or by using Invoke as in Bike Hsu's answer.Ihab
J
10

EDIT: Jason's answer is now fuller than mine was in terms of the expression tree stuff, so I've removed that bit. However, I wanted to leave this:

I assume you're using these for a Where clause... why not just call Where with each expression in turn? That should have the same effect:

var query = ...;
foreach (var condition in conditions)
{
    query = query.Where(condition);
}
Jacinthe answered 17/12, 2009 at 15:32 Comment(3)
@Jon Skeet: combined will be typed as an Expression; you need to do some work to return it as an Expression<Func<Company, bool>>.Gruchot
I agree your first code is easier to understand, so I will make this the correct answer. However I am actually going to use the second snippet as this is exactly what I need - I was making things far too complex, thanks Jon.Dicho
Ironically I was editing while both of these comments were written - but as it was this second snippet that was used, I think I'll leave it as it is :)Jacinthe
P
2

Here we have a general question about combining Linq expressions. I have a general solution for this problem. I will provide an answer regarding the specific problem posted, although it's definitely not the way to go in such cases. But when simple solutions fail in your case, you may try to use this approach.

First you need a library consisting of 2 simple functions. They use System.Linq.Expressions.ExpressionVisitor to dynamically modify expressions. The key feature is unifying parameters inside the expression, so that 2 parameters with the same name were made identical (UnifyParametersByName). The remaining part is replacing a named parameter with given expression (ReplacePar). The library is available with MIT license on github: LinqExprHelper, but you may quickly write something on your own.

The library allows for quite simple syntax for combining complex expressions. You can mix inline lambda expressions, which are nice to read, together with dynamic expression creation and composition, which is very capable.

    private static Expression<Func<Company, bool>> Combine(IList<Expression<Func<Company, bool>>> expressions)
    {
        if (expressions.Count == 0)
        {
            return null;
        }

        // Prepare a master expression, used to combine other
        // expressions. It needs more input parameters, they will
        // be reduced later.
        // There is a small inconvenience here: you have to use
        // the same name "c" for the parameter in your input
        // expressions. But it may be all done in a smarter way.
        Expression <Func<Company, bool, bool, bool>> combiningExpr =
            (c, expr1, expr2) => expr1 && expr2;

        LambdaExpression combined = expressions[0];
        foreach (var expr in expressions.Skip(1))
        {
            // ReplacePar comes from the library, it's an extension
            // requiring `using LinqExprHelper`.
            combined = combiningExpr
                .ReplacePar("expr1", combined.Body)
                .ReplacePar("expr2", expr.Body);
        }
        return (Expression<Func<Company, bool>>)combined;
    }
Peltast answered 16/10, 2016 at 20:5 Comment(0)
A
1

Assume you have two expression e1 and e2, you can try this:

var combineBody = Expression.AndAlso(e1.Body, Expression.Invoke(e2, e1.Parameters[0]));
var finalExpression = Expression.Lambda<Func<TestClass, bool>>(combineBody, e1.Parameters).Compile();
Amalamalbena answered 13/8, 2022 at 9:37 Comment(1)
If you leave out the ".Compile()" at the end it is the 100% best answer to that question: Slim and simple without external libs and most important: correct! As @Blakeley discovered without the Invoke() call and passing the e1.Parameters to the lambda call you get the InvalidOperationException. And if you pass the e1.Parameters but do not use Invoke() later code trying to use the new lambda has trouble as there is a parameter used that is totally unknown.Ihab

© 2022 - 2024 — McMap. All rights reserved.