When to convert IEnumerable to IAsyncEnumerable

2.3k views Asked by At

In the .NET documentation for Controller Action Return Types (doc link), it shows this example on how to return a async response stream:

[HttpGet("asyncsale")]
public async IAsyncEnumerable<Product> GetOnSaleProductsAsync()
{
    var products = _productContext.Products.OrderBy(p => p.Name).AsAsyncEnumerable();

    await foreach (var product in products)
    {
        if (product.IsOnSale)
        {
            yield return product;
        }
    }
}

In the example above, _productContext.Products.OrderBy(p => p.Name).AsAsyncEnumerable() converts the returned IQueryable<Product> into an IAsyncEnumerable. But the below example also works and streams the response asycnhronously.

[HttpGet("asyncsale")]
public async IAsyncEnumerable<Product> GetOnSaleProductsAsync()
{
    var products = _productContext.Products.OrderBy(p => p.Name);

    foreach (var product in products)
    {
        if (product.IsOnSale)
        {
            yield return product;
        }
    }

    await Task.CompletedTask;
}

What's the reason for converting to IAsyncEnumerable first and doing await on the foreach? Is it simply for easier syntax or are there benefits of doing so?

Is there a benefit to converting any IEnumerable into IAsyncEnumerable, or only if the underlying IEnumerable is also streamable, for example through yield? If I have a list fully loaded into memory already, is it pointless to convert it into an IAsyncEnumerable?

2

There are 2 answers

3
Theodor Zoulias On BEST ANSWER

The benefit of an IAsyncEnumerable<T> over an IEnumerable<T> is that the former is potentially more scalable, because it doesn't use a thread while is is enumerated. Instead of having a synchronous MoveNext method, it has an asynchronous MoveNextAsync. This benefit becomes a moot point when the MoveNextAsync returns always an already completed ValueTask<bool> (enumerator.MoveNextAsync().IsCompleted == true), in which case you have just a synchronous enumeration masqueraded as asynchronous. There is no scalability benefit in this case. Which is exactly what's happening in the code shown in the question. You have the chassis of a Porsche, with a Trabant engine hidden under the hood.

If you want to obtain a deeper understanding of what's going on, you can enumerate the asynchronous sequence manually instead of the convenient await foreach, and collect debugging information regarding each step of the enumeration:

[HttpGet("asyncsale")]
public async IAsyncEnumerable<Product> GetOnSaleProductsAsync()
{
    var products = _productContext.Products.OrderBy(p => p.Name).AsAsyncEnumerable();

    Stopwatch stopwatch = new();
    await using IAsyncEnumerator<Product> enumerator = products.GetAsyncEnumerator();
    while (true)
    {
        stopwatch.Restart();
        ValueTask<bool> moveNextTask = enumerator.MoveNextAsync();
        TimeSpan elapsed1 = stopwatch.Elapsed;
        bool isCompleted = moveNextTask.IsCompleted;
        stopwatch.Restart();
        bool moved = await moveNextTask;
        TimeSpan elapsed2 = stopwatch.Elapsed;
        Console.WriteLine($"Create: {elapsed1}, Completed: {isCompleted}, Await: {elapsed2}");
        if (!moved) break;

        Product product = enumerator.Current;
        if (product.IsOnSale)
        {
            yield return product;
        }
    }
}

Most likely you'll discover that all MoveNextAsync operations are completed upon creation, at least some of them have a significant elapsed1 value, and all of them have zero elapsed2 values.

0
Peter Csala On

What's the reason for converting to IAsyncEnumerable first and doing await on the foreach?

If you want to take advantage of the async I/O. Whenever you fetch data from a database in case of IEnumerable it is a blocking operation. The calling thread (your foreach) have to wait until the database response arrives. While in case of IAsyncEnumerable the caller thread (your await foreach) can be assigned to a different request. So, it provides better scalability.

Is it simply for easier syntax or are there benefits of doing so?

If the next item is (most likely) not available yet and you need to perform an I/O operation to fetch it then you can free up in the meanwhile the (otherwise blocking) caller thread.

Is there a benefit to converting any IEnumerable into IAsyncEnumerable, or only if the underlying IEnumerable is also streamable, for example through yield?

In your particular example you have two IAsyncEnumerables. Your datasource and your http response. You don't have to use both. It is absolutely okay to stream an IEnumerable. It is also okay to fetch asynchronously the datasource and return the result whenever all the data is available.

If I have a list fully loaded into memory already, is it pointless to convert it into an IAsyncEnumerable?

Well, if you want to stream the response then yes it is not needed to asynchronously fetch the datasource.