IAsyncDisposable Pitfalls
Posted on July 19, 2022 in dotnet
The .NET ecosystem comes with more and more sophisticated Roslyn Analyzers which may seem annoying but actually are quite useful at improving our code and detecting hidden and tricky issues.
Say we have a Method
method that uses a thing
object, which implements IAsyncDisposable
and is produced by an async GetAsyncDisposableAsync
method. We know about the await using
pattern, which guarantees that the object will be async-disposed when the method exits. We have learned that we need to use ConfigureAwait
on awaited tasks (else, we get the CA2007: Do not directly await a Task warning). And so, we write the following code:
public async Task Method()
{
await using var thing = await GetAsyncDisposableAsync().ConfigureAwait(false);
// use the thing
}
It is a bit convoluted, but does what we want.
Or, does it?
Recently, This code started to produce the CA2007: Do not directly await a Task warning) again. Despite the fact that we are, well, obviously not directly awaiting the task.
Or... which task? It turns out that there are two tasks at stake here:
- When we obtain the
thing
object, we run the asyncGetAsyncDisposable
method, which returns aTask<IAsyncDisposable>
, which we await immediately - When the
await using
pattern implicitly async-disposes the object at the end of the method, it runs the asyncthing.DisposeAsync
method, which returns aValueTask
, which is awaited
When we write the following code, where GetAsyncDisposable
is synchronous, the compiler will reuse the ConfigureAwait
statement when awaiting the DisposeAsync
task.
await using var y = GetAsyncDisposable().ConfigureAwait(false);
However, when we write the following code, where GetAsyncDisposable
is asynchronous, the compiler has to use the ConfigureAwait
statement immediately and is left with no hint about how to await the DisposeAsync
task.
await using var y = await GetAsyncDisposableAsync().ConfigureAwait(false);
And, don't think about putting two ConfigureAwait
statements there, it does not work. There is a lengthy discussion about this situation in the C# lang repository, which essentially concludes that there is no pretty way to handle the situation. It's one area where the C# design fails.
Solution (sort-of)
The solution idea is to separate the await using
statement from the retrieval of the IAsyncDisposable
object. In other words: don't await using
something that is obtained asynchronously. Our code can become:
public async Task Method()
{
var thing = await GetAsyncDisposableAsync().ConfigureAwait(false);
await using (thing.ConfigureAwait(false))
{
// use the thing
}
}
And now we do have our two ConfigureAwait
calls, and C# will use the one in the await using
block when awaiting the DisposeAsync
ValueTask
. Note however that we lose the convenient one-line await using
syntax. We can get it back with this other version:
public async Task Method()
{
var thing = await GetAsyncDisposableAsync().ConfigureAwait(false);
await using var thingd = thing.ConfigureAwait(false);
// use the thing
}
The discussion on the C# repository proposes even more esoteric solutions and some weird-looking extension methods that, in my mind, render things even more confusing. It is an interesting read nevertheless :)
There used to be Disqus-powered comments here. They got very little engagement, and I am not a big fan of Disqus. So, comments are gone. If you want to discuss this article, your best bet is to ping me on Mastodon.