Zwracanie IAsyncEnumerable <T> i NotFound z kontrolera Asp.Net Core

10

Jaki jest prawidłowy podpis dla akcji kontrolera, która zwraca IAsyncEnumerable<T>i a, NotFoundResultale jest nadal przetwarzana w sposób asynchroniczny?

Użyłem tego podpisu i nie można go skompilować, ponieważ IAsyncEnumerable<T>nie jest to oczekiwane:

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

Ten kompiluje się dobrze, ale jego podpis nie jest asynchroniczny. Martwię się więc, czy zablokuje wątki puli wątków, czy nie:

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

Próbowałem użyć await foreachpętli w ten sposób, ale to oczywiście nie skompiluje:

[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
źródło
5
Zwracasz wiele MyObjectprzedmiotów z tym samym id? Zwykle nie wysyłałbyś NotFoundza coś, co zwraca IEnumerable- byłby po prostu pusty - lub zwracałbyś pojedynczy przedmiot z żądanym id/ NotFound.
crgolden
1
IAsyncEnumerablejest oczekiwany. Zastosowanie await foreach(var item from ThatMethodAsync()){...}.
Panagiotis Kanavos
Jeśli chcesz zwrócić IAsyncEnumerable<MyObject>, po prostu zwróć wynik, np return objects. Że nie będzie konwertować działanie HTTP streaming gRPC lub SignalR metody chociaż. Oprogramowanie pośrednie nadal będzie pobierać dane i wysyłać pojedynczą odpowiedź HTTP do klienta
Panagiotis Kanavos
Opcja 2 jest w porządku. Instalacja hydrauliczna programu ASP.NET Core zajmuje się wyliczaniem i jest dostępna IAsyncEnumerableod wersji 3.0.
Kirk Larkin
Dzięki chłopaki. Wiem, że nie mam tutaj zwrotu 404, ale to tylko wymyślony przykład. Rzeczywisty kod jest zupełnie inny. @KirkLarkin przepraszam, że jestem szkodnikiem, ale czy jesteś w 100% pewien, że nie spowoduje to blokowania? Jeśli tak, to opcja 2 jest oczywistym rozwiązaniem.
Frederick The Fool

Odpowiedzi:

6

Wariant 2, która przechodzi implementację IAsyncEnumerable<>do Okrozmowy, jest w porządku. Instalacja hydrauliczna programu ASP.NET Core zajmuje się wyliczaniem i jest dostępna IAsyncEnumerable<>od wersji 3.0.

Oto wezwanie z pytania, powtórzone dla kontekstu:

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

Wywołanie do Oktworzy instancję OkObjectResult, która dziedziczy ObjectResult. Wartość przekazana w celu Okjest typu object, który odbywa się w ObjectResult„s Valuenieruchomości. ASP.NET Core MVC używa wzorca polecenia , przy czym polecenie jest implementacją IActionResulti jest wykonywane przy użyciu implementacji IActionResultExecutor<T>.

Na ObjectResult, ObjectResultExecutorjest używany, aby włączyć ObjectResultdo odpowiedzi HTTP. Jest to realizacja stanowi ObjectResultExecutor.ExecuteAsyncto 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);
}

Jak pokazuje kod, Valuewłaściwość jest sprawdzana pod kątem implementacji IAsyncEnumerable<>(szczegóły są ukryte w wywołaniu do TryGetReader). Jeśli tak, ExecuteAsyncEnumerablejest wywoływany, który wykonuje wyliczenie, a następnie przekazuje wyliczony wynik do 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);
}

readerw powyższym fragmencie znajduje się miejsce, w którym występuje wyliczenie. Jest trochę pochowany, ale możesz zobaczyć źródło tutaj :

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

Jest IAsyncEnumerable<>on wyliczany na wartość List<>using await foreach, która prawie z definicji nie blokuje wątku żądania. Jak Panagiotis Kanavos zawołał w komentarzu do PO, wyliczenie to jest wykonywane w całości przed odesłaniem odpowiedzi do klienta.

Kirk Larkin
źródło
Dzięki za szczegółową odpowiedź Kirk :). Jedną z moich obaw jest sama metoda akcji w opcji 2. Zrozumiałam, że jej zwracana wartość będzie wyliczana asynchronicznie, ale nie zwraca Taskobiektu. Czy sam fakt w jakikolwiek sposób utrudnia asynchroniczność? Zwłaszcza w porównaniu do, powiedzmy, podobnej metody, która zwróciła a Task.
Frederick The Fool
1
Nie, w porządku. Nie ma powodu, aby zwrócić a Task, ponieważ sama metoda nie wykonuje żadnej pracy asynchronicznej. Jest to wyliczenie asynchroniczne, które jest obsługiwane jak opisano powyżej. Możesz zobaczyć, że Taskjest tam używany, podczas wykonywania ObjectResult.
Kirk Larkin