IDisposable Patterns
2 min readIDisposable Patterns
TL;DR
IDisposable is the .NET contract for releasing unmanaged resources (sockets, file handles, DB connections, timers) deterministically, paired with using/await using so cleanup runs even on exceptions. The senior-engineer details are calling GC.SuppressFinalize(this) to skip the finalizer cost, making Dispose idempotent, throwing ObjectDisposedException after disposal, and implementing IAsyncDisposable for resources that need to release asynchronously — leaks here mean exhausted sockets and outages.
How it works
- Implement
IDisposablewhen you own unmanaged resources (file handles, sockets, DB connections) or wrap disposables. - Pattern:
public sealed class PriceFeed : IDisposable
{
private bool _disposed;
private readonly Timer _timer;
public PriceFeed()
{
_timer = new Timer(_ => PullAsync().GetAwaiter().GetResult());
}
public void Dispose()
{
if (_disposed) return;
_timer.Dispose();
_disposed = true;
GC.SuppressFinalize(this);
}
}
- Usage: Wrap with
using/await usingso cleanup runs even on exceptions.
await using var connection = await _dbFactory.CreateConnectionAsync();
Quick recall Q&A
IDisposable?When it owns unmanaged resources or wraps objects that implement IDisposable (streams, DbContexts, timers) and must release them deterministically.
GC.SuppressFinalize(this)?It prevents the GC from invoking the finalizer once you’ve disposed the object, saving an extra GC cycle and improving performance.
Implement IAsyncDisposable and use await using to asynchronously release resources like pooled connections or streams.
Resources leak—sockets stay open, file handles remain locked, and finalizers eventually run, adding GC pressure. In services, this can lead to outages.
Guard with an _disposed flag, throw ObjectDisposedException when methods run after disposal, and make Dispose idempotent.
Rarely—only when you wrap unmanaged resources without safe handles. Prefer SafeHandle + IDisposable instead of writing finalizers yourself.
The scope disposes services when it ends. Don’t capture scoped services beyond scope lifetime; create scopes per operation if needed.
Use Mock<IDisposable> to verify Dispose is called, or check resource state (e.g., timer disposed). For async disposal, assert tasks complete and resources release handles.
DisposeAsync and Dispose?DisposeAsync returns a ValueTask and awaits asynchronous cleanup. Dispose runs synchronously. Implement both when supporting async resource release but ensure Dispose calls DisposeAsync().GetAwaiter().GetResult() if needed.
using translate in IL?It compiles to a try/finally block where Dispose is invoked in the finally clause, guaranteeing cleanup even if exceptions occur.