Come scrivere query LINQ asincrona?

Dopo aver letto un sacco di materiale correlato a LINQ, improvvisamente mi sono reso conto che nessun articolo introduce come scrivere query LINQ asincrona.

Supponiamo di utilizzare LINQ to SQL, l’istruzione sottostante è chiara. Tuttavia, se il database SQL risponde lentamente, il thread che utilizza questo blocco di codice potrebbe essere ostacolato.

var result = from item in Products where item.Price > 3 select item.Name; foreach (var name in result) { Console.WriteLine(name); } 

Sembra che le attuali specifiche della query LINQ non forniscano supporto.

C’è un modo per fare LINQ di programmazione asincrona? Funziona come se ci fosse una notifica di callback quando i risultati sono pronti per l’uso senza alcun ritardo di blocco su I / O.

Mentre LINQ non ha questo di per sé, il framework stesso … Puoi facilmente eseguire il tuo proprio esecutore di query asincrone in 30 righe o giù di lì … In effetti, l’ho appena buttato insieme per te 🙂

EDIT: Attraverso la scrittura di questo, ho scoperto il motivo per cui non l’hanno implementato. Non è in grado di gestire i tipi anonimi poiché sono locali con ambito. Quindi, non hai modo di definire la tua funzione di callback. Questa è una cosa piuttosto importante dal momento che molte cose da linq a sql le creano nella clausola select. Ognuno dei suggerimenti seguenti subisce lo stesso destino, quindi continuo a pensare che questo sia il più facile da usare!

EDIT: l’unica soluzione è non usare tipi anonimi. È ansible dichiarare la richiamata semplicemente tenendo IEnumerable (nessun tipo args) e utilizzare reflection per accedere ai campi (ICK !!). Un altro modo sarebbe dichiarare la richiamata come “dynamic” … oh … aspetta … Non è ancora uscita. 🙂 Questo è un altro esempio decente di come potrebbe essere utilizzata la dynamic. Alcuni potrebbero chiamarlo abuso.

