System.DisposableObject is a lightweight .NET base-class library that implements the Dispose Pattern so you don't have to. Inherit from one of two abstract base classes and override a pair of simple hook methods — the boilerplate is handled for you.
DisposableObject— implementsIDisposablewith a full finalizer, double-dispose guard, and re-entrancy guard.AsyncDisposableObject— extendsDisposableObjectwithIAsyncDisposable, enablingawait usingwhile keeping synchronous callers working.
- Installation
- Quick Start
- API Reference
- Advanced Usage
- How Disposal Works
- Contributing
- License
- References
Install the package from NuGet:
dotnet add package System.DisposableObjectOr via the Package Manager Console in Visual Studio:
Install-Package System.DisposableObjectInherit from DisposableObject and override whichever hook methods your class needs.
using System;
public class DatabaseConnection : DisposableObject
{
private SqlConnection _connection;
public DatabaseConnection(string connectionString)
{
_connection = new SqlConnection(connectionString);
_connection.Open();
}
protected override void OnDisposeManagedObjects()
{
// Dispose CLR-managed objects (objects that implement IDisposable).
_connection?.Dispose();
_connection = null;
}
protected override void OnDisposeUnmanagedObjects()
{
// Release native handles, file descriptors, COM objects, etc.
// Do NOT reference other .NET objects here — this method can be
// called by the GC finalizer after managed objects have been collected.
}
}Use it with a using statement (recommended):
using (var db = new DatabaseConnection(connectionString))
{
// Work with db here.
} // OnDisposeManagedObjects() and OnDisposeUnmanagedObjects() are called here.Or call Dispose() explicitly:
var db = new DatabaseConnection(connectionString);
// ... use db ...
db.Dispose();Inherit from AsyncDisposableObject when your class wraps async-capable resources or when callers may use await using.
using System;
using System.Threading.Tasks;
public class AsyncDatabaseConnection : AsyncDisposableObject
{
private SqlConnection _connection;
public AsyncDatabaseConnection(string connectionString)
{
_connection = new SqlConnection(connectionString);
}
protected override void OnDisposeManagedObjects()
{
// Synchronous cleanup of managed resources.
_connection?.Dispose();
_connection = null;
}
protected override void OnDisposeUnmanagedObjects()
{
// Native resource cleanup.
}
}Use it with await using (recommended for async code):
await using (var db = new AsyncDatabaseConnection(connectionString))
{
// Work with db here.
} // DisposeAsync() → Dispose() → hooks called here.Synchronous callers still work as before:
using (var db = new AsyncDatabaseConnection(connectionString))
{
// Works fine — AsyncDisposableObject inherits DisposableObject.
}Namespace: System
Assembly: System.DisposableObject
Inherits: System.Dynamic.DynamicObject
Implements: IDisposable
DisposableObject is abstract. You cannot instantiate it directly; create a class that inherits from it.
| Property | Type | Description |
|---|---|---|
IsDisposed |
bool |
true after Dispose() has been called. Read-only to subclasses; set internally. |
InProcessOfDisposing |
bool (virtual) |
true while disposal is in progress. Used as a re-entrancy guard. |
AssertWhenNotDisposed |
bool (virtual) |
When true, a Trace.Assert or warning is emitted if the finalizer fires before Dispose(). Defaults to false. |
| Method | Description |
|---|---|
void Dispose() |
Disposes the object. Calls OnDisposeManagedObjects() and OnDisposeUnmanagedObjects(), then calls GC.SuppressFinalize(this). Safe to call multiple times. |
bool TryInvokeMember(...) |
Overrides DynamicObject.TryInvokeMember. Calls AccessMethod() before delegating, ensuring that dynamic member invocations throw ObjectDisposedException on disposed instances. |
Override any of the following in your subclass:
| Method | When called | Notes |
|---|---|---|
OnDisposeManagedObjects() |
During an explicit Dispose() call only |
Safe to access other managed (.NET) objects here. |
OnDisposeUnmanagedObjects() |
During both explicit Dispose() and GC finalization |
Do not reference managed objects here — they may already have been collected. |
AccessMethod() |
Call at the start of any public method in your subclass | Throws ObjectDisposedException if IsDisposed is true. |
OnGetClassName() |
Called when a diagnostic message is generated | Override to return a meaningful class name (defaults to this.ToString()). |
OnNotDisposedProperly() |
Called from the finalizer when AssertWhenNotDisposed is true |
Return true to suppress the base-class warning/assertion and handle it yourself. |
Namespace: System
Assembly: System.DisposableObject
Inherits: DisposableObject
Implements: IDisposable, IAsyncDisposable
Adds a DisposeAsync() method on top of everything provided by DisposableObject. The hook methods (OnDisposeManagedObjects, OnDisposeUnmanagedObjects) are identical — there is no separate async hook.
| Method | Description |
|---|---|
ValueTask DisposeAsync() |
Calls the synchronous Dispose() and returns ValueTask.CompletedTask. Safe to call multiple times and to mix with synchronous Dispose() calls. |
Call AccessMethod() at the start of any public or internal method in your subclass to ensure the object has not been disposed:
public class MyResource : DisposableObject
{
private byte[] _buffer = new byte[1024];
public int ReadData(byte[] destination, int count)
{
// Throws ObjectDisposedException if Dispose() was already called.
this.AccessMethod();
// ... perform the read ...
return count;
}
protected override void OnDisposeManagedObjects()
{
_buffer = null;
}
}After Dispose() is called, any subsequent call to ReadData will throw ObjectDisposedException with the name of the type.
Enable AssertWhenNotDisposed in your subclass constructor during development to catch objects that are garbage-collected without an explicit Dispose():
public class LeakDetectingResource : DisposableObject
{
public LeakDetectingResource()
{
// Enable only in DEBUG builds to help catch disposal leaks.
#if DEBUG
this.AssertWhenNotDisposed = true;
#endif
}
protected override void OnDisposeManagedObjects() { /* ... */ }
}When AssertWhenNotDisposed is true and the GC finalizer runs without a prior Dispose():
- If
OnNotDisposedProperly()returnsfalse(the default), aTrace.Assertfires. - If
OnNotDisposedProperly()returnstrue, aTrace.TraceWarningis emitted instead and no assertion fires.
Override OnGetClassName() to provide a more readable name in trace/assert messages:
public class MyService : DisposableObject
{
protected override string OnGetClassName() => nameof(MyService);
protected override void OnDisposeManagedObjects() { /* ... */ }
}The diagram below shows the code paths during object lifetime.
Explicit call: obj.Dispose()
│
▼
DisposableObject.Dispose()
│ ── calls GC.SuppressFinalize(this)
▼
DisposableObject.Dispose(disposing: true)
├─► OnDisposeManagedObjects() ← override in subclass
└─► OnDisposeUnmanagedObjects() ← override in subclass
GC finalizer: ~DisposableObject()
│ (only reached if Dispose() was never called)
▼
DisposableObject.Dispose(disposing: false)
└─► OnDisposeUnmanagedObjects() ← managed objects must NOT be touched
Async call: await obj.DisposeAsync()
│
▼
AsyncDisposableObject.DisposeAsync()
│ ── delegates to synchronous Dispose()
└─► (same flow as explicit synchronous call above)
Key guarantees provided by the base classes:
| Guarantee | How it is enforced |
|---|---|
| Hooks called at most once | IsDisposed flag checked before calling hooks |
| No recursive disposal | InProcessOfDisposing re-entrancy guard |
| Finalizer suppressed after explicit dispose | GC.SuppressFinalize(this) called from Dispose() |
| Dynamic member access blocked after dispose | TryInvokeMember calls AccessMethod() |
Contributions are welcome. Please open an issue or submit a pull request on GitHub.
This library is distributed under the GNU Lesser General Public License v3.0 or later (LGPL-3.0-or-later). See LICENSE for the full text.