Skip to content

Repository files navigation

NuGet StatsBuildCode Coverage#yourfirstpr

ReactiveUI.Primitives

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 a Subject<T>). Map transforms each value (Rx Select); Keep filters values (Rx Where); Spark turns 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 .Reactive package 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

  1. Install
  2. Agent Skills
  3. Target frameworks and dependencies
  4. Core model
  5. Creation factories
  6. Operators
  7. ReactiveUI.Primitives.Async
  8. Extension helpers
  9. Stateful signals and subject-like types
  10. Sequencers
  11. Threading, disposal, and error semantics
  12. Source-generator bridge behavior
  13. Migration guides
  14. Benchmarks and performance posture
  15. 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.

PackageNuGetUse when
ReactiveUI.DisposablesDispBYou only need the disposable primitives such as Disposable, MultipleDisposable, Slot, or Pocket.
ReactiveUI.Primitives.CoreCoreBThe type-agnostic core shared by the lean and System.Reactive-flavoured leaves (usually a transitive dependency).
ReactiveUI.PrimitivesPrimBThe default lean signal/operator/sequencer package, including the migrated ReactiveUI.Extensions helpers.
ReactiveUI.Primitives.ReactiveRxBThe Primitives and extension-helper APIs compiled against System.Reactive Unit and IScheduler.
ReactiveUI.Primitives.Async.CoreAsyncCoreBThe type-agnostic async core shared by the async leaves.
ReactiveUI.Primitives.AsyncAsyncBNative IObservableAsync<T> / IObserverAsync<T> signals.
ReactiveUI.Primitives.ObservableEventsEventsBOptional analyzer package that exposes .NET events as provider-native IObservable<T> properties.
ReactiveUI.Primitives.R3Bridge.GeneratorR3BridgeBOptional analyzer package that generates R3 and R3Async bridge adapters.
ReactiveUI.Primitives.Async.ReactiveAsyncRxBAsync Primitives compiled against System.Reactive Unit and IScheduler.
ReactiveUI.Primitives.WpfWpfBWPF dispatcher sequencer integration.
ReactiveUI.Primitives.Wpf.ReactiveWpfRxBWPF dispatcher scheduler integration for System.Reactive-first projects.
ReactiveUI.Primitives.WinFormsWinFormsBWindows Forms control sequencer integration.
ReactiveUI.Primitives.WinForms.ReactiveWinFormsRxBWindows Forms control scheduler integration for System.Reactive-first projects.
ReactiveUI.Primitives.WinUIWinUIBWinUI dispatcher-queue sequencer integration.
ReactiveUI.Primitives.WinUI.ReactiveWinUIRxBWinUI dispatcher-queue scheduler integration for System.Reactive-first projects.
ReactiveUI.Primitives.BlazorBlazorBBlazor renderer sequencer integration.
ReactiveUI.Primitives.Blazor.ReactiveBlazorRxBBlazor renderer scheduler integration for System.Reactive-first projects.
ReactiveUI.Primitives.AvaloniaAvaloniaBAvalonia UI-thread sequencer integration.
ReactiveUI.Primitives.Avalonia.ReactiveAvaloniaRxBAvalonia UI-thread scheduler integration for System.Reactive-first projects.
ReactiveUI.Primitives.MauiMauiBMAUI dispatcher sequencer integration.
ReactiveUI.Primitives.Maui.ReactiveMauiRxBMAUI 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
Loading

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:

usingReactiveUI.Primitives;usingReactiveUI.Primitives.Async;usingReactiveUI.Primitives.Concurrency;usingReactiveUI.Primitives.Disposables;usingReactiveUI.Primitives.Extensions;usingReactiveUI.Primitives.Extensions.Reactive;usingReactiveUI.Primitives.Async.Signals;usingReactiveUI.Primitives.Async.Reactive;usingReactiveUI.Primitives.Reactive;usingReactiveUI.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.

AgentRecommended project-local installNotes
OpenAI Codex.agents/skills/reactiveui-primitives/SKILL.mdCodex also supports user-level skills under $HOME/.agents/skills.
Claude Code.claude/skills/reactiveui-primitives/SKILL.mdClaude Code also supports personal skills under ~/.claude/skills.
Cline.cline/skills/reactiveui-primitives/SKILL.mdCline skills must be enabled in Cline's feature settings.
GitHub Copilot.github/instructions/reactiveui-primitives.instructions.mdFor repository-wide behavior, summarize or link the skill from .github/copilot-instructions.md.
Cursor.cursor/rules/reactiveui-primitives.mdcCursor 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.mdWindsurf can consume repository guidance via markdown rules; CLAUDE.md is the canonical file in this repo.
Gemini CLIGEMINI.md or an imported file referenced from GEMINI.mdGemini 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.0
  • net9.0
  • net10.0
  • net11.0
  • net462
  • net472
  • net48
  • net481

Package TFM groups are:

  • ReactiveUI.Disposables, ReactiveUI.Primitives.Core, ReactiveUI.Primitives.Async.Core, ReactiveUI.Primitives.Async, and ReactiveUI.Primitives.Async.Reactive: $(LibraryTargetFrameworks).
  • ReactiveUI.Primitives.ObservableEvents and ReactiveUI.Primitives.R3Bridge.Generator: netstandard2.0.
  • ReactiveUI.Primitives: $(LibraryTargetFrameworks) plus net10.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 as ReactiveUI.Primitives, compiled with System.Reactive Unit and IScheduler aliases.
  • ReactiveUI.Primitives.Wpf and ReactiveUI.Primitives.Wpf.Reactive: net8.0-windows, net9.0-windows, net10.0-windows, net11.0-windows, net462, net472, net48, net481.
  • ReactiveUI.Primitives.WinForms and ReactiveUI.Primitives.WinForms.Reactive: net8.0-windows, net9.0-windows, net10.0-windows, net11.0-windows, net462, net472, net48, net481.
  • ReactiveUI.Primitives.WinUI and ReactiveUI.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.Blazor and ReactiveUI.Primitives.Blazor.Reactive: net8.0, net9.0, net10.0, net11.0.
  • ReactiveUI.Primitives.Avalonia and ReactiveUI.Primitives.Avalonia.Reactive: net8.0, net9.0, net10.0, net11.0.
  • ReactiveUI.Primitives.Maui and ReactiveUI.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:

usingReactiveUI.Primitives;usingReactiveUI.Primitives.Signals;varsignal=newSignal<int>();usingIDisposablesubscription=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(...) returns IDisposable; disposing the subscription unsubscribes.
  • HasObservers and IsDisposed expose basic lifecycle state.
  • The Subscribe(Action<T>) extension uses an optimized direct-action path for Signal<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 SubscribeMixins:

usingReactiveUI.Primitives;usingReactiveUI.Primitives.Signals;varsignal=newSignal<string>();usingvarnextOnly=signal.Subscribe(value =>Console.WriteLine(value));usingvarfull=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.

Disposables, handles, and slots

Subscriptions and scheduled work return IDisposable. ReactiveUI.Primitives includes lightweight disposable primitives in ReactiveUI.Primitives.Disposables:

TypeUse
Disposable.Create(Action)Create an IDisposable from a cleanup action.
Disposable.EmptyNo-op disposable.
BooleanDisposableTrack simple disposed state.
CancellationDisposableTie disposal to a CancellationTokenSource.
MultipleDisposableComposite-disposable equivalent; add/remove multiple disposables.
CompositeDisposableSystem.Reactive-compatible alias over MultipleDisposable.
PocketNamed MultipleDisposable specialization.
SingleDisposable / AssignmentSlotSingle-assignment disposable container.
SingleReplaceableDisposable / SlotReplaceable disposable container.
Handle, Handle<T>, Handle<T1,T2>, Handle<T1,T2,T3>Lightweight handle wrappers for resource lifetimes.

Example:

usingReactiveUI.Primitives;usingReactiveUI.Primitives.Disposables;usingReactiveUI.Primitives.Signals;varsubscriptions=newMultipleDisposable();varsignal=newSignal<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.

FactoryPurpose
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:

usingReactiveUI.Primitives;usingReactiveUI.Primitives.Signals;IObservable<int>values=Signal.Sequence(1,5);usingvarsubscription=values.Subscribe(
value =>Console.WriteLine(value),
error =>Console.Error.WriteLine(error),()=>Console.WriteLine("range completed"));

Custom source example:

usingReactiveUI.Primitives.Disposables;usingReactiveUI.Primitives.Signals;IObservable<string>source=Signal.CreateSafe<string>(observer =>{observer.OnNext("ready");observer.OnCompleted();returnDisposable.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 namePrimitives nameLINQ / System.Reactive namePrimitives name
SelectMapMergeBlend
SelectWithMapWithConcatChain
WhereKeepAmbRace
WhereWithKeepWithSwitchSwitchTo
WhereNotNullKeepNotNullZipPair
DoTapCombineLatestSyncLatest
DoWithTapWithWithLatestFromLatch
ScanFoldSelectManyFlatMap
AggregateReduceDelayShift
DistinctUntilChangedUniqueTimeoutExpire
DistinctUntilChangedByUniqueBySampleProbe
IgnoreElementsIgnoreValuesRetryReattempt
MaterializeSparkDematerializeUnspark
usingReactiveUI.Primitives;usingReactiveUI.Primitives.Signals;// Reads exactly like System.Reactive, and builds the identical sinks as Map/Keep/Fold.usingvarsubscription=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.Primitives namespace, a file that also imports System.Reactive.Linq will 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 conceptReactiveUI.Primitives API
SelectMap
stateful Select without closureMapWith
WhereKeep
stateful Where without closureKeepWith
non-null filteringKeepNotNull
fused Where + SelectChoose
OfType / CastKeepType<TResult> / CastTo<TResult>
side effectsTap, TapWith
ScanFold
AggregateReduce
DistinctDistinct
DistinctUntilChangedUnique
key-based distinctDistinctBy, UniqueBy
Take / SkipTake, Skip
TakeWhile / SkipWhileTakeWhile, SkipWhile
IgnoreElementsIgnoreValues
DefaultIfEmptyDefaultIfEmpty

Example:

usingReactiveUI.Primitives;usingReactiveUI.Primitives.Signals;IObservable<string>labels=Signal.Sequence(1,10).Keep(value =>value%2==0).Map(value =>$"even:{value}").Tap(label =>Console.WriteLine($"observed {label}"));usingvarsubscription=labels.Subscribe(Console.WriteLine);

Composition

ConceptAPI
sequential concatenationChain
concurrent mergeBlend
fused merge + adjacent distinctBlendUnique
first source winsRace
latest inner source winsSwitchTo
filter-null + project + switch to latest innerSwitchSelect
pairwise zipPair
latest-value combinationSyncLatest
System.Reactive-named latest combinationCombineLatest
combine left emission with latest right valueLatch
latest-fusion aliasPairLatest, FuseLatest
last values after both completeForkJoin
retryReattempt
catch/rescueRecover, Rescue, Resume, Signal.Recover
final actionSignal.OnCleanup

Blend example:

usingReactiveUI.Primitives;usingReactiveUI.Primitives.Signals;IObservable<int>low=Signal.Sequence(1,3);IObservable<int>high=Signal.Sequence(100,3);usingvarmerged=Signal.Blend(low,high).Subscribe(value =>Console.WriteLine(value));

SyncLatest example:

usingReactiveUI.Primitives;usingReactiveUI.Primitives.Signals;varwidth=newStateSignal<int>(640);varheight=newStateSignal<int>(480);usingvararea=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.

Multi-source latest example:

usingReactiveUI.Primitives;usingReactiveUI.Primitives.Signals;varfirst=newStateSignal<int>(1);varsecond=newStateSignal<int>(2);varthird=newStateSignal<int>(3);usingvartotal=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):

usingReactiveUI.Primitives;usingReactiveUI.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.usingvarevens=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.varkey=newStateSignal<string?>(null);usingvarlatest=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

ConceptAPI
delayed subscriptionDelayStart
delayed valuesShift
quiet-period samplingCalm / Stabilize
periodic samplingProbe
timeoutExpire
schedule subscriptionSubscribeOn
timestamp valuesTimestamp
measure intervalsTimeInterval
fixed-size buffersBuffer(count), Buffer(count, skip)
collect to list/array signalCollectList, CollectArray, ToList, ToArray
collect asynchronouslyCollectListAsync, CollectArrayAsync, ToListAsync, ToArrayAsync
first/last value taskFirstAsync, FirstOrDefaultAsync, LastAsync, LastOrDefaultAsync

Direct static helpers are available when a call site wants an explicit source argument instead of extension-method syntax:

HelperPurpose
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:

usingReactiveUI.Primitives;usingReactiveUI.Primitives.Concurrency;usingReactiveUI.Primitives.Signals;usingvarsubscription=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.

usingReactiveUI.Primitives;usingReactiveUI.Primitives.Core;usingReactiveUI.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:

APIPurpose
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.
ResultCompletion result that represents success or terminal failure.
Optional<T>Allocation-free optional value used by replay/latest async signals.
AsyncContextDispatch abstraction over SynchronizationContext, TaskScheduler, or ISequencer.
ConcurrentWitnessCallsExceptionRaised when a serial witness detects concurrent observer calls.
UnhandledExceptionHandlerCentral 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 groupAPIs
Mutable signalsSignal.Create<T>(), Signal.Create<T>(SignalCreationOptions), Signal.CreateBehavior<T>(startValue), Signal.CreateBehavior<T>(startValue, BehaviorSignalCreationOptions), Signal.CreateReplayLatest<T>(), Signal.CreateReplayLatest<T>(ReplayLatestSignalCreationOptions)
Signal optionsSignalCreationOptions, BehaviorSignalCreationOptions, ReplayLatestSignalCreationOptions, PublishingOption
Stateless factoriesSignalAsync.Emit, EmitRxVoid, None, Fail, Return, Empty, Never, Throw
Sequence factoriesSequence, Range, FromEnumerable, FromAsyncEnumerable, ToAsyncSignal, Create, CreateAsBackgroundJob, Defer, FromAsync, Use, Using
Time factoriesAfter, Every, Pulse, Timer, Interval
Async disposablesDisposableAsync.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:

CategoryAPIs
Projection/filteringMap, 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
CompositionBind, FlatMap, SelectMany, Chain, Concat, Blend, Merge, SwitchTo, Switch, Pair, Zip, SyncLatest, PairLatest, CombineLatest, CombineLatestValuesAreAllTrue, CombineLatestValuesAreAllFalse, GroupBy
Error/retry/recoveryReattempt, Retry, Recover, Rescue, Resume, Catch, OnErrorResumeAsFailure
Time/schedulingShift, Delay, Expire, Timeout, Throttle, ObserveOn, Yield
Lifetime/multicastMulticast, Publish, StatelessPublish, ReplayLatestPublish, StatelessReplayLatestPublish, RefCount, OnDispose, TakeUntil, TakeUntilOptions, CompletionSignalDelegate, Wrap
Sequence boundariesTake, Skip, TakeWhile, SkipWhile, Lead, Prepend, StartWith
Terminal helpersFirstAsync, FirstOrDefaultAsync, LastAsync, LastOrDefaultAsync, SingleAsync, SingleOrDefaultAsync, AnyAsync, AllAsync, ContainsAsync, CountAsync, LongCountAsync, ToListAsync, CollectListAsync, CollectArrayAsync, ToDictionaryAsync, ToAsyncEnumerable, WaitCompletionAsync, ForEachAsync, SubscribeAsync

Basic async sequence example:

usingReactiveUI.Primitives.Async;List<string>labels=awaitSignalAsync.Sequence(1,12).Keep(static value =>value%2==0).Map(static value =>$"even:{value}").ToListAsync();

Mutable async signal example:

usingReactiveUI.Primitives.Async;usingReactiveUI.Primitives.Async.Signals;ISignalAsync<int>requests=Signal.Create<int>();awaitusingIAsyncDisposablesubscription=awaitrequests.Values.Map(static value =>value*2).SubscribeAsync(value =>Console.WriteLine(value));awaitrequests.OnNextAsync(21,CancellationToken.None);awaitrequests.OnCompletedAsync(Result.Success);

Async context example:

usingReactiveUI.Primitives.Async;AsyncContextcontext=AsyncContext.From(TaskScheduler.Default);awaitusingIAsyncDisposablesubscription=awaitSignalAsync.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:

APIPurpose
Heartbeat<T> / IHeartbeat<T>Value plus heartbeat metadata from heartbeat operators.
Stale<T> / IStale<T>Value plus stale/fresh state from stale-detection operators.
ContinuationDisposable continuation helper for bridging synchronous waits.
Observables.Return<T>(value)Single-value observable factory.
ObserverExtensions.FastForEachPushes enumerable values into an observer with array/list fast paths.
ObservableSubscriptionExtensionsSynchronous test/utility helpers: SubscribeGetValue, SubscribeAndComplete, SubscribeGetError, WaitForValue, WaitForCompletion, WaitForError.

Extension operators are grouped below by feature area:

CategoryAPIs
Filtering/projectionWhereIsNotNull, SkipWhileNull, Not, WhereTrue, WhereFalse, WhereSelect, SelectConstant, TrySelect, SelectManyThen, Pairwise, Partition, Filter, ForEach, Shuffle, LatestOrDefault, GetMin, GetMax, CombineLatestValuesAreAllTrue, CombineLatestValuesAreAllFalse
Error/retryCatchIgnore, CatchAndReturn, CatchReturn, CatchReturnUnit, LogErrors, OnErrorRetry, RetryWithBackoff, RetryWithDelay, RetryForeverWithDelay, RetryWithFixedDelay
Time/schedulingSyncTimer, ObserveOnIf, ScheduleSafe, Schedule, SampleLatest, DetectStale, Conflate, Heartbeat, ThrottleFirst, ThrottleUntilTrue, ThrottleOnScheduler, ThrottleDistinct, DebounceImmediate, DebounceUntil, WaitUntil
Buffer/collectionBufferUntil, BufferUntilIdle, BufferUntilInactive, FromArray, RunAll, FirstMatchFromCandidates
Async/sync interactionSynchronizeSynchronous, SubscribeSynchronous, SynchronizeAsync, SubscribeAsync, SelectAsync, SelectAsyncSequential, SelectLatestAsync, SelectAsyncConcurrent, DropIfBusy, WithLimitedConcurrency
State/property/lifetimeAsSignal, ToReadOnlyBehavior, ReplayLastOnSubscribe, SwitchIfEmpty, TakeUntil, Start, Using, While, ScanWithInitial, ToHotTask, ToHotValueTask, ToPropertyObservable, OnNext(params), DoOnSubscribe, DoOnDispose

Filtering and projection example:

usingReactiveUI.Primitives;usingReactiveUI.Primitives.Extensions;usingReactiveUI.Primitives.Signals;IObservable<string>labels=Signal.Sequence(1,10).WhereSelect(static value =>value%2==0,static value =>$"even:{value}");usingIDisposablesubscription=labels.Subscribe(Console.WriteLine);

Scheduling example:

usingReactiveUI.Primitives.Concurrency;usingReactiveUI.Primitives.Extensions;ISequencersequencer=ThreadPoolSequencer.Instance;usingIDisposablework="ready".Schedule(TimeSpan.FromMilliseconds(50),sequencer).Subscribe(Console.WriteLine);

Async selector example over a BCL observable:

usingReactiveUI.Primitives;usingReactiveUI.Primitives.Extensions;usingReactiveUI.Primitives.Signals;IObservable<string>names=Signal.Sequence(1,3).SelectAsyncSequential(staticasync value =>{awaitTask.Yield();return$"item:{value}";});usingIDisposablesubscription=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 typeReactiveUI.Primitives equivalentNotes
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 holderStateSignal<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) pipelineDelayableNotificationSignal<T>Passes notifications through immediately while not delayed, buffers them while delayed, and emits a de-duplicated batch on Flush.

State example:

usingReactiveUI.Primitives;usingReactiveUI.Primitives.Signals;vartemperature=newStateSignal<double>(21.5);ReadOnlyState<string>status=temperature.ToReadOnlyState(value =>value>=25.0?"warm":"normal");usingvarstateSubscription=status.Changed.Subscribe(Console.WriteLine);temperature.Value=26.2;temperature.Refresh();

Replay example:

usingReactiveUI.Primitives;usingReactiveUI.Primitives.Signals;varhistory=newReplaySignal<string>(bufferSize:2);history.OnNext("A");history.OnNext("B");history.OnNext("C");usingvarsubscription=history.Subscribe(Console.WriteLine);// replays B, C

Delayable example:

