ReactiveExtensionsSharp 0.3.0
dotnet add package ReactiveExtensionsSharp --version 0.3.0
NuGet\Install-Package ReactiveExtensionsSharp -Version 0.3.0
<PackageReference Include="ReactiveExtensionsSharp" Version="0.3.0" />
<PackageVersion Include="ReactiveExtensionsSharp" Version="0.3.0" />
<PackageReference Include="ReactiveExtensionsSharp" />
paket add ReactiveExtensionsSharp --version 0.3.0
#r "nuget: ReactiveExtensionsSharp, 0.3.0"
#:package ReactiveExtensionsSharp@0.3.0
#addin nuget:?package=ReactiveExtensionsSharp&version=0.3.0
#tool nuget:?package=ReactiveExtensionsSharp&version=0.3.0
ReactiveExtensionsSharp
A .NET port of RxJS — same operators, same semantics, same names you already know, in idiomatic C#.
Why a new Rx library when Rx.NET exists? ReactiveExtensionsSharp isn't trying to replace it — it's a deliberately faithful port of RxJS specifically, built to make it painless to bring JS reactive code (and the libraries built on it, like Puppeteer) over to .NET without re-learning a different Rx dialect. If you know pipe(map(...), filter(...), takeUntil(...)), you already know ReactiveExtensionsSharp.
Quick taste
// RxJS
import { fromEvent, interval } from 'rxjs';
import { map, filter, takeUntil } from 'rxjs/operators';
const clicks$ = fromEvent(button, 'click');
interval(1000)
.pipe(
map(x => x * x),
filter(x => x % 2 === 0),
takeUntil(clicks$),
)
.subscribe(x => console.log(x));
Same pipeline in ReactiveExtensionsSharp — real, compiling code:
<a id='snippet-quick-taste-csharp'></a>
var clicks = Observable.FromEvent<EventArgs>(h => button.Click += h, h => button.Click -= h);
Observable.Interval(TimeSpan.FromSeconds(1))
.Map(x => x * x)
.Filter(x => x % 2 == 0)
.TakeUntil(clicks)
.Subscribe(x => Console.WriteLine(x));
<sup><a href='https://github.com/hardkoded/ReactiveExtensions-Sharp/blob/main/test/ReactiveExtensionsSharp.Tests/Samples/QuickTasteSample.cs#L19-L27' title='Snippet source file'>snippet source</a> | <a href='#snippet-quick-taste-csharp' title='Start of snippet'>anchor</a></sup>
What about pipe?
RxJS's pipe(op1, op2, op3) is mostly just method chaining wearing a different hat — source.Map(...).Filter(...)
above is the translation. But RxJS's pipe() has a second job: called on its own (not as an Observable
method), it builds a reusable transformation out of several operators, so you can define it once and apply
it to multiple streams. ReactiveExtensionsSharp covers that with OperatorFunction<TSource, TResult> + Pipe:
<a id='snippet-pipe-csharp'></a>
public static void Run(Observable<int> numbersA, Observable<int> numbersB)
{
// A reusable transformation, defined once - the equivalent of RxJS's standalone
// `const squareAndFilterEven = pipe(map(x => x * x), filter(x => x % 2 === 0));`
OperatorFunction<int, int> squareAndFilterEven = source => source.Map(x => x * x).Filter(x => x % 2 == 0);
numbersA.Pipe(squareAndFilterEven).Subscribe(x => Console.WriteLine(x));
numbersB.Pipe(squareAndFilterEven).Subscribe(x => Console.WriteLine(x));
}
<sup><a href='https://github.com/hardkoded/ReactiveExtensions-Sharp/blob/main/test/ReactiveExtensionsSharp.Tests/Samples/PipeSample.cs#L11-L21' title='Snippet source file'>snippet source</a> | <a href='#snippet-pipe-csharp' title='Start of snippet'>anchor</a></sup>
There's no hand-written 9-arity pipe(op1, op2, ..., op9) overload set, deliberately — it exists in RxJS mainly
to work around JS not having method chaining with generics the way C# does. Pipe here only takes a single,
already-composed OperatorFunction; build that function with ordinary chaining, same as everywhere else.
The example that started this project
Puppeteer (the JS browser-automation library) builds its Locator.click()/.fill() actions
on exactly one rxjs combinator: retry a flaky DOM action, racing the whole thing against a timeout and a
cancellation signal. Its own wrapper for this is pipe(retry({delay}), raceWith(fromAbortSignal(...), timeout(...))) — three operators composed once, reused everywhere it needs "keep trying until this works, but
don't wait forever." Puppeteer's own .NET port, puppeteer-sharp, doesn't have that composition available, so
the equivalent logic there is a hand-rolled while(true) loop with a linked CancellationTokenSource and five
catch clauses to tell "timed out" apart from "cancelled" apart from "just retry."
ReactiveExtensionsSharp ports that exact combinator as RetryAndRaceWithSignalAndTimer, proven against a real launched Chrome:
<a id='snippet-retry-until-timeout-csharp'></a>
// Retries a flaky async operation - "find an element that may not have rendered yet" is the
// Puppeteer case, but this works for any operation that fails until some condition is met -
// until it succeeds, times out, or the caller cancels.
public static async Task<string> FindElementOnceItRendersAsync(Func<Task<string>> tryFindElement, CancellationToken cancellationToken)
=> await Observable.Defer(() => Observable.From(tryFindElement()))
.RetryAndRaceWithSignalAndTimer(TimeSpan.FromSeconds(5), cancellationToken)
.ConfigureAwait(false);
<sup><a href='https://github.com/hardkoded/ReactiveExtensions-Sharp/blob/main/test/ReactiveExtensionsSharp.Tests/Samples/RetryUntilTimeoutSample.cs#L13-L21' title='Snippet source file'>snippet source</a> | <a href='#snippet-retry-until-timeout-csharp' title='Start of snippet'>anchor</a></sup>
Install
dotnet add package ReactiveExtensionsSharp
Targets netstandard2.0, net8.0, and net10.0.
Contributing
Every operator PR follows the same recipe: find its spec in upstream RxJS (spec/operators/*-spec.ts or spec/observables/*-spec.ts at tag 7.8.2), port the test cases first, then implement until green. See CLAUDE.md for the full set of conventions.
License
MIT
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net5.0 was computed. net5.0-windows was computed. net6.0 was computed. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 was computed. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
| .NET Core | netcoreapp2.0 was computed. netcoreapp2.1 was computed. netcoreapp2.2 was computed. netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
| .NET Standard | netstandard2.0 is compatible. netstandard2.1 was computed. |
| .NET Framework | net461 was computed. net462 was computed. net463 was computed. net47 was computed. net471 was computed. net472 was computed. net48 was computed. net481 was computed. |
| MonoAndroid | monoandroid was computed. |
| MonoMac | monomac was computed. |
| MonoTouch | monotouch was computed. |
| Tizen | tizen40 was computed. tizen60 was computed. |
| Xamarin.iOS | xamarinios was computed. |
| Xamarin.Mac | xamarinmac was computed. |
| Xamarin.TVOS | xamarintvos was computed. |
| Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETStandard 2.0
- No dependencies.
-
net10.0
- No dependencies.
-
net8.0
- No dependencies.
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.