Skip to main content

rx-observables

Use when writing, editing, or reviewing C# code that uses System.Reactive (Rx.NET); working with `IObservable`, `IObserver`, `Subject`, `BehaviorSubject`, `ReplaySubject`, or `AsyncSubject`; composing operators like `Select`, `Where`, `Merge`, `Switch`, `Publish`, `RefCount`, `Catch`, `Retry`, `ObserveOn`, or `SubscribeOn`; managing subscription lifecycle with `CompositeDisposable`, `SerialDisposable`, or `DisposeWith`; testing time-dependent reactive code with `TestScheduler` from `Microsoft.Reactive.Testing`; designing hot vs cold observables or enforcing the Rx serial-notification contract. Triggers on phrases like "observable stream", "reactive pipeline", "Rx.NET", "subject leak", "OnNext on wrong thread", "test scheduler", or edits touching files that import `System.Reactive`. Do NOT use for plain `IEnumerable` LINQ, `IAsyncEnumerable`, or TPL `Task`-based async.

Ir para a instalação

Informações da origem

Repositório
recyclarr/recyclarr
Última atividade na origem
18 de abril de 2026 às 17:21
Idioma detectado do SKILL.md
inglês
Estrelas
2.106
Forks
36

Opções de instalação

Por padrão, está selecionado o prompt que primeiro revisa a origem. Você pode mudar para um comando direto ou baixar uma cópia local.

Revise os arquivos de origem

Leia o SKILL.md e os arquivos complementares exibidos pelo SkillsMP antes de decidir se vai instalar.

Exibindo SKILL.md

SKILL.md
Instruções da origem · Visualização somente leitura
name
rx-observables
description
Use when writing, editing, or reviewing C# code that uses System.Reactive (Rx.NET); working with `IObservable`, `IObserver`, `Subject`, `BehaviorSubject`, `ReplaySubject`, or `AsyncSubject`; composing operators like `Select`, `Where`, `Merge`, `Switch`, `Publish`, `RefCount`, `Catch`, `Retry`, `ObserveOn`, or `SubscribeOn`; managing subscription lifecycle with `CompositeDisposable`, `SerialDisposable`, or `DisposeWith`; testing time-dependent reactive code with `TestScheduler` from `Microsoft.Reactive.Testing`; designing hot vs cold observables or enforcing the Rx serial-notification contract. Triggers on phrases like "observable stream", "reactive pipeline", "Rx.NET", "subject leak", "OnNext on wrong thread", "test scheduler", or edits touching files that import `System.Reactive`. Do NOT use for plain `IEnumerable` LINQ, `IAsyncEnumerable`, or TPL `Task`-based async.
# Rx.NET Patterns and Practices Patterns, anti-patterns, and decision guidance for System.Reactive (Rx.NET) in C#. Covers interface design, subject usage, subscription lifecycle, error handling, and testing. ## Exposing Observables **Expose `IObservable<T>` as a property; never inherit from it.** Classes should not implement `IObservable<T>` directly. Expose a property instead, enabling consumers to compose operators before subscribing. ```csharp public class SensorReadings { private readonly Subject<double> _readings = new(); // Wrap with AsObservable() to prevent downcasting to Subject public IObservable<double> Readings => _readings.AsObservable(); internal void Report(double value) => _readings.OnNext(value); } ``` Key rules: - Always call `AsObservable()` on subjects before exposing them. This prevents consumers from calling `OnNext`/`OnError`/`OnCompleted`. - Never return null from a method or property typed `IObservable<T>`. Return `Observable.Empty<T>()` or `Observable.Never<T>()` instead. - Avoid implementing `IObservable<T>` or `IObserver<T>` yourself. Use `Observable.Create` or subjects. ## Subject Selection Subjects are "mutable variables of the Rx world" (Erik Meijer). Use them only to generate a hot observable imperatively from a local source with no direct external observable to adapt. ### Decision: Subject vs Observable.Create Use `Observable.Create` when: - Wrapping an external async/callback/event source into an observable - Each subscriber needs independent state (cold observable) - Cleanup logic ties naturally to the subscription's `IDisposable` Use a `Subject` when: - You need to imperatively push values from code you control (local source) - Multiple subscribers must share the same live stream (hot observable) - Modeling property-change or event-like notifications If the source is an existing observable and you want hot behavior, use `Publish`/`RefCount` instead of piping through a subject. ### Which Subject Type | Type | Replays | Seed | Use when | |----------------------|----------|------|-----------------------------------------------------| | `Subject<T>` | None | No | Fire-and-forget events; no history needed | | `BehaviorSubject<T>` | Last (1) | Yes | Property-change semantics; always has current value | | `ReplaySubject<T>` | N or all | No | Late subscribers need historical values | | `AsyncSubject<T>` | Last | No | Single async result (like `Task<T>`) | `BehaviorSubject<T>` requires a seed value and immediately pushes it to new subscribers. Prefer it for "current state" scenarios. `ReplaySubject<T>` can replay a bounded window (`new ReplaySubject<T>(bufferSize)`) or a time window. Always bound the buffer to avoid unbounded memory growth. ### Subjects Must Be Private Subjects are implementation details. Expose only `IObservable<T>` publicly: ```csharp private readonly BehaviorSubject<int> _count = new(0); public IObservable<int> Count => _count.AsObservable(); ``` ## Imperative Read-back Is a Design Smell Exposing `.Value` / `.Current` from a `BehaviorSubject<T>` publicly introduces imperatively-accessed shared state, undermining the reactive model. Ben Lesh (RxJS lead): "using `getValue()` is a huge code smell... you're doing something imperative in declarative paradigm." **Acceptable**: Internal read-modify-write within the owning class (e.g. `_subject.OnNext(_subject.Value + 1)`). **Avoid**: Public `.Value` properties that let consumers pull state instead of subscribing. Alternatives: - Pass data as parameters to consumers instead of letting them read state. - Use `Scan` within the observable chain for stateful accumulation. - Use an OAPH-like pattern (subscribe internally, cache latest, expose read-only) when a synchronous read is genuinely necessary. ## Hot vs Cold Observables **Cold**: Each `Subscribe` triggers independent execution. Factory methods like `Observable.Create`, `Observable.Defer`, `Observable.Timer` produce cold observables. **Hot**: All subscribers share a single underlying source. Subjects are inherently hot. Use `Publish`/`RefCount` to share a cold source: ```csharp IObservable<long> shared = Observable .Interval(TimeSpan.FromSeconds(1)) .Publish() .RefCount(); ``` `Publish().RefCount()` connects on first subscriber and disconnects when the last unsubscribes. Use `Publish().AutoConnect(n)` if you need to wait for `n` subscribers before connecting. ## Subscription Lifecycle Every `Subscribe` returns an `IDisposable`. Failing to dispose leaks subscriptions and can leak the subscriber (prevented from GC by the observable's reference to it). ### Disposable Types | Type | Behavior | |--------------------------------|------------------------------------------------------------| | `CompositeDisposable` | Groups multiple disposables; disposes all together. | | `SerialDisposable` | Holds one at a time; setting a new one disposes previous. | | `SingleAssignmentDisposable` | Assigned once; throws on second assignment. | | `MultipleAssignmentDisposable` | Like Serial but does NOT dispose previous on reassignment. | | `CancellationDisposable` | Bridges `CancellationTokenSource` to `IDisposable`. | | `RefCountDisposable` | Prevents disposal until all dependents dispose. | | `BooleanDisposable` | Tracks `IsDisposed` state. | | `Disposable.Empty` | No-op; useful as default or from `Observable.Create`. | | `Disposable.Create(action)` | Runs `action` on first `Dispose` call (idempotent). | ### Patterns Collect subscriptions with `CompositeDisposable` and dispose in one call: ```csharp private readonly CompositeDisposable _subscriptions = new(); public void Initialize() { source1.Subscribe(HandleItem1).DisposeWith(_subscriptions); source2.Subscribe(HandleItem2).DisposeWith(_subscriptions); } public void Dispose() => _subscriptions.Dispose(); ``` `DisposeWith` is in the `System.Reactive.Disposables.Fluent` namespace (available since Rx.NET 6.1). Use `SerialDisposable` when replacing one subscription with another: ```csharp private readonly SerialDisposable _current = new(); public void SwitchSource(IObservable<int> newSource) { // Disposes previous subscription automatically _current.Disposable = newSource.Subscribe(Handle); } ``` Only dispose subscriptions early if you need to unsubscribe before the observable completes. Finite sequences (`Observable.Return`, `.Take`, etc.) clean up on completion. ## Error Handling An `OnError` notification terminates the sequence. Subscribers that don't provide an `OnError` handler will have the exception thrown on the calling thread. **Always provide an `OnError` handler in `Subscribe`.** ### Operators | Operator | Behavior | |---------------------------------------|-------------------------------------------------------| | `Catch<TSource, TException>(handler)` | Catches typed exception; returns fallback observable. | | `Catch(fallback1, fallback2, ...)` | On error, moves to next sequence. | | `Retry()` / `Retry(count)` | Resubscribes on error. Bound the count. | | `OnErrorResumeNext(next)` | Continues with next on error OR completion. | | `Finally(action)` | Runs on completion, error, or disposal. | Prefer typed `Catch<TSource, TException>` over untyped variants to avoid swallowing unexpected exceptions: ```csharp IObservable<string> resilient = source .Catch<string, TimeoutException>(_ => Observable.Empty<string>()); ``` **Avoid**: Putting side effects in `Select`/`Where` callbacks. Use `Do` for explicit side effects before operators that may error. ## Thread Safety The Rx contract requires that `OnNext`, `OnError`, and `OnCompleted` calls are serialized (non-concurrent, non-overlapping). Subjects do NOT enforce this internally. Concurrent `OnNext` calls can produce out-of-order delivery or corrupt operator state. If multiple threads may call `OnNext`: ```csharp // Wrap with Synchronize to enforce sequential delivery var safe = subject.Synchronize(); ``` Or use `Observable.Create` which naturally scopes to a single subscriber. **Never call `OnNext` concurrently on a subject.** This violates the Rx contract and causes subtle threading bugs in downstream operators. `ObserveOn(scheduler)` and `SubscribeOn(scheduler)` control which thread notifications are delivered on. Place `ObserveOn`/`SubscribeOn` immediately before `Subscribe`, not buried mid-chain. ## Testing Use `Microsoft.Reactive.Testing` NuGet package for `TestScheduler`. ### TestScheduler `TestScheduler` virtualizes time, allowing time-dependent tests to run in microseconds instead of real time: ```csharp var scheduler = new TestScheduler(); // Advance virtual time instead of waiting real seconds var results = scheduler.Start(() => Observable.Interval(TimeSpan.FromSeconds(1), scheduler).Take(3) ); Assert.That(results.Messages, Has.Count.EqualTo(4)); // 3 OnNext + 1 OnCompleted ``` Key methods: - `AdvanceBy(ticks)`: Move clock forward by relative amount. - `AdvanceTo(ticks)`: Move clock to absolute time. - `Start(create)`: Creates, subscribes, disposes; returns recorded messages. One tick = 100ns. Use `TimeSpan.FromSeconds(n).Ticks` for readability. ### Inject Schedulers Accept `IScheduler` as a parameter in production code so tests can supply `TestScheduler`: ```csharp public IObservable<T> PollEvery<T>( TimeSpan interval, Func<T> fetch, IScheduler scheduler) { return Observable.Interval(interval, scheduler).Select(_ => fetch()); } ``` ### Testing Without TestScheduler For simpler cases, subscribe and collect values into a list: ```csharp var results = new List<int>(); source.Subscribe(results.Add); // ... trigger emissions ... Assert.That(results, Is.EqualTo(new[] { 1, 2, 3 })); ``` Use `Subject<T>` in tests as a controllable source to push values imperatively. This is one of the legitimate uses of subjects. ## Common Anti-patterns 1. **Implementing `IObservable<T>` on a class.** Use composition (expose a property) not inheritance. 2. **Exposing `Subject<T>` publicly.** Always wrap with `AsObservable()`. Public subjects let anyone call `OnNext` or `OnCompleted` on your stream. 3. **Using `getValue()`/`.Value` publicly.** Indicates imperative thinking in a reactive codebase. Subscribe to the stream instead. 4. **Subscribing without `OnError`.** Unhandled `OnError` throws. Always handle errors. 5. **Forgetting to dispose subscriptions.** Leaked subscriptions cause memory leaks and unexpected side effects. 6. **Concurrent `OnNext` on subjects.** Violates Rx contract. Use `Synchronize()` or restructure to avoid concurrent calls. 7. **Blocking operators (`First`, `Last`, `Single`).** These block the calling thread. Use async alternatives (`FirstAsync`) or test-specific patterns. 8. **Side effects in `Select`/`Where`.** Use `Do` for explicit side effects. Keep `Select` and `Where` pure. 9. **Unbounded `ReplaySubject`.** Always specify a buffer size or time window to prevent unbounded memory growth. 10. **Using a subject to bridge an existing observable.** Use `Publish`, `Replay`, or other sharing operators instead of subscribing to one observable and forwarding into a subject. ## Quick Reference ```txt Need to... Use... ------------------------------------+------------------------------------- Wrap event/callback/async -> Rx Observable.Create / Observable.FromEvent Push values imperatively (local) Subject<T> (private, expose AsObservable) Current-value property semantics BehaviorSubject<T> Share a cold observable Publish().RefCount() Group subscription disposal CompositeDisposable + DisposeWith Replace subscriptions SerialDisposable Handle errors in chain Catch<TSource, TException> Retry transient failures Retry(count) Control time in tests TestScheduler (Microsoft.Reactive.Testing) Enforce sequential OnNext subject.Synchronize() ```
Ver no GitHub