You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
A focused collection of high–value Reactive Extensions (Rx) operators that do not ship with System.Reactive but are commonly needed when building reactive .NET applications.
Reference the project directly while developing locally.
Quick Start
usingSystem;usingSystem.Reactive.Linq;usingReactiveUI.Extensions;varsource=Observable.Interval(TimeSpan.FromMilliseconds(120)).Take(10).Select(i =>(long?)(i%3==0?null:i));// 1. Filter nulls + convert to a Unit signal.varsignal=source.WhereIsNotNull().AsSignal();// 2. Add a heartbeat if the upstream goes quiet for 500ms.varwithHeartbeat=source.WhereIsNotNull().Heartbeat(TimeSpan.FromMilliseconds(500),Scheduler.Default);// 3. Retry with exponential backoff up to 5 times.varresilient=Observable.Defer(()=>Observable.Throw<long>(newInvalidOperationException("Boom"))).RetryWithBackoff(maxRetries:5,initialDelay:TimeSpan.FromMilliseconds(100));// 4. Conflate bursty updates.varconflated=source.Conflate(TimeSpan.FromMilliseconds(300),Scheduler.Default);using(conflated.Subscribe(Console.WriteLine)){Console.ReadLine();}
API Catalog
Below is the full list of extension methods (grouped logically).
Some overloads omitted for brevity.
// Shared timer for a given period (one underlying timer per distinct TimeSpan)varsharedTimer=ReactiveExtensions.SyncTimer(TimeSpan.FromSeconds(1));// Delay emission of a single value42.Schedule(TimeSpan.FromMilliseconds(250),Scheduler.Default).Subscribe(v =>Console.WriteLine($"Delayed: {v}"));// Safe scheduling when a scheduler may be nullIScheduler?maybeScheduler=null;maybeScheduler.ScheduleSafe(()=>Console.WriteLine("Ran inline"));// ThrottleFirst: allow first item per window, ignore restvarthrottled=Observable.Interval(TimeSpan.FromMilliseconds(50)).ThrottleFirst(TimeSpan.FromMilliseconds(200));// DebounceImmediate: emit first immediately then debounce restvardebounced=Observable.Interval(TimeSpan.FromMilliseconds(40)).DebounceImmediate(TimeSpan.FromMilliseconds(250));// ThrottleDistinct: throttle but only emit when the value actually changesvarsource=Observable.Interval(TimeSpan.FromMilliseconds(50)).Take(20);vardistinctThrottled=source.ThrottleDistinct(TimeSpan.FromMilliseconds(200));
Inactivity / Liveness
// Heartbeat emits IHeartbeat<T> where IsHeartbeat == true during quiet periodsvarheartbeats=Observable.Interval(TimeSpan.FromMilliseconds(400)).Take(5).Heartbeat(TimeSpan.FromMilliseconds(300),Scheduler.Default);// DetectStale emits IStale<T>: one stale marker after inactivity, or fresh update wrappersvarstaleAware=Observable.Timer(TimeSpan.Zero,TimeSpan.FromMilliseconds(500)).Take(3).DetectStale(TimeSpan.FromMilliseconds(300),Scheduler.Default);// BufferUntilInactive groups events separated by inactivityvarbursts=Observable.Interval(TimeSpan.FromMilliseconds(60)).Take(20);vargroups=bursts.BufferUntilInactive(TimeSpan.FromMilliseconds(200));
Error Handling & Resilience
varflaky=Observable.Create<int>(o =>{o.OnNext(1);o.OnError(newInvalidOperationException("Fail"));return()=>{};});// Ignore all errors and complete silentlyvarflakySafe=flaky.CatchIgnore();// Replace error with a fallback valuevarwithFallback=flaky.CatchAndReturn(-1);// Retry only specific exception type with loggingvarretried=flaky.OnErrorRetry<int,InvalidOperationException>(ex =>Console.WriteLine(ex.Message),retryCount:3);// Retry with exponential backoffvarbackoff=flaky.RetryWithBackoff(maxRetries:5,initialDelay:TimeSpan.FromMilliseconds(100));
IObservable<int>inputs=Observable.Range(1,5);// Sequential (preserves order)varseq=inputs.SelectAsyncSequential(async i =>{awaitTask.Delay(50);returni*2;});// Latest only (cancels previous)varlatest=inputs.SelectLatestAsync(async i =>{awaitTask.Delay(100);returni;});// Limited parallelismvarconcurrent=inputs.SelectAsyncConcurrent(async i =>{awaitTask.Delay(100);returni;},maxConcurrency:2);// Asynchronous subscription (serializing tasks)inputs.SubscribeAsync(async i =>awaitTask.Delay(10));// Synchronous gate: ensures per-item async completion before next is emittedinputs.SubscribeSynchronous(async i =>awaitTask.Delay(25));// ToHotTask: convert an observable to a Task that starts immediatelyvarsource=Observable.Return(42);vartask=source.ToHotTask();varresult=awaittask;// 42
Backpressure / Conflation
// Conflate: enforce minimum spacing between emissions while always outputting the most recent valuevarnoisy=Observable.Interval(TimeSpan.FromMilliseconds(20)).Take(30);varconflated=noisy.Conflate(TimeSpan.FromMilliseconds(200),Scheduler.Default);
Selective & Conditional Emission
// TakeUntil predicate (inclusive)varuntilFive=Observable.Range(1,100).TakeUntil(x =>x==5);// WaitUntil first match then completevarfirstEven=Observable.Range(1,10).WaitUntil(x =>x%2==0);// SampleLatest: sample the latest value whenever a trigger firesvarsource=Observable.Interval(TimeSpan.FromMilliseconds(100)).Take(10);vartrigger=Observable.Interval(TimeSpan.FromMilliseconds(300)).Take(3);varsampled=source.SampleLatest(trigger);// SwitchIfEmpty: provide a fallback if the source completes without emittingvarempty=Observable.Empty<int>();varfallback=Observable.Return(42);varresult=empty.SwitchIfEmpty(fallback);// emits 42// DropIfBusy: drop values if the previous async operation is still runningvarinputs=Observable.Range(1,5);varprocessed=inputs.DropIfBusy(async x =>{awaitTask.Delay(200);Console.WriteLine(x);});
Buffering & Transformation
// BufferUntil - collect chars between delimitersvarchars="<a><bc><d>".ToCharArray().ToObservable();varframes=chars.BufferUntil('<','>');// emits "<a>", "<bc>", "<d>"// Shuffle arrays in-placevararrays=Observable.Return(new[]{1,2,3,4,5});varshuffled=arrays.Shuffle();// BufferUntilIdle: emit a batch when the stream goes quietvarevents=Observable.Interval(TimeSpan.FromMilliseconds(100)).Take(10);varbatches=events.BufferUntilIdle(TimeSpan.FromMilliseconds(250));// Pairwise: emit consecutive pairsvarnumbers=Observable.Range(1,5);varpairs=numbers.Pairwise();// emits (1,2), (2,3), (3,4), (4,5)// ScanWithInitial: scan that always emits the initial value firstvarvalues=Observable.Return(5);varaccumulated=values.ScanWithInitial(10,(acc,x)=>acc+x);// emits 10, then 15
Subscription & Side Effects
varstream=Observable.Range(1,3).DoOnSubscribe(()=>Console.WriteLine("Subscribed")).DoOnDispose(()=>Console.WriteLine("Disposed"));using(stream.Subscribe(Console.WriteLine)){// auto dispose at using end}
Utility & Miscellaneous
// Emit list contents quickly with low allocationsvarlistSource=Observable.Return<IEnumerable<int>>(newList<int>{1,2,3});listSource.ForEach().Subscribe(Console.WriteLine);// Using helper for deterministic disposalvarvalue=newMemoryStream().Using(ms =>ms.Length);// While loop (reactive)varcounter=0;ReactiveExtensions.While(()=>counter++<3,()=>Console.WriteLine(counter)).Subscribe();// Batch push with OnNext paramsvarsubj=newSubject<int>();subj.OnNext(1,2,3,4);// ToReadOnlyBehavior: create a read-only behavior subjectvar(observable,observer)=ReactiveExtensions.ToReadOnlyBehavior(10);observer.OnNext(20);// observable emits 10, then 20// ToPropertyObservable: observe property changes on INotifyPropertyChangedpublicclassViewModel:INotifyPropertyChanged{privatestring_name;publicstringName{get=>_name;set{_name=value;PropertyChanged?.Invoke(this,newPropertyChangedEventArgs(nameof(Name)));}}publiceventPropertyChangedEventHandler?PropertyChanged;}varvm=newViewModel();varnameChanges=vm.ToPropertyObservable(x =>x.Name);vm.Name="Hello";// observable emits "Hello"
Async Observables (IObservableAsync<T>)
A fully async-native observable framework that mirrors the System.Reactive programming model but uses ValueTask, CancellationToken, and IAsyncDisposable throughout. Every observer callback (OnNextAsync, OnErrorResumeAsync, OnCompletedAsync) is asynchronous, enabling true end-to-end async/await pipelines without blocking threads.
Key design goals:
Async all the way down – no synchronous observer callbacks; every notification is ValueTask-based
Cancellation-first – every operator and subscription accepts CancellationToken
Like Create, but the subscribe delegate runs as a background job, returning immediately.
ObservableAsync.Return<T>(value)
Emits a single value then completes.
ObservableAsync.Empty<T>()
Completes immediately without emitting any values.
ObservableAsync.Never<T>()
Never emits and never completes.
ObservableAsync.Throw<T>(exception)
Completes immediately with the specified error.
ObservableAsync.Range(start, count)
Emits a sequence of integers.
ObservableAsync.Interval(period)
Emits incrementing long values at a regular interval. Accepts optional TimeProvider.
ObservableAsync.Timer(dueTime)
Emits 0L after the specified delay. Supports one-shot and periodic overloads. Accepts optional TimeProvider.
ObservableAsync.Defer<T>(factory)
Defers creation of the observable until subscription time.
ObservableAsync.FromAsync<T>(func)
Wraps an async function as a single-element observable.
task.ToObservableAsync()
Converts a Task<T> to an async observable.
asyncEnumerable.ToObservableAsync()
Converts an IAsyncEnumerable<T> to an async observable.
enumerable.ToObservableAsync()
Converts an IEnumerable<T> to an async observable.
// Create from factory delegatevarcustom=ObservableAsync.Create<string>(async(observer,ct)=>{awaitobserver.OnNextAsync("Hello",ct);awaitobserver.OnNextAsync("World",ct);awaitobserver.OnCompletedAsync(Result.Success);returnDisposableAsync.Empty;});// From a TaskvarfromTask=Task.FromResult(42).ToObservableAsync();// From IAsyncEnumerableasyncIAsyncEnumerable<int>GenerateAsync(){for(inti=0;i<5;i++){awaitTask.Delay(50);yieldreturni;}}varfromAsyncEnum=GenerateAsync().ToObservableAsync();// Periodic timer (with TimeProvider support)varticks=ObservableAsync.Interval(TimeSpan.FromSeconds(1));// Deferred creationvardeferred=ObservableAsync.Defer<int>(()=>ObservableAsync.Return(DateTime.Now.Second));
Transformation Operators
Operator
Description
Select(selector)
Projects each element using a synchronous transform function.
Select(asyncSelector)
Projects each element using an async transform function.
SelectMany(selector)
Flattens nested async observables produced by a projection. Multiple overloads for collection selectors.
Scan(seed, accumulator)
Applies an accumulator over the sequence, emitting each intermediate result. Sync and async overloads.
Cast<TResult>()
Casts each element to the specified type.
OfType<TResult>()
Filters elements that are assignable to the specified type.
GroupBy(keySelector)
Groups elements by key, emitting GroupedAsyncObservable<TKey, T> per group.
varsource=ObservableAsync.Range(1,5);// Sync projectionvardoubled=source.Select(x =>x*2);// Async projectionvarprojected=source.Select(async(x,ct)=>{awaitTask.Delay(10,ct);returnx.ToString();});// Flat map: each item produces a sub-sequencevarflattened=source.SelectMany(x =>ObservableAsync.Range(x*10,3));// Running total with ScanvarrunningTotal=source.Scan(0,(acc,x)=>acc+x);// 1, 3, 6, 10, 15// Group by even/oddvargrouped=source.GroupBy(x =>x%2==0?"even":"odd");
Filtering Operators
Operator
Description
Where(predicate)
Filters elements using a synchronous predicate.
Where(asyncPredicate)
Filters elements using an async predicate.
Take(count)
Takes the first N elements then completes.
Skip(count)
Skips the first N elements.
TakeWhile(predicate)
Takes elements while the predicate holds, then completes.
SkipWhile(predicate)
Skips elements while the predicate holds, then emits the rest.
TakeUntil(other)
Takes elements until another async observable, Task, CancellationToken, or predicate signals.
Distinct()
Emits only elements not previously seen.
DistinctBy(keySelector)
Emits only elements with unique keys.
DistinctUntilChanged()
Suppresses consecutive duplicate elements.
DistinctUntilChangedBy(keySelector)
Suppresses consecutive elements with the same key.
Catches errors and switches to a fallback async observable.
CatchAndIgnoreErrorResume()
Catches error-resume notifications and suppresses them, allowing the sequence to continue.
Retry()
Re-subscribes to the source indefinitely on error.
Retry(count)
Re-subscribes up to count times on error.
OnErrorResumeAsFailure()
Converts OnErrorResumeAsync calls into OnCompletedAsync with a Result.Failure.
// Catch and switch to fallbackvarflaky=ObservableAsync.Throw<int>(newInvalidOperationException("Oops"));varsafe=flaky.Catch(ex =>ObservableAsync.Return(-1));// emits -1// Retry up to 3 timesvarretried=flaky.Retry(3);// Retry indefinitelyvarforever=flaky.Retry();// Convert error-resume to failure completionvarstrict=someSource.OnErrorResumeAsFailure();
Timing & Scheduling (Async)
Operator
Description
Throttle(timeSpan)
Suppresses values that arrive within the specified window after the last emission. Accepts optional TimeProvider.
Delay(timeSpan)
Delays every emission by the specified duration. Accepts optional TimeProvider.
Timeout(timeSpan)
Raises a TimeoutException if no value arrives within the specified window.
Timeout(timeSpan, fallback)
Switches to a fallback source on timeout instead of faulting.
ObserveOn(context)
Shifts observer notifications to the specified AsyncContext, SynchronizationContext, TaskScheduler, or IScheduler.
Yield()
Yields control (via Task.Yield) between each notification, ensuring other work can interleave.
varsource=ObservableAsync.Interval(TimeSpan.FromMilliseconds(50)).Take(10);// Throttle rapid emissions (debounce)varthrottled=source.Throttle(TimeSpan.FromMilliseconds(200));// Delay each value by 100msvardelayed=source.Delay(TimeSpan.FromMilliseconds(100));// Timeout if nothing arrives in 2 secondsvarguarded=source.Timeout(TimeSpan.FromSeconds(2));// Timeout with fallbackvarwithFallback=source.Timeout(TimeSpan.FromSeconds(2),ObservableAsync.Return(999L));// Schedule notifications onto a specific contextvaronContext=source.ObserveOn(newAsyncContext(SynchronizationContext.Current!));// Yield between each notificationvaryielded=source.Yield();
Aggregation & Element Operators
Operator
Description
AggregateAsync(seed, accumulator)
Applies an accumulator over the sequence and returns the final result.
CountAsync()
Returns the number of elements.
LongCountAsync()
Returns the element count as long.
AnyAsync() / AnyAsync(predicate)
Returns true if the sequence has any elements (optionally matching a predicate).
AllAsync(predicate)
Returns true if all elements match the predicate.
ContainsAsync(value)
Returns true if the sequence contains the specified value.
FirstAsync()
Returns the first element or throws if empty.
FirstOrDefaultAsync()
Returns the first element or default if empty.
LastAsync()
Returns the last element or throws if empty.
LastOrDefaultAsync()
Returns the last element or default if empty.
SingleAsync()
Returns the only element; throws if empty or more than one.
SingleOrDefaultAsync()
Returns the only element or default; throws if more than one.
ToListAsync()
Collects all elements into a List<T>.
ToDictionaryAsync(keySelector)
Collects all elements into a Dictionary<TKey, T>.
ForEachAsync(action)
Invokes an async action for each element.
WaitCompletionAsync()
Awaits the completion of the observable without capturing values.
varsource=ObservableAsync.Range(1,5);varct=CancellationToken.None;intcount=awaitsource.CountAsync(ct);// 5intsum=awaitsource.AggregateAsync(0,(a,x)=>a+x,ct);// 15boolhasEvens=awaitsource.AnyAsync(x =>x%2==0,ct);// trueboolallPos=awaitsource.AllAsync(x =>x>0,ct);// trueboolhas3=awaitsource.ContainsAsync(3,ct);// trueintfirst=awaitsource.FirstAsync(ct);// 1intlast=awaitsource.LastAsync(ct);// 5List<int>all=awaitsource.ToListAsync(ct);// [1, 2, 3, 4, 5]vardict=awaitsource.ToDictionaryAsync(x =>x.ToString(),ct);// {"1":1, "2":2, ...}// Iterate with async actionawaitsource.ForEachAsync(async(x,ct2)=>Console.WriteLine($"Item: {x}"),ct);// Wait for completion without capturing valuesawaitsource.WaitCompletionAsync(ct);
Side Effects & Lifecycle
Operator
Description
Do(onNext)
Performs a synchronous side effect for each element without altering the sequence.
Do(asyncOnNext)
Performs an async side effect for each element.
OnDispose(action)
Registers a synchronous action to run when the subscription is disposed.
OnDispose(asyncAction)
Registers an async action to run on disposal.
Using(resourceFactory, observableFactory)
Creates a disposable resource tied to the subscription lifetime.
varsource=ObservableAsync.Range(1,3);// Sync side effectvarlogged=source.Do(x =>Console.WriteLine($"[LOG] {x}"));// Async side effectvarasyncLogged=source.Do(async(x,ct)=>{awaitTask.Delay(1,ct);Console.WriteLine($"[ASYNC LOG] {x}");});// Cleanup on disposalvarwithCleanup=source.OnDispose(()=>Console.WriteLine("Cleaned up!"));// Async cleanupvarwithAsyncCleanup=source.OnDispose(async()=>{awaitTask.Delay(10);Console.WriteLine("Async cleanup done!");});// Using: tie a resource to the subscription lifetimevarwithResource=ObservableAsync.Using(()=>newMemoryStream(),
stream =>ObservableAsync.Return(stream.Length));
Multicasting & Sharing
Operator
Description
Publish()
Returns a ConnectableObservableAsync<T> that multicasts the source to multiple observers using a serial subject.
StatelessPublish()
Returns a connectable observable using a stateless serial subject.
ReplayLatest()
Returns a connectable observable that replays the most recent value to new subscribers.
RefCount()
Automatically connects a connectable observable when the first observer subscribes and disconnects when the last unsubscribes.
Multicast(subjectFactory)
General-purpose multicasting with a custom subject factory.
varsource=ObservableAsync.Interval(TimeSpan.FromMilliseconds(100)).Take(5);// Publish + explicit connectvarpublished=source.Publish();awaitusingvarsub1=awaitpublished.SubscribeAsync(async(v,ct)=>Console.WriteLine($"Sub1: {v}"),CancellationToken.None);awaitusingvarsub2=awaitpublished.SubscribeAsync(async(v,ct)=>Console.WriteLine($"Sub2: {v}"),CancellationToken.None);awaitusingvarconnection=awaitpublished.ConnectAsync(CancellationToken.None);// RefCount: auto-connect on first subscriber, auto-disconnect on lastvarshared=source.Publish().RefCount();awaitusingvarsub3=awaitshared.SubscribeAsync(async(v,ct)=>Console.WriteLine($"Shared: {v}"),CancellationToken.None);// ReplayLatest: new subscribers get the most recent value immediatelyvarreplayed=source.ReplayLatest().RefCount();
Bridging & Conversion
Method
Description
observable.ToObservableAsync()
Wraps a classic IObservable<T> as IObservableAsync<T>. Synchronous notifications are forwarded sequentially through async callbacks.
asyncObservable.ToObservable()
Exposes an IObservableAsync<T> as a classic IObservable<T>. Async callbacks are awaited; the synchronous observer is notified on the completing thread.
asyncObservable.ToAsyncEnumerable()
Converts an async observable to IAsyncEnumerable<T> for consumption with await foreach.
source.Wrap(converter)
Wraps each value using a converter function producing a new async observable type.
Subjects act as both an observer and an observable, enabling manual publishing of values to multiple subscribers.
Factory Method
Description
SubjectAsync.Create<T>()
Creates a subject with default options (serial publishing, stateful).
SubjectAsync.Create<T>(options)
Creates a subject with configurable publishing (Serial / Concurrent) and statefulness.
SubjectAsync.CreateBehavior<T>(startValue)
Creates a behavior subject that replays the latest value (starting with startValue) to new subscribers.
SubjectAsync.CreateReplayLatest<T>()
Creates a subject that replays only the most recent value to new subscribers (no initial value required).
Publishing options:
Option
Behavior
PublishingOption.Serial
Notifications are delivered to observers one at a time. Safe for single-producer scenarios.
PublishingOption.Concurrent
Notifications are delivered concurrently to all observers. Useful when observers are independent.
// Basic subjectvarsubject=SubjectAsync.Create<int>();awaitusingvarsub=awaitsubject.Values.SubscribeAsync(async(value,ct)=>Console.WriteLine($"Got: {value}"),CancellationToken.None);awaitsubject.OnNextAsync(1,CancellationToken.None);awaitsubject.OnNextAsync(2,CancellationToken.None);awaitsubject.OnCompletedAsync(Result.Success);// Behavior subject: new subscribers receive the current valuevarbehavior=SubjectAsync.CreateBehavior("initial");awaitusingvarbehaviorSub=awaitbehavior.Values.SubscribeAsync(async(value,ct)=>Console.WriteLine($"Behavior: {value}"),// prints "initial"CancellationToken.None);awaitbehavior.OnNextAsync("updated",CancellationToken.None);// prints "updated"// Concurrent publishing for fan-out scenariosvarconcurrent=SubjectAsync.Create<int>(newSubjectCreationOptions{PublishingOption=PublishingOption.Concurrent,IsStateless=false});// ReplayLatest: replays the last value without a required start valuevarreplay=SubjectAsync.CreateReplayLatest<double>();awaitreplay.OnNextAsync(3.14,CancellationToken.None);// Late subscriber still receives 3.14awaitusingvarlateSub=awaitreplay.Values.SubscribeAsync(async(v,ct)=>Console.WriteLine($"Replay: {v}"),CancellationToken.None);
Async Disposables
Type
Description
DisposableAsync.Empty
A no-op IAsyncDisposable – useful as a default or placeholder.
DisposableAsync.Create(func)
Creates an IAsyncDisposable from a Func<ValueTask> delegate. Ensures the delegate runs at most once.
CompositeDisposableAsync
A thread-safe collection of IAsyncDisposable objects that are disposed together. Supports Add, Remove, and bulk disposal.
// Create from a delegatevardisposable=DisposableAsync.Create(async()=>{awaitTask.Delay(10);Console.WriteLine("Resource released");});awaitdisposable.DisposeAsync();// Compose multiple async disposablesvarcomposite=newCompositeDisposableAsync();composite.Add(DisposableAsync.Create(()=>{Console.WriteLine("A");returndefault;}));composite.Add(DisposableAsync.Create(()=>{Console.WriteLine("B");returndefault;}));Console.WriteLine($"Count: {composite.Count}");// 2awaitcomposite.DisposeAsync();// disposes A and BConsole.WriteLine($"IsDisposed: {composite.IsDisposed}");// true
Performance Notes
FastForEach path avoids iterator allocations for List<T>, IList<T>, and arrays.
SyncTimer ensures only one shared timer per period reducing timer overhead.
Conflate helps tame high–frequency producers without dropping the final value of a burst.
Heartbeat and DetectStale use lightweight scheduling primitives.
Most operators avoid capturing lambdas in hot loops where practical.
Async observable operators use ValueTask throughout to minimize allocations on the hot path.
ObserverAsync<T> base class detects concurrent re-entrant calls, raising ConcurrentObserverCallsException in debug scenarios.
Subject variants (serial vs. concurrent, stateful vs. stateless) let you choose the right trade-off between safety and throughput.
Thread Safety
All operators are pure functional transformations unless documented otherwise.
SyncTimer uses a ConcurrentDictionary and returns a hot IConnectableObservable that connects once per unique TimeSpan.
Methods returning shared observables (SyncTimer, Partition result sequences) are safe for multi-subscriber usage unless the upstream is inherently side-effecting.
ISubjectAsync<T> subjects with PublishingOption.Serial serialize notifications; PublishingOption.Concurrent variants deliver to all observers concurrently.
CompositeDisposableAsync is thread-safe for concurrent Add/Remove/DisposeAsync calls.
License
MIT – see LICENSE file.
Contributing
Issues / PRs welcome. Please keep additions dependency–free and focused on broadly useful reactive patterns.
Change Log (Excerpt)
(Keep this section updated as the library evolves.)
Fixed SynchronizeSynchronous to properly propagate OnError and OnCompleted events.
Removed DisposeWith extension use System.Reactive.Disposables.Fluent from System.Reactive.
Added fully async-native observable framework (IObservableAsync<T>, IObserverAsync<T>) with 60+ operators, subjects, bridge extensions, and async disposables.
Happy reactive coding!
About
A focused collection of high–value Reactive Extensions (Rx) operators that do not ship with System.Reactive but are commonly needed when building reactive .NET applications.