¿Qué sucede con la devolución de IEnumerable si se usa con async/await (transmisión de datos desde SQL Server con Dapper)?

¿Qué sucede con la devolución de IEnumerable si se usa con async/await (transmisión de datos desde SQL Server con Dapper)?

Actualización de marzo de 2020

.NET Core 3.0 (y 3.1) han salido ahora, con soporte completo para flujos asíncronos. Microsoft.Bcl.AsyncInterfaces agrega soporte para ellos a .NET Standard 2.0 y .NET Framework 4.6.1+, aunque 4.7.2 debe usarse por razones de cordura. Como explican los documentos sobre el soporte de implementación de .NET Standard

Respuesta original

Si revisa el código fuente, verá que su sospecha es casi correcta. Cuando buffered es falso, QueryAsync transmitirá sincrónicamente .

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

Como explica el comentario, no es posible usar ReadAsync cuando se espera que el tipo de valor devuelto sea IEnumerable. Es por eso que se tuvieron que introducir los enumerables asíncronos de C# 8.

El código para ExecuteReaderSync es:

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

Utiliza Read en lugar de ReadAsync .

Las transmisiones asíncronas de C#8 permitirán reescribir esto para devolver un IAsyncEnumerable . Simplemente cambiar la versión de idioma no resolverá el problema.

Teniendo en cuenta los documentos actuales sobre transmisiones asíncronas, esto podría verse así:

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

Peeeeeero los flujos asíncronos son una de las cosas que solo pueden funcionar en .NET Core, y probablemente aún no estén implementadas. Cuando traté de escribir uno en Sharplab.io, Kaboom. [connection lost, reconnecting…]


En el contexto de dapper específicamente , sí:necesita una API diferente como lo explica la excelente respuesta de @Panagiotis. Lo que sigue no es una respuesta como tal, pero es un contexto adicional que los implementadores que enfrentan los mismos desafíos pueden considerar.

Todavía no he "aplicado" esto para que sea elegante (aunque tengo para SE.Redis), y estoy dividido entre varias opciones:

  1. añadir una nueva API para .NET Core solo , devolviendo un tipo enumerable asincrónico apropiado
  2. elimine por completo la API existente como un cambio importante (un "principal", etc.), cambiándola para que devuelva un tipo enumerable asíncrono

Probablemente elegiremos "1", pero debo decir que la segunda opción es inusualmente tentadora, por buenas razones:

  • la API existente probablemente no haga lo que la gente espera que haga
  • querríamos un nuevo código para comenzar a usarlo

Pero lo extraño es el .NET Core 3.0-ness de IAsyncEnumerable<T> - como obviamente, Dapper no solo apunta a .NET Core 3.0; podríamos:

  1. limite la función a .NET Core 3.0 y devuelva IAsyncEnumerable<T>
  2. limitar la biblioteca a .NET Core 3.0 y devuelve IAsyncEnumerable<T>
  3. tomar una dependencia de System.Linq.Async (que no es "oficial", pero es lo suficientemente oficial para nuestros propósitos) para los marcos anteriores y devolver IAsyncEnumerable<T>
  4. devuelve un tipo enumerable personalizado que no es en realidad IAsyncEnumerable<T> (pero que implementa IAsyncEnumerable<T> cuando esté disponible), e implemente manualmente la máquina de estado:la naturaleza tipo pato de foreach significa que esto funcionará bien siempre que nuestro tipo enumerable personalizado proporcione los métodos correctos

Creo que probablemente ve con la opción 3, pero para reiterar:sí, algo debe cambiar.


(Se supone que esto es un comentario // No hay suficiente reputación, hasta ahora )

Marc Gravell menciona en su respuesta que IAsyncEnumerable<T> sería preferible, pero debido a la dependencia de NET Core 3.0, podría ser mejor depender de System.Linq.Async (que podría considerarse como "suficientemente oficial")...

En este contexto, me vino a la mente https://github.com/Dasync/AsyncEnumerable (licencia MIT):Su objetivo es ayudar

Una pregunta más, RE:"¿Qué sucede cuando se lanza C# 8.0?" (Preguntas frecuentes)