C#idiomatic C#
implementing-disposeasyncstandard/garbage-collection

Core library

Implement a DisposeAsync method

Learn how to implement DisposeAsync and DisposeAsyncCore methods to perform asynchronous resource cleanup.

  • page standard/garbage-collection/implementing-disposeasync
  • updated 10/20/2025

The System.IAsyncDisposable interface was introduced as part of C# 8.0. You implement the System.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 System.IAsyncDisposable.DisposeAsync returns a System.Threading.Tasks.ValueTask that represents the asynchronous disposal operation.

It's typical when implementing the System.IAsyncDisposable interface that classes also implement the System.IDisposable interface. A good implementation pattern of the System.IAsyncDisposable interface is to be prepared for either synchronous or asynchronous disposal, however, it's not a requirement. If no synchronous disposable of your class is possible, having only System.IAsyncDisposable is acceptable. All of the guidance for implementing the disposal pattern also applies to the asynchronous implementation. This article assumes that you're already familiar with how to implement a Dispose method.


ms.topic: include ms.date: 04/07/2021 ms.custom: include

Explore DisposeAsync and DisposeAsyncCore methods

The System.IAsyncDisposable interface declares a single parameterless method, System.IAsyncDisposable.DisposeAsync. Any nonsealed class should define a DisposeAsyncCore() method that also returns a System.Threading.Tasks.ValueTask.

  • A public System.IAsyncDisposable.DisposeAsync implementation that has no parameters.

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

    csharp
    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:

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

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

    // Suppress finalization.
    GC.SuppressFinalize(this);
}

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 System.IAsyncDisposable. The DisposeAsyncCore() method is virtual so that derived classes can define custom cleanup in their overrides.

Implement the async dispose pattern

All nonsealed 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. Some of the following examples use a NoopAsyncDisposable class that is defined as follows:

sample · csharpdocs/standard/garbage-collection/snippets/dispose-async/NoopAsyncDisposable.cs

csharp
public sealed class NoopAsyncDisposable : IAsyncDisposable
{
    ValueTask IAsyncDisposable.DisposeAsync() => ValueTask.CompletedTask;
}

Here's an example implementation of the async dispose pattern that uses the NoopAsyncDisposable type. The type implements DisposeAsync by returning System.Threading.Tasks.ValueTask.CompletedTask.

sample · csharpdocs/standard/garbage-collection/snippets/dispose-async/ExampleAsyncDisposable.cs

csharp
public class ExampleAsyncDisposable : IAsyncDisposable
{
    private IAsyncDisposable? _example;

    public ExampleAsyncDisposable() =>
        _example = new NoopAsyncDisposable();

    public async ValueTask DisposeAsync()
    {
        await DisposeAsyncCore().ConfigureAwait(false);

        GC.SuppressFinalize(this);
    }

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

        _example = null;
    }
}

In the preceding example:

  • The ExampleAsyncDisposable is a nonsealed class that implements the System.IAsyncDisposable interface.
  • It contains a private IAsyncDisposable field, _example, that's initialized in the constructor.
  • The DisposeAsync method delegates to the DisposeAsyncCore method and calls System.GC.SuppressFinalize to notify the garbage collector that the finalizer doesn't have to run.
  • It contains a DisposeAsyncCore() method that calls the _example.DisposeAsync() method, and sets the field to null.
  • The DisposeAsyncCore() method is virtual, which allows subclasses to override it with custom behavior.

Sealed alternative async dispose pattern

If your implementing class can be sealed, you can implement the async dispose pattern by overriding the System.IAsyncDisposable.DisposeAsync method. The following example shows how to implement the async dispose pattern for a sealed class:

sample · csharpdocs/standard/garbage-collection/snippets/dispose-async/SealedExampleAsyncDisposable.cs

csharp
public sealed class SealedExampleAsyncDisposable : IAsyncDisposable
{
    private readonly IAsyncDisposable _example;

    public SealedExampleAsyncDisposable() =>
        _example = new NoopAsyncDisposable();

    public ValueTask DisposeAsync() => _example.DisposeAsync();
}

In the preceding example:

  • The SealedExampleAsyncDisposable is a sealed class that implements the System.IAsyncDisposable interface.
  • The containing _example field is readonly and is initialized in the constructor.
  • The DisposeAsync method calls the _example.DisposeAsync() method, implementing the pattern through the containing field (cascading disposal).

Implement both dispose and async dispose patterns

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

sample · csharpdocs/standard/garbage-collection/snippets/dispose-async/ExampleConjunctiveDisposable.cs

csharp
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().ConfigureAwait(false);

        Dispose(disposing: false);
        GC.SuppressFinalize(this);
    }

    protected virtual void Dispose(bool disposing)
    {
        if (disposing)
        {
            _disposableResource?.Dispose();
            _disposableResource = null;

            if (_asyncDisposableResource is IDisposable disposable)
            {
                disposable.Dispose();
                _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 System.IDisposable.Dispose and System.IAsyncDisposable.DisposeAsync implementations are both simple boilerplate code.

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

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

Each implementation strives to dispose of all possible disposable objects. This ensures that the cleanup is cascaded properly.

Using async disposable

To properly consume an object that implements the System.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.

sample · csharpdocs/standard/garbage-collection/snippets/dispose-async/ExampleConfigureAwaitProgram.cs

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

        Console.ReadLine();
    }
}

For situations where the usage of ConfigureAwait isn't needed, the await using statement could be simplified as follows:

sample · csharpdocs/standard/garbage-collection/snippets/dispose-async/ExampleUsingStatementProgram.cs

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

sample · csharpdocs/standard/garbage-collection/snippets/dispose-async/ExampleUsingDeclarationProgram.cs

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

        // Interact with the exampleAsyncDisposable instance.

        Console.ReadLine();
    }
}

Multiple await keywords in a single line

Sometimes the await keyword may appear multiple times within a single line. For example, consider the following code:

csharp
await using var transaction = await context.Database.BeginTransactionAsync(token);

In the preceding example:

Stacked usings

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

Acceptable pattern one

sample · csharpdocs/standard/garbage-collection/snippets/dispose-async/ExamplePatterns.csone

csharp
// <one>

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();
    }
}
// </one>

// <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();
    }
}
// </two>

// <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();
    }
}
// </three>

// <dontdothis>
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();
    }
}
// </dontdothis>

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

Acceptable pattern two

sample · csharpdocs/standard/garbage-collection/snippets/dispose-async/ExamplePatterns.cstwo

csharp
// <one>

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();
    }
}
// </one>

// <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();
    }
}
// </two>

// <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();
    }
}
// </three>

// <dontdothis>
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();
    }
}
// </dontdothis>

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

sample · csharpdocs/standard/garbage-collection/snippets/dispose-async/ExamplePatterns.csthree

csharp
// <one>

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();
    }
}
// </one>

// <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();
    }
}
// </two>

// <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();
    }
}
// </three>

// <dontdothis>
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();
    }
}
// </dontdothis>

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 example 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 didn't complete successfully. As a result, the constructor for AnotherAsyncDisposable is responsible for disposing any resources allocated before it throws an exception. If the ExampleAsyncDisposable type has a finalizer, it's eligible for finalization.

sample · csharpdocs/standard/garbage-collection/snippets/dispose-async/ExamplePatterns.csdontdothis

csharp
// <one>

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();
    }
}
// </one>

// <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();
    }
}
// </two>

// <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();
    }
}
// </three>

// <dontdothis>
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();
    }
}
// </dontdothis>

See also

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