Skip to content

feat: re-platform ReactiveUI onto ReactiveUI.Primitives — leaner, faster, System.Reactive optional#4382

Merged
glennawatson merged 2 commits into
mainfrom
feature/primitives-migration
Jun 20, 2026
Merged

feat: re-platform ReactiveUI onto ReactiveUI.Primitives — leaner, faster, System.Reactive optional#4382
glennawatson merged 2 commits into
mainfrom
feature/primitives-migration

Conversation

@glennawatson

@glennawatson glennawatson commented Jun 16, 2026

Copy link
Copy Markdown
Contributor

⚠️ Breaking Changes

ReactiveUI now ships in two interchangeable distributions with an identical public API. Both are built on
the same ReactiveUI.Primitives internals and the same high-performance custom schedulers/sinks — the only
difference is which reactive interop types appear in the public API:

You want… Reference these packages Public reactive types
The new, lighter default (no System.Reactive dependency) ReactiveUI, ReactiveUI.Wpf, ReactiveUI.WinForms, ReactiveUI.WinUI, ReactiveUI.Maui, ReactiveUI.Blazor, ReactiveUI.AndroidX, … ReactiveUI.Primitives — RxVoid, ISequencer, Signal<T>
Drop-in interop with existing System.Reactive code ReactiveUI.Reactive, ReactiveUI.Wpf.Reactive, ReactiveUI.WinForms.Reactive, ReactiveUI.WinUI.Reactive, ReactiveUI.Maui.Reactive, ReactiveUI.Blazor.Reactive, … System.Reactive — Unit, IScheduler

WhenAnyValue, ReactiveCommand, ToProperty/OAPH, bindings, routing and interactions are the same in both — you
pick a distribution, you don't rewrite code. The .Reactive family is not "old ReactiveUI": it runs on the exact
same Primitives engine and custom schedulers as the default; it simply surfaces System.Reactive.Unit/IScheduler
(and Subject<T>) in its public API so it composes with code that already uses System.Reactive.

If you upgrade the default ReactiveUI packages, the public reactive types change:

  • System.Reactive.Concurrency.ISchedulerReactiveUI.Primitives.Concurrency.ISequencer (e.g. RxApp.MainThreadScheduler/TaskpoolScheduler, also exposed via RxSchedulers).
  • System.Reactive.UnitReactiveUI.Primitives.RxVoid (e.g. ReactiveCommand<Unit, Unit>ReactiveCommand<RxVoid, RxVoid>).
  • Subject<T> / BehaviorSubject<T> / ReplaySubject<T>Signal<T> / BehaviorSignal<T> / ReplaySignal<T>.
  • The default packages no longer pull in System.Reactive transitively. If your code also calls System.Reactive operators (System.Reactive.Linq), add a direct System.Reactive reference or use the .Reactive packages.

DynamicData is no longer a dependency of core. The DynamicData-backed routing and collection helpers (the
change-set / ToObservableChangeSet-style routing and auto-persist mixins) moved into a new ReactiveUI.Routing
package (ReactiveUI.Routing.Reactive for the System.Reactive flavor). Core RoutingState/IScreen/RoutedViewHost
stay in the main package; if you used the DynamicData routing/collection extensions, add ReactiveUI.Routing.

To take the upgrade with zero source changes, reference the matching ReactiveUI*.Reactive packages — they keep
IScheduler, Unit and Subject<T>.

Why this change?

System.Reactive is a large, general-purpose dependency, but ReactiveUI only uses a small, well-defined slice of it —
schedulers, a handful of subjects, and a few operators behind WhenAnyValue/ToProperty/ReactiveCommand. Carrying
the whole library meant every ReactiveUI app inherited its size, its transitive footprint, and its allocation/
throughput characteristics on the hottest MVVM paths.

ReactiveUI.Primitives is a purpose-built, allocation-conscious reactive core with fused sinks tailored to exactly
those paths, plus our own scheduler implementations written largely for speed. As part of this, most of the
schedulers now live in ReactiveUI.Primitives
and are shared by both distributions. Moving onto it lets us:

  • Drop the System.Reactive (and DynamicData) dependency from the default packages — smaller closure, better
    trimming/AOT story.
  • Cut allocations and time dramatically on the operators apps use constantly, for both distributions.
  • Keep everyone supported — the .Reactive distribution keeps the System.Reactive interop types, so this is an
    opt-in change of public types, not a forced rewrite.

Performance

Representative micro-benchmarks (BenchmarkDotNet, net10.0), last fully-System.Reactive release 23.2.28 vs the new
default (Primitives) distribution. Lower is better; allocation is per operation.

WhenAnyValue — subscribe

