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 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
| Concept | Purpose | When Used | Example |
|---|---|---|---|
| IDisposable | Interface for cleanup | Unmanaged resources | Implement to release resources |
| using statement | Auto-dispose in block | C# 7 and earlier | using (var x = new File()) { } |
| using declaration | Auto-dispose when scope ends | C# 8+ (preferred) | using var x = new File(); |
| Dispose() | Release resources | Implement IDisposable | Close files, connections, handles |
| ~Finalizer | Safety net cleanup | Rarely needed now | Called by GC if Dispose not called |
| GC.SuppressFinalize | Skip finalizer | In Dispose() | Don't call finalizer if disposed |
| Managed resource | .NET object (List, string) | Auto-cleaned by GC | No special cleanup needed |
| Unmanaged resource | OS 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
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.
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
- Use using declaration (C# 8+) - Cleaner than using statement
- Implement IDisposable for unmanaged resources - File, connection, socket
- Always call GC.SuppressFinalize - In Dispose() method
- Check _disposed flag - Prevent double-dispose errors
- Dispose managed resources - Close files, connections before setting to null
- Use IAsyncDisposable for async cleanup - For async Dispose operations
- Document what gets disposed - Comments explain cleanup
- Dispose in finally or using - Guarantee cleanup even on exception
- Avoid finalizers - Modern dispose pattern doesn't need them
- Don't hold unmanaged resources long - Dispose ASAP
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."
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
}
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.
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.
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).
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 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.