Home .Net [Tutorial] C# IDisposable vs IAsyncDisposable

[Tutorial] C# IDisposable vs IAsyncDisposable

by Trent
0 comments
C# IDisposable Example Featured Image

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!

You may also like