Skip to content

A Brief Discussion on the Dispose Pattern and the using Statement

TLDR

  • Unmanaged resources (such as database connections and files) must be manually released by implementing the IDisposable interface or defining a finalizer.
  • When implementing the Dispose Pattern, prioritize the Dispose() method and use GC.SuppressFinalize(this) to prevent the garbage collector from calling the finalizer repeatedly.
  • IAsyncDisposable is used for asynchronous resource release; it is recommended to implement IDisposable simultaneously to maintain backward compatibility.
  • The using statement is essentially syntactic sugar for try...finally, ensuring that an object correctly calls Dispose() when it goes out of scope.
  • The using declaration syntax introduced in C# 8.0 reduces nested indentation and improves code readability.

Dispose Pattern

Releasing Unmanaged Resources

When does this issue arise: When developers use unmanaged resources such as database connections or file access, the CLR garbage collector (GC) cannot manage these resources automatically, so they must be released manually.

There are two main ways to release unmanaged resources:

  • Implement the IDisposable interface: Manually release resources in the Dispose() method.
  • Declare a finalizer: Automatically called when the GC collects the object, though the Dispose() method should be prioritized.

Implementation Example

When implementing the Dispose Pattern, it is recommended to use the following standard pattern to ensure that resources are released correctly and not executed repeatedly:

csharp
public class ResourceHandle : IDisposable {
    private bool disposed = false;
    private IntPtr unmanagedResource;
    private ManagedResource managedResource;

    public ResourceHandle() {
        unmanagedResource = IntPtr.Zero;
        managedResource = new ManagedResource();
    }

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

    protected virtual void Dispose(bool disposing) {
        if (!disposed) {
            if (disposing) {
                managedResource?.Dispose();
                managedResource = null;
            }

            if (unmanagedResource != IntPtr.Zero) {
                FreeUnmanagedResource(unmanagedResource);
                unmanagedResource = IntPtr.Zero;
            }
            disposed = true;
        }
    }

    ~ResourceHandle() {
        Dispose(false);
    }
}

Asynchronous Dispose Pattern

When does this issue arise: When the resource release process involves asynchronous operations (such as network requests), the IAsyncDisposable interface introduced in .NET Core 3.0 should be used.

  • Best Practice Recommendation: Implement both IDisposable and IAsyncDisposable to ensure that resources can still be handled correctly in older frameworks that do not support asynchronous release.
  • Priority: In the ASP.NET Core dependency injection (DI) container, if an object implements both, DisposeAsync() will be called first.
csharp
class ExampleConjunctiveDisposable : IDisposable, IAsyncDisposable {
    IDisposable? disposableResource = new MemoryStream();
    IAsyncDisposable? asyncDisposableResource = new MemoryStream();

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

    public async ValueTask DisposeAsync() {
        await DisposeCoreAsync().ConfigureAwait(false);
        Dispose(disposing: false);
        GC.SuppressFinalize(this);
    }

    protected virtual void Dispose(bool disposing) {
        if (disposing) {
            disposableResource?.Dispose();
            disposableResource = null;
            if (asyncDisposableResource is IDisposable disposable) {
                disposable.Dispose();
                asyncDisposableResource = null;
            }
        }
    }

    protected virtual async ValueTask DisposeCoreAsync() {
        if (asyncDisposableResource is not null) {
            await asyncDisposableResource.DisposeAsync().ConfigureAwait(false);
        }
        if (disposableResource is IAsyncDisposable disposable) {
            await disposable.DisposeAsync().ConfigureAwait(false);
        } else {
            disposableResource?.Dispose();
        }
        asyncDisposableResource = null;
        disposableResource = null;
    }
}

using Statement

Ensuring Resource Release

When does this issue arise: To avoid resource leaks caused by forgetting to call Dispose(), or when cleanup logic cannot be executed due to an exception.

The compiler automatically converts the using statement into a try...finally structure, ensuring that resources are released regardless of whether an exception occurs.

csharp
// Traditional syntax
using (ResourceHandle handle = new ResourceHandle()) {
    // Execution logic
}

// C# 8.0 new syntax: Automatically calls Dispose() when leaving scope
{
    using ResourceHandle handle = new ResourceHandle();
    // Execution logic
}

Nesting and Asynchronous Processing

For the release of multiple resources, nested structures can be simplified:

  • Combined Declaration: If the types are the same, they can be combined into a single using.
  • Asynchronous Release: If the object implements IAsyncDisposable, await using must be used.
csharp
// Combined declaration
using (ResourceHandle handle1 = new ResourceHandle(), handle2 = new ResourceHandle()) {
    // Execution logic
}

// Asynchronous release
await using (AsyncDisposableObject resource = new AsyncDisposableObject()) {
    // Execution logic
}

Change Log

  • 2024-08-08 Initial document creation.