Skip to content

reactivemarbles/Minimalist.Reactive

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

82 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Minimalist.Reactive

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.

Goals and design posture

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 of Subject<T>, Map instead of only Select, Keep instead of only Where, Spark instead 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.

Install

When the package is available on your configured NuGet feed:

dotnet add package Minimalist.Reactive

Then 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.dll
  • Minimalist.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.

Target frameworks and dependencies

The production library targets:

  • net462
  • net472
  • net481
  • net9.0-windows10.0.19041.0
  • net10.0-windows10.0.19041.0
  • net9.0-ios
  • net9.0-tvos
  • net9.0-macos
  • net9.0-maccatalyst
  • net10.0-ios
  • net10.0-tvos
  • net10.0-macos
  • net10.0-maccatalyst
  • net9.0-android
  • net10.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.

Core model

Signal<T>

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(...) 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

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.

Disposables, handles, and slots

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 factories

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

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.

Transformation and filtering

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);

Composition

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;

Time, buffering, and async helpers

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 materialization

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();

Stateful signals and subject-like types

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, C

Error 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

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.

Source-generator bridge behavior

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.SystemReactiveBridge
  • Minimalist.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.

System.Reactive to Minimalist.Reactive migration guide

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.

Factory mapping

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.

Subject/state mapping

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.

Operator mapping

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.

Disposable mapping

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

Scheduler mapping

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

Testing migration

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 / 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.

R3 migration notes

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 and performance posture

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.

Repository layout

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.

Validation commands

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 --check

To 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.md is 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.

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.Return/Empty/Throw/Timer/Interval.
  3. Replace hot-path operators with Minimalist names: Select -> Map, Where -> Keep, Do -> Tap, Aggregate -> Fold, Amb -> Race.
  4. Replace composite/serial disposables with MultipleDisposable/Pocket and SingleReplaceableDisposable/Slot.
  5. Keep System.Reactive/R3 at application boundaries only when required; use generated bridge methods when those packages are already referenced.
  6. Run build, tests, pack, and git diff --check before publishing or merging.

License

Minimalist.Reactive is licensed under the MIT license. See LICENSE for details.

About

No description, website, or topics provided.

Resources

License

Stars

1 star

Watchers

3 watching

Forks

Sponsor this project

 

Packages

 
 
 

Contributors

Languages