Scenario 23.2.28 (System.Reactive) New default (Primitives)
Arity 1 2,900 ns · 3.63 KB 736 ns · 3.06 KB
Arity 2 5,697 ns · 7.57 KB 1,634 ns · 3.47 KB
Arity 3 8,322 ns · 11.25 KB 2,524 ns · 5.20 KB

WhenAnyValue — emit (10k changes)

Scenario 23.2.28 (System.Reactive) New default (Primitives)
Arity 1 3,325 µs · 6,797 KB 707 µs · 509 KB
Arity 2 3,915 µs · 6,797 KB 1,134 µs · 703 KB
Arity 3 4,115 µs · 7,031 KB 1,268 µs · 937 KB

ToProperty / OAPH

Scenario 23.2.28 (System.Reactive) New default (Primitives)
Create 7.33 µs · 7.76 KB 1.02 µs · 1.93 KB
Emit (10k) 5,893 µs · 10,859 KB 1,961 µs · 2,109 KB

Net effect: roughly 3–4× faster on subscribe and emit, and 5–13× less allocation (e.g. WhenAnyValue.Emit
arity 1 drops from ~6.8 MB to ~0.5 MB — a ~13× reduction; ToProperty.Create from ~7.3 µs to ~1.0 µs). Because the
fast schedulers/sinks live in Primitives and back both distributions, the .Reactive family improves over 23.2.28 too
while keeping the System.Reactive interop types.

Numbers are from a short BenchmarkDotNet job on a developer machine, so absolute values have wide error bars — but
the released-vs-new gap is far beyond run-to-run noise.

What kind of change does this PR introduce?

Feature — a re-platforming of ReactiveUI onto ReactiveUI.Primitives, delivered as two interchangeable distributions.

What is the new behavior?

  • Every ReactiveUI library is produced in two flavors from one shared source:
    • the default family (ReactiveUI, ReactiveUI.Wpf, …) exposes the ReactiveUI.Primitives interop types and
      carries no System.Reactive dependency;
    • the .Reactive family (ReactiveUI.Reactive, ReactiveUI.Wpf.Reactive, …) exposes the System.Reactive
      interop types (Unit/IScheduler) over the same Primitives engine.
  • Both share the same internals and the new custom schedulers/sinks; most schedulers now live in Primitives.
  • DynamicData integration (change-set routing/collection/auto-persist mixins) is now a separate ReactiveUI.Routing
    package, so core no longer depends on DynamicData.
  • Same public surface across distributions, so you choose a distribution rather than rewriting code.
  • Public API baselines (PublicAPI.Shipped.txt) are tracked again for every target framework so the surface of both
    distributions is locked and reviewable.

What is the current behavior?

ReactiveUI is a single distribution built directly on System.Reactive and DynamicData, so every consumer takes those
transitive dependencies and the System.Reactive types (IScheduler, Unit, Subject<T>) appear throughout the public
surface. There is no System.Reactive-free option.

What might this PR break?

See Breaking Changes above. In short: upgrading the default ReactiveUI* packages swaps the public reactive types
(ISchedulerISequencer, UnitRxVoid, SubjectSignal) and drops the transitive System.Reactive/DynamicData
dependencies; the DynamicData routing/collection helpers move to ReactiveUI.Routing. Consumers who want no source
changes can move to the ReactiveUI*.Reactive packages, which retain Unit/IScheduler/Subject<T>.

Checklist

  • I have read the Contribute guide
  • Tests have been added or updated (for bug fixes / features)
  • Docs have been added or updated (for bug fixes / features)
  • Changes target the main branch
  • PR title follows Conventional Commits

Additional information

Verification: the full solution builds clean on Linux and Windows; the core, routing, AOT, builder, Splat, testing and
Blazor suites are green on Linux, and the WPF (330) and WinForms (2011) suites are green on real Windows for both
distributions.

@glennawatson glennawatson force-pushed the feature/primitives-migration branch from 56f99b4 to d748997 Compare June 18, 2026 05:30
@glennawatson glennawatson changed the title feature: Convert to having both ReactiveUI.Primtiives/System.Reactive feat: dual ReactiveUI.Primitives / System.Reactive distributions with identical API Jun 18, 2026
Replaces System.Reactive with ReactiveUI.Primitives (5.4.0) across the core
and every platform, behind a lean/.Reactive seam so both surfaces ship from
one shared source with an identical public API.

- Default packages (ReactiveUI, ReactiveUI.Wpf, …) run on ReactiveUI.Primitives
  with no System.Reactive dependency, exposing RxVoid/ISequencer/Signal; the
  .Reactive packages expose System.Reactive Unit/IScheduler interop types over
  the same Primitives engine and custom fused sinks/schedulers.
