Skip to content

Proposal: IAsyncEnumerable.ConfigureAwait(bool continueOnCapturedContext) #27565

@stephentoub

Description

@stephentoub

Related to https://github.com/dotnet/corefx/issues/32640.

Background

With the addition of IAsyncEnumerable<T>, developers will be able to write asynchronous foreach loops, e.g.

foreach await (T item in source)
{
    ProcessItem(item);
}

This loop expands by the compiler into the equivalent of:

IAsyncEnumerator<T> e = source.GetAsyncEnumerator();
try
{
    while (await e.MoveNextAsync())
    {
        T item = e.Current;
        ProcessItem(item);
    }
}
finally
{
    await e.DisposeAsync();
}

Note that as part of that expansion, the compiler introduced several awaits not visible in the original source code (which is part of why a developer annotates the foreach as foreach await). This introduces an issue, in that a developer now no longer has explicit syntactic control for that await operation, which means that there’s nowhere for a developer to append ConfigureAwait(false) if they want to prevent the await from grabbing the current context and scheduling the continuation back to it. Our guidelines are that library code by default should use ConfigureAwait(false) on every await, so the lack of that here is problematic for library authors.

Proposal

Allow a developer to write:

foreach await (T item in source.ConfigureAwait(false))
{
    ProcessItem(item);
}

This would be achieved with an extension method as follows:

namespace System.Collections.Generic
{
    public static class AsyncEnumerable
    {
        public static ConfiguredAsyncEnumerable<T> ConfigureAwait(this IAsyncEnumerable<T> source, bool continueOnCapturedContext);
    }
}

namespace System.Runtime.CompilerServices
{
    public struct ConfiguredAsyncEnumerable<T>
    {
        public Enumerator GetAsyncEnumerator();

        public struct Enumerator
        {
            public ConfiguredValueTaskAwaitable<T> MoveNextAsync();
            public T Current { get; }
            public ConfiguredValueTaskAwaitable DisposeAsync();
        }
    }
}

Because foreach await is able to bind by pattern, this ConfiguredAsyncEnumerable<T> can be foreach await’d, and the previously shown expansion will now how each of the awaits operating on a ConfiguredValueTaskAwaitable<T> or ConfiguredValueTaskAwaitable instead of a ValueTask<T> or ValueTask.

Open issues:

  • Do we want this? foreach await is just syntactic sugar for something a developer could otherwise write. However, it’s really useful syntactic sugar, and forcing a developer that wants to iterate through an async enumerable in a library to expand it out manually seems wrong.
  • Extension method type name and namespace. Presumably AsyncEnumerable is a nice name for a LINQ implementation to use. Do we want to claim it for this? But presumably it should also live in the same namespace as IAsyncEnumerable<T>, so that it’s always available when using that interface.

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions