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]