Skip to content
Ian Griffiths Ian Griffiths

.NET Conf 2025

Ian Griffiths, Technical Fellow at endjin, shares the latest updates on the Reactive Extensions for .NET (AKA ReactiveX AKA Rx.NET). Learn about the new features in Rx 6.1, what .NET 10 means for the project, and the significant packaging changes coming in Rx v7.0 that finally solve the long-standing deployment bloat issue.

In this talk:

  • Rx 6.1 New Features — DisposeWith operator for fluent CompositeDisposable usage, new TakeUntil overload with cancellation token support, and ResetExceptionDispatchState operator
  • The Bloat Problem Explained — Why self-contained Windows deployments were pulling in 90MB of unnecessary WPF and Windows Forms assemblies
  • Rx 7 Preview — How the new packaging model fixes bloat while maintaining source and binary compatibility
  • Community Contributions — Features from Chris Pullman (ReactiveUI), Neils Berger, Daniel Weber, and Adam Jones
  • Async Rx .NET — Status update and plans for a non-alpha release

Transcript

Hi! Thanks for listening to this talk about what's been happening lately with the Reactive Extensions for .NET and what we've got planned. My name's Ian Griffiths. I'm a technical fellow at endjin, and my employer endjin maintains the Reactive Extensions for .NET. You can find the source code in this repo on GitHub.

If you're listening to this talk, it's likely that you already know about the Reactive Extensions, or as we usually call them, just Rx. But just in case you don't, here's a very quick introduction. Rx is an event-driven programming model. It's useful in any application where things happen. It provides a functional declarative programming style for writing code that responds to events.

This model has become popular in other programming languages, especially JavaScript, but it was originally a .NET technology invented by Microsoft. Rx .NET was one of the first projects that Microsoft open sourced, one of the first to come under .NET Foundation ownership, and it's been a community-supported project for over a decade now.

Today I'm gonna talk about things that have been, things that are, and some things that have not yet come to pass. More specifically, I'll talk about Rx 6.1, our most recent release. I'll also talk about what .NET 10 means for Rx and also for the related Ix project that lives in the same repository.

There's a new feature in the .NET 10 runtime class libraries that has a significant impact on us. And finally, I'll talk about our progress towards the next release, Rx version 7.0. To put this all in context, it's useful to know the recent history of Rx. endjin took over maintenance at the start of 2023.

This was a little over two years since there had last been a version since Rx five. The project had ground to a halt because its previous maintainers were no longer able to devote much time to it. Our first job was to bring the codebase back into line with current tooling, 'cause it had fallen behind and wasn't actually able to build on the current version of Visual Studio.

So we addressed that and then went on to add tests for newer versions of .NET, and then produced a new release, Rx version six. We spent a big chunk of the next year bringing the documentation up to date in the form of the free online ebook, Intro to Rx. We also spent a lot of time working out how best to solve a problem that I'll be talking about later, because that work will not ship until Rx 7.0.

So there wasn't a lot of visible progress. We had a couple of minor bug fix releases, but it wasn't until last month that we produced a release with any new features. And that's what I want to talk about first: Rx version 6.1, which shipped in October 2025. We've bumped the minor version number because there are three new features in Rx 6.1.

All of these arose from community input. Two were written by community members, and the third was based on community suggestions. So we have a new DisposeWith operator, which enables a fluent programming style when working with the CompositeDisposable type. This comes from Chris Pullman, a major contributor to the ReactiveUI project, and this extension fits in well with common coding idioms in that world.

To show how this works, I've got a program here based on one of the ReactiveUI examples. It's a very simple front end, and I can type search terms in here and it finds packages for me. This happens to be a WPF app, and as you can see, the main window here has this text box for me to type into and a list box that shows the result.

The basic idea with ReactiveUI is that we can represent user input as Rx observable streams, and we can also direct the output of observable streams into user interface elements. This code here is the basic logic for handling search input, and it's mostly standard Rx operators. This first method is from ReactiveUI and it lets me get an observable stream representing input to my textbox.

We're then using Rx's Throttle to ensure we don't perform searches too often while the user types, and this Where clause filters out empty inputs. And ultimately this delivers results into another property. And so that's one of the basic ideas of the ReactiveUI library. It uses Rx to define how information flows through the application.