Buttalo nella tua libreria di utilità:

 public static class AsynchronousQueryExecutor { public static void Call(IEnumerable query, Action> callback, Action errorCallback) { Func, IEnumerable> func = new Func, IEnumerable>(InnerEnumerate); IEnumerable result = null; IAsyncResult ar = func.BeginInvoke( query, new AsyncCallback(delegate(IAsyncResult arr) { try { result = ((Func, IEnumerable>)((AsyncResult)arr).AsyncDelegate).EndInvoke(arr); } catch (Exception ex) { if (errorCallback != null) { errorCallback(ex); } return; } //errors from inside here are the callbacks problem //I think it would be confusing to report them callback(result); }), null); } private static IEnumerable InnerEnumerate(IEnumerable query) { foreach (var item in query) //the method hangs here while the query executes { yield return item; } } } 

E potresti usarlo in questo modo:

 class Program { public static void Main(string[] args) { //this could be your linq query var qry = TestSlowLoadingEnumerable(); //We begin the call and give it our callback delegate //and a delegate to an error handler AsynchronousQueryExecutor.Call(qry, HandleResults, HandleError); Console.WriteLine("Call began on seperate thread, execution continued"); Console.ReadLine(); } public static void HandleResults(IEnumerable results) { //the results are available in here foreach (var item in results) { Console.WriteLine(item); } } public static void HandleError(Exception ex) { Console.WriteLine("error"); } //just a sample lazy loading enumerable public static IEnumerable TestSlowLoadingEnumerable() { Thread.Sleep(5000); foreach (var i in new int[] { 1, 2, 3, 4, 5, 6 }) { yield return i; } } } 

Andando a mettere questo sul mio blog ora, molto utile.

Le soluzioni di The SoftwareJedi e ulrikb (aka user316318) sono valide per qualsiasi tipo di LINQ, ma (come indicato da Chris Moschini ) NON delegano a sottostanti chiamate asincrone che sfruttano le porte di completamento di I / O di Windows.

Il post Asynchronous DataContext di Wesley Bakker (triggersto da un post sul blog di Scott Hanselman ) descrive la class per LINQ su SQL che utilizza sqlCommand.BeginExecuteReader / sqlCommand.EndExecuteReader, che utilizza le porte di completamento I / O di Windows.

Le porte di completamento I / O forniscono un modello di threading efficiente per l’elaborazione di più richieste I / O asincrone su un sistema multiprocessore.

Basato sulla risposta di Michael Freidgeim e sul blog post di Scott Hansellman e sul fatto che è ansible utilizzare async / await , è ansible implementare il ExecuteAsync(...) riutilizzabile ExecuteAsync(...) , che esegue in modo asincrono SqlCommand sottostante:

 protected static async Task> ExecuteAsync(IQueryable query, DataContext ctx, CancellationToken token = default(CancellationToken)) { var cmd = (SqlCommand)ctx.GetCommand(query); if (cmd.Connection.State == ConnectionState.Closed) await cmd.Connection.OpenAsync(token); var reader = await cmd.ExecuteReaderAsync(token); return ctx.Translate(reader); } 

E poi puoi (ri) usarlo in questo modo:

 public async Task WriteNamesToConsoleAsync(string connectionString, CancellationToken token = default(CancellationToken)) { using (var ctx = new DataContext(connectionString)) { var query = from item in Products where item.Price > 3 select item.Name; var result = await ExecuteAsync(query, ctx, token); foreach (var name in result) { Console.WriteLine(name); } } } 

Ho avviato un semplice progetto github denominato Asynq per eseguire l’esecuzione di query LINQ-to-SQL asincrone. L’idea è abbastanza semplice anche se “fragile” in questa fase (a partire dall’8 / 16/2011):

  1. Lascia che LINQ-to-SQL DbCommand il lavoro “pesante” di traduzione di IQueryable in un DbCommand tramite DataContext.GetCommand() .
  2. Per SQL 200 [058], DbCommand il cast DbCommand astratta ottenuta da GetCommand() per ottenere SqlCommand . Se stai usando SQL CE non sei fortunato dato che SqlCeCommand non espone il modello asincrono per BeginExecuteReader e EndExecuteReader .
  3. Utilizzare BeginExecuteReader ed EndExecuteReader da SqlCommand utilizzando il modello I / O asincrono standard di .NET framework per ottenere un DbDataReader nel delegato di callback del completamento passato al metodo BeginExecuteReader .
  4. Ora abbiamo un DbDataReader che non abbiamo idea di quali colonne contiene né come mappare quei valori su ElementType IQueryable (molto probabilmente un tipo anonimo nel caso di join). Certo, a questo punto potresti scrivere a mano il tuo mappatore di colonne che materializza i risultati nel tuo tipo anonimo o altro. Dovresti scriverne uno nuovo per ogni tipo di risultato della query, a seconda di come LINQ-to-SQL tratta il tuo IQueryable e quale codice SQL genera. Questa è un’opzione piuttosto sgradevole e non la consiglio poiché non è gestibile né sarebbe sempre corretta. LINQ-to-SQL può modificare il modulo di query in base ai valori dei parametri che si passano, ad esempio query.Take(10).Skip(0) produce SQL diverso da query.Take(10).Skip(10) , e forse un diverso schema di risultati. La soluzione migliore è gestire questo problema di materializzazione a livello di codice:
  5. “Reimplementare” un materializzatore di object runtime semplicistico che estrae le colonne da DbDataReader in un ordine definito in base agli attributi di mapping LINQ-to-SQL di Tipo ElementType per IQueryable . L’implementazione corretta è probabilmente la parte più impegnativa di questa soluzione.

Come altri hanno scoperto, il metodo DataContext.Translate() non gestisce tipi anonimi e può solo mappare un DbDataReader direttamente a un object proxy LINQ-to-SQL correttamente attribuito. Dal momento che la maggior parte delle query che vale la pena scrivere in LINQ coinvolgeranno complessi join che inevitabilmente richiedono tipi anonimi per la clausola di selezione finale, è piuttosto inutile utilizzare comunque questo metodo DataContext.Translate() annaffiato.

Ci sono alcuni piccoli inconvenienti a questa soluzione quando si utilizza il provider IQueryable maturo LINQ-to-SQL esistente:

  1. Non puoi associare una singola istanza di object a più proprietà di tipo anonimo nella clausola di selezione finale di IQueryable , ad esempio from x in db.Table1 select new { a = x, b = x } . LINQ-to-SQL tiene traccia di quali colonne di ordinali mappano a quali proprietà; non espone queste informazioni all’utente finale in modo da non avere idea di quali colonne in DbDataReader vengano riutilizzate e quali siano “distinte”.
  2. Non è ansible includere valori costanti nella clausola select finale – questi non vengono tradotti in SQL e saranno assenti da DbDataReader quindi è necessario creare una logica personalizzata per estrarre questi valori costanti dall’albero Expression IQueryable , che sarebbe essere piuttosto una seccatura e non è semplicemente giustificabile.

Sono sicuro che ci sono altri modelli di query che potrebbero rompersi, ma questi sono i due più grandi che potrei pensare che potrebbero causare problemi in un livello di accesso ai dati LINQ-to-SQL esistente.

Questi problemi sono facili da sconfiggere – semplicemente non li fanno nelle tue query poiché nessuno dei due pattern fornisce alcun beneficio al risultato finale della query. Speriamo che questo consiglio si applichi a tutti i modelli di query che potrebbero causare problemi di materializzazione degli oggetti :-P. È un problema difficile risolvere non avendo accesso alle informazioni di mapping delle colonne di LINQ-to-SQL.

Un approccio più “completo” alla risoluzione del problema sarebbe quello di reimplementare efficacemente quasi tutto LINQ-to-SQL, che richiede un po ‘più tempo :-P. Partendo da una implementazione di provider LINQ-to-SQL open source di qualità sarebbe un buon modo per andare qui. Il motivo per cui è necessario reimplementarlo è che si avrà accesso a tutte le informazioni di mapping delle colonne utilizzate per materializzare i risultati di DbDataReader su un’istanza dell’object senza alcuna perdita di informazioni.