- Most schedulers now live in ReactiveUI.Primitives and back both distributions;
  in-repo custom schedulers removed in favour of the Primitives apple/android
  reactive sequencers, resolved via seam namespace imports (no per-file #if).
- DynamicData integration (change-set routing/collection/auto-persist mixins)
  moved into a separate ReactiveUI.Routing package, so core no longer depends
  on DynamicData; core RoutingState/IScreen/RoutedViewHost stay in the main
  package.
- Re-enable PublicAPI tracking (RS0016/RS0017/RS0037) and regenerate Shipped
  baselines for all TFMs, including the android Resource designer entries.
- Fixes: WinForms event handler unwires synchronously; interaction runner no
  longer disposes a live inline-scheduled handler; WaitForDispatcherScheduler
  lean generic Schedule overloads; routing test app-builder initialization;
  WPF markup-compile _wpftmp temp project output type; example/benchmark and
  MAUI sample build/doc fixes.
- README documents the two distributions, the type differences, the routing
  package split, and the performance gains.

Verified: full slnx builds 0/0 on Linux and Windows; core, routing, AOT,
builder, splat, testing, blazor suites green on Linux; WPF (330) and WinForms
(2011) suites green on real Windows, both distributions.
@glennawatson glennawatson force-pushed the feature/primitives-migration branch from 2f3dacd to cdc71db Compare June 18, 2026 05:57
@glennawatson glennawatson marked this pull request as ready for review June 18, 2026 05:57
… non-Windows run)

- MauiDispatcherSequencer schedules delays via the dispatcher's native
  DispatchDelayed in Primitives 5.4.0, not a created timer. Update the Maui
  test + MockDispatcher to assert the DispatchDelayed path (the mock no longer
  throws from DispatchDelayed); covers ReactiveUI.Maui.Tests and the
  ReactiveUI.Builder.Maui.Tests project that references it.
- The WPF test assemblies have no cross-platform tests left (XamlApiApprovalTests
  were replaced by PublicAPI baselines), so on non-Windows they ran zero tests
  and MTP failed the run. Add a single cross-platform smoke test (compiled into
  both Wpf.Tests and Wpf.Tests.Reactive via the unconditional API/ include) so
  the non-Windows run is valid; the real WPF tests still run on Windows.
@glennawatson glennawatson changed the title feat: dual ReactiveUI.Primitives / System.Reactive distributions with identical API feat: re-platform ReactiveUI onto ReactiveUI.Primitives — leaner, faster, System.Reactive optional Jun 18, 2026
[ExcludeFromCodeCoverage]
[AttributeUsage(AttributeTargets.All)]
internal sealed class PreserveAttribute : Attribute
public sealed class PreserveAttribute : Attribute

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this stay internal

</ItemGroup>

<ItemGroup>
<PackageReference Include="ReactiveUI.Primitives" ExcludeAssets="analyzers" />

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is correct, but I spotted some places where the analysers were not turned off.

@sonarqubecloud

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
0.0% Coverage on New Code (required ≥ 80%)

See analysis details on SonarQube Cloud

@codecov

codecov Bot commented Jun 18, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 57.24907% with 115 lines in your changes missing coverage. Please review.
✅ Project coverage is 89.69%. Comparing base (742cb4e) to head (04a384f).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
src/ReactiveUI.Core/Mixins/ExpressionMixins.cs 34.58% 83 Missing and 4 partials ⚠️
...lazor/Builder/BlazorReactiveUIBuilderExtensions.cs 68.75% 5 Missing ⚠️
src/ReactiveUI.Core/Mixins/CompatMixins.cs 0.00% 5 Missing ⚠️
...veUI.Core/Bindings/BindingTypeConverterDispatch.cs 60.00% 4 Missing ⚠️
src/ReactiveUI.Blazor/ReactiveComponentBase.cs 25.00% 2 Missing and 1 partial ⚠️
...c/ReactiveUI.Blazor/ReactiveLayoutComponentBase.cs 25.00% 2 Missing and 1 partial ⚠️
...c/ReactiveUI.Blazor/ReactiveOwningComponentBase.cs 25.00% 2 Missing and 1 partial ⚠️
src/ReactiveUI.Core/Mixins/ChangeSetMixins.cs 85.71% 2 Missing ⚠️
...dings/Command/CommandBinderImplementationMixins.cs 0.00% 1 Missing ⚠️
src/ReactiveUI.Core/Expression/ReflectionMixins.cs 66.66% 0 Missing and 1 partial ⚠️
... and 1 more
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #4382      +/-   ##
==========================================
- Coverage   89.93%   89.69%   -0.25%     
==========================================
  Files         366      341      -25     
  Lines       15653    14948     -705     
  Branches     1613     1502     -111     
==========================================
- Hits        14078    13408     -670     
+ Misses       1219     1208      -11     
+ Partials      356      332      -24     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@glennawatson glennawatson merged commit 42b4cce into main Jun 20, 2026
9 of 13 checks passed
@glennawatson glennawatson deleted the feature/primitives-migration branch June 20, 2026 02:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants