c# - Restituzione di IEnumerable<T> vs. IQueryable<T>




linq linq-to-sql (10)

Durante l'utilizzo di LINQ su entità, è importante capire quando utilizzare IEnumerable e IQueryable. Se usiamo IEnumerable, la query verrà eseguita immediatamente. Se utilizziamo IQueryable, l'esecuzione della query verrà posticipata fino a quando l'applicazione richiede l'enumerazione. Ora vediamo cosa dovrebbe essere considerato mentre decidiamo se usare IQueryable o IEnumerable. L'utilizzo di IQueryable offre la possibilità di creare una query LINQ complessa utilizzando più istruzioni senza eseguire la query a livello di database. La query viene eseguita solo quando viene enumerata la query LINQ finale.

Qual è la differenza tra restituire IQueryable<T> vs. IEnumerable<T> ?

IQueryable<Customer> custs = from c in db.Customers
where c.City == "<City>"
select c;

IEnumerable<Customer> custs = from c in db.Customers
where c.City == "<City>"
select c;

Entrambi saranno esecuzioni posticipate e quando si dovrebbe preferire l'altro?


Entrambi ti daranno un'esecuzione differita, sì.

Per quanto è preferibile rispetto all'altro, dipende da quale sia l'origine dati sottostante.

La restituzione di un oggetto IEnumerable automaticamente al runtime di utilizzare LINQ to Objects per interrogare la raccolta.

Restituire un IQueryable (che implementa IEnumerable , tra l'altro) fornisce la funzionalità extra per tradurre la query in qualcosa che potrebbe funzionare meglio sulla sorgente sottostante (LINQ to SQL, LINQ to XML, ecc.).


In generale, si desidera conservare il tipo statico originale della query finché non è importante.

Per questo motivo, puoi definire la variabile come "var" invece di IQueryable<> o IEnumerable<> e saprai che non stai cambiando il tipo.

Se inizi con un IQueryable<> , in genere vuoi mantenerlo come IQueryable<> fino a quando non ci sono alcuni validi motivi per cambiarlo. La ragione di ciò è che si desidera fornire al processore di query quante più informazioni possibili. Ad esempio, se utilizzerai solo 10 risultati (che hai chiamato Take(10) ), vuoi che SQL Server lo sappia in modo che possa ottimizzare i piani di query e inviare solo i dati che utilizzerai .

Un motivo valido per modificare il tipo da IQueryable<> a IEnumerable<> potrebbe essere che si sta chiamando una funzione di estensione che l'implementazione di IQueryable<> nel proprio oggetto specifico non può gestire o gestire in modo inefficiente. In tal caso, potresti voler convertire il tipo in IEnumerable<> (assegnando a una variabile di tipo IEnumerable<> o utilizzando il metodo di estensione AsEnumerable , ad esempio) in modo che le funzioni di estensione che chiami siano quelle del Classe Enumerable invece della classe Queryable .


In termini generali, consiglierei quanto segue:

  • Restituisci IQueryable<T> se vuoi consentire allo sviluppatore di utilizzare il tuo metodo per perfezionare la query restituita prima dell'esecuzione.

  • Restituire IEnumerable se si desidera trasportare un set di oggetti su cui eseguire l'enumerazione.

Immagina un IQueryable come quello che è: una "query" per i dati (che puoi perfezionare se vuoi). Un oggetto IEnumerable è un insieme di oggetti (che è già stato ricevuto o è stato creato) su cui è possibile enumerare.


La risposta migliore è buona, ma non menziona gli alberi di espressione che spiegano "come" le due interfacce differiscono. Fondamentalmente, ci sono due serie identiche di estensioni LINQ. Where() , Sum() , Count() , FirstOrDefault() , ecc. Hanno tutte due versioni: una che accetta le funzioni e una che accetta le espressioni.

  • La firma della versione IEnumerable è: Where(Func<Customer, bool> predicate)

  • La firma della versione IQueryable è: Where(Expression<Func<Customer, bool>> predicate)

Probabilmente hai usato entrambi quelli senza rendertene conto perché entrambi sono chiamati usando la stessa sintassi:

Ad esempio, Where(x => x.City == "<City>") funziona su entrambi IEnumerable e IQueryable

  • Quando si utilizza Where() su una raccolta IEnumerable , il compilatore passa una funzione compilata a Where()

  • Quando si utilizza Where() su una raccolta IQueryable , il compilatore passa un albero di espressioni a Where() . Un albero di espressioni è come il sistema di riflessione ma per il codice. Il compilatore converte il tuo codice in una struttura dati che descrive cosa fa il tuo codice in un formato facilmente digeribile.

Perché preoccuparsi di questa cosa dell'albero dell'espressione? Voglio solo Where() per filtrare i miei dati. Il motivo principale è che entrambi gli ORM di Linq2SQL e di EF possono convertire alberi di espressioni direttamente in SQL, dove il codice verrà eseguito molto più velocemente.

Oh, sembra un aumento di prestazioni gratuito, dovrei usare AsQueryable() dappertutto in quel caso? No, IQueryable è utile solo se il fornitore di dati sottostante può fare qualcosa con esso. La conversione di qualcosa come una List normale in IQueryable non ti darà alcun beneficio.


Molto è stato detto in precedenza, ma tornando alle origini, in un modo più tecnico:

  1. IEnumerable è una raccolta di oggetti in memoria che puoi enumerare : una sequenza in memoria che consente di scorrere (facilita l'accesso al ciclo foreach , sebbene tu possa utilizzare solo IEnumerator ). Risiedono nella memoria così com'è.
  2. IQueryable è un albero di espressioni che verrà tradotto in qualcos'altro a un certo punto con la possibilità di enumerare il risultato finale . Immagino che questo sia ciò che confonde molte persone.

Ovviamente hanno connotazioni diverse.

IQueryable rappresenta un albero di espressioni (una query, semplicemente) che verrà tradotto in qualcos'altro dal provider di query sottostante non appena vengono chiamate API di rilascio, come le funzioni di aggregazione LINQ (somma, conteggio ecc.) O elenco [Array, dizionario, ...]. Inoltre, gli oggetti IQueryable implementano IEnumerable , IEnumerable<T> modo che se rappresentano una query il risultato di tale query possa essere iterato. Significa che IQueryable non deve essere solo una query. Il termine giusto è che sono alberi di espressione .

Ora, come vengono eseguite queste espressioni e a cosa si rivolgono, tutto dipende dai cosiddetti provider di query (esecutori di espressioni a cui possiamo pensare).

Nel mondo Entity Framework (che è il mistico fornitore di dati sottostanti o il provider di query) le espressioni IQueryable sono tradotte in query T-SQL native. Nhibernate fa cose simili con loro. È possibile scrivere il proprio seguendo i concetti descritti in LINQ: Creazione di un collegamento IQueryable Provider , ad esempio, e si potrebbe desiderare di avere un'API di query personalizzata per il servizio del fornitore del negozio di prodotti.

Quindi, in pratica, gli oggetti IQueryable vengono costruiti fino a quando non li rilasciamo esplicitamente e comunichiamo al sistema di riscriverli in SQL o qualsiasi altra cosa e inviare la catena di esecuzione per l'elaborazione successiva.

Come se l'esecuzione fosse differita , è una caratteristica LINQ per mantenere lo schema ad albero delle espressioni nella memoria e inviarlo nell'esecuzione solo su richiesta, ogni volta che determinate API vengono chiamate contro la sequenza (lo stesso conteggio, ToList, ecc.).

L'uso corretto di entrambi dipende in larga misura dalle attività che stai affrontando per il caso specifico. Per il ben noto modello di repository io personalmente scelgo di restituire IList , cioè IEnumerable over Lists (indicizzatori e simili). Quindi è un mio consiglio usare IQueryable solo all'interno di repository e IEnumerable in qualsiasi altro punto del codice. Non si parla dei problemi di testabilità che IQueryable rompe e rovina il principio della separazione delle preoccupazioni . Se restituisci un'espressione dai repository, i consumatori possono giocare con il livello di persistenza come vorrebbe.

Una piccola aggiunta al pasticcio :) (da una discussione nei commenti)) Nessuno di essi è un oggetto in memoria poiché non sono veri tipi di per sé, sono dei marcatori di un tipo - se si vuole andare così in profondità. Ma ha senso (e questo è il motivo per cui anche MSDN dice in questo modo) di pensare a IEnumerables come raccolte in memoria mentre IQueryables come alberi di espressione. Il punto è che l'interfaccia IQueryable eredita l'interfaccia IEnumerable in modo che se rappresenta una query, i risultati di tale query possano essere enumerati. L'enumerazione provoca l'esecuzione dell'albero delle espressioni associato a un oggetto IQueryable. Quindi, in effetti, non è possibile chiamare alcun membro IEnumerable senza avere l'oggetto in memoria. Entrerà lì se lo fai, comunque, se non è vuoto. IQueryables sono solo query, non i dati.


Possiamo usare entrambi allo stesso modo, e sono solo diversi nella performance.

IQueryable si esegue solo contro il database in modo efficiente. Significa che crea un'intera query di selezione e ottiene solo i record correlati.

Ad esempio, vogliamo prendere i primi 10 clienti il ​​cui nome inizia con "Nimal". In questo caso, la query di selezione verrà generata come select top 10 * from Customer where name like 'Nimal%' .

Ma se usassimo IEnumerable, la query sarebbe come select * from Customer where name like 'Nimal%' e la top ten verranno filtrati al livello di codifica C # (ottiene tutti i record dei clienti dal database e li passa in C #) .


Recentemente ho riscontrato un problema con IEnumerable v. IQueryable . L'algoritmo utilizzato per prima ha eseguito una query IQueryable per ottenere un set di risultati. Questi sono stati quindi passati a un ciclo foreach , con gli elementi istanziati come classe Entity Framework (EF). Questa classe EF è stata quindi utilizzata nella clausola from di una query Linq to Entity, causando il risultato in IEnumerable .