usingReactiveUI.Primitives.Signals;vardelayed=true;varnotifications=Signal.Delayable<string>(()=>delayed, items =>items.Distinct());usingvarsubscription=notifications.Subscribe(Console.WriteLine);notifications.OnNext("A");notifications.OnNext("A");// buffered while delayeddelayed=false;notifications.Flush();// emits the de-duplicated batch: A

Error and completion example:

usingReactiveUI.Primitives;usingReactiveUI.Primitives.Signals;IObservable<int>failed=Signal.Fail<int>(newInvalidOperationException("not available"));usingvarsubscription=failed.Subscribe(
value =>Console.WriteLine(value),
error =>Console.WriteLine($"failed: {error.Message}"),()=>Console.WriteLine("completed"));

Sequencers

A sequencer decides when and on which thread scheduled work runs. Rx calls this a scheduler. Sequencers live in ReactiveUI.Primitives.Concurrency and implement ISequencer. The core ReactiveUI.Primitives package does not reference WPF, Windows Forms, WinUI, Blazor, Avalonia, or MAUI. The optional integration packages supply the UI-thread sequencers.

SequencerPurpose
Sequencer.Immediate / ImmediateSequencer.InstanceExecute work immediately.
Sequencer.CurrentThread / CurrentThreadSequencer.InstanceQueue recursive/current-thread work deterministically.
ThreadPoolSequencer.InstanceSchedule work through the thread pool.
TaskPoolSequencer.InstanceSchedule work through tasks.
SynchronizationContextSequencerSchedule through a SynchronizationContext.
DispatcherSequencerSchedule onto a WPF dispatcher from ReactiveUI.Primitives.Wpf.
ControlSequencerSchedule onto a Windows Forms control from ReactiveUI.Primitives.WinForms.
DispatcherQueueSequencerSchedule onto a WinUI dispatcher queue from ReactiveUI.Primitives.WinUI.
BlazorRendererSequencerSchedule component work through Blazor's renderer from ReactiveUI.Primitives.Blazor.
AvaloniaSchedulerSchedule onto an Avalonia dispatcher from ReactiveUI.Primitives.Avalonia.
MauiDispatcherSequencerSchedule onto an MAUI dispatcher from ReactiveUI.Primitives.Maui.
VirtualClockVirtual-time scheduling for deterministic tests.

WPF, Windows Forms, WinUI, Blazor, and MAUI sequencers derive from DispatchSequencerBase. That shared base batches ready work into a single posted dispatcher drain, preserves FIFO order, skips cancelled work lazily, and routes delayed UI work through the shared ThreadPoolSequencer timing queue before marshaling back to the UI thread. Platform packages only provide the final dispatcher-specific post primitive. AvaloniaScheduler provides the same coalesced dispatcher drain behavior and uses dispatcher-bound timers for delayed work, so both posted and delayed callbacks stay associated with the selected Avalonia dispatcher and priority.

AvaloniaScheduler.Instance uses Dispatcher.UIThread at DispatcherPriority.Background. To bind scheduling to a specific dispatcher or priority, construct new AvaloniaScheduler(dispatcher) or new AvaloniaScheduler(dispatcher, priority). The lean type is ReactiveUI.Primitives.Concurrency.AvaloniaScheduler; the System.Reactive-compatible type is ReactiveUI.Primitives.Reactive.Concurrency.AvaloniaScheduler from ReactiveUI.Primitives.Avalonia.Reactive.

Scheduling APIs include absolute, relative, recursive, and action-based overloads:

usingReactiveUI.Primitives.Concurrency;IDisposablescheduled=ThreadPoolSequencer.Instance.Schedule(TimeSpan.FromMilliseconds(100),()=>Console.WriteLine("scheduled work"));scheduled.Dispose();

For hot convenience-call paths, prefer the stateful overload with a static callback to avoid closure capture:

sequencer.Schedule(observer,static target =>target.OnCompleted());

Use virtual clocks for deterministic time-sensitive tests rather than sleeping a real thread.

Threading, disposal, and error semantics

ReactiveUI.Primitives follows the BCL observer contract and keeps ownership explicit:

  • OnNext is delivered synchronously on the thread that invokes it unless an operator or sequencer explicitly schedules work elsewhere.
  • Time-based factories and operators use ISequencer overloads where deterministic or UI-thread dispatch matters. Use VirtualClock for tests; avoid sleeping real threads.
  • A subscription is an IDisposable. Disposing a subscription removes that observer and prevents later notifications to that subscription. Disposing a composite (MultipleDisposable, Pocket, Slot, etc.) cascades to contained disposables according to the container contract.
  • Terminal notifications are single-assignment: OnCompleted and OnError end a signal, and later values are ignored by terminated sources.
  • OnError(Exception) requires a non-null exception and propagates the terminal error to current subscribers. Operators such as Recover, Rescue, Resume, Reattempt, and Signal.Recover are the explicit recovery points.
  • Observer callback exceptions are guarded by the operator/source that owns the callback. Prefer CreateSafe for custom sources unless you are deliberately implementing lower-level observer semantics.
  • The default lean packages have no runtime dependency on System.Reactive, R3, or R3Async. The .Reactive variants intentionally reference System.Reactive, and bridge generators only emit R3/R3Async boundary adapters when a consuming project already references those packages.

Observable-event source generation

ReactiveUI.Primitives.ObservableEvents is a standalone incremental source-generator package. It has no runtime dependency on a particular observable implementation; it inspects the consuming compilation and emits adapters for the first compatible provider it finds:

  • ReactiveUI.Primitives.Signals.Signal and RxVoid for lean Primitives projects.
  • ReactiveUI.Primitives.Reactive.Signals.Signal and System.Reactive.Unit for .Reactive projects.
  • System.Reactive.Linq.Observable and System.Reactive.Unit for standalone System.Reactive projects that do not reference any ReactiveUI.Primitives package.

Install it alongside the observable provider already used by the application:

dotnet add package ReactiveUI.Primitives.ObservableEvents

Instance generation is activated by calling Events() once for an event host. The generator replaces the marker result with a strongly typed wrapper whose properties subscribe and unsubscribe from the corresponding public events:

usingReactiveUI.Primitives.ObservableEvents;IObservable<EventArgs>changes=viewModel.Events().Changed;

Request public static events with an assembly attribute. Static observable properties are generated on RxEvents in the event host's namespace. Their names use length-prefixed host and event identifiers so type, nesting, and event-name boundaries cannot collide:

[assembly:ReactiveUI.Primitives.ObservableEvents.GenerateStaticEventObservables(typeof(AppEvents))]IObservable<string>messages=RxEvents.T9AppEvents7Message;

Delegate payloads use RxVoid or Unit for no parameters, the sole parameter for one parameter, the event-args parameter for conventional (object sender, TEventArgs args) events, and a named tuple for other multi-parameter delegates. Delegates returning void, Task, or ValueTask are supported. Unsupported signatures produce RXOE003; missing providers and empty requests produce RXOE001 and RXOE002 respectively.

Source-generator bridge behavior

A source generator is a compiler component that writes extra C# code into your project at build time. R3 and R3Async bridge generation is opt-in through the standalone analyzer package:

dotnet add package ReactiveUI.Primitives.R3Bridge.Generator

The package ships one analyzer assembly:

  • ReactiveUI.Primitives.R3Bridge.Generator.dll

The generator is no longer embedded in ReactiveUI.Primitives or ReactiveUI.Primitives.Async, and those runtime packages do not depend on it. Add the generator package only to projects that need generated R3 or R3Async bridge methods.

That assembly currently contains two conditional generators:

  • R3BridgeGenerator for R3 Observable<T> boundaries and R3-to-Primitives.Async adapters.
  • R3AsyncBridgeGenerator for R3Async AsyncObservable<T> boundaries.

The generator stamps the consuming assembly with an assembly metadata attribute:

[assembly:System.Reflection.AssemblyMetadata("ReactiveUI.Primitives.R3Bridge.Generator","0.1.0")]

It does not generate a custom marker attribute type. This avoids duplicate generated type identities across project-reference and InternalsVisibleTo builds, including the CS0436 warning path seen when two compilations both generate the same internal marker type.

Bridge extension methods are emitted only when the consumer project already references the relevant external library symbols:

  • R3 bridge checks for R3.Observable<T>, R3.Observer<T>, and R3.Result.
  • R3-to-Primitives.Async bridge checks for the same R3 symbols plus ReactiveUI.Primitives.Async.IObservableAsync<T>.
  • R3Async bridge checks for R3Async.AsyncObservable<T>, R3Async.AsyncObserver<T>, R3Async.Result, and ReactiveUI.Primitives.Async.IObservableAsync<T>.

Generated bridge namespace:

  • ReactiveUI.Primitives.R3Bridge

Generated R3 bridge methods:

  • AsPrimitivesSignal<T>(this R3.Observable<T> source)
  • AsR3Observable<T>(this System.IObservable<T> source)
  • AsPrimitivesAsyncObservable<T>(this R3.Observable<T> source) when ReactiveUI.Primitives.Async is referenced
  • AsR3Observable<T>(this ReactiveUI.Primitives.Async.IObservableAsync<T> source) when ReactiveUI.Primitives.Async is referenced

Generated R3Async bridge methods:

  • AsPrimitivesAsyncObservable<T>(this R3Async.AsyncObservable<T> source) when R3Async and ReactiveUI.Primitives.Async are referenced
  • AsR3AsyncObservable<T>(this ReactiveUI.Primitives.Async.IObservableAsync<T> source) when R3Async and ReactiveUI.Primitives.Async are referenced

R3 bridge example, when the consuming project references R3 and the generator package:

dotnet add package ReactiveUI.Primitives
dotnet add package ReactiveUI.Primitives.R3Bridge.Generator
dotnet add package R3
usingReactiveUI.Primitives;usingReactiveUI.Primitives.R3Bridge;usingReactiveUI.Primitives.Signals;// R3.Observable<int> r3Source = ...;IObservable<int>primitivesSource=r3Source.AsPrimitivesSignal();R3.Observable<int>r3Again=Signal.Sequence(1,3).AsR3Observable();

R3 async bridge example, when the consuming project references R3, ReactiveUI.Primitives.Async, and the generator package:

dotnet add package ReactiveUI.Primitives.Async
dotnet add package ReactiveUI.Primitives.R3Bridge.Generator
dotnet add package R3
usingReactiveUI.Primitives.Async;usingReactiveUI.Primitives.R3Bridge;// R3.Observable<int> r3Source = ...;IObservableAsync<int>primitivesAsync=r3Source.AsPrimitivesAsyncObservable();R3.Observable<int>r3Again=primitivesAsync.AsR3Observable();

