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 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.