is it possible to dynamically generate such a predicate using LambdaExpressions
?
Expression> predicate = t =>
t
As pointed out in the answers by @Matt Warren if yow want to combine lambdas you will need to do it by hand and will need to set the correct expression parameters.
Firstlly, you will need a ExpressionVisitor
that can replace node that you want:
private class SwapVisitor : ExpressionVisitor
{
public readonly Expression _from;
public readonly Expression _to;
public SwapVisitor(Expression from, Expression to)
{
_from = from;
_to = to;
}
public override Expression Visit(Expression node) => node == _from ? _to : base.Visit(node);
}
Secondly, you will need to combine lambdas by hand:
private static Expression<Func<Test, bool>> CreatePredicate()
{
Expression<Func<LevelDetail, DateTime?>> left = ld => ld.LevelDate;
// I didn't include EF, so I did test it just use directly Test.TestDate
//Expression<Func<Test, DateTime?>> right = t => t.TestDate;
Expression<Func<Test, DateTime?>> right = t => DbFunctions.AddDays(t.TestDate, 1);
var testParam = Expression.Parameter(typeof(Test), "test_par");
var levelParam = Expression.Parameter(typeof(Level), "level_par");
var detailParam = Expression.Parameter(typeof(LevelDetail), "detail_par");
// Swap parameters for right and left operands to the correct parameters
var swapRight = new SwapVisitor(right.Parameters[0], testParam);
right = swapRight.Visit(right) as Expression<Func<Test, DateTime?>>;
var swapLeft = new SwapVisitor(left.Parameters[0], detailParam);
left = swapLeft.Visit(left) as Expression<Func<LevelDetail, DateTime?>>;
BinaryExpression comparer = Expression.GreaterThan(left.Body, right.Body);
var lambdaComparer = Expression.Lambda<Func<LevelDetail, bool>>(comparer, detailParam);
// Well, we created here the lambda for ld => ld.LevelDate > DbFunctions.AddDays(t.TestDate, 1)
var anyInfo = typeof(Enumerable).GetMethods().Where(info => info.Name == "Any" && info.GetParameters().Length == 2).Single();
// Will create **l.LevelDetails.Any(...)** in the code below
var anyInfoDetail = anyInfo.MakeGenericMethod(typeof(LevelDetail));
var anyDetailExp = Expression.Call(anyInfoDetail, Expression.Property(levelParam, "LevelDetails"), lambdaComparer);
var lambdaAnyDetail = Expression.Lambda<Func<Level, bool>>(anyDetailExp, levelParam);
// Will create **t.Levels.Any(...)** in the code below and will return the finished lambda
var anyInfoLevel = anyInfo.MakeGenericMethod(typeof(Level));
var anyLevelExp = Expression.Call(anyInfoLevel, Expression.Property(testParam, "Levels"), lambdaAnyDetail);
var lambdaAnyLevel = Expression.Lambda<Func<Test, bool>>(anyLevelExp, testParam);
return lambdaAnyLevel;
}
And the code below contains usage of this:
var predicate = CreatePredicate();
var levelDetail = new LevelDetail { LevelDate = new DateTime(2017, 08, 19) };
var level = new Level { LevelDetails = new List<LevelDetail> { levelDetail } };
var test = new Test { TestDate = new DateTime(2027, 08, 19), Levels = new List<Level> { level } };
var result = predicate.Compile()(test);
I would recommend using nein-linq to combine, build and compose predicates (and many other expression puzzles), or LinqKit
Both support Entity Framework
For example, using nein-linq
Given:
public static class TestExpressions
{
[InjectLambda]
public static bool IsTestDateEarlierThan(this Test test, DateTime? dateTime, int numberOfDays)
{
return dateTime > test.TestDate.AddDays(numberOfDays);
}
public static Expression<Func<Test, DateTime?, int, bool>> IsTestDateEarlierThan()
{
return (test, dateTime, numberOfDays) => dateTime > DbFunctions.AddDays(test.TestDate, numberOfDays);
}
// Simple caching...
private static readonly Func<Test, int, bool> _hasAnyLevelDateAfterTestDays = HasAnyLevelDateAfterTestDays().Compile();
[InjectLambda]
public static bool HasAnyLevelDateAfterTestDays(this Test test, int numberOfDays)
{
return _hasAnyLevelDateAfterTestDays(test, numberOfDays);
}
public static Expression<Func<Test, int, bool>> HasAnyLevelDateAfterTestDays()
{
return (test, numberOfDays) => test.Levels.Any(l => l.LevelDetails.Any(ld => test.IsTestDateEarlierThan(ld.LevelDate, numberOfDays)));
}
}
When:
var testList = new List<Test>
{
new Test {
Levels = new List<Level> {
new Level {
LevelDetails = new List<LevelDetail> {
new LevelDetail {
LevelDate = DateTime.Today
}
}
}
},
// Not matched
TestDate = DateTime.Today
},
new Test {
Levels = new List<Level> {
new Level {
LevelDetails = new List<LevelDetail> {
new LevelDetail {
LevelDate = DateTime.Today
}
}
}
},
// Not matched
TestDate = DateTime.Today.AddDays(-1)
},
new Test {
Levels = new List<Level> {
new Level {
LevelDetails = new List<LevelDetail> {
new LevelDetail {
LevelDate = DateTime.Today
}
}
}
},
// Matched
TestDate = DateTime.Today.AddDays(-2)
}
};
Then:
var testQuery = testList.AsQueryable();
// Alternative one
var result1 = testQuery
.ToInjectable() // Don't forget!!
.Where(test => test.Levels.Any(l => l.LevelDetails.Any(ld => test.IsTestDateEarlierThan(ld.LevelDate, 1))))
.ToList();
// Alternative two: You get the point :)
var result2 = testQuery
.ToInjectable() // Don't forget!!
.Where(test => test.HasAnyLevelDateAfterTestDays(1))
.ToList();
When you build an expression with nested lambda's the inner lambda's expressions will be able to access the outer lambda's parameters. It works the same way with Expression<T> lambdas as with regular C# lambdas.
If you are working with Expression<T> lambdas and trying to combine them, you'll need to work with them at the API level (do it by hand), and not expect the automatic C# language syntax to Expression<T> conversion to help you out.
One thing to note: when you created the two original lambdas (via conversion to Expression<T>), they each got their own ParameterExpression instances, which will make it impossible to combine them because both bodies will need to be referencing the same instance (unless you replace one for the other using an ExpressionVisitor.)