Now if we look at the actual main window, this is where the code that you just saw gets hooked up to the user interface elements. You can see that this connects the application logic's search term input to the actual text box, and this connects the search logic's output to an actual list box. And this code here is where the new feature that we've added in Rx 6.1 comes in useful.

UI elements get created and torn down all the time. Entries in the search results, for example, appear when I type things and get replaced when I type something else. So each time some new UI opens up, not only do we need to run this sort of code to connect everything up, we also need to be ready to shut it all down cleanly. To enable that, this WhenActivated method passes me this argument, which uses Rx's CompositeDisposable type. Now that's been in Rx for years and it's just a collection of IDisposable objects, the idea being that they can all be disposed at once when needed. Any disposable objects I put in here will automatically be disposed when this user interface element goes away.

Right now I'm just calling Add to add things to that CompositeDisposable, and that's okay, but it's not quite the normal style for a ReactiveUI app. Normally we chain method calls together one after another in what's often called the fluent style. And if I show you another UI element, the one for an individual search result, you can see it's using that CompositeDisposable slightly differently.

Instead of wrapping each of these setup lines in a call to Add, I've got just one more fluent invocation on the end of it with this DisposeWith method. It's a small change, but it enables teardown to be handled slightly more neatly. And that's Rx 6.1's new DisposeWith feature. We also have a new overload of the TakeUntil operator.

This comes from Neils Berger and incorporates feedback from Daniel Weber, both members of the Rx community. Existing overloads enable a sequence to be observed until either an element matches some criteria or some other observable source completes. But this now enables a cancellation token to signal the instant at which the sequence should complete.

Finally, we have a new operator called ResetExceptionDispatchState, developed in response to feedback from Adam Jones, and this one is best shown by example. Rx has always offered this Throw operator. When you subscribe to the observable it returns, it immediately calls OnError, passing this exception. Since we construct just a single exception, it will reuse it for each subscription.

Normally that's not a problem, but there's one situation in which this can produce surprising behavior. But before I show you that situation, I want to demonstrate something about exceptions that has nothing to do with Rx and which you might not be aware of. I've got a different program here that creates a single exception object, and then each time around this loop, we wrap that in a task and await the task. That await will, of course, throw the exception, and this C# catch block just displays it. But watch what happens when I run this. Each time around the loop, my stack trace gets longer and longer. This is a feature of how the .NET runtime throws asynchronous exceptions. Each throw appends the current location to the stack trace, and normally that's what we want.

If an exception has traveled through multiple await statements before reaching a catch block, we want the stack trace to reflect that whole history, which is why the asynchronous rethrow appends new information to the existing stack trace. But this mechanism assumes that when the error occurred, something did actually use a normal throw operation.

And finally, there are a few changes that are minor, but which are technically breaking changes that have been waiting for the next major release. So we made a small change to nullability handling of the OfType operator to align it with some other LINQ implementations. And there are some behaviors that we consider to be unintended and where we think the fix will align with how people expect things to work, but technically it's a breaking change. And finally, we're gonna stop producing new versions of the old compatibility facades that were introduced in Rx version four.

But the big one is the fix for the bloat issues. We are going to introduce a significant change in Rx .NET packaging. It will only affect people building UI applications on Windows, though. I've got a console application with a Windows-specific target framework. It needs that because it invokes certain Windows APIs. The Program.cs here, I'm using the network availability API provided by WinRT, and I've wrapped it using Rx to provide a stream of notifications when the computer loses or acquires network connections. That's a Windows-specific API, but this is just a console app.

So this illustrates that just because you specified a Windows target framework, it doesn't necessarily mean you are building a classic desktop application with a user interface. But look at this build output folder. Now, I've configured this project to use self-contained deployment, meaning that it brings its own copy of the .NET runtime and any other components that it needs to run.

This application can be installed by simply copying this entire folder to the target machine. It does not need the .NET runtime to be pre-installed, but it's huge. Now part of that is simply that the .NET runtime is quite large, but that's not the whole story. This is much bigger than a self-contained console app would normally be.

Looking more closely, we see these PresentationFramework components. These are part of WPF, one of the Windows desktop frameworks that .NET offers. And a little further down, we can see that the other one, Windows Forms, is here too. These two UI frameworks are adding about 90 megabytes to the size of this folder.

