The .NET runtime comes with efficient resource management, helping you build robust applications. It can allocate, manage, and reclaim memory efficiently for objects, helping prevent memory leaks and memory exhaustion. Most cleanup is done automatically. However, there are scenarios where you, as the developer, need to manually clean up unmanaged resources that the runtime cannot see.
This guide will briefly explain how .NET's resource management works and its limitations when it comes to dealing with unmanaged resources. You'll then see how the IDisposable
interface can be implemented in different scenarios to ensure resources are cleaned up properly.
Resource Management in .NET
The .NET runtime manages a memory heap for your application's reference types. While the runtime handles the allocation, the Garbage Collector (GC) is responsible for automatically reclaiming memory from objects that are no longer in use. It is also responsible for optimizing memory by compacting the heap. This entire process frees you from manually deallocating memory.
ℹ️ What are Reference Types?
Reference types are any .NET types whose values get stored on the heap instead of the stack. When you assign such a type to a variable, the variable stores a reference to the object on the heap. This is unlike value types, which store the actual value in the variable.
Since the runtime manages the memory for reference types, it's called managed memory. However, applications can often use unmanaged resources. The memory for these resources sits outside the runtime's control and visibility. Such resources can include file handles, database connections, and network sockets. Because it has no visibility of these resources, the GC has no way of compacting or reclaiming this memory automatically. Instead, the class itself needs to release these unmanaged resources to prevent memory leaks.
To solve this problem, .NET provides the IDisposable
interface to deterministically cleanup resources. The following section demonstrates a simple IDisposable
implementation that is most commonly used.
The Basic Dispose Pattern: A Simple Implementation
In most cases, your classes will work with unmanaged resources that are already wrapped in their own IDisposable
classes.
For instance, your class might use the NpgsqlConnection
class to establish a connection to a PostgreSQL database. Even though the database connection is an unmanaged resource, the NpgsqlConnection
class already implements a Dispose()
method to manage those resources. Your class's Dispose()
method simply needs to call the database's Dispose()
method.
This is demonstrated in the code snippet below:
public class CustomerRepository : IDisposable
{
private bool _disposed = false;
private readonly NpgsqlConnection _connection;
public CustomerRepository(string connectionString)
{
_connection = new NpgsqlConnection(connectionString);
_connection.Open();
}
// Methods that use the database connection...
public void Dispose()
{
if (_disposed)
return;
_disposed = true;
// Call the database Dispose() method to handle cleanup
_connection.Dispose();
}
}
Notice how the CustomerRepository.Dispose()
method makes a call to the underlying database connection to dispose it. Now the unmanaged resource is taken care of. Nothing too complicated!
The _disposed
field also keeps track of whether the dispose method has been called already. This is important, since your Dispose()
method should always be idempotent, otherwise exceptions might be thrown.
ℹ️ Cascading
Dispose()
Calls
Whenever your class owns a class that implementsIDisposable
, it must cascade dispose calls down to those owned objects to ensure proper cleanup.However, this is not necessary if your class doesn't own the resource (e.g., it was passed in as a dependency in the constructor).
Consuming the IDisposable
Class
When consuming the class above, you should use the using
block as it automatically calls Dispose()
for you when you're finished with the object:
using (var repository = new CustomerRepository(_connectionString))
{
// Use repository here...
} // Calls repository.Dispose() automatically
Notice how the Dispose()
method is called as soon as you reach the end of the using
block.
You can also remove the block, in which case the Dispose()
method will be called when the surrounding code block ends:
private Customer? FindCustomer(int id)
{
// Create repository with `using var`
using var repository = new CustomerRepository(_connectionString);
return repository.Get(id);
} // Calls repository.Dispose() automatically
In the snippet above, the Dispose()
method will be called automatically after the function returns.
The Full Dispose Pattern: Handling Unmanaged Resources
The implementation above will suffice for most of your IDisposable
implementations. However, there might be times when you're dealing with unmanaged resources directly (i.e., they aren't wrapped in their own IDisposable
). In those cases, there are a couple more things to consider in your implementation.
Take a look at the implementation below:
using Microsoft.Win32.SafeHandles;
using System.ComponentModel;
using System.Runtime.InteropServices;
public class UnmanagedFileHandler : IDisposable
{
// Raw, unmanaged resource pointer
private IntPtr _handle;
// Managed resource
private readonly MemoryStream _buffer;
private bool _disposed = false;
// Using a constant for the invalid handle value for clarity.
private static readonly IntPtr INVALID_HANDLE_VALUE = new(-1);
// Import the CreateFile function from the Windows API.
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
private static extern IntPtr CreateFile(
string lpFileName,
uint dwDesiredAccess,
uint dwShareMode,
IntPtr lpSecurityAttributes,
uint dwCreationDisposition,
uint dwFlagsAndAttributes,
IntPtr hTemplateFile);
// Import the CloseHandle function.
[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool CloseHandle(IntPtr hObject);
// Constants for file access from the Windows API.
private const uint GENERIC_WRITE = 0x40000000;
private const uint CREATE_ALWAYS = 2;
private const uint NO_SHARING = 0;
private const uint DEFAULT_ATTRIBUTES = 0;
public UnmanagedFileHandler(string filePath)
{
// Call the unmanaged Windows API to get a file handle.
_handle = CreateFile(
filePath,
GENERIC_WRITE,
NO_SHARING,
IntPtr.Zero,
CREATE_ALWAYS,
DEFAULT_ATTRIBUTES,
IntPtr.Zero);
_buffer = new MemoryStream();
// Check if the handle is valid. If not, throw an exception.
if (_handle == INVALID_HANDLE_VALUE)
throw new Win32Exception(Marshal.GetLastWin32Error(), "Failed to create the file handle.");
}
// File methods here...
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
~UnmanagedFileHandler()
{
Dispose(false);
}
protected virtual void Dispose(bool disposing)
{
if (_disposed)
return;
if (disposing)
{
// Dispose managed resources if called from Dispose()
_buffer.Dispose();
}
// Always dispose unmanaged resources
if (_handle != IntPtr.Zero && _handle != INVALID_HANDLE_VALUE)
{
CloseHandle(_handle);
_handle = IntPtr.Zero;
}
_disposed = true;
}
}
That's a bit more code!
First, notice how the class has an IntPtr
pointing to a file, which is created using Window's low-level CreateFile
method. This pointer is an unmanaged resource that has to be cleaned up manually.
A MemoryStream
is also created to act as a buffer. This is another unmanaged resource. However, because the MemoryStream
class implements IDisposable
, you only need to call the Dispose()
method on that field.
There's also a new Dispose(bool disposing)
method. It cleans up managed and unmanaged resources. This method can be called from two places in the class: the IDisposable.Dispose()
method, or the class finalizer.
ℹ️ What are Finalizers?
Finalizers are another name for destructors in C#. These methods have a simple signature:public ~ClassName() {}
The GC calls the finalizer before reclaiming the object's memory.
When calling the new Dispose(bool disposing)
method, the disposing
parameter is determined by the origin of the method call:
- When called from
IDisposable.Dispose()
, thendisposing
istrue
, meaning both managed and unmanaged resources should be cleaned up. - When called from the finalizer (i.e.,
~UnmanagedFileHandler()
), thendisposing
isfalse
, so only unmanaged resources are cleaned up. This is because the GC will finalize the owned managed resources, so no need toDispose()
them ourselves.
GC.SuppressFinalize()
is also called on the current object in the Dispose()
method. This tells the GC that it does not need to call the finalizer method on this class.
This is necessary for performance reasons, since finalizers are not exactly efficient. When the GC encounters a class with a finalizer that needs to be reclaimed, it first places that finalizer on a queue to execute later. This is to prevent the current GC run from being potentially delayed by calling the finalizer immediately. Once the current GC run is finished, the finalizer is executed. Only after the finalizer is executed does the class become eligible to be reclaimed.
So by suppressing the finalizer on the class, the memory for that class can be immediately reclaimed without waiting for the finalizer to execute first.
Finally, you'll see the Dispose(bool disposing)
method is virtual
. In the next section, you'll find out why this is necessary when it comes to class inheritance with IDisposable
.
Disposing of Inherited Classes
What happens if a class inherits from your IDisposable
class and uses its own unmanaged resources? The child class's resources also need to be cleaned up, along with the parent class's resources.
Fortunately, since the Dispose(bool disposing)
method is virtual
, a child class can execute its own cleanup logic when the class is disposed.
The code snippet below is for a LogFileHandler
class, which inherits from the UnmanagedFileHandler
class referenced above.
public class LogFileHandler : UnmanagedFileHandler
{
private bool _disposed = false;
private readonly MemoryStream _logBuffer;
public LogFileHandler(string filePath) : base(filePath)
{
_logBuffer = new MemoryStream();
}
// Log methods here...
// Override the parent class Dispose method to clean up the additional resource.
protected override void Dispose(bool disposing)
{
if (_disposed)
return;
_disposed = true;
if (disposing)
{
// Dispose the additional managed resource
_logBuffer.Dispose();
}
// No unmanaged resources to clean up in this child class
// Call the base class Dispose method
base.Dispose(disposing);
}
}
The LogFileHandler
class has its own MemoryStream
field, which is used to buffer log messages. This resource implements the IDisposable
interface, so the LogFileHandler
must override the Dispose(bool disposing)
method from the parent class to dispose of the buffer. When overriding the method, the base class Dispose(bool disposing)
method must still be called.
Since the Dispose(bool disposing)
method is already called from the IDisposable.Dispose()
and finalizer in the base class, there's no need to implement them in the LogFileHandler
class.
ℹ️ What If I Never Plan On Inheriting?
If yourIDisposable
class will never be inherited from, mark the class assealed
and remove thevirtual
flag from theDispose(bool disposing)
method.
Best Practices
When implementing any of the patterns above, keep these things in mind to ensure your disposal logic is robust.
Ensure Idempotency
Calls to Dispose()
should always be idempotent to avoid exceptions from being thrown. This can happen if part of disposing of a property sets it to an invalid value (like null
):
public void Dispose()
{
_connection.Dispose();
_connection = null;
}
If, for any reason, the Dispose()
method above is called again, a NullReferenceException
will be thrown because _connection
was set to null
previously.
So it's best to always use a _disposed
private field to track if the dispose has already been run:
public void Dispose()
{
if (_disposed)
return;
_disposed = true;
_connection.Dispose();
_connection = null;
}
Now the method will return early if it's ever called multiple times.
Don't Throw Exceptions in Finalizers
When implementing a finalizers that dispose resources, it's crucial to avoid throwing any exceptions as they can have unintended side effects, like causing the entire application to crash.
Always write your finalizers as defensively as possible to prevent unhandled exceptions from surfacing.
Cascade Dispose Calls to Owned Resources
Any class that owns resources that implement IDisposable
must implement the IDisposable
interface and call the Dispose()
method on those resources in its own Dispose()
method. If not done, owned resources won't be released, which could cause memory leaks.
Always Call Base Class Dispose
If a class inherits from another class implementing IDisposable
, make sure you override the Dispose(bool disposing)
method to release any unmanaged resources in the inherited class.
Also, always call the base class's Dispose(bool disposing)
method from the overridden method.
Use SafeHandle
to Managed Those Unmanaged Resources
The full Dispose pattern is necessary if your class deals with unmanaged resources directly (i.e., the resources don't have an existing IDisposable
wrapper, such as IntPtr
). However, the .NET runtime comes with SafeHandle
classes that can wrap any raw unmanaged IntPtr
in an IDisposable
. These wrapper classes manage the pointer for you, meaning your class only needs to implement the Basic Dispose pattern.
Disposing Asynchronously with IAsyncDisposable
When your class holds resources that involve asynchronous operations during cleanup (like closing a database connection or releasing a lock), you should implement the IAsyncDisposable
instead of (or in addition to) IDisposable
. This allows for non-blocking cleanup, keeping your application responsive.
The IAsyncDisposable
interface expects a ValueTask DisposeAsync()
method to be implemented.
Below is an example of implementing the IAsyncDisposable
interface:
public class CustomerRepository : IAsyncDisposable
{
// ...
public async ValueTask DisposeAsync()
{
// Implement asychronous cleanup logic
await _connection.DisposeAsync();
// Also implement any synchronous cleanup here as well
_buffer.Dispose();
}
}
Consuming an IAsyncDisposable
class is similar to the IDisposable
class. You just need to add await
to your using statement:
await using (var repository = new CustomerRepository(_connectionString))
{
// Use repository here
} // Calls repository.DisposeAsync() automatically
Conclusion
While the .NET GC does a great job of managing memory, it cannot clean unmanaged resources, like low-level file handles or database connections. The IDisposable
interface helps ensure all managed and unmanaged resources are cleaned up deterministically.
Hopefully, this guide has shed some light on the Dispose pattern in C#. In most cases, you'll stick to the first, simple implementation. However, if you ever have to clean up unmanaged resources manually, a bit more code is needed to make it work.
By implementing the IDisposable
interface properly, you'll reduce the number of potential memory leaks, resulting in a robust and stable end product.
Top comments (0)