Devolver IAsyncEnumerable <T> y NotFound desde Asp.Net Core Controller

10

¿Cuál es la firma correcta para una acción de controlador que devuelve un IAsyncEnumerable<T>y a NotFoundResultpero que aún se procesa de forma asíncrona?

IAsyncEnumerable<T>Usé esta firma y no se compila porque no está a la espera:

[HttpGet]
public async Task<IActionResult> GetAll(Guid id)
{
    try
    {
        return Ok(await repository.GetAll(id)); // GetAll() returns an IAsyncEnumerable
    }
    catch (NotFoundException e)
    {
        return NotFound(e.Message);
    }
}

Este se compila bien pero su firma no es asíncrona. Entonces me preocupa si bloqueará los hilos del grupo de hilos o no:

[HttpGet]
public IActionResult GetAll(Guid id)
{
    try
    {
        return Ok(repository.GetAll(id)); // GetAll() returns an IAsyncEnumerable
    }
    catch (NotFoundException e)
    {
        return NotFound(e.Message);
    }
}

Intenté usar un await foreachbucle como este, pero eso obviamente tampoco compilaría:

[HttpGet]
public async IAsyncEnumerable<MyObject> GetAll(Guid id)
{
    IAsyncEnumerable<MyObject> objects;
    try
    {
        objects = contentDeliveryManagementService.GetAll(id); // GetAll() returns an IAsyncEnumerable
    }
    catch (DeviceNotFoundException e)
    {
        return NotFound(e.Message);
    }

    await foreach (var obj in objects)
    {
        yield return obj;
    }
}
Federico el tonto
fuente
55
¿Estás devolviendo varios MyObjectartículos con lo mismo id? Normalmente, no enviaría un NotFoundpor algo que devuelve un IEnumerable, simplemente estaría vacío, o devolvería el artículo individual con el id/ solicitado NotFound.
crgolden
1
IAsyncEnumerableEstá a la espera. Uso await foreach(var item from ThatMethodAsync()){...}.
Panagiotis Kanavos
Si desea regresar IAsyncEnumerable<MyObject>, simplemente devuelva el resultado, por ejemplo return objects. Sin embargo, eso no convertirá una acción HTTP en un método gRPC o SignalR de transmisión. El middleware seguirá consumiendo los datos y enviará una única respuesta HTTP al cliente
Panagiotis Kanavos
La opción 2 está bien. La fontanería ASP.NET Core se encarga de la enumeración y tiene IAsyncEnumerableconocimiento de 3.0.
Kirk Larkin
Gracias chicos. Sé que no he devuelto un 404 aquí, pero este es solo un ejemplo artificial. El código real es bastante diferente. @KirkLarkin lamento ser una plaga, pero ¿estás 100% seguro de que esto no causará ningún bloqueo? En caso afirmativo, la opción 2 es la solución obvia.
Frederick The Fool

Respuestas:

6

La opción 2, que pasa una implementación IAsyncEnumerable<>a la Okllamada, está bien. La fontanería ASP.NET Core se encarga de la enumeración y tiene IAsyncEnumerable<>conocimiento de 3.0.

Aquí está la llamada de la pregunta, repetida para el contexto:

return Ok(repository.GetAll(id)); // GetAll() returns an IAsyncEnumerable

La llamada a Okcrea una instancia de OkObjectResult, que hereda ObjectResult. El valor pasado a Okes de tipo object, que se celebra en el ObjectResult's Valuepropiedad. ASP.NET Core MVC usa el patrón de comando , mediante el cual el comando es una implementación de IActionResulty se ejecuta usando una implementación de IActionResultExecutor<T>.

Para ObjectResult, ObjectResultExecutorse utiliza para convertir el ObjectResulten una respuesta HTTP. Es la implementación de ObjectResultExecutor.ExecuteAsynceso es IAsyncEnumerable<>-aware:

public virtual Task ExecuteAsync(ActionContext context, ObjectResult result)
{
    // ...

    var value = result.Value;

    if (value != null && _asyncEnumerableReaderFactory.TryGetReader(value.GetType(), out var reader))
    {
        return ExecuteAsyncEnumerable(context, result, value, reader);
    }

    return ExecuteAsyncCore(context, result, objectType, value);
}

Como muestra el código, la Valuepropiedad se verifica para ver si se implementa IAsyncEnumerable<>(los detalles están ocultos en la llamada a TryGetReader). Si lo hace, ExecuteAsyncEnumerablese llama, que realiza la enumeración y luego pasa el resultado enumerado a ExecuteAsyncCore:

private async Task ExecuteAsyncEnumerable(ActionContext context, ObjectResult result, object asyncEnumerable, Func<object, Task<ICollection>> reader)
{
    Log.BufferingAsyncEnumerable(Logger, asyncEnumerable);

    var enumerated = await reader(asyncEnumerable);
    await ExecuteAsyncCore(context, result, enumerated.GetType(), enumerated);
}

readeren el fragmento anterior es donde ocurre la enumeración. Está enterrado un poco, pero puedes ver la fuente aquí :

private async Task<ICollection> ReadInternal<T>(object value)
{
    var asyncEnumerable = (IAsyncEnumerable<T>)value;
    var result = new List<T>();
    var count = 0;

    await foreach (var item in asyncEnumerable)
    {
        if (count++ >= _mvcOptions.MaxIAsyncEnumerableBufferLimit)
        {
            throw new InvalidOperationException(Resources.FormatObjectResultExecutor_MaxEnumerationExceeded(
                nameof(AsyncEnumerableReader),
                value.GetType()));
        }

        result.Add(item);
    }

    return result;
}

El IAsyncEnumerable<>se enumera en un List<>uso await foreach, que, casi por definición, no bloquea un hilo de solicitud. Como Panagiotis Kanavos llamó en un comentario sobre el OP, esta enumeración se realiza en su totalidad antes de que se envíe una respuesta al cliente.

Kirk Larkin
fuente
Gracias por la respuesta detallada Kirk :). Una preocupación que tengo es con el método de acción en sí en la opción 2. Comprendí que su valor de retorno se enumerará de forma asincrónica, pero no devuelve un Taskobjeto. ¿Ese hecho en sí mismo obstaculiza la asincronía de alguna manera? Especialmente comparado con, digamos, un método similar que devolvió a Task.
Frederick The Fool
1
No, está bien. No hay razón para devolver a Task, porque el método en sí no realiza ningún trabajo asincrónico. La enumeración es asincrónica, que se maneja como se describe anteriormente. Puede ver que Taskse usa allí, en la ejecución de ObjectResult.
Kirk Larkin