Create predicate with a BinaryExpression containing multiple parameters
Asked Answered
D

3

0

is it possible to dynamically generate such a predicate using LambdaExpressions?

Expression<Func<Test, bool>> predicate = t =>
    t.Levels.Any(l =>
        l.LevelDetails.Any( ld =>
            ld.LevelDate > DbFunctions.AddDays(t.TestDate, 1)
        )
    );

As long as the parameters in the inner BinaryExpression are identical or the right part of the expression is constant, there is no problem. But the example expressionld.LevelDate > DbFunctions.AddDays (t.TestDate, 1) contains two different ExpressionParameters which are independent from each other. What I am looking for is something like this:

Expression<Func<LevelDetail, DateTime?>> left = 
    ld => ld.LevelDate;
Expression<Func<Test, DateTime?>> right = 
    t => DbFunctions.AddDays(t.TestDate, 1);

BinaryExpression expr = 
    Expression.GreaterThan(
        ((LambdaExpression)left).Body,
        ((LambdaExpression)right).Body
    );
Expression<Func<Test, bool>> predicate = t =>
    t.Levels.Any(l =>
        l.LevelDetails.Any( **expr** )
    );

class Test {
    public DateTime TestDate { get; set; }
    public virtual ICollection<Level> Levels { get; set; }
}
class Level {
    public virtual ICollection<LevelDetail> LevelDetails { get; set; }
}
class LevelDetail {
    public DateTime LevelDate { get; set; }
}

Kind regards!

Dextran answered 18/8, 2017 at 21:37 Comment(1)
You can't do that in an expression tree, but you can build an expression tree to do that. t.Levels.Any(...) is a MethodCallExpression on a MemberExpression on t... and so on. You can use LINQPad as an aid to get the structure of the expression tree without the expressions replaced, then write code to build the expression tree with replacements.Justifier
G
0

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);
Golding answered 18/8, 2017 at 23:14 Comment(0)
H
0

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();
Hachure answered 19/8, 2017 at 1:12 Comment(0)
O
-1

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.)

Oliy answered 18/8, 2017 at 21:49 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.