2015-09-24 1 views
7

Я использую EF и имею таблицу базы данных, которая имеет несколько полей времени даты, которые заполняются, когда в записи выполняются различные операции. В настоящее время я создаю систему отчетности, которая включает фильтрацию по этим датам, но поскольку фильтры (эта дата в пределах диапазона и т. Д.) Имеют одинаковое поведение в каждом поле, я хотел бы повторно использовать свою логику фильтрации, чтобы я пишите только один фильтр даты и используйте его в каждом поле.Как повторно использовать полевой фильтр в LINQ для объектов

Мой исходный код фильтрации выглядит примерно так:

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; 
} 

Это прекрасно работает, но я бы предпочел, чтобы уменьшить дублирование кода в «где» методы, как алгоритм фильтра одинакова для каждого поля даты.

То, что я предпочел бы что-то, что выглядит следующим образом (я создать класс или структуры для значений фильтра позже), где я могу инкапсулировать алгоритм совпадения с использованием, может быть метод расширения:

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 на всю таблицу перед фильтрацией с помощью метода расширения) тянет всю базу данных и запросов, которые он в качестве объектов вместо запроса базовая база данных, поэтому она не масштабируется BLE.

Я также изучил использование PredicateBuilder из Linqkit, но не смог найти способ написать код без использования метода Invoke.

Я знаю, что есть методы, где можно выражать части запроса в виде строк, которые включают имена полей, но я бы предпочел использовать более безопасный способ записи этого кода.

Кроме того, в идеальном мире я мог бы переделать базу данных, чтобы иметь несколько записей «даты», связанных с одной записью «item», но я не имею права изменять схему базы данных таким образом.

Есть ли другой способ, чтобы написать расширение, чтобы он не использовал Invoke, или я должен заниматься повторным использованием моего кода фильтрации по-другому?

ответ

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; 
} 

Я изменил 2-й параметр для Expression<Func<TestItemTable, DateTime>> и добавил недостающую вызов метода AsExpandable() LinqKit в. Таким образом, Invoke() будет называть LinqKit's Invoke(), который затем способен делать свою магию.

Использование:

IEnumerable<TestItemTable> result = context.TestItemTables 
    .WhereFilter(record => record.DateFieldOne, dateOneIsBefore, dateOneIsAfter) 
    .WhereFilter(record => record.DateFieldTwo, dateTwoIsBefore, dateTwoIsAfter) 
    .ToList(); 
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) 

Работа с выражениями может выглядеть странно на первый, но, по крайней мере, для простых случаев нет ничего сложного.

+0

Спасибо, я мог бы что-то отсутствует, хотя ... с ненулевым значением даты фильтра (мои извинения - я, возможно, следовало бы включить это в примере кода), например: DateTime? dateOneIsAfter = new DateTime (2000, 12, 31); я получаю исключение: Необработанное исключение типа «System.InvalidCastException» произошло в ReusableDataFilter.FrontEnd.exe Дополнительная информация: Не удается привести объект типа «System.Linq.Expressions.UnaryExpression» для типа «System .Linq.Expressions.MemberExpression. на линии: var dateFieldName = ((MemberExpression) fieldData.Body) .Member.Name; – NvR

+0

@NVR обновленный ответ для работы с датами, не допускающими нулевое значение. – Evk

+0

Хотя OP жаловался на этот код, но из Exception, это означает, что он должен каким-то образом изменить код, 'fieldData' будет чем-то вроде' e => e.DateFieldOne', а его 'Body' должен быть, безусловно,' MemberExpression' (но каким-то образом это «UnaryExpresion», он может фактически использовать «e => e.DateFieldOne.Value'). Ваш исходный код (с 'DateTime?') Должен работать для 'e => DateFieldOne', текущий код должен работать для' e => e.DateFieldOne.Value'. Поэтому я бы добавил +1 за усилия. OP может обойти случай, чтобы узнать об Expression. – Hopeless

Смежные вопросы