IDisposable & Resource Management

Reviewed & published by Brayan K

By the end of this lesson you'll be able to clean up resources deterministically in C# — implementing IDisposable, letting using call Dispose for you, writing the full dispose pattern with a finalizer safety net, and knowing why using beats hand-written try/finally every time.

Part of the free C# course at LearnCodingFast — hands-on lessons with worked examples and the output they print, plus practice exercises and a quick quiz.

What You'll Learn

💡 Real-World Analogy

The garbage collector (GC) is like a tidy housemate who quietly clears away .NET objects you've finished with — you never have to think about ordinary memory. But some things have to be returned explicitly, like a library book. You borrow it (open a file, grab a connection), you use it, and then you must hand it back so the next person can have it. If you just drop it on the floor and walk off, the book is "leaked" — nobody else can borrow it until, much later, someone notices. IDisposable.Dispose() is the "return the book" counter, and the using keyword is the friend who walks you to the desk and makes sure you actually hand it back — even if you trip on the way (an exception).

Why "deterministic" cleanup matters

The GC handles managed memory automatically, but it runs whenever it likes — you can't predict the moment. That's fine for plain memory, but a file handle or database connection held open "until the GC gets round to it" can block other code for seconds or minutes. Deterministic cleanup means cleanup happens at a moment you control — the instant you're done — and that is exactly what IDisposable gives you.

Two kinds of resource sit behind everything below:

📊 Resource Management at a Glance

ToolWhat it doesWhen to reach for it
IDisposableContract with one method, Dispose()Any class that owns a resource needing release
using (...) { }Statement: disposes at end of the blockA resource used inside one clear block
using var x = ...;Declaration: disposes at end of scopeA resource used for the rest of a method
Dispose(bool)Shared cleanup for caller + finalizerClasses that can be inherited from
~ClassName()Finalizer: GC's last-resort cleanupOnly when you directly hold unmanaged handles
SafeHandleA ready-made finalizable wrapperAlmost always — instead of a raw handle + finalizer

Most everyday code only ever needs the first three rows: implement IDisposable and let using do the work. The finalizer rows are for the rare class that owns a native handle directly.

1. Implementing IDisposable

IDisposable is an interface with exactly one method: void Dispose(). When your class owns something that must be released — a file, a connection, a lock — you implement this interface and put the "give it back" code inside Dispose. That's the whole idea: a single, agreed-upon method that everyone (and the using keyword) knows to call. Read this worked example, run it, then you'll write your own.

using System;

// IDisposable is a CONTRACT: "I own something that must be cleaned up,
// and here is the single method — Dispose() — that does the cleanup."
class FileLogger : IDisposable
{
    private readonly string _name;

    public FileLogger(string name)
    {
        _name = name;
        Console.WriteLine($"Opened log: {_name}");   // pretend we opened a file handle
    }

    public void Write(string message)
    {
        Console.WriteLine($"[{_name}] {message}");
    }

    // The ONE method IDisposable forces you to provide.
    // Put your "give the resource back" code here.
    public void Dispose()
    {
        Console.WriteLine($"Closed log: {_name}");    // the handle is released here
    }
}

class Program
{
    static void Main()
    {
        var logger = new FileLogger("app.log");       // Opened log: app.log
        logger.Write("Application started");          // [app.log] Application started

        logger.Dispose();   // YOU must remember to call this — or the file stays open
        // Closed log: app.log
    }
}

// ✅ Expected output:
//    Opened log: app.log
//    [app.log] Application started
//    Closed log: app.log

Your turn. The class below just needs to declare that it implements IDisposable and provide the one required method. Fill in the three ___ blanks, then run it.

using System;

// 🎯 YOUR TURN — make this class implement IDisposable, then run it.
class Resource ___          // 👉 add ":IDisposable" so the class promises the contract
{
    public Resource() => Console.WriteLine("Created");

    // 1) Implement the method IDisposable requires.
    //    It must be 'public void Dispose()' and print "Disposed".
    public void ___()        // 👉 the method name is  Dispose
    {
        Console.WriteLine("___");   // 👉 print the word  Disposed
    }
}

class Program
{
    static void Main()
    {
        var r = new Resource();
        r.Dispose();   // call cleanup by hand for now

        // ✅ Expected output:
        //    Created
        //    Disposed
    }
}

2. The using Statement vs the using Declaration

Calling Dispose() by hand is easy to forget, so C# gives you using to call it for you. There are two forms. The using statement has a { block } and disposes the moment control leaves that block. The using declaration (C# 8+) drops the braces — using var x = ...; — and disposes at the end of the enclosing scope (usually the method). Both guarantee Dispose runs, even if an exception is thrown.

using System;