R3Async bridge example, when the consuming project references R3Async, ReactiveUI.Primitives.Async, and the generator package:

dotnet add package ReactiveUI.Primitives.Async
dotnet add package ReactiveUI.Primitives.R3Bridge.Generator
dotnet add package R3Async
usingReactiveUI.Primitives.Async;usingReactiveUI.Primitives.R3Bridge;// R3Async.AsyncObservable<int> r3AsyncSource = ...;IObservableAsync<int>primitivesAsync=r3AsyncSource.AsPrimitivesAsyncObservable();R3Async.AsyncObservable<int>r3AsyncAgain=primitivesAsync.AsR3AsyncObservable();

The R3 snippets are intentionally shown as migration shapes because they require the consuming application to reference R3 or R3Async and opt into ReactiveUI.Primitives.R3Bridge.Generator. ReactiveUI.Primitives itself remains free of R3 and R3Async runtime dependencies. System.Reactive interop lives in the .Reactive package variants, which recompile the same Primitives APIs against System.Reactive Unit and IScheduler.

System.Reactive to ReactiveUI.Primitives migration guide

ReactiveUI.Primitives is not a byte-for-byte clone of System.Reactive. It keeps the standard IObservable<T> contracts but favors a smaller runtime, explicit state types, and Primitives naming. Migrate one vertical slice at a time: factories first, then subject/state types, then operators and schedulers.

When a project must keep System.Reactive Unit or IScheduler in its public surface, use ReactiveUI.Primitives.Reactive or ReactiveUI.Primitives.Async.Reactive. The former ReactiveUI.Primitives.Extensions.Reactive helpers are included in ReactiveUI.Primitives.Reactive. When the goal is to migrate away from those public System.Reactive types, use the lean packages and the mappings below.

Migration track: existing xyz project

Use this track when the project should eventually stop exposing System.Reactive types and use the lean ReactiveUI.Primitives package family.

  1. Inventory references and public API. Mark each project that exposes System.Reactive.Unit, IScheduler, IObservable<T> extension methods, UI schedulers, Subject<T> types, or ReactiveUI.Extensions helpers.
  2. Add the lean packages needed by the existing project:
dotnet add xyz/xyz.csproj package ReactiveUI.Primitives
dotnet add xyz/xyz.csproj package ReactiveUI.Primitives.Async
  1. Add only the matching UI integration package when the project owns UI-thread dispatch:
dotnet add xyz/xyz.csproj package ReactiveUI.Primitives.Wpf
dotnet add xyz/xyz.csproj package ReactiveUI.Primitives.WinForms
dotnet add xyz/xyz.csproj package ReactiveUI.Primitives.WinUI
dotnet add xyz/xyz.csproj package ReactiveUI.Primitives.Blazor
dotnet add xyz/xyz.csproj package ReactiveUI.Primitives.Avalonia
dotnet add xyz/xyz.csproj package ReactiveUI.Primitives.Maui
  1. Convert boundary types deliberately: System.Reactive.Unit to RxVoid, IScheduler to ISequencer, Rx subjects to Signal<T>, StateSignal<T>, ReplaySignal<T>, or FinalSignal<T>, and composite disposable types to MultipleDisposable, Pocket, Slot, or AssignmentSlot.
  2. Keep code compiling during the first pass by using the Rx-name compatibility layer (Select, Where, Aggregate, Scan, Merge, Concat, CombineLatest, SelectMany, and related aliases). Then move hot paths to Primitives names (Map, Keep, Reduce, Fold, Blend, Chain, SyncLatest, FlatMap) where that makes the code clearer.
  3. Replace scheduler construction and tests: use Sequencer.Immediate, Sequencer.CurrentThread, ThreadPoolSequencer.Instance, TaskPoolSequencer.Instance, UI sequencers, and VirtualClock.
  4. Remove System.Reactive and ReactiveUI.Extensions package references only after the project builds without System.Reactive.Linq, System.Reactive.Subjects, System.Reactive.Disposables, or System.Reactive.Concurrency imports.
  5. Run tests and package/API approval checks. For time-sensitive tests, use virtual time rather than real sleeps.

Migration track: new xyz.Reactive project

Use this track when an existing Rx-based source base must remain source-compatible for consumers while the repository moves implementation work onto ReactiveUI.Primitives. The pattern is to keep or create a xyz lean package and add a new xyz.Reactive package that references the .Reactive Primitives range.

  1. Move shared implementation files into a shared source folder that can be linked by both projects.
  2. In shared source, use the neutral identifiers RxVoid and ISequencer. In the lean project they bind to ReactiveUI.Primitives types; in the .Reactive project they bind to System.Reactive.Unit and System.Reactive.Concurrency.IScheduler.
  3. Gate namespaces when the public namespace must differ:
#if REACTIVE_SHIMnamespacexyz.Reactive;
#else
namespacexyz;
#endif
  1. Reference the .Reactive packages from xyz.Reactive:
dotnet add xyz.Reactive/xyz.Reactive.csproj package ReactiveUI.Primitives.Reactive
dotnet add xyz.Reactive/xyz.Reactive.csproj package ReactiveUI.Primitives.Async.Reactive
  1. Add the matching reactive UI package only when the project exposes UI scheduling:
dotnet add xyz.Reactive/xyz.Reactive.csproj package ReactiveUI.Primitives.Wpf.Reactive
dotnet add xyz.Reactive/xyz.Reactive.csproj package ReactiveUI.Primitives.WinForms.Reactive
dotnet add xyz.Reactive/xyz.Reactive.csproj package ReactiveUI.Primitives.WinUI.Reactive
dotnet add xyz.Reactive/xyz.Reactive.csproj package ReactiveUI.Primitives.Blazor.Reactive
dotnet add xyz.Reactive/xyz.Reactive.csproj package ReactiveUI.Primitives.Avalonia.Reactive
dotnet add xyz.Reactive/xyz.Reactive.csproj package ReactiveUI.Primitives.Maui.Reactive
  1. Configure the reactive project to define REACTIVE_SHIM and alias the System.Reactive types if your repository does not already centralize this in Directory.Build.props:
<PropertyGroup>
<DefineConstants>$(DefineConstants);REACTIVE_SHIM</DefineConstants>
</PropertyGroup>
<ItemGroup>
<UsingInclude="System.Reactive.Unit"Alias="RxVoid" />
<UsingInclude="System.Reactive.Concurrency.IScheduler"Alias="ISequencer" />
</ItemGroup>
  1. Prefer zero source changes in the first xyz.Reactive pass: keep Rx names such as Select, Where, SelectMany, CombineLatest, Merge, Concat, Throttle, and WithLatestFrom where compatibility matters. The .Reactive Primitives packages supply those names over the Primitives implementation.
  2. Build both packages side by side. xyz should have no System.Reactive runtime dependency; xyz.Reactive should keep System.Reactive-facing APIs for existing consumers.

Factory mapping

System.ReactiveReactiveUI.PrimitivesNotes
Observable.Return(value)Signal.Emit(value)Emits one value and completes.
Observable.Empty<T>()Signal.None<T>()Completes immediately.
Observable.Never<T>()Signal.Silent<T>() or Signal.Silent<T>(witness)Non-terminating signal; witness overload helps type inference.
Observable.Throw<T>(ex)Signal.Fail<T>(ex)Emits terminal error.
Observable.Range(start, count)Signal.Sequence(start, count)Optional scheduler overload exists.
Observable.Repeat(value)Signal.Loop(value)Indefinite repeat.
Observable.Repeat(value, count)Signal.Loop(value, count)Fixed repeat.
Observable.Defer(factory)Signal.Lazy(factory)Create source per subscription.
Observable.FromAsync(...)Signal.FromAsync(...)Invoke a task factory per subscription.
Observable.Create<T>(...)Signal.Create<T>(...) or Signal.CreateSafe<T>(...)Prefer CreateSafe for general custom sources.
Observable.Using(...)Signal.Use(...)Resource scoped to subscription.
Observable.Timer(dueTime)Signal.After(dueTime)Emits long tick 0.
Observable.Timer(dueTime, period)Signal.After(dueTime, period)Periodic long ticks.
Observable.Interval(period)Signal.Pulse(period) or Signal.Every(period)Repeating ticks.
ToObservable() from enumerableSignal.FromEnumerable(values), values.ToSignal(), or values.ToObservable()Cancellation-token overloads are available.
task conversionSignal.FromTask(task)Function-based task signals also exist.

Subject/state mapping

System.ReactiveReactiveUI.PrimitivesMigration detail
new Subject<T>()new Signal<T>()Use OnNext, OnError, OnCompleted, and Subscribe.
new BehaviorSubject<T>(initial)new StateSignal<T>(initial)Keeps Value getter/setter and emits changes through Changed.
mutable reactive propertynew StateSignal<T>(initial)Set Value to emit. Use Changed for observable state stream.
new ReplaySubject<T>()new ReplaySignal<T>()Unbounded replay.
new ReplaySubject<T>(bufferSize)new ReplaySignal<T>(bufferSize)Size-limited replay.
new ReplaySubject<T>(window)new ReplaySignal<T>(window)Time-window replay.
new AsyncSubject<T>()new FinalSignal<T>()Awaitable final-value signal shape.

Operator mapping

