This looks like a bug or at least an unintentional limitation, when reading the spec proposal.
The spec states that the presence of yield
results in an iterator method; and the presence of both async
and yield
results in an asynchronous iterator method.
But I would like to understand why I cannot return an IAsyncEnumerable in my case.
The async
keyword is making this into an asynchronous iterator method. Since you need the async
for the await
, then you'll need to use yield
as well.
When writing "classic" IEnumerable methods, you can either return an IEnumerable or yield return several values. Why am I not allowed to do the same with IAsyncEnumerable?
With both IEnumerable<T>
and IAsyncEnumerable<T>
, you can perform synchronous work before returning the enumerable directly. In this case, the method is not special at all; it just does some work and then returns a value to its caller.
But you can't do asynchronous work before returning an asynchronous enumerator. In this case, you need the async
keyword. Adding the async
keyword forces the method to either be an asynchronous method or an asynchronous iterator method.
To put it another way, all methods can be classified into these different types in C#:
- Normal methods. No
async
or yield
present.
- Iterator methods. A
yield
in the body without async
. Must return IEnumerable<T>
(or IEnumerator<T>
).
- Asynchronous methods. An
async
is present without yield
. Must return a tasklike.
- Asynchronous iterator methods. Both
async
and yield
are present. Must return IAsyncEnumerable<T>
(or IAsyncEnumerator<T>
).
From yet another perspective, consider the state machine that must be used to implement such a method, and especially think about when the await GetRequiredThingAsync()
code runs.
In the synchronous world without yield
, GetRequiredThing()
would run before returning the enumerable. In the synchronous world with yield
, GetRequiredThing()
would run when the first item of the enumerable is requested.
In the asynchronous world without yield
, await GetRequiredThingAsync()
would run before returning the async enumerable (and in that case, the return type would be Task<IAsyncEnumerable<T>>
, since you have to do asynchronous work to get the async enumerable). In the asynchronous world with yield
, await GetRequiredThingAsync()
would run when the first item of the enumerable is requested.
Generally speaking, the only case when you want to do work before returning the enumerable is when you're doing precondition checks (which are synchronous by nature). Doing an API/DB call is not normal; most of the time the expected semantics are that any API/DB calls will be done as part of enumeration. In other words, even the synchronous code probably should have been using foreach
and yield
, just like the asynchronous code is forced to do.
On a side note, it would be nice in these scenarios to have a yield*
for both synchronous and asynchronous iterators, but C# does not support that.