We can mitigate this a little by enabling trimming. That will make the whole folder a great deal smaller, but even so, it ends up being many megabytes larger than it would've been if these UI frameworks weren't here. So why are they here? It's because of the unfortunate consequences of a design decision made back in Rx version four.

This decision is known as "the great unification," and it took us through a world in which Rx consisted of multiple components in which it was kind of unclear what they all did, and even more unclear which ones were required in which situations, and it took us into a world where there's exactly one component: System.Reactive. And at the time, this was great, but the decision to include UI framework support as part of this great unification has turned out to be problematic. The effect is that if you build an application with a target in which Windows Forms and WPF are available, Rx will provide its Windows Forms and WPF support, and that's a problem because to do that, it adds an implicit reference to the desktop UI framework, meaning that self-contained deployments get a copy of these UI frameworks even if the application itself isn't actually using them.

I'm now gonna upgrade this project to a preview build of Rx version 7.0 and rebuild it. Now the project appears to work exactly as before, and our goal is that for anyone who wasn't building WPF, Windows Forms, or UWP applications, that it all carries on working exactly as before.

So if you weren't running into this bloat issue, or perhaps because you weren't using self-contained deployment, then this shouldn't really affect you. But this example does use it, so let's look at the output folder. This is now much smaller. This is almost exactly the same size as a simple Hello World application, made slightly larger just by the presence of the System.Reactive assembly, but those PresentationFramework and Windows Forms assemblies are gone.

So you can see that the main effect has been achieved, but this has consequences for applications that actually are using the UI framework features. So here I've got a WPF project that is using Rx six, and this line of code here is using a WPF-specific feature. Specifically, this ensures that any events that emerge from this observable are delivered via the correct thread for this window.

I'm now gonna try upgrading this to Rx 7.0.

Now I've got compiler errors. To enable Windows projects to use Rx without being forced to have a dependency on the desktop UI frameworks, we've had to hide the relevant types. They are still actually present at runtime to ensure binary compatibility, but they are effectively invisible at build time. I could spend well over an hour explaining why this was the least bad available solution to the problem, but we don't have time for that in this particular talk. Anyway, notice that in addition to the error, we've got this diagnostic. Now we've added a code analyzer to Rx 7.0 that detects this exact situation and tells you what you need to do to fix it.

We've done this because we know people will get this error when they upgrade and it isn't entirely obvious what to do about it. So the analyzer tells me that I need to add a reference to the new System.Reactive.WPF NuGet package. So let's go back and find that. And when I add this, I'm back in business.

So this provides source-level compatibility as long as you add the new package reference. And as I mentioned, we do actually provide binary-level compatibility by using a trick with reference assemblies to hide these types just at build time. So the need to add a new package is slightly annoying, but our view is that UI framework-specific support should really always have been an opt-in feature.

So this gets us finally to the place where we want to be. Now you might be wondering why it's taken so long to get here. I've been talking about this for two years now. We first opened a GitHub discussion on this back in November 2023, and we did produce a prototype just a few months later to show what it would look like in the hope of getting some feedback.

Now, we didn't get a lot, so we announced in October 2024 that we were gonna move forward, but that did produce some negative community feedback. The plan we had at the time was somewhat more radical and would've been more disruptive. Its end state would've avoided the weird trick we've had to use with reference assemblies.

And it would also have enabled us finally to remove the UAP target framework from the main Rx package. But it would also have created a lot of problems for people who weren't actually affected by the issue we were trying to fix. So we had a rethink and came up with a new plan. The critical difference is that now System.Reactive remains as the main assembly, meaning that a lot of Rx users shouldn't even notice this change.

We also introduced an extensive set of tests designed to find the kind of subtle problems that emerge with any attempt to fix this issue. We called that test suite "Rx Gauntlet," and that is what's given us the confidence to move forward with this new plan.

One last thing: last year I talked about Async Rx .NET. We have made a little progress on extending the test suite, but we considered the code bloat issue with Rx .NET to be of higher priority. We will be getting back to Async Rx .NET, and we hope to have a non-alpha release next year.

My name's Ian Griffiths. Thanks for listening.