ReactiveUI.Disposables
7.5.0
Prefix Reserved
dotnet add package ReactiveUI.Disposables --version 7.5.0
NuGet\Install-Package ReactiveUI.Disposables -Version 7.5.0
<PackageReference Include="ReactiveUI.Disposables" Version="7.5.0" />
<PackageVersion Include="ReactiveUI.Disposables" Version="7.5.0" />
<PackageReference Include="ReactiveUI.Disposables" />
paket add ReactiveUI.Disposables --version 7.5.0
#r "nuget: ReactiveUI.Disposables, 7.5.0"
#:package ReactiveUI.Disposables@7.5.0
#addin nuget:?package=ReactiveUI.Disposables&version=7.5.0
#tool nuget:?package=ReactiveUI.Disposables&version=7.5.0
<br>
<a href="https://www.nuget.org/packages/ReactiveUI.Primitives">
<img src="https://img.shields.io/nuget/dt/ReactiveUI.Primitives.svg">
</a>
<a href="https://reactiveui.net/slack">
<img src="https://img.shields.io/badge/chat-slack-blue.svg">
</a>
<img alt="ReactiveUI.Primitives" width="160" height="160" src="https://github.com/reactiveui/styleguide/blob/master/logo_primitives/logo.png?raw=true">
ReactiveUI.Primitives
ReactiveUI.Primitives is a small, fast library for reactive programming in .NET. Reactive programming means working with values that arrive over time, such as button clicks, timer ticks, or network replies, rather than values you already hold.
If you know LINQ, you already know the shape. LINQ queries a collection you already hold and pulls values out of an
IEnumerable<T>. Reactive programming queries values that arrive over time: an IObservable<T> pushes each value to
you as it happens. The operators carry over, so Select, Where, and Aggregate keep their meaning here. This library
also gives them the names Map, Keep, and Fold.
It gives you that model without a runtime dependency on System.Reactive, R3, or R3Async. Those are the established reactive libraries for .NET, and this package stands in for them in the common cases.
It builds on two interfaces that .NET already ships. IObservable<T> is a source you subscribe to. IObserver<T> is
the subscriber that receives each value. The library renames a few common concepts for clarity. It also favours code
paths that allocate little memory and run under ahead-of-time (AOT) compilation. AOT compiles the app to native code
before it runs, so the app cannot generate new code while running.
Goals and design posture
ReactiveUI.Primitives aims to:
- Cover the Rx model over
IObservable<T>: creating streams, subscribing, holding state, scheduling work, and composing operators. A stream is a sequence of values delivered over time. - Rename a few concepts where a clearer name helps. A
Signal<T>is a source you can both push values into and subscribe to (Rx calls this aSubject<T>).Maptransforms each value (RxSelect);Keepfilters values (RxWhere);Sparkturns each notification into a value you can inspect. - Stay AOT-friendly. The production package uses no runtime reflection, no generated code, no expression compilation, and no hidden dependency on System.Reactive, R3, or R3Async.
- Allocate as little as possible on hot paths. For example,
Signal<T>subscribes a single delegate directly, and the common return, empty, and never sources reuse one shared instance. - Run in production across modern .NET and .NET Framework, with separate integration packages for Windows UI and other
platforms. A target framework (TFM) is the .NET version and platform a build targets, such as
net8.0. - Support migration. The
.Reactivepackage variants match System.Reactive's public surface, and source-generator bridges connect to R3 or R3Async when your project already uses them.
Why not System.Reactive or R3?
System.Reactive is the original Rx library for .NET, and the reason IObservable<T> exists. It is mature and widely
used. Its weak point is performance: a typical operator chain allocates several objects per operator and per value, and
that grows under heavy load.
R3 is a newer library aimed at that weak point. It is fast. It reaches that speed partly by replacing IObservable<T>
with its own Observable<T> type. That swap means existing code, and the wider ecosystem built on IObservable<T>,
does not carry over without adaptation.
We wanted the speed without the break, so we kept IObservable<T>, the interface .NET already ships and most C# code
already knows. Our benchmarks pointed at the cause: the interface was not the bottleneck. The cost lived in how the
operators were implemented, not in the abstraction. So we kept the familiar contract and rebuilt the operators as
low-allocation sinks (see Why the operators are built this way).
This keeps the change small for anyone already on IObservable<T>. You keep the contract and the mental model, and you
gain the lower allocation profile. When you do need full System.Reactive or R3 behaviour, the .Reactive package
variants and the R3/R3Async source-generator bridges cover those boundaries.
Where we could not stay on the standard types
Keeping IObservable<T> and IObserver<T> was easy, because both ship in .NET itself. Two related types do not, so we
had to make a call.
The first is the scheduler. A scheduler decides when and on which thread work runs. .NET has no scheduler type of its
own. The standard one, IScheduler, lives in System.Reactive, so using it would pull System.Reactive back in as a
runtime dependency. That is the dependency we set out to avoid. So the lean library defines its own small scheduling
contract, ISequencer.
The second is Unit. Unit is the type that means "a value carrying no information", used for streams that report that
something happened but carry no data. .NET has no such type either, and the common Unit also lives in System.Reactive.
So the lean library defines its own, RxVoid.
These two types are the only places the lean surface departs from the System.Reactive shape. The .Reactive package
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
- Install
- Agent Skills
- Target frameworks and dependencies
- Core model
- Creation factories
- Operators
- ReactiveUI.Primitives.Async
- Extension helpers
- Stateful signals and subject-like types
- Sequencers
- Threading, disposal, and error semantics
- Source-generator bridge behavior
- Migration guides
- Benchmarks and performance posture
- Repository layout
Install
All packages are published on NuGet.org. Install the base package:
dotnet add package ReactiveUI.Primitives
The library is split into a layered set of packages, so you can pull only the surface that matches your integration point. Every package below is produced by a packable project in the current solution and ships at the same version. Target frameworks vary by package; the exact matrices are documented under Target frameworks and dependencies.
| Package | NuGet | Use when |
|---|---|---|
| ReactiveUI.Disposables | You only need the disposable primitives such as Disposable, MultipleDisposable, Slot, or Pocket. |
|
| ReactiveUI.Primitives.Core | The type-agnostic core shared by the lean and System.Reactive-flavoured leaves (usually a transitive dependency). | |
| ReactiveUI.Primitives | The default lean signal/operator/sequencer package, including the migrated ReactiveUI.Extensions helpers. |
|
| ReactiveUI.Primitives.Reactive | The Primitives and extension-helper APIs compiled against System.Reactive Unit and IScheduler. |
|
| ReactiveUI.Primitives.Async.Core | The type-agnostic async core shared by the async leaves. | |
| ReactiveUI.Primitives.Async | Native IObservableAsync<T> / IObserverAsync<T> signals. |
|
| ReactiveUI.Primitives.ObservableEvents | Optional analyzer package that exposes .NET events as provider-native IObservable<T> properties. |
|
| ReactiveUI.Primitives.R3Bridge.Generator | Optional analyzer package that generates R3 and R3Async bridge adapters. | |
| ReactiveUI.Primitives.Async.Reactive | Async Primitives compiled against System.Reactive Unit and IScheduler. |
|
| ReactiveUI.Primitives.Wpf | WPF dispatcher sequencer integration. | |
| ReactiveUI.Primitives.Wpf.Reactive | WPF dispatcher scheduler integration for System.Reactive-first projects. | |
| ReactiveUI.Primitives.WinForms | Windows Forms control sequencer integration. | |
| ReactiveUI.Primitives.WinForms.Reactive | Windows Forms control scheduler integration for System.Reactive-first projects. | |
| ReactiveUI.Primitives.WinUI | WinUI dispatcher-queue sequencer integration. | |
| ReactiveUI.Primitives.WinUI.Reactive | WinUI dispatcher-queue scheduler integration for System.Reactive-first projects. | |
| ReactiveUI.Primitives.Blazor | Blazor renderer sequencer integration. | |
| ReactiveUI.Primitives.Blazor.Reactive | Blazor renderer scheduler integration for System.Reactive-first projects. | |
| ReactiveUI.Primitives.Avalonia | Avalonia UI-thread sequencer integration. | |
| ReactiveUI.Primitives.Avalonia.Reactive | Avalonia UI-thread scheduler integration for System.Reactive-first projects. | |
| ReactiveUI.Primitives.Maui | MAUI dispatcher sequencer integration. | |
| ReactiveUI.Primitives.Maui.Reactive | MAUI dispatcher scheduler integration for System.Reactive-first projects. |
How the packages layer
The base and async families use type-agnostic .Core projects, with a lean leaf binding the shared
RxVoid/ISequencer source to lightweight implementations and a .Reactive leaf recompiling it against
System.Reactive's Unit/IScheduler. Type-agnostic extension-helper sources are compiled into
ReactiveUI.Primitives.Core, while the lean and System.Reactive helper surfaces ship from ReactiveUI.Primitives and
ReactiveUI.Primitives.Reactive. The src/ReactiveUI.Primitives.Extensions.Core directory is source only; it is not a
project or NuGet package. The platform packages also come in lean and .Reactive leaves. (Arrows point from a package
to what it depends on.)
graph TD
SR["System.Reactive"]
Disp["ReactiveUI.Disposables"]
Core["ReactiveUI.Primitives.Core"]
Prim["ReactiveUI.Primitives<br/>(lean)"]
Rx["ReactiveUI.Primitives.Reactive"]
AsyncCore["...Async.Core"]
Async["...Async (lean)"]
AsyncRx["...Async.Reactive"]
Plat["Wpf / WinForms / WinUI / Blazor<br/>Avalonia / Maui"]
PlatRx["Wpf.Reactive / WinForms.Reactive / WinUI.Reactive<br/>Blazor.Reactive / Avalonia.Reactive / Maui.Reactive"]
Core --> Disp
Prim --> Core
Prim --> Disp
Rx --> Core
Rx --> SR
AsyncCore --> Core
Async --> Prim
Async --> AsyncCore
AsyncRx --> Rx
AsyncRx --> AsyncCore
Plat --> Prim
PlatRx --> Rx
ReactiveUI.Primitives.Extensions and ReactiveUI.Primitives.Extensions.Reactive are no longer separate projects or
NuGet packages. Their implementations now ship from ReactiveUI.Primitives and ReactiveUI.Primitives.Reactive,
respectively. No API code was removed: the former lean Extensions package already depended on
ReactiveUI.Primitives, and the former Reactive Extensions package already depended on
ReactiveUI.Primitives.Reactive. Replace only the package reference; the existing
ReactiveUI.Primitives.Extensions* namespaces remain unchanged.
Then import the namespaces you need:
using ReactiveUI.Primitives;
using ReactiveUI.Primitives.Async;
using ReactiveUI.Primitives.Concurrency;
using ReactiveUI.Primitives.Disposables;
using ReactiveUI.Primitives.Extensions;
using ReactiveUI.Primitives.Extensions.Reactive;
using ReactiveUI.Primitives.Async.Signals;
using ReactiveUI.Primitives.Async.Reactive;
using ReactiveUI.Primitives.Reactive;
using ReactiveUI.Primitives.Signals;
The package metadata is configured to include this README in the NuGet package via PackageReadmeFile=README.md. The
base package also packs Skill.md at the package root and a Codex-ready copy at
.agents/skills/reactiveui-primitives/SKILL.md.
R3 and R3Async bridge generation lives in the standalone ReactiveUI.Primitives.R3Bridge.Generator analyzer package:
dotnet add package ReactiveUI.Primitives.R3Bridge.Generator
That generator does not add runtime R3 or R3Async dependencies to ReactiveUI.Primitives. It emits bridge code only when
the consuming compilation already references the relevant external library symbols. System.Reactive interop is provided
by the .Reactive package variants rather than by generated System.Reactive bridge methods.
Agent Skills
The base ReactiveUI.Primitives NuGet package includes Skill.md at the package root and a Codex-ready copy at
.agents/skills/reactiveui-primitives/SKILL.md. It is an agent-oriented guide for choosing the correct
ReactiveUI.Primitives package, using Async, extension helpers, UI sequencers, bridge source generators, and migration from
System.Reactive package variants, R3, or R3Async while assuming the libraries are consumed from NuGet packages.
After package restore, locate the file in the local NuGet package cache:
$version = "<version>"
$skill = "$env:USERPROFILE\.nuget\packages\reactiveui.primitives\$version\.agents\skills\reactiveui-primitives\SKILL.md"
On macOS or Linux:
version="<version>"
skill="$HOME/.nuget/packages/reactiveui.primitives/$version/.agents/skills/reactiveui-primitives/SKILL.md"
Install or link the packaged SKILL.md into the instruction location supported by the agent. Skill.md remains at the
package root for agents or tools that expect a singular markdown guide rather than a skill folder.
| Agent | Recommended project-local install | Notes |
|---|---|---|
| OpenAI Codex | .agents/skills/reactiveui-primitives/SKILL.md |
Codex also supports user-level skills under $HOME/.agents/skills. |
| Claude Code | .claude/skills/reactiveui-primitives/SKILL.md |
Claude Code also supports personal skills under ~/.claude/skills. |
| Cline | .cline/skills/reactiveui-primitives/SKILL.md |
Cline skills must be enabled in Cline's feature settings. |
| GitHub Copilot | .github/instructions/reactiveui-primitives.instructions.md |
For repository-wide behavior, summarize or link the skill from .github/copilot-instructions.md. |
| Cursor | .cursor/rules/reactiveui-primitives.mdc |
Cursor project rules are version-controlled under .cursor/rules; CLAUDE.md is authoritative in this repo, and AGENTS.md can point to it for compatibility. |
| Windsurf | .windsurf/rules/reactiveui-primitives.md |
Windsurf can consume repository guidance via markdown rules; CLAUDE.md is the canonical file in this repo. |
| Gemini CLI | GEMINI.md or an imported file referenced from GEMINI.md |
Gemini CLI loads hierarchical context files and supports importing other markdown files with @file.md. |
Target frameworks and dependencies
Most shared library packages use $(LibraryTargetFrameworks) from src/Directory.Build.props and currently target:
net8.0net9.0net10.0net11.0net462net472net48net481
Package TFM groups are:
ReactiveUI.Disposables,ReactiveUI.Primitives.Core,ReactiveUI.Primitives.Async.Core,ReactiveUI.Primitives.Async, andReactiveUI.Primitives.Async.Reactive:$(LibraryTargetFrameworks).ReactiveUI.Primitives.ObservableEventsandReactiveUI.Primitives.R3Bridge.Generator:netstandard2.0.ReactiveUI.Primitives:$(LibraryTargetFrameworks)plusnet10.0-android,net11.0-android, and Apple platform TFMs (net10.0-ios,net11.0-ios,net10.0-tvos,net11.0-tvos,net10.0-macos,net11.0-macos,net10.0-maccatalyst,net11.0-maccatalyst) when building on Windows or macOS.ReactiveUI.Primitives.Reactive: the same matrix asReactiveUI.Primitives, compiled with System.ReactiveUnitandIScheduleraliases.ReactiveUI.Primitives.WpfandReactiveUI.Primitives.Wpf.Reactive:net8.0-windows,net9.0-windows,net10.0-windows,net11.0-windows,net462,net472,net48,net481.ReactiveUI.Primitives.WinFormsandReactiveUI.Primitives.WinForms.Reactive:net8.0-windows,net9.0-windows,net10.0-windows,net11.0-windows,net462,net472,net48,net481.ReactiveUI.Primitives.WinUIandReactiveUI.Primitives.WinUI.Reactive:net8.0-windows10.0.19041.0,net9.0-windows10.0.19041.0,net10.0-windows10.0.19041.0,net11.0-windows10.0.19041.0.ReactiveUI.Primitives.BlazorandReactiveUI.Primitives.Blazor.Reactive:net8.0,net9.0,net10.0,net11.0.ReactiveUI.Primitives.AvaloniaandReactiveUI.Primitives.Avalonia.Reactive:net8.0,net9.0,net10.0,net11.0.ReactiveUI.Primitives.MauiandReactiveUI.Primitives.Maui.Reactive:net10.0,net11.0.
Runtime package dependencies are intentionally small. The default production packages do not depend on System.Reactive,
R3, R3Async, or the optional R3 bridge generator. ReactiveUI.Primitives references ReactiveUI.Disposables,
and ReactiveUI.Primitives.Core. ReactiveUI.Primitives.Core contains the type-agnostic implementation used by the
extension-helper surfaces. ReactiveUI.Disposables references System.ValueTuple only for net462.
The .Reactive leaf packages intentionally reference System.Reactive through src/Directory.Build.props. They
recompile the shared Primitives source with RxVoid aliased to System.Reactive.Unit, ISequencer aliased to
System.Reactive.Concurrency.IScheduler, and the shared source shifted into .Reactive namespaces.
ReactiveUI.Primitives, ReactiveUI.Primitives.Reactive, ReactiveUI.Primitives.Async.Core,
ReactiveUI.Primitives.Async, and ReactiveUI.Primitives.Async.Reactive add .NET Framework compatibility/support
packages where required, such as
System.ValueTuple, Microsoft.Bcl.TimeProvider, System.Threading.Channels, System.Runtime.CompilerServices.Unsafe,
System.ComponentModel.Annotations, System.Buffers, System.Memory, and System.Collections.Immutable. Add the standalone
ReactiveUI.Primitives.R3Bridge.Generator analyzer package to generate R3/R3Async bridge methods in consuming projects
that already reference those external libraries.
ReactiveUI.Primitives.Blazor and ReactiveUI.Primitives.Blazor.Reactive reference Microsoft.AspNetCore.Components.
ReactiveUI.Primitives.Avalonia and ReactiveUI.Primitives.Avalonia.Reactive reference Avalonia.
ReactiveUI.Primitives.Maui and ReactiveUI.Primitives.Maui.Reactive reference Microsoft.Maui.Core and
Microsoft.Extensions infrastructure packages. ReactiveUI.Primitives.WinUI and ReactiveUI.Primitives.WinUI.Reactive
reference Microsoft.WindowsAppSDK. The remaining shared package references are analyzer, SourceLink, versioning,
ILLink,
reference-assembly, or build-time support packages such as Blazor.Common.Analyzers, Microsoft.SourceLink.GitHub, MinVer,
Roslynator.Analyzers, SonarAnalyzer.CSharp, StyleSharp.Analyzers, Microsoft.NET.ILLink.Tasks, and
Microsoft.NETFramework.ReferenceAssemblies. Benchmark projects may reference System.Reactive,
System.Reactive.Async 6.0.0-alpha.18, R3, and ReactiveUI.Extensions as comparison baselines, but those references are
not
production dependencies.
Core model
Signal<T>
Signal<T> is the basic signal type: a source you can both push values into and subscribe to. It implements
ISignal<T>, which combines IObserver<T>, IObservable<T>, and IsDisposed.
Use it when code needs to push values into a stream and let observers subscribe:
using ReactiveUI.Primitives;
using ReactiveUI.Primitives.Signals;
var signal = new Signal<int>();
using IDisposable subscription = signal.Subscribe(
value => Console.WriteLine($"next: {value}"),
error => Console.WriteLine($"error: {error.Message}"),
() => Console.WriteLine("completed"));
signal.OnNext(1);
signal.OnNext(2);
signal.OnCompleted();
Important behavior:
OnNext(T)sends a value to active subscribers.OnError(Exception)terminates the signal with an error.OnCompleted()terminates the signal successfully.Subscribe(...)returnsIDisposable; disposing the subscription unsubscribes.HasObserversandIsDisposedexpose basic lifecycle state.- The
Subscribe(Action<T>)extension uses an optimized direct-action path forSignal<T>when possible.
Observers and witnesses
ReactiveUI.Primitives keeps the standard IObserver<T> shape and provides helper observer implementations internally
under the Core namespace.
Common user-facing subscription overloads live in SubscribeExtensions:
using ReactiveUI.Primitives;
using ReactiveUI.Primitives.Signals;
var signal = new Signal<string>();
using var nextOnly = signal.Subscribe(value => Console.WriteLine(value));
using var full = signal.Subscribe(
value => Console.WriteLine(value),
error => Console.Error.WriteLine(error),
() => Console.WriteLine("done"));
The library uses the term witness for lightweight observer wrappers. You normally use delegates or IObserver<T>
directly rather than constructing witness types by hand.
Using Primitives alongside System.Reactive
Packages such as DynamicData can bring in System.Reactive transitively. Importing both System and
ReactiveUI.Primitives then exposes two sets of Subscribe extension methods for IObservable<T>.
Use SubscribePrimitives to select the Primitives implementation without changing the observable:
using var subscription = saveCommand.ThrownExceptions.SubscribePrimitives(
error => activity.AddItem(error.ToString()));
It has the same five callback overloads and behavior as Subscribe, including disposal and unhandled-error
propagation. Existing Subscribe APIs remain available. An explicit static call also selects Primitives:
using var subscription = SubscribeExtensions.Subscribe(
saveCommand.ThrownExceptions,
error => activity.AddItem(error.ToString()));
Putting using ReactiveUI.Primitives; inside the consuming namespace also gives its extension methods
precedence over a global using System;. This applies per namespace; a global import alone does not
resolve the conflict. Import only one set of LINQ operators when their signatures overlap.
SubscribeSafe is not a drop-in rename: its single Action<Exception> overload handles terminal errors,
not values emitted by an IObservable<Exception>. To handle exception values with SubscribeSafe, supply
both onNext and onError explicitly.
System.Reactive declares its own observer-taking SubscribeSafe in the System namespace, so that one
overload is ambiguous under the same conditions as Subscribe. Use SubscribeSafePrimitives(observer) to
select the Primitives implementation. The callback shapes of SubscribeSafe have no System.Reactive
counterpart and stay callable under their own name.
Scheduling event handlers and drawing
ObserveOn schedules downstream notifications. Moving work from an event handler into a subscriber
after ObserveOn therefore changes when that work runs, even when the scheduler targets the UI thread.
For paint events such as SkiaSharp's PaintSurface, draw synchronously while the event's surface is valid.
Do not defer use of its canvas through ObserveOn or an await. Schedule a redraw request instead, and
perform the drawing in the resulting paint callback.
The Signal.FromEventPattern<TEventHandler, TEventArgs>(conversion, addHandler, removeHandler) overload
lets a custom event handler perform synchronous work before invoking the notification callback. Each
subscription owns its converted handler and detaches that same handler on disposal. Supplying the conversion
also avoids deriving the handler reflectively, which is what makes this shape trim- and AOT-safe.
Three siblings build on the same conversion. FromEventPattern<TEventHandler, TSender, TEventArgs> keeps the
sender's static type instead of erasing it to object. FromEvent<TEventHandler, TEventArgs> emits the event
argument on its own, for events that carry no sender, and FromEvent<TEventArgs>(addHandler, removeHandler)
covers the plain Action<TEventArgs> case. Every one of them, and every FromEventPattern overload, accepts a
trailing sequencer that attaches and detaches the handler as scheduled work rather than on the subscribing
thread — the shape to use when an event may only be subscribed from the UI thread:
using var painted = Signal.FromEventPattern<SKPaintSurfaceEventArgs>(
handler => view.SkiaElement.PaintSurface += handler,
handler => view.SkiaElement.PaintSurface -= handler,
RxSchedulers.MainThreadScheduler)
.SubscribePrimitives(pattern => Draw(pattern.EventArgs));
Disposing cancels a pending attach, so a subscription torn down before the sequencer ran it never leaves the handler on the event.
Throttle (also called Calm or Stabilize) waits for a quiet period after the most recent value.
By default its timer uses the thread pool; it does not marshal the result to the UI thread. Use
Throttle(duration, uiSequencer) or put ObserveOn(uiSequencer) after Throttle when the subscriber
requires the UI thread. Source completion flushes a pending value immediately, matching Rx debounce
semantics; it does not wait for the remaining quiet period.
Disposables, handles, and slots
Subscriptions and scheduled work return IDisposable. ReactiveUI.Primitives includes lightweight disposable primitives
in ReactiveUI.Primitives.Disposables:
| Type | Use |
|---|---|
Disposable.Create(Action) |
Create an IDisposable from a cleanup action. |
Disposable.Empty |
No-op disposable. |
BooleanDisposable |
Track simple disposed state. |
CancellationDisposable |
Tie disposal to a CancellationTokenSource. |
MultipleDisposable |
Composite-disposable equivalent; add/remove multiple disposables. |
CompositeDisposable |
System.Reactive-compatible alias over MultipleDisposable. |
Pocket |
Named MultipleDisposable specialization. |
SingleDisposable / AssignmentSlot |
Single-assignment disposable container. |
SingleReplaceableDisposable / Slot |
Replaceable disposable container. |
Handle, Handle<T>, Handle<T1,T2>, Handle<T1,T2,T3> |
Lightweight handle wrappers for resource lifetimes. |
Example:
using ReactiveUI.Primitives;
using ReactiveUI.Primitives.Disposables;
using ReactiveUI.Primitives.Signals;
var subscriptions = new MultipleDisposable();
var signal = new Signal<int>();
signal.Subscribe(value => Console.WriteLine(value)).DisposeWith(subscriptions);
signal.Subscribe(value => Console.WriteLine(value * 10)).DisposeWith(subscriptions);
signal.OnNext(3);
subscriptions.Dispose();
Creation factories
Creation APIs live on ReactiveUI.Primitives.Signals.Signal.
| Factory | Purpose |
|---|---|
Signal.Create<T>(Func<IObserver<T>, IDisposable>) |
Build a custom observable. |
Signal.CreateSafe<T>(Func<IObserver<T>, IDisposable>) |
Build a custom observable with safety wrapping. |
Signal.CreateWithState<T,TState>(...) |
Build a custom observable while passing state explicitly. |
Signal.Lazy<T>(Func<IObservable<T>>) |
Create the source per subscription. |
Signal.Emit<T>(T) |
Emit one value and complete. Specialized fast paths exist for bool, int, and RxVoid. |
Signal.None<T>() |
Complete without values. |
Signal.Silent<T>() / Signal.Silent<T>(T witness) |
Never emit and never complete. |
Signal.Fail<T>(Exception) |
Terminate with an error. |
Signal.Sequence(int start, int count) |
Emit an integer range and complete. |
Signal.Loop<T>(T value) / Signal.Loop<T>(T value, int count) |
Repeat indefinitely or a fixed number of times. |
Signal.Unfold<TState,TResult>(...) / Signal.Iterate<TState,TResult>(...) |
Generate a finite sequence from state. |
Signal.Use<TResource,T>(...) |
Tie a resource lifetime to a subscription. |
Signal.FromEventPattern(...) |
Convert .NET events to EventPattern<TEventArgs> values. |
Signal.FromEnumerable<T>(IEnumerable<T>) |
Convert an enumerable. |
Signal.FromEnumerable<T>(IEnumerable<T>, CancellationToken) |
Convert an enumerable and stop synchronous enumeration when cancelled. |
Signal.FromAsyncEnumerable<T>(IAsyncEnumerable<T>, CancellationToken) |
Convert an async enumerable on modern TFMs. |
Signal.FromTask<T>(Task<T>) |
Convert an existing task to a signal. |
Signal.FromAsync<T>(Func<Task<T>>) |
Invoke a task factory per subscription. |
Signal.FromAsync<T>(Func<CancellationToken, Task<T>>) |
Invoke a cancellable task factory per subscription; disposing that subscription cancels only that subscription's token. |
Signal.FromAsync<T>(Func<CancellationToken, Task<T>>, CancellationToken) |
Link each subscription to an external token; external cancellation is forwarded as an observer error while subscribed. |
Signal.After(TimeSpan, ISequencer?) |
Emit one long tick after a delay. |
Signal.Every(TimeSpan, ISequencer?) |
Emit increasing long ticks repeatedly. |
Signal.Pulse(...) |
Alias of Every. |
Signal.After(...) |
One-shot and periodic timer overloads. |
Signal.Chain(...), Signal.Blend(...), Signal.Race(...) |
Compose multiple sources. |
Signal.Pair(...), Signal.SyncLatest(...), Signal.PairLatest(...), Signal.ForkJoin(...) |
Pairwise combination helpers. |
Signal.Scheduled<T>(ISequencer) / Signal.Scheduled<T>(ISequencer, IObserver<T>?) |
Multicast signal that dispatches notifications on a sequencer, with an optional default observer active while no other subscribers are present. |
Signal.Delayable<T>(Func<bool>, Func<IList<T>, IEnumerable<T>>) |
Multicast signal that buffers notifications while delayed and emits a de-duplicated batch when Flush is called. |
Example:
using ReactiveUI.Primitives;
using ReactiveUI.Primitives.Signals;
IObservable<int> values = Signal.Sequence(1, 5);
using var subscription = values.Subscribe(
value => Console.WriteLine(value),
error => Console.Error.WriteLine(error),
() => Console.WriteLine("range completed"));
Custom source example:
using ReactiveUI.Primitives.Disposables;
using ReactiveUI.Primitives.Signals;
IObservable<string> source = Signal.CreateSafe<string>(observer =>
{
observer.OnNext("ready");
observer.OnCompleted();
return Disposable.Empty;
});
Operators
Operators are extension methods over IObservable<T>. Like a LINQ query over IEnumerable<T>, an operator takes a
stream and returns a new stream, so you can chain them into a pipeline. ReactiveUI.Primitives ships its own names
(Map, Keep, Fold, Blend, SwitchTo, and more). These names avoid call-resolution clashes with System.Reactive
or R3. The familiar System.Reactive and LINQ names also work (see below), so you can write whichever reads best.
Why the operators are built this way
Each operator is a purpose-built sink, not a wrapper around another observable. A wrapper chain allocates an observable and an observer for every operator, on every subscription, and each value then hops through the whole stack. A sink does the operator's work in one object and hands the result straight to the next stage. Fewer objects and fewer hops mean fewer allocations per value.
That difference matters most under high throughput. Reactive pipelines often run where events never stop and volume is large: device and sensor telemetry (IoT), market data and payment flows in banking, and log or metric ingestion. At millions of events per second, per-value allocations create work for the garbage collector, and that work shows up as pauses. Keeping allocations low gives steadier latency and higher sustained throughput. This is why the library favours direct subscription and shared singletons, and why the dedicated names bind the compiler straight to these sink-based operators with no ambiguity against the System.Reactive or LINQ overloads.
System.Reactive / LINQ name layer
The everyday System.Reactive and LINQ names are first-class operators. Each builds the same sink as its Primitives-named counterpart, with identical behaviour and allocation profile. A sink is the small object that receives each value and does the operator's work. These names are not wrappers. Both name sets are fully supported and interchangeable, so pick whichever reads best.
| LINQ / System.Reactive name | Primitives name | LINQ / System.Reactive name | Primitives name | |
|---|---|---|---|---|
Select |
Map |
Merge |
Blend |
|
SelectWith |
MapWith |
Concat |
Chain |
|
Where |
Keep |
Amb |
Race |
|
WhereWith |
KeepWith |
Switch |
SwitchTo |
|
WhereNotNull |
KeepNotNull |
Zip |
Pair |
|
Do |
Tap |
CombineLatest |
SyncLatest |
|
DoWith |
TapWith |
WithLatestFrom |
Latch |
|
Scan |
Fold |
SelectMany |
FlatMap |
|
Aggregate |
Reduce |
Delay |
Shift |
|
DistinctUntilChanged |
Unique |
Timeout |
Expire |
|
DistinctUntilChangedBy |
UniqueBy |
Sample |
Probe |
|
IgnoreElements |
IgnoreValues |
Retry |
Reattempt |
|
Materialize |
Spark |
Dematerialize |
Unspark |
using ReactiveUI.Primitives;
using ReactiveUI.Primitives.Signals;
// Reads exactly like System.Reactive, and builds the identical sinks as Map/Keep/Fold.
using var subscription = Signal.Sequence(1, 10)
.Where(value => value % 2 == 0)
.Select(value => value * value)
.Scan(0, (total, value) => total + value)
.Subscribe(Console.WriteLine);
Caveat: because these names live in the
ReactiveUI.Primitivesnamespace, a file that also importsSystem.Reactive.Linqwill get ambiguous-call errors on shared names like.Select/.Where. Use the Primitives names (Map/Keep) in those mixed files, or migrate the file fully off System.Reactive.
Transformation and filtering
| System.Reactive-style concept | ReactiveUI.Primitives API | |
|---|---|---|
Select |
Map |
Prefer Map for the distinct Primitives style. |
stateful Select without closure |
MapWith |
|
Where |
Keep |
|
stateful Where without closure |
KeepWith |
|
| non-null filtering | KeepNotNull |
|
fused Where + Select |
Choose |
Chooser returns (HasValue, Value); the explicit flag lets a non-nullable value type be skipped in one sink. |
OfType / Cast |
KeepType<TResult> / CastTo<TResult> |
|
| side effects | Tap, TapWith |
|
Scan |
Fold |
|
Aggregate |
Reduce |
|
Distinct |
Distinct |
|
DistinctUntilChanged |
Unique |
|
| key-based distinct | DistinctBy, UniqueBy |
|
Take / Skip |
Take, Skip |
|
TakeWhile / SkipWhile |
TakeWhile, SkipWhile |
|
IgnoreElements |
IgnoreValues |
|
DefaultIfEmpty |
DefaultIfEmpty |
Example:
using ReactiveUI.Primitives;
using ReactiveUI.Primitives.Signals;
IObservable<string> labels = Signal.Sequence(1, 10)
.Keep(value => value % 2 == 0)
.Map(value => $"even:{value}")
.Tap(label => Console.WriteLine($"observed {label}"));
using var subscription = labels.Subscribe(Console.WriteLine);
Composition
| Concept | API |
|---|---|
| sequential concatenation | Chain |
| concurrent merge | Blend |
| fused merge + adjacent distinct | BlendUnique |
| first source wins | Race |
| latest inner source wins | SwitchTo |
| filter-null + project + switch to latest inner | SwitchSelect |
| pairwise zip | Pair |
| latest-value combination | SyncLatest |
| System.Reactive-named latest combination | CombineLatest |
| combine left emission with latest right value | Latch |
| latest-fusion alias | PairLatest, FuseLatest |
| last values after both complete | ForkJoin |
| retry | Reattempt |
| catch/rescue | Recover, Rescue, Resume, Signal.Recover |
| final action | Signal.OnCleanup |
Blend example:
using ReactiveUI.Primitives;
using ReactiveUI.Primitives.Signals;
IObservable<int> low = Signal.Sequence(1, 3);
IObservable<int> high = Signal.Sequence(100, 3);
using var merged = Signal.Blend(low, high)
.Subscribe(value => Console.WriteLine(value));
SyncLatest example:
using ReactiveUI.Primitives;
using ReactiveUI.Primitives.Signals;
var width = new StateSignal<int>(640);
var height = new StateSignal<int>(480);
using var area = Signal.SyncLatest(width, height, (w, h) => w * h)
.Subscribe(value => Console.WriteLine($"area={value}"));
width.Value = 800;
height.Value = 600;
SyncLatest and the System.Reactive-named CombineLatest overloads support multi-source projections up to 16 total
sources. The .Reactive package variants expose the same overloads with System.Reactive.Unit and IScheduler
conventions, which keeps migrated Rx code using familiar CombineLatest names while running on the Primitives
implementation.
CombineLatest also provides tuple results for 2–16 sources without a selector. Tuple members are named
First, Second, Third, and so on, and values start flowing after every source has produced a value:
using var dimensions = width.CombineLatest(height)
.SubscribePrimitives(size => Console.WriteLine($"{size.First} x {size.Second}"));
When the sources share an element type and are too many to name, or they only exist as a collection,
CombineLatest also combines them into an IList<T>, with an optional selector over that list. The
collection is enumerated once, when the operator is called, and every notification carries its own list:
using var totals = gauges.CombineLatest(readings => Total(readings))
.SubscribePrimitives(total => Console.WriteLine($"total={total}"));
Listing two to sixteen same-typed sources inline still selects the tuple overload that names each of them; the list overload takes over past that arity, and whenever the sources arrive as an array or a sequence.
Multi-source latest example:
using ReactiveUI.Primitives;
using ReactiveUI.Primitives.Signals;
var first = new StateSignal<int>(1);
var second = new StateSignal<int>(2);
var third = new StateSignal<int>(3);
using var total = first
.SyncLatest(second, third, static (a, b, c) => a + b + c)
.Subscribe(value => Console.WriteLine($"total={value}"));
third.Value = 10;
The Rx-name SelectMany observable overloads keep concurrent merge semantics. Use FlatMap or Bind when you want the
Primitives name, and use SelectMany when porting existing Rx code or keeping LINQ query syntax.
Fused projection example (Choose and SwitchSelect):
using ReactiveUI.Primitives;
using ReactiveUI.Primitives.Signals;
// Choose folds Where + Select into one sink. The explicit HasValue flag lets a
// non-nullable value type be dropped without a nullable wrapper.
using var evens = Signal.Sequence(1, 6)
.Choose(value => (value % 2 == 0, value * 10))
.Subscribe(value => Console.WriteLine($"even*10={value}"));
// SwitchSelect folds WhereNotNull + Select + Switch: skips null keys, projects each
// to an inner source, and mirrors only the latest inner.
var key = new StateSignal<string?>(null);
using var latest = key
.SwitchSelect(selectedKey => Signal.Sequence(selectedKey.Length, 3))
.Subscribe(value => Console.WriteLine($"latest={value}"));
key.Value = "ab";
key.Value = "abcd";
Time, buffering, and async helpers
| Concept | API |
|---|---|
| delayed subscription | DelayStart |
| delayed values | Shift |
| quiet-period sampling | Calm / Stabilize |
| periodic sampling | Probe |
| timeout | Expire |
| schedule subscription | SubscribeOn |
| timestamp values | Timestamp |
| measure intervals | TimeInterval |
| fixed-size buffers | Buffer(count), Buffer(count, skip) |
| collect to list/array signal | CollectList, CollectArray, ToList, ToArray |
| collect asynchronously | CollectListAsync, CollectArrayAsync, ToListAsync, ToArrayAsync |
| first/last value task | FirstAsync, FirstOrDefaultAsync, LastAsync, LastOrDefaultAsync |
Direct static helpers are available when a call site wants an explicit source argument instead of extension-method syntax:
| Helper | Purpose |
|---|---|
Signal.Expire(source, dueTime) / Signal.Expire(source, dueTime, sequencer) |
Apply the Primitives timeout operator directly to a source. |
Signal.Timeout(source, dueTime) / Signal.Timeout(source, dueTime, sequencer) |
System.Reactive-name alias for the direct Expire helper. |
Signal.ToTask(source) / Signal.ToTask(source, cancellationToken) |
Await source completion and return the final value, matching ToTask(). |
Signal.RunAsync(source) / Signal.RunAsync(source, cancellationToken) |
Subscribe immediately and return an awaitable signal for the run. |
After example:
using ReactiveUI.Primitives;
using ReactiveUI.Primitives.Concurrency;
using ReactiveUI.Primitives.Signals;
using var subscription = Signal.After(
dueTime: TimeSpan.FromMilliseconds(250),
period: TimeSpan.FromSeconds(1),
scheduler: ThreadPoolSequencer.Instance)
.Take(3)
.Subscribe(
tick => Console.WriteLine($"tick {tick}"),
error => Console.Error.WriteLine(error),
() => Console.WriteLine("timer completed"));
Spark materialization
Spark<T> represents value/error/completion notifications. Use Spark to convert stream events into values and
Unspark to turn them back into observer notifications.
using ReactiveUI.Primitives;
using ReactiveUI.Primitives.Core;
using ReactiveUI.Primitives.Signals;
IObservable<Spark<int>> sparks = Signal.Sequence(1, 3).Spark();
IObservable<int> values = sparks.Unspark();
ReactiveUI.Primitives.Async
ReactiveUI.Primitives.Async is the async counterpart to the base ReactiveUI.Primitives surface. Its observers
deliver each notification through a ValueTask and accept a CancellationToken, so a producer can await the consumer.
Use it when notification, disposal, or stream collection must run asynchronously. It keeps the Primitives vocabulary,
generates the R3 and R3Async bridges, and offers System.Reactive-flavoured .Reactive variants.
Core async contracts and data types:
| API | Purpose |
|---|---|
IObservableAsync<T> |
Async observable contract. SubscribeAsync receives an IObserverAsync<T> and returns an IAsyncDisposable. |
IObserverAsync<T> |
Async observer contract with OnNextAsync, OnErrorResumeAsync, OnCompletedAsync, and inherited DisposeAsync. |
WitnessAsync<T> |
Base observer type for implementing async observers with disposal, cancellation linking, and concurrency checks. |
ISignalAsync<T> |
Pushable async signal that combines IObserverAsync<T>, IObservableAsync<T>, and a Values observable. |
SignalAsync<T> |
Abstract base and static factory/operator host for async observables. |
ConnectableSignalAsync<T> |
Async connectable sequence returned by multicast/publish operators. |
Result |
Completion result that represents success or terminal failure. |
Optional<T> |
Allocation-free optional value used by replay/latest async signals. |
AsyncContext |
Dispatch abstraction over SynchronizationContext, TaskScheduler, or ISequencer. |
ConcurrentWitnessCallsException |
Raised when a serial witness detects concurrent observer calls. |
UnhandledExceptionHandler |
Central handler for async fire-and-forget failures. |
Async signal factories live in two places. Use ReactiveUI.Primitives.Async.Signals.Signal when you need a mutable
signal, and use SignalAsync when you need a sequence factory or operator:
| Factory group | APIs |
|---|---|
| Mutable signals | Signal.Create<T>(), Signal.Create<T>(SignalCreationOptions), Signal.CreateBehavior<T>(startValue), Signal.CreateBehavior<T>(startValue, BehaviorSignalCreationOptions), Signal.CreateReplayLatest<T>(), Signal.CreateReplayLatest<T>(ReplayLatestSignalCreationOptions) |
| Signal options | SignalCreationOptions, BehaviorSignalCreationOptions, ReplayLatestSignalCreationOptions, PublishingOption |
| Stateless factories | SignalAsync.Emit, EmitRxVoid, None, Fail, Return, Empty, Never, Throw |
| Sequence factories | Sequence, Range, FromEnumerable, FromAsyncEnumerable, ToAsyncSignal, Create, CreateAsBackgroundJob, Defer, FromAsync, Use, Using |
| Time factories | After, Every, Pulse, Timer, Interval |
| Async disposables | DisposableAsync.Empty, DisposableAsync.Create, DisposableAsyncSlot, SingleAssignmentDisposableAsync, SingleReplaceableDisposableAsync, MultipleDisposableAsync |
Async operators follow the same naming style as the core package where that avoids collisions with System.Reactive/R3, while preserving familiar aliases for compatibility:
| Category | APIs |
|---|---|
| Projection/filtering | Map, MapWith, Keep, KeepWith, KeepNotNull, KeepType, CastTo, Select, Where, OfType, Cast, Tap, Do, Fold, Scan, ReduceAsync, AggregateAsync, Distinct, Unique, DistinctBy, UniqueBy, DistinctUntilChanged, DistinctUntilChangedBy, SkipWhileNull, WhereIsNotNull, WhereTrue, WhereFalse, Not, GetMin, GetMax, ForEach |
| Composition | Bind, FlatMap, SelectMany, Chain, Concat, Blend, Merge, SwitchTo, Switch, Pair, Zip, SyncLatest, PairLatest, CombineLatest, CombineLatestValuesAreAllTrue, CombineLatestValuesAreAllFalse, GroupBy |
| Error/retry/recovery | Reattempt, Retry, Recover, Rescue, Resume, Catch, OnErrorResumeAsFailure |
| Time/scheduling | Shift, Delay, Expire, Timeout, Throttle, ObserveOn, Yield |
| Lifetime/multicast | Multicast, Publish, StatelessPublish, ReplayLatestPublish, StatelessReplayLatestPublish, RefCount, OnDispose, TakeUntil, TakeUntilOptions, CompletionSignalDelegate, Wrap |
| Sequence boundaries | Take, Skip, TakeWhile, SkipWhile, Lead, Prepend, StartWith |
| Terminal helpers | FirstAsync, FirstOrDefaultAsync, LastAsync, LastOrDefaultAsync, SingleAsync, SingleOrDefaultAsync, AnyAsync, AllAsync, ContainsAsync, CountAsync, LongCountAsync, ToListAsync, CollectListAsync, CollectArrayAsync, ToDictionaryAsync, ToAsyncEnumerable, WaitCompletionAsync, ForEachAsync, SubscribeAsync |
Basic async sequence example:
using ReactiveUI.Primitives.Async;
List<string> labels = await SignalAsync.Sequence(1, 12)
.Keep(static value => value % 2 == 0)
.Map(static value => $"even:{value}")
.ToListAsync();
Mutable async signal example:
using ReactiveUI.Primitives.Async;
using ReactiveUI.Primitives.Async.Signals;
ISignalAsync<int> requests = Signal.Create<int>();
await using IAsyncDisposable subscription = await requests.Values
.Map(static value => value * 2)
.SubscribeAsync(value => Console.WriteLine(value));
await requests.OnNextAsync(21, CancellationToken.None);
await requests.OnCompletedAsync(Result.Success);
Async context example:
using ReactiveUI.Primitives.Async;
AsyncContext context = AsyncContext.From(TaskScheduler.Default);
await using IAsyncDisposable subscription = await SignalAsync.Sequence(1, 3)
.ObserveOn(context)
.SubscribeAsync(static value => Console.WriteLine(value));
ReactiveUI.Primitives.R3Bridge.Generator also emits async bridge adapters. A consumer that references R3,
ReactiveUI.Primitives.Async, and the generator can use generated
AsPrimitivesAsyncObservable<T>(this R3.Observable<T>) and
AsR3Observable<T>(this IObservableAsync<T>); a consumer that references R3Async can use
AsPrimitivesAsyncObservable<T>(this R3Async.AsyncObservable<T>) and
AsR3AsyncObservable<T>(this IObservableAsync<T>). System.Reactive-shaped async APIs are handled by
ReactiveUI.Primitives.Async.Reactive, not by generated System.Reactive.Async adapters.
Extension helpers
The ReactiveUI.Primitives.Extensions namespace migrates the non-async helper surface from ReactiveUI.Extensions onto
ReactiveUI.Primitives. The lean implementation is based on the BCL IObservable<T> contract, uses ISequencer for
scheduling, and does not reference System.Reactive, R3, or R3Async. The corresponding
ReactiveUI.Primitives.Extensions.Reactive namespace ships from ReactiveUI.Primitives.Reactive and uses
System.Reactive Unit and IScheduler conventions.
These namespaces previously shipped from separate ReactiveUI.Primitives.Extensions and
ReactiveUI.Primitives.Extensions.Reactive packages. Their code has been consolidated into the base lean and Reactive
packages; no helper implementation or public namespace was removed.
Core utility surface:
| API | Purpose |
|---|---|
Heartbeat<T> / IHeartbeat<T> |
Value plus heartbeat metadata from heartbeat operators. |
Stale<T> / IStale<T> |
Value plus stale/fresh state from stale-detection operators. |
Continuation |
Disposable continuation helper for bridging synchronous waits. |
Observables.Return<T>(value) |
Single-value observable factory. |
ObserverExtensions.FastForEach |
Pushes enumerable values into an observer with array/list fast paths. |
ObservableSubscriptionExtensions |
Synchronous test/utility helpers: SubscribeGetValue, SubscribeAndComplete, SubscribeGetError, WaitForValue, WaitForCompletion, WaitForError. |
Extension operators are grouped below by feature area:
| Category | APIs |
|---|---|
| Filtering/projection | WhereIsNotNull, SkipWhileNull, Not, WhereTrue, WhereFalse, WhereSelect, SelectConstant, TrySelect, SelectManyThen, Pairwise, Partition, Filter, ForEach, Shuffle, LatestOrDefault, GetMin, GetMax, CombineLatestValuesAreAllTrue, CombineLatestValuesAreAllFalse |
| Error/retry | CatchIgnore, CatchAndReturn, CatchReturn, CatchReturnUnit, LogErrors, OnErrorRetry, RetryWithBackoff, RetryWithDelay, RetryForeverWithDelay, RetryWithFixedDelay |
| Time/scheduling | SyncTimer, ObserveOnIf, ScheduleSafe, Schedule, SampleLatest, DetectStale, Conflate, Heartbeat, ThrottleFirst, ThrottleUntilTrue, ThrottleOnScheduler, ThrottleDistinct, DebounceImmediate, DebounceUntil, WaitUntil |
| Buffer/collection | BufferUntil, BufferUntilIdle, BufferUntilInactive, FromArray, RunAll, FirstMatchFromCandidates |
| Async/sync interaction | SynchronizeSynchronous, SubscribeSynchronous, SynchronizeAsync, SubscribeAsync, SelectAsync, SelectAsyncSequential, SelectLatestAsync, SelectAsyncConcurrent, DropIfBusy, WithLimitedConcurrency |
| State/property/lifetime | AsSignal, ToReadOnlyBehavior, ReplayLastOnSubscribe, SwitchIfEmpty, TakeUntil, Start, Using, While, ScanWithInitial, ToHotTask, ToHotValueTask, ToPropertyObservable, OnNext(params), DoOnSubscribe, DoOnDispose |
Filtering and projection example:
using ReactiveUI.Primitives;
using ReactiveUI.Primitives.Extensions;
using ReactiveUI.Primitives.Signals;
IObservable<string> labels = Signal.Sequence(1, 10)
.WhereSelect(
static value => value % 2 == 0,
static value => $"even:{value}");
using IDisposable subscription = labels.Subscribe(Console.WriteLine);
Scheduling example:
using ReactiveUI.Primitives.Concurrency;
using ReactiveUI.Primitives.Extensions;
ISequencer sequencer = ThreadPoolSequencer.Instance;
using IDisposable work = "ready"
.Schedule(TimeSpan.FromMilliseconds(50), sequencer)
.Subscribe(Console.WriteLine);
Async selector example over a BCL observable:
using ReactiveUI.Primitives;
using ReactiveUI.Primitives.Extensions;
using ReactiveUI.Primitives.Signals;
IObservable<string> names = Signal.Sequence(1, 3)
.SelectAsyncSequential(static async value =>
{
await Task.Yield();
return $"item:{value}";
});
using IDisposable subscription = names.Subscribe(Console.WriteLine);
These helpers are intended for applications that already use the operators from ReactiveUI.Extensions and want the
same shapes without pulling System.Reactive or R3 into the lean production dependency graph.
Filter(string pattern) creates a regex with a 30-second match timeout so ordinary filters remain stable under
instrumented CI runs while still protecting against runaway patterns. Use Filter(Regex regex) when a caller-specified
regex timeout or options set must be preserved exactly.
Stateful signals and subject-like types
ReactiveUI.Primitives uses explicit names instead of cloning every System.Reactive subject type name.
| System.Reactive type | ReactiveUI.Primitives equivalent | Notes |
|---|---|---|
Subject<T> |
Signal<T> |
Push values, errors, and completion to subscribers. |
BehaviorSubject<T> |
StateSignal<T> |
Stores the latest value, exposes a mutable Value, and emits changes through Changed. |
ReplaySubject<T> |
ReplaySignal<T> |
Replays buffered values by size and/or time window. |
AsyncSubject<T> |
FinalSignal<T> |
Awaitable subject-like signal; also implements IAwaitSignal<T>. |
ReactiveProperty<T> / state holder |
StateSignal<T> plus ReadOnlyState<T> |
Mutable state and read-only projected state. |
Subject<T>.ObserveOn(scheduler) |
ScheduledSignal<T> |
Multicast signal that dispatches its notifications on an ISequencer, with an optional default observer active while no other subscribers are present. |
Buffer(boundary).SelectMany(distinct) pipeline |
DelayableNotificationSignal<T> |
Passes notifications through immediately while not delayed, buffers them while delayed, and emits a de-duplicated batch on Flush. |
State example:
using ReactiveUI.Primitives;
using ReactiveUI.Primitives.Signals;
var temperature = new StateSignal<double>(21.5);
ReadOnlyState<string> status = temperature.ToReadOnlyState(value =>
value >= 25.0 ? "warm" : "normal");
using var stateSubscription = status.Changed.Subscribe(Console.WriteLine);
temperature.Value = 26.2;
temperature.Refresh();
Replay example:
using ReactiveUI.Primitives;
using ReactiveUI.Primitives.Signals;
var history = new ReplaySignal<string>(bufferSize: 2);
history.OnNext("A");
history.OnNext("B");
history.OnNext("C");
using var subscription = history.Subscribe(Console.WriteLine); // replays B, C
Delayable example:
using ReactiveUI.Primitives.Signals;
var delayed = true;
var notifications = Signal.Delayable<string>(() => delayed, items => items.Distinct());
using var subscription = notifications.Subscribe(Console.WriteLine);
notifications.OnNext("A");
notifications.OnNext("A"); // buffered while delayed
delayed = false;
notifications.Flush(); // emits the de-duplicated batch: A
Error and completion example: