|
| 1 | +# Architecture: System.DisposableObject |
| 2 | + |
| 3 | +## Overview |
| 4 | + |
| 5 | +`System.DisposableObject` is a lightweight .NET class library that provides ready-to-use base classes implementing the [Dispose Pattern](https://docs.microsoft.com/en-us/dotnet/standard/garbage-collection/implementing-dispose) for both synchronous (`IDisposable`) and asynchronous (`IAsyncDisposable`) resource cleanup. Consuming code inherits from one of the two base classes and overrides hook methods rather than re-implementing the full dispose pattern from scratch. |
| 6 | + |
| 7 | +The library is distributed as a NuGet package (`System.DisposableObject`) and targets **net10.0**. |
| 8 | + |
| 9 | +--- |
| 10 | + |
| 11 | +## Repository Layout |
| 12 | + |
| 13 | +``` |
| 14 | +System.DisposableObject/ ← repository root |
| 15 | +├── Image/ |
| 16 | +│ └── dispose.png ← NuGet package icon |
| 17 | +├── README.md ← Quick-start documentation (also bundled in the NuGet package) |
| 18 | +├── LICENSE ← LGPL-3.0-or-later |
| 19 | +└── Src/ |
| 20 | + ├── System.DisposableObject Solution.sln ← Visual Studio solution |
| 21 | + ├── NuGet.Publish.cmd ← Helper script to push *.nupkg files to nuget.org |
| 22 | + ├── System.DisposableObject/ ← Library project (produces NuGet package) |
| 23 | + │ ├── System.DisposableObject.csproj |
| 24 | + │ ├── DisposableObject.cs |
| 25 | + │ ├── AsyncDisposableObject.cs |
| 26 | + │ └── System.DisposableObject.xml ← Generated XML documentation |
| 27 | + └── System.DisposableObject.Example/ ← Console application demonstrating usage |
| 28 | + ├── System.DisposableObject.Example.csproj |
| 29 | + ├── Program.cs |
| 30 | + ├── SomeObject.cs |
| 31 | + └── SomeAsyncObject.cs |
| 32 | +``` |
| 33 | + |
| 34 | +--- |
| 35 | + |
| 36 | +## Key Technologies |
| 37 | + |
| 38 | +| Technology | Role | |
| 39 | +|---|---| |
| 40 | +| **C# / .NET 10** | Implementation language and target runtime | |
| 41 | +| **Visual Studio 2019+ solution format** | IDE project organisation (`Format Version 12.00`) | |
| 42 | +| **`IDisposable`** | Standard synchronous resource-cleanup interface | |
| 43 | +| **`IAsyncDisposable`** | Asynchronous resource-cleanup interface (C# 8 / .NET Core 3+) | |
| 44 | +| **`System.Dynamic.DynamicObject`** | Base class for `DisposableObject`; enables dynamic dispatch and validates that the object has not been disposed before any member invocation | |
| 45 | +| **`System.Diagnostics.Trace`** | Emits warnings and optional assertions when an object is finalised without having been explicitly disposed | |
| 46 | +| **NuGet** | Package distribution; the library project generates both a `.nupkg` and a `.snupkg` (symbols) on build | |
| 47 | +| **LGPL-3.0-or-later** | Open-source licence | |
| 48 | + |
| 49 | +--- |
| 50 | + |
| 51 | +## Class Hierarchy |
| 52 | + |
| 53 | +``` |
| 54 | +System.Dynamic.DynamicObject (BCL) |
| 55 | + └── System.DisposableObject (abstract) — implements IDisposable |
| 56 | + └── System.AsyncDisposableObject (abstract) — also implements IAsyncDisposable |
| 57 | +``` |
| 58 | + |
| 59 | +### `DisposableObject` (`DisposableObject.cs`) |
| 60 | + |
| 61 | +The core base class. Key design decisions: |
| 62 | + |
| 63 | +* **Inherits `DynamicObject`** — `TryInvokeMember` is overridden to call `AccessMethod()` before every dynamic dispatch, guaranteeing that calling a method on an already-disposed instance throws `ObjectDisposedException`. |
| 64 | +* **Standard dispose pattern** — implements the canonical `Dispose()` / `Dispose(bool disposing)` / finalizer trio. |
| 65 | +* **Re-entrancy guard** — `InProcessOfDisposing` flag prevents recursive disposal. |
| 66 | +* **Double-disposal guard** — `IsDisposed` flag ensures `OnDisposeManagedObjects` and `OnDisposeUnmanagedObjects` are called at most once. |
| 67 | +* **`GC.SuppressFinalize`** — called from `Dispose()` to remove the object from the finalisation queue after an explicit dispose. |
| 68 | +* **Debug assistance** — `AssertWhenNotDisposed` (defaults to `false`) causes a `Trace.Assert` or warning when the finaliser fires without a prior `Dispose()` call; `OnGetClassName()` and `OnNotDisposedProperly()` are overridable hooks for customising the diagnostic output. |
| 69 | + |
| 70 | +#### Overridable hooks for consumers |
| 71 | + |
| 72 | +| Method | Purpose | |
| 73 | +|---|---| |
| 74 | +| `OnDisposeManagedObjects()` | Clean up CLR-managed objects (called only when disposing explicitly) | |
| 75 | +| `OnDisposeUnmanagedObjects()` | Clean up native/unmanaged handles (called on both explicit dispose and GC finalisation) | |
| 76 | +| `OnGetClassName()` | Return a human-readable class name for diagnostic messages | |
| 77 | +| `OnNotDisposedProperly()` | Custom handling when GC finalises a non-disposed instance; return `true` to suppress the base-class warning | |
| 78 | +| `AccessMethod()` | Guard method to call at the start of any public method to enforce "not disposed" invariant | |
| 79 | + |
| 80 | +### `AsyncDisposableObject` (`AsyncDisposableObject.cs`) |
| 81 | + |
| 82 | +Extends `DisposableObject` with `IAsyncDisposable` support. The `DisposeAsync()` implementation delegates to the synchronous `Dispose()` path and returns a completed `ValueTask`, making it safe to use in `await using` statements while maintaining full backwards compatibility with synchronous callers. |
| 83 | + |
| 84 | +--- |
| 85 | + |
| 86 | +## Data / Control Flow During Disposal |
| 87 | + |
| 88 | +``` |
| 89 | +Explicit call: obj.Dispose() |
| 90 | + │ |
| 91 | + ▼ |
| 92 | + DisposableObject.Dispose() |
| 93 | + │ calls GC.SuppressFinalize(this) |
| 94 | + ▼ |
| 95 | + DisposableObject.Dispose(disposing: true) |
| 96 | + ├─► OnDisposeManagedObjects() ← override in subclass |
| 97 | + └─► OnDisposeUnmanagedObjects() ← override in subclass |
| 98 | +
|
| 99 | +GC finaliser: ~DisposableObject() |
| 100 | + │ (object was never explicitly disposed) |
| 101 | + ▼ |
| 102 | + DisposableObject.Dispose(disposing: false) |
| 103 | + └─► OnDisposeUnmanagedObjects() ← managed objects must NOT be touched |
| 104 | +
|
| 105 | +Async call: await obj.DisposeAsync() |
| 106 | + │ |
| 107 | + ▼ |
| 108 | + AsyncDisposableObject.DisposeAsync() |
| 109 | + │ delegates to synchronous Dispose() |
| 110 | + └─► (same flow as explicit synchronous call above) |
| 111 | +``` |
| 112 | + |
| 113 | +--- |
| 114 | + |
| 115 | +## Example Project (`System.DisposableObject.Example`) |
| 116 | + |
| 117 | +A minimal console application (`net10.0`) that exercises both base classes: |
| 118 | + |
| 119 | +* **`SomeObject`** — inherits `DisposableObject`; overrides both hook methods. |
| 120 | +* **`SomeAsyncObject`** — inherits `AsyncDisposableObject`; overrides both hook methods. |
| 121 | +* **`Program.Main`** — instantiates each class inside a `using` block to trigger disposal. |
| 122 | + |
| 123 | +The example project references the library via a `ProjectReference` (not a NuGet reference), so it always builds against the local source. |
| 124 | + |
| 125 | +--- |
| 126 | + |
| 127 | +## Build & Publish |
| 128 | + |
| 129 | +| Task | Command / File | |
| 130 | +|---|---| |
| 131 | +| Build library | `dotnet build` inside `Src/System.DisposableObject/` | |
| 132 | +| Generate NuGet package | Automatic on build (`<GeneratePackageOnBuild>true</GeneratePackageOnBuild>`) | |
| 133 | +| Publish to nuget.org | Run `Src/NuGet.Publish.cmd` (Windows only; requires NuGet API key) | |
| 134 | + |
| 135 | +The package includes: |
| 136 | +* The compiled assembly and XML documentation. |
| 137 | +* Debug symbols as a separate `.snupkg` file. |
| 138 | +* `README.md` (displayed on nuget.org). |
| 139 | +* The `dispose.png` icon. |
0 commit comments