Dispose pattern in .NET

Implementing IDisposable and IAsyncDisposable interfaces

When working with .NET, you’ll often hear about the garbage collector and how it manages the allocation and release the application’s memory from the managed heap, making our life easier.

This is true for the majority of objects but sometimes we have to work with unmanaged resources — files, network or database connections — that we must explicitly release since the garbage collector doesn’t know how to do the cleanup for us, despite being able to track the object that encapsulates the unmanaged resource.

To help preventing memory leaks, .NET provides a simple and standard way to cleanup unmanaged resources called dispose pattern, which consists in implementing the IDisposable interface and calling Dispose method when resources aren’t needed, which is usually done via the using keyword.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
using (var file = File.OpenText(filePath))
{
var fileContent = file.ReadToEnd();
}

// equivalent code
var file = File.OpenText(filePath)
try
{
var fileContent = file.ReadToEnd();
}
finally
{
file.Dispose();
}

When .NET Core 3.0 was released, Microsoft introduced the interface IAsyncDisposable to allow for asynchronous cleanup operations by calling the DisposeAsync method, usually done with await using keywords.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
await using (var x = new SomeAsyncDisposableClass())
{
// do stuff
}

// equivalent code
var x = new SomeAsyncDisposableClass();
try
{
// do stuff
}
finally
{
await x.DisposeAsync();
}

If a class implements IAsyncDisposable it’s usually a good practice to also implement IDisposable despite not being a requirement. Not all code runs asynchronously, so this ensures the class is ready for both scenarios.

In this article I’m going to demonstrate and explain step by step how to properly implement the dispose pattern using both IDisposable and IAsyncDisposable interfaces.

Example scenario

To use as an example, we are going to implement a class that wraps SQL Server connections and uses Dapper to simplify mappings and every time someone queries the database it will write the SQL instruction to the log. This provides a simple example of a class that holds both managed and unmanaged resources that can also be disposed asynchronously — SQL Server network connection.

The class without any disposable implementations:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class SqlServerQueryRunner
{
private ILogger<SqlServerQueryRunner> _logger;
private SqlConnection _connection;

public SqlServerQueryRunner(
ILogger<SqlServerQueryRunner> logger,
string connectionString
)
{
ArgumentNullException.ThrowIfNull(logger);
ArgumentNullException.ThrowIfNull(connectionString);

_logger = logger;
_connection = new SqlConnection(connectionString);
}

public async ValueTask<IEnumerable<T>> QueryAsync<T>(CancellationToken ct, string sql, object parameters = null)
{
_logger.LogDebug("Querying database: {Sql}", sql);

// using Dapper for simplicity
return await _connection.QueryAsync<T>(new CommandDefinition(
sql,
parameters,
cancellationToken: ct
));
}
}

As you can see, this class receives a connection string and creates a SQL Server connection. Because this class is the owner of the SqlConnection instance, it must dispose it or else resources won’t be released.

IDisposable interface

Implement the IDisposable interface by disposing unmanaged resources inside the Dispose method, in this case the SQL Server connection:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class SqlServerQueryRunner : IDisposable
{
private ILogger<SqlServerQueryRunner> _logger;
private SqlConnection _connection;

// ...

public void Dispose()
{
_connection.Dispose();
}

// ...
}

This code looks good and will work as expected, but there are some scenarios to consider:

  1. Will the class be sealed? If not, we must provide a way for extenders to cleanup their unmanaged resources, if any;
  2. Do I want to a safeguard from forgetting to dispose? If yes, we must implement a class finalizer that will try to cleanup resources when garbage collected;

Both scenarios are solved by creating a virtual Dispose method, to be overridden by extenders, that receives a flag to indicate if it’s being called from a dispose or finalizer.

This usually means resources are explicitly disposed only when disposing, otherwise references are cleared and we assume unmanaged resources also have finalizers.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class SqlServerQueryRunner : IDisposable
{
private ILogger<SqlServerQueryRunner> _logger;
private SqlConnection _connection;

// ...

~SqlServerQueryRunner() => Dispose(false);

public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}

protected virtual void Dispose(bool disposing)
{
if (disposing)
_connection?.Dispose();

_connection = null;
_logger = null;
}

// ...
}

You now may be asking: But if we have finalizers that will be triggered when the class is garbage collected, why implement IDisposable?

That’s a fair question with a simple answer: You want garbage collection to be as fast as possible because it is a synchronous operation that stops all processes while it’s running.

This also means unmanaged resources wouldn’t be released until garbage collection happened, which may take some time — imagine a reference to a file that you didn’t need but the process would still be locking it, now you want to open it again but can’t, because the garbage collector haven’t run yet.

If you look at the Dispose method, you’ll realize there’s a call to GC.SuppressFinalize, which is a method that actually tells the garbage collector: look, this class has a finalizer but you can ignore it because it is irrelevant, the developer already released resources using the dispose pattern.

IAsyncDisposable interface

Implementing the IAsyncDisposable interface is very similar to IDisposable with a small difference, we call Dispose(false) inside the method DisposeAsync to keep functional equivalence with the synchronous dispose pattern and further ensuring that finalizer code paths are still invoked. There’s no need to dispose resources synchronously if they were already disposed asynchronously.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class SqlServerQueryRunner : IDisposable, IAsyncDisposable
{
private ILogger<SqlServerQueryRunner> _logger;
private SqlConnection _connection;

// ...
public async ValueTask DisposeAsync()
{
await DisposeAsyncCore().ConfigureAwait(false);

Dispose(false);
GC.SuppressFinalize(this);
}

protected virtual async ValueTask DisposeAsyncCore()
{
if(_connection is not null)
await _connection.DisposeAsync().ConfigureAwait(false);
}

// ...
}

As a small note, if the class was sealed, the method DisposeAsyncCore wouldn’t be needed and all code could be inside the DisposeAsync method.

ObjectDisposedException

When implementing a disposable class it’s also a good practice to throw an ObjectDisposedException when it’s been disposed but some code is still trying to use it.

This usually is as simple as creating a flag to be set by the Dispose(bool) method and checking for it on all methods that may apply.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public class SqlServerQueryRunner : IDisposable, IAsyncDisposable
{
private ILogger<SqlServerQueryRunner> _logger;
private SqlConnection _connection;
private bool _disposed;

// ...

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

if (disposing)
_connection?.Dispose();

_connection = null;
_logger = null;

_disposed = true;
}

// ...

public async ValueTask<IEnumerable<T>> QueryAsync<T>(CancellationToken ct, string sql, object parameters = null)
{
ObjectDisposedException.ThrowIf(_disposed, this);

_logger.LogDebug("Querying database: {Sql}", sql);

// using Dapper for simplicity
return await _connection.QueryAsync<T>(new CommandDefinition(
sql,
parameters,
cancellationToken: ct
));
}
}

Conclusion

In this article I explained the dispose pattern in .NET, which is used to cleanup and release unmanaged resources from memory, either synchronously by implementing the IDisposable interface, or asynchronously with IAsyncDisposable.

For the example we used a simple wrapper for SqlConnection that holds references to both unmanaged and managed resources, making sure both were properly cleared and an ObjectDisposedException was thrown if the class was used after being disposed.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
public class SqlServerQueryRunner : IDisposable, IAsyncDisposable
{
private ILogger<SqlServerQueryRunner> _logger;
private SqlConnection _connection;
private bool _disposed;

public SqlServerQueryRunner(
ILogger<SqlServerQueryRunner> logger,
string connectionString
)
{
ArgumentNullException.ThrowIfNull(logger);
ArgumentNullException.ThrowIfNull(connectionString);

_logger = logger;
_connection = new SqlConnection(connectionString);
}

#region IDisposable

~SqlServerQueryRunner() => Dispose(false);

public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}

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

if (disposing)
_connection?.Dispose();

_connection = null;
_logger = null;

_disposed = true;
}

#endregion

#region IAsyncDisposable

public async ValueTask DisposeAsync()
{
await DisposeAsyncCore().ConfigureAwait(false);

Dispose(false);
GC.SuppressFinalize(this);
}

protected virtual async ValueTask DisposeAsyncCore()
{
if (_disposed)
return;

if (_connection is not null)
await _connection.DisposeAsync().ConfigureAwait(false);
}

#endregion

public async ValueTask<IEnumerable<T>> QueryAsync<T>(CancellationToken ct, string sql, object parameters = null)
{
ObjectDisposedException.ThrowIf(_disposed, this);

_logger.LogDebug("Querying database: {Sql}", sql);

// using Dapper for simplicity
return await _connection.QueryAsync<T>(new CommandDefinition(
sql,
parameters,
cancellationToken: ct
));
}
}