IDisposable
C# Programming
Resource Management
.NET Framework
Memory Management

Implementing IDisposable correctly

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

IDisposable exists for deterministic cleanup of resources that should not wait for garbage collection. Managed memory can be reclaimed later by the runtime, but file handles, sockets, database connections, and native buffers often need to be released at a specific moment. The correct disposal pattern depends on whether your class owns only managed disposables or also owns unmanaged resources directly.

The Simple Case

If the class only holds other managed objects that already implement IDisposable, you usually do not need the full finalizer pattern. A straightforward implementation is enough:

csharp
1public sealed class ReportWriter : IDisposable
2{
3    private readonly StreamWriter _writer;
4    private bool _disposed;
5
6    public ReportWriter(string path)
7    {
8        _writer = new StreamWriter(path);
9    }
10
11    public void WriteLine(string line)
12    {
13        ThrowIfDisposed();
14        _writer.WriteLine(line);
15    }
16
17    public void Dispose()
18    {
19        if (_disposed) return;
20        _writer.Dispose();
21        _disposed = true;
22    }
23
24    private void ThrowIfDisposed()
25    {
26        if (_disposed)
27            throw new ObjectDisposedException(nameof(ReportWriter));
28    }
29}

That covers many application-level classes. The type owns _writer, so it is responsible for disposing it once and then refusing further use.

Use using at the Call Site

A correct IDisposable implementation still needs correct usage:

csharp
using var writer = new ReportWriter("report.txt");
writer.WriteLine("hello");

using ensures Dispose() is called even if an exception happens later in the scope.

The Full Dispose Pattern

The classic Dispose(bool disposing) pattern is needed when the class directly owns unmanaged resources or has a finalizer:

csharp
1public class NativeBufferHolder : IDisposable
2{
3    private IntPtr _buffer;
4    private bool _disposed;
5
6    public NativeBufferHolder(int size)
7    {
8        _buffer = System.Runtime.InteropServices.Marshal.AllocHGlobal(size);
9    }
10
11    ~NativeBufferHolder() => Dispose(false);
12
13    public void Dispose()
14    {
15        Dispose(true);
16        GC.SuppressFinalize(this);
17    }
18
19    protected virtual void Dispose(bool disposing)
20    {
21        if (_disposed) return;
22
23        if (_buffer != IntPtr.Zero)
24        {
25            System.Runtime.InteropServices.Marshal.FreeHGlobal(_buffer);
26            _buffer = IntPtr.Zero;
27        }
28
29        _disposed = true;
30    }
31}

Here, Dispose() is called by user code and can clean managed and unmanaged state, while the finalizer is only a fallback for unmanaged cleanup.

Prefer SafeHandle

Modern .NET guidance strongly prefers SafeHandle over handwritten finalizer logic. If you can delegate unmanaged cleanup to a safe handle type, do that instead of managing raw IntPtr values yourself.

csharp
1public sealed class NativeResourceWrapper : IDisposable
2{
3    private readonly SafeHandle _handle;
4    private bool _disposed;
5
6    public NativeResourceWrapper(SafeHandle handle)
7    {
8        _handle = handle;
9    }
10
11    public void Dispose()
12    {
13        if (_disposed) return;
14        _handle.Dispose();
15        _disposed = true;
16    }
17}

Inheritance Considerations

If a disposable class is meant to be inherited, cleanup ordering matters. Derived classes should release their own resources first and then call the base implementation so the base class does not tear down shared state too early.

csharp
1protected override void Dispose(bool disposing)
2{
3    if (disposing)
4    {
5        _childResource?.Dispose();
6    }
7
8    base.Dispose(disposing);
9}

That pattern is only needed for extensible hierarchies, but when it is needed, getting the order wrong can create subtle disposal bugs.

Common Pitfalls

  • Adding a finalizer when the class only owns managed disposables. Fix: use the simple pattern unless unmanaged resources are actually involved.
  • Making Dispose() non-idempotent. Fix: guard cleanup so repeated calls do not crash.
  • Continuing to use an object after disposal. Fix: throw ObjectDisposedException or otherwise make the state explicit.
  • Writing raw finalizer logic when SafeHandle would do the job. Fix: prefer SafeHandle for unmanaged-resource wrappers.
  • Treating IDisposable as a memory-only concept. Fix: think in terms of deterministic release of scarce external resources.

Summary

  • Implement IDisposable when your type owns disposable or unmanaged resources.
  • For classes that only own managed disposables, a simple Dispose() method is often enough.
  • Use the full Dispose(bool disposing) pattern only when you directly manage unmanaged resources or need a finalizer.
  • Prefer SafeHandle over raw finalizer logic when working with native resources.
  • Make disposal idempotent and use using to ensure callers actually trigger cleanup.

Course illustration
Course illustration

All Rights Reserved.