2013-04-26 26 views
11

ho bisogno di fare un po 'di filtraggio su un ObjectSet per ottenere le entità di cui ho bisogno in questo modo:dinamicamente aggiungere nuove espressioni lambda per creare un filtro

query = this.ObjectSet.Where(x => x.TypeId == 3); // this is just an example; 

Più avanti nel codice (e prima di lanciare l'esecuzione differita) Filtro la query nuovamente in questo modo:

query = query.Where(<another lambda here ...>); 

Che funziona abbastanza bene finora.

Ecco il mio problema:

Le entità contiene un DateFrom proprietà e un DateTo proprietà , che sono entrambi i tipi DataTime. Rappresentano un periodo di tempo .

ho bisogno di filtrare le entità per ottenere solo quelli che fanno parte di una collezione diperiodi di tempo. I periodi della collezione non sono necessariamente contigui, quindi, la logica per retreive le entità che si presenta come:

entities.Where(x => x.DateFrom >= Period1.DateFrom and x.DateTo <= Period1.DateTo) 
|| 
entities.Where(x => x.DateFrom >= Period2.DateFrom and x.DateTo <= Period2.DateTo) 
|| 

... e così via per tutti i periodi della collezione.

ho provato a farlo:

foreach (var ratePeriod in ratePeriods) 
{ 
    var period = ratePeriod; 

    query = query.Where(de => 
     de.Date >= period.DateFrom && de.Date <= period.DateTo); 
} 

Ma una volta che lancio l'esecuzione differita, si traduce questo in SQL proprio come lo voglio (un filtro per ciascuno dei periodi di tempo a molti periodi lì è nella raccolta), MA, si traduce in AND confronti invece di O confronti, che non restituisce nessuna entità, poiché un'entità non può essere parte di più di un periodo di tempo, ovviamente.

Ho bisogno di costruire una sorta di linq dinamico qui per aggregare i filtri del periodo.


Aggiornamento

Sulla base di risposta di Hatten, ho aggiunto il seguente membro:

private Expression<Func<T, bool>> CombineWithOr<T>(Expression<Func<T, bool>> firstExpression, Expression<Func<T, bool>> secondExpression) 
{ 
    // Create a parameter to use for both of the expression bodies. 
    var parameter = Expression.Parameter(typeof(T), "x"); 
    // Invoke each expression with the new parameter, and combine the expression bodies with OR. 
    var resultBody = Expression.Or(Expression.Invoke(firstExpression, parameter), Expression.Invoke(secondExpression, parameter)); 
    // Combine the parameter with the resulting expression body to create a new lambda expression. 
    return Expression.Lambda<Func<T, bool>>(resultBody, parameter); 
} 

dichiarato una nuova espressione CombineWithOr:

Expression<Func<DocumentEntry, bool>> resultExpression = n => false; 

e lo ha utilizzato nel mio periodo di raccolta iterazione come questa:

foreach (var ratePeriod in ratePeriods) 
{ 
    var period = ratePeriod; 
    Expression<Func<DocumentEntry, bool>> expression = de => de.Date >= period.DateFrom && de.Date <= period.DateTo; 
    resultExpression = this.CombineWithOr(resultExpression, expression); 
} 

var documentEntries = query.Where(resultExpression.Compile()).ToList(); 

Ho esaminato l'SQL risultante ed è come se l'espressione non abbia alcun effetto. L'SQL risultante restituisce i filtri precedentemente programmati ma non i filtri combinati. Perché ?


Update 2

ho voluto dare il suggerimento di feO2x una prova, così ho riscritto la mia domanda filtro in questo modo:

query = query.AsEnumerable() 
    .Where(de => ratePeriods 
     .Any(rp => rp.DateFrom <= de.Date && rp.DateTo >= de.Date)) 

Come potete vedere, ho aggiunto AsEnumerable() ma il compilatore mi ha dato un errore che non è in grado di convertire IEnumerable in IQueryable, quindi ho aggiunto ToQueryable() alla fine della mia query:

query = query.AsEnumerable() 
    .Where(de => ratePeriods 
     .Any(rp => rp.DateFrom <= de.Date && rp.DateTo >= de.Date)) 
      .ToQueryable(); 

Tutto funziona correttamente. Posso compilare il codice e avviare questa query. Tuttavia, non si adatta alle mie esigenze.

Durante la creazione del profilo SQL risultante, posso vedere che il filtro non fa parte della query SQL perché filtra le date in memoria durante il processo. Immagino che tu lo sappia già e questo è ciò che intendevi suggerire.

tuo suggerimento funziona, ma, dal momento che recupera tutte le entità dal database (e ci sono migliaia e migliaia di loro) prima di filtraggio in-memory, è davvero lento di tornare quella quantità enorme dal database .

Quello che voglio veramente è inviare il filtro dei periodi come parte della query SQL risultante, quindi non restituirà una quantità enorme di entità prima di finire con il processo di filtraggio.

+0

Perché stai facendo 'query = query.AsEnumerable()'? Ecco perché la query non fa parte della query SQL? Non capisco il ragionamento alla base della conversione in enumerabile, e quindi di tornare a queryable. – hattenn

+0

È stato uno dei molti modi in cui ho cercato di rendere comprensibile la query dal provider Linq-To-Entities. Sfortunatamente, l'omissione della chiamata AsEnumerable() rende la query inutilizzabile in un contesto Linq-To-Entities. Ottengo questo tipo di esecuzione: il tipo di nodo di espressione LINQ "Invoke" non è supportato in LINQ alle entità. –

risposta

8

Nonostante i buoni suggerimenti, ho dovuto utilizzare lo LinqKit. Uno dei motivi è che dovrò ripetere lo stesso tipo di aggregazione dei predicati in molti altri punti del codice. L'uso di LinqKit è il più semplice, per non dire che posso farlo scrivendo solo poche righe di codice.

Ecco come ho risolto il mio problema con LinqKit:

var predicate = PredicateBuilder.False<Document>(); 
foreach (var submittedPeriod in submittedPeriods) 
{ 
    var period = period; 
    predicate = predicate.Or(d => 
     d.Date >= period.DateFrom && d.Date <= period.DateTo); 
} 

E mi lancio esecuzione differita (notare che io chiamo AsExpandable() poco prima):

var documents = this.ObjectSet.AsExpandable().Where(predicate).ToList(); 

ho guardato lo SQL risultante e fa un buon lavoro nel tradurre i miei predicati in SQL.

1

Che ne dite di questo codice:

var targets = query.Where(de => 
    ratePeriods.Any(period => 
     de.Date >= period.DateFrom && de.Date <= period.DateTo)); 

Io uso l'operatore LINQ Any per determinare se v'è un periodo di tasso che è conforme alla de.Date. Anche se non sono abbastanza sicuro di come questo viene tradotto in istruzioni SQL efficienti per entità. Se potessi pubblicare l'SQL risultante, sarebbe abbastanza interessante per me.

Spero che questo aiuti.

UPDATE dopo la risposta di hattenn:

Non credo che la soluzione di hattenn avrebbe funzionato, perché Entity Framework utilizza espressioni LINQ per produrre lo SQL o DML che viene eseguito sul database. Pertanto, Entity Framework si basa sull'interfaccia IQueryable<T> anziché su IEnumerable<T>. Ora gli operatori LINQ predefiniti (come Where, Any, OrderBy, FirstOrDefault e così via) sono implementati su entrambe le interfacce, quindi la differenza è a volte difficile da vedere. La principale differenza di queste interfacce è che, nel caso dei metodi di estensione IEnumerable<T>, le enumerazioni restituite vengono continuamente aggiornate senza effetti collaterali, mentre nel caso di IQueryable<T> viene ricomposta l'espressione effettiva, che non è priva di effetti collaterali (cioè si sta modificando l'albero delle espressioni che viene infine utilizzato per creare la query SQL).

Ora Entity Framework supporta il ca. 50 operatori di query standard di LINQ, ma se si scrivono i propri metodi che manipolano un IQueryable<T> (come il metodo di hatenn), ciò risulterebbe in una struttura di espressioni che Entity Framework potrebbe non essere in grado di analizzare perché semplicemente non conosce la nuova estensione metodo. Questa potrebbe essere la causa del perché non è possibile vedere i filtri combinati dopo averli composti (anche se mi aspetterei un'eccezione).

Quando si fa la soluzione con il lavoro di ogni operatore:

Nei commenti, vi ha detto che avete incontrato un System.NotSupportedException: Impossibile creare un valore costante di tipo 'RatePeriod'. In questo contesto sono supportati solo tipi primitivi o tipi di enumerazione. Questo è il caso in cui gli oggetti RatePeriod sono oggetti in memoria e non tracciati da Entity Framework ObjectContext o DbContext. Ho creato una piccola soluzione di test che può essere scaricata da qui: https://dl.dropboxusercontent.com/u/14810011/LinqToEntitiesOrOperator.zip

Ho utilizzato Visual Studio 2012 con LocalDB e Entity Framework 5. Per visualizzare i risultati, apri la classe LinqToEntitiesOrOperatorTest, quindi apri Test Explorer, crea la soluzione ed esegui tutti i test. Riconoscerai che ComplexOrOperatorTestWithInMemoryObjects fallirà, tutti gli altri dovrebbero passare.

Il contesto ho usato si presenta così:

public class DatabaseContext : DbContext 
{ 
    public DbSet<Post> Posts { get; set; } 
    public DbSet<RatePeriod> RatePeriods { get; set; } 
} 
public class Post 
{ 
    public int ID { get; set; } 
    public DateTime PostDate { get; set; } 
} 
public class RatePeriod 
{ 
    public int ID { get; set; } 
    public DateTime From { get; set; } 
    public DateTime To { get; set; } 
} 

Beh, è ​​così semplice come si arriva :-). Nel progetto di test, ci sono due importanti metodi di prova unità:

[TestMethod] 
    public void ComplexOrOperatorDBTest() 
    { 
     var allAffectedPosts = 
      DatabaseContext.Posts.Where(
       post => 
       DatabaseContext.RatePeriods.Any(period => period.From < post.PostDate && period.To > post.PostDate)); 

     Assert.AreEqual(3, allAffectedPosts.Count()); 
    } 

    [TestMethod] 
    public void ComplexOrOperatorTestWithInMemoryObjects() 
    { 
     var inMemoryRatePeriods = new List<RatePeriod> 
      { 
       new RatePeriod {ID = 1000, From = new DateTime(2002, 01, 01), To = new DateTime(2006, 01, 01)}, 
       new RatePeriod {ID = 1001, From = new DateTime(1963, 01, 01), To = new DateTime(1967, 01, 01)} 
      }; 

     var allAffectedPosts = 
      DatabaseContext.Posts.Where(
       post => inMemoryRatePeriods.Any(period => period.From < post.PostDate && period.To > post.PostDate)); 
     Assert.AreEqual(3, allAffectedPosts.Count()); 
    } 

noti che il primo metodo passa mentre la seconda non riesce con l'eccezione di cui sopra, anche se entrambi i metodi fanno esattamente la stessa cosa, tranne che nel Nel secondo caso ho creato oggetti di tasso durante la memoria di cui lo DatabaseContext non sa nulla.

Cosa si può fare per risolvere questo problema?

  1. fare la tua RatePeriod oggetti risiedono nella stessa ObjectContext o DbContext rispettivamente? Quindi usali proprio come fa nel primo test di unità menzionato sopra.

  2. In caso contrario, è possibile caricare tutti i tuoi messaggi in una sola volta o ciò comporterebbe un OutOfMemoryException? In caso contrario, è possibile utilizzare il seguente codice. Si noti la chiamata AsEnumerable() che provoca l'utilizzo dell'operatore Where rispetto all'interfaccia IEnumerable<T> anziché IQueryable<T>. In effetti, questo si traduce in tutti i messaggi sia caricato in memoria e poi filtrato:

    [TestMethod] 
    public void CorrectComplexOrOperatorTestWithInMemoryObjects() 
    { 
        var inMemoryRatePeriods = new List<RatePeriod> 
         { 
          new RatePeriod {ID = 1000, From = new DateTime(2002, 01, 01), To = new DateTime(2006, 01, 01)}, 
          new RatePeriod {ID = 1001, From = new DateTime(1963, 01, 01), To = new DateTime(1967, 01, 01)} 
         }; 
    
        var allAffectedPosts = 
         DatabaseContext.Posts.AsEnumerable() 
             .Where(
              post => 
              inMemoryRatePeriods.Any(
               period => period.From < post.PostDate && period.To > post.PostDate)); 
        Assert.AreEqual(3, allAffectedPosts.Count()); 
    } 
    
  3. Se la seconda soluzione non è possibile, allora vi consiglio di scrivere una procedura TSQL memorizzata in cui si passa nel ciclo dei tassi e che forma l'istruzione SQL corretta. Questa soluzione è anche la più performante.

+0

Questa è davvero una buona idea! Sfortunatamente, mi dà questo errore: Impossibile creare un valore costante di tipo "RatePeriod". In questo contesto sono supportati solo tipi primitivi o tipi di enumerazione_. Perché ? Devo proiettare i due tipi DateTime dall'oggetto RatePeriod prima di chiamare il metodo Any?Come potrei ottenere quello? –

+0

Ho aggiornato il mio post per riflettere l'eccezione che hai incontrato. – feO2x

+0

Wow, grazie mille per il tempo dedicato a questo. Ci vorrà del tempo questo fine settimana per controllarlo e tornerò da te. Questo è molto apprezzato. –

3

È possibile utilizzare un metodo simile al seguente:

Expression<Func<T, bool>> CombineWithOr<T>(Expression<Func<T, bool>> firstExpression, Expression<Func<T, bool>> secondExpression) 
{ 
    // Create a parameter to use for both of the expression bodies. 
    var parameter = Expression.Parameter(typeof(T), "x"); 
    // Invoke each expression with the new parameter, and combine the expression bodies with OR. 
    var resultBody = Expression.Or(Expression.Invoke(firstExpression, parameter), Expression.Invoke(secondExpression, parameter)); 
    // Combine the parameter with the resulting expression body to create a new lambda expression. 
    return Expression.Lambda<Func<T, bool>>(resultBody, parameter); 
} 

E poi:

Expression<Func<T, bool>> resultExpression = n => false; // Always false, so that it won't affect the OR. 
foreach (var ratePeriod in ratePeriods) 
{ 
    var period = ratePeriod; 
    Expression<Func<T, bool>> expression = (de => de.Date >= period.DateFrom && de.Date <= period.DateTo); 
    resultExpression = CombineWithOr(resultExpression, expression); 
} 

// Don't forget to compile the expression in the end. 
query = query.Where(resultExpression.Compile()); 

Per ulteriori informazioni, si consiglia di controllare il seguente:

Combining two expressions (Expression<Func<T, bool>>)

http://www.albahari.com/nutshell/predicatebuilder.aspx

Edit: La linea Expression<Func<DocumentEntry, bool>> resultExpression = n => false; è solo un segnaposto. Il metodo CombineWithOr necessita di due metodi da combinare, se si scrive Expression<Func<DocumentEntry, bool>> resultExpression;', you can't use it in the call to CombineWithOr ciclo for the first time in your foreach`. E 'proprio come il seguente codice:

int resultOfMultiplications = 1; 
for (int i = 0; i < 10; i++) 
    resultOfMultiplications = resultOfMultiplications * i; 

Se non c'è niente in resultOfMultiplications per cominciare, non è possibile utilizzare nel vostro ciclo.

Sul motivo per cui la lambda è n => false. Perché non ha alcun effetto in una dichiarazione OR. Ad esempio, false OR someExpression OR someExpression equivale a someExpression OR someExpression. Che false non ha alcun effetto.

+0

Modificherò la mia domanda usando il codice suggerito non appena posso risolvere questo problema: Alla riga 'Espressione > resultExpression = n => n ==! N;' il compilatore mi dice _Cannot applicare ! operatore a operando di tipo DocumentEntry_. (DocumentEntry è il mio tipo di entità). –

+0

Siamo spiacenti, 'n' non è un bool, basta sostituirlo con' n => false'. È stato uno stupido errore dalla mia parte. Controlla la mia modifica per vedere. – hattenn

+0

Grazie. Ho aggiornato la mia domanda con la mia implementazione del codice e ho fatto qualche altra domanda. –

0

In ogni caso, penso che la creazione dinamica di query LINQ non sia stata così semplice come pensavo. Provare a utilizzare Entity SQL, simile al modo di seguito:

var filters = new List<string>(); 
foreach (var ratePeriod in ratePeriods) 
{ 
    filters.Add(string.Format("(it.Date >= {0} AND it.Date <= {1})", ratePeriod.DateFrom, ratePeriod.DateTo)); 
} 

var filter = string.Join(" OR ", filters); 
var result = query.Where(filter); 

questo potrebbe non essere esattamente corretto (non ho provato), ma dovrebbe essere qualcosa di simile a questo.