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

本文关键字:创建 过滤器 表达式 lambda 添加 动态 | 更新日期: 2023-09-27 18:10:58

我需要在ObjectSet上做一些过滤,以通过这样做获得我需要的实体:

query = this.ObjectSet.Where(x => x.TypeId == 3); // this is just an example;

在代码后面(以及在启动延迟执行之前),我再次像这样过滤查询:

query = query.Where(<another lambda here ...>);

到目前为止效果还不错。

我的问题是:

实体包含一个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)
||

我已经试过了:

foreach (var ratePeriod in ratePeriods)
{
    var period = ratePeriod;
    query = query.Where(de =>
        de.Date >= period.DateFrom && de.Date <= period.DateTo);
}

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

我需要在这里建立某种动态链接来聚合周期过滤器。


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

声明一个新的CombineWithOr表达式:

Expression<Func<DocumentEntry, bool>> resultExpression = n => false;

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

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

我查看了结果SQL,就像表达式根本没有效果一样。生成的SQL返回先前编程的过滤器,但不返回组合过滤器。为什么?


更新2

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

query = query.AsEnumerable()
    .Where(de => ratePeriods
        .Any(rp => rp.DateFrom <= de.Date && rp.DateTo >= de.Date))

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

query = query.AsEnumerable()
    .Where(de => ratePeriods
        .Any(rp => rp.DateFrom <= de.Date && rp.DateTo >= de.Date))
            .ToQueryable();

一切正常。我可以编译代码并启动这个查询。但是,它不适合我的需要。

在分析生成的SQL时,我可以看到过滤不是SQL查询的一部分,因为它在过程中过滤内存中的日期。我猜你已经知道那件事了,这正是你想要建议的。

你的建议是有效的,但是,因为它从数据库中获取所有的实体(并且有成千上万的实体),然后在内存中过滤它们,所以从数据库中获取大量的数据真的很慢。

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

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

尽管有很好的建议,我还是选择了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);
}

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

var documents = this.ObjectSet.AsExpandable().Where(predicate).ToList();

我查看了结果SQL,它在将谓词转换为SQL方面做得很好。

您可以使用如下方法:

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

然后:

Expression<Func<T, bool>> resultExpression = n => false; // Always false, so that it won't affect the OR.
foreach (var ratePeriod in ratePeriods)
{
    var period = ratePeriod;
    Expression<Func<T, bool>> expression = (de => de.Date >= period.DateFrom && de.Date <= period.DateTo);
    resultExpression = CombineWithOr(resultExpression, expression);
}
// Don't forget to compile the expression in the end.
query = query.Where(resultExpression.Compile());

有关更多信息,您可以查看以下内容:

组合两个表达式(Expression<T,>>)

http://www.albahari.com/nutshell/predicatebuilder.aspx

编辑:Expression<Func<DocumentEntry, bool>> resultExpression = n => false;只是一个占位符。CombineWithOr方法需要两个方法来组合,如果你写Expression<Func<DocumentEntry, bool>> resultExpression;', you can't use it in the call to CombineWithOr for the first time in your foreach '循环。就像下面的代码:

int resultOfMultiplications = 1;
for (int i = 0; i < 10; i++)
    resultOfMultiplications = resultOfMultiplications * i;

如果resultOfMultiplications中没有开头,则不能在循环中使用它。

关于为什么lambda是n => false。因为它在OR语句中没有任何影响。例如:false OR someExpression OR someExpression = someExpression OR someExpressionfalse没有任何作用

下面的代码如何:

var targets = query.Where(de => 
    ratePeriods.Any(period => 
        de.Date >= period.DateFrom && de.Date <= period.DateTo));

我使用LINQ Any运算符来确定是否有任何符合de.Date的速率周期。虽然我不太确定这是如何转化为有效的SQL语句的实体。如果你能发布结果SQL,那对我来说将是相当有趣的。

希望对你有帮助。

我不认为哈登的解决方案会起作用,因为实体框架使用LINQ表达式来产生对数据库执行的SQL或DML。因此,实体框架依赖于IQueryable<T>接口,而不是IEnumerable<T>。现在,默认的LINQ操作符(如Where、Any、OrderBy、FirstOrDefault等)在两个接口上都实现了,因此有时很难看出区别。这些接口的主要区别在于,在IEnumerable<T>扩展方法的情况下,返回的枚举值不断更新而没有副作用,而在IQueryable<T>的情况下,实际的表达式被重新组合,这不是没有副作用(即您正在更改最终用于创建SQL查询的表达式树)。

