ZetCode

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.

Program.cs
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.

Program.cs
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.

Program.cs
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.

Program.cs
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.

Program.cs
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.

Program.cs
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:

The using statement releases the resource even when the block throws, and an abandoned object is collected without any release.

Program.cs
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

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all C# tutorials.