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.

설치로 이동

소스 정보

저장소
recyclarr/recyclarr
최근 소스 활동
2026년 4월 18일 17:21
감지된 SKILL.md 언어
영어
스타
2,106
포크
36

설치 방법

기본적으로 소스를 먼저 확인하는 Prompt가 선택됩니다. 직접 명령으로 전환하거나 로컬 사본을 다운로드할 수도 있습니다.

소스 파일 검토

설치 여부를 결정하기 전에 SKILL.md와 SkillsMP에 표시된 보조 파일을 읽어 보세요.

SKILL.md 표시 중

SKILL.md
소스 지침 · 읽기 전용 미리보기
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() ```
GitHub에서 보기