C# IDisposable
Interface is an excellent tool for managing the cleanup of resources in a class. Did you know that IDisposable
C# code now has async
support? This tutorial will cover how to implement IDisposable
in C#. Then, we will compare our C# IDisposable
example with its async
alternative.
Let’s get disposing ?
What is the C# IDisposable Interface?
The C# IDisposable interface is an interface that helps to facilitate the cleanup of unmanaged resources. An unmanaged resource typically wraps an operating system object, such as a file or database connection.
The CodeSloth blog treats test-specific ElasticSearch/OpenSearch indices as unmanaged resources. These live outside of the management of the Common Language Runtime, yet we want to bind their lifetime to tests within this runtime. Check out the TestIndex
concept in the Search code samples on GitHub for a complete example. We’ll cover a snippet of it below.
C# IDisposable vs Finalize (Destructors)
The primary difference between IDisposable
and Finalizers
is that the Dispose()
method must be invoked manually, whereas the .Net Garbage Collector can only invoke a finalizer. This can impact performance because the garbage collection time for a given resource is unknown. For this reason, you should prefer IDisposable
over Finalize
.
There are multiple ways that you can invoke Dispose()
.
Explicitly Calling Dispose() on C# IDisposable
public class ExampleDisposable : IDisposable { public void Dispose() { ... } } public class Application { public void RunApp() { var disposable = new ExampleDisposable(); disposable.Dispose(); } }
This method is not ideal because it is possible to forget to invoke the Dispose() method. Furthermore, if the application throws an exception the call to Dispose()
will not happen and your unmanaged resources will not be cleaned up. Littering your code with try/finally
blocks will deepen nesting and increase cognitive complexity.
Failing to clean up resources is especially undesirable in the case of our ElasticSearch tests. We would have to manually remove them from the cluster if automated resource management failed.
C# IDisposable Example Using Statement
To avoid the Dispose()
method being skipped by an exception, you can use a using
statement that scopes the lifetime of the IDisposable
object to a given block.
public class ExampleDisposable : IDisposable { public void Dispose() { ... } } public class Application { public void RunApp() { using (var disposable = new ExampleDisposable()) { // Do something in here } } }
This is preferable to the prior example because the compiler will create Intermediary Language that produces a try/finally
, which ensures that the disposable object’s Dispose()
method is guaranteed to be called when the resource goes out of the block’s scope.
Using Declaration for IDisposable in C#
The best way to consume a disposable object is by using a using declaration
.
public class ExampleDisposable : IDisposable { public void Dispose() { ... } } public class Application { public void RunApp() { using var disposable = new ExampleDisposable(); // Do something in here // disposable is automatically cleaned up } }
A using declaration
is similar to a using statement
, except it avoids the nesting caused by curly braces. Instead of explicitly scoping the disposable resource within braces, this approach will scope the disposable to the method, effectively wrapping the scope of the variable in a try/finally
to ensure that Dispose()
is called when the object goes out of scope. Even if an exception is thrown.
How do I Implement the IDisposable C# Interface?
The IDisposable interface is straightforward to implement. Let’s take our TestIndex
as an example.
using Elasticsearch.Net; using Nest; namespace ElasticSearchTestInfrastructure { public class ElasticsearchTestIndex : IDisposable { private readonly IElasticClient ElasticClient; public string Name { get; init; } = Guid.NewGuid().ToString(); public ElasticsearchTestIndex(IElasticClient elasticClient) { ElasticClient = elasticClient; } ... public async void Dispose() { try { await ElasticClient.Indices.DeleteAsync(Name); } catch (Exception ex) { // Swallow the exception here - we tried our best to tidy up } GC.SuppressFinalize(this); } } }
Your class simply needs to implement IDisposable
and provide a definition for the void Dispose()
method. This method can perform whatever logic you require to release unmanaged resources.
In this example, we are tearing down an ElasticSearch index. Can you spot the problem? We’ll discuss this shortly.
Increasing C# IDisposable Performance
Suppose you are explicitly implementing the IDisposable interface and are sure that your unmanaged resources are being released by it. In that case, there is no need for the implicit finalize method to be called.
To stop the redundant call and increase app performance, be sure to call
GC.SuppressFinalize(this);
This will tell the garbage collector that it does not need to invoke the finalize method of the class.
IDisposable in C# is Not Asynchronous
In the example above, the code implemented the Dispose()
method with async void
. This is because the DeleteAsync
method allows us to leverage await
, which requires a method to be async
. Unfortunately, this does not give the caller of the method the ability to await
a result, and the code will continue to run (or terminate) without regard for the completion of the async method call.
This means our Dispose()
method above may not successfully tear down the index before a given test finishes or the class is disposed of.
For this reason, IDisposable
should only implement synchronous cleanup tasks.
C# IAsyncDisposable Supports Async Await
If you wish to implement asynchronous disposal of unmanaged resources implement IAsyncDisposable
.
public class ExampleDisposable : IAsyncDisposable { public ValueTask DisposeAsync() { throw new NotImplementedException(); } } public class Application { public async Task RunApp() { await using var disposable = new ExampleDisposable(); // Do something in here // disposable is automatically cleaned up } }
IAsyncDisposable
has a slightly different interface. It requires us to implement ValueTask DisposeAsync()
.
A ValueTask
, unlike a Task
is a struct. It has some performance benefits associated with struct
allocation being stack-based rather than heap-based, but comes with caveats such as only being able to be awaited once. Either way, we do not have a choice of the return type of the DisposeAsync()
method, but know that it operates similarly to a regular Task
.
IAsyncDisposable
must also be consumed in an async
way. Notice that we use an await
keyword before the using statement
. You’ll receive a compiler error without this!
This allows us to revise the TestIndex
implementation.
using Elasticsearch.Net; using Nest; namespace ElasticSearchTestInfrastructure { public class ElasticsearchTestIndex : IAsyncDisposable { private readonly IElasticClient ElasticClient; public string Name { get; init; } = Guid.NewGuid().ToString(); public ElasticsearchTestIndex(IElasticClient elasticClient) { ElasticClient = elasticClient; } ... public async ValueTask DisposeAsync() { try { await ElasticClient.Indices.DeleteAsync(Name); } catch (Exception ex) { // Swallow the exception here - we tried our best to tidy up } GC.SuppressFinalize(this); } } }
Now we can be sure that our ElasticSearch index will be removed when the DisposeAsync()
method is called as the using statement
IL generated finally
block is executed. We can also suppress the finalizer here, as we have nothing else to clean up.
Sloth Summary
IDisposable
and IAsyncDisposable
are great interfaces for helping with the destruction of unmanaged resources, both synchronously and asynchronously.
Disposable objects must be manually disposed of. Using statement
and awaited using statements
are a great way to achieve this in an exception-safe manner. This is because they generate IL code to wrap the disposable object in a try/finally
to ensure Dipose()
is called.
If you are implementing IDisposable
consider whether you need the finalizer
to be invoked. If not, call GC.SuppressFinalize(this);
to make your code a little more efficient.
Remember to prefer implementing IDisposable
and IAsyncDisposable
over finalizers, as they give you much more control over the lifetime of your resources. You never know when garbage collection will be performed!