Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
112 changes: 112 additions & 0 deletions src/ReactiveUI.Primitives.Reactive/Disposables/ContainerDisposable.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// A <see cref="MultipleDisposable"/> that a System.Reactive consumer can use as a
/// <see cref="CompositeDisposable"/>, so an activation-scoped container flows into APIs written against
/// System.Reactive - <c>DisposeWith</c> above all - without the caller converting it by hand.
/// </summary>
/// <remarks>
/// <para>
/// The conversion is identity-stable: every conversion of the same container yields the same
/// <see cref="CompositeDisposable"/>, 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 <see cref="MultipleDisposable.Add"/>.
/// </para>
/// <para>
/// Registrations made through the composite are not visible to the container's own
/// <see cref="ICollection{T}"/> members: the composite occupies a single slot, so <c>Count</c> counts it once
/// and <c>Contains</c>/<c>Remove</c> do not see through it.
/// </para>
/// </remarks>
[System.Diagnostics.DebuggerDisplay("Count = {Count}, IsDisposed = {IsDisposed}")]
public sealed class ContainerDisposable : MultipleDisposable
{
/// <summary>Serializes creation of the composite.</summary>
private readonly Lock _gate = new();

/// <summary>The composite handed to System.Reactive consumers, created on first conversion.</summary>
private CompositeDisposable? _composite;

/// <summary>Initializes a new instance of the <see cref="ContainerDisposable"/> class.</summary>
public ContainerDisposable()
{
}

/// <summary>Initializes a new instance of the <see cref="ContainerDisposable"/> class.</summary>
/// <param name="first">The first disposable.</param>
/// <param name="second">The second disposable.</param>
public ContainerDisposable(IDisposable first, IDisposable second)
: base(first, second)
{
}

/// <summary>Initializes a new instance of the <see cref="ContainerDisposable"/> class.</summary>
/// <param name="first">The first disposable.</param>
/// <param name="second">The second disposable.</param>
/// <param name="third">The third disposable.</param>
public ContainerDisposable(IDisposable first, IDisposable second, IDisposable third)
: base(first, second, third)
{
}

/// <summary>Initializes a new instance of the <see cref="ContainerDisposable"/> class from a group of disposables.</summary>
/// <param name="disposables">Disposables that will be disposed together.</param>
/// <exception cref="ArgumentExceptionHelper"><paramref name="disposables"/> is <see langword="null"/>.</exception>
public ContainerDisposable(params IDisposable[] disposables)
: base(disposables)
{
}

/// <summary>Hands the container to a System.Reactive consumer as the composite it owns.</summary>
/// <param name="container">The container to convert.</param>
/// <exception cref="ArgumentExceptionHelper"><paramref name="container"/> is <see langword="null"/>.</exception>
public static implicit operator CompositeDisposable(ContainerDisposable container)
{
ArgumentExceptionHelper.ThrowIfNull(container);

return container.ToCompositeDisposable();
}

/// <summary>Gets the <see cref="CompositeDisposable"/> this container owns, creating it on first call.</summary>
/// <returns>The composite whose contents are disposed along with this container.</returns>
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;
}
}

/// <inheritdoc/>
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();
}
}
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>Miscellaneous Primitives extensions.</summary>
public static partial class LinqExtensions
{
/// <summary>Disposal-tracking operators for a disposable.</summary>
/// <typeparam name="T">The disposable type.</typeparam>
/// <param name="disposable">The disposable.</param>
extension<T>(T disposable)
where T : IDisposable
{
/// <summary>Disposes the IDisposable with the container.</summary>
/// <param name="disposables">The container.</param>
/// <returns>The original disposable.</returns>
/// <exception cref="ArgumentExceptionHelper"><paramref name="disposables"/> is <see langword="null"/>.</exception>
/// <remarks>
/// A <see cref="ContainerDisposable"/> converts to a System.Reactive <c>CompositeDisposable</c>, 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
/// <c>DisposeWith(MultipleDisposable)</c> and System.Reactive's <c>DisposeWith(CompositeDisposable)</c>
/// - and is ambiguous. Taking the container exactly makes this an identity match, which wins outright.
/// </remarks>
public T DisposeWith(ContainerDisposable disposables)
{
ArgumentExceptionHelper.ThrowIfNull(disposables);

disposables.Add(disposable);
return disposable;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -425,6 +425,7 @@ namespace ReactiveUI.Primitives.Reactive
extension<T>(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<object?> source)
{
Expand Down Expand Up @@ -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}")]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -425,6 +425,7 @@ namespace ReactiveUI.Primitives.Reactive
extension<T>(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<object?> source)
{
Expand Down Expand Up @@ -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}")]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -425,6 +425,7 @@ namespace ReactiveUI.Primitives.Reactive
extension<T>(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<object?> source)
{
Expand Down Expand Up @@ -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}")]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -425,6 +425,7 @@ namespace ReactiveUI.Primitives.Reactive
extension<T>(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<object?> source)
{
Expand Down Expand Up @@ -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}")]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -425,6 +425,7 @@ namespace ReactiveUI.Primitives.Reactive
extension<T>(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<object?> source)
{
Expand Down Expand Up @@ -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}")]
Expand Down
15 changes: 15 additions & 0 deletions src/ReactiveUI.Primitives.Reactive/PublicAPI/net10.0/PublicAPI.txt
Original file line number Diff line number Diff line change
Expand Up @@ -424,6 +424,7 @@ namespace ReactiveUI.Primitives.Reactive
extension<T>(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<object?> source)
{
Expand Down Expand Up @@ -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}")]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -425,6 +425,7 @@ namespace ReactiveUI.Primitives.Reactive
extension<T>(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<object?> source)
{
Expand Down Expand Up @@ -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}")]
Expand Down
Loading
Loading