Description
Background and Motivation
While it is currently possible to configure task awaitables via the [Value]Task.ConfigureAwait
methods, the current API only exposes one setting: continueOnCapturedContext
. We have identified a number of other await settings that could be useful to users:
- Accepting a cancellation token.
- Awaiting on failing tasks without throwing an exception.
- Forcing asynchronous execution of continuations.
- Allowing for future extensions in configurability.
This proposal combines the feature requests from #27723 and #22144.
Usage examples
At the core of the proposal is introducing the following struct in System.Threading.Tasks
:
public readonly struct AwaitBehavior
{
public CancellationToken CancellationToken { get; init; } // = default;
public TimeSpan Timeout { get; init; } // = Timeout.InfiniteTimeSpan;
public bool ContinueOnCapturedContext { get; init; } // = true;
public bool ForceAsync { get; init; } // = false;
public bool SuppressExceptions { get; init; } // = false;
}
and exposing Task/ValueTask ConfigureAwait
overloads that accept it like so:
var result = await task.ConfigureAwait(new AwaitBehavior { ContinueOnCapturedContext = false }); // equivalent to task.ConfigureAwait(false);
Suppressing exceptions
ValueTask<string> failedTask = ValueTask.FromException<string>(new Exception());
string result = await failedTask.ConfigureAwait(new AwaitBehavior { SuppressExceptions = true });
Assert.Equal(null, result);
Cancellation
await task.ConfigureAwait(new AwaitBehavior { CancellationToken = new CancellationToken(true) }); // throws TaskCanceledException
Timeouts
await task.ConfigureAwait(new AwaitBehavior { Timeout = TimeSpan.FromSeconds(1) }); // throws TimeoutException
Note that timeouts are declarative on awaitables; a timer will only be allocated whenever an awaiter instance is created, so we should expect the following behaviour:
var task = Task.Delay(10_000);
var awaitableWithTimeout = task.ConfigureAwait(new AwaitBehavior { Timeout = TimeSpan.FromSeconds(1) });
try { await awaitableWithTimeout ; } catch { } // throws TimeoutException
await task;
await awaitableWithTimeout; // does not throw
Finally, it should be possible to combine all the above settings:
string result = await task.ConfigureAwait(
new AwaitBehavior
{
ContinueOnCapturedContext = false,
SuppressExceptions = true,
Timeout = TimeSpan.FromSeconds(5),
CancellationToken = new CancellationToken(true),
});
Proposed API
namespace System.Threading.Tasks
{
public readonly struct AwaitBehavior
{
public CancellationToken CancellationToken { get; init; } // = default;
public TimeSpan Timeout { get; init; } // = Timeout.InfiniteTimeSpan;
public bool ContinueOnCapturedContext { get; init; } // = true;
public bool ForceAsync { get; init; } // = false;
public bool SuppressExceptions { get; init; } // = false;
}
public partial class Task
{
public System.Runtime.CompilerServices.ConfiguredCancelableTaskAwaitable ConfigureAwait(AwaitBehavior awaitBehavior) { throw null; }
}
public partial class Task<TResult>
{
public new System.Runtime.CompilerServices.ConfiguredCancelableTaskAwaitable<TResult> ConfigureAwait(AwaitBehavior awaitBehavior) { throw null; }
}
public partial struct ValueTask
{
public System.Runtime.CompilerServices.ConfiguredCancelableValueTaskAwaitable ConfigureAwait(AwaitBehavior awaitBehavior) { throw null; }
}
public partial struct ValueTask<TResult>
{
public System.Runtime.CompilerServices.ConfiguredCancelableValueTaskAwaitable<TResult> ConfigureAwait(AwaitBehavior awaitBehavior) { throw null; }
}
}
namespace System.Runtime.CompilerServices
{
public readonly struct ConfiguredCancelableTaskAwaitable
{
public System.Runtime.CompilerServices.ConfiguredCancelableTaskAwaitable.ConfiguredCancelableTaskAwaiter GetAwaiter() { throw null; }
public readonly struct ConfiguredCancelableTaskAwaiter : System.Runtime.CompilerServices.ICriticalNotifyCompletion, System.Runtime.CompilerServices.INotifyCompletion
{
public bool IsCompleted { get { throw null; } }
public void GetResult() { }
public void OnCompleted(System.Action continuation) { }
public void UnsafeOnCompleted(System.Action continuation) { }
}
}
public readonly struct ConfiguredCancelableTaskAwaitable<TResult>
{
public System.Runtime.CompilerServices.ConfiguredCancelableTaskAwaitable<TResult>.ConfiguredCancelableTaskAwaiter GetAwaiter() { throw null; }
public readonly partial struct ConfiguredCancelableTaskAwaiter : System.Runtime.CompilerServices.ICriticalNotifyCompletion, System.Runtime.CompilerServices.INotifyCompletion
{
public bool IsCompleted { get { throw null; } }
public TResult GetResult() { throw null; }
public void OnCompleted(System.Action continuation) { }
public void UnsafeOnCompleted(System.Action continuation) { }
}
}
public readonly partial struct ConfiguredCancelableValueTaskAwaitable
{
public System.Runtime.CompilerServices.ConfiguredCancelableValueTaskAwaitable.ConfiguredCancelableValueTaskAwaiter GetAwaiter() { throw null; }
public readonly partial struct ConfiguredCancelableValueTaskAwaiter : System.Runtime.CompilerServices.ICriticalNotifyCompletion, System.Runtime.CompilerServices.INotifyCompletion
{
public bool IsCompleted { get { throw null; } }
public void GetResult() { }
public void OnCompleted(System.Action continuation) { }
public void UnsafeOnCompleted(System.Action continuation) { }
}
}
public readonly partial struct ConfiguredCancelableValueTaskAwaitable<TResult>
{
public System.Runtime.CompilerServices.ConfiguredCancelableValueTaskAwaitable<TResult>.ConfiguredCancelableValueTaskAwaiter GetAwaiter() { throw null; }
public readonly partial struct ConfiguredCancelableValueTaskAwaiter : System.Runtime.CompilerServices.ICriticalNotifyCompletion, System.Runtime.CompilerServices.INotifyCompletion
{
public bool IsCompleted { get { throw null; } }
public TResult GetResult() { throw null; }
public void OnCompleted(System.Action continuation) { }
public void UnsafeOnCompleted(System.Action continuation) { }
}
}
}
Implementation
See this commit for a prototype implementation.
Open Questions
- Should we add more options to the
AwaitBehavior
struct? There's already Allow an await to capture the current TaskScheduler instead of the current SynchronizationContext #47433 proposing capturing the current TaskScheduler. - Is
ConfiguredCancelable[Value]TaskAwaitable
an appropriate name for the new awaiters? - Should we consider adding convenience methods for timeout and cancellation token like the ones listed in Developers can have access to more options when configuring async awaitables #47525 (comment)? Note that the latter would cause conflicts with our existing CA2016 analyzer.
- Should cancellations surface as
TaskCanceledException
orOperationCanceledException
? The latter might make more sense since we're cancelling an awaiter, and not a task.