DI027

Rx Subscription On Longer-Lived Observable Without Dispose

The Rx twin of DI025. `IObservable<T>.Subscribe(...)` returns an `IDisposable` token that unsubscribes the observer when disposed — there is no `-=` to prove missing, so the leak proof inverts to a **discarded token**. A **transient** or **scoped** registered service subscribes an instance-capturing handler (method group, `this`-capturing lambda, or stored delegate) to an observable exposed by a longer-lived publisher — an injected **singleton** dependency, or a **scoped** publisher shared by a transient subscriber — and throws the returned token away. The observable is reached through the same classified receivers as DI025 (an injected member proven ctor-assigned, a constructor parameter, or a stable chained projection such as `_source.Ticks`), and the publisher's registration lifetime is resolved with the same rules (most conservative registration wins, closed registrations preferred over open-generic fallbacks, keyed-only registrations excluded).

Default severity: Warning · Code fix: No

Why it matters

A discarded subscription is a live one. The observable holds the observer, the observer captures the subscriber, and nothing ever releases it, so the longer-lived publisher roots every subscriber instance the container creates — leaking memory on each resolution and invoking stale observers against released state. DI027 is a single **Warning** tier: whether the publisher is singleton or a scope-shared scoped, a discarded token that outlives the subscriber is a definite leak.

README problem example

README better pattern

No — planned. The safe repair (introduce `IDisposable`, store the token, dispose it) depends on the subscriber's registered lifetime exactly like the DI025 tier-3 assist, and is deferred to a follow-up.

Repo sample extraction

Examples pulled from the sample app

Open full sample file

Sample app Rx subscription-leak warning

    public class Bad_SubscribeWithoutDispose
    {
        private readonly ITicker _ticker;

        public Bad_SubscribeWithoutDispose(ITicker ticker)
        {
            _ticker = ticker;
            // [DI027] The IDisposable token is discarded, so every instance the container creates
            // stays rooted in the singleton ticker's observer list.
            _ticker.Subscribe(OnTick);
        }

        private void OnTick(int value) { }
    }

Sample app store-and-dispose pattern

    public class Good_StoreAndDispose : IDisposable
    {
        private readonly IDisposable _subscription;

        public Good_StoreAndDispose(ITicker ticker)
        {
            // Keep the token and dispose it when the subscriber is released.
            _subscription = ticker.Subscribe(OnTick);
        }

        public void Dispose() => _subscription.Dispose();

        private void OnTick(int value) { }
    }

Related guides

  • No problem-guide pages point here yet.