C#idiomatic C#
resource-managementidioms

Idioms

Disposal and lifetimes

Cleanup that happens on schedule: `using`, the dispose pattern, and the ownership rules that decide who releases a resource.

The garbage collector reclaims memory and nothing else. These are the decisions that make cleanup deterministic: which resources a using covers, which type owns each disposable, and what a finalizer is still allowed to do when nobody calls Dispose.

A `using` declaration replaces the hand-written `try/finally`IDE0063

The using statement and the using declaration compile to the same try/finally, so Dispose runs when the block exits by exception and when it exits by return. The declaration drops the braces and ties the lifetime to the enclosing scope; several instances declared in one statement are disposed in reverse order of declaration.

Preferred

csharp
using var reader = new StreamReader(path);
return reader.ReadToEnd();

Avoid

csharp
var reader = new StreamReader(path);
var text = reader.ReadToEnd();
reader.Dispose();

Corpus: using statement · Using objects that implement IDisposable · IDE0063: Use simple 'using' statement

Build a returned disposable outside the block that cleans it upCA2000

A using block disposes its resource before the caller can see it, so a method that returns a disposable finishes every step that can fail before it moves the instance to the value it returns. The temporary stays in a try/finally that releases it on the paths that never reach the return.

Preferred

csharp
var temp = new SerialPort(name);
SerialPort port = null;
try { temp.Open(); SomeWork(); port = temp; temp = null; }
finally { temp?.Dispose(); }
return port;

Avoid

csharp
var port = new SerialPort(name);
port.Open();
SomeWork();
return port;

Corpus: CA2000: Dispose objects before losing scope · Using objects that implement IDisposable

A type that owns a disposable field implements `IDisposable`CA1001

A class that declares an IDisposable field owns an unmanaged resource indirectly, so the class implements IDisposable and disposes the field from its own Dispose. It does not implement a finalizer: nothing unmanaged is held directly, and the field brings its own finalization with it.

Preferred

csharp
sealed class Log : IDisposable
{
    private readonly StreamWriter _writer = new("app.log");
    public void Dispose() => _writer.Dispose();
}

Avoid

csharp
sealed class Log
{
    private readonly StreamWriter _writer = new("app.log");
}

Corpus: CA1001: Types that own disposable fields should be disposable · Implement a Dispose method

Dispose only what you ownCA2213

Ownership decides who disposes. An instance the type created is the type's to release, while one that arrived through a parameter belongs to its creator and is left alone, so a class holding a caller's object does not become disposable on its account. Disposing on both sides releases the same resource twice.

Preferred

csharp
sealed class Api(HttpClient client)
{
    public Task<string> GetAsync(string url) => client.GetStringAsync(url);
}

Avoid

csharp
sealed class Api(HttpClient client) : IDisposable
{
    public void Dispose() => client.Dispose();
}

Corpus: CA2213: Disposable fields should be disposed · Implement a Dispose method

`Dispose()` delegates to `Dispose(bool)`CA1063

Any unsealed type that implements IDisposable adds a protected virtual void Dispose(bool disposing) overload: the public method calls it with true, and a finalizer calls it with false. A derived type overrides that overload, releases the state it added, and passes disposing to base.Dispose(disposing) so the base releases what it owns.

Preferred

csharp
public void Dispose() => Dispose(true);

protected virtual void Dispose(bool disposing)
{
    if (disposing) _inner.Dispose();
}

Avoid

csharp
class Derived : Base, IDisposable
{
    public void Dispose() => _extra.Dispose();
}

Corpus: CA1063: Implement IDisposable correctly · CA2215: Dispose methods should call base class dispose · Cleaning up unmanaged resources

Suppress finalization from `Dispose`CA1816

Once Dispose has released the resources, the finalizer has nothing left to do, so the method calls GC.SuppressFinalize(this) for the object it just cleaned up. An unsealed type makes the call even when it has no finalizer, so a derived type that adds one does not have to reimplement IDisposable to reach the same suppression.

Preferred

csharp
GC.SuppressFinalize(this);

Avoid

csharp
GC.SuppressFinalize(_inner);

Corpus: CA1816: Call GC.SuppressFinalize correctly · Implement a Dispose method

A finalizer only for a handle you own directly

