如何在LINQ to Entities

时间:2015-09-24 09:11:56

标签: c# entity-framework linq-to-entities

我正在使用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;
}

这样可以正常工作,但我更倾向于减少'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;
}

扩展方法可能类似于:

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

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

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

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

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

我是否需要另一种方式来编写扩展,以便它不使用Invoke,或者我应该以不同的方式处理我的过滤代码的重用?

2 个答案:

答案 0 :(得分:3)

在你的情况下没有太多的重复,但我仍然会展示你如何用原始表达式做你想做的事情(例如):

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)

一开始使用表达式可能看起来很奇怪,但至少对于简单的情况,没有什么太复杂了。

答案 1 :(得分: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;
}

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