2015-09-24 38 views
7

Używam EF i mieć tabelę bazy danych, która ma wiele pól daty, które są wypełniane, ponieważ różne operacje są wykonywane na rekord. Obecnie tworzę system raportowania, który obejmuje filtrowanie według tych dat, ale ponieważ filtry (jest to data w zakresie, itp.) Mają takie samo zachowanie w każdym polu, chciałbym ponownie użyć mojej logiki filtrowania, więc zapisuj tylko jeden filtr daty i używaj go w każdym polu.Jak ponownie użyć filtru pola w LINQ do encji

Mój początkowy kod filtrowanie wygląda mniej więcej tak:

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

Działa to dobrze, ale wolałbym, aby zmniejszyć kod duplikowane w „gdzie” metodami jak algorytmu filtra jest taka sama dla każdego pola daty.

Co wolałbym coś, co wygląda następująco (będę tworzyć klasy lub struct dla wartości filtrów późniejszych), gdzie mogę hermetyzacji algorytm dopasowania użyciu może metodę rozszerzenia:

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

Gdzie metoda rozszerzenie mogłoby wyglądać następująco:

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

Stosując powyższy kod, jeśli mój kod filtrowanie jest następujący:

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

otrzymuję następujący wyjątek:

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. 

Problem mam jest użycie Invoke dostać danej dziedzinie są pytani jak tej techniki nie rozwiązuje ładnie do SQL, ponieważ gdybym zmodyfikować mój kod filtrujący do następujące będzie działać bez błędów:

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

problem polega na tym, że kod (używając ToList na całej tabeli przed filtrowanie metodą wydłużania) ciągnie w całej bazy danych i zapytań go jako obiekty zamiast odpytywania bazowa baza danych, więc nie jest skalą ble.

Badałem również użycie PredicateBuilder z Linqkit, ale nie mogłem znaleźć sposobu na napisanie kodu bez użycia metody Invoke.

Wiem, że istnieją techniki, w których można wyrazić części zapytania jako ciągi zawierające nazwy pól, ale wolałbym użyć bezpieczniejszego sposobu pisania tego kodu w bardziej typie.

Również w idealnym świecie mógłbym przeprojektować bazę danych tak, aby zawierała wiele rekordów "daty" związanych z pojedynczym rekordem "produktu", ale nie mam takiej możliwości zmiany schematu bazy danych.

Czy jest inny sposób, muszę napisać rozszerzenie, aby nie używać Invoke, czy też powinienem przeciwdziałać ponownemu wykorzystaniu mojego kodu filtrującego w inny sposób?

Odpowiedz

2

Tak, LinqKit jest sposobem, aby przejść tutaj. Ale tracisz kilka kawałków w swoim metodę rozszerzenia:

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

Zmieniłem 2nd parametr do Expression<Func<TestItemTable, DateTime>> i dodano brakującą wezwanie do AsExpandable() metody LinqKit użytkownika. W ten sposób Invoke() będzie wywoływać LinqKit's Invoke(), który jest wtedy w stanie wykonać swoją magię.

Zastosowanie:

IEnumerable<TestItemTable> result = context.TestItemTables 
    .WhereFilter(record => record.DateFieldOne, dateOneIsBefore, dateOneIsAfter) 
    .WhereFilter(record => record.DateFieldTwo, dateTwoIsBefore, dateTwoIsAfter) 
    .ToList(); 
3

W twoim przypadku nie ma wiele dublowanie, ale jeszcze pokażę, jak można zrobić to, co chcesz z wyrażeniami surowych (jako przykład):

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

połączeń jest jak można się spodziewać:

WhereFilter(record => record.DateFieldOne, dateOneIsBefore, dateOneIsAfter) 

Praca z wyrażeniami może wyglądać dziwnie na początku, ale przynajmniej w prostych przypadkach nie ma nic zbyt skomplikowanego.

+0

Dziękuję, może brakować czegoś chociaż ... Z niepuste wartości daty filtra (moje przeprosiny - I być może to powinno zawierać w kodzie próbki), np: DateTime? dateOneIsAfter = new DateTime (2000, 12, 31); otrzymuję wyjątek: nieobsługiwany wyjątek typu „System.InvalidCastException” wystąpił w ReusableDataFilter.FrontEnd.exe Informacje dodatkowe: Nie można rzutować obiektu typu „System.Linq.Expressions.UnaryExpression” do rodzaju „system .Linq.Expressions.MemberExpression ". na linii: var dateFieldName = ((MemberExpression) fieldData.Body) .Member.Name; – NvR

+0

@NvR zaktualizowano odpowiedź, aby działała z nie-nullowanymi datami. – Evk

+0

Podczas gdy OP narzekali na ten kod, ale z wyjątku, oznacza to, że musi jakoś zmodyfikować kod, "fieldData" będzie czymś w rodzaju 'e => e.DateFieldOne' i jego' Body' powinno być z pewnością 'MemberExpression' (ale w jakiś sposób jest to "UnaryExpresion", może on faktycznie użyć 'e => e.DateFieldOne.Value'). Twój oryginalny kod (z 'DateTime?') Powinien działać dla 'e => DateFieldOne', bieżący kod powinien działać dla' e => e.DateFieldOne.Value'. Więc dałbym +1 za trud. PO może ominąć szansę poznania Wyrażenia. – Hopeless

Powiązane problemy