如何在LINQ to Entities中重用字段过滤器

NvR*_*NvR 7 c# linq-to-entities entity-framework

我正在使用EF并且有一个数据库表,其中包含许多日期时间字段,这些字段在记录上执行各种操作时被填充.我目前正在构建一个报告系统,它涉及按这些日期过滤,但由于过滤器(这个日期在一个范围内等等)在每个字段上都有相同的行为,我想重用我的过滤逻辑,所以我只编写单个日期过滤器并在每个字段上使用它.

我的初始过滤代码类似于:

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;
}
Run Code Online (Sandbox Code Playgroud)

这样可以正常工作,但我更倾向于减少'Where'方法中的重复代码,因为每个日期字段的过滤算法都相同.

我更喜欢的是看起来像下面这样的东西(我稍后会为过滤器值创建一个类或结构),我可以使用扩展方法封装匹配算法:

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;
}
Run Code Online (Sandbox Code Playgroud)

扩展方法可能类似于:

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;
}
Run Code Online (Sandbox Code Playgroud)

使用上面的代码,如果我的过滤代码如下:

IEnumerable<TestItemTable> result = context.TestItemTables
    .WhereFilter(record => record.DateFieldOne, dateOneIsBefore, dateOneIsAfter)
    .WhereFilter(record => record.DateFieldTwo, dateTwoIsBefore, dateTwoIsAfter)
    .ToList();
Run Code Online (Sandbox Code Playgroud)

我得到以下异常:

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.
Run Code Online (Sandbox Code Playgroud)

我遇到的问题是使用Invoke来查询特定字段,因为这种技术不能很好地解析SQL,因为如果我将过滤代码修改为以下内容,它将运行而不会出现错误:

IEnumerable<TestItemTable> result = context.TestItemTables
    .ToList()
    .AsQueryable()
    .WhereFilter(record => record.DateFieldOne, dateOneIsBefore, dateOneIsAfter)
    .WhereFilter(record => record.DateFieldTwo, dateTwoIsBefore, dateTwoIsAfter)
    .ToList();
Run Code Online (Sandbox Code Playgroud)

这个问题是代码(在使用扩展方法过滤之前在整个表上使用ToList)拉入整个数据库并将其作为对象进行查询,而不是查询底层数据库,因此它不可扩展.

我还研究过使用Linqkit的PredicateBuilder,但是在没有使用Invoke方法的情况下找不到编写代码的方法.

我知道有些技术可以将查询的某些部分表示为包含字段名称的字符串,但我更喜欢使用更安全的方式来编写此代码.

此外,在理想的世界中,我可以重新设计数据库,使其具有与单个"项目"记录相关的多个"日期"记录,但我无法以这种方式更改数据库模式.

有没有其他方法我需要编写扩展,所以它不使用Invoke,或者我应该以不同的方式处理我的过滤代码的重用?

hai*_*770 2

是的,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;
}
Run Code Online (Sandbox Code Playgroud)

我将第二个参数更改为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();
Run Code Online (Sandbox Code Playgroud)