System.ReactiveReactiveUI.PrimitivesNotes
SelectMapPrefer Map for distinct Primitives style.
WhereKeepPredicate filtering.
SelectManyFlatMap, Bind, or Rx-name SelectManyObservable overloads preserve concurrent merge semantics; enumerable overloads flatten inline.
AggregateReduceEmits final accumulated value on completion.
ScanFoldEmits every accumulated value.
DoTapSide effect while preserving values.
Take / SkipTake / SkipCount-based overloads.
TakeWhile / SkipWhileTakeWhile / SkipWhilePredicate-based.
DistinctDistinctFull seen-set distinct.
DistinctUntilChangedUniqueAdjacent dedupe.
OfType / CastKeepType / CastToObject-source projections.
MaterializeSparkConverts notifications into Spark<T>.
DematerializeUnsparkConverts Spark<T> values back into notifications.
Where + SelectChooseSingle fused sink; chooser returns (HasValue, Value) so a non-nullable value type can be skipped.
MergeBlend or Signal.BlendWorks over source-of-sources and params factories.
Merge + DistinctUntilChangedBlendUniqueSingle fused merge + adjacent dedupe over a params source set.
ConcatChain or Signal.ChainSequential composition.
AmbRaceFirst source to produce a value or terminal signal wins.
SwitchSwitchToLatest inner observable wins.
Select + SwitchSwitchSelectFilters null source values, projects each to an inner observable, and mirrors only the latest.
ZipPair or Signal.PairPair values by index.
CombineLatestSyncLatest, Rx-name CombineLatest, or Signal.SyncLatestLatest values after all sources have emitted; overloads support up to 16 total sources.
WithLatestFromLatchLeft emission paired with latest right value.
ForkJoinForkJoinLast values after completion.
ThrottleCalm / StabilizeQuiet-period emission.
SampleProbePeriodic latest-value sampling.
DelayShiftDelay emitted values.
DelaySubscriptionDelayStartDelay source subscription.
TimeoutExpireError on missing value before due time.
Buffer(count)Buffer(count)Fixed-size buffers.
SubscribeOnSubscribeOnSchedule source subscription.
ToList / ToArrayToList / ToArray or CollectList / CollectArraySignal results.
FirstAsync / LastAsyncFirstAsync / LastAsyncTask result.
CountAsync / AnyAsyncCountAsync / AnyAsyncTask-shaped terminal helpers, including cancellation overloads.

Disposable mapping

System.ReactiveReactiveUI.Primitives
Disposable.CreateDisposable.Create
Disposable.EmptyDisposable.Empty
BooleanDisposableBooleanDisposable
CancellationDisposableCancellationDisposable
CompositeDisposableMultipleDisposable or Pocket
SerialDisposableSingleReplaceableDisposable or Slot
SingleAssignmentDisposableSingleDisposable or AssignmentSlot
IDisposable.Dispose()unchanged

Sequencer mapping

System.Reactive scheduler conceptReactiveUI.Primitives scheduler
ImmediateScheduler.InstanceSequencer.Immediate or ImmediateSequencer.Instance
CurrentThreadScheduler.InstanceSequencer.CurrentThread or CurrentThreadSequencer.Instance
ThreadPoolScheduler.InstanceThreadPoolSequencer.Instance
TaskPoolScheduler.DefaultTaskPoolSequencer.Instance
synchronization-context schedulingSynchronizationContextSequencer
WPF dispatcher schedulingDispatcherSequencer from ReactiveUI.Primitives.Wpf
Windows Forms control schedulingControlSequencer from ReactiveUI.Primitives.WinForms
WinUI dispatcher queue schedulingDispatcherQueueSequencer from ReactiveUI.Primitives.WinUI
Blazor renderer schedulingBlazorRendererSequencer from ReactiveUI.Primitives.Blazor
Avalonia dispatcher schedulingAvaloniaScheduler from ReactiveUI.Primitives.Avalonia
MAUI dispatcher schedulingMauiDispatcherSequencer from ReactiveUI.Primitives.Maui
TestScheduler / virtual timeVirtualClock

Testing migration

System.Reactive test code commonly uses TestScheduler and marble helpers. ReactiveUI.Primitives currently exposes virtual-time primitives rather than cloning the full Rx testing API. Prefer repository-native tests that:

  • Use VirtualClock for deterministic scheduling.
  • Assert values collected through Subscribe delegates.
  • Dispose subscriptions explicitly.
  • Use CollectArrayAsync, CollectListAsync, or FirstAsync when a task-shaped assertion is clearer.

R3Async to ReactiveUI.Primitives.Async migration guide

ReactiveUI.Primitives.Async is the native async-observable package. Use it when observer work is asynchronous, subscription/disposal needs ValueTask, or cancellation must flow through each notification. It differs from R3Async by using ReactiveUI.Primitives.Result for completion.

There is no generated System.Reactive.Async bridge in the current package set. Use ReactiveUI.Primitives.Async.Reactive when you need async Primitives APIs compiled against System.Reactive Unit and IScheduler, and keep any other async-observable adapter code at package or API edges.

R3AsyncReactiveUI.Primitives.AsyncMigration detail
R3Async.AsyncObservable<T>IObservableAsync<T> / SignalAsync<T>Use generated AsPrimitivesAsyncObservable() at external boundaries.
R3Async.AsyncObserver<T>IObserverAsync<T> / WitnessAsync<T>Use WitnessAsync<T> for custom observers that need disposal, cancellation, and concurrency checks.
R3Async.ResultReactiveUI.Primitives.ResultBoth carry success/failure; bridge adapters convert between them.
OnErrorResumeAsyncOnErrorResumeAsyncSame error-resume concept; Primitives passes the active CancellationToken.
OnCompletedAsync(R3Async.Result)OnCompletedAsync(ReactiveUI.Primitives.Result)Completion remains result-based.

R3Async bridge example:

usingReactiveUI.Primitives.Async;usingReactiveUI.Primitives.R3Bridge;// R3Async.AsyncObservable<int> r3AsyncSource = ...;IObservableAsync<int>native=r3AsyncSource.AsPrimitivesAsyncObservable();R3Async.AsyncObservable<int>external=native.AsR3AsyncObservable();

Keep R3Async bridge conversions at package or API edges. Inside the application or library, prefer SignalAsync factories, IObservableAsync<T> operators, and IObserverAsync<T> observers directly.

R3 migration notes

R3 uses its own Observable<T> type and observer model. ReactiveUI.Primitives stays on the BCL IObservable<T> shape for runtime interoperability.

R3 conceptReactiveUI.Primitives equivalent
R3.Observable<T>BCL IObservable<T> from ReactiveUI.Primitives factories/operators.
R3 subjectSignal<T> / StateSignal<T> / ReplaySignal<T> depending on state/replay needs.
R3 Select / WhereMap / Keep.
R3 time operatorsSignal.After, Signal.Pulse, Calm, Probe, Shift, scheduler overloads.
R3 bridgeGenerated AsPrimitivesSignal / AsR3Observable; async bridge methods add AsPrimitivesAsyncObservable / AsR3Observable when R3 and ReactiveUI.Primitives.Async are referenced by the consumer.

Use the generated bridge only at boundaries. Prefer native ReactiveUI.Primitives operators inside new code.

ReactiveUI.Extensions migration notes

ReactiveUI.Primitives is the migration target for the non-async helpers that previously lived in ReactiveUI.Extensions. The helpers remain in the ReactiveUI.Primitives.Extensions namespace and intentionally keep their names where those names already describe the behavior and do not collide with the core Primitives vocabulary. Scheduling overloads use ISequencer instead of System.Reactive schedulers.

ReactiveUI.Extensions usageReactiveUI.Primitives usage
WhereIsNotNull, SkipWhileNull, WhereTrue, WhereFalse, NotSame names over BCL IObservable<T>.
WhereSelect, SelectConstant, TrySelect, SelectManyThen, Pairwise, PartitionSame helper names; implemented with direct observers and fused operator shapes where useful.
SyncTimer, ObserveOnIf, Schedule, ScheduleSafe, throttle/debounce helpersSame helper names; use ISequencer overloads for scheduling.
CatchIgnore, CatchAndReturn, CatchReturn, retry helpersSame helper names; no System.Reactive dependency.
SubscribeAsync, SelectAsync, SelectLatestAsync, DropIfBusySame BCL observable helper names for Task/ValueTask interop.
RunAll, BufferUntil, FirstMatchFromCandidates, ToHotTask, ToHotValueTaskSame helper names; backed by ReactiveUI.Primitives runtime utilities.

For async-native streams, prefer ReactiveUI.Primitives.Async and its IObservableAsync<T> operators. For existing BCL observable helpers, migrate to ReactiveUI.Primitives; existing ReactiveUI.Primitives.Extensions imports remain valid.

Benchmarks and performance posture

Benchmarks live in src/benchmarks/ReactiveUI.Primitives.Benchmarks. The benchmark project may reference System.Reactive, System.Reactive.Async 6.0.0-alpha.18, R3, and ReactiveUI.Extensions to compare throughput and allocation behavior; the production packages must not.

The latest complete BenchmarkDotNet run finished on 2026-06-08 at 19:39:12 Europe/London with .NET SDK 11.0.100-preview.4.26230.115 and .NET runtime 10.0.8 on Windows 11. It executed 617 benchmarks with no failed benchmark process in 01:16:58:

dotnet run --project src/benchmarks/ReactiveUI.Primitives.Benchmarks/ReactiveUI.Primitives.Benchmarks.csproj --framework net10.0--configuration Release --no-restore ----filter "*"--join--launchCount 1--warmupCount 1--iterationCount 3

Latest artifact paths:

  • BenchmarkDotNet.Artifacts/BenchmarkRun-20260608-182233.log
  • BenchmarkDotNet.Artifacts/run-full-benchmarks-20260608-182212.outer.log
  • BenchmarkDotNet.Artifacts/results/BenchmarkRun-joined-2026-06-08-19-39-12-report-github.md
  • BenchmarkDotNet.Artifacts/results/BenchmarkRun-joined-2026-06-08-19-39-12-report.html
  • BenchmarkDotNet.Artifacts/results/BenchmarkRun-joined-2026-06-08-19-39-12-report.csv

The joined run exports 617 raw BenchmarkDotNet rows: 238 ReactiveUI.Primitives or ReactiveUI.Primitives.Async cases, 157 System.Reactive cases, 132 R3 cases, and 90 ReactiveUI.Extensions cases. The current table includes the async replay-latest subscription scenario and subject multicast fan-out scenarios that were not present in the previous 610-row run.

The table below groups ReactiveUI.Primitives and ReactiveUI.Primitives.Async into the ReactiveUI.Primitives column, aligns each primitive benchmark with any System.Reactive, R3, or ReactiveUI.Extensions alternative from the same benchmark scenario, and uses NA where no alternative exists. It contains 238 alphabetically ordered scenario rows. Cells use Mean / Allocated, and long scenario parameter values from BenchmarkDotNet are restored to their full names.

External-baseline posture from this run: ReactiveUI.Primitives is faster than System.Reactive in 151/157 measured comparisons, faster than R3 in 131/132 measured comparisons, and faster than ReactiveUI.Extensions 4.0.0 in 58/90 measured comparisons. Rows that are not faster remain listed for direct comparison.

