13

I was playing around with C# 8.0 preview and can't get IAsyncEnumerable to work.

I tried the following

public static async IAsyncEnumerable<int> Get()
{
    for(int i=0; i<10; i++)
    {
        await Task.Delay(100);
        yield return i;
    }
}

I ended up using a Nuget package named AsyncEnumerator, but I'm getting the following error:

  1. Error CS1061 'IAsyncEnumerable<int>' does not contain a definition for 'GetAwaiter' and no accessible extension method 'GetAwaiter' accepting a first argument of type 'IAsyncEnumerable<int>' could be found (are you missing a using directive or an assembly reference?)
  2. Error CS1624 The body of 'Program.Get()' cannot be an iterator block because 'IAsyncEnumerable<int>' is not an iterator interface type

What am I missing here?

svick
  • 236,525
  • 50
  • 385
  • 514
Alen Alex
  • 897
  • 4
  • 15
  • 31
  • 2
    "I ended up using a Nuget package named AsyncEnumerator" - was that following instructions, or just because it sounded like it was probably right? There have been lots of async sequence packages with somewhat-incompatible models. Unless this is a package which is *meant* to work with C# 8.0, I'd be surprised if it did. – Jon Skeet Dec 06 '18 at 12:22
  • what version of Visual Studio do you use? – Sanan Fataliyev Dec 06 '18 at 12:24
  • @AlenAlex it's a bug in the compiler. No package is needed. The problem isn't specific to Visual Studio. A project created from the command line will throw the same errors when you compile with `dotnet build` – Panagiotis Kanavos Dec 06 '18 at 12:27
  • @SananFataliyev I'm using vs 2019 preview – Alen Alex Dec 06 '18 at 12:28

2 Answers2

14

That's a bug in the compiler that can be fixed by adding a few lines of code found here :

namespace System.Threading.Tasks
{
    using System.Runtime.CompilerServices;
    using System.Threading.Tasks.Sources;

    internal struct ManualResetValueTaskSourceLogic<TResult>
    {
        private ManualResetValueTaskSourceCore<TResult> _core;
        public ManualResetValueTaskSourceLogic(IStrongBox<ManualResetValueTaskSourceLogic<TResult>> parent) : this() { }
        public short Version => _core.Version;
        public TResult GetResult(short token) => _core.GetResult(token);
        public ValueTaskSourceStatus GetStatus(short token) => _core.GetStatus(token);
        public void OnCompleted(Action<object> continuation, object state, short token, ValueTaskSourceOnCompletedFlags flags) => _core.OnCompleted(continuation, state, token, flags);
        public void Reset() => _core.Reset();
        public void SetResult(TResult result) => _core.SetResult(result);
        public void SetException(Exception error) => _core.SetException(error);
    }
}

namespace System.Runtime.CompilerServices
{
    internal interface IStrongBox<T> { ref T Value { get; } }
}

As Mads Torgersen explains in Take C# 8 for a spin :

But if you try compiling and running it, you get an embarassing number of errors. That’s because we messed up a bit, and didn’t get the previews of .NET Core 3.0 and Visual Studio 2019 perfectly aligned. Specifically, there’s an implementation type that async iterators leverage that’s different from what the compiler expects.

You can fix this by adding a separate source file to your project, containing this bridging code. Compile again, and everything should work just fine.

Update

Looks there's another bug when Enumerable.Range() is used inside the async iterator.

The GetNumbersAsync() method in the issue ends after only two iterations :

static async Task Main(string[] args)
{
    await foreach (var num in GetNumbersAsync())
    {
        Console.WriteLine(num);
    }
}

private static async IAsyncEnumerable<int> GetNumbersAsync()
{
    var nums = Enumerable.Range(0, 10);
    foreach (var num in nums)
    {
        await Task.Delay(100);
        yield return num;
    }
}

This will print only :

0
1

This won't happen with an array or even another iterator method:

private static async IAsyncEnumerable<int> GetNumbersAsync()
{
    foreach (var num in counter(10))
    {
        await Task.Delay(100);
        yield return num;
    }
}

private static IEnumerable<int> counter(int count)
{
    for(int i=0;i<count;i++)
    {
        yield return i;
    }
}

This will print the expected :

0
1
2
3
4
5
6
7
8
9

Update 2

Seems that's a know bug as well: Async-Streams: iteration stops early on Core

Community
  • 1
  • 1
Panagiotis Kanavos
  • 120,703
  • 13
  • 188
  • 236
  • Au contraire, both your code snippets are functioning as expected. That's because you're materializing your `IEnumerable` to an `int[]` just before the `foreach` which makes the problem go away. Since that's not always a good idea (for instance, maybe you've got gigabytes of records coming into your proces from elsewhere) check my answer as it provides a small workaround which uses resources carefully. – Eduard Dumitru Dec 07 '18 at 22:52
  • @EduardDumitru I won't argue with the .NET Core team which considers this a bug. I already showed that working with an *array* works OK. Your code doesn't use `Enumerable.Range()` in the loop, it uses an array – Panagiotis Kanavos Dec 10 '18 at 08:28
