IVSoftware.Portable.Disposable
2.0.0-beta
Prefix Reserved
dotnet add package IVSoftware.Portable.Disposable --version 2.0.0-beta
NuGet\Install-Package IVSoftware.Portable.Disposable -Version 2.0.0-beta
<PackageReference Include="IVSoftware.Portable.Disposable" Version="2.0.0-beta" />
<PackageVersion Include="IVSoftware.Portable.Disposable" Version="2.0.0-beta" />
<PackageReference Include="IVSoftware.Portable.Disposable" />
paket add IVSoftware.Portable.Disposable --version 2.0.0-beta
#r "nuget: IVSoftware.Portable.Disposable, 2.0.0-beta"
#:package IVSoftware.Portable.Disposable@2.0.0-beta
#addin nuget:?package=IVSoftware.Portable.Disposable&version=2.0.0-beta&prerelease
#tool nuget:?package=IVSoftware.Portable.Disposable&version=2.0.0-beta&prerelease
IVSoftware.Portable.Disposable 2.0
DisposableHost addresses a family of problems that ordinary using blocks and try-catch-finally patterns only partially solve. The version 2 release is a drop-in replacement that adds a fluent extension layer, making access to lifetime events more intuitive.
As dispenser of smart disposable tokens, it provides lifetime management for developers who deal with temporary state. One of its strengths is maintaining non-static, system-wide states that must be shared safely between multiple consumers�because it doesn�t just count, it tracks which senders check out a token and in what order.
Its flexible reference-counting mechanism hosts DisposableToken objects as well as an object dictionary for the lifetime of the IDisposable scope. It raises BeginUsing when count increments to 1, FinalDispose when count decrements to 0, and CountChanged when any movement occurs.
Common examples include:
- Overlapping wait cursors that disappear too soon or hang eternally.
- Ephemeral event subscriptions (especially in test code) that quietly remain attached after their scope ends.
- Figuratively, the "Three-Body Problem" of UI flows.
Here, we're borrowing (without permission) a term from physics that implies unpredictability - the "Three-Body Problem" of UI development arises when three or more components must remain in sync, even as each independently initiates updates. Without a shared lifetime to carry who started it, the system loses its center - updates chase one another in circles, events echo, and state collapses into chaos.
In these cases, what is needed is not just another try-finally, but a coordinated lifetime - a single shared scope that knows when the first participant arrives and the last one leaves.
That is the service that DisposableHost provides - a static dispenser of non-static contexts.
Each host manages its own short-lived world: reference counted, event aware, and automatically self-cleaning.
Hello World, Using Style
Let's ease into things with a familiar using block and make a fluent chain with the WithOnDispose extension. What we see is a clean pattern for working with one of the three events of DisposableHost.
This might not be the revolution just yet, but notice what stands out: we state right at the beginning how cleanup will occur. Now contrast that to what happens in real projects, where that same cleanup might be buried hundreds of lines down - if it's remembered at all.
using (this.WithOnDispose(
onDispose: (sender, e) =>
{
// This is the cleanup contract when the work at hand is finished.
builder.Add(stopwatch.WriteLine("Hello! - Context disposed."));
}))
{
// This is the work itself.
builder.Add(stopwatch.WriteLine("Beginning block", restart: true));
await Task.Delay(TimeSpan.FromSeconds(1));
}
The debugging log now shows:
00:00.0000 Beginning block
00:01.0118 Hello! - Context disposed
Real World Use Case
Here's a handy snippet you can copy straight to your MSTest code. Think about those times when you want to subscribe to an event, but don't want to leave it hanging past a limited scope you're testing. No more hanging tests for you!
// Housekeeping
Stopwatch stopwatch = new();
string actual, expected;
var btn = new Button();
int clickCount = 0;
var builder = new List<string>();
// Up-front contract - with no fine print.
using (this.WithOnDispose(
onInit: ((sender, e) =>
{
// Subscribe to `btn.Click`.
builder.Add(stopwatch.WriteLine("Beginning block", restart: true));
btn.Click += localOnClick;
}),
onDispose: ((sender, e) =>
{
// When the block ends, Unsubscribe.
btn.Click -= localOnClick;
builder.Add(stopwatch.WriteLine("Context disposed"));
})))
{
// Simulate multiple clicks within the active lifetime.
btn.PerformClick();
for (int i = 2; i <= 5; i++)
{
await Task.Delay(TimeSpan.FromSeconds(0.25));
btn.PerformClick();
}
Assert.AreEqual(5, clickCount, "Expecting 5 clicks while subscribed.");
}
// Now that we have left the block, any additional
// clicks should not increment count.
btn.PerformClick();
// Let's make certain.
Assert.AreEqual(5, clickCount, "Expecting count is unfazed - the event has been unsubscribed.");
builder.Add(stopwatch.WriteLine(
$"After the additional out-of-scope click the count remains at {clickCount}."));
void localOnClick(object? sender, EventArgs e)
{
builder.Add(stopwatch.WriteLine($"Count = {++clickCount}"));
}
expected = string.Join(Environment.NewLine, builder);
{ } // <- L O O K
The debugging log now shows.
00:00.0000 Beginning block
00:00.0068 Count = 1
00:00.2776 Count = 2
00:00.5382 Count = 3
00:00.7991 Count = 4
00:01.0661 Count = 5
00:01.0696 Context disposed
00:01.0708 After the additional out-of-scope click the count remains at 5.
Browse Working Examples
This might be a good time to mention that the Project Repo has a suite of MSTests including some friendly [ExampleMethod] samples but also the real nitty-gritty [TestMethod] content where DisposableHost is acually tested.
For example, you may have read the Button example above and thought, "I hope they're running that in an STA thread." We are. The sample code is where you'll find those details.
What would you like to explore next?
There�s a lot more going on beneath the simple token pattern you saw above. The following continuations unpack the deeper mechanics and use-cases that add more value than just a smarter using block.
Each topic has a focused README with runnable examples you can explore at your own pace.
The Case of the Vanishing Wait Cursor
Let us begin with a familiar mystery: a wait cursor that blinks off too soon.
When both an outer and an inner routine manage the same cursor flag in their own try-finally blocks, the inner one inevitably finishes first and resets the cursor, even though the outer work is still running. The result feels random - a flickering cursor that disappears while the task is still in progress.
This gives a first peak at using contexts at a static or shared scope.
Dictionary as Context
Every host doubles as a lightweight key�value store that exists only for the duration of its scope. This enables temporary, shared state (like a �current instance� reference) to travel safely across async or nested operations.
Subclassing
DisposableHost is not sealed. You can extend it to express domain-specific lifetimes � disposable databases, test sandboxes, connection pools, or any shared resource that needs clean teardown without leaks.
Batch Collection Updates
The same lifetime logic powers AutoObservableCollection<T>, letting you batch or suppress notifications while bulk-updating items. This keeps your UI reactive but calm under load.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net5.0 was computed. net5.0-windows was computed. net6.0 was computed. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 was computed. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 was computed. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
| .NET Core | netcoreapp2.0 was computed. netcoreapp2.1 was computed. netcoreapp2.2 was computed. netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
| .NET Standard | netstandard2.0 is compatible. netstandard2.1 was computed. |
| .NET Framework | net461 was computed. net462 was computed. net463 was computed. net47 was computed. net471 was computed. net472 was computed. net48 was computed. net481 was computed. |
| MonoAndroid | monoandroid was computed. |
| MonoMac | monomac was computed. |
| MonoTouch | monotouch was computed. |
| Tizen | tizen40 was computed. tizen60 was computed. |
| Xamarin.iOS | xamarinios was computed. |
| Xamarin.Mac | xamarinmac was computed. |
| Xamarin.TVOS | xamarintvos was computed. |
| Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETStandard 2.0
- IVSoftware.Portable.Threading (>= 1.3.1)
NuGet packages (1)
Showing the top 1 NuGet packages that depend on IVSoftware.Portable.Disposable:
| Package | Downloads |
|---|---|
|
IVSoftware.Portable.Xml.Linq.XBoundObject
A lightweight extension for System.Xml.Linq that adds runtime object binding, hierarchical modeling, and flexible path resolution via enriched XAttribute support. Includes enum-based metadata, tree construction from flat paths, and event-driven behaviors — ideal for dynamic UIs, workflows, and cross-platform .NET apps. (Ever wish XAttribute had a Tag property? Now it does.) |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 2.0.0-beta | 183 | 10/10/2025 |
| 1.2.0 | 835 | 3/10/2024 |
Version 2.0.0-beta — major modernization of the DisposableHost lifecycle model.
* Introduces fluent extension methods (WithOnDispose, WithOnInit, WithOnCountChanged)
for inline, declarative lifetime management without subclassing.
* Rewritten concurrency model where all host events (BeginUsing, CountChanged,
FinalDispose) now fire outside internal locks.
* Added per-token Disposed event for fine-grained lifetime observation.
* Added CountChangedAction enum and richer CountChangedEventArgs context.
* Host dictionary snapshots are now exposed as ReadOnlyDictionary clones
during FinalDispose and token disposal for safe inspection.
* ToString() and diagnostic output are now thread-safe.
* Backward compatibility retained: existing handlers for BeginUsing,
CountChanged, and FinalDispose remain valid.
This release prepares the package for cross-framework beta testing as
IVSoftware.Portable.Disposable 2.0.0.