I have the following interface:
public interface IValidationSystem<T>
{
IAsyncEnumerable<ValidationResult> ValidateAsync(T obj);
}
And I am trying to implement it this way:
public class Foo
{ }
public class Bar
{ }
public class BarValidationSystem : IValidationSystem<T>
{
public async IAsyncEnumerable<ValidationResult> ValidateAsync(Bar bar)
{
var foo = await GetRequiredThingAsync();
return GetErrors(bar, foo).Select(e => new ValidationResult(e)).ToAsyncEnumerable();
}
private static IEnumerable<string> GetErrors(Bar bar, Foo foo)
{
yield return "Something is wrong";
yield return "Oops something else is wrong";
yield return "And eventually, this thing is wrong too";
}
private Task<Foo> GetRequiredThingAsync()
{
return Task.FromResult(new Foo());
}
}
But this does not compile:
CS1622 Cannot return a value from an iterator. Use the yield return statement to return a value, or yield break to end the iteration.
I know I can fix by iterating the enumerable:
foreach (var error in GetErrors(bar, foo))
{
yield return new ValidationResult(error);
}
Or by returning a Task<IEnumerable<ValidationResult>>
:
public async Task<IEnumerable<ValidationResult>> ValidateAsync(Bar bar)
{
var foo = await GetRequiredThingAsync;
return GetErrors(bar, foo).Select(e => new ValidationResult(e));
}
But I would like to understand why I cannot return an IAsyncEnumerable
in my case. 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
?
IAsyncEnumerable<T> exposes an enumerator that has a MoveNextAsync() method that can be awaited. This means a method that produces this result can make asynchronous calls in between yielding results. Cool! This method can now yield data asynchronously.
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#:
async
or yield
present.yield
in the body without async
. Must return IEnumerable<T>
(or IEnumerator<T>
).async
is present without yield
. Must return a tasklike.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.
The usual syntax for an async method is to return a Task:
public async Task<Foo> GetFooAsync()
{
/...
}
If you make it async
but not return a Task<T>
then the compiler will flag an error in the header.
There is an exception: an iterator method that returns IAsyncEnumerable
private static async IAsyncEnumerable<int> ThisShouldReturnAsTask()
{
yield return 0;
await Task.Delay(100);
yield return 1;
}
In your example, you have an async function that returns IAsyncEnumerable
, but is NOT an iterator. (It just returns a straight enumerable object, instead of yielding values one-by-one.)
Hence the error: "Cannot return a value from an iterator"
If you changed the signature to return Task<IAsyncEnumerable<ValidationResult>>
public async Task<IAsyncEnumerable<ValidationResult>> ValidateAsync(Bar bar)
{
var foo = await GetRequiredThingAsync();
return GetErrors(bar, foo).Select(e => new ValidationResult(e)).ToAsyncEnumerable();
}
then you will need to change the way you invoke it: it would have to be
await foreach(var f in await ValidateAsync(new Bar()))
Instead of
await foreach(var f in ValidateAsync(new Bar()))
See examples to play with here: https://dotnetfiddle.net/yPTdqp
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With