Skip to Content
Course content

223: Async Disposal with IAsyncDisposable

Click on the "Edit" button in the top corner of the screen to edit your slide content.

A few years back, I was reviewing a PR for a junior dev who had built a custom wrapper for a cloud-based messaging queue. Everything looked great until I noticed the Dispose method. Inside, they were calling a remote API to "gracefully close" the session. Since IDisposable.Dispose() is synchronous, they had used .GetAwaiter().GetResult() to force the async API call to run synchronously. The result? Intermittent, nightmare-inducing deadlocks in the production environment that only happened under heavy load. They were trying to do the right thing by cleaning up resources, but they were fighting the framework to do it.

This is exactly why IAsyncDisposable exists. When your cleanup logic involves I/O—like flushing a stream to a disk, closing a network connection, or calling a web service—doing it synchronously is a recipe for performance bottlenecks or complete application freezes. IAsyncDisposable allows you to signal to the caller that the cleanup process itself is asynchronous.

The Mechanics of DisposeAsync

Instead of the traditional Dispose() method, you implement IAsyncDisposable, which requires a single method: ValueTask DisposeAsync(). You'll notice it returns a ValueTask rather than a Task. This is a performance optimization; in many cases, the cleanup might complete synchronously, and ValueTask avoids an unnecessary heap allocation in those scenarios.

public class CloudSession : IAsyncDisposable
{
    private readonly HttpClient _client = new HttpClient();
    private readonly string _sessionId = Guid.NewGuid().ToString();

    public async ValueTask DisposeAsync()
    {
        // We can actually 'await' the cleanup here!
        await _client.PostAsync($"/sessions/{_sessionId}/close", null);
        _client.Dispose();
        
        Console.WriteLine("Session closed asynchronously.");
    }
}

To use this class, you don't use a standard using block. Instead, you use await using. This tells the compiler to call DisposeAsync and await its completion before exiting the scope. If you use a regular using on a class that only implements IAsyncDisposable, the code simply won't compile.

Handling the Hybrid Disposal Pattern

Now, here is where it gets a bit tricky. In the real world, you often encounter classes that need to support both synchronous and asynchronous disposal—perhaps for backward compatibility or because the class is consumed by different types of callers. I usually recommend implementing both IDisposable and IAsyncDisposable in these cases.

The key is to avoid duplicating your cleanup logic. You should create a private method that handles the actual work, and have both disposal methods call into it. However, remember that the synchronous Dispose() cannot await anything. If you find yourself in a position where you must support both, but the cleanup must be async, you have a design problem. But for standard resource management, the pattern looks like this:

public class DataVault : IDisposable, IAsyncDisposable
{
    public void Dispose()
    {
        // Perform synchronous cleanup
        Cleanup();
        GC.SuppressFinalize(this);
    }

    public async ValueTask DisposeAsync()
    {
        // Perform asynchronous cleanup
        await CleanupAsync();
        
        // Still call SuppressFinalize to tell the GC we're done
        GC.SuppressFinalize(this);
    }

    private void Cleanup() { /* Sync logic */ }
    private async Task CleanupAsync() { /* Async logic */ }
}

One final tip: always call GC.SuppressFinalize(this). Whether you are disposing synchronously or asynchronously, you're telling the Garbage Collector that the object has already been cleaned up, so it doesn't need to put it on the finalizer queue. It's a small detail that keeps your application's memory management snappy.




📋 Practical Task

Exercise: Implementing an Async RemoteLogBuffer

You are tasked with creating a RemoteLogBuffer class that collects logs in memory and flushes them to a remote server only when the object is disposed. Because the flush operation involves a network call, it must be handled asynchronously to prevent blocking the main thread.

Requirements:

  • Implement IAsyncDisposable in the RemoteLogBuffer class.
  • Create a method public void Log(string message) that adds strings to a private List<string>.
  • Implement DisposeAsync(). Inside this method, simulate a network call by using await Task.Delay(100) and then print the number of logs that were "flushed" to the console.
  • In your Main method, instantiate the RemoteLogBuffer using the await using syntax, add a few logs, and ensure the program waits for the disposal to complete.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.