How it works
Some resources need asynchronous work during cleanup. IAsyncDisposable exposes DisposeAsync for that purpose, and await using arranges to await it when the owning scope ends. The sample uses a tiny simulated lease so the lifetime can be observed without requiring a file, network connection, or native resource.
Use prints working, reaches its scope boundary, and begins asynchronous disposal. DisposeAsync yields and then prints released. Only after that cleanup finishes does the caller's await Use complete and print finished. The output therefore demonstrates that cleanup is part of the owned operation's completion contract.
An asynchronous disposal method can fail, just as ordinary work can fail. A real API should consider how cleanup failures are reported alongside a failure from the body. Do not assume that reaching an await using declaration makes every later resource transition infallible or that cleanup can be ignored because it happens at the end.
Ownership still matters. A method should not dispose a borrowed asynchronous resource merely because the interface is available. The component that acquires or explicitly accepts ownership should establish the cleanup boundary. Consumers borrowing the resource need to know how long they may safely use it.
Practice by adding a second asynchronous lease and observing the reverse disposal order. Then throw a controlled exception inside Use and catch it in the caller, verifying that released appears before the caller handles completion. Keep diagnostic messages distinct enough to identify which lease was released.
Finally, avoid implementing Dispose by synchronously blocking on DisposeAsync in an environment where that can deadlock or freeze a user interface. Follow the resource's documented disposal pattern and propagate asynchronous completion through callers where needed. The correct lifetime boundary should remain explicit, testable, and compatible with the execution environment.
A working example
using System;
using System.Linq;
using System.Collections.Generic;
using System.Globalization;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading;
using System.Threading.Tasks;
using System.Runtime.CompilerServices;
await Use();
Console.WriteLine("finished");
static async Task Use() {
await using var lease = new AsyncLease();
Console.WriteLine("working");
}
class AsyncLease : IAsyncDisposable {
public async ValueTask DisposeAsync() {
await Task.Yield();
Console.WriteLine("released");
}
}
Expected output
working released finished
The method does not finish until its asynchronous disposal has been awaited.