如何在 LINQ to Entities 中重用字段过滤器
How to reuse a field filter in LINQ to Entities
我正在使用 EF 并有一个数据库 table,其中包含许多日期时间字段,这些字段会在对记录执行各种操作时填充。
我目前正在构建一个报告系统,该系统涉及按这些日期进行过滤,但是因为过滤器(这个日期在一个范围内,等等)在每个字段上都有相同的行为,所以我想重用我的过滤逻辑,所以我只编写一个日期过滤器并在每个字段上使用它。
我的初始过滤代码如下所示:
DateTime? dateOneIsBefore = null;
DateTime? dateOneIsAfter = null;
DateTime? dateTwoIsBefore = null;
DateTime? dateTwoIsAfter = null;
using (var context = new ReusableDataEntities())
{
IEnumerable<TestItemTable> result = context.TestItemTables
.Where(record => ((!dateOneIsAfter.HasValue
|| record.DateFieldOne > dateOneIsAfter.Value)
&& (!dateOneIsBefore.HasValue
|| record.DateFieldOne < dateOneIsBefore.Value)))
.Where(record => ((!dateTwoIsAfter.HasValue
|| record.DateFieldTwo > dateTwoIsAfter.Value)
&& (!dateTwoIsBefore.HasValue
|| record.DateFieldTwo < dateTwoIsBefore.Value)))
.ToList();
return result;
}
这很好用,但我更愿意减少 'Where' 方法中的重复代码,因为每个日期字段的筛选算法都相同。
我更喜欢看起来像下面这样的东西(稍后我将为过滤器值创建一个 class 或结构),我可以在其中使用扩展方法封装匹配算法:
DateTime? dateOneIsBefore = null;
DateTime? dateOneIsAfter = null;
DateTime? dateTwoIsBefore = null;
DateTime? dateTwoIsAfter = null;
using (var context = new ReusableDataEntities())
{
IEnumerable<TestItemTable> result = context.TestItemTables
.WhereFilter(record => record.DateFieldOne, dateOneIsBefore, dateOneIsAfter)
.WhereFilter(record => record.DateFieldTwo, dateTwoIsBefore, dateTwoIsAfter)
.ToList();
return result;
}
扩展方法可能类似于:
internal static IQueryable<TestItemTable> WhereFilter(this IQueryable<TestItemTable> source, Func<TestItemTable, DateTime> fieldData, DateTime? dateIsBefore, DateTime? dateIsAfter)
{
source = source.Where(record => ((!dateIsAfter.HasValue
|| fieldData.Invoke(record) > dateIsAfter.Value)
&& (!dateIsBefore.HasValue
|| fieldData.Invoke(record) < dateIsBefore.Value)));
return source;
}
使用上面的代码,如果我的过滤代码如下:
IEnumerable<TestItemTable> result = context.TestItemTables
.WhereFilter(record => record.DateFieldOne, dateOneIsBefore, dateOneIsAfter)
.WhereFilter(record => record.DateFieldTwo, dateTwoIsBefore, dateTwoIsAfter)
.ToList();
我得到以下异常:
A first chance exception of type 'System.NotSupportedException' occurred in EntityFramework.SqlServer.dll
Additional information: LINQ to Entities does not recognize the method 'System.DateTime Invoke(RAC.Scratch.ReusableDataFilter.FrontEnd.TestItemTable)' method, and this method cannot be translated into a store expression.
我遇到的问题是使用 Invoke 获取被查询的特定字段,因为这种技术不能很好地解决 SQL,因为如果我将过滤代码修改为以下内容,它将 运行 没有错误:
IEnumerable<TestItemTable> result = context.TestItemTables
.ToList()
.AsQueryable()
.WhereFilter(record => record.DateFieldOne, dateOneIsBefore, dateOneIsAfter)
.WhereFilter(record => record.DateFieldTwo, dateTwoIsBefore, dateTwoIsAfter)
.ToList();
这个问题是代码(在使用扩展方法过滤之前在整个 table 上使用 ToList)拉入整个数据库并将其作为对象查询而不是查询底层数据库,因此它是不可扩展。
我也研究过使用 Linqkit 中的 PredicateBuilder,但它找不到不使用 Invoke 方法编写代码的方法。
我知道有一些技术可以将部分查询表达为包含字段名称的字符串,但我更愿意使用一种类型更安全的方式来编写此代码。
此外,在理想情况下,我可以重新设计数据库,使多个 'date' 记录与单个 'item' 记录相关,但我不能随意以这种方式更改数据库模式.
我是否需要另一种方法来编写扩展,使其不使用 Invoke,或者我是否应该以不同的方式处理我的过滤代码的重用?
是的,LinqKit 是去这里的路。但是,您在扩展方法中遗漏了一些部分:
internal static IQueryable<TestItemTable> WhereFilter(this IQueryable<TestItemTable> source, Expression<Func<TestItemTable, DateTime>> fieldData, DateTime? dateIsBefore, DateTime? dateIsAfter)
{
source = source.AsExpandable().Where(record => ((!dateIsAfter.HasValue
|| fieldData.Invoke(record) > dateIsAfter.Value)
&& (!dateIsBefore.HasValue
|| fieldData.Invoke(record) < dateIsBefore.Value)));
return source;
}
我将第二个参数更改为 Expression<Func<TestItemTable, DateTime>>
并将缺少的调用添加到 LinqKit 的 AsExpandable()
方法。这样,Invoke()
将调用 LinqKit 的 Invoke()
,然后它就可以发挥它的魔力了。
用法:
IEnumerable<TestItemTable> result = context.TestItemTables
.WhereFilter(record => record.DateFieldOne, dateOneIsBefore, dateOneIsAfter)
.WhereFilter(record => record.DateFieldTwo, dateTwoIsBefore, dateTwoIsAfter)
.ToList();
在你的例子中没有太多重复,但我仍然会展示你如何用原始表达式做你想做的事(作为例子):
internal static class QueryableExtensions {
internal static IQueryable<T> WhereFilter<T>(this IQueryable<T> source, Expression<Func<T, DateTime>> fieldData, DateTime? dateIsBefore, DateTime? dateIsAfter) {
if (dateIsAfter == null && dateIsBefore == null)
return source;
// this represents you "record" parameter in lambda
var arg = Expression.Parameter(typeof(T), "record");
// this is the name of your field ("DateFieldOne")
var dateFieldName = ((MemberExpression)fieldData.Body).Member.Name;
// this is expression "c.DateFieldOne"
var dateProperty = Expression.Property(arg, typeof(T), dateFieldName);
Expression left = null;
Expression right = null;
// this is "c.DateFieldOne > dateIsAfter"
if (dateIsAfter != null)
left = Expression.GreaterThan(dateProperty, Expression.Constant(dateIsAfter.Value, typeof(DateTime)));
// this is "c.DateFieldOne < dateIsBefore"
if (dateIsBefore != null)
right = Expression.LessThan(dateProperty, Expression.Constant(dateIsBefore.Value, typeof(DateTime)));
// now we either combine with AND or not, depending on values
Expression<Func<T, bool>> combined;
if (left != null && right != null)
combined = Expression.Lambda<Func<T, bool>>(Expression.And(left, right), arg);
else if (left != null)
combined = Expression.Lambda<Func<T, bool>>(left, arg);
else
combined = Expression.Lambda<Func<T, bool>>(right, arg);
// applying that to where and done.
source = source.Where(combined);
return source;
}
}
电话如您所料:
WhereFilter(record => record.DateFieldOne, dateOneIsBefore, dateOneIsAfter)
起初使用表达式可能看起来很奇怪,但至少对于简单的情况来说没有什么太复杂的。
我正在使用 EF 并有一个数据库 table,其中包含许多日期时间字段,这些字段会在对记录执行各种操作时填充。 我目前正在构建一个报告系统,该系统涉及按这些日期进行过滤,但是因为过滤器(这个日期在一个范围内,等等)在每个字段上都有相同的行为,所以我想重用我的过滤逻辑,所以我只编写一个日期过滤器并在每个字段上使用它。
我的初始过滤代码如下所示:
DateTime? dateOneIsBefore = null;
DateTime? dateOneIsAfter = null;
DateTime? dateTwoIsBefore = null;
DateTime? dateTwoIsAfter = null;
using (var context = new ReusableDataEntities())
{
IEnumerable<TestItemTable> result = context.TestItemTables
.Where(record => ((!dateOneIsAfter.HasValue
|| record.DateFieldOne > dateOneIsAfter.Value)
&& (!dateOneIsBefore.HasValue
|| record.DateFieldOne < dateOneIsBefore.Value)))
.Where(record => ((!dateTwoIsAfter.HasValue
|| record.DateFieldTwo > dateTwoIsAfter.Value)
&& (!dateTwoIsBefore.HasValue
|| record.DateFieldTwo < dateTwoIsBefore.Value)))
.ToList();
return result;
}
这很好用,但我更愿意减少 'Where' 方法中的重复代码,因为每个日期字段的筛选算法都相同。
我更喜欢看起来像下面这样的东西(稍后我将为过滤器值创建一个 class 或结构),我可以在其中使用扩展方法封装匹配算法:
DateTime? dateOneIsBefore = null;
DateTime? dateOneIsAfter = null;
DateTime? dateTwoIsBefore = null;
DateTime? dateTwoIsAfter = null;
using (var context = new ReusableDataEntities())
{
IEnumerable<TestItemTable> result = context.TestItemTables
.WhereFilter(record => record.DateFieldOne, dateOneIsBefore, dateOneIsAfter)
.WhereFilter(record => record.DateFieldTwo, dateTwoIsBefore, dateTwoIsAfter)
.ToList();
return result;
}
扩展方法可能类似于:
internal static IQueryable<TestItemTable> WhereFilter(this IQueryable<TestItemTable> source, Func<TestItemTable, DateTime> fieldData, DateTime? dateIsBefore, DateTime? dateIsAfter)
{
source = source.Where(record => ((!dateIsAfter.HasValue
|| fieldData.Invoke(record) > dateIsAfter.Value)
&& (!dateIsBefore.HasValue
|| fieldData.Invoke(record) < dateIsBefore.Value)));
return source;
}
使用上面的代码,如果我的过滤代码如下:
IEnumerable<TestItemTable> result = context.TestItemTables
.WhereFilter(record => record.DateFieldOne, dateOneIsBefore, dateOneIsAfter)
.WhereFilter(record => record.DateFieldTwo, dateTwoIsBefore, dateTwoIsAfter)
.ToList();
我得到以下异常:
A first chance exception of type 'System.NotSupportedException' occurred in EntityFramework.SqlServer.dll
Additional information: LINQ to Entities does not recognize the method 'System.DateTime Invoke(RAC.Scratch.ReusableDataFilter.FrontEnd.TestItemTable)' method, and this method cannot be translated into a store expression.
我遇到的问题是使用 Invoke 获取被查询的特定字段,因为这种技术不能很好地解决 SQL,因为如果我将过滤代码修改为以下内容,它将 运行 没有错误:
IEnumerable<TestItemTable> result = context.TestItemTables
.ToList()
.AsQueryable()
.WhereFilter(record => record.DateFieldOne, dateOneIsBefore, dateOneIsAfter)
.WhereFilter(record => record.DateFieldTwo, dateTwoIsBefore, dateTwoIsAfter)
.ToList();
这个问题是代码(在使用扩展方法过滤之前在整个 table 上使用 ToList)拉入整个数据库并将其作为对象查询而不是查询底层数据库,因此它是不可扩展。
我也研究过使用 Linqkit 中的 PredicateBuilder,但它找不到不使用 Invoke 方法编写代码的方法。
我知道有一些技术可以将部分查询表达为包含字段名称的字符串,但我更愿意使用一种类型更安全的方式来编写此代码。
此外,在理想情况下,我可以重新设计数据库,使多个 'date' 记录与单个 'item' 记录相关,但我不能随意以这种方式更改数据库模式.
我是否需要另一种方法来编写扩展,使其不使用 Invoke,或者我是否应该以不同的方式处理我的过滤代码的重用?
是的,LinqKit 是去这里的路。但是,您在扩展方法中遗漏了一些部分:
internal static IQueryable<TestItemTable> WhereFilter(this IQueryable<TestItemTable> source, Expression<Func<TestItemTable, DateTime>> fieldData, DateTime? dateIsBefore, DateTime? dateIsAfter)
{
source = source.AsExpandable().Where(record => ((!dateIsAfter.HasValue
|| fieldData.Invoke(record) > dateIsAfter.Value)
&& (!dateIsBefore.HasValue
|| fieldData.Invoke(record) < dateIsBefore.Value)));
return source;
}
我将第二个参数更改为 Expression<Func<TestItemTable, DateTime>>
并将缺少的调用添加到 LinqKit 的 AsExpandable()
方法。这样,Invoke()
将调用 LinqKit 的 Invoke()
,然后它就可以发挥它的魔力了。
用法:
IEnumerable<TestItemTable> result = context.TestItemTables
.WhereFilter(record => record.DateFieldOne, dateOneIsBefore, dateOneIsAfter)
.WhereFilter(record => record.DateFieldTwo, dateTwoIsBefore, dateTwoIsAfter)
.ToList();
在你的例子中没有太多重复,但我仍然会展示你如何用原始表达式做你想做的事(作为例子):
internal static class QueryableExtensions {
internal static IQueryable<T> WhereFilter<T>(this IQueryable<T> source, Expression<Func<T, DateTime>> fieldData, DateTime? dateIsBefore, DateTime? dateIsAfter) {
if (dateIsAfter == null && dateIsBefore == null)
return source;
// this represents you "record" parameter in lambda
var arg = Expression.Parameter(typeof(T), "record");
// this is the name of your field ("DateFieldOne")
var dateFieldName = ((MemberExpression)fieldData.Body).Member.Name;
// this is expression "c.DateFieldOne"
var dateProperty = Expression.Property(arg, typeof(T), dateFieldName);
Expression left = null;
Expression right = null;
// this is "c.DateFieldOne > dateIsAfter"
if (dateIsAfter != null)
left = Expression.GreaterThan(dateProperty, Expression.Constant(dateIsAfter.Value, typeof(DateTime)));
// this is "c.DateFieldOne < dateIsBefore"
if (dateIsBefore != null)
right = Expression.LessThan(dateProperty, Expression.Constant(dateIsBefore.Value, typeof(DateTime)));
// now we either combine with AND or not, depending on values
Expression<Func<T, bool>> combined;
if (left != null && right != null)
combined = Expression.Lambda<Func<T, bool>>(Expression.And(left, right), arg);
else if (left != null)
combined = Expression.Lambda<Func<T, bool>>(left, arg);
else
combined = Expression.Lambda<Func<T, bool>>(right, arg);
// applying that to where and done.
source = source.Where(combined);
return source;
}
}
电话如您所料:
WhereFilter(record => record.DateFieldOne, dateOneIsBefore, dateOneIsAfter)
起初使用表达式可能看起来很奇怪,但至少对于简单的情况来说没有什么太复杂的。