Retornando IAsyncEnumerable <T> e NotFound do Asp.Net Core Controller

10

Qual é a assinatura certa para uma ação do controlador que retorna um IAsyncEnumerable<T>e um NotFoundResultmas ainda é processada de maneira assíncrona?

Eu usei essa assinatura e ela não é compilada porque IAsyncEnumerable<T>não é possível esperar:

[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 compila bem, mas sua assinatura não é assíncrona. Então, eu estou preocupado se ele irá bloquear os threads do pool de threads ou não:

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

Eu tentei usar um await foreachloop assim, mas que obviamente também não seria compilado:

[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;
    }
}
Frederick The Fool
fonte
5
Você está retornando vários MyObjectitens com o mesmo id? Normalmente, você não enviaria um NotFoundpara algo que retorne um IEnumerable- apenas estaria vazio - ou você retornaria o item único com o id/ solicitado NotFound.
crgolden 21/01
11
IAsyncEnumerableé aguardável. Use await foreach(var item from ThatMethodAsync()){...}.
Panagiotis Kanavos 21/01
Se você deseja retornar IAsyncEnumerable<MyObject>, basta retornar o resultado, por exemplo return objects. Isso não converterá uma ação HTTP em um método gRPC ou SignalR de streaming. O middleware ainda consumirá os dados e enviará uma única resposta HTTP ao cliente
Panagiotis Kanavos
A opção 2 está correta. O encanamento do ASP.NET Core cuida da enumeração e é IAsyncEnumerablereconhecido como 3.0.
Kirk Larkin
Obrigado rapazes. Eu sei que não retornei um 404 aqui, mas este é apenas um exemplo artificial. O código real é bem diferente. @KirkLarkin desculpe ser uma praga, mas você tem 100% de certeza de que isso não causará nenhum bloqueio? Se sim, a opção 2 é a solução óbvia.
Frederick The Fool

Respostas:

6

A opção 2, que passa uma implementação de IAsyncEnumerable<>para a Okchamada, está correta. O encanamento do ASP.NET Core cuida da enumeração e é IAsyncEnumerable<>reconhecido como 3.0.

Aqui está a chamada da pergunta, repetida para o contexto:

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

A chamada para Okcria uma instância de OkObjectResult, que herda ObjectResult. O valor passado para Oké do tipo object, que é realizada na ObjectResult's Valuepropriedade. O ASP.NET Core MVC usa o padrão de comando , pelo qual o comando é uma implementação IActionResulte é executado usando uma implementação de IActionResultExecutor<T>.

For ObjectResult, ObjectResultExecutoré usado para transformar ObjectResultem uma resposta HTTP. É a implementação do ObjectResultExecutor.ExecuteAsyncque é 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 o código mostra, a Valuepropriedade é verificada para ver se é implementada IAsyncEnumerable<>(os detalhes estão ocultos na chamada para TryGetReader). Se sim, ExecuteAsyncEnumerableé chamado, que executa a enumeração e passa o resultado enumerado para 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);
}

readerno trecho acima é onde ocorre a enumeração. Está enterrado um pouco, mas você pode ver a fonte aqui :

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

O IAsyncEnumerable<>é enumerado em uma List<>utilização await foreach, que, quase por definição, não bloqueia um encadeamento de solicitação. Como Panagiotis Kanavos chamou em um comentário sobre o OP, essa enumeração é realizada na íntegra antes de uma resposta ser enviada de volta ao cliente.

Kirk Larkin
fonte
Obrigado pela resposta detalhada Kirk :). Uma preocupação que tenho é com o próprio método de ação na opção 2. Entendi que seu valor de retorno será enumerado de forma assíncrona, mas ele não retorna um Taskobjeto. Esse fato em si dificulta a assincronicidade? Especialmente comparado a, digamos, um método semelhante que retornou a Task.
Frederick The Fool
11
Não, está tudo bem. Não há motivo para retornar a Task, porque o próprio método não realiza nenhum trabalho assíncrono. É a enumeração assíncrona, que é tratada como descrito acima. Você pode ver que Taské usado lá, na execução do ObjectResult.
Kirk Larkin