Using Async/Await With Disposable Objects

This article will show by example how to dispose of disposable objects when using the async/ await pattern. When using await, Microsoft recommends using the ConfigureAwait method as shown below,

await this._channel.Writer.WriteAsync(item, token).ConfigureAwait(false);

Using ConfigureAwait(false) avoids forcing the callback to be invoked on the original context or scheduler as Steven Toub explains in this article: ConfigureAwait FAQ - .NET Blog (microsoft.com). This can improve performance and avoids deadlocks. But it’s a bit tricky when creating the object using the using pattern since ConfigureAwait() returns ConfiguredAsyncDisposable.

How To Implement

Let’s change some code in my open-source project Spargine. The code below converts a string to a Brotli compressed string.

await using(var inputStream = new MemoryStream(Encoding.Unicode.GetBytes(input))) 
{
    await using(var outputStream = new MemoryStream()) 
    {
        await using(var brotliStream = new BrotliStream(outputStream, level)) 
        {
            await inputStream.CopyToAsync(brotliStream).ConfigureAwait(false);
            await brotliStream.FlushAsync().ConfigureAwait(false);
            return Convert.ToBase64String(outputStream.ToArray());
        }
    }
}

When I add ConfigureAwait(false) to the creation of MemoryStream, an error will appear,

Using Async/Await with Disposable Objects

The error appears since inputStream is now actually a ConfiguredAsyncDisposable type so CopyToAsync() is not available. To fix this, we need create the disposable type, before the using statement as shown below,

var inputStream = new MemoryStream(Encoding.Unicode.GetBytes(input));

await using(inputStream.ConfigureAwait(false)) 
{
    var outputStream = new MemoryStream();

    await using(outputStream.ConfigureAwait(false)) 
    {
        var brotliStream = new BrotliStream(outputStream, level);

        await using(brotliStream.ConfigureAwait(false)) 
        {
            await inputStream.CopyToAsync(brotliStream).ConfigureAwait(false);
            await brotliStream.FlushAsync().ConfigureAwait(false);
            return Convert.ToBase64String(outputStream.ToArray());
        }
    }
}

Unfortunately, this code will then cause the following error,

CA2000: Call System.IDisposable.Dispose on object created by 'new MemoryStream()' before all references to it are out of scope

To fix it, add this attribute to the method,

[SuppressMessage("Microsoft.Build", "CS2000")]

I do wish that Microsoft solved this issue without this many code changes on our part. Maybe someday... fingers crossed.

Summary

Hidden dispose issues are more difficult to solve, like this one. I highly recommend adding the IDisposableAnalyzers NuGet package to all your projects to uncover disposable issues like this. I believe this should be part of .NET, not a package that has to be added since virtual memory issues are a big deal if your codebase has any and I can say most of the projects out there do!

Do you have any questions or comments? Please make them below.



McCarter Consulting
Specializing in software architecture, Microsoft .NET, mentoring, resume review/prep. Available now!