【问题标题】:Dynamically add new lambda expressions to create a filter动态添加新的 lambda 表达式以创建过滤器
【发布时间】:2013-04-26 16:00:05
【问题描述】:

我需要对 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<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 查询的一部分,这样它就不会在完成过滤过程之前返回大量实体。

【问题讨论】:

  • 你为什么要query = query.AsEnumerable()?这就是为什么您的查询不是您的 SQL 查询的一部分?我不明白将其转换为可枚举然后返回可查询的原因。
  • 这是我尝试让 Linq-To-Entities 提供程序理解查询的众多方法之一。不幸的是,省略 AsEnumerable() 调用会使查询在 Linq-To-Entities 上下文中不可用。我得到这个执行:LINQ to Entities 不支持 LINQ 表达式节点类型“Invoke”。

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


【解决方案1】:

尽管有很好的建议,但我还是选择了 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 方面做得很好。

【讨论】:

    【解决方案2】:

    您可以使用如下方法:

    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&lt;Func&lt;DocumentEntry, bool&gt;&gt; resultExpression = n =&gt; false; 行只是一个占位符。 CombineWithOr方法需要两个方法组合,如果你写Expression&lt;Func&lt;DocumentEntry, bool&gt;&gt; resultExpression;', you can't use it in the call toCombineWithOrfor the first time in yourforeach`循环。就像下面的代码:

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

    如果resultOfMultiplications 中没有任何内容,则不能在循环中使用它。

    至于为什么 lambda 是 n =&gt; false。因为它在OR 语句中没有任何作用。例如,false OR someExpression OR someExpression 等于 someExpression OR someExpression。那个false 没有任何作用。

    【讨论】:

    • 一旦我能解决这个问题,我将使用您建议的代码编辑我的问题:在Expression&lt;Func&lt;DocumentEntry, bool&gt;&gt; resultExpression = n =&gt; n == !n; 行,编译器告诉我无法应用!操作符类型为 DocumentEntry 的操作数。 (DocumentEntry 是我的实体类型)。
    • 对不起,n 不是布尔值,只需将其替换为 n =&gt; false。这是我的一个愚蠢的错误。查看我的编辑以查看。
    • 谢谢。我已经用我的代码实现更新了我的问题,并提出了更多问题。
    • 我进行了编辑。至于为什么您的查询不能按预期工作,我不知道。我看不出有什么问题。尝试在var documentEntries = query.Where(resultExpression.Compile()).ToList(); 行放置一个调试点并查看resultExpression 的内容。
    • 感谢您的解释。我现在正在苦苦挣扎:试图弄清楚为什么我在翻译后的 SQL 中看不到这些组合过滤器......
    【解决方案3】:

    这段代码怎么样:

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

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

    希望这会有所帮助。

    哈滕回答后更新:

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

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

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

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

    我将 Visual Studio 2012 与 LocalDB 和 Entity Framework 5 结合使用。要查看结果,请打开类 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());
        }
    

    请注意,第一个方法通过,而第二个方法失败,除了上面提到的异常,虽然这两种方法做的事情完全相同,除了在第二种情况下,我在内存中创建了利率周期对象 DatabaseContext 不知道.

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

    1. 您的RatePeriod 对象是否分别位于相同的ObjectContextDbContext 中?然后像我在上面提到的第一个单元测试中那样直接使用它们。

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

      [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 语句。此解决方案也是性能最高的解决方案。

    【讨论】:

    • 这确实是个好主意!不幸的是,它给了我这个错误:无法创建“RatePeriod”类型的常量值。此上下文仅支持原始类型或枚举类型。这是为什么 ?在调用 Any 方法之前,我是否必须从 RatePeriod 对象投影两种 DateTime 类型?我怎样才能做到这一点?
    • 哇,非常感谢您抽出宝贵的时间。这个周末我会花一些时间检查一下,我会回复你的。非常感谢。
    • @feO2x,你说的老实说根本没有任何意义。我提供的方法与IQueryable&lt;T&gt;IEnumerable&lt;T&gt; 没有任何关系。它只是创建一个 LINQ 查询,您可以像使用任何其他查询一样使用它。这与使用 a =&gt; a 或任何其他查询提供 Where 方法没有什么不同。
    • 大家好,我在原始问题中写了更新#2。
    • @hattenn:LINQ 表达式可以通过System.Linq.Expressions.Expression&lt;T&gt; 类与普通 LINQ 查询区分开来。在这种情况下,Entity Framework 使用这个表达式树来创建 SQL,这无法使用普通的 LINQ 查询来完成(EF 必须解析 IL 代码,而不是更简单的 Expression&lt;T&gt; 结构)。我提到了IEnumerable&lt;T&gt;IQueryable&lt;T&gt;,因为这些接口上的 LINQ 运算符的主要区别在于,第一个主要使用延迟 IL 执行,而后一个使用无法直接执行的表达式树。
    【解决方案4】:

    无论如何,我认为动态 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);
    

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-10-18
      • 1970-01-01
      相关资源
      最近更新 更多