Implement a DisposeAsync method

The System.IAsyncDisposable interface was introduced as part of C# 8.0. You implement the IAsyncDisposable.DisposeAsync() method when you need to perform resource cleanup, just as you would when implementing a Dispose method. One of the key differences however, is that this implementation allows for asynchronous cleanup operations. The DisposeAsync() returns a ValueTask that represents the asynchronous dispose operation.

It is typical when implementing the IAsyncDisposable interface that classes will also implement the IDisposable interface. A good implementation pattern of the IAsyncDisposable interface is to be prepared for either synchronous or asynchronous dispose. All of the guidance for implementing the dispose pattern also applies to the asynchronous implementation. This article assumes that you're already familiar with how to implement a Dispose method.

Tip

With regard to dependency injection, when registering services in an IServiceCollection, the service lifetime is managed implicitly on your behalf. The IServiceProvider and corresponding IHost orchestrate resource cleanup. Specifically, implementations of IDisposable and IAsyncDisposable are properly disposed at the end of their specified lifetime.

For more information, see Dependency injection in .NET.

DisposeAsync() and DisposeAsyncCore()

The IAsyncDisposable interface declares a single parameterless method, DisposeAsync(). Any non-sealed class should have an additional DisposeAsyncCore() method that also returns a ValueTask.

  • A public IAsyncDisposable.DisposeAsync() implementation that has no parameters.

  • A protected virtual ValueTask DisposeAsyncCore() method whose signature is:

    protected virtual ValueTask DisposeAsyncCore()
    {
    }
    

The DisposeAsync() method

The public parameterless DisposeAsync() method is called implicitly in an await using statement, and its purpose is to free unmanaged resources, perform general cleanup, and to indicate that the finalizer, if one is present, need not run. Freeing the memory associated with a managed object is always the domain of the garbage collector. Because of this, it has a standard implementation:

public async ValueTask DisposeAsync()
{
    // Perform async cleanup.
    await DisposeAsyncCore();

    // Dispose of unmanaged resources.
    Dispose(false);

#pragma warning disable CA1816 // Dispose methods should call SuppressFinalize
    // Suppress finalization.
    GC.SuppressFinalize(this);
#pragma warning restore CA1816 // Dispose methods should call SuppressFinalize
}

Note

One primary difference in the async dispose pattern compared to the dispose pattern, is that the call from DisposeAsync() to the Dispose(bool) overload method is given false as an argument. When implementing the IDisposable.Dispose() method, however, true is passed instead. This helps ensure functional equivalence with the synchronous dispose pattern, and further ensures that finalizer code paths still get invoked. In other words, the DisposeAsyncCore() method will dispose of managed resources asynchronously, so you don't want to dispose of them synchronously as well. Therefore, call Dispose(false) instead of Dispose(true).

The DisposeAsyncCore() method

The DisposeAsyncCore() method is intended to perform the asynchronous cleanup of managed resources or for cascading calls to DisposeAsync(). It encapsulates the common asynchronous cleanup operations when a subclass inherits a base class that is an implementation of IAsyncDisposable. The DisposeAsyncCore() method is virtual so that derived classes can define additional cleanup in their overrides.

Tip

If an implementation of IAsyncDisposable is sealed, the DisposeAsyncCore() method is not needed, and the asynchronous cleanup can be performed directly in the IAsyncDisposable.DisposeAsync() method.

Implement the async dispose pattern

All non-sealed classes should be considered a potential base class, because they could be inherited. If you implement the async dispose pattern for any potential base class, you must provide the protected virtual ValueTask DisposeAsyncCore() method. Here is an example implementation of the async dispose pattern that uses a System.Text.Json.Utf8JsonWriter.

using System;
using System.IO;
using System.Text.Json;
using System.Threading.Tasks;

public class ExampleAsyncDisposable : IAsyncDisposable, IDisposable
{
    private Utf8JsonWriter _jsonWriter = new(new MemoryStream());

    public void Dispose()
    {
        Dispose(disposing: true);
        GC.SuppressFinalize(this);
    }

