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.


Similar Articles
McCarter Consulting
Software architecture, code & app performance, code quality, Microsoft .NET & mentoring. Available!