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

时间:2013-04-26 16:00:05

标签: c# linq linq-to-entities linq-expressions

我需要对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比较,根本不返回实体,因为实体不能成为一个以上时间段的一部分,显然。

我需要在这里构建一些动态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);
}

声明了一个新的CombineWithOr Expression:

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,就像Expression完全没有效果一样。生成的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查询的一部分发送,因此在完成过滤过程之前它不会返回大量实体。

4 个答案:

答案 0 :(得分: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);
}

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

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

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

答案 1 :(得分:4)

您可以使用以下方法:

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

有关详细信息,您可能需要查看以下内容:

Combining two expressions (Expression<Func<T, bool>>)

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

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

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没有任何影响。

答案 2 :(得分:1)

这段代码怎么样:

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

我使用LINQ Any运算符来确定是否存在符合de.Date的速率周期。虽然我不太确定如何将它转换为实体的高效SQL语句。如果您可以发布生成的SQL,那对我来说非常有趣。

希望这有帮助。

在hattenn回答后更新:

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

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

Any运算符的解决方案何时起作用

在评论中,您告诉您遇到了System.NotSupportedException无法创建“RatePeriod”类型的常量值。在此上下文中仅支持原始类型或枚举类型。RatePeriod对象是内存中对象而实体框架ObjectContext或{{1}未跟踪时,就是这种情况。 }。我制作了一个小型测试解决方案,可以从这里下载:https://dl.dropboxusercontent.com/u/14810011/LinqToEntitiesOrOperator.zip

我将Visual Studio 2012与LocalDB和Entity Framework 5一起使用。要查看结果,请打开类DbContext,然后打开测试资源管理器,构建解决方案并运行所有测试。你会发现LinqToEntitiesOrOperatorTest会失败,所有其他人都会失败。

我使用的上下文如下:

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()); } 没有知道。

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

  1. 您的DatabaseContext个对象分别位于同一个RatePeriodObjectContext?然后就像我在上面提到的第一个单元测试中那样使用它们。

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

    IQueryable<T>
  3. 如果第二种解决方案不可行,那么我建议您编写一个TSQL存储过程,在该过程中传入速率周期并形成正确的SQL语句。该解决方案也是性能最佳的解决方案。

答案 3 :(得分:0)

无论如何,我认为动态LINQ查询创建并不像我想象的那么简单。尝试使用Entity 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);

这可能不完全正确(我还没有尝试过),但它应该与此类似。