使用包含多个参数的BinaryExpression创建谓词

时间:2017-08-18 21:37:07

标签: c# linq lambda expression

可以使用LambdaExpressions动态生成这样的谓词吗?

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

只要内部BinaryExpression中的参数相同或表达式的右边部分是常量,就没有问题。但示例表达式ld.LevelDate > DbFunctions.AddDays (t.TestDate, 1)包含两个不同的ExpressionParameters,它们彼此独立。我正在寻找的是这样的:

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; }
}

亲切的问候!

3 个答案:

答案 0 :(得分:0)

使用嵌套的lambda构建表达式时,内部lambda的表达式将能够访问外部lambda的参数。它与Expression&lt; T&gt;的工作方式相同。 lambdas和普通的C#lambdas一样。

如果您正在使用Expression&lt; T&gt; lambdas并试图将它们组合起来,你需要在API级别使用它们(手动完成),并且不要期望Expression&lt; T&gt;的自动C#语言语法。转换来帮助你。

有一点需要注意:当你创建两个原始lambda(通过转换为Expression&lt; T&gt;)时,它们每个都有自己的ParameterExpression实例,这将使它们无法组合它们,因为两个实体都需要引用它们相同的实例(除非您使用ExpressionVisitor替换另一个实例。)

答案 1 :(得分:0)

正如@Matt Warren在答案中指出的那样,如果你想要组合lambdas,你需要手动完成它,并且需要设置正确的表达式参数。

首先,您需要一个可以替换您想要的节点的ExpressionVisitor

    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);
    }

其次,您需要手动组合lambda:

    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;
    }

以下代码包含以下用法:

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

答案 2 :(得分:0)

我建议使用nein-linq来组合,构建和组合谓词(以及许多其他表达难题), 或LinqKit

两者都支持实体框架

例如,使用nein-linq

<强>假设:

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)));
    }       
}

:当

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)
    }
};

,然后

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();