class Door : IDisposable
{
    private readonly string _room;
    public Door(string room)
    {
        _room = room;
        Console.WriteLine($"Opened {_room} door");
    }
    public void Dispose() => Console.WriteLine($"Closed {_room} door");
}

class Program
{
    static void Main()
    {
        // FORM 1 — the 'using' STATEMENT. It has a { } block.
        // Dispose() runs the instant control leaves the block — even on an exception.
        using (var d = new Door("kitchen"))
        {
            Console.WriteLine("...inside the kitchen...");
        }   // <- "Closed kitchen door" prints HERE, automatically

        Console.WriteLine("--- moved on ---");

        // FORM 2 — the 'using' DECLARATION (C# 8+). No block; no extra indentation.
        // Dispose() runs at the END of the enclosing scope (here, end of Main).
        using var hall = new Door("hall");
        Console.WriteLine("...in the hall...");

        // At the end of Main, 'hall' is disposed automatically — "Closed hall door".
    }
}

// ✅ Expected output:
//    Opened kitchen door
//    ...inside the kitchen...
//    Closed kitchen door
//    --- moved on ---
//    Opened hall door
//    ...in the hall...
//    Closed hall door

Now you try. Add the single keyword that turns the line below into a using declaration, so Dispose() is called for you. Pay attention to the order in the expected output — cleanup runs last.

using System;

class Connection : IDisposable
{
    public Connection() => Console.WriteLine("Connected");
    public void Dispose() => Console.WriteLine("Disconnected");
}

class Program
{
    static void Main()
    {
        Console.WriteLine("Start");

        // 🎯 YOUR TURN — let the compiler call Dispose() for you.

        // 1) Wrap the connection in a 'using' DECLARATION so it disposes
        //    automatically at the end of Main. Add the keyword before 'var'.
        ___ var conn = new Connection();   // 👉 the keyword is  using

        Console.WriteLine("Working...");

        // 2) Nothing else to write — Dispose() is called for you at end of scope.

        // ✅ Expected output (note the ORDER — cleanup comes LAST):
        //    Start
        //    Connected
        //    Working...
        //    Disconnected
    }
}

3. The Standard Dispose Pattern

A one-line Dispose() is plenty for simple classes. But when a class might be inherited from and may hold an unmanaged handle, you use the full pattern. It centres on a protected virtual void Dispose(bool disposing) method that does all the real cleanup, and the disposing flag tells it who called: a person (true — safe to touch other managed objects) or the garbage collector's finalizer (false — only free raw handles). A _disposed guard makes sure cleanup runs at most once.

using System;

// The STANDARD dispose pattern — use this when a class might be inherited
// AND may hold unmanaged resources. It coordinates three callers safely:
// explicit Dispose(), inherited Dispose(), and the finalizer.
class ResourceWrapper : IDisposable
{
    private bool _disposed;        // guard so cleanup runs at most ONCE

    public ResourceWrapper() => Console.WriteLine("Acquired resource");

    public void DoWork()
    {
        // .NET 7+: throws ObjectDisposedException if already disposed.
        ObjectDisposedException.ThrowIf(_disposed, this);
        Console.WriteLine("Doing work");
    }

    // ALL real cleanup happens here. 'disposing' tells us WHO called us:
    //   true  -> a human called Dispose()  -> safe to touch managed objects
    //   false -> the finalizer (GC) called -> only free unmanaged handles
    protected virtual void Dispose(bool disposing)
    {
        if (_disposed) return;                 // already cleaned up — do nothing
        if (disposing)
        {
            // Free MANAGED resources (other IDisposable fields) here.
            Console.WriteLine("Freed managed resources");
        }
        // Free UNMANAGED resources (raw OS handles) here — runs either way.
        Console.WriteLine("Freed unmanaged resources");
        _disposed = true;
    }

    // The public entry point callers use (directly or via 'using').
    public void Dispose()
    {
        Dispose(true);              // do the real cleanup, managed + unmanaged
        GC.SuppressFinalize(this);  // we cleaned up — tell the GC to skip the finalizer
    }

    // The finalizer (note the ~). The SAFETY NET: if someone forgets Dispose(),
    // the GC eventually calls this and at least the unmanaged handle is freed.
    ~ResourceWrapper() => Dispose(false);
}

class Program
{
    static void Main()
    {
        using (var r = new ResourceWrapper())   // Acquired resource
        {
            r.DoWork();                          // Doing work
        }   // Dispose() runs here -> Freed managed -> Freed unmanaged
        Console.WriteLine("Done");
    }
}

// ✅ Expected output:
//    Acquired resource
//    Doing work
//    Freed managed resources
//    Freed unmanaged resources
//    Done

🔎 Deep Dive: finalizers, SuppressFinalize & SafeHandle

