Minimalist.Reactive is a compact, high-performance reactive library for .NET applications that want Rx-style composition without a runtime dependency on System.Reactive or R3. It keeps the BCL IObservable<T> / IObserver<T> contracts where they are useful, adds Minimalist names for common concepts, and focuses on predictable AOT-friendly code paths with low allocation overhead.
Minimalist.Reactive is designed to:
- Provide Rx-style stream creation, subscription, state, scheduling, and composition over
IObservable<T>. - Use a distinct vocabulary where it improves clarity:
Signal<T>instead ofSubject<T>,Mapinstead of onlySelect,Keepinstead of onlyWhere,Sparkinstead of notification materialization. - Stay AOT-friendly: no runtime reflection, dynamic code generation, expression compilation, or hidden dependency on System.Reactive/R3 in the production package.
- Minimize allocations in hot paths, including direct single-action subscribers for
Signal<T>and reusable immutable singleton signals for common return/empty/never cases. - Support broad production target frameworks, including .NET Framework, Windows desktop, and modern mobile/desktop TFMs.
- Allow migration from System.Reactive/R3 through source-generator bridges when the consuming project already references those libraries.
When the package is available on your configured NuGet feed:
dotnet add package Minimalist.ReactiveThen import the namespaces you need:
using Minimalist.Reactive;
using Minimalist.Reactive.Concurrency;
using Minimalist.Reactive.Disposables;
using Minimalist.Reactive.Signals;The package metadata is configured to include this README in the NuGet package via PackageReadmeFile=README.md. The base package also packs both bridge source-generator assemblies under analyzers/dotnet/cs:
Minimalist.Reactive.SystemReactiveBridge.Generator.dllMinimalist.Reactive.R3Bridge.Generator.dll
Those generators are analyzers. They do not add runtime System.Reactive or R3 dependencies to Minimalist.Reactive. They emit bridge code only when the consuming compilation already references the relevant external library symbols.
The production library targets:
net462net472net481net9.0-windows10.0.19041.0net10.0-windows10.0.19041.0net9.0-iosnet9.0-tvosnet9.0-macosnet9.0-maccatalystnet10.0-iosnet10.0-tvosnet10.0-macosnet10.0-maccatalystnet9.0-androidnet10.0-android
Runtime package dependencies are intentionally small. The production package does not depend on System.Reactive or R3. System.ValueTuple is used for net462 only. Benchmark projects may reference System.Reactive and R3 as comparison baselines, but those references are not production dependencies.
Signal<T> is the basic subject-like primitive. It implements ISignal<T>, which combines IObserver<T>, IObservable<T>, and IsDisposed.
Use it when code needs to push values into a stream and let observers subscribe:
using Minimalist.Reactive;
using Minimalist.Reactive.Signals;
var signal = new Signal<int>();
using IDisposable subscription = signal.Subscribe(
value => Console.WriteLine($"next: {value}"),
error => Console.WriteLine($"error: {error.Message}"),
() => Console.WriteLine("completed"));
signal.OnNext(1);
signal.OnNext(2);
signal.OnCompleted();Important behavior:
OnNext(T)sends a value to active subscribers.OnError(Exception)terminates the signal with an error.OnCompleted()terminates the signal successfully.Subscribe(...)returnsIDisposable; disposing the subscription unsubscribes.HasObserversandIsDisposedexpose basic lifecycle state.- The
Subscribe(Action<T>)extension uses an optimized direct-action path forSignal<T>when possible.
Minimalist.Reactive keeps the standard IObserver<T> shape and provides helper observer implementations internally under the Core namespace.
Common user-facing subscription overloads live in SubscribeMixins:
using Minimalist.Reactive;
using Minimalist.Reactive.Signals;
var signal = new Signal<string>();
using var nextOnly = signal.Subscribe(value => Console.WriteLine(value));
using var full = signal.Subscribe(
value => Console.WriteLine(value),
error => Console.Error.WriteLine(error),
() => Console.WriteLine("done"));The library uses the term witness for lightweight observer wrappers. You normally use delegates or IObserver<T> directly rather than constructing witness types by hand.
Subscriptions and scheduled work return IDisposable. Minimalist.Reactive includes lightweight disposable primitives in Minimalist.Reactive.Disposables:
| Type | Use |
|---|---|
Disposable.Create(Action) |
Create an IDisposable from a cleanup action. |
Disposable.Empty |
No-op disposable. |
BooleanDisposable |
Track simple disposed state. |
CancellationDisposable |
Tie disposal to a CancellationTokenSource. |
MultipleDisposable |
Composite-disposable equivalent; add/remove multiple disposables. |
Pocket |
Named MultipleDisposable specialization. |
SingleDisposable / AssignmentSlot |
Single-assignment disposable container. |
SingleReplaceableDisposable / Slot |
Replaceable disposable container. |
Handle, Handle<T>, Handle<T1,T2>, Handle<T1,T2,T3> |
Lightweight handle wrappers for resource lifetimes. |
Example:
using Minimalist.Reactive;
using Minimalist.Reactive.Disposables;
using Minimalist.Reactive.Signals;
var subscriptions = new MultipleDisposable();
var signal = new Signal<int>();
signal.Subscribe(value => Console.WriteLine(value)).DisposeWith(subscriptions);
signal.Subscribe(value => Console.WriteLine(value * 10)).DisposeWith(subscriptions);
signal.OnNext(3);
subscriptions.Dispose();Creation APIs live on Minimalist.Reactive.Signals.Signal.
| Factory | Purpose |
|---|---|
Signal.Create<T>(Func<IObserver<T>, IDisposable>) |
Build a custom observable. |
Signal.CreateSafe<T>(Func<IObserver<T>, IDisposable>) |
Build a custom observable with safety wrapping. |
Signal.CreateWithState<T,TState>(...) |
Build a custom observable while passing state explicitly. |
Signal.Defer<T>(Func<IObservable<T>>) |
Create the source per subscription. |
Signal.Return<T>(T) |
Emit one value and complete. Specialized fast paths exist for bool, int, and RxVoid. |
Signal.Empty<T>() |
Complete without values. |
Signal.Never<T>() / Signal.Never<T>(T witness) |
Never emit and never complete. |
Signal.Throw<T>(Exception) |
Terminate with an error. |
Signal.Range(int start, int count) |
Emit an integer range and complete. |
Signal.Repeat<T>(T value) / Repeat<T>(T value, int count) |
Repeat indefinitely or a fixed number of times. |
Signal.Unfold<TState,TResult>(...) |
Generate a finite sequence from state. |
Signal.Use<TResource,T>(...) |
Tie a resource lifetime to a subscription. |
Signal.FromEnumerable<T>(IEnumerable<T>) |
Convert an enumerable. |
Signal.FromAsyncEnumerable<T>(IAsyncEnumerable<T>, CancellationToken) |
Convert an async enumerable on modern TFMs. |
Signal.FromTask<T>(Task<T>) |
Convert a task to a signal. |
Signal.After(TimeSpan, IScheduler?) |
Emit one long tick after a delay. |
Signal.Every(TimeSpan, IScheduler?) |
Emit increasing long ticks repeatedly. |
Signal.Pulse(...) |
Alias of Every. |
Signal.Interval(...) |
Alias of Every. |
Signal.Timer(...) |
Alias/overload for one-shot and periodic timers. |
Signal.Concat(...), Signal.Merge(...), Signal.Race(...) |
Compose multiple sources. |
Signal.Zip(...), Signal.CombineLatest(...), Signal.ZipLatest(...), Signal.ForkJoin(...) |
Pairwise combination helpers. |
Example:
using Minimalist.Reactive;
using Minimalist.Reactive.Signals;
IObservable<int> values = Signal.Range(1, 5);
using var subscription = values.Subscribe(
value => Console.WriteLine(value),
error => Console.Error.WriteLine(error),
() => Console.WriteLine("range completed"));Custom source example:
using Minimalist.Reactive.Disposables;
using Minimalist.Reactive.Signals;
IObservable<string> source = Signal.CreateSafe<string>(observer =>
{
observer.OnNext("ready");
observer.OnCompleted();
return Disposable.Empty;
});Operators are extension methods over IObservable<T>. Minimalist.Reactive intentionally includes both canonical LINQ/Rx names where useful and Minimalist names where the library wants a distinct surface.
| System.Reactive-style concept | Minimalist.Reactive API |
|---|---|
Select |
Map |
stateful Select without closure |
MapWith |
Where |
Keep; Where delegates to Keep. |
stateful Where without closure |
KeepWith |
| non-null filtering | KeepNotNull |
OfType / Cast |
OfType<TResult> / Cast<TResult> |
| side effects | Tap, TapWith |
Scan |
Scan |
Aggregate |
Fold |
Distinct |
Distinct |
DistinctUntilChanged |
DistinctUntilChanged |
| key-based distinct | DistinctBy, DistinctUntilChangedBy |
Take / Skip |
Take, Skip |
TakeWhile / SkipWhile |
TakeWhile, SkipWhile |
IgnoreElements |
IgnoreValues |
DefaultIfEmpty |
DefaultIfEmpty |
Example:
using Minimalist.Reactive;
using Minimalist.Reactive.Signals;
IObservable<string> labels = Signal.Range(1, 10)
.Keep(value => value % 2 == 0)
.Map(value => $"even:{value}")
.Tap(label => Console.WriteLine($"observed {label}"));
using var subscription = labels.Subscribe(Console.WriteLine);| Concept | API |
|---|---|
| sequential concatenation | Concat |
| concurrent merge | Merge |
| first source wins | Race |
| latest inner source wins | Switch |
| pairwise zip | Zip |
| latest-value combination | CombineLatest |
| combine left emission with latest right value | WithLatest |
| latest-fusion alias | ZipLatest, FuseLatest |
| last values after both complete | ForkJoin |
| retry | Retry |
| catch/rescue | Rescue, Resume, Signal.Catch |
| final action | Signal.Finally |
Merge example:
using Minimalist.Reactive;
using Minimalist.Reactive.Signals;
IObservable<int> low = Signal.Range(1, 3);
IObservable<int> high = Signal.Range(100, 3);
using var merged = Signal.Merge(low, high)
.Subscribe(value => Console.WriteLine(value));CombineLatest example:
using Minimalist.Reactive;
using Minimalist.Reactive.Signals;
var width = new StateSignal<int>(640);
var height = new StateSignal<int>(480);
using var area = Signal.CombineLatest(width, height, (w, h) => w * h)
.Subscribe(value => Console.WriteLine($"area={value}"));
width.Value = 800;
height.Value = 600;| Concept | API |
|---|---|
| delayed subscription | DelayStart |
| delayed values | Delay |
| quiet-period sampling | Throttle |
| periodic sampling | Sample |
| timeout | Timeout |
| timestamp values | Timestamp |
| measure intervals | TimeInterval |
| fixed-size buffers | Buffer(count), Buffer(count, skip) |
| collect to list/array signal | CollectList, CollectArray |
| collect asynchronously | CollectListAsync, CollectArrayAsync |
| first value task | FirstAsync, FirstOrDefaultAsync |
Timer example:
using Minimalist.Reactive;
using Minimalist.Reactive.Concurrency;
using Minimalist.Reactive.Signals;
using var subscription = Signal.Timer(
dueTime: TimeSpan.FromMilliseconds(250),
period: TimeSpan.FromSeconds(1),
scheduler: ThreadPoolScheduler.Instance)
.Take(3)
.Subscribe(
tick => Console.WriteLine($"tick {tick}"),
error => Console.Error.WriteLine(error),
() => Console.WriteLine("timer completed"));Spark<T> represents value/error/completion notifications. Use Sparkify to convert stream events into values and Unspark to turn them back into observer notifications.
using Minimalist.Reactive;
using Minimalist.Reactive.Core;
using Minimalist.Reactive.Signals;
IObservable<Spark<int>> sparks = Signal.Range(1, 3).Sparkify();
IObservable<int> values = sparks.Unspark();Minimalist.Reactive uses explicit names instead of cloning every System.Reactive subject type name.
| System.Reactive type | Minimalist.Reactive equivalent | Notes |
|---|---|---|
Subject<T> |
Signal<T> |
Push values, errors, and completion to subscribers. |
BehaviorSubject<T> |
BehaviourSignal<T> or StateSignal<T> |
Stores the latest value and emits it to new subscribers. StateSignal<T> adds a mutable Value setter and Changed. |
ReplaySubject<T> |
ReplaySignal<T> |
Replays buffered values by size and/or time window. |
AsyncSubject<T> |
AsyncSignal<T> |
Awaitable subject-like signal; also implements IAwaitSignal<T>. |
ReactiveProperty<T> / state holder |
StateSignal<T> plus ReadOnlyState<T> |
Mutable state and read-only projected state. |
State example:
using Minimalist.Reactive;
using Minimalist.Reactive.Signals;
var temperature = new StateSignal<double>(21.5);
ReadOnlyState<string> status = temperature.ToReadOnlyState(value =>
value >= 25.0 ? "warm" : "normal");
using var stateSubscription = status.Changed.Subscribe(Console.WriteLine);
temperature.Value = 26.2;
temperature.Refresh();Replay example:
using Minimalist.Reactive;
using Minimalist.Reactive.Signals;
var replay = new ReplaySignal<string>(bufferSize: 2);
replay.OnNext("A");
replay.OnNext("B");
replay.OnNext("C");
using var subscription = replay.Subscribe(Console.WriteLine); // replays B, CError and completion example:
using Minimalist.Reactive;
using Minimalist.Reactive.Signals;
IObservable<int> failed = Signal.Throw<int>(new InvalidOperationException("not available"));
using var subscription = failed.Subscribe(
value => Console.WriteLine(value),
error => Console.WriteLine($"failed: {error.Message}"),
() => Console.WriteLine("completed"));Schedulers live in Minimalist.Reactive.Concurrency and implement IScheduler.
| Scheduler | Purpose |
|---|---|
Scheduler.Immediate / ImmediateScheduler.Instance |
Execute work immediately. |
Scheduler.CurrentThread / CurrentThreadScheduler.Instance |
Queue recursive/current-thread work deterministically. |
ThreadPoolScheduler.Instance |
Schedule work through the thread pool. |
TaskPoolScheduler.Instance |
Schedule work through tasks. |
DispatcherScheduler |
Schedule onto a WPF dispatcher on Windows TFMs. |
VirtualClock / TestClock |
Virtual-time scheduling for deterministic tests. |
Scheduling APIs include absolute, relative, recursive, and action-based overloads:
using Minimalist.Reactive.Concurrency;
IDisposable scheduled = ThreadPoolScheduler.Instance.Schedule(
TimeSpan.FromMilliseconds(100),
() => Console.WriteLine("scheduled work"));
scheduled.Dispose();Use virtual clocks for deterministic time-sensitive tests rather than sleeping a real thread.
The base package includes two bridge generators as analyzers:
- System.Reactive bridge generator.
- R3 bridge generator.
The generators always emit small internal marker attributes. They emit bridge extension methods only when the consumer project already references the relevant external library:
- System.Reactive bridge checks for
System.Reactive.Linq.Observable. - R3 bridge checks for
R3.Observable<T>.
Generated bridge namespaces:
Minimalist.Reactive.SystemReactiveBridgeMinimalist.Reactive.R3Bridge
Generated System.Reactive bridge methods:
AsMinimalistSignal<T>(this System.IObservable<T> source)AsSystemObservable<T>(this System.IObservable<T> source)
Generated R3 bridge methods:
AsMinimalistSignal<T>(this R3.Observable<T> source)AsR3Observable<T>(this System.IObservable<T> source)
System.Reactive bridge example, when the consuming project already references System.Reactive:
using Minimalist.Reactive;
using Minimalist.Reactive.Signals;
using Minimalist.Reactive.SystemReactiveBridge;
using System.Reactive.Linq;
IObservable<int> rxSource = Observable.Range(1, 3);
IObservable<int> minimalistSource = rxSource.AsMinimalistSignal();
using var subscription = minimalistSource
.Map(value => value * 10)
.Subscribe(Console.WriteLine);
IObservable<int> systemObservable = Signal.Range(1, 3).AsSystemObservable();R3 bridge example, when the consuming project already references R3:
using Minimalist.Reactive;
using Minimalist.Reactive.R3Bridge;
using Minimalist.Reactive.Signals;
// R3.Observable<int> r3Source = ...;
// IObservable<int> minimalistSource = r3Source.AsMinimalistSignal();
// R3.Observable<int> r3Again = Signal.Range(1, 3).AsR3Observable();The R3 snippet is intentionally shown as a migration shape because it requires the consuming application to reference R3. Minimalist.Reactive itself remains free of an R3 runtime dependency.
Minimalist.Reactive 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 Minimalist naming. Migrate one vertical slice at a time: factories first, then subject/state types, then operators and schedulers.
| System.Reactive | Minimalist.Reactive | Notes |
|---|---|---|
Observable.Return(value) |
Signal.Return(value) |
Emits one value and completes. |
Observable.Empty<T>() |
Signal.Empty<T>() |
Completes immediately. |
Observable.Never<T>() |
Signal.Never<T>() or Signal.Never<T>(witness) |
Non-terminating signal; witness overload helps type inference. |
Observable.Throw<T>(ex) |
Signal.Throw<T>(ex) |
Emits terminal error. |
Observable.Range(start, count) |
Signal.Range(start, count) |
Optional scheduler overload exists. |
Observable.Repeat(value) |
Signal.Repeat(value) |
Indefinite repeat. |
Observable.Repeat(value, count) |
Signal.Repeat(value, count) |
Fixed repeat. |
Observable.Defer(factory) |
Signal.Defer(factory) |
Create source 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.Timer(dueTime) or Signal.After(dueTime) |
Emits long tick 0. |
Observable.Timer(dueTime, period) |
Signal.Timer(dueTime, period) |
Periodic long ticks. |
Observable.Interval(period) |
Signal.Interval(period) or Signal.Every(period) |
Repeating ticks. |
ToObservable() from enumerable |
Signal.FromEnumerable(values) or values.ToSignal() |
ToSignal extension is available. |
| task conversion | Signal.FromTask(task) |
Function-based task signals also exist. |
| System.Reactive | Minimalist.Reactive | Migration detail |
|---|---|---|
new Subject<T>() |
new Signal<T>() |
Use OnNext, OnError, OnCompleted, and Subscribe. |
new BehaviorSubject<T>(initial) |
new BehaviourSignal<T>(initial) |
Keeps Value getter and emits latest value to subscribers. |
| mutable reactive property | new 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 AsyncSignal<T>() |
Awaitable signal shape. |
| System.Reactive | Minimalist.Reactive | Notes |
|---|---|---|
Select |
Map |
Prefer Map for distinct Minimalist style. |
Where |
Keep or Where |
Where delegates to Keep. |
SelectMany |
SelectMany or Bind |
Bind is the Minimalist alias. |
Aggregate |
Fold |
Emits final accumulated value on completion. |
Scan |
Scan |
Emits every accumulated value. |
Do |
Tap |
Side effect while preserving values. |
Take / Skip |
Take / Skip |
Count-based overloads. |
TakeWhile / SkipWhile |
TakeWhile / SkipWhile |
Predicate-based. |
Distinct |
Distinct |
Full seen-set distinct. |
DistinctUntilChanged |
DistinctUntilChanged |
Adjacent dedupe. |
OfType / Cast |
OfType / Cast |
Object-source projections. |
Materialize |
Sparkify |
Converts notifications into Spark<T>. |
Dematerialize |
Unspark |
Converts Spark<T> values back into notifications. |
Merge |
Merge or Signal.Merge |
Works over source-of-sources and params factories. |
Concat |
Concat or Signal.Concat |
Sequential composition. |
Amb |
Race |
First source to produce a value or terminal signal wins. |
Switch |
Switch |
Latest inner observable wins. |
Zip |
Zip or Signal.Zip |
Pair values by index. |
CombineLatest |
CombineLatest or Signal.CombineLatest |
Latest values after both sources have emitted. |
WithLatestFrom |
WithLatest |
Left emission paired with latest right value. |
ForkJoin |
ForkJoin |
Last values after completion. |
Throttle |
Throttle |
Quiet-period emission. |
Sample |
Sample |
Periodic latest-value sampling. |
Delay |
Delay |
Delay emitted values. |
DelaySubscription |
DelayStart |
Delay source subscription. |
Timeout |
Timeout |
Error on missing value before due time. |
Buffer(count) |
Buffer(count) |
Fixed-size buffers. |
ToList / ToArray |
CollectList / CollectArray |
Signal results. |
FirstAsync |
FirstAsync |
Task result. |
| System.Reactive | Minimalist.Reactive |
|---|---|
Disposable.Create |
Disposable.Create |
Disposable.Empty |
Disposable.Empty |
BooleanDisposable |
BooleanDisposable |
CancellationDisposable |
CancellationDisposable |
CompositeDisposable |
MultipleDisposable or Pocket |
SerialDisposable |
SingleReplaceableDisposable or Slot |
SingleAssignmentDisposable |
SingleDisposable or AssignmentSlot |
IDisposable.Dispose() |
unchanged |
| System.Reactive scheduler concept | Minimalist.Reactive scheduler |
|---|---|
ImmediateScheduler.Instance |
Scheduler.Immediate or ImmediateScheduler.Instance |
CurrentThreadScheduler.Instance |
Scheduler.CurrentThread or CurrentThreadScheduler.Instance |
ThreadPoolScheduler.Instance |
ThreadPoolScheduler.Instance |
| task-pool scheduling | TaskPoolScheduler.Instance |
| dispatcher scheduling | DispatcherScheduler |
TestScheduler / virtual time |
VirtualClock or TestClock |
System.Reactive test code commonly uses TestScheduler and marble helpers. Minimalist.Reactive currently exposes virtual-time primitives rather than cloning the full Rx testing API. Prefer repository-native tests that:
- Use
TestClock/VirtualClockfor deterministic scheduling. - Assert values collected through
Subscribedelegates. - Dispose subscriptions explicitly.
- Use
CollectArrayAsync,CollectListAsync, orFirstAsyncwhen a task-shaped assertion is clearer.
R3 uses its own Observable<T> type and observer model. Minimalist.Reactive stays on the BCL IObservable<T> shape for runtime interoperability.
| R3 concept | Minimalist.Reactive equivalent |
|---|---|
R3.Observable<T> |
BCL IObservable<T> from Minimalist.Reactive factories/operators. |
| R3 subject | Signal<T> / StateSignal<T> / ReplaySignal<T> depending on state/replay needs. |
R3 Select / Where |
Map / Keep. |
| R3 time operators | Signal.Timer, Signal.Interval, Throttle, Sample, Delay, scheduler overloads. |
| R3 bridge | Generated AsMinimalistSignal / AsR3Observable when R3 is referenced by the consumer. |
Use the generated bridge only at boundaries. Prefer native Minimalist.Reactive operators inside new code.
Benchmarks live in src/Minimalist.Reactive.Benchmarks. The benchmark project may reference System.Reactive and R3 to compare throughput and allocation behavior; the production package must not.
Recovered benchmark evidence in docs/PERFORMANCE.md records that the optimized Signal<T> single-subscriber dispatch path outperformed System.Reactive and R3 on focused subject throughput cases:
- Count=32: Minimalist.Reactive 88.21 ns / 208 B; System.Reactive 117.55 ns / 224 B; R3 139.75 ns / 232 B.
- Count=1024: Minimalist.Reactive 1,620.33 ns / 208 B; System.Reactive 1,751.44 ns / 224 B; R3 2,396.59 ns / 232 B.
Performance constraints used by the project:
- Preserve observer and terminal notification semantics.
- Preserve safe unsubscription and disposal behavior.
- Avoid reflection and dynamic code generation in runtime hot paths.
- Prefer sealed helpers, direct fast paths, and predictable branch behavior.
- Keep allocations minimal in emit loops and single-subscriber cases.
| Path | Purpose |
|---|---|
src/Minimalist.Reactive |
Production runtime library. |
src/Minimalist.Reactive.SystemReactiveBridge.Generator |
Source generator for System.Reactive bridge adapters. |
src/Minimalist.Reactive.R3Bridge.Generator |
Source generator for R3 bridge adapters. |
src/Minimalist.Reactive.Tests |
Test project using Microsoft Testing Platform/TUnit-style validation. |
src/Minimalist.Reactive.Benchmarks |
BenchmarkDotNet comparison harness. |
docs/API-COVERAGE.md |
Public API inventory and parity notes. |
docs/PERFORMANCE.md |
Benchmark plan and recovered benchmark evidence. |
docs/TASKLIST.md |
Project task/status notes. |
docs/research |
System.Reactive and R3 API inventory research. |
From WSL, use Windows dotnet for this repository:
"/mnt/c/Program Files/dotnet/dotnet.exe" restore src/Minimalist.Reactive.sln
"/mnt/c/Program Files/dotnet/dotnet.exe" build src/Minimalist.Reactive.sln --configuration Release --no-restore
"/mnt/c/Program Files/dotnet/dotnet.exe" test --project src/Minimalist.Reactive.Tests/Minimalist.Reactive.Tests.csproj --configuration Release --no-build -- --minimum-expected-tests 1
"/mnt/c/Program Files/dotnet/dotnet.exe" pack src/Minimalist.Reactive/Minimalist.Reactive.csproj --configuration Release --no-restore -v minimal
git diff --checkTo run the focused benchmark used by the performance notes:
"/mnt/c/Program Files/dotnet/dotnet.exe" run --project src/Minimalist.Reactive.Benchmarks/Minimalist.Reactive.Benchmarks.csproj --configuration Release --no-build -- --filter '*SubjectThroughput*'For NuGet package verification, inspect the generated .nupkg and confirm:
README.mdis present.- The nuspec contains
<readme>README.md</readme>. - Bridge generator DLLs are present under
analyzers/dotnet/cs. - Production runtime dependencies do not include System.Reactive or R3.
- Replace subject construction with
Signal<T>,StateSignal<T>, orReplaySignal<T>depending on current behavior. - Replace factories:
Observable.Return/Empty/Throw/Timer/IntervaltoSignal.Return/Empty/Throw/Timer/Interval. - Replace hot-path operators with Minimalist names:
Select -> Map,Where -> Keep,Do -> Tap,Aggregate -> Fold,Amb -> Race. - Replace composite/serial disposables with
MultipleDisposable/PocketandSingleReplaceableDisposable/Slot. - Keep System.Reactive/R3 at application boundaries only when required; use generated bridge methods when those packages are already referenced.
- Run build, tests, pack, and
git diff --checkbefore publishing or merging.
Minimalist.Reactive is licensed under the MIT license. See LICENSE for details.