Cosa succede con la restituzione di IEnumerable se utilizzato con async/await (streaming di dati da SQL Server con Dapper)?

Cosa succede con la restituzione di IEnumerable se utilizzato con async/await (streaming di dati da SQL Server con Dapper)?

Aggiornamento marzo 2020

.NET Core 3.0 (e 3.1) sono ora disponibili, con supporto completo per flussi asincroni. Microsoft.Bcl.AsyncInterfaces aggiunge il supporto per loro a .NET Standard 2.0 e .NET Framework 4.6.1+, anche se 4.7.2 dovrebbe essere usato per motivi di sanità mentale. Come spiegano i documenti sul supporto per l'implementazione di .NET Standard

Risposta originale

Se controlli il codice sorgente, vedrai che il tuo sospetto è quasi corretto. Quando buffered è falso, QueryAsync trasmetterà in streaming in modo sincrono .

if (command.Buffered)
{
    var buffer = new List<T>();
    var convertToType = Nullable.GetUnderlyingType(effectiveType) ?? effectiveType;
    while (await reader.ReadAsync(cancel).ConfigureAwait(false))
    {
        object val = func(reader);
        if (val == null || val is T)
        {
            buffer.Add((T)val);
        }
        else
        {
            buffer.Add((T)Convert.ChangeType(val, convertToType, CultureInfo.InvariantCulture));
        }
    }
    while (await reader.NextResultAsync(cancel).ConfigureAwait(false)) { /* ignore subsequent result sets */ }
    command.OnCompleted();
    return buffer;
}
else
{
    // can't use ReadAsync / cancellation; but this will have to do
    wasClosed = false; // don't close if handing back an open reader; rely on the command-behavior
    var deferred = ExecuteReaderSync<T>(reader, func, command.Parameters);
    reader = null; // to prevent it being disposed before the caller gets to see it
    return deferred;
}

Come spiega il commento, non è possibile utilizzare ReadAsync quando il tipo restituito dovrebbe essere IEnumerable. Ecco perché è stato necessario introdurre gli enumerabili asincroni di C# 8.

Il codice per ExecuteReaderSync è:

private static IEnumerable<T> ExecuteReaderSync<T>(IDataReader reader, Func<IDataReader, object> func, object parameters)
{
    using (reader)
    {
        while (reader.Read())
        {
            yield return (T)func(reader);
        }
        while (reader.NextResult()) { /* ignore subsequent result sets */ }
        (parameters as IParameterCallbacks)?.OnCompleted();
    }
}

Usa Read invece di ReadAsync .

I flussi asincroni C#8 consentiranno di riscriverlo per restituire un IAsyncEnumerable . Cambiare semplicemente la versione della lingua non risolverà il problema.

Dati i documenti attuali sui flussi asincroni, potrebbe essere simile a:

private static async IAsyncEnumerable<T> ExecuteReaderASync<T>(IDataReader reader, Func<IDataReader, object> func, object parameters)
{
    using (reader)
    {
        while (await reader.ReadAsync())
        {
            yield return (T)func(reader);
        }

        while (await reader.NextResultAsync(cancel).ConfigureAwait(false)) { /* ignore subsequent result sets */ }
         command.OnCompleted();
        (parameters as IParameterCallbacks)?.OnCompleted();
    }
}

Buongiorno i flussi asincroni sono una delle cose che possono funzionare solo su .NET Core e probabilmente non sono ancora implementate. Quando ho provato a scriverne uno in Sharplab.io, Kaboom. [connection lost, reconnecting…]


Nel contesto di dapper in particolare , sì:necessita di un'API diversa come spiegato dall'eccellente risposta di @Panagiotis. Quella che segue non è una risposta in quanto tale, ma è un contesto aggiuntivo che gli implementatori che devono affrontare le stesse sfide potrebbero voler considerare.

Non l'ho ancora "aggiustato" per dapper (anche se ho per SE.Redis), e sono combattuto tra varie opzioni:

  1. aggiungi una nuova API per .NET Core solo , restituendo un tipo enumerabile asincrono appropriato
  2. distruggi completamente l'API esistente come modifica sostanziale (una "importante" ecc.), modificandola per restituire un tipo enumerabile asincrono

Probabilmente andremo con "1", ma devo dire che la seconda opzione è insolitamente allettante, per buoni motivi:

  • L'API esistente probabilmente non fa ciò che la gente si aspetta che faccia
  • vorremmo che il nuovo codice iniziasse a usarlo

Ma la cosa strana è la .NET Core 3.0 di IAsyncEnumerable<T> - poiché ovviamente Dapper non ha come target solo .NET Core 3.0; potremmo:

  1. limita la funzionalità a .NET Core 3.0 e restituisce IAsyncEnumerable<T>
  2. limita la libreria a .NET Core 3.0 e restituisci IAsyncEnumerable<T>
  3. prendere una dipendenza da System.Linq.Async (che non è "ufficiale", ma è abbastanza ufficiale per i nostri scopi) per i framework precedenti e restituire IAsyncEnumerable<T>
  4. restituisce un tipo enumerabile personalizzato che non è in realtà IAsyncEnumerable<T> (ma che implementa IAsyncEnumerable<T> quando disponibile) e implementare manualmente la macchina a stati, la natura tipo anatra di foreach significa che funzionerà bene purché il nostro tipo enumerabile personalizzato fornisca i metodi giusti

Penso che probabilmente vai con l'opzione 3, ma per ribadire:sì, qualcosa deve cambiare.


(Questo dovrebbe essere un commento // non abbastanza reputazione, finora )

Marc Gravell menziona nella sua risposta che IAsyncEnumerable<T> sarebbe preferibile, ma a causa della dipendenza da NET Core 3.0, potrebbe essere meglio prendere una dipendenza da System.Linq.Async (che potrebbe essere considerato "abbastanza ufficiale")...

In questo contesto, mi è venuto in mente https://github.com/Dasync/AsyncEnumerable (licenza MIT):mira ad aiutare

Ancora una frase, RE:"Cosa succede quando viene rilasciato C# 8.0?" (FAQ)