Sure you could.
The easy way
var tests = new List<Func<int, bool>>() {
(x) => x > 10,
(x) => x < 100,
(x) => x != 42
};
We 're going to aggregate all these predicates into one by incrementally logical-and-ing each one with the already existing result. Since we need to start somewhere, we 'll start with x => true
because that predicate is neutral when doing AND (start with x => false
if you OR):
var seed = (Func<int, bool>)(x => true);
var allTogether = tests.Aggregate(
seed,
(combined, expr) => (Func<int, bool>)(x => combined(x) && expr(x)));
Console.WriteLine(allTogether.Invoke(30)); // True
That was easy! It does have a few limitations though:
- it only works on objects (as does your example)
- it might be a tiny bit inefficient when your list of predicates gets large (all those function calls)
The hard way (using expression trees instead of compiled lambdas)
This will work everywhere (e.g. you can also use it to pass predicates to SQL providers such as Entity Framework) and it will also give a more "compact" final result in any case. But it's going to be much harder to make it work. Let's get to it.
First, change your input to be expression trees. This is trivial because the compiler does all the work for you:
var tests = new List<Expression<Func<int, bool>>>() {
(x) => x > 10,
(x) => x < 100,
(x) => x != 42
};
Then aggregate the bodies of these expressions into one, the same idea as before. Unfortunately, this is not trivial and it is not going to work all the way, but bear with me:
var seed = (Expression<Func<int, bool>>)
Expression.Lambda(Expression.Constant(true),
Expression.Parameter(typeof(int), "x"));
var allTogether = tests.Aggregate(
seed,
(combined, expr) => (Expression<Func<int, bool>>)
Expression.Lambda(
Expression.And(combined.Body, expr.Body),
expr.Parameters
));
Now what we did here was build one giant BinaryExpression
expression from all the individual predicates.
You can now pass the result to EF or tell the compiler to turn this into code for you and run it, and you get short-circuiting for free:
Console.WriteLine(allTogether.Compile().Invoke(30)); // should be "true"
Unfortunately, this final step won't work for esoteric technical reasons.
But why won't it work?
Because allTogether
represents an expression tree that goes somewhat like this:
FUNCTION
PARAMETERS: PARAM(x)
BODY: AND +-- NOT-EQUAL +---> PARAM(x)
| \---> CONSTANT(42)
|
AND +-- LESS-THAN +---> PARAM(x)
| \---> CONSTANT(100)
|
AND +-- GREATER-THAN +---> PARAM(x)
| \---> CONSTANT(10)
|
TRUE
Each node in the above tree represents an Expression
object in the expression tree to be compiled. The catch is that all 4 of those PARAM(x)
nodes, while logically identical are in fact different instances (that help the compiler gave us by creating expression trees automatically? well, each one naturally has their own parameter instance), while for the end result to work they must be the same instance. I know this because it has bitten me in the past.
So, what needs to be done here is that you then iterate over the resulting expression tree, find each occurrence of a ParameterExpression
and replace each an every one of them with the same instance. That same instance will also be the second argument used when constructing seed
.
Showing how to do this will make this answer way longer than it has any right to be, but let's do it anyway. I 'm not going to comment very much, you should recognize what's going on here:
class Visitor : ExpressionVisitor
{
private Expression param;
public Visitor(Expression param)
{
this.param = param;
}
protected override Expression VisitParameter(ParameterExpression node)
{
return param;
}
}
And then:
var param = Expression.Parameter(typeof(int), "x");
var seed = (Expression<Func<int, bool>>)
Expression.Lambda(Expression.Constant(true),
param);
var visitor = new Visitor(param);
var allTogether = tests.Aggregate(
seed,
(combined, expr) => (Expression<Func<int, bool>>)
Expression.Lambda(
Expression.And(combined.Body, expr.Body),
param
),
lambda => (Expression<Func<int, bool>>)
// replacing all ParameterExpressions with same instance happens here
Expression.Lambda(visitor.Visit(lambda.Body), param)
);
Console.WriteLine(allTogether.Compile().Invoke(30)); // "True" -- works!