Skip to content

Commit 95fcd62

Browse files
Copilotporrey
andauthored
Non-breaking improvements to DisposableObject and AsyncDisposableObject (#1)
* Add ARCHITECTURE.md document Agent-Logs-Url: https://github.com/porrey/System.DisposableObject/sessions/70e014c3-08e8-4c52-825f-444e4b5e00ee Co-authored-by: porrey <12475115+porrey@users.noreply.github.com> * Add xUnit test project with full coverage for DisposableObject and AsyncDisposableObject Agent-Logs-Url: https://github.com/porrey/System.DisposableObject/sessions/3d850767-144a-4c06-9445-39b4cf34592a Co-authored-by: porrey <12475115+porrey@users.noreply.github.com> * Update README.md with comprehensive library documentation Agent-Logs-Url: https://github.com/porrey/System.DisposableObject/sessions/11483a01-0a39-425e-870f-dd8384f636d9 Co-authored-by: porrey <12475115+porrey@users.noreply.github.com> * Add badges for build status, license, and NuGet * Implement recommended improvements: thread-safety, public IsDisposed, Disposed event, TryGetMember/TrySetMember guards, DisposeAsyncCore hook, remove redundant finalizer, fix docs Agent-Logs-Url: https://github.com/porrey/System.DisposableObject/sessions/14f1433d-61f9-4adc-b81e-a6eb782b62cf Co-authored-by: porrey <12475115+porrey@users.noreply.github.com> * Update .NET workflow to use newer action versions * Updated packages --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: porrey <12475115+porrey@users.noreply.github.com> Co-authored-by: Daniel Porrey <porrey@users.noreply.github.com>
1 parent 1b183fa commit 95fcd62

12 files changed

Lines changed: 1386 additions & 151 deletions

.github/workflows/dotnet.yml

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
# This workflow will build a .NET project
2+
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-net
3+
4+
name: .NET
5+
6+
on:
7+
push:
8+
branches: [ "master" ]
9+
pull_request:
10+
branches: [ "master" ]
11+
12+
jobs:
13+
test:
14+
15+
runs-on: ubuntu-latest
16+
17+
steps:
18+
- uses: actions/checkout@v5
19+
- name: Setup .NET
20+
uses: actions/setup-dotnet@v5
21+
with:
22+
dotnet-version: 10.0.x
23+
- name: Restore dependencies
24+
run: dotnet restore
25+
working-directory: Src/System.DisposableObject.sln
26+
- name: Build
27+
run: dotnet build --no-restore
28+
working-directory: Src/System.DisposableObject.sln
29+
- name: Test
30+
run: dotnet test --no-build --verbosity normal
31+
working-directory: Src/System.DisposableObject.sln

ARCHITECTURE.md

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
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

Comments
 (0)