Skip to main content

30. IDisposable and Resource Management

Level: Intermediate to Advanced
Goal: Learn to properly clean up resources (files, connections) when no longer needed.

Some things in a program need cleanup:

  • File handles (close file)
  • Database connections (close connection)
  • HTTP clients (release connection)

Forgetting cleanup wastes resources. IDisposable helps you clean up automatically.

ℹ️ What You'll Learn
  • What IDisposable is and why it matters
  • Managed vs unmanaged resources
  • using statement and using declarations
  • Implementing Dispose pattern correctly
  • Finalizers and GC.SuppressFinalize
  • When to implement IDisposable
  • Best practices for cleanup

Quick Definitions

  • IDisposable - Interface for cleaning up resources (files, connections)
  • Dispose() - Method that releases resources (close file, disconnect DB)
  • using statement - Auto-closes resource when block exits (C# 7)
  • using declaration - Auto-closes resource when variable scope ends (C# 8+)
  • Unmanaged resource - OS handle like file, socket, database connection
  • Managed resource - .NET object like List, string (GC cleans automatically)
  • Finalizer - Safety net: called by GC if Dispose wasn't called
  • Resource leak - Forgetting Dispose locks file, wastes connection slots
  • StreamWriter - File writer that needs Dispose to close file
  • SqlConnection - Database connection that needs Dispose to return to pool

IDisposable Reference

ConceptPurposeWhen UsedExample
IDisposableInterface for cleanupUnmanaged resourcesImplement to release resources
using statementAuto-dispose in blockC# 7 and earlierusing (var x = new File()) { }
using declarationAuto-dispose when scope endsC# 8+ (preferred)using var x = new File();
Dispose()Release resourcesImplement IDisposableClose files, connections, handles
~FinalizerSafety net cleanupRarely needed nowCalled by GC if Dispose not called
GC.SuppressFinalizeSkip finalizerIn Dispose()Don't call finalizer if disposed
Managed resource.NET object (List, string)Auto-cleaned by GCNo special cleanup needed
Unmanaged resourceOS handle (file, socket)Requires Dispose()File pointer, database connection

The Problem

// [X] Resource leak - file handle never released
static void SaveAttendance(string data)
{
var writer = new StreamWriter("attendance.csv");
writer.WriteLine(data);
// writer never disposed - file locked until GC runs (unpredictable)
}

using Statement - Auto Dispose

// ? Always disposed - even if exception occurs
static void SaveAttendance(string data)
{
using (var writer = new StreamWriter("attendance.csv"))
{
writer.WriteLine(data);
} // Dispose() called here automatically

// Same with database connections (with EF Core / ADO.NET)
using (var conn = new SqlConnection(connectionString))
{
conn.Open();
// use connection
} // connection closed and returned to pool
}

// C# 8+ - using declaration (cleaner)
static void SaveAttendanceModern(string data)
{
using var writer = new StreamWriter("attendance.csv");
writer.WriteLine(data);
// Dispose() called when writer goes out of scope
}

Implementing IDisposable - School File Writer

public class AttendanceExporter : IDisposable
{
private StreamWriter? _writer;
private bool _disposed = false;
private readonly string _filePath;

public AttendanceExporter(string filePath)
{
_filePath = filePath;
_writer = new StreamWriter(filePath, append: true);
_writer.AutoFlush = true;
}

public void WriteRecord(string studentName, bool present, DateTime date)
{
if (_disposed)
throw new ObjectDisposedException(nameof(AttendanceExporter));

_writer!.WriteLine($"{date:yyyy-MM-dd},{studentName},{(present ? "P" : "A")}");
}

public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this); // tell GC: no need to call finalizer
}

protected virtual void Dispose(bool disposing)
{
if (_disposed) return;

if (disposing)
{
_writer?.Flush();
_writer?.Dispose(); // release managed resource
_writer = null;
}

_disposed = true;
}

~AttendanceExporter() // finalizer - safety net if Dispose not called
{
Dispose(false);
}
}

Usage

// using ensures Dispose always called
using var exporter = new AttendanceExporter(@"C:\School\attendance.csv");

foreach (var (student, present) in attendanceData)
exporter.WriteRecord(student.Name, present, DateTime.Today);

// Dispose() called here - file closed, handle released

async Dispose - IAsyncDisposable

💻 Try It — Console App
💡 Paste into Program.cs and press F5⌥ GitHub
public class AsyncAttendanceExporter : IAsyncDisposable
{
private StreamWriter? _writer;

public AsyncAttendanceExporter(string path)
=> _writer = new StreamWriter(path);

public async Task WriteAsync(string line)
=> await _writer!.WriteLineAsync(line);

public async ValueTask DisposeAsync()
{
if (_writer != null)
{
await _writer.FlushAsync();
await _writer.DisposeAsync();
_writer = null;
}
}
}

// await using
await using var exporter = new AsyncAttendanceExporter("log.csv");
await exporter.WriteAsync("Sahasra Kumar,Present");

When You'll Use This in SMS

SMS opens and closes resources constantly:

// Read student CSV file
using var reader = new StreamReader("students.csv");
foreach (var line in reader.ReadToEnd().Split('\n'))
ImportStudent(line); // Auto-closes when block ends

// Database operation
using (var connection = new SqlConnection(connStr))
{
connection.Open();
// queries...
} // Auto-closes connection, returns to pool

// Report generation with file
using var writer = new StreamWriter("report.pdf");
// write report
// Auto-closes file when scope ends

Real impact: Without Dispose = files locked, database connections stuck in pool, memory leaks. With using = clean, guaranteed cleanup.


ℹ️ Video Tutorial

