Sitemap

Reactive Programming

11 min readMar 7, 2025

--

Press enter or click to view image in full size

Have you heard of the Observer pattern? What if I tell you that a simple pattern like that could shape your entire coding style? Don’t believe me? See it yourself.

Today, you will learn about Reactive programming. How it can be implemented in C#. You will see why events suck. The benefits the Observer pattern can bring. How all that led to Rx.Net. And finally, how it can be applied.

In case you are interested in other programming paradigms, look here:

Meanwhile, we are starting.

Reactive Programming

Reactive programming is one of those things that is easier to explain with examples. So let’s see our example 🙃.

Imagine you have an expression sum = a + b. Traditionally, the variable sum won’t change if a or b changes:

var a = 1;
var sum = a + 2; // 3

// change variable used in expression to calculate 'sum'
a = 10;

// 'sum' remains the same
Console.WriteLine(sum); // 3 👈

However, in reactive programming, if a or b changes, sum will be automatically recomputed. Let’s see the next pseudo-code:

var a = 1;
var sum <= a + 2; // <= reactive assignment

// change variable used in expression to calculate 'sum'
a = 10;

// 'sum' changes automatically
Console.WriteLine(sum); // 12 👈

So, here you have it:

Reactive programming is a programming paradigm where code automatically reacts to changes in data.

Even though it may seem straightforward at first, you will discover how many advanced techniques this paradigm brings.

Let’s see that below.

C# and Reactive Programming

While some languages have built-in support for the “reactive-assignment” operator, C# is not one of those 😭.

It does not mean we cannot be reactive, though. We are design patterns experts after all, we can figure something out 😁.

We need to react whenever the value of a changes and update the sum. The first thing that comes to my mind is the Observer pattern, which in C# is implemented in the form of events.

Since regular int type does not have this functionality, we will create our own type:

class ObservableNumber
{
public event Action ValueSet;

public int Value
{
get
{
return field;
}
set
{
field = value;
ValueSet?.Invoke();
}
}
}

Now, we can rewrite the example from above using this new type:

var a = new ObservableNumber { Value = 1 };

// reactive assignment
var sum = a.Value + 2;
a.ValueSet += () => sum = a.Value + 2;

// change variable used in expression to calculate 'sum'
a.Value = 10;

// 'sum' changes automatically
Console.WriteLine(sum); // 12

Here you have it: reactiveness in C# 😎. But, there is always a big BUT 🍑.

While events work in simple scenarios, it is hard to build something complex on top of them because of the following limitations:

  • you cannot store events as fields
  • you cannot pass them as arguments to methods or return them as the method’s result
  • you cannot write an extension method for an event
  • you cannot combine events to produce another event
  • you cannot wrap an event to adapt its value
  • you cannot publish events asynchronously
  • etc

Seeing all the problems, Microsoft decided to experiment. They created a library that uses actual classes instead of events and called it reactive extension (RX). It is not part of C# but needs to be installed as a separate NuGet package.

Let’s rewrite our example for the last time using Rx.Net:

var a = new BehaviorSubject<int>(1);

// reactive assignment
var sum = default(int);
a.Subscribe(() => sum = a.Value + 2);

// change variable used in expression to calculate 'sum'
a.OnNext(10);

// 'sum' changes automatically
Console.WriteLine(sum); // 12

I know, I know. It looks grumpy. But this is because you are not used to it. Read the article to the end to see how fantastic reactive programming is 😉.

Overview

Rx.Net is nothing more than just the implementation of the traditional Observer pattern.

On the diagram you have:

  • Observable — is something you can subscribe to observe for changes
  • Observer — is something that subscribes, observes, and reacts to changes
  • Subject — is a concrete entity that holds a state and can be both observable and observer

We can investigate each component in more detail.

Observable

The observable (aka producer) represents a read-only data source that you can subscribe to observer changes

interface IObservable<T>
{
IDisposable Subscribe(Action<T> onNextValue);
}

For example, let’s say you want to implement an observable collection in a way that every time you add a new value to a collection, an event is raised.

Get iamprovidence’s stories in your inbox

Join Medium for free to get updates from this writer.

In that case, you simply inherit and implement the interface:

class MyObservableCollection<T> : List<T>, IObservable<T>
{
// list of subscribers
private List<Action<T>> _observers = new List<Action<T>>();

public IDisposable Subscribe(Action<T> onNextValue)
{
_observers.Add(onNextValue);

return Disposable.Empty;
}

public new void Add(T item)
{
base.Add(item);

// notify all subscribers
foreach (Action<T> onNext in _observers) onNext(item);
}
. . .
}

Later in the code, the collection can be used like this:

var subject = new MyObservableList();
IObservable<int> observable = subject;

observable.Subscribe(val =>
{
Console.WriteLine(val); // perform an action on an item
});

// no need to read data, observable will actively push values to out code
subject.Add(1);
subject.Add(2);

Notice, IObservable<T> will push new values to your code when they are available. Comparing to regular IEnumerable<T>, that you need to pull for new items (typically with a foreach loop):

var list = new List<int>();
IEnumerable<int> enumerable = list;