Sono abbastanza nuovo per EF e Linq per Entities, quindi ci è voluto un po 'per capire quale fosse il collo di bottiglia. Utilizzando MiniProfiling, ho trovato la query e poi ho convertito tutte le singole operazioni in una singola query Linq IQ per Entità. L' IEnumerable impiegava 15 secondi e l' IQueryable impiegava 0,5 secondi per essere eseguito. C'erano tre tabelle coinvolte e, dopo aver letto questo, credo che la query IEnumerable stava effettivamente formando un prodotto trasversale a tre tabelle e filtrando i risultati.

Cerca di utilizzare IQueryables come regola empirica e traccia il tuo lavoro per rendere misurabili le tue modifiche.


Sì, entrambi utilizzano l'esecuzione differita. Illustriamo la differenza utilizzando il profiler di SQL Server ....

Quando eseguiamo il seguente codice:

MarketDevEntities db = new MarketDevEntities();

IEnumerable<WebLog> first = db.WebLogs;
var second = first.Where(c => c.DurationSeconds > 10);
var third = second.Where(c => c.WebLogID > 100);
var result = third.Where(c => c.EmailAddress.Length > 11);

Console.Write(result.First().UserName);

Nel profiler di SQL Server troviamo un comando uguale a:

"SELECT * FROM [dbo].[WebLog]"

Richiede circa 90 secondi per eseguire quel blocco di codice contro una tabella WebLog con 1 milione di record.

Quindi, tutti i record di tabella vengono caricati in memoria come oggetti e quindi con ciascuno .Where () sarà un altro filtro in memoria rispetto a questi oggetti.

Quando utilizziamo IQueryable anziché IEnumerable nell'esempio precedente (seconda riga):

Nel profiler di SQL Server troviamo un comando uguale a:

"SELECT TOP 1 * FROM [dbo].[WebLog] WHERE [DurationSeconds] > 10 AND [WebLogID] > 100 AND LEN([EmailAddress]) > 11"

Impiega circa quattro secondi per eseguire questo blocco di codice usando IQueryable .

IQueryable ha una proprietà chiamata Expression che memorizza un'espressione ad albero che inizia a essere creata quando abbiamo usato il result nel nostro esempio (che è chiamato esecuzione differita) e alla fine questa espressione sarà convertita in una query SQL da eseguire sul motore del database .


Vorrei chiarire alcune cose a causa di risposte apparentemente contrastanti (in particolare per quanto riguarda IEnumerable).

(1) IQueryable estende l'interfaccia IEnumerable . (Puoi inviare un IQueryable a qualcosa che si aspetta IEnumerable senza errori.)

(2) Sia IQueryable che IEnumerable LINQ tentano il caricamento lento durante l'iterazione sul set di risultati. (Si noti che l'implementazione può essere vista nei metodi di estensione dell'interfaccia per ciascun tipo).

In altre parole, IEnumerables non sono esclusivamente "in-memory". IQueryables non vengono sempre eseguiti sul database. IEnumerable deve caricare le cose in memoria (una volta recuperate, forse pigramente) perché non ha un fornitore di dati astratto. IQueryables basano su un fornitore astratto (come LINQ-to-SQL), sebbene questo potrebbe anche essere il provider .NET in-memory.

Caso d'uso di esempio

(a) Recupera un elenco di record come IQueryable dal contesto EF. (Nessun record è in memoria.)

(b) Passare l' IQueryable a una vista il cui modello è IEnumerable . (Valido. IQueryable estende IEnumerable ).

(c) Iterare e accedere ai record del set di dati, alle entità figlio e alle proprietà dalla vista. (Può causare eccezioni!)

Possibili problemi

(1) IEnumerable tenta il caricamento lento e il contesto dei dati è scaduto. Eccezione generata perché il provider non è più disponibile.

(2) I proxy entità Entity Framework sono abilitati (impostazione predefinita) e si tenta di accedere a un oggetto (virtuale) correlato con un contesto di dati scaduti. Lo stesso di (1).

(3) Set di risultati attivi multipli (MARS). Se si sta iterando su IEnumerable in un blocco foreach( var record in resultSet ) e si tenta contemporaneamente di accedere a record.childEntity.childProperty , si può finire con MARS a causa del caricamento lento sia del set di dati sia dell'entità relazionale. Ciò causerà un'eccezione se non è abilitata nella stringa di connessione.

Soluzione

  • Ho scoperto che abilitare MARS nella stringa di connessione funziona in modo inaffidabile. Ti suggerisco di evitare MARS a meno che non sia ben compreso e esplicitamente desiderato.

Esegui la query e memorizza i risultati richiamando resultList = resultSet.ToList() Questo sembra essere il modo più semplice per garantire che le tue entità siano in memoria.

Nei casi in cui si sta accedendo alle entità correlate, è comunque possibile richiedere un contesto di dati. In DbSet , puoi disabilitare i proxy di entità e Include esplicitamente entità correlate dal tuo DbSet .





iqueryable