动态添加新的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查询的一部分,这样在完成过滤过程之前它就不会返回大量的实体。
尽管有很好的建议,我还是选择了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
编辑:行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 someExpression
。false
没有任何作用
下面的代码如何:
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
对象是内存对象并且不被实体框架ObjectContext
或DbContext
跟踪时,就会出现这种情况。我做了一个小的测试解决方案,可以从这里下载: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
不知道的速率周期对象。
你能做些什么来解决这个问题?
您的
RatePeriod
对象分别驻留在同一个ObjectContext
或DbContext
中吗?然后使用它们,就像我在上面提到的第一个单元测试中所做的那样。如果没有,你能一次加载你所有的帖子吗?或者这会导致
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()); }
如果第二个解决方案不可能,那么我建议编写一个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);
这可能不是完全正确的(我没有尝试过),但它应该是类似于以下内容: