-
Notifications
You must be signed in to change notification settings - Fork 5.4k
Proposal: IAsyncEnumerable<T>.WithCancellation extension method #28105
Description
Background
Per recent design revisions, IAsyncEnumerable<T>.GetAsyncEnumerator will now accept a CancellationToken. To get a cancellation token into the GetAsyncEnumerator call, code can use GetAsyncEnumerator explicitly, e.g.
IAsyncEnumerator<T> enumerator = enumerable.GetAsyncEnumerator(cancellationToken);
try
{
while (await enumerator.MoveNextAsync())
{
T current = enumerator.Current;
…
}
}
finally
{
await enumerator.DisposeAsync();
}To use await foreach, as there’s no language-provided facility for passing a CancellationToken into the compiler-generated GetAsyncEnumerator call, to get a CancellationToken in we need to take advantage of the pattern-matching support await foreach supplies. By returning a struct that exposes the right pattern, we can smuggle a CancellationToken in, stored in that struct, such that when the compiler-generated code invokes that struct’s GetAsyncEnumerator(), it in turn calls GetAsyncEnumerator(_cancellationToken) on the underlying enumerable.
To make that simple, we should add a WithCancellation extension method on IAsyncEnumerable<T> that accepts the CancellationToken, allowing you to write:
await foreach (T item in enumerable.WithCancellation(cancellationToken))
{
…
}with that generating the equivalent of:
var enumerator = enumerable.WithCancellation(cancellationToken).GetAsyncEnumerator();
try
{
while (await enumerator.MoveNextAsync())
{
T current = enumerator.Current;
…
}
}
finally
{
await enumerator.DisposeAsync();
}However, we also need to deal with the ability to ConfigureAwait(false) an enumerable. We’ve already added the ConfigureAwait(bool) extension method for IAsyncEnumerable<T>, and we need to be able to allow developers to use both it and WithCancellation. Since both of these would return structs that by design do not implement IAsyncEnumerable<T>, we need another mechanism to allow chaining them, e.g.
await foreach (T item in enumerable.WithCancellation(cancellationToken).ConfigureAwait(false))
{
…
}Proposal
As part of adding the async-related interfaces, we also added a ConfigureAwait extension method and a new ConfiguredAsyncEnumerable struct type. We replace those with a slightly updated version:
-namespace System.Threading.Tasks
-{
- public static class TaskExtensions // previously existing class
- {
- public static ConfiguredAsyncEnumerable<T> ConfigureAwait<T>(this IAsyncEnumerable<T> source, bool continueOnCapturedContext);
- }
-}
-
-namespace System.Runtime.CompilerServices
-{
- [StructLayout(LayoutKind.Auto)]
- public readonly struct ConfiguredAsyncEnumerable<T>
- {
- public Enumerator GetAsyncEnumerator(CancellationToken cancellationToken = default);
- public readonly struct Enumerator
- {
- public ConfiguredValueTaskAwaitable<bool> MoveNextAsync();
- public T Current { get; }
- public ConfiguredValueTaskAwaitable DisposeAsync();
- }
- }
-}
+namespace System.Threading.Tasks
+{
+ public static class TaskExtensions // previously existing class
+ {
+ public static ConfiguredCancelableAsyncEnumerable<T> WithCancellation<T>(this IAsyncEnumerable<T> source, CancellationToken cancellationToken);
+ public static ConfiguredCancelableAsyncEnumerable<T> ConfigureAwait<T>(this IAsyncEnumerable<T> source, bool continueOnCapturedContext);
+ }
+}
+
+namespace System.Runtime.CompilerServices
+{
+ [StructLayout(LayoutKind.Auto)]
+ public readonly struct ConfiguredCancelableAsyncEnumerable<T>
+ {
+ public ConfiguredCancelableAsyncEnumerable<T> WithCancellation(CancellationToken cancellationToken);
+ public ConfiguredCancelableAsyncEnumerable<T> ConfigureAwait(bool continueOnCapturedContext);
+
+ public Enumerator GetAsyncEnumerator();
+ public readonly struct Enumerator
+ {
+ public ConfiguredValueTaskAwaitable<bool> MoveNextAsync();
+ public T Current { get; }
+ public ConfiguredValueTaskAwaitable DisposeAsync();
+ }
+ }
+}This let’s you write:
await foreach (T item in source) { … }
await foreach (T item in source.ConfigureAwait(false)) { … }
await foreach (T item in source.WithCancellation(token)) { … }
await foreach (T item in source.ConfigureAwait(false).WithCancellation(token)) { … }
await foreach (T item in source.WithCancellation(token).ConfigureAwait(false)) { … }It of course also lets you write something a bit non-sensical, like:
await foreach (T item in source.ConfigureAwait(false).ConfigureAwait(true).WithCancellation(token1).WithCancellation(token2)) { … }and in such a case we would just use the last value specified, e.g. this would end up being equivalent to:
await foreach (T item in source.ConfigureAwait(true).WithCancellation(token2)) { … }Note that the GetAsyncEnumerator() on the struct is defined to be parameterless (this assumes the compiler's pattern-matching support allows that... if not, it'll be defined to take a token as well). Since this struct is only ever meant to be used via the provided extension methods with the await foreach construct, there’s little use for also accepting a CancellationToken into GetAsyncEnumerator(), and allowing that would just be confusing, as that argument will be ignored by the implementation.
cc: @jcouv, @MadsTorgersen, @terrajobst, @bartonjs