Create predicate with a BinaryExpression containing multiple parameters

前端 未结 3 588
旧巷少年郎
旧巷少年郎 2021-01-27 10:52

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

Expression> predicate = t =>
    t         


        
相关标签:
3条回答
  • 2021-01-27 11:04

    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);
    
    0 讨论(0)
  • 2021-01-27 11:11

    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();
    
    0 讨论(0)
  • 2021-01-27 11:15

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

    0 讨论(0)
提交回复
热议问题