ScenarioReactiveUI.PrimitivesSystem.ReactiveR3ReactiveUI.Extensions
After161.4435 ns / 584 B934.2132 ns / 25056 B273.3047 ns / 552 BNA
AggregateAnyCount (Operator core GC profile)197.3247 ns / 824 B5,651.5205 ns / 5856 B670.9280 ns / 1280 BNA
AggregateAnyCount (Operator map keep)209.1845 ns / 824 B5,801.8443 ns / 5856 B612.6859 ns / 1280 BNA
All19.2005 ns / 96 B2,664.6337 ns / 2520 B89.5099 ns / 192 BNA
AllContains29.5381 ns / 192 B5,262.9316 ns / 5048 B213.4439 ns / 392 BNA
AllRange20.3476 ns / 96 B2,605.7495 ns / 2520 B90.4437 ns / 192 BNA
AsSignal41.0844 ns / 112 B2,688.2408 ns / 2536 B194.0944 ns / 160 B2,646.8338 ns / 2488 B
AutoConnect141.0078 ns / 408 B2,760.6903 ns / 2736 BNANA
AutoConnectSubscribe143.5240 ns / 408 B2,837.9738 ns / 2736 BNANA
BehaviorEmit15,580.1615 ns / 160 BNANANA
BufferRange70.1760 ns / 304 B1,463.5930 ns / 1656 B118.6141 ns / 360 BNA
BufferUntil48.1740 ns / 264 BNANA45.4763 ns / 264 B
BufferUntilIdle2,070.5617 ns / 6504 BNANA28,683.3995 ns / 21207 B
BufferUntilInactive2,101.6589 ns / 6504 BNANA28,331.4789 ns / 21206 B
CastTo95.6295 ns / 200 B1,507.3750 ns / 1568 B168.6829 ns / 216 BNA
CatchAndReturn20.4972 ns / 128 B195.6230 ns / 368 B129.3629 ns / 264 B68.4632 ns / 184 B
CatchIgnore19.7715 ns / 128 B177.7583 ns / 344 B123.2607 ns / 240 B64.5144 ns / 184 B
CatchReturn14.7025 ns / 128 B185.6928 ns / 368 B127.5877 ns / 264 B63.3613 ns / 184 B
CatchReturnUnit10.5709 ns / 88 BNANA61.0721 ns / 144 B
CollectArray (Terminal collection GC profile)39.4273 ns / 360 B2,932.9110 ns / 3144 B184.3065 ns / 784 BNA
CollectArray (Terminal collection)37.4360 ns / 360 B2,742.8235 ns / 3144 B180.8473 ns / 784 BNA
CollectArrayAsync35.0986 ns / 384 B2,838.6592 ns / 3384 B169.3271 ns / 784 BNA
CollectList (Terminal collection GC profile)76.1907 ns / 392 B2,682.1894 ns / 2992 B177.1869 ns / 632 BNA
CollectList (Terminal collection)72.8729 ns / 392 B2,645.8995 ns / 2992 B167.3003 ns / 632 BNA
CollectListAsync47.9789 ns / 352 B1,498.7641 ns / 2056 B124.4369 ns / 480 BNA
CombineLatest41.1023 ns / 192 B3,327.7110 ns / 2824 B689.0555 ns / 344 BNA
CombineLatestRanges41.6113 ns / 192 B3,203.0270 ns / 2824 B676.1603 ns / 344 BNA
CombineLatestValuesAreAllFalse214.4874 ns / 936 B363.2088 ns / 648 BNA232.9241 ns / 1176 B
CombineLatestValuesAreAllTrue209.2835 ns / 936 B373.2756 ns / 648 BNA230.6682 ns / 1176 B
CommandExecuteAsync36.1038 ns / 152 B725.3990 ns / 1089 B115.4143 ns / 296 BNA
CommandResultSubscribeAsync63.7978 ns / 224 B41.2514 ns / 136 B70.1836 ns / 160 BNA
CompletedSpark0.0000 ns / 0 B0.0083 ns / 0 B0.0167 ns / 0 BNA
CompletedTaskBridge10.4882 ns / 88 B867.1664 ns / 793 B45.4824 ns / 88 BNA
Concat75.7136 ns / 256 B2,931.5501 ns / 2856 B260.7747 ns / 360 BNA
ConcatRanges76.5450 ns / 256 B2,961.9462 ns / 2856 B255.5487 ns / 360 BNA
Conflate4,146.7812 ns / 2312 BNANA35,228.6641 ns / 16970 B
Contains10.9311 ns / 96 B2,733.3856 ns / 2528 B99.1364 ns / 200 BNA
ContainsRange10.1873 ns / 96 B2,670.6758 ns / 2528 B94.0961 ns / 200 BNA
Continuation.Dispose25.3797 ns / 192 BNANA25.5549 ns / 192 B
Continuation.Lock1,260.4535 ns / 464 BNANA1,190.6156 ns / 464 B
Continuation.LockValueTask1,175.4602 ns / 464 BNANA1,208.8696 ns / 464 B
CountPredicate (Terminal collection GC profile)37.8831 ns / 96 B2,621.1035 ns / 2520 B98.5233 ns / 200 BNA
CountPredicate (Terminal collection)20.0000 ns / 96 B2,647.6879 ns / 2520 B99.6155 ns / 200 BNA
CreateSafeSubscribe38.6123 ns / 112 BNANANA
CreateSubscribe38.9402 ns / 112 B49.9716 ns / 168 B67.1235 ns / 152 BNA
CreateWithState61.2168 ns / 192 B87.3642 ns / 256 B120.9910 ns / 240 BNA
CurrentThreadSchedule8.3874 ns / 88 B18.1439 ns / 88 B32.2492 ns / 56 BNA
DebounceImmediate1,754.9197 ns / 4064 BNANA30,137.8438 ns / 18054 B
DebounceUntil1,221.7361 ns / 776 BNANA7,788.1093 ns / 6126 B
DefaultIfEmptyEmpty5.5498 ns / 64 B68.9009 ns / 144 B67.3876 ns / 136 BNA
DeferSubscribe82.5640 ns / 240 B1,447.0622 ns / 1512 B122.1637 ns / 152 BNA
DelayRange165.0811 ns / 536 B6,285.0703 ns / 39584 B2,091.7877 ns / 2200 BNA
DelayStartRange164.0063 ns / 536 B2,503.0134 ns / 26456 B338.4165 ns / 552 BNA
DematerializeRange71.9757 ns / 184 B1,473.7932 ns / 1528 B205.2736 ns / 208 BNA
DetectStale208.3042 ns / 600 BNANA938.7462 ns / 25128 B
DisposableCollectionDispose68.9755 ns / 424 B103.6309 ns / 512 B86.0407 ns / 480 BNA
DoOnDispose76.8536 ns / 232 BNANA80.8925 ns / 232 B
DoOnSubscribe76.4659 ns / 192 BNANA77.0226 ns / 192 B
DropIfBusy387.0132 ns / 240 BNANA378.0149 ns / 240 B
Emit10241,581.4852 ns / 192 B1,750.5569 ns / 136 B2,029.8888 ns / 160 BNA
Empty3.0465 ns / 40 B48.0018 ns / 96 B30.6985 ns / 56 BNA
EmptySubscribe2.9831 ns / 40 B52.8440 ns / 96 B34.6125 ns / 56 BNA
Every526.0310 ns / 1192 B2,858.5448 ns / 34001 B337.4532 ns / 552 BNA
FastForEach52.5630 ns / 40 BNANA52.4816 ns / 40 B
Filter128.6757 ns / 120 B787.8662 ns / 984 BNA123.8859 ns / 120 B
FirstAsync5.9440 ns / 56 B2,582.4415 ns / 2792 B77.0095 ns / 208 BNA
FirstMatchFromCandidates48.3142 ns / 216 BNANA40.4904 ns / 216 B
FirstOrDefaultAsync6.0260 ns / 56 B1,410.5826 ns / 1768 B66.3999 ns / 208 BNA
FlatMap737.5430 ns / 728 B3,836.4955 ns / 3872 B1,104.8553 ns / 1040 BNA
FlatMapRange723.2042 ns / 728 B3,745.7840 ns / 3872 B1,090.7939 ns / 1040 BNA
Fold (Operator stateful filter GC profile)1,963.0732 ns / 144 BNANANA
Fold (Operator stateful filter)97.7211 ns / 144 B2,642.7624 ns / 2520 BNANA
ForEach75.0722 ns / 160 B157.6810 ns / 200 BNA78.4165 ns / 160 B
ForkJoin25.3112 ns / 192 B3,744.3144 ns / 3136 B1,155.6442 ns / 504 BNA
ForkJoinRanges21.9770 ns / 192 B3,497.1976 ns / 3136 B968.2838 ns / 504 BNA
FromArray61.7537 ns / 72 B2,471.6468 ns / 2504 B79.5506 ns / 88 B60.1525 ns / 72 B
FromAsyncEnumerableSubscribeAsync1,126.3758 ns / 600 B1,623.9187 ns / 1838 B1,272.2635 ns / 1023 BNA
FromEnumerable53.6114 ns / 40 B2,548.2366 ns / 2504 B78.8076 ns / 88 BNA
FromEnumerableSubscribe54.1935 ns / 40 B2,552.4211 ns / 2504 B78.3198 ns / 88 BNA
FromEventPattern121.1161 ns / 624 B1,735.8404 ns / 2422 BNANA
GetMax114.3242 ns / 408 B182.2547 ns / 328 BNA216.9035 ns / 1152 B
GetMin112.0888 ns / 408 B183.0003 ns / 328 BNA218.7370 ns / 1152 B
Heartbeat291.0539 ns / 800 BNANA2,565.3634 ns / 26096 B
HistorySubscribe345.0974 ns / 352 B707.3779 ns / 696 B423.2712 ns / 688 BNA
IgnoreValuesRange28.7628 ns / 128 B1,424.1602 ns / 1504 B78.7061 ns / 160 BNA
Iterate11.8113 ns / 0 B2,363.1381 ns / 2768 BNANA
KeepNotNull106.0897 ns / 192 B1,546.9787 ns / 1624 B231.1263 ns / 312 BNA
KeepType103.0512 ns / 192 B1,515.3027 ns / 1568 B193.8181 ns / 216 BNA
KeepWith52.2025 ns / 136 B1,468.1596 ns / 1608 B129.0034 ns / 280 BNA
LastOrDefaultAsync12.7311 ns / 192 B1,421.3154 ns / 1872 B75.4668 ns / 208 BNA
LatestOrDefault54.2813 ns / 136 BNANA53.2314 ns / 136 B
LogErrors70.2575 ns / 224 BNANA68.4577 ns / 224 B
LongCountPredicate20.3864 ns / 104 B2,559.9402 ns / 2536 B109.9617 ns / 272 BNA
MapKeep133.9801 ns / 208 B2,770.7661 ns / 2584 B319.2388 ns / 272 BNA
MapWith46.4196 ns / 136 B1,461.4366 ns / 1608 B137.8286 ns / 248 BNA
MaterializeRange46.3840 ns / 120 B1,487.8726 ns / 1880 B100.6545 ns / 136 BNA
Merge78.0824 ns / 256 B4,071.0686 ns / 3952 B718.8251 ns / 352 BNA
MergeRanges76.2008 ns / 256 B4,001.1660 ns / 3952 B702.6162 ns / 352 BNA
MulticastConnect149.9700 ns / 368 B2,745.4174 ns / 2696 B392.8761 ns / 368 BNA
NeverSubscribeDispose0.0180 ns / 0 B5.1627 ns / 40 B19.4681 ns / 56 BNA
Not27.1375 ns / 120 B857.5258 ns / 1040 B91.8254 ns / 152 B28.2047 ns / 120 B
ObserveOnIf67.2485 ns / 104 BNANA65.0702 ns / 104 B
ObserveOnImmediate27.0528 ns / 96 B17,127.3834 ns / 11307 B993.3502 ns / 432 BNA
ObserveOnSafe65.0889 ns / 104 BNANA65.1163 ns / 104 B
OnCleanup139.4753 ns / 504 B1,500.6293 ns / 1528 B141.2798 ns / 216 BNA
OnErrorRetry134.0780 ns / 424 BNANA133.8272 ns / 424 B
OnNext51.8824 ns / 40 BNANA51.7027 ns / 40 B
Pairwise512.4518 ns / 160 B3,585.0555 ns / 5120 BNA520.0611 ns / 160 B
Partition275.7950 ns / 440 BNANA263.9068 ns / 440 B
Publish150.2879 ns / 368 B2,773.0882 ns / 2696 B418.1471 ns / 368 BNA
PublishLiveConnect153.5308 ns / 368 B3,153.5601 ns / 2696 B438.2998 ns / 368 BNA
Race39.9629 ns / 192 B1,584.4902 ns / 1760 B303.8675 ns / 360 BNA
RaceRanges41.2323 ns / 192 B1,567.0142 ns / 1760 B274.3877 ns / 360 BNA
Range53.4398 ns / 96 B2,740.4466 ns / 2472 B94.6010 ns / 80 BNA
RangeMapKeep152.2807 ns / 208 B2,722.9541 ns / 2584 B303.9360 ns / 272 BNA
RangeSubscribe53.7344 ns / 96 B2,687.8141 ns / 2472 B75.1094 ns / 80 BNA
ReadOnlyStateProjection103.6957 ns / 224 B96.4655 ns / 328 B177.4105 ns / 312 BNA
ReattemptRange88.9289 ns / 432 B1,510.6942 ns / 1664 BNANA
Recover97.4338 ns / 336 B1,504.3451 ns / 1560 B163.6832 ns / 264 BNA
Reduce (Operator stateful filter GC profile)624.4331 ns / 144 BNANANA
Reduce (Operator stateful filter)45.3575 ns / 144 B2,781.7262 ns / 2520 BNANA
RefCount211.3754 ns / 488 BNA570.7335 ns / 488 BNA
RefCountSubscribe187.9160 ns / 488 BNA597.1460 ns / 488 BNA
Repeat8.8482 ns / 0 B2,586.2862 ns / 2408 B76.6422 ns / 80 BNA
RepeatSubscribe7.3905 ns / 0 B2,537.4997 ns / 2408 B73.8453 ns / 80 BNA
Replay (Connectable GC profile)639.9297 ns / 512 B3,954.6961 ns / 3408 B912.8537 ns / 1360 BNA
Replay (Subject GC profile)352.6527 ns / 352 B725.4187 ns / 696 B426.9037 ns / 688 BNA
ReplayEmit16,608.7830 ns / 352 BNANANA
ReplayLastOnSubscribe64.3566 ns / 104 BNANA64.9676 ns / 104 B
ReplayLatestSubscribeDisposeAsync2,729.7429 ns / 4736 BNANANA
ReplayLiveLateSubscribe634.1315 ns / 512 B3,933.9536 ns / 3408 B946.8956 ns / 1360 BNA
Resume90.3509 ns / 336 B1,607.4910 ns / 1720 BNANA
RetryForeverWithDelay126.4210 ns / 352 BNANA125.3468 ns / 352 B
RetryWithBackoff126.0700 ns / 336 BNANA127.8376 ns / 336 B
RetryWithDelay113.8488 ns / 264 BNANA111.5882 ns / 264 B
RetryWithFixedDelay127.4925 ns / 336 BNANA135.5845 ns / 336 B
Return (Factory GC profile)0.6648 ns / 0 B54.5551 ns / 120 B34.2715 ns / 80 BNA
Return (Reactive extensions)5.4016 ns / 64 B51.9597 ns / 120 B29.6606 ns / 56 B4.8531 ns / 64 B
ReturnSubscribe0.2305 ns / 0 B51.1068 ns / 120 B31.9637 ns / 80 BNA
RunAll21.7965 ns / 136 BNANA24.1500 ns / 136 B
SafeWitness17.4100 ns / 136 B15.8467 ns / 136 B24.8238 ns / 128 BNA
SampleLatest (Operator time scheduler)260.1148 ns / 784 B2,328.9173 ns / 26264 B360.7133 ns / 664 BNA
SampleLatest (Reactive extensions)1,005.8374 ns / 488 BNANA1,054.2662 ns / 840 B
ScanWithInitial500.3109 ns / 200 B2,538.0716 ns / 2560 BNA510.5500 ns / 200 B
Schedule32.4787 ns / 216 BNANA765.7171 ns / 677 B
ScheduleSafe23.9532 ns / 144 BNANA1,510.3594 ns / 597 B
SelectAsync1,277.8845 ns / 2104 B28,626.1719 ns / 32266 BNA1,240.8623 ns / 2104 B
SelectAsyncConcurrent1,154.4372 ns / 2120 BNANA1,180.0831 ns / 2120 B
SelectAsyncSequential1,190.9455 ns / 2104 BNANA1,274.9363 ns / 2104 B
SelectConstant56.2966 ns / 136 B2,540.6148 ns / 2544 B184.1691 ns / 160 B54.6658 ns / 136 B
SelectLatestAsync1,675.4246 ns / 2032 BNANA1,653.0930 ns / 2032 B
SelectManyThen31.9010 ns / 224 B354.3868 ns / 752 BNA31.3855 ns / 224 B
SequenceCountAsync813.8323 ns / 704 BNANA798.8903 ns / 704 B
SequenceMapKeepToListAsync2,001.8091 ns / 1600 BNANA1,950.6413 ns / 1600 B
Share197.9768 ns / 488 B2,925.1001 ns / 2880 B560.0629 ns / 488 BNA
ShareLiveSubscribe190.3772 ns / 488 B2,960.3324 ns / 2880 B544.7921 ns / 488 BNA
Shuffle145.1861 ns / 96 BNANA146.1696 ns / 96 B
SignalBroadcastAsync6,400.1401 ns / 2256 BNANA6,840.4302 ns / 2320 B
SignalEmit1,598.2307 ns / 192 BNANANA
SignalFanOutChurn40,048.2300 ns / 41256 BNANANA
SignalMulticast43,417.1940 ns / 600 B3,268.6291 ns / 728 B7,277.5419 ns / 608 BNA
SignalMulticast86,441.3053 ns / 1072 B6,053.3424 ns / 1656 B13,045.9407 ns / 1120 BNA
SignalSubscribeDisposeChurn39,876.6439 ns / 41112 BNANANA
Skip (Operator stateful filter GC profile)1,735.5486 ns / 136 BNANANA
Skip (Operator stateful filter)86.5946 ns / 136 B2,658.2239 ns / 2512 BNANA
SkipWhile (Operator stateful filter GC profile)1,800.1429 ns / 144 BNANANA
SkipWhile (Operator stateful filter)94.1695 ns / 144 B2,700.6545 ns / 2520 BNANA
SkipWhileNull22.9011 ns / 112 B644.6796 ns / 944 BNA22.1427 ns / 112 B
Start23.0806 ns / 96 BNANA936.0223 ns / 535 B
StartSubscribe47.5984 ns / 208 B860.5110 ns / 751 B66.4836 ns / 160 BNA
StartWithAppend35.9490 ns / 168 B1,030.8613 ns / 1283 B157.9538 ns / 288 BNA
StartWithAppendDefaultIfEmpty36.0423 ns / 168 B994.2108 ns / 1283 B151.1573 ns / 288 BNA
State102415,860.3556 ns / 160 B16,763.4572 ns / 200 B16,556.1635 ns / 192 BNA
StateEmit15,824.8088 ns / 160 BNANANA
StateSignal102416,183.5083 ns / 160 B17,070.4020 ns / 200 B16,583.5164 ns / 192 BNA
StateSignal32546.1293 ns / 160 B605.4201 ns / 200 B630.1873 ns / 192 BNA
StateSignalUpdates557.0397 ns / 160 B583.7040 ns / 200 B622.1236 ns / 192 BNA
SubjectEmit10241,590.1508 ns / 192 B1,761.6808 ns / 136 B2,091.8153 ns / 160 BNA
SubjectEmit3294.0824 ns / 192 B99.2329 ns / 136 B124.9866 ns / 160 BNA
SubjectSubscribeDispose643,557.3485 ns / 4360 B3,994.1933 ns / 38472 B3,825.6100 ns / 6728 BNA
SubjectSubscribeDispose8352.1052 ns / 704 B318.3937 ns / 1288 B493.2540 ns / 904 BNA
SubscribeAndComplete0.2067 ns / 0 BNANA0.2286 ns / 0 B
SubscribeAsync967.6081 ns / 544 BNANA996.4746 ns / 544 B
SubscribeDispose643,743.0097 ns / 4360 B4,234.4889 ns / 38472 B3,772.4909 ns / 6728 BNA
SubscribeGetError6.0310 ns / 48 BNANA50.6131 ns / 104 B
SubscribeGetValue15.8250 ns / 56 BNANA15.8805 ns / 56 B
SubscribeOnImmediate102.0478 ns / 416 B2,089.6776 ns / 2257 B134.4352 ns / 200 BNA
SubscribeSynchronous1,030.9847 ns / 544 BNANA999.5322 ns / 544 B
Switch84.3075 ns / 312 B2,342.9420 ns / 2360 B797.3904 ns / 448 BNA
SwitchIfEmpty65.5825 ns / 224 BNANA109.1341 ns / 280 B
SwitchRanges84.5506 ns / 312 B2,248.5388 ns / 2360 B765.5251 ns / 448 BNA
SynchronizeAsync796.1634 ns / 1280 BNANA818.4586 ns / 1280 B
SynchronizeSynchronous818.4827 ns / 1280 BNANA793.0097 ns / 1280 B
SyncTimer2,513.3305 ns / 1080 BNANA12,247.8994 ns / 26240 B
TakeRange65.3818 ns / 200 B1,487.1980 ns / 1552 B99.5176 ns / 160 BNA
TakeUntil518.1361 ns / 192 B2,618.4101 ns / 2520 BNA508.4543 ns / 192 B
TakeWhile (Operator stateful filter GC profile)1,668.5275 ns / 144 BNANANA
TakeWhile (Operator stateful filter)100.4530 ns / 144 B2,649.6499 ns / 2520 BNANA
TapRange62.0108 ns / 200 B1,460.5331 ns / 1520 B130.6363 ns / 216 BNA
TapWith38.1821 ns / 136 B1,479.6060 ns / 1608 B147.0685 ns / 304 BNA
TaskSignalSubscribe37.9094 ns / 240 B731.7046 ns / 886 B40.2932 ns / 160 BNA
ThrottleBurst598.0057 ns / 1184 B2,818.8936 ns / 36480 B1,717.3992 ns / 1512 BNA
ThrottleDistinct1,787.3767 ns / 4232 BNANA28,694.7072 ns / 18678 B
ThrottleFirst1,119.1755 ns / 224 BNANA1,143.5209 ns / 224 B
ThrottleOnScheduler1,835.5199 ns / 2400 BNANA30,618.2012 ns / 16366 B
ThrottleUntilTrue4,465.7651 ns / 1633 BNANA5,547.6550 ns / 1385 B
Throw63.0356 ns / 120 B129.4794 ns / 240 B98.1077 ns / 200 BNA
ThrowSubscribe63.0707 ns / 120 B115.1738 ns / 240 B98.3755 ns / 200 BNA
TimeIntervalRange26.6122 ns / 120 B2,009.7466 ns / 1616 B480.1429 ns / 160 BNA
TimeoutIdle311.1149 ns / 808 B1,442.3126 ns / 29776 B441.6512 ns / 784 BNA
TimestampRange40.2701 ns / 120 B1,796.7855 ns / 1512 B360.3219 ns / 152 BNA
ToHotTask35.2807 ns / 112 B91.3744 ns / 240 BNA33.2869 ns / 112 B
ToHotValueTask26.8257 ns / 72 BNANA26.8298 ns / 72 B
ToPropertyObservable26,227.7832 ns / 4941 BNANA26,438.4603 ns / 4941 B
ToReadOnlyBehavior58.7424 ns / 192 BNANA58.4042 ns / 192 B
ToTask14.9083 ns / 192 B2,635.3250 ns / 2824 B95.4554 ns / 208 BNA
TrySelect104.2941 ns / 120 BNANA103.1948 ns / 120 B
UnfoldSubscribe10.4787 ns / 0 B2,327.2008 ns / 2768 B98.2107 ns / 152 BNA
Unique (Operator stateful filter GC profile)1,900.0763 ns / 144 BNANANA
Unique (Operator stateful filter)109.5600 ns / 144 B2,694.3320 ns / 2520 BNANA
UniqueBy (Operator stateful filter GC profile)1,957.1433 ns / 152 BNANANA
UniqueBy (Operator stateful filter)103.9614 ns / 152 B2,661.1942 ns / 2568 BNANA
UseSubscribe43.0683 ns / 144 B88.4371 ns / 168 B76.1832 ns / 176 BNA
Using6.0721 ns / 56 BNANA6.2145 ns / 56 B
WaitForCompletion22.0032 ns / 96 BNANA22.5720 ns / 96 B
WaitForError25.0017 ns / 96 BNANA65.9992 ns / 152 B
WaitForValue30.4019 ns / 104 BNANA30.6159 ns / 104 B
WaitUntil518.4325 ns / 224 B835.7113 ns / 1080 BNA543.8398 ns / 224 B
WhereFalse21.3931 ns / 120 B758.8829 ns / 1040 B88.0082 ns / 184 B19.7443 ns / 120 B
WhereIsNotNull20.9544 ns / 104 B621.3007 ns / 904 B100.0026 ns / 264 B21.2683 ns / 104 B
WhereSelect80.1184 ns / 152 B2,631.6110 ns / 2616 B174.5875 ns / 240 B77.8284 ns / 152 B
WhereTrue20.9512 ns / 120 B766.8200 ns / 1040 B83.7375 ns / 184 B21.0377 ns / 120 B
While122.1943 ns / 280 BNANA123.8769 ns / 280 B
WithLatest40.6575 ns / 192 B3,589.8584 ns / 2824 B396.8615 ns / 248 BNA
WithLatestRanges40.9675 ns / 192 B3,514.5040 ns / 2824 B269.3590 ns / 248 BNA
WithLimitedConcurrency2,505.1640 ns / 5448 BNANA2,486.2790 ns / 5448 B
Zip (Operator core GC profile)43.2304 ns / 192 B3,821.7608 ns / 2976 B781.9091 ns / 656 BNA
Zip (Operator zip)37.2355 ns / 192 B3,337.3608 ns / 2976 B753.2998 ns / 656 BNA

