top of page

5 Reasons to Not Use Observables in C#

Observables can be a pain to debug. Most debuggers aren’t particularly suited for tracing streams of data, and the stack traces you get are unimpressive. Let’s dive a little deeper into the stack traces. Take this piece of code which throws an error after a few integers have passed through the stream.

public void ObservableTest()
   IObservable<int> observable = Observable.Range(0, 5)
       .Select(i => i * 2)
       .Do(i =>
           if (i > 5)
               throw new Exception("That's an illegally large number");

       onNext: (i) => Console.WriteLine(i),
       onError: (err) =>
       throw err;

The humongous Stacktrace is as follows:

Error Message:
   System.Exception : That's an illegally large number
  Stack Trace:
     at MyProject.Test.ObservableTest.<>c.<Foo1>b__0_3(Exception err) in /home/geewee/programming/MyProject.Test/ObservableTest.cs:line 30
   at System.Reactive.AnonymousSafeObserver`1.OnError(Exception error) in D:\a\1\s\Rx.NET\Source\src\System.Reactive\AnonymousSafeObserver.cs:line 62
   at System.Reactive.Sink`1.ForwardOnError(Exception error) in D:\a\1\s\Rx.NET\Source\src\System.Reactive\Internal\Sink.cs:line 61
   at System.Reactive.Linq.ObservableImpl.Do`1.OnNext._.OnNext(TSource value) in D:\a\1\s\Rx.NET\Source\src\System.Reactive\Linq\Observable\Do.cs:line 42
   at System.Reactive.Sink`1.ForwardOnNext(TTarget value) in D:\a\1\s\Rx.NET\Source\src\System.Reactive\Internal\Sink.cs:line 50
   at System.Reactive.Linq.ObservableImpl.Select`2.Selector._.OnNext(TSource value) in D:\a\1\s\Rx.NET\Source\src\System.Reactive\Linq\Observable\Select.cs:line 48
   at System.Reactive.Sink`1.ForwardOnNext(TTarget value) in D:\a\1\s\Rx.NET\Source\src\System.Reactive\Internal\Sink.cs:line 50
   at System.Reactive.Linq.ObservableImpl.RangeRecursive.RangeSink.LoopRec(IScheduler scheduler) in D:\a\1\s\Rx.NET\Source\src\System.Reactive\Linq\Observable\Range.cs:line 62
   at System.Reactive.Linq.ObservableImpl.RangeRecursive.RangeSink.<>c.<LoopRec>b__6_0(IScheduler innerScheduler, RangeSink this) in D:\a\1\s\Rx.NET\Source\src\System.Reactive\Linq\Observable\Range.cs:line 62
   at System.Reactive.Concurrency.ScheduledItem`2.InvokeCore() in D:\a\1\s\Rx.NET\Source\src\System.Reactive\Concurrency\ScheduledItem.cs:line 208
   at System.Reactive.Concurrency.CurrentThreadScheduler.Trampoline.Run(SchedulerQueue`1 queue) in D:\a\1\s\Rx.NET\Source\src\System.Reactive\Concurrency\CurrentThreadScheduler.cs:line 168
   at System.Reactive.Concurrency.CurrentThreadScheduler.Schedule[TState](TState state, TimeSpan dueTime, Func`3 action) in D:\a\1\s\Rx.NET\Source\src\System.Reactive\Concurrency\CurrentThreadScheduler.cs:line 118
   at System.Reactive.Concurrency.LocalScheduler.Schedule[TState](TState state, Func`3 action) in D:\a\1\s\Rx.NET\Source\src\System.Reactive\Concurrency\LocalScheduler.cs:line 32
   at System.Reactive.Concurrency.Scheduler.ScheduleAction[TState](IScheduler scheduler, TState state, Action`1 action) in D:\a\1\s\Rx.NET\Source\src\System.Reactive\Concurrency\Scheduler.Simple.cs:line 61
   at System.Reactive.Producer`2.SubscribeRaw(IObserver`1 observer, Boolean enableSafeguard) in D:\a\1\s\Rx.NET\Source\src\System.Reactive\Internal\Producer.cs:line 119
   at System.Reactive.Producer`2.Subscribe(IObserver`1 observer) in D:\a\1\s\Rx.NET\Source\src\System.Reactive\Internal\Producer.cs:line 97
   at System.ObservableExtensions.Subscribe[T](IObservable`1 source, Action`1 onNext, Action`1 onError) in D:\a\1\s\Rx.NET\Source\src\System.Reactive\Observable.Extensions.cs:line 95
   at MyProject.Test.ObservableTest.ObservableTest() in /home/geewee/programming/OAI/MyProject.Test/ObservableTest.cs:line 25

Removing all the Rx.Net internal stuff, this is the only information in the stack trace we care about:

Error Message:
   System.Exception : That's an illegally large number
  Stack Trace:
     at MyProject.Test.ObservableTest.<>c.<ObservableTest>b__0_3(Exception err) in /home/geewee/programming/MyProject.Test/ObservableTests.cs:line 30
     at MyProject.Test.ObservableTest.ObservableTest() in /home/geewee/programming/OAI/MyProject.Test/ObservableTests.cs:line 25

We get the line number where we subscribed to the observable, and the function inside the chain where the error was thrown. I have no idea what transformations the data has gone through. If the stream has been dynamically constructed I might not even know what it looks like.

These issues aren’t unique to observables. Composing long LINQ-expressions suffer from the same issues. When composing several different functions together, the stack traces very quickly stop becoming meaningful. The below is the complete stack trace the Enumerable/LINQ version of the Observable code.

Error Message:
   System.Exception : That's an illegally large number
  Stack Trace:
     at MyProject.Tests.ObservableTests.<>c.<TestLinq_StackTraces>b__2_1(Int32 i) in /home/geewee/programming/MyProject.Test/ObservableTests.cs:line 55
   at System.Linq.Utilities.<>c__DisplayClass2_0`3.<CombineSelectors>b__0(TSource x)
   at System.Linq.Enumerable.SelectRangeIterator`1.MoveNext()
   at MyProject.Tests.ObservableTests.TestLinq_StackTraces() in /home/geewee/programming/MyProject.Test/ObservableTests.cs:line 60

It’s much cleaner, but it doesn’t give us any more information than the observable version.

The poor debugging and stack-traces are a perpetual thorn in my side when working with Observables. But if the same issues exist with long chains of Enumerable, why is that less of an issue? The short answer — I don’t know.

My best guess is that when using Observables there’s a strong tendency to keep everything as an Observable stream. The longer your streams are the harder debugging with a minuscule stack trace becomes.

It’s Hard To Use Scopes

A very common thing I need to do is attach some sort of context to a request or a series of events.

var id = "myCoolId"
using (LogContext.PushProperty("id", id))
    // Every log statement in here will have the `id=myCoolId` attached 
    await DoSomething(id);

This is a very common usage patterns for logging, for example in Web Apps if you want to attach a specific GUID to a Request, so you can later correlate all logs for that specific request.

Something like this isn’t possible when using Rx.Net, as it’s threading model doesn’t carry over the ExecutionContext.

This means that we can’t use something like an AsyncLocal or a ThreadLocal to keep context for a specific piece of data or stream. If we want the myCoolId to be attached to all of the logs, we'll need to pass it into every step of our observable chain, and manually pass it to every logging call. I know there's a value in being explicit, but this is a time where choosing Rx.Net locks you out of some pretty handy language features.

It never really took off

Looking at the timelines, it seems like in the hype cycled peaked a few years ago. If it ever really took off.

Google Trends for system.reactive compared to the Javascript version of the ReactiveX framework.

Comparing it in google searches to RxJS it seems much less widely used. Looking at the Stack Overflow trends reveals a little more nuanced picture however:

Stack Overflow trends for system.reactive compared to the Javascript and Java versions of the ReactiveX framework.

While still much less popular than RxJS and the Java ReactiveX version, Rx.Net does seem to predate their popularity by quite a few years. Microsoft was out early with the reactive paradigm but never really seemed to manage to make it take off.

Now while your technology choices shouldn’t be about how hyped something is — the popularity is always worth taking into account. Some old technologies are sturdy, battle-tested and well-documented. Then they don’t have to be shiny.

However with some of the pitfalls, particularly around the documentation issues, Rx.Net feels neither robust or shiny to me.

While this is a negative article, I’m not saying Rx.Net or the Reactive paradigm is always bad. Some of the issues are about the tooling and documentation. I mention that most debuggers aren’t suited for debugging streams, but this is a tooling problem and not inherent to the reactive paradigm. E.g. Jetbrains has an excellent Java Stream Debugger.

Observables seems like a very natural fit for some languages and domains, like reacting to user interactions in JavaScript, as the large adoption of RxJs suggests. I’m just saying that it doesn’t feel like a particularly natural fit for C#, and if you have some data that can be modelled as either Enumerables or Observables, I would think twice about using Observables.

Source: Meidum

The Tech Platform



bottom of page