    public async ValueTask DisposeAsync()
    {
        await DisposeAsyncCore();

        Dispose(disposing: false);
#pragma warning disable CA1816 // Dispose methods should call SuppressFinalize
        GC.SuppressFinalize(this);
#pragma warning restore CA1816 // Dispose methods should call SuppressFinalize
    }

    protected virtual void Dispose(bool disposing)
    {
        if (disposing)
        {
            _jsonWriter?.Dispose();
        }

        _jsonWriter = null;
    }

    protected virtual async ValueTask DisposeAsyncCore()
    {
        if (_jsonWriter is not null)
        {
            await _jsonWriter.DisposeAsync().ConfigureAwait(false);
        }

        _jsonWriter = null;
    }
}

The preceding example uses the Utf8JsonWriter. For more information about System.Text.Json, see How to migrate from Newtonsoft.Json to System.Text.Json.

Implement both dispose and async dispose patterns

You may need to implement both the IDisposable and IAsyncDisposable interfaces, especially when your class scope contains instances of these implementations. Doing so ensures that you can properly cascade clean up calls. Here is an example class that implements both interfaces and demonstrates the proper guidance for cleanup.

using System;
using System.IO;
using System.Threading.Tasks;

class ExampleConjunctiveDisposableusing : IDisposable, IAsyncDisposable
{
    IDisposable _disposableResource = new MemoryStream();
    IAsyncDisposable _asyncDisposableResource = new MemoryStream();

    public void Dispose()
    {
        Dispose(disposing: true);
        GC.SuppressFinalize(this);
    }

    public async ValueTask DisposeAsync()
    {
        await DisposeAsyncCore();

        Dispose(disposing: false);
#pragma warning disable CA1816 // Dispose methods should call SuppressFinalize
        GC.SuppressFinalize(this);
#pragma warning restore CA1816 // Dispose methods should call SuppressFinalize
    }

    protected virtual void Dispose(bool disposing)
    {
        if (disposing)
        {
            _disposableResource?.Dispose();
            (_asyncDisposableResource as IDisposable)?.Dispose();
        }

        _disposableResource = null;
        _asyncDisposableResource = null;
    }

    protected virtual async ValueTask DisposeAsyncCore()
    {
        if (_asyncDisposableResource is not null)
        {
            await _asyncDisposableResource.DisposeAsync().ConfigureAwait(false);
        }

        if (_disposableResource is IAsyncDisposable disposable)
        {
            await disposable.DisposeAsync().ConfigureAwait(false);
        }
        else
        {
            _disposableResource?.Dispose();
        }

        _asyncDisposableResource = null;
        _disposableResource = null;
    }
}

The IDisposable.Dispose() and IAsyncDisposable.DisposeAsync() implementations are both simple boilerplate code.

In the Dispose(bool) overload method, the IDisposable instance is conditionally disposed of if it is not null. The IAsyncDisposable instance is cast as IDisposable, and if it is also not null, it is disposed of as well. Both instances are then assigned to null.

With the DisposeAsyncCore() method, the same logical approach is followed. If the IAsyncDisposable instance is not null, its call to DisposeAsync().ConfigureAwait(false) is awaited. If the IDisposable instance is also an implementation of IAsyncDisposable, it's also disposed of asynchronously. Both instances are then assigned to null.

Using async disposable

To properly consume an object that implements the IAsyncDisposable interface, you use the await and using keywords together. Consider the following example, where the ExampleAsyncDisposable class is instantiated and then wrapped in an await using statement.

using System;
using System.Threading.Tasks;

class ExampleConfigureAwaitProgram
{
    static async Task Main()
    {
        var exampleAsyncDisposable = new ExampleAsyncDisposable();
        await using (exampleAsyncDisposable.ConfigureAwait(false))
        {
            // Interact with the exampleAsyncDisposable instance.
        }

        Console.ReadLine();
    }
}

Important

Use the ConfigureAwait(IAsyncDisposable, Boolean) extension method of the IAsyncDisposable interface to configure how the continuation of the task is marshalled on its original context or scheduler. For more information on ConfigureAwait, see ConfigureAwait FAQ.

For situations where the usage of ConfigureAwait is not needed, the await using statement could be simplified as follows:

using System;
using System.Threading.Tasks;

class ExampleUsingStatementProgram
{
    static async Task Main()
    {
        await using (var exampleAsyncDisposable = new ExampleAsyncDisposable())
        {
            // Interact with the exampleAsyncDisposable instance.
        }

        Console.ReadLine();
    }
}

Furthermore, it could be written to use the implicit scoping of a using declaration.

using System;
using System.Threading.Tasks;

class ExampleUsingDeclarationProgram
{
    static async Task Main()
    {
        await using var exampleAsyncDisposable = new ExampleAsyncDisposable();

        // Interact with the exampleAsyncDisposable instance.

        Console.ReadLine();
    }
}

Stacked usings

In situations where you create and use multiple objects that implement IAsyncDisposable, it's possible that stacking await using statements with ConfigureAwait could prevent calls to DisposeAsync() in errant conditions. To ensure that DisposeAsync() is always called, you should avoid stacking. The following three code examples show acceptable patterns to use instead.

Acceptable pattern one

using System;
using System.Threading.Tasks;

class ExampleOneProgram
{
    static async Task Main()
    {
        var objOne = new ExampleAsyncDisposable();
        await using (objOne.ConfigureAwait(false))
        {
            // Interact with the objOne instance.

            var objTwo = new ExampleAsyncDisposable();
            await using (objTwo.ConfigureAwait(false))
            {
                // Interact with the objOne and/or objTwo instance(s).
            }
        }

        Console.ReadLine();
    }
}

In the preceding example, each asynchronous clean up operation is explicitly scoped under the await using block. The outer scope is defined by how objOne sets its braces, enclosing objTwo, as such objTwo is disposed first, followed by objOne. Both IAsyncDisposable instances have their DisposeAsync() method awaited, so each instance performs its asynchronous clean up operation. The calls are nested, not stacked.

Acceptable pattern two

class ExampleTwoProgram
{
    static async Task Main()
    {
        var objOne = new ExampleAsyncDisposable();
        await using (objOne.ConfigureAwait(false))
        {
            // Interact with the objOne instance.
        }

        var objTwo = new ExampleAsyncDisposable();
        await using (objTwo.ConfigureAwait(false))
        {
            // Interact with the objTwo instance.
        }

        Console.ReadLine();
    }
}

In the preceding example, each asynchronous clean up operation is explicitly scoped under the await using block. At the end of each block, the corresponding IAsyncDisposable instance has its DisposeAsync() method awaited, thus performing its asynchronous clean up operation. The calls are sequential, not stacked. In this scenario objOne is disposed first, then objTwo is disposed.

Acceptable pattern three

class ExampleThreeProgram
{
    static async Task Main()
    {
        var objOne = new ExampleAsyncDisposable();
        await using var ignored1 = objOne.ConfigureAwait(false);

        var objTwo = new ExampleAsyncDisposable();
        await using var ignored2 = objTwo.ConfigureAwait(false);

        // Interact with objOne and/or objTwo instance(s).

        Console.ReadLine();
    }
}

In the preceding example, each asynchronous clean up operation is implicitly scoped with the containing method body. At the end of the enclosing block, the IAsyncDisposable instances perform their asynchronous clean up operations. This runs in reverse order from which they were declared, meaning that objTwo is disposed before objOne.

Unacceptable pattern

The highlighted lines in the following code show what it means to have "stacked usings". If an exception is thrown from the AnotherAsyncDisposable constructor, neither object is properly disposed of. The variable objTwo is never assigned because the constructor did not complete successfully. As a result, the constructor for AnotherAsyncDisposable is responsible for disposing any resources allocated before it throws an exception.

class DoNotDoThisProgram
{
    static async Task Main()
    {
        var objOne = new ExampleAsyncDisposable();
        // Exception thrown on .ctor
        var objTwo = new AnotherAsyncDisposable();

        await using (objOne.ConfigureAwait(false))
        await using (objTwo.ConfigureAwait(false))
        {
            // Neither object has its DisposeAsync called.
        }

        Console.ReadLine();
    }
}

Tip

Avoid this pattern as it could lead to unexpected behavior.

See also

For a dual implementation example of IDisposable and IAsyncDisposable, see the Utf8JsonWriter source code on GitHub.