Querying Event Streams with Reactive Extensions in C
Reactive Extensions (Rx.NET) enables developers to treat asynchronous event streams as push-based data collections that can be queried and transformed using LINQ-style syntax. This article covers how to install Rx.NET, convert standard .NET events and asynchronous operations into observable sequences, apply filtering and time-based operators, and handle stream consumption safely through subscriptions.
Getting Started with Rx.NET
To begin using Reactive Extensions in C#, install the official NuGet package:
dotnet add package System.ReactiveRx centers around two primary interfaces defined in the
System namespace:
IObservable<T>: Represents a provider for push-based notification (the event producer).IObserver<T>: Receives notifications from an observable (the event consumer).
Creating Observable Sequences
You can create observables from static values, time-based intervals, or existing standard .NET events.
From Standard .NET Events
To transform traditional UI clicks, file watchers, or custom events
into an observable stream, use
Observable.FromEventPattern:
using System;
using System.Reactive.Linq;
public class Button
{
public event EventHandler Clicked;
public void Click() => Clicked?.Invoke(this, EventArgs.Empty);
}
// Convert the event to an IObservable sequence
Button button = new Button();
IObservable<System.Reactive.EventPattern<EventArgs>> clicks =
Observable.FromEventPattern<EventArgs>(
handler => button.Clicked += handler,
handler => button.Clicked -= handler);From Timers or Periodic Operations
You can generate continuous streams using built-in generators like
Observable.Interval:
// Emits an incrementing long value every 500 milliseconds
IObservable<long> ticks = Observable.Interval(TimeSpan.FromMilliseconds(500));Querying Event Streams with LINQ
Because IObservable<T> supports LINQ, you can
manipulate asynchronous streams using standard operators such as
Where, Select, and Take, as well
as time-aware operators unique to Rx.
Filtering and Projection
var numbers = Observable.Range(1, 20);
IObservable<string> evenSquares = numbers
.Where(x => x % 2 == 0)
.Select(x => $"Square: {x * x}");Time-Based Operators
Reactive Extensions excels at handling continuous real-time inputs, such as user keystrokes or sensor data, using time-based operators:
- Throttle (Debounce): Emits an item only after a specified timespan has passed without any new items. Useful for search input fields.
- Buffer: Groups items into batches based on time intervals or count limits.
- Sample: Takes the most recent item emitted over periodic intervals.
// Debouncing rapid input (e.g., search text)
IObservable<string> searchInputs = GetSearchInputStream();
IObservable<string> debouncedQueries = searchInputs
.Throttle(TimeSpan.FromMilliseconds(300))
.DistinctUntilChanged();Consuming the Stream with Subscriptions
To listen to the results of your query, call the
Subscribe method on the observable. Subscribe
provides callbacks for values emitted (OnNext), errors
encountered (OnError), and stream completion
(OnCompleted).
IDisposable subscription = debouncedQueries.Subscribe(
query => Console.WriteLine($"Execute search for: {query}"),
error => Console.WriteLine($"Error occurred: {error.Message}"),
() => Console.WriteLine("Query stream completed.")
);
// To stop listening and clean up event handlers:
subscription.Dispose();Managing Concurrency with Schedulers
Rx uses IScheduler to control threading contexts for
observing and subscribing.
SubscribeOn: Dictates the thread on which the observable begins its work.ObserveOn: Dictates the thread on which observer callbacks (OnNext) are executed.
using System.Reactive.Concurrency;
searchInputs
.ObserveOn(TaskPoolScheduler.Default) // Process background computations on TaskPool
.Select(query => HeavySearchOperation(query))
.ObserveOn(Scheduler.CurrentThread) // Return results to the main or UI thread
.Subscribe(results => DisplayResults(results));By combining observable creation, LINQ transformation pipelines, and thread scheduling, Reactive Extensions provides a declarative and robust framework for handling complex, asynchronous event architectures in C#.