Mengembalikan IAsyncEnumerable <T> dan NotFound dari Asp.Net Core Controller

10

Apa tanda tangan yang tepat untuk tindakan pengontrol yang mengembalikan IAsyncEnumerable<T>dan NotFoundResulttetapi masih diproses secara async?

Saya menggunakan tanda tangan ini dan tidak dapat dikompilasi karena IAsyncEnumerable<T>tidak bisa ditunggu-tunggu:

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

Yang ini kompilasi dengan baik tetapi tanda tangannya tidak async. Jadi saya khawatir apakah itu akan memblokir thread pool thread atau tidak:

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

Saya mencoba menggunakan await foreachloop seperti ini tapi itu jelas tidak mau dikompilasi:

[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
sumber
5
Anda mengembalikan beberapa MyObjectitem dengan barang yang sama id? Biasanya Anda tidak akan mengirim NotFounduntuk sesuatu yang mengembalikan IEnumerable- itu hanya akan kosong - atau Anda akan mengembalikan satu item dengan yang diminta id/ NotFound.
crgolden
1
IAsyncEnumerablesudah bisa ditunggu. Gunakan await foreach(var item from ThatMethodAsync()){...}.
Panagiotis Kanavos
Jika Anda ingin kembali IAsyncEnumerable<MyObject>, cukup kembalikan hasilnya, mis return objects. Itu tidak akan mengubah aksi HTTP menjadi streaming gRPC atau metode SignalR. Middleware masih akan mengkonsumsi data dan mengirim satu respons HTTP ke klien
Panagiotis Kanavos
Opsi 2 baik-baik saja. ASP.NET Core plumbing menangani enumerasi dan IAsyncEnumerable-sadar mulai 3.0.
Kirk Larkin
Terima kasih kawan Saya tahu saya tidak harus mengembalikan 404 di sini, tapi ini hanya contoh yang dibuat-buat. Kode sebenarnya sangat berbeda. @KirkLarkin maaf menjadi hama, tetapi apakah Anda 100% yakin ini tidak akan menyebabkan pemblokiran? Jika ya, maka Opsi 2 adalah solusi yang jelas.
Frederick The Fool

Jawaban:

6

Opsi 2, yang melewati sebuah implementasi dari IAsyncEnumerable<>ke Okpanggilan, baik-baik saja. ASP.NET Core plumbing menangani enumerasi dan IAsyncEnumerable<>-sadar mulai 3.0.

Inilah panggilan dari pertanyaan, diulang untuk konteks:

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

Panggilan untuk Okmembuat turunan dari OkObjectResult, yang mewarisi ObjectResult. Nilai yang dikirimkan ke Okadalah tipe object, yang diadakan di ObjectResult's Valueproperti. ASP.NET Core MVC menggunakan pola perintah , di mana perintah adalah implementasi IActionResultdan dieksekusi menggunakan implementasi IActionResultExecutor<T>.

Sebab ObjectResult, ObjectResultExecutordigunakan untuk mengubah ObjectResultrespon HTTP menjadi. Ini implementasi dari ObjectResultExecutor.ExecuteAsync- IAsyncEnumerable<>sadar:

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

Seperti yang ditunjukkan kode, Valueproperti diperiksa untuk melihat apakah itu diterapkan IAsyncEnumerable<>(detailnya disembunyikan dalam panggilan ke TryGetReader). Jika ya, ExecuteAsyncEnumerabledipanggil, yang melakukan enumerasi dan kemudian meneruskan hasil enumerasi ke 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);
}

readerdalam cuplikan di atas adalah tempat enumerasi terjadi. Terkubur sedikit, tetapi Anda dapat melihat sumbernya di sini :

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

The IAsyncEnumerable<>adalah disebutkan menjadi List<>menggunakan await foreach, yang, hampir menurut definisi, tidak memblokir thread permintaan. Seperti Panagiotis Kanavos memanggil dalam komentar di OP, enumerasi ini dilakukan secara penuh sebelum tanggapan dikirim kembali ke klien.

Kirk Larkin
sumber
Terima kasih untuk jawaban terinci Kirk :). Satu masalah yang saya miliki adalah dengan metode tindakan itu sendiri dalam opsi 2. Saya mengerti bahwa nilai kembalinya akan dihitung secara asinkron, tetapi tidak mengembalikan Taskobjek. Apakah fakta itu sendiri menghambat asinkronisitas? Terutama dibandingkan dengan, katakanlah, metode serupa yang mengembalikan a Task.
Frederick The Fool
1
Tidak, tidak apa-apa. Tidak ada alasan untuk mengembalikan a Task, karena metode itu sendiri tidak melakukan pekerjaan asinkron. Enumerasi yang asinkron, yang ditangani seperti dijelaskan di atas. Anda dapat melihat bahwa Taskdigunakan di sana, dalam pelaksanaan ObjectResult.
Kirk Larkin