A finalizer — written ~ClassName() — is a last-resort cleanup the GC may run on an object before reclaiming it. It's the safety net for the case where someone forgets to call Dispose(). But finalizers are costly: a finalizable object survives an extra GC generation and its cleanup happens on a separate thread at an unpredictable time. That's why, when you do clean up properly, Dispose() calls GC.SuppressFinalize(this) — it tells the GC "I've handled it, skip the finalizer," reclaiming the object sooner and cheaper.

The modern advice is: don't write your own finalizer at all. Instead, wrap the raw OS handle in a SafeHandle (e.g. SafeFileHandle). SafeHandle is a built-in type that already has a correct, reliable finalizer and is hardened against thread-abort and handle-recycling attacks. Your class then just holds a SafeHandle field, implements a simple Dispose() that disposes the handle, and needs no finalizer of its own.

using Microsoft.Win32.SafeHandles;

class FileReader : IDisposable
{
    private readonly SafeFileHandle _handle;   // SafeHandle owns the finalizer

    public void Dispose() => _handle.Dispose(); // no ~FileReader() needed!
}

Rule of thumb: write the full Dispose(bool) + finalizer pattern only if you truly hold a raw handle directly. In real code you almost never should — reach for SafeHandle (or a class that already wraps it, like FileStream) and keep your Dispose() trivially simple.

4. Why using Beats Manual try/finally

You could guarantee cleanup yourself with try/finally — put the work in try and Dispose() in finally, which runs even when an exception is thrown. In fact, that's exactly what the compiler turns a using statement into. So using isn't magic; it's a guaranteed-correct shorthand that you can't forget to write and can't get wrong (no mistyped variable, no missing finally). Read the worked example to see the two are identical.

using System;

class Handle : IDisposable
{
    public Handle() => Console.WriteLine("Acquired");
    public void Use() => throw new InvalidOperationException("boom!");
    public void Dispose() => Console.WriteLine("Released");
}

class Program
{
    static void Main()
    {
        // The 'using' STATEMENT is just sugar for this try/finally.
        // Even though Use() throws, 'finally' still runs Dispose().
        var h = new Handle();          // Acquired
        try
        {
            h.Use();                   // throws an exception
        }
        finally
        {
            h.Dispose();               // Released  <- runs anyway, no leak
        }

        // The line above is EXACTLY what this one-liner compiles to:
        //     using (var h = new Handle()) { h.Use(); }
        // 'using' is shorter, can't be forgotten, and guarantees cleanup.
    }
}

Pro Tips

Common Errors (and the fix)

📋 Quick Reference

TaskCodeNotes
Declare disposableclass C : IDisposablePromises a Dispose()
Cleanup methodpublic void Dispose() { ... }Release resources here
using statementusing (var x = ...) { }Disposes at block end
using declarationusing var x = ...;Disposes at scope end
Pattern coreprotected virtual void Dispose(bool)Shared cleanup
Skip finalizerGC.SuppressFinalize(this)Call inside Dispose()
Finalizer~ClassName() => Dispose(false);Last-resort safety net
Guard after disposeObjectDisposedException.ThrowIf(d, this).NET 7+ fail-fast
Async cleanupawait using var x = ...;For IAsyncDisposable

Frequently Asked Questions

Q: If the GC frees memory automatically, why do I need Dispose at all?

The GC only knows about managed memory, and it runs whenever it chooses. File handles, sockets, and native memory are unmanaged — the GC can't release them on time, and "eventually" is too late when a file stays locked. Dispose gives you cleanup at a moment you control.

Q: What's the difference between a using statement and a using declaration?

A statement has braces — using (var x = ...) { ... } — and disposes at the end of that block. A declaration drops the braces — using var x = ...; — and disposes at the end of the enclosing scope. Use the declaration when the resource lives for the rest of the method.

Q: Do I always need a finalizer when I implement IDisposable?

No — and usually you shouldn't write one. A finalizer is only for the rare class that holds a raw unmanaged handle directly. If you wrap that handle in a SafeHandle (the recommended approach), the SafeHandle provides the finalizer and your class needs none.

Q: What does GC.SuppressFinalize(this) actually do?

It tells the garbage collector "this object's cleanup is already done, don't bother running its finalizer." That lets the GC reclaim the object sooner and avoids the extra cost of finalization. You call it inside Dispose() precisely because Dispose() already did the work.

Q: Is it safe to call Dispose more than once?

It should be — and it's your job to make it so. Guard Dispose(bool) with a _disposed flag (if (_disposed) return;) so the second call quietly does nothing. The framework's own types follow exactly this rule.

Mini-Challenge: the Full Dispose Pattern

