2015-09-24 15 views
7

Sto usando EF e ho una tabella di database che ha un numero di campi di data e ora che vengono compilati come varie operazioni vengono eseguite sul record. Attualmente sto costruendo un sistema di reporting che implica il filtraggio in base a queste date, ma poiché i filtri (è questa data all'interno di un intervallo, ecc ...) hanno lo stesso comportamento su ogni campo, vorrei riutilizzare la mia logica di filtraggio scrivi solo un singolo filtro data e usalo su ogni campo.Come riutilizzare un filtro di campo in LINQ alle entità

Il mio codice di filtraggio iniziale simile a:

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

Questo funziona bene, ma preferirei per ridurre il codice duplicato in 'Dove' metodi come l'algoritmo di filtro è la stessa per ogni campo della data.

Quello che preferirei è qualcosa che è simile al seguente (creerò una classe o struct per i valori di filtro successive) dove posso incapsulare l'algoritmo partita usando forse un metodo di estensione:

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

Dove il metodo di estensione potrebbe somigliare:

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

Utilizzando il codice di cui sopra, se il mio codice di filtraggio è la seguente:

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

ottengo la seguente eccezione:

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. 

Il problema che ho è l'uso di Invoke per ottenere il particolare campo di essere interrogato come questa tecnica non risolve bene a SQL, perché se modifico il mio codice di filtraggio per la seguente verrà eseguito senza errori:

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

il problema con questo è che il codice (utilizzando ToList sull'intera tabella prima di filtrare con il metodo di estensione) tira nell'intero database e query come oggetti anziché interrogare il database sottostante quindi non è scalea ble.

Ho anche studiato utilizzando PredicateBuilder da Linqkit, ma non è riuscito a trovare un modo per scrivere il codice senza utilizzare il metodo Invoke.

So che esistono tecniche in cui è possibile esprimere parti della query come stringhe che includono nomi di campi, ma preferirei usare un modo più sicuro per scrivere questo codice.

Inoltre, in un mondo ideale potrei riprogettare il database per avere più record di "date" relativi a un singolo record "item", ma non sono libero di modificare lo schema del database in questo modo.

C'è un altro modo per scrivere l'estensione in modo che non usi Invoke, o dovrei affrontare il riutilizzo del mio codice di filtro in un modo diverso?

risposta

2

Sì, LinqKit è il modo per andare qui. Ma, vi state perdendo alcuni pezzi nel vostro metodo di estensione:

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

ho cambiato il secondo parametro da Expression<Func<TestItemTable, DateTime>> e ha aggiunto la chiamata mancante per il metodo di LinqKit AsExpandable(). In questo modo, Invoke() chiamerebbe LinqKit Invoke() che è quindi in grado di fare la sua magia.

Usage:

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

Nel tuo caso non c'è molto la duplicazione, ma ancora ti mostrerò come si può fare quello che vuoi con le espressioni crude (come esempio):

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

chiamata è, come si aspetterebbe:

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

lavorare con le espressioni potrebbe sembrare strano in un primo momento, ma almeno per i casi semplici non c'è nulla di troppo complesso.

+0

Grazie, potrei mancare qualcosa di se ... Con un valore di filtro data non nullo (le mie scuse - Ho forse avrebbero dovuto includere questo nel codice di esempio), ad esempio: DateTime? dateOneIsAfter = new DateTime (2000, 12, 31); ottengo l'eccezione: un'eccezione non gestita di tipo 'System.InvalidCastException' in ReusableDataFilter.FrontEnd.exe Ulteriori informazioni: Impossibile eseguire il cast oggetto di tipo 'System.Linq.Expressions.UnaryExpression' digitare 'sistema .Linq.Expressions.MemberExpression'. sulla riga: var dateFieldName = ((MemberExpression) fieldData.Body) .Member.Name; – NvR

+0

@NvR risposta aggiornata per lavorare con date non annullabili. – Evk

+0

Mentre l'OP si lamentava di questo codice ma dall'eccezione, significa che deve modificare il codice in qualche modo, il 'fieldData' sarebbe qualcosa come' e => e.DateFieldOne' e il suo 'Body' dovrebbe essere sicuramente un' MemberExpression' (ma in qualche modo è un 'UnaryExpresion', potrebbe effettivamente usare' e => e.DateFieldOne.Value'). Il tuo codice originale (con 'DateTime?') Dovrebbe funzionare per 'e => DateFieldOne', il codice corrente dovrebbe funzionare per' e => e.DateFieldOne.Value'. Quindi vorrei +1 per lo sforzo. L'OP potrebbe ignorare la possibilità di conoscere Expression. – Hopeless

Problemi correlati