C# IDisposable
last modified October 5, 2026
C# IDisposable tutorial shows how to release resources deterministically with
the Dispose method and the using statement.
The garbage collector reclaims managed memory, but it knows nothing about
unmanaged handles, open files, sockets, or locks. A type that owns such a
resource implements IDisposable and releases it in the
Dispose method. The using statement guarantees that
Dispose runs even when an exception is thrown, which makes cleanup
deterministic instead of dependent on a collection. IAsyncDisposable
plays the same role for asynchronous cleanup, where releasing a resource
requires awaiting an operation such as a flush or a network round trip.
C# IDisposable example
In the first example, a small Resource class implements
IDisposable and is used inside a using statement.
using System;
using (var resource = new Resource("database connection"))
{
Console.WriteLine("inside the using block");
resource.Use();
}
Console.WriteLine("after the using block");
Console.WriteLine("done");
class Resource : IDisposable
{
private readonly string _name;
public Resource(string name)
{
_name = name;
Console.WriteLine($"resource '{_name}' acquired");
}
public void Use()
{
Console.WriteLine($"resource '{_name}' in use");
}
public void Dispose()
{
Console.WriteLine($"resource '{_name}' released");
}
}
The class declares that it implements IDisposable and provides the
required Dispose method.
class Resource : IDisposable
The using statement calls Dispose at the closing
brace, so the release happens before the statements that follow the block.
$ dotnet run resource 'database connection' acquired inside the using block resource 'database connection' in use resource 'database connection' released after the using block done
The release line appears between the block and the statements that follow it,
which is the whole point of IDisposable: cleanup happens at a known
place, not whenever a collection happens to run.
C# using declaration
C# 8 added the using declaration, which disposes a variable at the end of the enclosing block instead of at the end of a nested statement.
using System;
Console.WriteLine("using statement:");
using (var a = new Resource("A"))
{
a.Use();
}
Console.WriteLine();
Console.WriteLine("using declaration:");
using var b = new Resource("B");
b.Use();
Console.WriteLine("end of the enclosing block");
class Resource : IDisposable
{
private readonly string _name;
public Resource(string name)
{
_name = name;
Console.WriteLine($"resource '{_name}' acquired");
}
public void Use()
{
Console.WriteLine($"resource '{_name}' in use");
}
public void Dispose()
{
Console.WriteLine($"resource '{_name}' released");
}
}
The statement form scopes the resource to the braces that follow it, so
A is released right after the block.
using (var a = new Resource("A"))
{
a.Use();
}
The declaration form has the scope of the enclosing block. Here that block is
the whole top-level program, so B is released after the last
statement.
using var b = new Resource("B");
$ dotnet run using statement: resource 'A' acquired resource 'A' in use resource 'A' released using declaration: resource 'B' acquired resource 'B' in use end of the enclosing block resource 'B' released
Both forms compile to the same try/finally pattern. The declaration form is shorter; the statement form lets you control exactly where the resource is released.
C# implementing IDisposable
A class that owns an unmanaged handle uses the full dispose pattern, so that
the handle is freed even when Dispose is never called explicitly.
using System;
using System.Runtime.InteropServices;
using (var buffer = new UnmanagedBuffer(16))
{
buffer.Write(42);
Console.WriteLine($"read back: {buffer.Read()}");
}
var reusable = new UnmanagedBuffer(8);
reusable.Dispose();
reusable.Dispose();
Console.WriteLine("the second Dispose call is safe");
var disposed = new UnmanagedBuffer(4);
disposed.Dispose();
try
{
disposed.Write(1);
}
catch (ObjectDisposedException ex)
{
Console.WriteLine($"caught ObjectDisposedException for {ex.ObjectName}");
}
class UnmanagedBuffer : IDisposable
{
private IntPtr _handle;
private readonly int _size;
private bool _disposed;
public UnmanagedBuffer(int size)
{
_size = size;
_handle = Marshal.AllocHGlobal(size);
Console.WriteLine($"allocated {_size} bytes of unmanaged memory");
}
public void Write(int value)
{
ObjectDisposedException.ThrowIf(_disposed, this);
Marshal.WriteInt32(_handle, value);
}
public int Read()
{
ObjectDisposedException.ThrowIf(_disposed, this);
return Marshal.ReadInt32(_handle);
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (_disposed)
{
return;
}
if (disposing)
{
// release managed resources here
Console.WriteLine($"releasing the managed wrapper of the {_size}-byte buffer");
}
Marshal.FreeHGlobal(_handle);
_handle = IntPtr.Zero;
_disposed = true;
}
~UnmanagedBuffer()
{
Dispose(false);
}
}
The public Dispose calls the protected overload with
true, which means the call comes from user code and managed
resources may be touched. It then asks the runtime to skip the finalizer,
because the cleanup already happened.
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
The overload checks _disposed first, so a second call is a no-op.
The disposing flag separates the two callers: the finalizer passes
false and must not touch other managed objects, because they may
already have been collected.
protected virtual void Dispose(bool disposing)
The _disposed field is set after the handle is released, and every
member that must not run on a disposed instance starts with a guard.
ObjectDisposedException.ThrowIf(_disposed, this);
The finalizer calls Dispose(false) only because the type owns an
unmanaged handle directly.
~UnmanagedBuffer()
$ dotnet run allocated 16 bytes of unmanaged memory read back: 42 releasing the managed wrapper of the 16-byte buffer allocated 8 bytes of unmanaged memory releasing the managed wrapper of the 8-byte buffer the second Dispose call is safe allocated 4 bytes of unmanaged memory releasing the managed wrapper of the 4-byte buffer caught ObjectDisposedException for UnmanagedBuffer
The finalizer is unnecessary when the type owns no unmanaged resource directly,
which is the common case for classes that wrap a SafeHandle or
another disposable object. A sealed class can also drop the
protected virtual indirection and make the overload private,
because no derived type can override it.
C# IAsyncDisposable
IAsyncDisposable is the asynchronous counterpart of
IDisposable. It declares a single method,
ValueTask DisposeAsync(), and it is consumed with
await using.
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
await using (var writer = new AsyncWriter("statement.log"))
{
await writer.WriteLineAsync("first line");
await writer.WriteLineAsync("second line");
}
Console.WriteLine("after the await using statement");
await using var writer2 = new AsyncWriter("declaration.log");
await writer2.WriteLineAsync("third line");
Console.WriteLine("the declaration form is still open");
var writer3 = new AsyncWriter("explicit.log");
await writer3.WriteLineAsync("fourth line");
await writer3.DisposeAsync();
Console.WriteLine("the explicit DisposeAsync finished");
class AsyncWriter : IAsyncDisposable
{
private readonly string _path;
private readonly List<string> _buffer = new();
public AsyncWriter(string path)
{
_path = path;
Console.WriteLine($"opening {_path}");
}
public async Task WriteLineAsync(string line)
{
await Task.Delay(20);
_buffer.Add(line);
Console.WriteLine($"buffering '{line}'");
}
public async ValueTask DisposeAsync()
{
await Task.Delay(20);
Console.WriteLine($"flushing {_buffer.Count} line(s) to {_path}");
_buffer.Clear();
Console.WriteLine($"closing {_path}");
}
}
DisposeAsync returns ValueTask rather than
Task. Cleanup often completes synchronously, and
ValueTask avoids allocating a task object in that case.
public async ValueTask DisposeAsync()
The statement form and the declaration form both await
DisposeAsync at the end of their scope, exactly like their
synchronous counterparts.
await using (var writer = new AsyncWriter("statement.log"))
The declaration form stays open until the end of the enclosing block, so its
flush runs after the explicit DisposeAsync call of the third
writer.
await using var writer2 = new AsyncWriter("declaration.log");
$ dotnet run opening statement.log buffering 'first line' buffering 'second line' flushing 2 line(s) to statement.log closing statement.log after the await using statement opening declaration.log buffering 'third line' the declaration form is still open opening explicit.log buffering 'fourth line' flushing 1 line(s) to explicit.log closing explicit.log the explicit DisposeAsync finished flushing 1 line(s) to declaration.log closing declaration.log
A type can implement both interfaces. In that case await using
prefers DisposeAsync, and a plain using still calls
Dispose.
using System;
using System.Threading.Tasks;
await using (var resource = new DualResource())
{
Console.WriteLine("inside the await using block");
}
class DualResource : IDisposable, IAsyncDisposable
{
public void Dispose()
{
Console.WriteLine("Dispose (synchronous) called");
}
public ValueTask DisposeAsync()
{
Console.WriteLine("DisposeAsync called");
return ValueTask.CompletedTask;
}
}
$ dotnet run inside the await using block DisposeAsync called
C# using with multiple resources
Several resources can be acquired together. Disposal always happens in the reverse order of acquisition, like nested blocks closing from the inside out.
using System;
using (Resource outer = new Resource("outer"))
using (Resource inner = new Resource("inner"))
{
Console.WriteLine("inside the nested using");
}
using (Resource a = new Resource("a"), b = new Resource("b"))
{
Console.WriteLine("inside the comma separated using");
}
Console.WriteLine("done");
class Resource : IDisposable
{
private readonly string _name;
public Resource(string name)
{
_name = name;
Console.WriteLine($"resource '{_name}' acquired");
}
public void Dispose()
{
Console.WriteLine($"resource '{_name}' released");
}
}
Two chained using statements behave like a nested block: the inner
resource is released first.
using (Resource outer = new Resource("outer"))
using (Resource inner = new Resource("inner"))
A single statement can also declare several resources of the same type, separated by commas. They are released in reverse order as well.
using (Resource a = new Resource("a"), b = new Resource("b"))
$ dotnet run resource 'outer' acquired resource 'inner' acquired inside the nested using resource 'inner' released resource 'outer' released resource 'a' acquired resource 'b' acquired inside the comma separated using resource 'b' released resource 'a' released done
The output proves the LIFO order in both forms. Keeping that order matters when one resource depends on another, for example when a reader is disposed before the stream it reads from.
C# dispose pattern guidelines
Keep the following rules in mind when implementing and consuming
IDisposable:
- Do not release managed objects from a finalizer thread. The finalizer calls
Dispose(false)and should only free the unmanaged handle. - Make
Disposeidempotent. A second call must do nothing instead of throwing or double freeing. - Set the
_disposedflag before releasing, and throwObjectDisposedExceptionfrom members used after disposal. - Do not dispose an object you did not create unless ownership is documented. A method that receives a stream usually borrows it and leaves it open.
- Use
usinginstead of writing try/finally by hand; the compiler generates the correct pattern, including the null check. - Prefer
await usingin asynchronous code, and implementIAsyncDisposablewhen cleanup itself is asynchronous. - Remember that the garbage collector does not call
Dispose. It may call the finalizer, which is why a type without a finalizer leaks its unmanaged handle when nobody disposes it.
The using statement releases the resource even when the block
throws, and an abandoned object is collected without any release.
using System;
try
{
using (var resource = new Resource("guarded"))
{
Console.WriteLine("about to throw");
throw new InvalidOperationException("boom");
}
}
catch (InvalidOperationException ex)
{
Console.WriteLine($"caught: {ex.Message}");
}
CreateAbandoned();
GC.Collect();
GC.WaitForPendingFinalizers();
Console.WriteLine("after a full garbage collection");
static void CreateAbandoned()
{
_ = new Resource("abandoned");
}
class Resource : IDisposable
{
private readonly string _name;
public Resource(string name)
{
_name = name;
Console.WriteLine($"resource '{_name}' acquired");
}
public void Dispose()
{
Console.WriteLine($"resource '{_name}' released");
}
}
$ dotnet run resource 'guarded' acquired about to throw resource 'guarded' released caught: boom resource 'abandoned' acquired after a full garbage collection
The guarded resource is released before the exception is caught, while the
abandoned one is collected without ever calling Dispose. That is
the difference between deterministic cleanup and waiting for the garbage
collector.
Source
IDisposable Interface - Microsoft Learn
IAsyncDisposable Interface - Microsoft Learn
Implement a Dispose method - Microsoft Learn
The using statement - Microsoft Learn
In this article we have worked with resource cleanup in C#.
Author
List all C# tutorials.