IDisposable explained: Dispose pattern, using statements, using declarations, unmanaged resources, StreamWriter, SqlConnection cleanup. Video coming soon. Subscribe to NexCoding YouTube for updates.


Common Mistakes

? Not disposing resources (resource leak):

var file = new StreamWriter("data.csv");
file.WriteLine("data");
// Never closed - file locked until GC runs (unpredictable)

? Use using statement:

using var file = new StreamWriter("data.csv");
file.WriteLine("data");
// Disposed here automatically

? Not implementing IDisposable when holding unmanaged resources:

public class FileProcessor
{
private FileStream _file; // Unmanaged resource!
// No IDisposable, no Dispose() - resource leak
}

? Implement IDisposable:

public class FileProcessor : IDisposable
{
private FileStream _file;
public void Dispose()
{
_file?.Dispose();
}
}

? Calling Dispose() multiple times (can crash):

var file = new StreamWriter("data.csv");
file.Dispose();
file.Dispose(); // Second call might throw

? Check if already disposed:

public void Dispose()
{
if (_disposed) return; // Already disposed
_file?.Dispose();
_disposed = true;
}

? Forgetting GC.SuppressFinalize in Dispose():

public void Dispose()
{
_file?.Dispose();
// Finalizer still called by GC - waste of resources
}

? Always call GC.SuppressFinalize:

public void Dispose()
{
_file?.Dispose();
GC.SuppressFinalize(this); // Skip finalizer
}

Best Practices

  1. Use using declaration (C# 8+) - Cleaner than using statement
  2. Implement IDisposable for unmanaged resources - File, connection, socket
  3. Always call GC.SuppressFinalize - In Dispose() method
  4. Check _disposed flag - Prevent double-dispose errors
  5. Dispose managed resources - Close files, connections before setting to null
  6. Use IAsyncDisposable for async cleanup - For async Dispose operations
  7. Document what gets disposed - Comments explain cleanup
  8. Dispose in finally or using - Guarantee cleanup even on exception
  9. Avoid finalizers - Modern dispose pattern doesn't need them
  10. Don't hold unmanaged resources long - Dispose ASAP

🎯 Q1: Why do we need IDisposable if GC exists?

GC frees managed memory (.NET objects). But unmanaged resources (file handles, DB connections, sockets) need EXPLICIT release.

GC doesn't know about OS handles. File stays locked, connection stays open, memory not freed until finalizer runs (slow, unpredictable).

// GC can free Student object, but not file handle
using var file = new StreamWriter("data.csv");
var s = new Student { }; // GC cleans this
// Using statement closes file IMMEDIATELY - not waiting for GC

IDisposable = guarantee resources released ASAP, not "whenever GC feels like it."

🎯 Q2: When should I implement IDisposable?

Implement when class holds resources that must be released: files, connections, sockets, unmanaged memory.

DON'T implement if class only has managed objects (strings, Lists, other .NET objects).

// NEED IDisposable
public class DbContext
{
private SqlConnection _conn; // Unmanaged - need Dispose
}

// DON'T NEED IDisposable
public class Student
{
public string Name; // Managed - GC handles
public List<int> Marks; // Managed - GC handles
}
🎯 Q3: What's the difference between using statement and using declaration?

using statement (C# 7): Block scope. Dispose at end of block.

using declaration (C# 8+): Scope scope. Dispose at end of method/function.

// Statement - block scope
using (var file = new StreamWriter("data.csv"))
{
file.WriteLine("data");
} // Disposed here

// Declaration - method scope (preferred)
using var file = new StreamWriter("data.csv");
file.WriteLine("data");
// Disposed when method ends

Declaration is cleaner, modern C#. Use it.

🎯 Q4: What's the point of finalizers in Dispose pattern?

Finalizer = safety net. If someone forgets to Dispose(), finalizer runs eventually (called by GC) to release resources.

~AttendanceExporter() // Finalizer - safety net
{
Dispose(false); // Release resources
}

But finalizers are slow, unpredictable. Modern pattern: don't rely on finalizers, always use using.

With using, Dispose() called immediately, GC.SuppressFinalize() skips finalizer = efficient.

🎯 Q5: Why use IAsyncDisposable?

IAsyncDisposable for cleanup that's async (flushing, async close, etc).

public async ValueTask DisposeAsync()
{
await _writer.FlushAsync(); // Async operation
await _writer.DisposeAsync();
}

// Use with await using
await using var exporter = new AsyncExporter("file.csv");
// DisposeAsync() called, awaited automatically

Use when Dispose() needs async operations (most cases use sync Dispose).

🎯 Q6: What happens if I don't call Dispose()?

Resources stay locked/open until finalizer runs (slow, unpredictable).

Example: File locked by abandoned StreamWriter, other processes can't access it.

// WRONG - file never closed
void SaveData()
{
var file = new StreamWriter("data.csv");
file.WriteLine("data");
// file.Dispose() never called
// File stays locked until GC finalizes
}

// RIGHT - file closed immediately
void SaveData()
{
using var file = new StreamWriter("data.csv");
file.WriteLine("data");
} // file.Dispose() called here

Always use using, don't rely on GC.

🤖Use AI to Learn Faster

Use ChatGPT, Claude, or Copilot to go deeper on C# IDisposable and memory management. Try these prompts:

  • "Explain IDisposable in C# - why do we need it when GC exists?"
  • "What is the difference between Dispose and Finalize in C#?"
  • "When should I implement IDisposable in my own classes?"
  • "What resources MUST be disposed in a .NET backend application?"

💡 Tip: After reading this article, paste your own code into AI and ask "What could go wrong here and why?" — fastest way to find edge cases and deepen understanding.

Next Article

Multithreading ->

nexcoding.in