diff --git a/README.md b/README.md
index f276cd1e..6663146b 100644
--- a/README.md
+++ b/README.md
@@ -83,6 +83,12 @@ These two types are the only places the lean surface departs from the System.Rea
variants close the gap: they recompile the same source with `ISequencer` mapped to `IScheduler` and `RxVoid` mapped to
`System.Reactive.Unit`, so code that already speaks System.Reactive sees the types it expects.
+Disposal groups are a third seam, and one the shared types cannot close on their own: `MultipleDisposable` ships in the
+dependency-free `ReactiveUI.Disposables` package, so it cannot name `CompositeDisposable`. `ReactiveUI.Primitives.Reactive`
+adds `ContainerDisposable` for that - a `MultipleDisposable` that converts implicitly to a `CompositeDisposable` it owns
+and disposes. Hand one to `DisposeWith`, to a library that takes a `CompositeDisposable`, or to your own helper, and it
+just works; anything registered through the composite is disposed with the container.
+
## Table of contents
1. [Install](#install)
diff --git a/src/ReactiveUI.Primitives.Reactive/Disposables/ContainerDisposable.cs b/src/ReactiveUI.Primitives.Reactive/Disposables/ContainerDisposable.cs
new file mode 100644
index 00000000..a860613a
--- /dev/null
+++ b/src/ReactiveUI.Primitives.Reactive/Disposables/ContainerDisposable.cs
@@ -0,0 +1,112 @@
+// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved.
+// ReactiveUI Association Incorporated licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using System.Reactive.Disposables;
+
+namespace ReactiveUI.Primitives.Reactive.Disposables;
+
+///
+/// A that a System.Reactive consumer can use as a
+/// , so an activation-scoped container flows into APIs written against
+/// System.Reactive - DisposeWith above all - without the caller converting it by hand.
+///
+///
+///
+/// The conversion is identity-stable: every conversion of the same container yields the same
+/// , and the container owns that composite, so anything registered through it
+/// is disposed when the container is. Registering after the container is disposed disposes the registration
+/// immediately, matching .
+///
+///
+/// Registrations made through the composite are not visible to the container's own
+/// members: the composite occupies a single slot, so Count counts it once
+/// and Contains /Remove do not see through it.
+///
+///
+[System.Diagnostics.DebuggerDisplay("Count = {Count}, IsDisposed = {IsDisposed}")]
+public sealed class ContainerDisposable : MultipleDisposable
+{
+ /// Serializes creation of the composite.
+ private readonly Lock _gate = new();
+
+ /// The composite handed to System.Reactive consumers, created on first conversion.
+ private CompositeDisposable? _composite;
+
+ /// Initializes a new instance of the class.
+ public ContainerDisposable()
+ {
+ }
+
+ /// Initializes a new instance of the class.
+ /// The first disposable.
+ /// The second disposable.
+ public ContainerDisposable(IDisposable first, IDisposable second)
+ : base(first, second)
+ {
+ }
+
+ /// Initializes a new instance of the class.
+ /// The first disposable.
+ /// The second disposable.
+ /// The third disposable.
+ public ContainerDisposable(IDisposable first, IDisposable second, IDisposable third)
+ : base(first, second, third)
+ {
+ }
+
+ /// Initializes a new instance of the class from a group of disposables.
+ /// Disposables that will be disposed together.
+ /// is .
+ public ContainerDisposable(params IDisposable[] disposables)
+ : base(disposables)
+ {
+ }
+
+ /// Hands the container to a System.Reactive consumer as the composite it owns.
+ /// The container to convert.
+ /// is .
+ public static implicit operator CompositeDisposable(ContainerDisposable container)
+ {
+ ArgumentExceptionHelper.ThrowIfNull(container);
+
+ return container.ToCompositeDisposable();
+ }
+
+ /// Gets the this container owns, creating it on first call.
+ /// The composite whose contents are disposed along with this container.
+ public CompositeDisposable ToCompositeDisposable()
+ {
+ lock (_gate)
+ {
+ // A disposed composite is still the right answer once the container itself is disposed - it is the
+ // sink that disposes late arrivals. After Clear() or Remove() the container lives on, so a composite
+ // it disposed has to be replaced rather than handed out again.
+ var existing = _composite;
+ if (existing is not null && (!existing.IsDisposed || IsDisposed))
+ {
+ return existing;
+ }
+
+ var created = new CompositeDisposable();
+ _composite = created;
+
+ // Registering the composite with the container is what ties the two lifetimes together. On an
+ // already-disposed container this disposes the composite instead, which is what a caller adding to
+ // a disposed container should get.
+ Add(created);
+ return created;
+ }
+ }
+
+ ///
+ protected override void Dispose(bool disposing)
+ {
+ base.Dispose(disposing);
+
+ // The composite occupies a slot in the container, so the base disposed it just now - or Clear()/Remove()
+ // did on the way out. Disposing it here is idempotent and states the ownership outright. Nothing in this
+ // hierarchy has a finalizer and the class is sealed, so this only ever runs on the deterministic path.
+ _composite?.Dispose();
+ }
+}
diff --git a/src/ReactiveUI.Primitives.Reactive/Disposables/LinqExtensions.ContainerDisposable.cs b/src/ReactiveUI.Primitives.Reactive/Disposables/LinqExtensions.ContainerDisposable.cs
new file mode 100644
index 00000000..b9e73a39
--- /dev/null
+++ b/src/ReactiveUI.Primitives.Reactive/Disposables/LinqExtensions.ContainerDisposable.cs
@@ -0,0 +1,37 @@
+// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved.
+// ReactiveUI Association Incorporated licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using ReactiveUI.Primitives.Reactive.Disposables;
+
+namespace ReactiveUI.Primitives.Reactive;
+
+/// Miscellaneous Primitives extensions.
+public static partial class LinqExtensions
+{
+ /// Disposal-tracking operators for a disposable.
+ /// The disposable type.
+ /// The disposable.
+ extension(T disposable)
+ where T : IDisposable
+ {
+ /// Disposes the IDisposable with the container.
+ /// The container.
+ /// The original disposable.
+ /// is .
+ ///
+ /// A converts to a System.Reactive CompositeDisposable , so
+ /// without this overload a call site that imports both this namespace and System.Reactive's fluent
+ /// disposal helpers has two equally-good candidates - the inherited
+ /// DisposeWith(MultipleDisposable) and System.Reactive's DisposeWith(CompositeDisposable)
+ /// - and is ambiguous. Taking the container exactly makes this an identity match, which wins outright.
+ ///
+ public T DisposeWith(ContainerDisposable disposables)
+ {
+ ArgumentExceptionHelper.ThrowIfNull(disposables);
+
+ disposables.Add(disposable);
+ return disposable;
+ }
+ }
+}
diff --git a/src/ReactiveUI.Primitives.Reactive/PublicAPI/net10.0-android/PublicAPI.txt b/src/ReactiveUI.Primitives.Reactive/PublicAPI/net10.0-android/PublicAPI.txt
index aa6ead83..831e27a3 100644
--- a/src/ReactiveUI.Primitives.Reactive/PublicAPI/net10.0-android/PublicAPI.txt
+++ b/src/ReactiveUI.Primitives.Reactive/PublicAPI/net10.0-android/PublicAPI.txt
@@ -425,6 +425,7 @@ namespace ReactiveUI.Primitives.Reactive
extension(T disposable) where T : System.IDisposable
{
public T DisposeWith(ReactiveUI.Primitives.Disposables.MultipleDisposable disposables) { }
+ public T DisposeWith(ReactiveUI.Primitives.Reactive.Disposables.ContainerDisposable disposables) { }
}
extension(System.IObservable source)
{
@@ -1598,6 +1599,20 @@ namespace ReactiveUI.Primitives.Reactive.Core
public override readonly string ToString() { }
}
}
+namespace ReactiveUI.Primitives.Reactive.Disposables
+{
+ [System.Diagnostics.DebuggerDisplay("Count = {Count}, IsDisposed = {IsDisposed}")]
+ public sealed class ContainerDisposable : ReactiveUI.Primitives.Disposables.MultipleDisposable
+ {
+ public ContainerDisposable() { }
+ public ContainerDisposable(params System.IDisposable[] disposables) { }
+ public ContainerDisposable(System.IDisposable first, System.IDisposable second) { }
+ public ContainerDisposable(System.IDisposable first, System.IDisposable second, System.IDisposable third) { }
+ protected override void Dispose(bool disposing) { }
+ public System.Reactive.Disposables.CompositeDisposable ToCompositeDisposable() { }
+ public static implicit operator System.Reactive.Disposables.CompositeDisposable(ReactiveUI.Primitives.Reactive.Disposables.ContainerDisposable container) { }
+ }
+}
namespace ReactiveUI.Primitives.Reactive.Signals
{
[System.Diagnostics.DebuggerDisplay("Count = {_count}, MaximumCount = {_maximumCount}, IsDraining = {_isDraining}")]
diff --git a/src/ReactiveUI.Primitives.Reactive/PublicAPI/net10.0-ios/PublicAPI.txt b/src/ReactiveUI.Primitives.Reactive/PublicAPI/net10.0-ios/PublicAPI.txt
index f59aa979..85a5a2c4 100644
--- a/src/ReactiveUI.Primitives.Reactive/PublicAPI/net10.0-ios/PublicAPI.txt
+++ b/src/ReactiveUI.Primitives.Reactive/PublicAPI/net10.0-ios/PublicAPI.txt
@@ -425,6 +425,7 @@ namespace ReactiveUI.Primitives.Reactive
extension(T disposable) where T : System.IDisposable
{
public T DisposeWith(ReactiveUI.Primitives.Disposables.MultipleDisposable disposables) { }
+ public T DisposeWith(ReactiveUI.Primitives.Reactive.Disposables.ContainerDisposable disposables) { }
}
extension(System.IObservable source)
{
@@ -1592,6 +1593,20 @@ namespace ReactiveUI.Primitives.Reactive.Core
public override readonly string ToString() { }
}
}
+namespace ReactiveUI.Primitives.Reactive.Disposables
+{
+ [System.Diagnostics.DebuggerDisplay("Count = {Count}, IsDisposed = {IsDisposed}")]
+ public sealed class ContainerDisposable : ReactiveUI.Primitives.Disposables.MultipleDisposable
+ {
+ public ContainerDisposable() { }
+ public ContainerDisposable(params System.IDisposable[] disposables) { }
+ public ContainerDisposable(System.IDisposable first, System.IDisposable second) { }
+ public ContainerDisposable(System.IDisposable first, System.IDisposable second, System.IDisposable third) { }
+ protected override void Dispose(bool disposing) { }
+ public System.Reactive.Disposables.CompositeDisposable ToCompositeDisposable() { }
+ public static implicit operator System.Reactive.Disposables.CompositeDisposable(ReactiveUI.Primitives.Reactive.Disposables.ContainerDisposable container) { }
+ }
+}
namespace ReactiveUI.Primitives.Reactive.Signals
{
[System.Diagnostics.DebuggerDisplay("Count = {_count}, MaximumCount = {_maximumCount}, IsDraining = {_isDraining}")]
diff --git a/src/ReactiveUI.Primitives.Reactive/PublicAPI/net10.0-maccatalyst/PublicAPI.txt b/src/ReactiveUI.Primitives.Reactive/PublicAPI/net10.0-maccatalyst/PublicAPI.txt
index 20249c17..0c550dd8 100644
--- a/src/ReactiveUI.Primitives.Reactive/PublicAPI/net10.0-maccatalyst/PublicAPI.txt
+++ b/src/ReactiveUI.Primitives.Reactive/PublicAPI/net10.0-maccatalyst/PublicAPI.txt
@@ -425,6 +425,7 @@ namespace ReactiveUI.Primitives.Reactive
extension(T disposable) where T : System.IDisposable
{
public T DisposeWith(ReactiveUI.Primitives.Disposables.MultipleDisposable disposables) { }
+ public T DisposeWith(ReactiveUI.Primitives.Reactive.Disposables.ContainerDisposable disposables) { }
}
extension(System.IObservable source)
{
@@ -1592,6 +1593,20 @@ namespace ReactiveUI.Primitives.Reactive.Core
public override readonly string ToString() { }
}
}
+namespace ReactiveUI.Primitives.Reactive.Disposables
+{
+ [System.Diagnostics.DebuggerDisplay("Count = {Count}, IsDisposed = {IsDisposed}")]
+ public sealed class ContainerDisposable : ReactiveUI.Primitives.Disposables.MultipleDisposable
+ {
+ public ContainerDisposable() { }
+ public ContainerDisposable(params System.IDisposable[] disposables) { }
+ public ContainerDisposable(System.IDisposable first, System.IDisposable second) { }
+ public ContainerDisposable(System.IDisposable first, System.IDisposable second, System.IDisposable third) { }
+ protected override void Dispose(bool disposing) { }
+ public System.Reactive.Disposables.CompositeDisposable ToCompositeDisposable() { }
+ public static implicit operator System.Reactive.Disposables.CompositeDisposable(ReactiveUI.Primitives.Reactive.Disposables.ContainerDisposable container) { }
+ }
+}
namespace ReactiveUI.Primitives.Reactive.Signals
{
[System.Diagnostics.DebuggerDisplay("Count = {_count}, MaximumCount = {_maximumCount}, IsDraining = {_isDraining}")]
diff --git a/src/ReactiveUI.Primitives.Reactive/PublicAPI/net10.0-macos/PublicAPI.txt b/src/ReactiveUI.Primitives.Reactive/PublicAPI/net10.0-macos/PublicAPI.txt
index d2918a63..d55d7688 100644
--- a/src/ReactiveUI.Primitives.Reactive/PublicAPI/net10.0-macos/PublicAPI.txt
+++ b/src/ReactiveUI.Primitives.Reactive/PublicAPI/net10.0-macos/PublicAPI.txt
@@ -425,6 +425,7 @@ namespace ReactiveUI.Primitives.Reactive
extension(T disposable) where T : System.IDisposable
{
public T DisposeWith(ReactiveUI.Primitives.Disposables.MultipleDisposable disposables) { }
+ public T DisposeWith(ReactiveUI.Primitives.Reactive.Disposables.ContainerDisposable disposables) { }
}
extension(System.IObservable source)
{
@@ -1592,6 +1593,20 @@ namespace ReactiveUI.Primitives.Reactive.Core
public override readonly string ToString() { }
}
}
+namespace ReactiveUI.Primitives.Reactive.Disposables
+{
+ [System.Diagnostics.DebuggerDisplay("Count = {Count}, IsDisposed = {IsDisposed}")]
+ public sealed class ContainerDisposable : ReactiveUI.Primitives.Disposables.MultipleDisposable
+ {
+ public ContainerDisposable() { }
+ public ContainerDisposable(params System.IDisposable[] disposables) { }
+ public ContainerDisposable(System.IDisposable first, System.IDisposable second) { }
+ public ContainerDisposable(System.IDisposable first, System.IDisposable second, System.IDisposable third) { }
+ protected override void Dispose(bool disposing) { }
+ public System.Reactive.Disposables.CompositeDisposable ToCompositeDisposable() { }
+ public static implicit operator System.Reactive.Disposables.CompositeDisposable(ReactiveUI.Primitives.Reactive.Disposables.ContainerDisposable container) { }
+ }
+}
namespace ReactiveUI.Primitives.Reactive.Signals
{
[System.Diagnostics.DebuggerDisplay("Count = {_count}, MaximumCount = {_maximumCount}, IsDraining = {_isDraining}")]
diff --git a/src/ReactiveUI.Primitives.Reactive/PublicAPI/net10.0-tvos/PublicAPI.txt b/src/ReactiveUI.Primitives.Reactive/PublicAPI/net10.0-tvos/PublicAPI.txt
index ae2a41e0..f670a0a6 100644
--- a/src/ReactiveUI.Primitives.Reactive/PublicAPI/net10.0-tvos/PublicAPI.txt
+++ b/src/ReactiveUI.Primitives.Reactive/PublicAPI/net10.0-tvos/PublicAPI.txt
@@ -425,6 +425,7 @@ namespace ReactiveUI.Primitives.Reactive
extension(T disposable) where T : System.IDisposable
{
public T DisposeWith(ReactiveUI.Primitives.Disposables.MultipleDisposable disposables) { }
+ public T DisposeWith(ReactiveUI.Primitives.Reactive.Disposables.ContainerDisposable disposables) { }
}
extension(System.IObservable source)
{
@@ -1592,6 +1593,20 @@ namespace ReactiveUI.Primitives.Reactive.Core
public override readonly string ToString() { }
}
}
+namespace ReactiveUI.Primitives.Reactive.Disposables
+{
+ [System.Diagnostics.DebuggerDisplay("Count = {Count}, IsDisposed = {IsDisposed}")]
+ public sealed class ContainerDisposable : ReactiveUI.Primitives.Disposables.MultipleDisposable
+ {
+ public ContainerDisposable() { }
+ public ContainerDisposable(params System.IDisposable[] disposables) { }
+ public ContainerDisposable(System.IDisposable first, System.IDisposable second) { }
+ public ContainerDisposable(System.IDisposable first, System.IDisposable second, System.IDisposable third) { }
+ protected override void Dispose(bool disposing) { }
+ public System.Reactive.Disposables.CompositeDisposable ToCompositeDisposable() { }
+ public static implicit operator System.Reactive.Disposables.CompositeDisposable(ReactiveUI.Primitives.Reactive.Disposables.ContainerDisposable container) { }
+ }
+}
namespace ReactiveUI.Primitives.Reactive.Signals
{
[System.Diagnostics.DebuggerDisplay("Count = {_count}, MaximumCount = {_maximumCount}, IsDraining = {_isDraining}")]
diff --git a/src/ReactiveUI.Primitives.Reactive/PublicAPI/net10.0/PublicAPI.txt b/src/ReactiveUI.Primitives.Reactive/PublicAPI/net10.0/PublicAPI.txt
index d088608d..99a07ed3 100644
--- a/src/ReactiveUI.Primitives.Reactive/PublicAPI/net10.0/PublicAPI.txt
+++ b/src/ReactiveUI.Primitives.Reactive/PublicAPI/net10.0/PublicAPI.txt
@@ -424,6 +424,7 @@ namespace ReactiveUI.Primitives.Reactive
extension(T disposable) where T : System.IDisposable
{
public T DisposeWith(ReactiveUI.Primitives.Disposables.MultipleDisposable disposables) { }
+ public T DisposeWith(ReactiveUI.Primitives.Reactive.Disposables.ContainerDisposable disposables) { }
}
extension(System.IObservable source)
{
@@ -1572,6 +1573,20 @@ namespace ReactiveUI.Primitives.Reactive.Core
public override readonly string ToString() { }
}
}
+namespace ReactiveUI.Primitives.Reactive.Disposables
+{
+ [System.Diagnostics.DebuggerDisplay("Count = {Count}, IsDisposed = {IsDisposed}")]
+ public sealed class ContainerDisposable : ReactiveUI.Primitives.Disposables.MultipleDisposable
+ {
+ public ContainerDisposable() { }
+ public ContainerDisposable(params System.IDisposable[] disposables) { }
+ public ContainerDisposable(System.IDisposable first, System.IDisposable second) { }
+ public ContainerDisposable(System.IDisposable first, System.IDisposable second, System.IDisposable third) { }
+ protected override void Dispose(bool disposing) { }
+ public System.Reactive.Disposables.CompositeDisposable ToCompositeDisposable() { }
+ public static implicit operator System.Reactive.Disposables.CompositeDisposable(ReactiveUI.Primitives.Reactive.Disposables.ContainerDisposable container) { }
+ }
+}
namespace ReactiveUI.Primitives.Reactive.Signals
{
[System.Diagnostics.DebuggerDisplay("Count = {_count}, MaximumCount = {_maximumCount}, IsDraining = {_isDraining}")]
diff --git a/src/ReactiveUI.Primitives.Reactive/PublicAPI/net11.0-android/PublicAPI.txt b/src/ReactiveUI.Primitives.Reactive/PublicAPI/net11.0-android/PublicAPI.txt
index aa6ead83..831e27a3 100644
--- a/src/ReactiveUI.Primitives.Reactive/PublicAPI/net11.0-android/PublicAPI.txt
+++ b/src/ReactiveUI.Primitives.Reactive/PublicAPI/net11.0-android/PublicAPI.txt
@@ -425,6 +425,7 @@ namespace ReactiveUI.Primitives.Reactive
extension(T disposable) where T : System.IDisposable
{
public T DisposeWith(ReactiveUI.Primitives.Disposables.MultipleDisposable disposables) { }
+ public T DisposeWith(ReactiveUI.Primitives.Reactive.Disposables.ContainerDisposable disposables) { }
}
extension(System.IObservable source)
{
@@ -1598,6 +1599,20 @@ namespace ReactiveUI.Primitives.Reactive.Core
public override readonly string ToString() { }
}
}
+namespace ReactiveUI.Primitives.Reactive.Disposables
+{
+ [System.Diagnostics.DebuggerDisplay("Count = {Count}, IsDisposed = {IsDisposed}")]
+ public sealed class ContainerDisposable : ReactiveUI.Primitives.Disposables.MultipleDisposable
+ {
+ public ContainerDisposable() { }
+ public ContainerDisposable(params System.IDisposable[] disposables) { }
+ public ContainerDisposable(System.IDisposable first, System.IDisposable second) { }
+ public ContainerDisposable(System.IDisposable first, System.IDisposable second, System.IDisposable third) { }
+ protected override void Dispose(bool disposing) { }
+ public System.Reactive.Disposables.CompositeDisposable ToCompositeDisposable() { }
+ public static implicit operator System.Reactive.Disposables.CompositeDisposable(ReactiveUI.Primitives.Reactive.Disposables.ContainerDisposable container) { }
+ }
+}
namespace ReactiveUI.Primitives.Reactive.Signals
{
[System.Diagnostics.DebuggerDisplay("Count = {_count}, MaximumCount = {_maximumCount}, IsDraining = {_isDraining}")]
diff --git a/src/ReactiveUI.Primitives.Reactive/PublicAPI/net11.0-ios/PublicAPI.txt b/src/ReactiveUI.Primitives.Reactive/PublicAPI/net11.0-ios/PublicAPI.txt
index f59aa979..85a5a2c4 100644
--- a/src/ReactiveUI.Primitives.Reactive/PublicAPI/net11.0-ios/PublicAPI.txt
+++ b/src/ReactiveUI.Primitives.Reactive/PublicAPI/net11.0-ios/PublicAPI.txt
@@ -425,6 +425,7 @@ namespace ReactiveUI.Primitives.Reactive
extension(T disposable) where T : System.IDisposable
{
public T DisposeWith(ReactiveUI.Primitives.Disposables.MultipleDisposable disposables) { }
+ public T DisposeWith(ReactiveUI.Primitives.Reactive.Disposables.ContainerDisposable disposables) { }
}
extension(System.IObservable source)
{
@@ -1592,6 +1593,20 @@ namespace ReactiveUI.Primitives.Reactive.Core
public override readonly string ToString() { }
}
}
+namespace ReactiveUI.Primitives.Reactive.Disposables
+{
+ [System.Diagnostics.DebuggerDisplay("Count = {Count}, IsDisposed = {IsDisposed}")]
+ public sealed class ContainerDisposable : ReactiveUI.Primitives.Disposables.MultipleDisposable
+ {
+ public ContainerDisposable() { }
+ public ContainerDisposable(params System.IDisposable[] disposables) { }
+ public ContainerDisposable(System.IDisposable first, System.IDisposable second) { }
+ public ContainerDisposable(System.IDisposable first, System.IDisposable second, System.IDisposable third) { }
+ protected override void Dispose(bool disposing) { }
+ public System.Reactive.Disposables.CompositeDisposable ToCompositeDisposable() { }
+ public static implicit operator System.Reactive.Disposables.CompositeDisposable(ReactiveUI.Primitives.Reactive.Disposables.ContainerDisposable container) { }
+ }
+}
namespace ReactiveUI.Primitives.Reactive.Signals
{
[System.Diagnostics.DebuggerDisplay("Count = {_count}, MaximumCount = {_maximumCount}, IsDraining = {_isDraining}")]
diff --git a/src/ReactiveUI.Primitives.Reactive/PublicAPI/net11.0-maccatalyst/PublicAPI.txt b/src/ReactiveUI.Primitives.Reactive/PublicAPI/net11.0-maccatalyst/PublicAPI.txt
index 20249c17..0c550dd8 100644
--- a/src/ReactiveUI.Primitives.Reactive/PublicAPI/net11.0-maccatalyst/PublicAPI.txt
+++ b/src/ReactiveUI.Primitives.Reactive/PublicAPI/net11.0-maccatalyst/PublicAPI.txt
@@ -425,6 +425,7 @@ namespace ReactiveUI.Primitives.Reactive
extension(T disposable) where T : System.IDisposable
{
public T DisposeWith(ReactiveUI.Primitives.Disposables.MultipleDisposable disposables) { }
+ public T DisposeWith(ReactiveUI.Primitives.Reactive.Disposables.ContainerDisposable disposables) { }
}
extension(System.IObservable source)
{
@@ -1592,6 +1593,20 @@ namespace ReactiveUI.Primitives.Reactive.Core
public override readonly string ToString() { }
}
}
+namespace ReactiveUI.Primitives.Reactive.Disposables
+{
+ [System.Diagnostics.DebuggerDisplay("Count = {Count}, IsDisposed = {IsDisposed}")]
+ public sealed class ContainerDisposable : ReactiveUI.Primitives.Disposables.MultipleDisposable
+ {
+ public ContainerDisposable() { }
+ public ContainerDisposable(params System.IDisposable[] disposables) { }
+ public ContainerDisposable(System.IDisposable first, System.IDisposable second) { }
+ public ContainerDisposable(System.IDisposable first, System.IDisposable second, System.IDisposable third) { }
+ protected override void Dispose(bool disposing) { }
+ public System.Reactive.Disposables.CompositeDisposable ToCompositeDisposable() { }
+ public static implicit operator System.Reactive.Disposables.CompositeDisposable(ReactiveUI.Primitives.Reactive.Disposables.ContainerDisposable container) { }
+ }
+}
namespace ReactiveUI.Primitives.Reactive.Signals
{
[System.Diagnostics.DebuggerDisplay("Count = {_count}, MaximumCount = {_maximumCount}, IsDraining = {_isDraining}")]
diff --git a/src/ReactiveUI.Primitives.Reactive/PublicAPI/net11.0-macos/PublicAPI.txt b/src/ReactiveUI.Primitives.Reactive/PublicAPI/net11.0-macos/PublicAPI.txt
index d2918a63..d55d7688 100644
--- a/src/ReactiveUI.Primitives.Reactive/PublicAPI/net11.0-macos/PublicAPI.txt
+++ b/src/ReactiveUI.Primitives.Reactive/PublicAPI/net11.0-macos/PublicAPI.txt
@@ -425,6 +425,7 @@ namespace ReactiveUI.Primitives.Reactive
extension(T disposable) where T : System.IDisposable
{
public T DisposeWith(ReactiveUI.Primitives.Disposables.MultipleDisposable disposables) { }
+ public T DisposeWith(ReactiveUI.Primitives.Reactive.Disposables.ContainerDisposable disposables) { }
}
extension(System.IObservable source)
{
@@ -1592,6 +1593,20 @@ namespace ReactiveUI.Primitives.Reactive.Core
public override readonly string ToString() { }
}
}
+namespace ReactiveUI.Primitives.Reactive.Disposables
+{
+ [System.Diagnostics.DebuggerDisplay("Count = {Count}, IsDisposed = {IsDisposed}")]
+ public sealed class ContainerDisposable : ReactiveUI.Primitives.Disposables.MultipleDisposable
+ {
+ public ContainerDisposable() { }
+ public ContainerDisposable(params System.IDisposable[] disposables) { }
+ public ContainerDisposable(System.IDisposable first, System.IDisposable second) { }
+ public ContainerDisposable(System.IDisposable first, System.IDisposable second, System.IDisposable third) { }
+ protected override void Dispose(bool disposing) { }
+ public System.Reactive.Disposables.CompositeDisposable ToCompositeDisposable() { }
+ public static implicit operator System.Reactive.Disposables.CompositeDisposable(ReactiveUI.Primitives.Reactive.Disposables.ContainerDisposable container) { }
+ }
+}
namespace ReactiveUI.Primitives.Reactive.Signals
{
[System.Diagnostics.DebuggerDisplay("Count = {_count}, MaximumCount = {_maximumCount}, IsDraining = {_isDraining}")]
diff --git a/src/ReactiveUI.Primitives.Reactive/PublicAPI/net11.0-tvos/PublicAPI.txt b/src/ReactiveUI.Primitives.Reactive/PublicAPI/net11.0-tvos/PublicAPI.txt
index ae2a41e0..f670a0a6 100644
--- a/src/ReactiveUI.Primitives.Reactive/PublicAPI/net11.0-tvos/PublicAPI.txt
+++ b/src/ReactiveUI.Primitives.Reactive/PublicAPI/net11.0-tvos/PublicAPI.txt
@@ -425,6 +425,7 @@ namespace ReactiveUI.Primitives.Reactive
extension(T disposable) where T : System.IDisposable
{
public T DisposeWith(ReactiveUI.Primitives.Disposables.MultipleDisposable disposables) { }
+ public T DisposeWith(ReactiveUI.Primitives.Reactive.Disposables.ContainerDisposable disposables) { }
}
extension(System.IObservable source)
{
@@ -1592,6 +1593,20 @@ namespace ReactiveUI.Primitives.Reactive.Core
public override readonly string ToString() { }
}
}
+namespace ReactiveUI.Primitives.Reactive.Disposables
+{
+ [System.Diagnostics.DebuggerDisplay("Count = {Count}, IsDisposed = {IsDisposed}")]
+ public sealed class ContainerDisposable : ReactiveUI.Primitives.Disposables.MultipleDisposable
+ {
+ public ContainerDisposable() { }
+ public ContainerDisposable(params System.IDisposable[] disposables) { }
+ public ContainerDisposable(System.IDisposable first, System.IDisposable second) { }
+ public ContainerDisposable(System.IDisposable first, System.IDisposable second, System.IDisposable third) { }
+ protected override void Dispose(bool disposing) { }
+ public System.Reactive.Disposables.CompositeDisposable ToCompositeDisposable() { }
+ public static implicit operator System.Reactive.Disposables.CompositeDisposable(ReactiveUI.Primitives.Reactive.Disposables.ContainerDisposable container) { }
+ }
+}
namespace ReactiveUI.Primitives.Reactive.Signals
{
[System.Diagnostics.DebuggerDisplay("Count = {_count}, MaximumCount = {_maximumCount}, IsDraining = {_isDraining}")]
diff --git a/src/ReactiveUI.Primitives.Reactive/PublicAPI/net11.0/PublicAPI.txt b/src/ReactiveUI.Primitives.Reactive/PublicAPI/net11.0/PublicAPI.txt
index d088608d..99a07ed3 100644
--- a/src/ReactiveUI.Primitives.Reactive/PublicAPI/net11.0/PublicAPI.txt
+++ b/src/ReactiveUI.Primitives.Reactive/PublicAPI/net11.0/PublicAPI.txt
@@ -424,6 +424,7 @@ namespace ReactiveUI.Primitives.Reactive
extension(T disposable) where T : System.IDisposable
{
public T DisposeWith(ReactiveUI.Primitives.Disposables.MultipleDisposable disposables) { }
+ public T DisposeWith(ReactiveUI.Primitives.Reactive.Disposables.ContainerDisposable disposables) { }
}
extension(System.IObservable source)
{
@@ -1572,6 +1573,20 @@ namespace ReactiveUI.Primitives.Reactive.Core
public override readonly string ToString() { }
}
}
+namespace ReactiveUI.Primitives.Reactive.Disposables
+{
+ [System.Diagnostics.DebuggerDisplay("Count = {Count}, IsDisposed = {IsDisposed}")]
+ public sealed class ContainerDisposable : ReactiveUI.Primitives.Disposables.MultipleDisposable
+ {
+ public ContainerDisposable() { }
+ public ContainerDisposable(params System.IDisposable[] disposables) { }
+ public ContainerDisposable(System.IDisposable first, System.IDisposable second) { }
+ public ContainerDisposable(System.IDisposable first, System.IDisposable second, System.IDisposable third) { }
+ protected override void Dispose(bool disposing) { }
+ public System.Reactive.Disposables.CompositeDisposable ToCompositeDisposable() { }
+ public static implicit operator System.Reactive.Disposables.CompositeDisposable(ReactiveUI.Primitives.Reactive.Disposables.ContainerDisposable container) { }
+ }
+}
namespace ReactiveUI.Primitives.Reactive.Signals
{
[System.Diagnostics.DebuggerDisplay("Count = {_count}, MaximumCount = {_maximumCount}, IsDraining = {_isDraining}")]
diff --git a/src/ReactiveUI.Primitives.Reactive/PublicAPI/net462/PublicAPI.txt b/src/ReactiveUI.Primitives.Reactive/PublicAPI/net462/PublicAPI.txt
index 070cfc80..bba1c8a5 100644
--- a/src/ReactiveUI.Primitives.Reactive/PublicAPI/net462/PublicAPI.txt
+++ b/src/ReactiveUI.Primitives.Reactive/PublicAPI/net462/PublicAPI.txt
@@ -409,6 +409,7 @@ namespace ReactiveUI.Primitives.Reactive
extension(T disposable) where T : System.IDisposable
{
public T DisposeWith(ReactiveUI.Primitives.Disposables.MultipleDisposable disposables) { }
+ public T DisposeWith(ReactiveUI.Primitives.Reactive.Disposables.ContainerDisposable disposables) { }
}
extension(System.IObservable source)
{
@@ -1554,6 +1555,20 @@ namespace ReactiveUI.Primitives.Reactive.Core
public override readonly string ToString() { }
}
}
+namespace ReactiveUI.Primitives.Reactive.Disposables
+{
+ [System.Diagnostics.DebuggerDisplay("Count = {Count}, IsDisposed = {IsDisposed}")]
+ public sealed class ContainerDisposable : ReactiveUI.Primitives.Disposables.MultipleDisposable
+ {
+ public ContainerDisposable() { }
+ public ContainerDisposable(params System.IDisposable[] disposables) { }
+ public ContainerDisposable(System.IDisposable first, System.IDisposable second) { }
+ public ContainerDisposable(System.IDisposable first, System.IDisposable second, System.IDisposable third) { }
+ protected override void Dispose(bool disposing) { }
+ public System.Reactive.Disposables.CompositeDisposable ToCompositeDisposable() { }
+ public static implicit operator System.Reactive.Disposables.CompositeDisposable(ReactiveUI.Primitives.Reactive.Disposables.ContainerDisposable container) { }
+ }
+}
namespace ReactiveUI.Primitives.Reactive.Signals
{
[System.Diagnostics.DebuggerDisplay("Count = {_count}, MaximumCount = {_maximumCount}, IsDraining = {_isDraining}")]
diff --git a/src/ReactiveUI.Primitives.Reactive/PublicAPI/net472/PublicAPI.txt b/src/ReactiveUI.Primitives.Reactive/PublicAPI/net472/PublicAPI.txt
index 070cfc80..bba1c8a5 100644
--- a/src/ReactiveUI.Primitives.Reactive/PublicAPI/net472/PublicAPI.txt
+++ b/src/ReactiveUI.Primitives.Reactive/PublicAPI/net472/PublicAPI.txt
@@ -409,6 +409,7 @@ namespace ReactiveUI.Primitives.Reactive
extension(T disposable) where T : System.IDisposable
{
public T DisposeWith(ReactiveUI.Primitives.Disposables.MultipleDisposable disposables) { }
+ public T DisposeWith(ReactiveUI.Primitives.Reactive.Disposables.ContainerDisposable disposables) { }
}
extension(System.IObservable source)
{
@@ -1554,6 +1555,20 @@ namespace ReactiveUI.Primitives.Reactive.Core
public override readonly string ToString() { }
}
}
+namespace ReactiveUI.Primitives.Reactive.Disposables
+{
+ [System.Diagnostics.DebuggerDisplay("Count = {Count}, IsDisposed = {IsDisposed}")]
+ public sealed class ContainerDisposable : ReactiveUI.Primitives.Disposables.MultipleDisposable
+ {
+ public ContainerDisposable() { }
+ public ContainerDisposable(params System.IDisposable[] disposables) { }
+ public ContainerDisposable(System.IDisposable first, System.IDisposable second) { }
+ public ContainerDisposable(System.IDisposable first, System.IDisposable second, System.IDisposable third) { }
+ protected override void Dispose(bool disposing) { }
+ public System.Reactive.Disposables.CompositeDisposable ToCompositeDisposable() { }
+ public static implicit operator System.Reactive.Disposables.CompositeDisposable(ReactiveUI.Primitives.Reactive.Disposables.ContainerDisposable container) { }
+ }
+}
namespace ReactiveUI.Primitives.Reactive.Signals
{
[System.Diagnostics.DebuggerDisplay("Count = {_count}, MaximumCount = {_maximumCount}, IsDraining = {_isDraining}")]
diff --git a/src/ReactiveUI.Primitives.Reactive/PublicAPI/net48/PublicAPI.txt b/src/ReactiveUI.Primitives.Reactive/PublicAPI/net48/PublicAPI.txt
index 070cfc80..bba1c8a5 100644
--- a/src/ReactiveUI.Primitives.Reactive/PublicAPI/net48/PublicAPI.txt
+++ b/src/ReactiveUI.Primitives.Reactive/PublicAPI/net48/PublicAPI.txt
@@ -409,6 +409,7 @@ namespace ReactiveUI.Primitives.Reactive
extension(T disposable) where T : System.IDisposable
{
public T DisposeWith(ReactiveUI.Primitives.Disposables.MultipleDisposable disposables) { }
+ public T DisposeWith(ReactiveUI.Primitives.Reactive.Disposables.ContainerDisposable disposables) { }
}
extension(System.IObservable source)
{
@@ -1554,6 +1555,20 @@ namespace ReactiveUI.Primitives.Reactive.Core
public override readonly string ToString() { }
}
}
+namespace ReactiveUI.Primitives.Reactive.Disposables
+{
+ [System.Diagnostics.DebuggerDisplay("Count = {Count}, IsDisposed = {IsDisposed}")]
+ public sealed class ContainerDisposable : ReactiveUI.Primitives.Disposables.MultipleDisposable
+ {
+ public ContainerDisposable() { }
+ public ContainerDisposable(params System.IDisposable[] disposables) { }
+ public ContainerDisposable(System.IDisposable first, System.IDisposable second) { }
+ public ContainerDisposable(System.IDisposable first, System.IDisposable second, System.IDisposable third) { }
+ protected override void Dispose(bool disposing) { }
+ public System.Reactive.Disposables.CompositeDisposable ToCompositeDisposable() { }
+ public static implicit operator System.Reactive.Disposables.CompositeDisposable(ReactiveUI.Primitives.Reactive.Disposables.ContainerDisposable container) { }
+ }
+}
namespace ReactiveUI.Primitives.Reactive.Signals
{
[System.Diagnostics.DebuggerDisplay("Count = {_count}, MaximumCount = {_maximumCount}, IsDraining = {_isDraining}")]
diff --git a/src/ReactiveUI.Primitives.Reactive/PublicAPI/net481/PublicAPI.txt b/src/ReactiveUI.Primitives.Reactive/PublicAPI/net481/PublicAPI.txt
index 070cfc80..bba1c8a5 100644
--- a/src/ReactiveUI.Primitives.Reactive/PublicAPI/net481/PublicAPI.txt
+++ b/src/ReactiveUI.Primitives.Reactive/PublicAPI/net481/PublicAPI.txt
@@ -409,6 +409,7 @@ namespace ReactiveUI.Primitives.Reactive
extension(T disposable) where T : System.IDisposable
{
public T DisposeWith(ReactiveUI.Primitives.Disposables.MultipleDisposable disposables) { }
+ public T DisposeWith(ReactiveUI.Primitives.Reactive.Disposables.ContainerDisposable disposables) { }
}
extension(System.IObservable source)
{
@@ -1554,6 +1555,20 @@ namespace ReactiveUI.Primitives.Reactive.Core
public override readonly string ToString() { }
}
}
+namespace ReactiveUI.Primitives.Reactive.Disposables
+{
+ [System.Diagnostics.DebuggerDisplay("Count = {Count}, IsDisposed = {IsDisposed}")]
+ public sealed class ContainerDisposable : ReactiveUI.Primitives.Disposables.MultipleDisposable
+ {
+ public ContainerDisposable() { }
+ public ContainerDisposable(params System.IDisposable[] disposables) { }
+ public ContainerDisposable(System.IDisposable first, System.IDisposable second) { }
+ public ContainerDisposable(System.IDisposable first, System.IDisposable second, System.IDisposable third) { }
+ protected override void Dispose(bool disposing) { }
+ public System.Reactive.Disposables.CompositeDisposable ToCompositeDisposable() { }
+ public static implicit operator System.Reactive.Disposables.CompositeDisposable(ReactiveUI.Primitives.Reactive.Disposables.ContainerDisposable container) { }
+ }
+}
namespace ReactiveUI.Primitives.Reactive.Signals
{
[System.Diagnostics.DebuggerDisplay("Count = {_count}, MaximumCount = {_maximumCount}, IsDraining = {_isDraining}")]
diff --git a/src/ReactiveUI.Primitives.Reactive/PublicAPI/net8.0/PublicAPI.txt b/src/ReactiveUI.Primitives.Reactive/PublicAPI/net8.0/PublicAPI.txt
index 976c3125..e06d9452 100644
--- a/src/ReactiveUI.Primitives.Reactive/PublicAPI/net8.0/PublicAPI.txt
+++ b/src/ReactiveUI.Primitives.Reactive/PublicAPI/net8.0/PublicAPI.txt
@@ -409,6 +409,7 @@ namespace ReactiveUI.Primitives.Reactive
extension(T disposable) where T : System.IDisposable
{
public T DisposeWith(ReactiveUI.Primitives.Disposables.MultipleDisposable disposables) { }
+ public T DisposeWith(ReactiveUI.Primitives.Reactive.Disposables.ContainerDisposable disposables) { }
}
extension(System.IObservable source)
{
@@ -1554,6 +1555,20 @@ namespace ReactiveUI.Primitives.Reactive.Core
public override readonly string ToString() { }
}
}
+namespace ReactiveUI.Primitives.Reactive.Disposables
+{
+ [System.Diagnostics.DebuggerDisplay("Count = {Count}, IsDisposed = {IsDisposed}")]
+ public sealed class ContainerDisposable : ReactiveUI.Primitives.Disposables.MultipleDisposable
+ {
+ public ContainerDisposable() { }
+ public ContainerDisposable(params System.IDisposable[] disposables) { }
+ public ContainerDisposable(System.IDisposable first, System.IDisposable second) { }
+ public ContainerDisposable(System.IDisposable first, System.IDisposable second, System.IDisposable third) { }
+ protected override void Dispose(bool disposing) { }
+ public System.Reactive.Disposables.CompositeDisposable ToCompositeDisposable() { }
+ public static implicit operator System.Reactive.Disposables.CompositeDisposable(ReactiveUI.Primitives.Reactive.Disposables.ContainerDisposable container) { }
+ }
+}
namespace ReactiveUI.Primitives.Reactive.Signals
{
[System.Diagnostics.DebuggerDisplay("Count = {_count}, MaximumCount = {_maximumCount}, IsDraining = {_isDraining}")]
diff --git a/src/ReactiveUI.Primitives.Reactive/PublicAPI/net9.0/PublicAPI.txt b/src/ReactiveUI.Primitives.Reactive/PublicAPI/net9.0/PublicAPI.txt
index d088608d..99a07ed3 100644
--- a/src/ReactiveUI.Primitives.Reactive/PublicAPI/net9.0/PublicAPI.txt
+++ b/src/ReactiveUI.Primitives.Reactive/PublicAPI/net9.0/PublicAPI.txt
@@ -424,6 +424,7 @@ namespace ReactiveUI.Primitives.Reactive
extension(T disposable) where T : System.IDisposable
{
public T DisposeWith(ReactiveUI.Primitives.Disposables.MultipleDisposable disposables) { }
+ public T DisposeWith(ReactiveUI.Primitives.Reactive.Disposables.ContainerDisposable disposables) { }
}
extension(System.IObservable source)
{
@@ -1572,6 +1573,20 @@ namespace ReactiveUI.Primitives.Reactive.Core
public override readonly string ToString() { }
}
}
+namespace ReactiveUI.Primitives.Reactive.Disposables
+{
+ [System.Diagnostics.DebuggerDisplay("Count = {Count}, IsDisposed = {IsDisposed}")]
+ public sealed class ContainerDisposable : ReactiveUI.Primitives.Disposables.MultipleDisposable
+ {
+ public ContainerDisposable() { }
+ public ContainerDisposable(params System.IDisposable[] disposables) { }
+ public ContainerDisposable(System.IDisposable first, System.IDisposable second) { }
+ public ContainerDisposable(System.IDisposable first, System.IDisposable second, System.IDisposable third) { }
+ protected override void Dispose(bool disposing) { }
+ public System.Reactive.Disposables.CompositeDisposable ToCompositeDisposable() { }
+ public static implicit operator System.Reactive.Disposables.CompositeDisposable(ReactiveUI.Primitives.Reactive.Disposables.ContainerDisposable container) { }
+ }
+}
namespace ReactiveUI.Primitives.Reactive.Signals
{
[System.Diagnostics.DebuggerDisplay("Count = {_count}, MaximumCount = {_maximumCount}, IsDraining = {_isDraining}")]
diff --git a/src/tests/ReactiveUI.Primitives.Reactive.Tests/ContainerDisposableTests.cs b/src/tests/ReactiveUI.Primitives.Reactive.Tests/ContainerDisposableTests.cs
new file mode 100644
index 00000000..95c74fea
--- /dev/null
+++ b/src/tests/ReactiveUI.Primitives.Reactive.Tests/ContainerDisposableTests.cs
@@ -0,0 +1,154 @@
+// Copyright (c) 2019-2026 ReactiveUI Association Incorporated. All rights reserved.
+// ReactiveUI Association Incorporated licenses this file to you under the MIT license.
+// See the LICENSE file in the project root for full license information.
+
+using System.Reactive.Disposables;
+using ReactiveUI.Primitives.Disposables;
+using ReactiveUI.Primitives.Reactive.Disposables;
+
+namespace ReactiveUI.Primitives.Reactive.Tests;
+
+/// Tests for the container that presents itself to System.Reactive as a composite.
+public class ContainerDisposableTests
+{
+ /// The number of disposables seeded by the two-argument constructor.
+ private const int PairCount = 2;
+
+ /// The number of disposables seeded by the three-argument constructor.
+ private const int TripleCount = 3;
+
+ /// The number of disposables seeded by the array constructor.
+ private const int BatchCount = 4;
+
+ /// The number of disposables seeded across every constructor.
+ private const int SeededCount = PairCount + TripleCount + BatchCount;
+
+ /// The number of registrations made directly on the container plus through its composite.
+ private const int DirectAndComposedCount = 2;
+
+ /// Verifies the container hands the same composite to every conversion.
+ /// A task representing the asynchronous operation.
+ [Test]
+ public async Task ConversionYieldsTheSameCompositeEveryTime()
+ {
+ using ContainerDisposable container = [];
+
+ CompositeDisposable first = container;
+ CompositeDisposable second = container;
+
+ await Assert.That(second).IsSameReferenceAs(first);
+ await Assert.That(container.ToCompositeDisposable()).IsSameReferenceAs(first);
+ }
+
+ /// Verifies disposing the container disposes everything registered through the composite.
+ /// A task representing the asynchronous operation.
+ [Test]
+ public async Task DisposingTheContainerDisposesTheCompositeContents()
+ {
+ ContainerDisposable container = [];
+ var disposalCount = 0;
+ CompositeDisposable composite = container;
+ composite.Add(new ActionDisposable(() => disposalCount++));
+
+ await Assert.That(disposalCount).IsEqualTo(0);
+
+ container.Dispose();
+
+ await Assert.That(disposalCount).IsEqualTo(1);
+ await Assert.That(composite.IsDisposed).IsTrue();
+ }
+
+ /// Verifies converting a disposed container yields a sink that disposes late arrivals.
+ /// A task representing the asynchronous operation.
+ [Test]
+ public async Task ConversionAfterDisposalDisposesLateArrivals()
+ {
+ ContainerDisposable container = [];
+ container.Dispose();
+ var disposalCount = 0;
+
+ CompositeDisposable composite = container;
+ composite.Add(new ActionDisposable(() => disposalCount++));
+
+ await Assert.That(composite.IsDisposed).IsTrue();
+ await Assert.That(disposalCount).IsEqualTo(1);
+ await Assert.That(container.ToCompositeDisposable()).IsSameReferenceAs(composite);
+ }
+
+ /// Verifies clearing the container replaces the composite it disposed rather than reusing it.
+ /// A task representing the asynchronous operation.
+ [Test]
+ public async Task ClearReplacesTheCompositeSoTheContainerStaysUsable()
+ {
+ using ContainerDisposable container = [];
+ var disposalCount = 0;
+ CompositeDisposable beforeClear = container;
+ beforeClear.Add(new ActionDisposable(() => disposalCount++));
+
+ container.Clear();
+
+ await Assert.That(disposalCount).IsEqualTo(1);
+ await Assert.That(beforeClear.IsDisposed).IsTrue();
+
+ CompositeDisposable afterClear = container;
+ afterClear.Add(new ActionDisposable(() => disposalCount++));
+
+ await Assert.That(afterClear).IsNotSameReferenceAs(beforeClear);
+ await Assert.That(afterClear.IsDisposed).IsFalse();
+ await Assert.That(disposalCount).IsEqualTo(1);
+ }
+
+ /// Verifies the conversion rejects a null container.
+ /// A task representing the asynchronous operation.
+ [Test]
+ public async Task ConversionThrowsForNullContainer()
+ {
+ var exception = Assert.Throws(static () =>
+ _ = (CompositeDisposable)(ContainerDisposable)null!);
+
+ await Assert.That(exception.ParamName).IsEqualTo("container");
+ }
+
+ /// Verifies the container still behaves as a group of disposables in its own right.
+ /// A task representing the asynchronous operation.
+ [Test]
+ public async Task ContainerTracksItsOwnRegistrationsAlongsideTheComposite()
+ {
+ ContainerDisposable container = [];
+ var disposalCount = 0;
+ ActionDisposable direct = new(() => disposalCount++);
+ container.Add(direct);
+ CompositeDisposable composite = container;
+ composite.Add(new ActionDisposable(() => disposalCount++));
+
+ await Assert.That(container.Contains(direct)).IsTrue();
+
+ container.Dispose();
+
+ await Assert.That(disposalCount).IsEqualTo(DirectAndComposedCount);
+ }
+
+ /// Verifies the constructors seed the container with the supplied disposables.
+ /// A task representing the asynchronous operation.
+ [Test]
+ public async Task ConstructorsSeedTheSuppliedDisposables()
+ {
+ var disposalCount = 0;
+
+ ActionDisposable First() => new(() => disposalCount++);
+
+ ContainerDisposable pair = new(First(), First());
+ ContainerDisposable triple = new(First(), First(), First());
+ ContainerDisposable batch = new([First(), First(), First(), First()]);
+
+ await Assert.That(pair.Count).IsEqualTo(PairCount);
+ await Assert.That(triple.Count).IsEqualTo(TripleCount);
+ await Assert.That(batch.Count).IsEqualTo(BatchCount);
+
+ pair.Dispose();
+ triple.Dispose();
+ batch.Dispose();
+
+ await Assert.That(disposalCount).IsEqualTo(SeededCount);
+ }
+}
diff --git a/src/tests/ReactiveUI.Primitives.Reactive.Tests/LinqExtensionsTests.cs b/src/tests/ReactiveUI.Primitives.Reactive.Tests/LinqExtensionsTests.cs
index d466e48c..cb065c67 100644
--- a/src/tests/ReactiveUI.Primitives.Reactive.Tests/LinqExtensionsTests.cs
+++ b/src/tests/ReactiveUI.Primitives.Reactive.Tests/LinqExtensionsTests.cs
@@ -3,9 +3,15 @@
// See the LICENSE file in the project root for full license information.
using System.Reactive;
+using System.Reactive.Disposables;
+
+// Imported so the DisposeWith tests below are compiled with System.Reactive's own fluent disposal helpers in
+// scope - that is the call-site shape the ContainerDisposable overload exists to keep unambiguous.
+using System.Reactive.Disposables.Fluent;
using System.Reactive.Linq;
using ReactiveUI.Primitives.Advanced;
using ReactiveUI.Primitives.Disposables;
+using ReactiveUI.Primitives.Reactive.Disposables;
using ReactiveUI.Primitives.Reactive.Signals;
using ReactiveLinqExtensions = ReactiveUI.Primitives.Reactive.LinqExtensions;
@@ -54,6 +60,61 @@ public async Task DisposeWithThrowsForNullMultipleDisposable()
await Assert.That(exception.ParamName).IsEqualTo("disposables");
}
+ /// Verifies DisposeWith resolves to this overload for a container and tracks the original disposable.
+ /// A task representing the asynchronous operation.
+ [Test]
+ public async Task DisposeWithContainerReturnsConcreteDisposableAndTracksIt()
+ {
+ var disposalCount = 0;
+ ActionDisposable disposable = new(() => disposalCount++);
+ ContainerDisposable disposables = [];
+
+ var result = disposable.DisposeWith(disposables);
+
+ await Assert.That(result).IsSameReferenceAs(disposable);
+ await Assert.That(disposables.Contains(disposable)).IsTrue();
+
+ // Resolving to this overload rather than System.Reactive's is what keeps the registration on the
+ // container itself instead of the composite it would have been converted into.
+ await Assert.That(((CompositeDisposable)disposables).Count).IsEqualTo(0);
+
+ disposables.Dispose();
+
+ await Assert.That(disposalCount).IsEqualTo(1);
+ }
+
+ /// Verifies DisposeWith rejects a null container.
+ /// A task representing the asynchronous operation.
+ [Test]
+ public async Task DisposeWithThrowsForNullContainerDisposable()
+ {
+ using ActionDisposable disposable = new(static () => { });
+
+ var exception = Assert.Throws(() =>
+ disposable.DisposeWith((ContainerDisposable)null!));
+
+ await Assert.That(exception.ParamName).IsEqualTo("disposables");
+ }
+
+ /// Verifies a container converted to a composite still disposes what System.Reactive registered on it.
+ /// A task representing the asynchronous operation.
+ [Test]
+ public async Task DisposeWithComposedFromAContainerDisposesWithTheContainer()
+ {
+ ContainerDisposable disposables = [];
+ var disposalCount = 0;
+ ActionDisposable disposable = new(() => disposalCount++);
+
+ var result = disposable.DisposeWith((CompositeDisposable)disposables);
+
+ await Assert.That(result).IsSameReferenceAs(disposable);
+ await Assert.That(disposalCount).IsEqualTo(0);
+
+ disposables.Dispose();
+
+ await Assert.That(disposalCount).IsEqualTo(1);
+ }
+
/// Verifies fluent SubscribeSafe accepts nullable object values with Rx imports present.
/// A task representing the asynchronous operation.
[Test]