Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Pass-through for IAsyncEnumerable?

I'd like to know if there's a way I can write a function to "pass through" an IAsyncEnumerable... that is, the function will call another IAsyncEnumerable function and yield all results without having to write a foreach to do it?

I find myself writing this code pattern a lot. Here's an example:

async IAsyncEnumerable<string> MyStringEnumerator();

async IAsyncEnumerable<string> MyFunction()
{
   // ...do some code...

   // Return all elements of the whole stream from the enumerator
   await foreach(var s in MyStringEnumerator())
   {
      yield return s;
   }
}

For whatever reason (due to layered design) my function MyFunction wants to call MyStringEnumerator but then just yield everything without intervention. I have to keep writing these foreach loops to do it. If it were an IEnumerable I would return the IEnumerable. If it were C++ I could write a macro to do it.

What's best practice?

like image 651
Richard Hunt Avatar asked Jan 23 '20 10:01

Richard Hunt


People also ask

How does IAsyncEnumerable work?

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.

What is IAsyncEnumerable?

IAsyncEnumerable<T> Interface Exposes an enumerator that provides asynchronous iteration over values of a specified type.

What is task IEnumerable?

WhenAll(IEnumerable<Task>) Creates a task that will complete when all of the Task objects in an enumerable collection have completed. WhenAll(Task[]) Creates a task that will complete when all of the Task objects in an array have completed.


2 Answers

If it were an IEnumerable I would return the IEnumerable.

Well, you can just do the same thing with IAsyncEnumerable (note that the async is removed):

IAsyncEnumerable<string> MyFunction()
{
 // ...do some code...

 // Return all elements of the whole stream from the enumerator
 return MyStringEnumerator();
}

However, there's an important semantic consideration here. When calling an enumerator method, the ...do some code... will be executed immediately, and not when the enumerator is enumerated.

// (calling code)
var enumerator = MyFunction(); // `...do some code...` is executed here
...
await foreach (var s in enumerator) // it's not executed here when getting the first `s`
  ...

This is true for both synchronous and asynchronous enumerables.

If you want ...do some code... to be executed when the enumerator is enumerated, then you'll need to use the foreach/yield loop to get the deferred execution semantics:

async IAsyncEnumerable<string> MyFunction()
{
 // ...do some code...

 // Return all elements of the whole stream from the enumerator
 await foreach(var s in MyStringEnumerator())
   yield return s;
}

And you would have to use the same pattern in the synchronous world if you wanted deferred execution semantics with a synchronous enumerable, too:

IEnumerable<string> ImmediateExecution()
{
 // ...do some code...

 // Return all elements of the whole stream from the enumerator
 return MyStringEnumerator();
}

IEnumerable<string> DeferredExecution()
{
 // ...do some code...

 // Return all elements of the whole stream from the enumerator
 foreach(var s in MyStringEnumerator())
   yield return s;
}
like image 121
Stephen Cleary Avatar answered Nov 21 '22 04:11

Stephen Cleary


Returning Task<IAsyncEnumerable<Obj>> from the calling method seems to work

async IAsyncEnumerable<string> MyStringEnumerator();

async Task<IAsyncEnumerable<string>> MyFunction()
{
    await Something();

    return MyStringEnumerator();
}

You'll then need to await MyFunction(). So to use in an async foreach would be

await foreach (string s in await MyFunction()) {}
like image 40
Craig Miller Avatar answered Nov 21 '22 06:11

Craig Miller