动态添加新的lambda表达式以创建过滤器

12 c# linq linq-to-entities linq-expressions

我需要对ObjectSet进行一些过滤,以通过这样做获取我需要的实体:

query = this.ObjectSet.Where(x => x.TypeId == 3); // this is just an example;
Run Code Online (Sandbox Code Playgroud)

稍后在代码中(以及在启动延迟执行之前)我再次过滤查询,如下所示:

query = query.Where(<another lambda here ...>);
Run Code Online (Sandbox Code Playgroud)

到目前为止,这很有效.

这是我的问题:

实体包含DateFrom属性和DateTo属性,它们都是DataTime类型.它们代表了一段时间.

我需要过滤的实体只拿到那些一个部分集合时间段.集合的句点不一定是连续的,因此,检索实体的逻辑看起来像这样:

entities.Where(x => x.DateFrom >= Period1.DateFrom and x.DateTo <= Period1.DateTo)
||
entities.Where(x => x.DateFrom >= Period2.DateFrom and x.DateTo <= Period2.DateTo)
||
Run Code Online (Sandbox Code Playgroud)

......以及集合中所有时期的不断变化.

我试过这样做:

foreach (var ratePeriod in ratePeriods)
{
    var period = ratePeriod;

    query = query.Where(de =>
        de.Date >= period.DateFrom && de.Date <= period.DateTo);
}
Run Code Online (Sandbox Code Playgroud)

但是一旦我启动了延迟执行,它就像我想要的那样将它转换为SQL(对于集合中的多个句点,每个时间段都有一个过滤器),但是,它转换为AND比较而不是OR比较,根本不返回任何实体,因为一个实体显然不能超过一段时间.

我需要在这里构建一些动态linq来聚合周期过滤器.


更新

基于hatten的回答,我添加了以下成员:

private Expression<Func<T, bool>> CombineWithOr<T>(Expression<Func<T, bool>> firstExpression, Expression<Func<T, bool>> secondExpression)
{
    // Create a parameter to use for both of the expression bodies.
    var parameter = Expression.Parameter(typeof(T), "x");
    // Invoke each expression with the new parameter, and combine the expression bodies with OR.
    var resultBody = Expression.Or(Expression.Invoke(firstExpression, parameter), Expression.Invoke(secondExpression, parameter));
    // Combine the parameter with the resulting expression body to create a new lambda expression.
    return Expression.Lambda<Func<T, bool>>(resultBody, parameter);
}
Run Code Online (Sandbox Code Playgroud)

声明了一个新的CombineWithOr表达式:

Expression<Func<DocumentEntry, bool>> resultExpression = n => false;
Run Code Online (Sandbox Code Playgroud)

并在我的期间集合迭代中使用它,如下所示:

foreach (var ratePeriod in ratePeriods)
{
    var period = ratePeriod;
    Expression<Func<DocumentEntry, bool>> expression = de => de.Date >= period.DateFrom && de.Date <= period.DateTo;
    resultExpression = this.CombineWithOr(resultExpression, expression);
}

var documentEntries = query.Where(resultExpression.Compile()).ToList();
Run Code Online (Sandbox Code Playgroud)

我查看了生成的SQL,就像Expression完全没有效果一样.生成的SQL返回先前编程的过滤器,但不返回组合过滤器.为什么?


更新2

我想尝试一下feO2x的建议,所以我重写了我的过滤器查询,如下所示:

query = query.AsEnumerable()
    .Where(de => ratePeriods
        .Any(rp => rp.DateFrom <= de.Date && rp.DateTo >= de.Date))
Run Code Online (Sandbox Code Playgroud)

正如你所看到的,我添加AsEnumerable()但是编译器给了我一个错误,它无法将IEnumerable转换回IQueryable,所以我ToQueryable()在查询结束时添加了:

query = query.AsEnumerable()
    .Where(de => ratePeriods
        .Any(rp => rp.DateFrom <= de.Date && rp.DateTo >= de.Date))
            .ToQueryable();
Run Code Online (Sandbox Code Playgroud)

一切正常.我可以编译代码并启动此查询.但是,它不符合我的需要.

在分析生成的SQL时,我可以看到过滤不是SQL查询的一部分,因为它在过程中过滤了内存中的日期.我想你已经知道了,这就是你打算建议的.

您的建议可行,但是,因为它在内存中过滤它们之前从数据库中获取所有实体(并且有成千上万的实体),从数据库中获取大量数据真的很慢.

我真正想要的是将周期过滤作为结果SQL查询的一部分发送,因此在完成过滤过程之前它不会返回大量实体.

小智 8

尽管提出了很好的建议,但我不得不选择LinqKit.其中一个原因是我将不得不在代码中的许多其他地方重复相同类型的谓词聚合.使用LinqKit是最简单的,更不用说我只需编写几行代码即可完成.

以下是我使用LinqKit解决问题的方法:

var predicate = PredicateBuilder.False<Document>();
foreach (var submittedPeriod in submittedPeriods)
{
    var period = period;
    predicate = predicate.Or(d =>
        d.Date >= period.DateFrom && d.Date <= period.DateTo);
}
Run Code Online (Sandbox Code Playgroud)

我启动延迟执行(注意我AsExpandable()之前调用):

var documents = this.ObjectSet.AsExpandable().Where(predicate).ToList();
Run Code Online (Sandbox Code Playgroud)

我查看了生成的SQL,它在将谓词转换为SQL方面做得很好.