Rx.NET v6.0 Now Available
Earlier this year, endjin took over maintenance of the Rx.NET project with the assistance of the .NET Foundation. We have exciting news about this work.
Summary for impatient readers
If you're not into long, detailed blog posts, wow do you have the wrong author, but here you go:
We're pleased to announce the availability of a new major version, Rx v6.0.
- 🚀 First update in 2.5 years, with support for .NET 7.0, .NET 6.0, .NET Standard 2.0, .NET Framework 4.7.2
- ✂️ Support for trimming
- 🪲 Now using .snupkg for symbols
- 💥 New options for dealing with unhandled exceptions
- 🛠️ Modernised tooling and DevOps processes to reflect .NET as of H1 2023
- 💪 288 hours of effort went into this release.
- 🤞 Much more to come in v7.0 in H2 2023
Get the release:
- 📦 https://www.nuget.org/packages/System.Reactive/6.0.0
- 📄 https://github.com/dotnet/reactive/releases/tag/rxnet-v6.0.0
On a related note, you may have missed that we also made the first preview release of Async Rx.NET available 2 months ago.
- 📢 Alpha Release of Rx with much deeper support for
- ❌ No tests, benchmarks, docs or samples yet.
Get the release:
- 📦 https://www.nuget.org/packages/System.Reactive.Async/6.0.0-alpha.3
- 📄 https://github.com/dotnet/reactive/releases/tag/asyncrxnet-v6.0.0-alpha.3
And now we resume normal service, in the form of a great big detailed blog post.
Live data sources are fundamentally important in computer systems. Financial applications depend on a swift response to timely information. Computer networks have always been able to provide extensive information about their health and operation. Utility companies such as water providers have vast numbers of devices monitoring their operations. User interface and game building frameworks report user interactions in great detail. Delivery vans continuously report their progress. Aircraft provide performance telemetry to detect potential maintenance issues before they become serious problems, and cars are now starting to do the same. Many of us wear or carry devices that track our physical activity and even vital signs. And the improvements in machine learning have enriched the insights that can be derived from the ever-increasing volume and variety of live data.
At endjin, we have always thought that the Reactive Extensions to .NET are a brilliant way to work with event streams. They elevate live data sources to first class citizens, enabling our code to work with them as naturally as working with arrays and other collections. (For more information about Rx, read Carmel Eve's Rx Operators Deep Dive series, and also Richard Kerslake's blogs on Event stream manipulation for Rx with semantic logging.) Rx has certainly proven popular, with approximately 120 million downloads to date.
Rx was a significant contribution that .NET made to the world of programming (as is clear from how enthusiastically some of its spin-offs such as RxJS have been adopted in other ecosystems). Like many people, we were disappointed to see the project become moribund—the last release, v5.0.0, shipped in November 2020, and work on Rx.NET ground to a halt shortly after that.
The project's previous maintainers no longer had the time to keep the project moving forward. And although the .NET Foundation provides support in the form of DevOps infrastructure, it doesn't supply resources to support ongoing development. Projects depend on active support from their maintainers. One of the benefits of an open source project being under the .NET Foundation's governance is that in cases like this, where an important, widely used project is effectively abandoned, it is possible for them to take back control if new maintainers can be found, ensuring the project's continuity. As it happens, the Foundation took a light-touch approach here—they didn't want to force a change of ownership, so instead they set up meetings between endjin and the current maintainers. This worked out well—they agreed to make us the new maintainers, so the .NET Foundation didn't have to do anything beyond opening up lines of communication. But if the maintainers had been unreachable for any reason, they would have been able to regain control to keep the project alive.
At endjin, we believe that corporate use of open source has to be a two-way street. If companies benefit from open source projects without putting anything back in, those projects will inevitably wither, to the detriment of the whole ecosystem. If open source is to be sustainable, companies that benefit from it need to put something back. That's why we committed significant resources to get Rx.NET back on track, meaning that today, for the first time in two and a half years, there's a new release of Rx.NET.
What's new in Rx.NET 6.0?
The single most important change from Rx 5.0 to 6.0 is support for the current versions of .NET: .NET 6.0 and .NET 7.0. (Of course, we also continue to support .NET Framework 4.7.2 and higher, .NET Standard 2.0, and UWP, just like Rx 5.0 did.)
As it happens, Rx 5.0 did work on .NET 6.0 and .NET 7.0, but there was no way for potential users to know that. If you were to look at a NuGet package, and see that it targets out-of-support frameworks such as
net5.0, and that it hasn't shipped a new version since just after .NET 5.0 shipped in November 2020, that wouldn't give you a lot of confidence that it's going to work today.
So with Rx 6.0, we removed the targets corresponding to versions of .NET that Microsoft no longer supports, and added the
net6.0 target. Our test projects target
net7.0, the two versions of .NET Microsoft currently supports.
.NET 7.0 support
I want to make it absolutely clear that the
System.Reactive NuGet package fully supports .NET 7.0. The reason this needs clarification is that it doesn't include a
net7.0 target. This is a source of some confusion—developers sometimes look at the list of targets for a NuGet package, and assume that if a target runtime is not explicitly listed, then it is not supported. But that's not true.
Any package that targets
net6.0 will run on .NET 7.0. In fact it's perfectly valid for a NuGet package to specify targets corresponding to out-of-support versions of .NET, because current .NET runtimes support packages that target older versions. For example, a package targeting
netcore3.1 will run on .NET 7.0 even though .NET Core 3.1 went out of support some time ago.
What really matters is whether a library is tested and supported on the runtime you want to use. If a package includes a
net7.0 target, then that obviously gives you good reason to suppose that it has been tested on .NET 7.0, but just because a package doesn't include binaries specifically for the
net7.0 Target Framework Moniker (TFM) doesn't mean it's not supported on .NET 7.0.
In general, it's best for NuGet packages to minimize the number of different targets. Older versions of Rx caused problems by including different DLLs for different versions of the .NET Framework. This didn't work well with plug-ins: if a Visual Studio extension had been built for .NET FX 4.6 and it loaded Rx, that would cause problems for any extensions built for .NET FX 4.7, because the copy of Rx for the older TFM would already have been loaded at that point. This has caused some serious grief for Rx users in the past, so we want to avoid having more targets than is absolutely necessary.
Currently, the only benefit that adding a
net7.0 target could offer is that it would unambiguously signal to anyone looking in NuGet that Rx does indeed support .NET 7.0. But it would be identical to the
net6.0 version because there's nothing in .NET 7.0 that would mean we'd want to build a version of Rx for it that is in any way different from the one that runs on .NET 6.0. (We still need multiple targets. Much as we'd like to target nothing but
netstandard2.0 so that one DLL will work everywhere, we can't. We need a
net6.0 version to support trimming. And currently, we need separate targets for UWP, .NET Framework, and a Windows-specific .NET 6.0 TFM to provide support for the differences in UI frameworks, although see below for our plans to change this in Rx v.next.) Since we don't want to have any more targets than is necessary, we support .NET 7.0 through the
One feature people have been requesting increasingly often lately is better support for trimming. Recent versions of .NET have provided a wider range of options for how applications are packaged and deployed, including ever improving support for pre-compilation. Additionally, many people want to create applications that bundle a copy of the runtime, to avoid imposing system-wide pre-requisites. Trimming is an important feature that makes this more practical: the .NET SDK is able to work out which bits of code our application is really using to dramatically reduce the amount of code that needs to be shipped (and, where applicable, pre-compiled).
Rx 5.0 did not have any of the annotations for supporting trimming, so if you added a reference to Rx, you were going to ship a complete copy of the entire package even if you were only using a tiny fraction of its capabilities. For certain application deployment models, this could have a significant effect on download times and therefore startup performance. In practice, this was often enough to put people off using Rx. .NET 7.0 improved this slightly, because it is able to perform some trimming even on components that are not annotated, but since a lot of people don't want to use non-LTS versions of .NET, they needed trimming to work on .NET 6.0.
So we've added trimmability annotations to the
net6.0 target. Testing this in a client-side Blazor application, we saw a significant reduction in the size of the
System.Reactive.dll component. Here's how it looked without trimming:
And here's how it looked with trimming:
That's a reduction from 1.5MB to about 32KB! Obviously, the size will depend on how much Rx functionality you use, so YMMV, but we think this will be a game changer for some scenarios.
The main purpose of the v6.0 release is to re-align Rx with the current .NET ecosystem, rather than to add significant new functionality. Nonetheless, there are a couple of new features.
There are certain situations in which unhandled exceptions from a
Task that has been wrapped by Rx can cause
TaskScheduler.UnobservedExceptions. Rx 6.0 adds new overloads enabling applications to opt into swallowing these failures silently. This was a long-standing feature request, and although swallowing exceptions is mostly a bad idea, Rx has always done this in certain scenarios where failures occur after application code has unsubscribed, meaning that Rx has no way of reporting the exception. (On the whole, we would advise trying to avoid getting into situations where you depend on this change, but the fact is that there are some cases where it can't be avoided.) Rx 5.0's behaviour here with wrapped tasks was inconsistent—it did not swallow exceptions in scenarios where they would have been swallowed if
Task was not in use. This continues to be the default behaviour to preserve backwards compatibility, but the new overloads enable applications to get the same exception-swallowing behaviour for
Task-based scenarios as has always been the case for non-
SingleAssignmentDisposableValue type is now public. Various other disposable types have long been public, and the only reason for this one being
internal was that it's slightly easier to make mistakes while using it. In practice, though, use of the disposable types has tended to be restricted to relatively advanced Rx usage, so it seemed reasonable to make this available, since some people wanted it.
Aligning with current build tooling
The majority of the work in this release has gone into re-aligning the code with current .NET tooling. At the start of this year, Rx.NET wouldn't build on current versions of Visual Studio or the .NET SDK. This made it difficult for anyone to contribute to the project, because it meant setting up a machine with development tools that were out of date by exactly the right amount.
Our goal was to remove this source of friction, enabling future development work to proceed efficiently. So we have made numerous changes to enable the code to build on today's versions of the tools.
One such change might raise a few eyebrows: we have changed all the unit tests from XUnit to MSTest. Doubtless many will see this as a retrograde step, but it was necessary because XUnit is no longer capable of running tests on UWP, and xUnit's maintainers do not seem to have any interest in fixing that. Rx has long provided support specifically for UWP, which is why it offers a UWP-specific target. Obviously we need to run tests for this, and this is simply impossible on Visual Studio 2022 if you use XUnit. MSTest is the only .NET test framework that provides good support for UWP. As it happens, Rx used to use MSTest years ago, and although it migrated to xUnit, it never used any xUnit-specific functionality, so it was fairly straightforward to move back.
As part of the work to bring the tooling up to date, we've also been expanding on the internal documentation. We are starting to add ADRs (Architectural Decision Records) explaining the context and thinking behind decisions. On a related note, we've updated the repository readme, and have also added readmes to the NuGet packages.
When we embarked on this project, we published an Rx Roadmap for 2023. As it says there, our primary goal for Rx v7.0 is to address the problems with code bloat that afflict some UI frameworks when they try to use Rx. The basic problem is that any application that targets a moderately recent Windows-specific TFM (e.g.
net6.0-windows10.0.19041) ends up with dependencies on both Windows Forms and WPF, even if it's using neither. This means that if you try to use Rx in frameworks such as Avalonia, and if you are doing self-contained application deployments, your application ends up copying all of the Windows Forms and WPF components onto the target system. This makes application installers tens of megabytes larger than they otherwise would be.
We believe the only way to solve this will be to move UI-framework-specific functionality out of the main
System.Reactive component and into separate packages. When you use Rx v7.0, you won't get any WPF functionality unless you add a reference to the package that provides WPF-specific Rx features. Likewise for Windows Forms. We also want to make it possible for other UI framework authors to write first-class integration with Rx, so we need to make sure that the Windows Forms, WPF, and UWP code in Rx isn't enjoying any kind of special support that would be unavailable to 3rd parties.
This will be a breaking change, and it needs careful handling to avoid re-introducing the problems that led to earlier versions of Rx choosing to bundle everything into a single package in the first place. Since this will entail a significant shakeup of the Rx packages, we expect this to be the only major change in the next version.
That said, we will be triaging all open issues. Some might be addressed in future v6.x releases, some might make it into v7.0, and some might have to wait for later versions (if we do them at all).
We are also working on documentation. We plan to provide updated, comprehensive explanatory material introducing Rx, and explaining its design. We also have various Rx videos that Microsoft published but have subsequently removed, which we are working to put back into public view.
We will be working to get AsyncRx.NET suitable for production use. We will be adding a test suite, and reviewing its design.
Please try it out
This new release of
System.Reactive is available on NuGet today. If you're using Rx in your application, please try upgrading. If you have any problems, please file issues at https://github.com/dotnet/reactive/issues, or comment on the discussion at https://github.com/dotnet/reactive/discussions/1868. Meanwhile, we hope you enjoy this new version of the Reactive Extensions for .NET.
More Rx content
As well as the two series from Carmel Eve's Rx Operators Deep Dive and Richard Kerslake's Event stream manipulation for Rx with semantic logging, you can find further information here:
- Rx playlist (on the endjin YouTube channel)
- Rx 101 Workshop
- Rx talk for the dotnetsheff user group
- https://reaqtive.net/ — a persistent, reliable, distributed stream processing system based on Rx