Splat: the cross-platform building blocks under ReactiveUI
Cross-platform building blocks for .NET applications. Provides service location and dependency resolution, logging, platform-neutral images, colours and geometry, and unit test and design mode detection.
At a glance
- What is it?
- Splat is the ReactiveUI project's collection of .NET primitives that let shared code do things that otherwise need platform conditionals: load an image, hold a colour, read a log, find a service, and tell whether it is running under a test runner or in a designer. The abstraction is deliberately leaky, with a ToNative() call at the boundary, and the service location half was rewritten in version 19 around AOT friendly resolvers.
- Who is it for?
- Adopt Splat when you are writing a .NET codebase that has to target WPF, WinForms, WinUI 3, MAUI and Avalonia from shared view models, because the alternative is a codebase full of conditional compilation.
- Can I use it commercially?
- Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
- Is it still maintained?
- Yes. The repository received new commits within the last day.
- What is it written in?
- Mainly C#, according to GitHub's language statistics.
Answers come from the project's GitHub data, last synced on October 23, 2026, and from our analysis. They are not legal advice.
Editorial analysis
The leaky abstraction, and why it leaks on purpose
The README opens with a complaint that anyone who has written shared mobile code will recognise. Certain types of things are basically impossible to do in cross-platform mobile code today, yet there is no reason why. Writing a ViewModel that handles loading a gallery of pictures from disk will be completely riddled with ifdefs and basically unreadable.
Splat's answer is a usable leaky abstraction above platform code. The word leaky is the important part, and the README explains what that means in concrete terms. It always provides an extension method ToNative() and FromNative(), which converts the abstraction to the platform-specific version. Load the image in the cross-platform code, then call ToNative() in your view to actually display it.
So the boundary is drawn once, explicitly, at the point where the platform-specific type is genuinely needed, and the shared code above it is free of conditionals. That is a design choice with a cost, and the cost is that every cross-platform operation eventually needs an escape hatch. A library that hid the leak would have to model every platform's image type, which is not possible, so the leak is moved to a single named call that a reader can find.
The feature list is short and concrete. Cross-platform image loading and saving. A port of System.Drawing.Color for portable libraries, which is a small library that removes a hard dependency on a Windows-only type. Cross-platform geometry primitives, PointF, SizeF and RectangleF, plus a bunch of additional extension methods to make using them easier. A way to detect whether you are in a unit test runner or in design mode. A cross-platform logging framework. And simple yet flexible service location.
That is six things, which is a good sign for a library whose whole job is to be depended on by everything else in a framework.
What you get in the package, and where it runs
The package is on NuGet as Splat, and the README describes it with a pun that is also a piece of advice: always be NuGetting.
The binary targets are listed, and they tell you how widely the library is meant to be usable. .NET Framework 4.6.2, .NET Framework 4.7.2, .NET Standard 2.0, .NET 6.0 and .NET 8.0. The two framework targets are there for the desktop applications that still live on them, the standard target is what reaches the rest of the ecosystem, and the two modern targets are for current .NET.
The list of platforms it works with is longer than the list of targets, and it is the interesting part. WPF, Windows Forms, WinUI 3, MAUI with WinUI, Android, iOS and Mac, and Avalonia. That set covers the three major desktop UI stacks on Windows, the mobile stack, and the cross-platform desktop stack, which is what a library like this exists to serve.
The publishing target list and the platform list are not the same thing, and the difference is instructive. A netstandard2.0 build of the library can be consumed by all of those, but the platform-specific behaviour inside it is selected at run time, which is why the library needs a way to know what it is running on. That is what the ToNative() and FromNative() conversions are for, and it is why the package ships implementations for each platform rather than only the abstraction.
The project also carries an AGENTS.md and a CLAUDE.md at the root, a code of conduct, a contributing guide, a security policy, a codecov configuration, an .editorconfig and a .gitattributes file, plus a tests.runsettings file, which is the shape of a maintained library in a large organisation.
Service location, two properties and three kinds of registration
The service location half of Splat is the part with the most design detail, and it is deliberately modest. The README describes it as a simple service location implementation that is optimized for Desktop and Mobile applications, while still remaining reasonably flexible.
The design has two parts, and the split is the interesting bit. AppLocator.Current is the property to use to retrieve services, and it is a static variable that can be set on startup, to adapt Splat to other dependency injection frameworks. AppLocator.CurrentMutable is the property to use to register services. The README notes the team has been working from version 7 onward to make it easier to use your framework of choice, and the same sentence appears again later in the section.
Reading a service is two calls:
// To get a single service registration
var toaster = AppLocator.Current.GetService<IToaster>();
// To get all service registrations
var allToasterImpls = AppLocator.Current.GetServices<IToaster>();Registration has three shapes, and the difference between them is the difference between a factory, an instance and a deferred instance:
// Create a new Toaster any time someone asks
Locator.CurrentMutable.Register(() => new Toaster(), typeof(IToaster));
// Register a singleton instance
Locator.CurrentMutable.RegisterConstant(new ExtraGoodToaster(), typeof(IToaster));
// Register a singleton which won't get created until the first user accesses it
Locator.CurrentMutable.RegisterLazySingleton(() => new LazyToaster(), typeof(IToaster));The default implementation also allows new types to be registered at runtime, which is the property that lets a test substitute an implementation.
The toaster example is not accidental, and the naming is worth reading. The registry resolves by interface, it can hold several implementations of that interface at once, GetServices returns all of them, and the examples deliberately use a good toaster, an extra good toaster and a lazy toaster. This is a service locator with multiplicity built in, which is a different shape from the one-registration-per-interface model most containers enforce, and it is why a UI toolkit can have a real toaster in production and a fake one in a test without either of them knowing about the other.
The version 19 resolvers and what the table actually decides
Starting with v19, Splat provides two high-performance resolver implementations optimized for AOT compilation, GlobalGenericFirstDependencyResolver and InstanceGenericFirstDependencyResolver. The README states that both deliver significantly better performance than the legacy ModernDependencyResolver while supporting different isolation requirements, and that InstanceGenericFirstDependencyResolver is now the default container.
The table is the part to read carefully, because the two options differ in a way that matters more than the performance number.
GlobalGenericFirstDependencyResolver stores its containers process-wide in static fields, so the isolation is shared across all resolver instances and the performance is described as fastest, with direct static access and minimal memory cost. Its use case is a single global service locator, which is the classic pattern.
InstanceGenericFirstDependencyResolver stores containers per resolver instance, using a ConditionalWeakTable. The isolation is per resolver instance, the performance is very fast with one additional weak table lookup, and the memory is low with weak references, which the README describes as GC-friendly. Its use case is multiple independent resolvers and testing scenarios.
Both are lock-free on reads and thread-safe on writes, and both are AOT compatible, which is the point of the rewrite: a resolver that reflects over types is not AOT friendly, and the GenericFirst naming refers to the way the resolution is keyed.
The legacy ModernDependencyResolver remains supported for backward compatibility, and the README says new applications should strongly prefer one of the GenericFirst resolvers. That is a clear signal, and it is also a migration note: code that swaps the container implementation is unlikely to be affected, but code that depended on the shared static behaviour of the older resolver may be, and the default changing to the instance variant means two resolvers in the same process are no longer the same container by default.
Knowing whether you are in a test or in a designer
One feature deserves its own mention because it is the kind of thing nobody thinks to need until they hit it: detecting whether you are in a unit test runner, and detecting whether you are in design mode.
The test detection is a single call:
// If true, we are running unit tests
ModeDetector.InUnitTestRunner(); Why a library would need this is the interesting part. Cross-platform code that does real work, loading a file, showing a dialog, hitting the network, has to behave differently under a test runner, where the platform services do not exist, and in a designer, where the view is being rendered outside an application and the view model is being fed arbitrary values. Without a detection mechanism, the usual outcome is a code base littered with checks against an environment variable, or a test that fails because the platform stub was never wired up.
Splat gives the cross-platform code one place to ask, and the same abstraction covers the design mode case, which matters for the ReactiveUI family in particular, where view models are designed and edited in a designer tool rather than only at run time.
The logging piece completes the picture. Splat provides a cross-platform logging framework, and the package's topic list names NLog as the implementation behind it, which is a sensible choice: rather than inventing a logging API, it gives the application one logging surface that works on every platform and can be pointed at the logging library the application already uses.
Taken together with the service location and the platform primitives, the four parts of Splat cover the two problems a shared view model layer actually runs into, which are where do I get my service, and how do I log, plus the small platform facts such as the colour type and the image type that a view model needs but should not hard code.
Maintenance, licensing and the judgement to make
The licence is MIT, with the LICENSE file at the repository root, and the project is published on NuGet as Splat. This describes the published terms rather than legal advice.
The release record is steady and recent. Version 21.0.0 came out on 2026-08-03, 20.2.0 on 2026-07-25 and 20.1.0 on 2026-07-23, and the last push was on 2026-10-19. The repository is not archived, and the two major version steps in the visible history, 20 and 21, come with a 19 that carried the resolver rewrite, which suggests the versioning tracks the changes that can break a consumer.
The project belongs to the ReactiveUI organisation, and the README lists the core team by name and location, Glenn Watson, Rodney Littles II, David Vreony and Chris Pulman, and the topics list includes hacktoberfest, which points to the project accepting outside contributions through that route.
The judgement on Splat is mostly about whether the leaky abstraction suits your code. If your shared layer has to hand a platform object to a view, which is the normal shape of a view model, then ToNative() at that one boundary is a good deal cheaper than a set of conditional compilation blocks, and the other four parts, the colour type, the geometry primitives, the logging surface and the service locator, remove further reasons to branch. If your application is single platform and simple, then all of this is machinery you do not need, and the standard library is already fine.
The other judgement is about the service location half, and the README itself pushes you towards a decision. It is designed so that AppLocator.Current can be replaced at startup with your own container, and it says the team has been working since version 7 to make that easier. A team that already has a dependency injection container should read the section on swapping the locator before adopting Splat's own, because the value of Splat's service location is that it is the fallback, not that it is the best option.
Editorial conclusion
Adopt Splat when you are writing a .NET codebase that has to target WPF, WinForms, WinUI 3, MAUI and Avalonia from shared view models, because the alternative is a codebase full of conditional compilation. It is the wrong tool if you want a full inversion of control container, since its service location is deliberately simple and the README pushes you towards your own framework, and the wrong tool if you need AOT-safe trimming without accepting the resolver choices of version 19 and later. Before integrating, read the resolver table, because the default changed to InstanceGenericFirstDependencyResolver and the isolation semantics differ from the global variant, and decide whether the ToNative() boundary sits in the right layer of your own code.
Frequently asked questions
What does Splat provide for cross-platform .NET code?
Cross-platform image loading and saving, a portable port of System.Drawing.Color, the PointF, SizeF and RectangleF geometry primitives with extra extension methods, detection of a unit test runner and of design mode, a cross-platform logging framework, and simple service location.
How do I register and retrieve services with Splat?
AppLocator.Current is for retrieving and AppLocator.CurrentMutable is for registering. Registration comes in three forms, Register for a factory that creates a new instance each time, RegisterConstant for a singleton instance, and RegisterLazySingleton for a singleton that is not created until it is first used.
What changed in the Splat dependency resolvers in version 19?
Two AOT optimized resolvers were added, GlobalGenericFirstDependencyResolver with process-wide static containers and InstanceGenericFirstDependencyResolver with per-instance containers held in a ConditionalWeakTable. The instance variant is now the default, and the legacy ModernDependencyResolver remains supported for compatibility.
How does Splat let cross-platform code use platform types?
The abstraction is deliberately leaky and provides the extension methods ToNative() and FromNative(). Cross-platform code works with the Splat types, and the view layer calls ToNative() at the boundary to obtain the platform-specific object to display.
Which platforms and targets does the Splat package support?
The package ships binaries for .NET Framework 4.6.2 and 4.7.2, .NET Standard 2.0, .NET 6.0 and .NET 8.0, and the README says it works with WPF, Windows Forms, WinUI 3, MAUI on WinUI, Android, iOS and Mac, and Avalonia.
Official sources
Add this badge to your README
If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.
[](https://hysenlabs.com/projects/reactiveui-splat)Was this useful?