0

Regarding the bridging code needed to make Async enumerables work, I published a NuGet a couple of days ago that does just that: CSharp8Beta.AsyncIteratorPrerequisites.Unofficial

Contrary to popular belief, the following code actually produces the expected results:

private static async IAsyncEnumerable<int> GetNumbersAsync()
{
    var nums = Enumerable.Range(0, 10).ToArray();
    foreach (var num in nums)
    {
        await Task.Delay(100);
        yield return num;
    }
}

and that is because the IEnumerable<int> is being materialized into an int array. What would actually terminate after two iterations is iterating over the IEnumerable<int> itself like so:

var nums = Enumerable.Range(0, 10); // no more .ToArray()
foreach (var num in nums) {

Still, while turning queries into materialized collections might seem like a clever trick, it isn't always the case that you would like to buffer the entire sequence (thus losing both memory and time).

With performance in mind, what I found is that an almost zero allocating wrapper over the IEnumerable which would turn it into an IAsyncEnumerable plus using await foreach instead of just foreach would circumvent the issue.

I have recently published a new version of the NuGet package which now includes an extension method called ToAsync<T>() for IEnumerable<T> in general, placed in System.Collections.Generic which does just that. The method's signature is:

namespace System.Collections.Generic {
    public static class EnumerableExtensions {
        public static IAsyncEnumerable<T> ToAsync<T>(this IEnumerable<T> @this)

and upon adding the NuGet package to a .NET Core 3 project one could use it like so:

using System.Collections.Generic;
...

private static async IAsyncEnumerable<int> GetNumbersAsync() {
    var nums = Enumerable.Range(0, 10);
    await foreach (var num in nums.ToAsync()) {
        await Task.Delay(100);
            yield return num;
        }
    }
}

Notice the two changes:

  • foreach becomes await foreach
  • nums becoms nums.ToAsync()

The wrapper is as lightweight as possible and its implementation is based on the following classes (note that the using of ValueTask<T> as enforced by the IAsyncEnumerable<T> and IAsyncEnumerator<T> allows for a constant number of Heap allocations per foreach):

public static class EnumerableExtensions {

    public static IAsyncEnumerable<T> ToAsync<T>(this IEnumerable<T> @this) => new EnumerableAdapter<T>(@this);

    [MethodImpl(MethodImplOptions.AggressiveInlining)]
    public static IAsyncEnumerator<T> ToAsync<T>(this IEnumerator<T> @this) => new EnumeratorAdapter<T>(@this);


    private sealed class EnumerableAdapter<T> : IAsyncEnumerable<T> {
        private readonly IEnumerable<T> target;
        public EnumerableAdapter(IEnumerable<T> target) => this.target = target;
        public IAsyncEnumerator<T> GetAsyncEnumerator() => this.target.GetEnumerator().ToAsync();
    }

    private sealed class EnumeratorAdapter<T> : IAsyncEnumerator<T> {
        private readonly IEnumerator<T> enumerator;
        public EnumeratorAdapter(IEnumerator<T> enumerator) => this.enumerator = enumerator;

        public ValueTask<bool> MoveNextAsync() => new ValueTask<bool>(this.enumerator.MoveNext());
        public T Current => this.enumerator.Current;
        public ValueTask DisposeAsync() {
            this.enumerator.Dispose();
            return new ValueTask();
        }
    } 
}

To sum it up:

  • To be able to write async generator methods ( async IAsyncEnumerable<int> MyMethod() ...) and to consume async enumerables (await foreach (var x in ...) simply install the NuGet in your project.

  • In order to also circumvent the iteration premature stop, make sure you've got System.Collections.Generic in your using clauses, call .ToAsync() on your IEnumerable and turn your foreach into an await foreach.

Eduard Dumitru
  • 3,242
  • 17
  • 31
  • That's a terrible idea. It's not `popular belief` it's the .NET Core team itself that posted the bug. Using third party code to cover up a known bug it a *Preview 1* version can only cause confusion and incompatible code. Better to wait for Preview 2 than write code that will have to be removed in 1 month – Panagiotis Kanavos Dec 10 '18 at 08:36
  • If you look carefully you’ll notice that you are stating the fact that using Enumerable.Range(0, 10).ToArray will only print out 2 numbers and that I state that this isn’t true precisely because your snippet uses .ToArray(). You’re not echoing what the .NET Core team stated, copy paste bug I guess. My wording (“popular belief”) may not be the best one, indeed – Eduard Dumitru Dec 10 '18 at 08:38
  • As for releasing the code of a [C# Roslyn developer](https://github.com/jcouv) in your own package, you should ask Joulien first. I suspect causing incompatible versions of source code isnt' something he'd like – Panagiotis Kanavos Dec 10 '18 at 08:39
  • If you look carefully and find a typo, post a comment. Don't tell people to use incompatible code that's going to change in 1 month. – Panagiotis Kanavos Dec 10 '18 at 08:46