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

12

我需要在一个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 查询的一部分的原因吗?我不明白将其转换为可枚举类型,然后再转换回可查询类型的原因。 - hattenn
这是我尝试让Linq-To-Entities提供程序理解查询的众多方法之一。不幸的是,省略AsEnumerable()调用会使查询在Linq-To-Entities上下文中无法使用。我会收到以下异常:LINQ表达式节点类型“Invoke”在LINQ to Entities中不受支持。 - user2324540
4个回答

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语句。


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

如果需要更多信息,您可以查看以下内容:

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

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

编辑: 代码行Expression<Func<DocumentEntry, bool>> resultExpression = n => false;只是一个占位符。 CombineWithOr方法需要两个方法进行组合,如果你写了Expression<Func<DocumentEntry, bool>> resultExpression,在第一次使用CombineWithOr循环时就不能使用它了。就像下面的代码:

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 someExpression。那个false没有任何影响。

一旦我解决了这个问题,我会尽快使用您建议的代码编辑我的问题:在Expression<Func<DocumentEntry, bool>> resultExpression = n => n == !n;行,编译器告诉我“无法将!运算符应用于DocumentEntry类型的操作数”。(DocumentEntry是我的实体类型)。 - user2324540
抱歉,n不是布尔类型,请将其替换为n => false。这是我犯的一个愚蠢错误。请查看我的编辑以了解详情。 - hattenn
谢谢。我已经更新了我的问题,并提出了一些更多的问题,附上了您的代码实现。 - user2324540
我已经进行了编辑。至于为什么您的查询结果与预期不符,我不知道。我看不出任何问题。尝试在行var documentEntries = query.Where(resultExpression.Compile()).ToList();处设置一个调试点,并检查resultExpression的内容。 - hattenn
谢谢您的解释。我现在很困惑:试图弄清楚为什么我无法在翻译后的SQL中看到这些组合过滤器... - user2324540

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。因此,Entity Framework依赖于IQueryable<T>接口而不是IEnumerable<T>。现在默认的LINQ操作符(如Where、Any、OrderBy、FirstOrDefault等)都在这两个接口上实现,因此区别有时很难看出来。这些接口的主要区别在于,在IEnumerable<T>扩展方法的情况下,返回的可枚举对象会不断更新而没有副作用,而在IQueryable<T>的情况下,实际表达式会被重新组合,这不是没有副作用的(即您正在更改最终用于创建SQL查询的表达式树)。
现在Entity Framework支持LINQ的约50个标准查询运算符,但是如果您编写自己的方法来操作IQueryable(例如hatenn的方法),这将导致一个表达式树,Entity Framework可能无法解析,因为它根本不知道新的扩展方法。这可能是您在组合过滤器后无法看到组合过滤器的原因(尽管我会期望出现异常)。
使用Any运算符的解决方案何时起作用:
在评论中,您说遇到了System.NotSupportedException: Unable to create a constant value of type 'RatePeriod'. Only primitive types or enumeration types are supported in this context.这种情况发生在RatePeriod对象是内存对象而不是由Entity Framework ObjectContext或DbContext跟踪的情况下。我制作了一个小型测试解决方案,可以从此处下载:https://dl.dropboxusercontent.com/u/14810011/LinqToEntitiesOrOperator.zip 我使用带有LocalDB和Entity Framework 5的Visual Studio 2012。要查看结果,请打开类,然后打开测试资源管理器,构建解决方案并运行所有测试。您将会注意到将失败,其他测试应该通过。
我使用的上下文如下:
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语句。该解决方案也是最高效的。


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

0

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

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


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接