现在实体框架支持LINQ的大约50个标准查询操作符,但是如果您编写自己的方法来操作IQueryable<T>(如hatenn的方法),这将导致实体框架可能无法解析表达式树,因为它根本不知道新的扩展方法。这可能就是为什么在组合过滤器后看不到它们的原因(尽管我希望出现异常)。

Any运算符的解何时起作用:

在评论中,您告诉您遇到了System.NotSupportedException: 无法创建类型为'RatePeriod'的常量值。在此上下文中只支持基本类型或枚举类型。RatePeriod对象是内存对象并且不被实体框架ObjectContextDbContext跟踪时,就会出现这种情况。我做了一个小的测试解决方案,可以从这里下载:https://dl.dropboxusercontent.com/u/14810011/LinqToEntitiesOrOperator.zip

我使用Visual Studio 2012与LocalDB和实体框架5。要查看结果,请打开LinqToEntitiesOrOperatorTest类,然后打开Test Explorer,构建解决方案并运行所有测试。你会发现ComplexOrOperatorTestWithInMemoryObjects会失败,其他的都应该通过。

我使用的上下文看起来像这样:

public class DatabaseContext : DbContext
{
    public DbSet<Post> Posts { get; set; }
    public DbSet<RatePeriod> RatePeriods { get; set; }
}
public class Post
{
    public int ID { get; set; }
    public DateTime PostDate { get; set; }
}
public class RatePeriod
{
    public int ID { get; set; }
    public DateTime From { get; set; }
    public DateTime To { get; set; }
}

好吧,这很简单:-)。在测试项目中,有两个重要的单元测试方法:

    [TestMethod]
    public void ComplexOrOperatorDBTest()
    {
        var allAffectedPosts =
            DatabaseContext.Posts.Where(
                post =>
                DatabaseContext.RatePeriods.Any(period => period.From < post.PostDate && period.To > post.PostDate));
        Assert.AreEqual(3, allAffectedPosts.Count());
    }
    [TestMethod]
    public void ComplexOrOperatorTestWithInMemoryObjects()
    {
        var inMemoryRatePeriods = new List<RatePeriod>
            {
                new RatePeriod {ID = 1000, From = new DateTime(2002, 01, 01), To = new DateTime(2006, 01, 01)},
                new RatePeriod {ID = 1001, From = new DateTime(1963, 01, 01), To = new DateTime(1967, 01, 01)}
            };
        var allAffectedPosts =
            DatabaseContext.Posts.Where(
                post => inMemoryRatePeriods.Any(period => period.From < post.PostDate && period.To > post.PostDate));
        Assert.AreEqual(3, allAffectedPosts.Count());
    }

注意,第一个方法通过了,而第二个方法由于上面提到的异常而失败,尽管这两个方法做的事情完全相同,只是在第二个情况下,我在内存中创建了DatabaseContext不知道的速率周期对象。

你能做些什么来解决这个问题?

  1. 您的RatePeriod对象分别驻留在同一个ObjectContextDbContext中吗?然后使用它们,就像我在上面提到的第一个单元测试中所做的那样。

  2. 如果没有,你能一次加载你所有的帖子吗?或者这会导致OutOfMemoryException ?如果没有,可以使用下面的代码。注意,AsEnumerable()调用导致Where运算符被用于IEnumerable<T>接口而不是IQueryable<T>。实际上,这将导致所有帖子被加载到内存中,然后进行过滤:

    [TestMethod]
    public void CorrectComplexOrOperatorTestWithInMemoryObjects()
    {
        var inMemoryRatePeriods = new List<RatePeriod>
            {
                new RatePeriod {ID = 1000, From = new DateTime(2002, 01, 01), To = new DateTime(2006, 01, 01)},
                new RatePeriod {ID = 1001, From = new DateTime(1963, 01, 01), To = new DateTime(1967, 01, 01)}
            };
        var allAffectedPosts =
            DatabaseContext.Posts.AsEnumerable()
                           .Where(
                               post =>
                               inMemoryRatePeriods.Any(
                                   period => period.From < post.PostDate && period.To > post.PostDate));
        Assert.AreEqual(3, allAffectedPosts.Count());
    }
    
  3. 如果第二个解决方案不可能,那么我建议编写一个TSQL存储过程,在其中传递您的速率周期,并形成正确的SQL语句。

无论如何,我认为动态LINQ查询的创建并不像我想象的那么简单。尝试使用实体SQL,类似于下面的方法:

var filters = new List<string>();
foreach (var ratePeriod in ratePeriods)
{
    filters.Add(string.Format("(it.Date >= {0} AND it.Date <= {1})", ratePeriod.DateFrom, ratePeriod.DateTo));
}
var filter = string.Join(" OR ", filters);
var result = query.Where(filter);

这可能不是完全正确的(我没有尝试过),但它应该是类似于以下内容: