IDisposable
C#
Memory Management
Software Design
Resource Management

How do you prevent IDisposable from spreading to all your classes?

Data Structures & Algorithms practice on Codemia

Step through 300 algorithm problems with animated visualisers that show the data structure changing as the code runs.

Practice algorithms

Introduction

When a class implements IDisposable, every class that owns it must also implement IDisposable, creating a "viral" spread through your codebase. To prevent this, limit IDisposable to the lowest possible level by using dependency injection (let the DI container manage disposal), wrapping disposable resources in using blocks at the point of use, encapsulating disposal behind non-disposable abstractions, or using factory/pool patterns that centralize resource lifecycle management.

The Problem

csharp
1// FileLogger is IDisposable because it owns a StreamWriter
2public class FileLogger : IDisposable
3{
4    private readonly StreamWriter _writer;
5    public FileLogger(string path) => _writer = new StreamWriter(path);
6    public void Log(string msg) => _writer.WriteLine(msg);
7    public void Dispose() => _writer.Dispose();
8}
9
10// OrderService must be IDisposable because it owns FileLogger
11public class OrderService : IDisposable
12{
13    private readonly FileLogger _logger;
14    public OrderService() => _logger = new FileLogger("orders.log");
15    public void ProcessOrder() => _logger.Log("Order processed");
16    public void Dispose() => _logger.Dispose();
17}
18
19// OrderController must be IDisposable because it owns OrderService
20public class OrderController : IDisposable
21{
22    private readonly OrderService _service;
23    public OrderController() => _service = new OrderService();
24    public void Dispose() => _service.Dispose();
25}
26
27// IDisposable spreads up the entire chain!

Strategy 1: Dependency Injection (Best)

Let the DI container manage the disposable object's lifetime:

csharp
1// Define a non-disposable interface
2public interface ILogger
3{
4    void Log(string message);
5}
6
7// Implementation is IDisposable
8public class FileLogger : ILogger, IDisposable
9{
10    private readonly StreamWriter _writer;
11    public FileLogger(string path) => _writer = new StreamWriter(path);
12    public void Log(string msg) => _writer.WriteLine(msg);
13    public void Dispose() => _writer.Dispose();
14}
15
16// OrderService does NOT implement IDisposable
17public class OrderService
18{
19    private readonly ILogger _logger;
20
21    public OrderService(ILogger logger)
22    {
23        _logger = logger;  // Does not own the logger
24    }
25
26    public void ProcessOrder() => _logger.Log("Order processed");
27}
28
29// DI container handles disposal
30services.AddSingleton<ILogger>(new FileLogger("app.log"));
31services.AddTransient<OrderService>();
32// Container disposes FileLogger when the application shuts down

OrderService depends on ILogger (not IDisposable), so it does not need Dispose().

Strategy 2: using Blocks at the Point of Use

Create and dispose resources locally instead of storing them as fields:

csharp
1public class DataExporter
2{
3    private readonly string _connectionString;
4
5    public DataExporter(string connectionString)
6    {
7        _connectionString = connectionString;
8    }
9
10    public void Export(string query)
11    {
12        // Resource created and disposed locally — DataExporter is NOT IDisposable
13        using var connection = new SqlConnection(_connectionString);
14        connection.Open();
15        using var command = new SqlCommand(query, connection);
16        using var reader = command.ExecuteReader();
17
18        while (reader.Read())
19        {
20            // Process rows
21        }
22    }  // connection, command, reader all disposed here
23}

DataExporter does not hold a SqlConnection as a field, so it does not need IDisposable.

Strategy 3: Factory Pattern

Delegate resource creation to a factory that the caller disposes:

csharp
1public interface IConnectionFactory
2{
3    SqlConnection Create();
4}
5
6public class ConnectionFactory : IConnectionFactory
7{
8    private readonly string _connectionString;
9    public ConnectionFactory(string cs) => _connectionString = cs;
10
11    public SqlConnection Create() => new SqlConnection(_connectionString);
12}
13
14public class UserRepository
15{
16    private readonly IConnectionFactory _factory;
17
18    public UserRepository(IConnectionFactory factory)
19    {
20        _factory = factory;  // Factory is not IDisposable
21    }
22
23    public User GetUser(int id)
24    {
25        using var conn = _factory.Create();  // Caller disposes the connection
26        conn.Open();
27        // Query and return user
28        return new User();
29    }
30}

The factory itself is not IDisposable — only the connections it creates are.

Strategy 4: Encapsulate Behind a Non-Disposable Wrapper

Hide the disposable resource behind a wrapper that manages its own lifecycle:

csharp
1public class MessageQueue
2{
3    private readonly string _endpoint;
4
5    public MessageQueue(string endpoint) => _endpoint = endpoint;
6
7    public void Send(string message)
8    {
9        // Create, use, and dispose the connection within the method
10        using var client = new HttpClient();
11        client.PostAsync(_endpoint, new StringContent(message)).Wait();
12    }
13}
14
15// MessageQueue is NOT IDisposable
16// Callers don't know or care about HttpClient

Strategy 5: Object Pool

For expensive disposable resources, pool them instead of creating/disposing per use:

csharp
1public class ConnectionPool
2{
3    private readonly ConcurrentBag<SqlConnection> _pool = new();
4    private readonly string _connectionString;
5
6    public ConnectionPool(string cs) => _connectionString = cs;
7
8    public SqlConnection Rent()
9    {
10        if (_pool.TryTake(out var conn))
11            return conn;
12        return new SqlConnection(_connectionString);
13    }
14
15    public void Return(SqlConnection conn) => _pool.Add(conn);
16}
17
18public class OrderRepository
19{
20    private readonly ConnectionPool _pool;
21
22    public OrderRepository(ConnectionPool pool) => _pool = pool;
23
24    public void SaveOrder(Order order)
25    {
26        var conn = _pool.Rent();
27        try
28        {
29            conn.Open();
30            // Save order
31        }
32        finally
33        {
34            _pool.Return(conn);  // Return to pool, not dispose
35        }
36    }
37}

When IDisposable Should Spread

Not all spreading is bad. If a class truly owns a disposable resource (exclusively responsible for its lifetime), it should implement IDisposable:

csharp
1// This is correct — FileProcessor owns and must dispose the stream
2public class FileProcessor : IDisposable
3{
4    private readonly FileStream _stream;
5
6    public FileProcessor(string path)
7    {
8        _stream = File.OpenRead(path);  // Owns this resource
9    }
10
11    public void Dispose() => _stream.Dispose();
12}

The goal is not to eliminate IDisposable entirely, but to push it down to the smallest scope possible.

Common Pitfalls

  • Making every class IDisposable just because it uses a disposable dependency: Only implement IDisposable if the class owns (creates and is responsible for) the disposable resource. If a dependency is injected, the injector is responsible for disposal, not the consumer.
  • Storing HttpClient as a disposable field: HttpClient is designed to be reused and should not be created/disposed per request. Use IHttpClientFactory in ASP.NET Core, which manages HttpClient lifetimes and pooling without requiring your classes to be IDisposable.
  • Implementing IDisposable without the full dispose pattern: If your class has a finalizer or is intended for inheritance, implement the full dispose pattern with Dispose(bool disposing) and GC.SuppressFinalize. For sealed classes with only managed resources, a simple Dispose() is sufficient.
  • Forgetting to dispose injected resources when the container does not manage them: If you manually create a disposable object outside a DI container, someone must dispose it. Unmanaged resources leak silently — there is no compiler warning for undisposed objects.
  • Using Dispose for application logic (not just cleanup): Dispose should only release resources (close files, connections, handles). Do not put business logic like "save final state" or "send shutdown notification" in Dispose — it may not be called if an exception occurs before the using block completes.

Summary

  • Use dependency injection to let the DI container manage disposable lifetimes
  • Prefer using blocks at the point of use instead of storing disposable fields
  • Hide disposable resources behind non-disposable interfaces or wrappers
  • Use factory patterns so callers create and dispose resources locally
  • Only implement IDisposable when your class truly owns the resource

Related reading
Course
Intermediate
27 lessons
15 hours
DSA Fundamentals

Master algorithmic patterns and data structures through hands-on LeetCode-style problems - from arrays and hashing to dynamic programming and advanced graphs.

View the course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

Data Structures & Algorithms practice on Codemia

Step through 300 algorithm problems with animated visualisers that show the data structure changing as the code runs.

Practice algorithms

All Rights Reserved.