BenchmarkDotNet emitted ZeroMeasurement warnings for several singleton or empty-method-scale paths, including Return, CompletedSpark, Never-style subscriptions, and SubscribeAndComplete. Those warnings mean the measured duration is indistinguishable from empty method overhead; the benchmark run still completed and exported all 617 rows.

Repository layout

PathPurpose
src/ReactiveUI.Primitives.slnxCurrent solution entrypoint.
src/ReactiveUI.DisposablesDisposable primitives shared by the package family.
src/ReactiveUI.Primitives.CoreType-agnostic core shared by lean and System.Reactive-flavoured Primitives leaves.
src/ReactiveUI.PrimitivesDefault lean signal/operator/sequencer package, extension helpers, and platform sequencers.
src/ReactiveUI.Primitives.ReactiveSystem.Reactive-flavoured Primitives leaf including the Reactive extension helpers.
src/ReactiveUI.Primitives.Async.CoreType-agnostic async core shared by async leaves.
src/ReactiveUI.Primitives.AsyncLean async observable/signal package built on IObservableAsync<T> and IObserverAsync<T>.
src/ReactiveUI.Primitives.Async.ReactiveSystem.Reactive-flavoured async Primitives leaf.
src/ReactiveUI.Primitives.Extensions.CoreSource-only extension-helper implementation linked into ReactiveUI.Primitives.Core; not a project or package.
src/ReactiveUI.Primitives.WpfOptional WPF dispatcher integration library.
src/ReactiveUI.Primitives.Wpf.ReactiveOptional WPF dispatcher scheduler integration library for System.Reactive consumers.
src/ReactiveUI.Primitives.WinFormsOptional Windows Forms control integration library.
src/ReactiveUI.Primitives.WinForms.ReactiveOptional Windows Forms control scheduler integration library for System.Reactive consumers.
src/ReactiveUI.Primitives.WinUIOptional WinUI dispatcher queue integration library.
src/ReactiveUI.Primitives.WinUI.ReactiveOptional WinUI dispatcher queue scheduler integration library for System.Reactive consumers.
src/ReactiveUI.Primitives.BlazorOptional Blazor renderer integration library.
src/ReactiveUI.Primitives.Blazor.ReactiveOptional Blazor renderer scheduler integration library for System.Reactive consumers.
src/ReactiveUI.Primitives.AvaloniaOptional Avalonia dispatcher sequencer integration library.
src/ReactiveUI.Primitives.Avalonia.ReactiveOptional Avalonia dispatcher scheduler integration library for System.Reactive consumers.
src/ReactiveUI.Primitives.MauiOptional MAUI dispatcher integration library.
src/ReactiveUI.Primitives.Maui.ReactiveOptional MAUI dispatcher scheduler integration library for System.Reactive consumers.
src/ReactiveUI.Primitives.ObservableEventsStandalone analyzer package for provider-aware observable event generation.
src/ReactiveUI.Primitives.R3Bridge.GeneratorStandalone analyzer package for optional R3 and R3Async bridge generation.
src/Primitives.SharedLinked lean/Reactive synchronous source.
src/Primitives.Async.SharedLinked lean/Reactive async source.
src/Primitives.Extensions.SharedLinked lean/Reactive Extensions source.
src/testsMicrosoft Testing Platform/TUnit-style test projects.
src/benchmarks/ReactiveUI.Primitives.BenchmarksBenchmarkDotNet comparison harness.

