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?
The benefit of an
IAsyncEnumerable<T>over anIEnumerable<T>is that the former is potentially more scalable, because it doesn't use a thread while is is enumerated. Instead of having a synchronousMoveNextmethod, it has an asynchronousMoveNextAsync. This benefit becomes a moot point when theMoveNextAsyncreturns always an already completedValueTask<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:Most likely you'll discover that all
MoveNextAsyncoperations are completed upon creation, at least some of them have a significantelapsed1value, and all of them have zeroelapsed2values.