When I was first introduced to Observables in C#, they sounded pretty damn good.“They just model streams of data”, “It’s just data over time” and “It’s just the push equivalent of an IEnumerable”.After working with them for a little while, I don’t think they’re as good as I was told.
The statements above might be true. Observables might be simple from a birds-eye perspective. Unfortunately simple doesn’t always mean easy and there’s are some things that will end up biting you in the ass.Here’s five of them.
The Framework Has No Consistent Name
When searching for documentation and solutions to problems with Observables, I always felt like I didn’t know what to search for.Previously I’ve always known the terms to use, django {your-problem-here}
, serilog {how-to-do-x}
{framework}-{problem description}
However with Observables I had a lot of problems finding people encountering similar issues, and I was never really sure what to put for the {framework}
partof my queries.
I spent a little time trying to figure out what the right term is. Turns out - nobody seems to know.
On Stack Overflow the most used tag is system.reactive
, but looking official documentation and Stack Overflow questions, I’ve seen it referred to as:
- Reactive Extensions [1] [2]
- Rx (short for Reactive Extensions) [1] [2]
- Rx.Net [1] [2]
- .NET Reactive Framework [1]
- ReactiveX [1]
With all of these names that are subtly different - what am I supposed to search for?
We’ll call it Rx.Net in the rest of the post - at least it’s short
The Documentation Is Hard To Find
Finding documentation online for Rx.Net is pretty close to impossible.While figuring out the search terms to use is hard, finding comprehensive documentation is much, much harder.
Rx.Net is the .NET flavour of ReactiveX, which luckily seems to have quite a bit of documentation!Unfortunately you pretty much have to know what you’re looking for already.If I want to read the documentation for how the C# Select
works, I need to know that in ReactiveX, Select
is called Map
When I know that, in the ReactiveX documentation I can find a language-agnostic explanation of what a Map/Select
does.There’s also language-specific documentation for each of the different flavours, including Rx.Net!
There’s also no API reference online anywhere. I can find the documentation in the editor, so I know it’s been written - but it isn’t hosted anywhere.The closest we get is this API reference from MSDN. From 2011.
Observables are hard to debug
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 pieceof code which throws an error after a few integers have passed through the stream.
[Fact]
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");
}
});
observable.Subscribe(
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, soyou 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. [1] [2]
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 Rx.net 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 trendsreveals 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 popularityby 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.
Top comments (0)