list.Add(1);
foreach (var val in enumerable)
{
Console.WriteLine(val); // perform an action on an item
}

list.Add(2);
// need to pull items from enumerable again to access added values
foreach (var val in enumerable)
{
Console.WriteLine(val);
}

Observer

The observer (aka consumer, aka subscriber) represents something that subscribe on events

In the example above, our observer is just a callback that will execute an action on a new item:

interface IObservable
{
IDisposable Subscribe(
Action<T> onNextValue // 👈 observer
);
}

However, the “next value appeared” event is not the only one. We can also subscribe to exceptions being thrown or the completion of the observable when no more items will be available:

interface IObservable
{
IDisposable Subscribe(Action<T> onNextValue);
IDisposable Subscribe(Action<T> onNextValue, Action onCompleted)
IDisposable Subscribe(Action<T> onNextValue, Action<Exception> onError, Action onCompleted);
}

Like in the example below:

var subject = new Subject<int>();
IObservable<int> observable = subject;

observable
.Subscribe(
onNext: (newValue) => Console.WriteLine(newValue),
onError: (exception) => Console.WriteLine("We're ALL DOOMED!! DOOM!!!"),
onCompleted: () => Console.WriteLine("Finished. Hurray!")
);

subject.OnNext(1);
subject.OnNext(2);
subject.OnCompleted(); // indicates that sequence has no more items
subject.OnNext(2); // will not be emitted

If you dislike callbacks and prefer a more traditional OOP approach, there is another method for you. It accepts an object implementing IObserver:

interface IObservable
{
IDisposable Subscribe(IObserver<T> observer);
}

Where IObserver is just an interface with sets of the same callbacks:

interface IObserver<T>
{
void OnNext(T value);
void OnError(Exception error);
void OnCompleted();
}

Which you can implement as a separate class:

class MyConsoleObserver<T> : IObserver<T>
{
public void OnNext(T newValue)
{
Console.WriteLine(newValue)
}

public void OnError(Exception error)
{
Console.WriteLine("We're ALL DOOMED!! DOOM!!!")
}

public void OnCompleted()
{
Console.WriteLine("Finished. Hurray!")
}
}

While it is more verbose, it also enables proper encapsulation, allows state sharing between methods, facilitates cleaner unit tests, reusability, and more.

IObservable<int> observable = subject;

observable.Subscribe(new MyConsoleObserver());
. . .

Subject

The final concept we going to discuss is the Subject.

Subject represents an entity that maintains the state, it can emit changes and you can subscribe to receive new values from it

interface ISubject<T> : IObservable<T>, IObserver<T>
{
}

You can also think of subjects as Rx.Net representation of C# events, that can be both subscribed and dispatched.

Press enter or click to view image in full size

There are 4 in-build subjects (but you can always create your own):

  • Subject. It only replays the values to consumers after a subscription is made.
var subject = new Subject<int>();

// this value won't be received by any subscriber
// since no one has subscribed yet
subject.OnNext(1);

// this subscriber will receive [2]
subject.Subscribe(value => Console.WriteLine(value));

subject.OnNext(2);
  • BehaviorSubject. This one remembers the last emitted value and sends it immediately to new subscribers as soon as a subscription is made.
var subject = new BehaviorSubject<int>();

subject.OnNext(1);
subject.OnNext(2);

// this subscriber will receive [2]
subject.Subscribe(value => Console.WriteLine(value));
  • ReplaySubject. A subject that stores all values it has emitted and replays them to any new subscribers.
var subject = new ReplaySubject<int>();
replaySubject.OnNext(1); // stores value 1
replaySubject.OnNext(2); // stores value 2
replaySubject.OnNext(3); // stores value 3

// this subscriber will receive [1, 2, 3]
subject.Subscribe(value => Console.WriteLine(value));
  • AsyncSubject. A subject that only emits the last value it received when the sequence is completed.
var subject = new AsyncSubject<int>();

subject.OnNext(1);
subject.OnNext(2);
subject.OnNext(3); // last value before completion

subject.OnCompleted();

// this subscriber will receive [3]
subject.Subscribe((newValue) => Console.WriteLine(newValue));

// can also be awaited
var val = await subject; // 3

Why Rx.Net?

At this point, you may be wondering why bother with Rx.Net and all its complexity?

As I said before, because events sucks and Subjects are just better events 🙃. They are objects, and all issues that were listed for events do not apply to them.

For example, ISubject inherits both IObservable and IObserver:

interface ISubject<T> : IObservable<T>, IObserver<T>
{
}

This means you can subscribe on a subject with a subject to have a chain of events 😳 (Xzibit would like that):

var subject1 = new Subject<int>();
var subject2 = new Subject<int>();

subject1.Subscribe(subject2); // subscribe with a subject
subject2.Subscribe(newVal => Console.WriteLine(newVal));

subject1.OnNext(1);

Additionaly, since IObservable is an interface, you can write extension methods on top of it:

public static class SampleExtensions
{
public static void ToConsole<T>(this IObservable<T> source)
{
source.Subscribe((newVal) => Console.WriteLine(newVal);
}
}

. . .
var list = new MyObservableList();
IObservable<int> observable = list;

observable.ToConsole(); // 👈

// add new values
list.Add(1);
list.Add(2);

And this is where sh*t gets real. Behold the true power of Rx.Net.

Rx already provides several extension methods, like .Where(), .Select(), .Take(), .Concat(), .GroupBy(), .Count(), and so on...

var sum = 0;

Observable
.Interval(TimeSpan.FromSeconds(1))
.Where(x => x % 2 == 1)
.Select(x => x * 10)
.Subscribe(x => sum = x);

Does it look familiar to you? Exactly. LINQ 😁.

You can apply LINQ operators to a stream of events. This means you can filter out events, enrich them, accumulate them, and so on.

Rx.Net has a whole range of operators that help you control the events flow.

Press enter or click to view image in full size

Practical Examples

All those book examples look great, but still, you may wonder how they can be applied in practice. Is there any practical usage except updating a variable sum in the expression sum = a + b? Turns out there is 🙃.

Reactive programming works great in user interface frameworks like WPF, Blazor, Angular, and so on. There, you have lots of events to propagate between components. Different fields depend on each other, and so on.

Picture a UI task where you have date pickers for start and end dates.

Press enter or click to view image in full size

You need to implement the following conditions:

  • if FromDate is empty, disable ToDate field
  • if FromDate is invalid, disable ToDate field
  • if FromDate is after ToDate, set ToDate same as FromDate
  • if ToDate is before FromDate, set FromDate same as ToDate

This can be easily solved with dynamic assignment rules:

// disable
var isEmpty = FromDateObservable.Select(fromDate => !fromDate.HasValue);
var isInvalid = FromDateObservable.Select(fromDate => !IsValidDate(fromDate));

Observable
.Merge(isEmpty, isInvalid)
.Subscribe((isDisabled) => ToDate.IsDisabled = isDisabled);

// reset fields
FromDateObservable
.Where(fromDate => fromDate > ToDate)
.Subscribe(fromDate => ToDate = fromDate);

ToDateObservable
.Where(toDate => toDate > FromDate)
.Subscribe(toDate => FromDate = toDate);

Another typical task would be to implement the search bar.

To optimize our application, we should perform the search only when:

  • the search bar contains more than 3 characters
  • the user has stopped typing for at least 500 milliseconds
  • the entered value differs from the previous one (so we don’t perform redundant queries with the same searchTerm)

Think for yourself how you would implement this logic. With reactive programming, it is just a few lines of code:

 Observable
.FromEventPattern<EventArgs>(textBox, nameof(textBox.KeyUp)) // convert the event to observable
.Select(evt => ((TextBox)evt.Sender).Text) // select text from the event
.Where(text => text.Length > 3) // trigger search only when there are more than 3 chars
.Debounce(TimeSpan.FromMilliseconds(500)) // wait 500ms for no more input
.DistinctUntilChanged() // when the user stops typing, ensure that the value is not the same as before
.Subscribe((searchTerm) =>
{
_users = DataBase.FindUsers(searchTerm);
});

Overall, reactive programming offers LINQ operators (and not only) that simplify common UI tasks and keep the code clean. Just ask any Angular developer — they’ll back me up 😏.

My thoughts 🤔

Even though Subjects have the upper hand on events, reactive programming is rarely seen in regular apps. Mostly because nobody even uses regular events 😅.

If you are a backend web developer, you are dealing with HTTP requests, which are isolated, short-lived, and stateless. This all makes event subscriptions inefficient. Of course, there are cases where you need to propagate some events across the system, still, it can be achieved with other, simpler approaches, making reactive programming excessive.

However, there are also applications with long-lived, stateful classes. Usually, those are desktop or mobile applications, games, SPA, etc. There, you have a UI interface that constantly accepts users’ inputs and propagates events through the entire system. With frameworks like WPF, Blazor, Angular, Unity, and others, reactive programming can significantly improve code quality, simplify event management, reduce bags, and improve responsiveness.

Wrapping it up

Reactive programming is a paradigm that focuses on handling event streams.

It helps to deal with complexity by providing a more declarative approach to handling state changes and events.

Unlike functional programming, which emphasizes immutability, reactive programming focuses on dynamic data flows.

The key benefits of reactive programming are:

  • improved responsiveness: reactive programming enables real-time updates, making applications more responsive to user input and events
  • simplified event management: it helps manage complex event flows and asynchronous data in a more structured and declarative manner
  • composability: reactive programming allows you to combine multiple data streams and operators seamlessly, offering high flexibility, reducing callback hell, and improving readability
  • enhanced user experience: by reacting to changes in data immediately, reactive programming helps provide smoother, more interactive user experiences

That is all I had for you today, folks 🙃.

💬 Let me know in the comments how often you use events in your code.

👏 Give this article a clap if you liked it

👇 Do not forget to check out other articles

☕️ Support me if you want

✅ Follow to receive more

😉 See U

--

--

iamprovidence
iamprovidence

👨🏼‍💻 Full Stack Dev writing about software architecture, patterns and other programming stuff https://www.buymeacoffee.com/iamprovidence