No blanks this time — just a brief and an outline to keep you on track. Implement the complete dispose pattern on a TempFile wrapper: a _disposed guard, a protected virtual Dispose(bool), a public Dispose() that calls it and suppresses finalization, and a finalizer that falls back to it. Then drive it from a using in Main and check your output against the expected lines.

using System;

// 🎯 MINI-CHALLENGE: implement the FULL dispose pattern on a resource wrapper.
//
// Build a class 'TempFile' that:
//   1. Implements IDisposable.
//   2. Has a private bool field '_disposed' (the run-once guard).
//   3. In its constructor, prints "Temp file created".
//   4. Has  protected virtual void Dispose(bool disposing):
//        - if _disposed is already true, return immediately.
//        - if 'disposing' is true, print "Cleaning managed state".
//        - always print "Deleting temp file".
//        - set _disposed = true.
//   5. Has  public void Dispose()  that calls Dispose(true)
//      and then  GC.SuppressFinalize(this).
//   6. Has a finalizer  ~TempFile()  that calls Dispose(false).
//
// In Main: wrap a TempFile in a 'using' statement or declaration so it
// disposes automatically.
//
// ✅ Expected output:
//    Temp file created
//    Cleaning managed state
//    Deleting temp file

class TempFile // : IDisposable
{
    // your fields, constructor, Dispose(bool), Dispose() and finalizer here
}

class Program
{
    static void Main()
    {
        // create a TempFile inside a 'using' here
    }
}

🎉 Lesson Complete

Practice quiz

How many methods does the IDisposable interface require you to implement?

  • Zero
  • Two - Dispose() and Finalize()
  • Exactly one - Dispose()
  • Three

Answer: Exactly one - Dispose(). IDisposable is a one-method contract: void Dispose().

When does a 'using' statement (with a block) call Dispose()?

  • The moment control leaves the block
  • At the end of the program
  • Only if an exception is thrown
  • When the GC runs

Answer: The moment control leaves the block. A using statement disposes the instant control leaves its braces, even on an exception.

When does a 'using' declaration (using var x = ...;) call Dispose()?

  • Immediately on the next line
  • Never
  • Only when GC.Collect is called
  • At the end of the enclosing scope

Answer: At the end of the enclosing scope. A using declaration disposes at the end of the enclosing scope, usually the end of the method.

In the standard dispose pattern, what does the 'disposing' parameter of Dispose(bool) tell you?

  • Whether the object is null
  • Whether a human called Dispose (true) or the finalizer called it (false)
  • How much memory to free
  • Whether to throw an exception

Answer: Whether a human called Dispose (true) or the finalizer called it (false). disposing=true means a caller invoked Dispose (safe to touch managed objects); false means the finalizer called it.

What is a finalizer (written ~ClassName()) for?

  • A last-resort cleanup if someone forgets to call Dispose
  • Speeding up the GC
  • Allocating unmanaged memory
  • Replacing the constructor

Answer: A last-resort cleanup if someone forgets to call Dispose. The finalizer is the GC's safety net that frees unmanaged resources if Dispose is never called.

What does GC.SuppressFinalize(this) do, called inside Dispose()?

  • Forces the GC to run now
  • Disposes all other objects
  • Tells the GC to skip the finalizer because cleanup is already done
  • Disables the garbage collector

Answer: Tells the GC to skip the finalizer because cleanup is already done. Once Dispose has cleaned up, SuppressFinalize tells the GC to skip the finalizer, reclaiming the object sooner.

A 'using' statement is essentially syntactic sugar for which construct?

  • A for loop
  • try/finally with Dispose() in finally
  • A lock statement
  • A switch expression

Answer: try/finally with Dispose() in finally. The compiler turns a using statement into a try/finally that calls Dispose in the finally block.

What is the modern recommendation instead of writing your own finalizer for a raw OS handle?

  • Call GC.Collect manually
  • Never dispose at all
  • Use a static field
  • Wrap the handle in a SafeHandle

Answer: Wrap the handle in a SafeHandle. SafeHandle already has a correct, reliable finalizer; wrap the handle in it and skip writing ~ClassName().

How do you make Dispose() safe to call more than once?

  • Throw on the second call
  • Guard with a _disposed flag (if (_disposed) return;)
  • Make it static
  • Call it from the constructor

Answer: Guard with a _disposed flag (if (_disposed) return;). A _disposed guard makes the second call a harmless no-op, so double-dispose can't corrupt state.

Why does a file handle held 'until the GC gets round to it' cause problems?

  • The GC never runs
  • Files don't use handles
  • Cleanup timing is non-deterministic, so the file can stay locked far too long
  • The GC frees it instantly

Answer: Cleanup timing is non-deterministic, so the file can stay locked far too long. The GC runs whenever it likes, so unmanaged resources need deterministic cleanup via Dispose.

Continue this course