Practical migration checklist

  1. Replace subject construction with Signal<T>, StateSignal<T>, or ReplaySignal<T> depending on current behavior.
  2. Replace factories: Observable.Return/Empty/Throw/Timer/Interval to Signal.Emit/None/Fail/After/Pulse.
  3. Replace hot-path operators with Primitives names: Select -> Map, Where -> Keep, SelectMany -> FlatMap, Do -> Tap, Scan -> Fold, Aggregate -> Reduce, Amb -> Race.
  4. Replace composite/serial disposables with MultipleDisposable/Pocket and SingleReplaceableDisposable/Slot.
  5. Keep System.Reactive, R3, or R3Async at application boundaries only when required; use .Reactive package variants for System.Reactive public-surface compatibility and generated bridge methods for R3/R3Async boundaries.
  6. Run build, tests, pack, and git diff --check before publishing or merging.

Contribute

ReactiveUI.Primitives is developed under an OSI-approved open source license, making it freely usable and distributable, even for commercial use. We ❤ the people who are involved in this project, and we'd love to have you on board, especially if you are just getting started or have never contributed to open-source before.

So here's to you, lovely person who wants to join us. This is how you can support us:

Code of Conduct

We are dedicated to providing a welcoming and inclusive community. Please read and follow our Code of Conduct.

License

ReactiveUI.Primitives is licensed under the MIT License.

About

A compact, high-performance reactive library for .NET applications

Topics

Resources

Code of conduct

Contributing

Stars

12 stars

Watchers

0 watching

Forks

Releases

Sponsor this project

Packages

Used by

Contributors

Languages