Disposal and resource lifetime
Skill Sarmkadan/dotnet-senior-skills/skills/disposal-and-resource-lifetime
Senior-level .NET review rules for AI coding agents - Claude Code skills, Cursor rules, and Copilot instructions from one source
npx -y skills add Sarmkadan/dotnet-senior-skills --skill disposal-and-resource-lifetimeAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
2 things to look at
- 26 days oldThe repository was created 26 days ago. New is not bad, but a brand new repository carrying a familiar-sounding name is the shape a typosquat arrives in, and there has been no time for anyone else to find a problem with it.
- 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
Review IDisposable/IAsyncDisposable usage in .NET - what to dispose, what never to dispose, using patterns, the dispose pattern itself, and finalizer rules. Use when reviewing resource management, using statements, or classes owning disposable fields.
SKILL.md
4.7 KB, as published. Nobody here has run it
Disposal and Resource Lifetime
The ownership rule
Whoever creates a disposable disposes it; whoever receives one does not. Every review question about disposal reduces to "who owns this instance?"
- Created in a method:
using/await using, no exceptions. - Created in a constructor and stored in a field: the class owns it, so the class implements
IDisposableand disposes the field. - Injected via DI: the container owns it. Disposing an injected
DbContextor typedHttpClientbreaks the next consumer of the same scoped instance. A class whose only disposables are injected does not implementIDisposableat all.
// non-compiling: illustrative
// WRONG: disposing what DI owns; second service in the scope gets a disposed context
public sealed class OrderService : IDisposable
{
private readonly AppDbContext _db;
public OrderService(AppDbContext db) => _db = db;
public void Dispose() => _db.Dispose();
}
// RIGHT: no IDisposable; the scope disposes the context
public sealed class OrderService
{
private readonly AppDbContext _db;
public OrderService(AppDbContext db) => _db = db;
}
What must never be wrapped in using
HttpClientfromIHttpClientFactory: disposal is a no-op on the handler you care about, butusingdocuments a false ownership. Sockets are managed by the factory's handler pool;new HttpClient()per call plususingis the classic socket-exhaustion bug (TIME_WAIT pileup under load).CancellationTokenSourcestill referenced by registered callbacks or linked sources elsewhere - dispose it, but only after nothing can use it; a fired timer callback touching a disposed CTS throwsObjectDisposedExceptionintermittently.- Streams passed into a serializer/reader with
leaveOpensemantics: check the flag.new StreamReader(stream)disposes the underlying stream by default; when the caller still needs it, passleaveOpen: true.
IAsyncDisposable
Anything holding resources whose cleanup does I/O (DbContext, transactions, System.Threading.Timer with in-flight callbacks, streams over network) prefers IAsyncDisposable:
await using var transaction = await _db.Database.BeginTransactionAsync(ct);
Rules:
- A type implementing
IAsyncDisposableshould also implementIDisposable(sync fallback) unless synchronous cleanup is impossible - non-DI callers and containers checking only one interface both exist. Dispose()calling.GetAwaiter().GetResult()onDisposeAsync()is sync-over-async in disguise; implement real sync cleanup or document that sync dispose is unsupported.await usingon a type that only hasIDisposabledoes not compile - do not "fix" it by fake-async wrappers.
Implementing IDisposable
For a sealed class holding only managed disposables - the 95% case - the full pattern is overkill:
public sealed class MeterRegistry : IDisposable
{
private readonly Meter _meter = new("app");
private bool _disposed;
public void Dispose()
{
if (_disposed) return;
_disposed = true;
_meter.Dispose();
}
}
Disposemust be idempotent and never throw.- The full
Dispose(bool disposing)+ finalizer pattern is only for classes directly owning unmanaged handles - and those should beSafeHandleinstead, which eliminates the finalizer entirely. A finalizer on a class holding only managed fields is a rejection: it costs a GC generation for every instance and its "cleanup" touches fields that may already be finalized. - Fields set to null in Dispose "to help the GC": noise, remove.
Review flags
- Disposable created and returned from a method: the method name must convey transfer (
Create,Open), and the caller mustusingit. A factory whose callers forget disposal shows up as connection-pool exhaustion at load, not in tests. - Disposable stored in a static or singleton but recreated per operation - the previous instance leaks.
Timer,FileSystemWatcher, event subscriptions: recreate implies dispose-old-first. using varscoping:using var stream = ...at the top of a long method holds the file handle for the whole method. Tighten with a block when the resource is only needed briefly.- Iterator methods (
yield return) and async methods:usinginside them disposes only when enumeration/awaiting completes - an abandoned, half-enumerated iterator defers disposal to GC time. For handles that must close deterministically, materialize instead of yielding.