Возвращение IAsyncEnumerable <T> и NotFound из основного контроллера Asp.Net

10

Какова правильная подпись для действия контроллера, которое возвращает IAsyncEnumerable<T>и, NotFoundResultно все еще обрабатывается асинхронно?

Я использовал эту подпись, и она не компилируется, потому что IAsyncEnumerable<T>не является ожидаемой:

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

Этот компилируется нормально, но его подпись не асинхронная. Так что я волнуюсь, заблокирует ли он потоки пула потоков или нет:

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

Я попытался использовать await foreachцикл таким образом, но он, очевидно, тоже не скомпилируется:

[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;
    }
}
Фредерик Дурак
источник
5
Вы возвращаете несколько MyObjectпредметов с одинаковыми id? Обычно вы не отправляете NotFoundчто-то, что возвращает IEnumerable- оно будет пустым - или вы возвращаете один элемент с запрошенным id/ NotFound.
Crgolden
1
IAsyncEnumerableожидаемо. Использование await foreach(var item from ThatMethodAsync()){...}.
Панагиотис Канавос
Если вы хотите вернуться IAsyncEnumerable<MyObject>, просто верните результат, например return objects. Это не преобразует действие HTTP в потоковый gRPC или метод SignalR. Промежуточное программное обеспечение будет по-прежнему потреблять данные и отправлять клиенту один HTTP-ответ
Panagiotis Kanavos
Вариант 2 в порядке. Сантехническое оборудование ASP.NET Core заботится о перечислении и IAsyncEnumerableподдерживает версии 3.0.
Кирк Ларкин
Спасибо, парни. Я знаю, что здесь я не получу 404, но это только надуманный пример. Фактический код совсем другой. @KirkLarkin извините за вредителей, но вы на 100% уверены, что это не приведет к блокировке? Если да, то вариант 2 является очевидным решением.
Фредерик Дурак

Ответы:

6

Вариант 2, который передает реализацию IAsyncEnumerable<>в Okвызов, подойдет. Сантехническое оборудование ASP.NET Core заботится о перечислении и IAsyncEnumerable<>поддерживает версии 3.0.

Вот ответ на вопрос, повторенный для контекста:

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

Вызов Okсоздает экземпляр OkObjectResult, который наследуется ObjectResult. Значение , переданное в Okимеет тип object, который проходит в ObjectResult«S Valueсобственности. ASP.NET Core MVC использует шаблон команды , в которой команда является реализацией IActionResultи выполняется с использованием реализации IActionResultExecutor<T>.

Для ObjectResult, ObjectResultExecutorиспользуется, чтобы превратить ObjectResultв ответ HTTP. Это реализация того, ObjectResultExecutor.ExecuteAsyncчто 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);
}

Как показывает код, Valueсвойство проверяется, чтобы увидеть, реализует ли оно IAsyncEnumerable<>(подробности скрыты в вызове TryGetReader). Если это так, ExecuteAsyncEnumerableвызывается, который выполняет перечисление, а затем передает перечисленный результат в 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);
}

readerв приведенном фрагменте, где происходит перечисление. Это похоронено немного, но вы можете увидеть источник здесь :

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

IAsyncEnumerable<>Перечисляется в List<>использовании await foreach, что, почти по определению, не блокирует поток запроса. Как Panagiotis Kanavos выкрикнул в комментарии к OP, это перечисление выполняется полностью перед отправкой ответа клиенту.

Кирк Ларкин
источник
Спасибо за подробный ответ Кирк :). Одна проблема, которую я имею, связана с самим методом действия в варианте 2. Я понял, что его возвращаемое значение будет перечисляться асинхронно, но он не возвращает Taskобъект. Этот факт сам по себе мешает асинхронности каким-либо образом? Особенно по сравнению, скажем, с аналогичным методом, который возвращает Task.
Фредерик Дурак
1
Нет, все в порядке. Нет причин возвращать a Task, поскольку сам метод не выполняет асинхронную работу. Это перечисление асинхронное, которое обрабатывается, как описано выше. Вы можете видеть, что Taskиспользуется там, в исполнении ObjectResult.
Кирк Ларкин