Write a finalizer only for a type that holds an unmanaged resource directly, and wrap that handle in a SafeHandle so the finalizer, the once-only release, and the synchronization come with it. Finalization runs at a moment the GC chooses and is not called at process exit on .NET 5 and later, so Dispose stays the reliable path; an empty finalizer only adds a second collection.

Preferred

csharp
private readonly SafeFileHandle _handle;
public void Dispose() => _handle.Dispose();

Avoid

csharp
private IntPtr _handle;
~Resource() => Marshal.FreeHGlobal(_handle);

Corpus: Finalizers · Implement a Dispose method · CA1821: Remove empty finalizers

`await using` for cleanup that must be awaited

Cleanup that performs I/O implements IAsyncDisposable, whose DisposeAsync returns a ValueTask that await using awaits instead of blocking a thread. Implement IDisposable as well when synchronous cleanup is possible: a class that offers only DisposeAsync leaks when a caller disposes it through Dispose, which never reaches the asynchronous path.

Preferred

csharp
await using var stream = File.Create(path, 4096, FileOptions.Asynchronous);
await WriteReportAsync(stream, ct);

Avoid

csharp
var stream = File.Create(path, 4096, FileOptions.Asynchronous);
await WriteReportAsync(stream, ct);

Corpus: Implement a DisposeAsync method · using statement

`CancellationTokenSource` goes in a `using`

The source implements IDisposable, so it is released when the operation that owns it finishes, and a using declaration around that operation ties the two lifetimes together. The token handed to the operation is a struct, which leaves the source as the only part of the pair with a lifetime to manage.

Preferred

csharp
using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
await RunAsync(cts.Token);

Avoid

csharp
var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30));
await RunAsync(cts.Token);

Corpus: Cancellation in Managed Threads

Do not dispose under a task that is still runningCA2025

A using block ends as soon as control leaves it, and a task started inside the block may still be using the instance it was handed. Await the task inside the scope so the instance outlives the work, or give the task a resource it owns.

Preferred

csharp
using var stream = File.OpenRead(path);
await HashAsync(stream, ct);

Avoid

csharp
using (var stream = File.OpenRead(path))
{
    return HashAsync(stream, ct);
}

Corpus: CA2025: Do not pass 'IDisposable' instances into unawaited tasks · Using objects that implement IDisposable

Throw `ObjectDisposedException` from the helperCA1513

A member that needs live state checks the disposed flag before it touches that state, and the throw helper does it in one call instead of an if block that dominates a small member and keeps it from being inlined.

Preferred

csharp
ObjectDisposedException.ThrowIf(_disposed, this);
return _buffer.Length;

Avoid

csharp
if (_disposed)
    throw new ObjectDisposedException(nameof(Buffer));

Corpus: CA1513: Use ObjectDisposedException throw helper

`GC.Collect` does not dispose anything

A collection reclaims memory and knows nothing about Dispose, so forcing one leaves a handle open and a pooled buffer unreturned. The forced mode is a full blocking collection of every generation, and inducing it often costs more performance than it saves.

Preferred

csharp
stream.Dispose();

Avoid

csharp
GC.Collect();

Corpus: Induced Collections · Finalizers

Return a rented array to the pool

Rent hands out a buffer the pool expects back, so the matching Return sits in a finally and both calls stay inside one method. The array is borrowed memory: a reference that escapes can be returned early and read again after the pool has handed the same array to another caller.

Preferred

csharp
var buffer = ArrayPool<byte>.Shared.Rent(size);
try { ReadInto(buffer); }
finally { ArrayPool<byte>.Shared.Return(buffer); }

Avoid

csharp
_buffer = ArrayPool<byte>.Shared.Rent(size);
Use(_buffer);
ArrayPool<byte>.Shared.Return(_buffer);

Corpus: CA1838: Avoid StringBuilder parameters for P/Invokes · Unsafe code best practices

Pattern-based `using` on a `ref struct`

A ref struct cannot implement an interface, so the using statement and declaration accept the disposable pattern instead: an accessible, parameterless instance Dispose method returning void is enough, and the resource is released at the end of the scope like any other.

Preferred

csharp
ref struct Lease
{
    public void Dispose() { }
}

Avoid

csharp
ref struct Lease
{
    private void Dispose() { }
}

Corpus: using statement