A library to make things cross-platform that should be.
$ dotnet add package Splat.DrawingCertain types of things are basically impossible to do in cross-platform
mobile code today, yet there's 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 aims to fix that, by providing a usable leaky abstraction above platform
code. It is leaky, because 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.
Splat currently supports:
Glenn Watson Melbourne, Australia |
Rodney Littles II Texas, USA |
David Vreony UK |
Chris Pulman UK |
Always Be NuGetting. Package contains binaries for:
// If true, we are running unit tests
ModeDetector.InUnitTestRunner(); Splat provides a simple service location implementation that is optimized for Desktop and Mobile applications, while still remaining reasonably flexible.
There are 2 parts to the locator design:
To get a service:
// To get a single service registration
var toaster = AppLocator.Current.GetService<IToaster>();
// To get all service registrations
var allToasterImpls = AppLocator.Current.GetServices<IToaster>();Locator.Current is a static variable that can be set on startup, to adapt Splat to other DI/IoC frameworks. We're currently working from v7 onward to make it easier to use your DI/IoC framework of choice.
The default implementation of Service Location also allows new types to be registered at runtime.
// 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));Starting with v19, Splat provides two high-performance resolver implementations optimized for AOT compilation: GlobalGenericFirstDependencyResolver and InstanceGenericFirstDependencyResolver. Both deliver significantly better performance than the legacy ModernDependencyResolver while supporting different isolation requirements. By default InstanceGenericFirstDependencyResolver is now the default container.
Note:
ModernDependencyResolver remains supported for backward compatibility, but new applications should strongly prefer one of the GenericFirst resolvers.
| Feature | GlobalGenericFirstDependencyResolver | InstanceGenericFirstDependencyResolver |
|---|---|---|
| Container Storage | Process-wide static containers | Per-resolver instance containers (ConditionalWeakTable) |
| Isolation | Shared across all resolver instances | Isolated per resolver instance |
| Performance | Fastest - direct static access | Very fast - one additional CWT lookup |
| Memory | Minimal - static fields only | Low - weak references, GC-friendly |
| Use Case | Single global service locator | Multiple independent resolvers, testing scenarios |
| Thread Safety | Lock-free reads, thread-safe writes | Lock-free reads, thread-safe writes |
| AOT Compatible | Yes | Yes |
The service locator is simple to use - register services at startup, then resolve them when needed:
// Register services at application startup
Locator.CurrentMutable.Register<IToaster>(() => new Toaster());
Locator.CurrentMutable.RegisterConstant<IConfiguration>(myConfig);
Locator.CurrentMutable.RegisterLazySingleton<ILogger>(() => new FileLogger());
// Resolve services anywhere in your application
var toaster = Locator.Current.GetService<IToaster>();
var config = Locator.Current.GetService<IConfiguration>();Key Concepts:
Register<IToaster>(() => new FastToaster(), "Fast")RegisterLazySingleton<T>RegisterConstant<T>Splat provides two resolver implementations with identical APIs but different tradeoffs around performance and state isolation.
Short version
Performance vs Isolation Tradeoff
The Global resolver achieves its speed by sharing static generic containers across the process. This is intentional and safe, but it means registrations are visible globally.
If global container state feels confusing or risky for your use case, choose the Instance resolver. The performance difference is measurable but rarely user-visible outside of very hot paths.
Based on internal benchmarks using realistic application workloads:
ModernDependencyResolver.For most applications, both are “fast enough”. The deciding factor is how much shared global state you are willing to expose to users.
The table below summarizes the practical performance and behavioral differences between Splat’s dependency resolvers, based on internal benchmarks using .NET 10 and realistic application workloads.
| Operation / Concern | GlobalGenericFirst | InstanceGenericFirst | ModernDependencyResolver |
|---|---|---|---|
| Service resolution (hit) | Fastest | Very fast (slightly slower than Global) | Slow |
| Service resolution (miss) | Fastest | Very fast (slightly slower than Global) | Very slow |
| Mixed workload (real application usage) | Fastest | ~25–30% slower than Global | ~3–4× slower |
| Bulk registration (startup) | Fastest | ~25–30% slower | Very slow (O(n²)) |
| Memory allocations | Lowest | Low (slightly higher than Global) | Very high |
| Scales to hundreds of services | Yes | Yes | Poor |
| Container state | Process-wide shared | Isolated per resolver instance | Isolated per resolver instance |
| Test isolation | Requires manual cleanup | Automatic | Automatic |
| AOT / NativeAOT support | Full | Full | Poor |
| Recommended for new code | Yes | Yes | No |
| Aspect | Global | Instance |
|---|---|---|
| Container state | Shared process-wide | Isolated per resolver |
| Surprise factor | Higher | Lower |
| Test isolation | Requires manual cleanup | Automatic |
| Performance | Fastest | Slightly slower |
| Debuggability | Harder (implicit coupling) | Easier (explicit ownership) |
For application authors
For library authors
For tests
// Global resolver (default) - all instances share registrations
var resolver1 = new GlobalGenericFirstDependencyResolver();
var resolver2 = new GlobalGenericFirstDependencyResolver();
resolver1.Register<IService>(() => new ServiceA());
// resolver2 can also see ServiceA registration!
// Instance resolver - each has independent registrations
var resolver1 = new InstanceGenericFirstDependencyResolver();
var resolver2 = new InstanceGenericFirstDependencyResolver();
resolver1.Register<IService>(() => new ServiceA());
resolver2.Register<IService>(() => new ServiceB());
// Completely isolated - resolver1 only sees ServiceA, resolver2 only sees ServiceBBoth resolvers use a generic-first, two-tier architecture designed for maximum performance:
When you call generic methods like GetService<IToaster>(), the resolver uses static generic containers:
// Each type T gets its own static container - no dictionary lookups needed!
internal static class Container<T>
{
private static readonly Entry<Registration<T>> Entries = new();
// ... resolution logic
}Performance characteristics:
When you call Type-based methods like GetService(typeof(IToaster)), the resolver uses a concurrent dictionary:
// Fallback for Type-based calls and interop with legacy code
internal static class ServiceTypeRegistry
{
private static readonly ConcurrentDictionary<(Type, string?), Entry<Func<object?>>> Entries;
// ... resolution logic with per-entry locking
}Why the fallback exists:
Contracts allow multiple registrations for the same type:
// Register different implementations with different contracts
Locator.CurrentMutable.Register<IToaster>(() => new FastToaster(), "Fast");
Locator.CurrentMutable.Register<IToaster>(() => new SlowToaster(), "Slow");
// Retrieve specific implementation
var fastToaster = Locator.Current.GetService<IToaster>("Fast");Contracts use a separate ContractContainer<T> for each type, providing the same lock-free performance as the non-contract path.
GenericFirstDependencyResolver is designed for high-concurrency scenarios:
Lock-free reads:
GetService calls use lock-free fast pathsVolatile.Write for proper memory orderingFine-grained locking for writes:
Clear semantics:
Clear() operations are designed as "stop-the-world" teardown operationsAlways prefer generic methods when the type is known at compile-time:
// FAST: Generic method - no dictionary lookup, no boxing
var toaster = Locator.Current.GetService<IToaster>();
// SLOW: Type-based method - dictionary lookup + potential boxing
var toaster = (IToaster)Locator.Current.GetService(typeof(IToaster));Benchmark results (500 registrations):
Transient (new instance every time):
Locator.CurrentMutable.Register<IToaster>(() => new Toaster());Singleton (shared instance):
// Created immediately
Locator.CurrentMutable.RegisterConstant<IToaster>(new Toaster());
// Created on first access (lazy)
Locator.CurrentMutable.RegisterLazySingleton<IToaster>(() => new Toaster());Multiple implementations:
// Get all registered implementations
Locator.CurrentMutable.Register<IPlugin>(() => new PluginA());
Locator.CurrentMutable.Register<IPlugin>(() => new PluginB());
var allPlugins = Locator.Current.GetServices<IPlugin>(); // Returns bothConstructor-based registration (requires parameterless constructor):
// Registers a factory that calls new TImplementation()
Locator.CurrentMutable.Register<IToaster, Toaster>();Due to overload resolution, there's one edge case when registering with a null service type (rare in practice):
// AMBIGUOUS: Calls generic method Register<int> with contract: null
resolver.Register(() => 5, null);
// CORRECT: Use named parameters to call non-generic method with serviceType: null
resolver.Register(() => 5, serviceType: null);In normal usage, this ambiguity doesn't occur:
// Unambiguous - calls generic method
resolver.Register<IToaster>(() => new Toaster());
// Unambiguous - calls Type-based method
resolver.Register(() => new Toaster(), typeof(IToaster));ModernDependencyResolver (v1-v18) had several performance and correctness issues:
O(n²) Registration Growth
Global Lock Contention
ReaderWriterLockSlim for all operationsReflection-Heavy, Poor AOT Support
Type-based dictionaries even for generic callsMemory Inefficiency
GenericFirstDependencyResolver (v19+) fixes all these issues:
Migration is seamless - GenericFirstDependencyResolver implements the same IMutableDependencyResolver interface, so existing code works without changes. Just recompile against v19+ to get the performance benefits.
When to use Type-based methods:
When to use generic methods (preferred):
There is a source generator that will inject constructor and properties. See here for instructions.
For each of the provided dependency resolver adapters, there is a specific package that allows the service locator to be implemented by another ioc container.
Note: When using ReactiveUI and overriding Splat's default behavior, you have to be sure to initialize ReactiveUI before your container finalizes.
Please note: If you are adjusting behaviours of Splat by working with your custom container directly. Please read the relevant projects documentation on REPLACING the registration. If the container supports appending\ multiple registrations you may get undesired behaviours, such as the wrong logger factory being used.
When targeting AOT or trimming scenarios, you can configure Splat (and consumers such as ReactiveUI) without reflection using the AppBuilder and IModule pattern.
using Splat;
using Splat.Builder;
// App services
public sealed class CoreModule : IModule
{
public void Configure(IMutableDependencyResolver resolver)
{
// register your services here
resolver.RegisterLazySingleton(() => new ApiClient(), typeof(IApiClient));
resolver.Register(() => new MainViewModel(resolver.GetService<IApiClient>()!), typeof(IMainViewModel));
}
}
// Logging (Serilog adapter) - requires Splat.Serilog package
using Splat.Serilog;
public sealed class SerilogModule : IModule
{
public void Configure(IMutableDependencyResolver resolver)
{
// assumes Serilog.Log has been configured elsewhere
resolver.UseSerilogFullLogger();
}
}
// Cross-platform drawing (IBitmapLoader) - requires Splat.Drawing package
public sealed class DrawingModule : IModule
{
public void Configure(IMutableDependencyResolver resolver)
{
resolver.RegisterPlatformBitmapLoader();
}
}Tip: You can also apply a module immediately to the current locator without a builder:
new CoreModule().Apply(); // calls module.Configure(Locator.CurrentMutable)using Splat;
using Splat.Builder;
// choose the resolver to configure (Locator.CurrentMutable by default)
new AppBuilder(Locator.CurrentMutable)
.UsingModule(new CoreModule())
.UsingModule(new SerilogModule())
.UsingModule(new DrawingModule())
// ad-hoc registrations can be added too
.WithCustomRegistration(r => r.RegisterConstant(new AppConfig("Prod"), typeof(AppConfig)))
.Build();If you adapt an external container to Splat first, UseCurrentSplatLocator() ensures later module registrations are applied to that container instance.
Microsoft.Extensions.DependencyInjection example:
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Splat;
using Splat.Builder;
using Splat.Microsoft.Extensions.DependencyInjection;
using Splat.Microsoft.Extensions.Logging;
var services = new ServiceCollection();
services.UseMicrosoftDependencyResolver(); // set AppLocator.Current to MS.DI resolver backed by IServiceCollection
// register framework logging provider that forwards to Splat
services.AddLogging(b => b.AddSplat());
// build container and rebind AppLocator.Current to the built provider
var provider = services.BuildServiceProvider();
provider.UseMicrosoftDependencyResolver();
new AppBuilder(AppLocator.CurrentMutable)
.UseCurrentSplatLocator() // target whatever AppLocator.CurrentMutable points to (MS.DI in this case)
.UsingModule(new CoreModule())
.WithCustomRegistration(r =>
{
// forward Microsoft.Extensions.Logging to Splat
var factory = provider.GetRequiredService<ILoggerFactory>();
r.UseMicrosoftExtensionsLoggingWithWrappingFullLogger(factory);
})
.Build();The same pattern works with other adapters (Autofac, DryIoc, SimpleInjector, Ninject) after setting AppLocator.Current to the adapter’s resolver.
AppBuilder is extensible. Override WithCoreServices() to ensure common registrations are always applied before modules:
using Splat;
using Splat.Builder;
public sealed class MyAppBuilder : AppBuilder
{
public MyAppBuilder(IMutableDependencyResolver resolver) : base(resolver) { }
public override AppBuilder WithCoreServices()
{
// register core bits once for your app
// e.g., default logger, scheduler, or platform services
// resolver is obtained per registration in Build(),
// so prefer idempotent registrations guarded by HasRegistration
return this;
}
}
// usage
new MyAppBuilder(Locator.CurrentMutable)
.UsingModule(new CoreModule())
.Build();Notes:
Splat provides a simple logging proxy for libraries and applications to set up. By default, this logging isn't configured (i.e. it logs to the Null Logger). To set up logging:
ILogger using Service Location.IEnableLogger
interface (this is a tag interface, no implementation actually needed).Log method to write log entries:this.Log().Warn("Something bad happened: {0}", errorMessage);
this.Log().ErrorException("Tried to do a thing and failed", exception);For static methods, LogHost.Default can be used as the object to write a log
entry for. The Static logger uses a different interface from the main logger to allow capture of additional
caller context as it doesn't have the details of the class instance etc. when compared to the normal logger.
To get the benefit of these you don't need to do much as they are optional parameters at the end of the methods
that are utilised by the compiler\framework. Currently we only capture CallerMemberName.
Splat has support for the following logging frameworks
| Target | Package | NuGet |
|---|---|---|
| Console | Splat | |
| Debug | Splat | |
| Log4Net | Splat.Log4Net | |
| Microsoft Extensions Logging | Splat.Microsoft.Extensions.Logging | |
| NLog | Splat.NLog | |
| Serilog | Splat.Serilog |
First configure Log4Net. For guidance see https://logging.apache.org/log4net/release/manual/configuration.html
using Splat.Log4Net;
// then in your service locator initialisation
Locator.CurrentMutable.UseLog4NetWithWrappingFullLogger();Thanks to @dpvreony for first creating this logger.
First configure Microsoft.Extensions.Logging. For guidance see https://docs.microsoft.com/en-us/aspnet/core/fundamentals/logging/
using Splat.Microsoft.Extensions.Logging;
// note: this is different from the other adapter extension methods
// as it needs knowledge of the logger factory
// also the "container" is how you configured the Microsoft.Logging.Extensions
var loggerFactory = container.Resolve<ILoggerFactory>();
// in theory it could also be
// var loggerFactory = new LoggerFactory();
/// then in your service locator initialisation
Locator.CurrentMutable.UseMicrosoftExtensionsLoggingWithWrappingFullLogger(loggerFactory);Thanks to @dpvreony for first creating this logger.
First configure NLog. For guidance see https://github.com/nlog/nlog/wiki/Tutorial and https://github.com/nlog/nlog/wiki/Configuration-file
using Splat.NLog;
// then in your service locator initialisation
Locator.CurrentMutable.UseNLogWithWrappingFullLogger();Thanks to @dpvreony for first creating this logger.
First configure Serilog. For guidance see https://github.com/serilog/serilog/wiki/Configuration-Basics
using Splat.Serilog;
// Then in your service locator initialisation
Locator.CurrentMutable.UseSerilogFullLogger()Thanks to @joelweiss for first creating this logger.
| Target | Package | NuGet |
|---|---|---|
| Splat.Drawing | Splat.Drawing |
// This System.Drawing class works, even on WinRT where it's not supposed to exist
// Also, this works in a Portable Library, in your ViewModel
ProfileBackgroundAccentColor = Color.FromArgb(255, 255, 255, 255);Later, in the view, we can use it:
ImageView.Background = ViewModel.ProfileBackgroundAccentColor.ToNativeBrush();
If targeting iOS or Mac in a cross-platform solution (e.g. iOS & Android), use the SplatColor class to define colors in your shared library (since Cocoa doesn't include System.Drawing.Color).
// In a shared library
SplatColor BackgroundColor = SplatColor.Red;// From an iOS project
UIColor bgColor = ViewModel.BackgroundColor.ToNative();
// From an Android project
Android.Graphics.Color bgColor = ViewModel.BackgroundColor.ToNative();
You can register with the Splat locators.
Locator.CurrentMutable.RegisterPlatformBitmapLoader();You can then load your images in a cross platform way:
//
// Load an Image
// This code even works in a Portable Library
//
var wc = new WebClient();
Stream imageStream = wc.OpenRead("http://octodex.github.com/images/Professortocat_v2.png");
// IBitmap is a type that provides basic image information such as dimensions
IBitmap profileImage = await BitmapLoader.Current.Load(imageStream, null /* Use original width */, null /* Use original height */);Then later, in your View:
// ToNative always converts an IBitmap into the type that the platform
// uses, such as UIBitmap on iOS or BitmapSource in WPF
ImageView.Source = ViewModel.ProfileImage.ToNative();Images can also be loaded from a Resource. On Android, this can either be a Resource ID casted to a string, or the name of the resource as as string (optionally including the extension).
var profileImage = await BitmapLoader.Current.LoadFromResource("DefaultAvatar.png", null, null);Bitmaps can also be created and saved - actually drawing on the image is beyond the scope of this library, you should do this in your view-specific code.
var blankImage = BitmapLoader.Current.Create(512.0f, 512.0f);
await blankImage.Save(CompressedBitmapFormat.Png, 0.0, File.Open("ItsBlank.png"));// If true, we are running inside Blend, so don't do anything
PlatformModeDetector.InDesignMode();Application Performance Monitoring is split into the follow sections
The table below shows the support across various APM packages
| Product | Package | NuGet | Maturity Level | Error Reporting | Feature Usage Tracking | View Tracking |
|---|---|---|---|---|---|---|
| Appcenter | Splat.AppCenter | Alpha | TODO | Native | Native | |
| Application Insights | Splat.ApplicationInsights | Alpha | TODO | Native | Native | |
| Exceptionless | Splat.Exceptionless | Alpha | TODO | Native | By Convention | |
| New Relic | N\A | N\A | Not Started | TODO | TODO | TODO |
| OpenTrace | N\A | N\A | Not Started | TODO | TODO | TODO |
| Raygun | Splat.Raygun | Prototype | TODO | By Convention | By Convention |
Splat comes with a default implementation that pushes events into your active Splat logging framework. This allows for design and testing prior to hooking up a full APM offering.
TODO
The most basic ability for feature usage tracking is to implement the Splat.ApplicationPerformanceMonitoring.IEnableFeatureUsageTracking interface. This has the same behaviour as the logging interface and allows Splat to inject whichever APM platform is registered with the ServiceLocator at initialization.
/// <summary>
/// Dummy object for testing IEnableFeatureUsageTracking.
/// </summary>
public sealed class TestObjectThatSupportsFeatureUsageTracking : IEnableFeatureUsageTracking
{
public async Task SomeFeatureIWantToTrack()
{
using (var trackingSession = this.FeatureUsageTrackingSession("featureName"))
{
try
{
// do some work here.
}
catch (Exception exception)
{
trackingSession.OnException(exception);
}
}
}
}Splat also has the notion of subfeatures, some APM platforms support this natively, others have been done by convention, which will be explained in the relevant library. Splat itself does not dictate when these should be used. It's up to you. You may have a primary feature (such as a search view) and then track buttons, etc. on that view as subfeatures.
/// <summary>
/// Dummy object for testing IEnableFeatureUsageTracking.
/// </summary>
public sealed class TestObjectThatSupportsFeatureUsageTracking : IEnableFeatureUsageTracking
{
public async Task SomeFeatureIWantToTrack()
{
using (var mainFeature = this.FeatureUsageTrackingSession("featureName"))
{
try
{
await DoSubFeature(mainFeature).ConfigureAwait(false);
}
catch (Exception exception)
{
mainFeature.OnException(exception);
}
}
}
public async Task SomeFeatureIWantToTrack(IFeatureUsageTrackingSession parentFeature)
{
using (var subFeature = parentFeature.SubFeature("subFeatureName"))
{
try
{
// do some work here.
}
catch (Exception exception)
{
subFeature.OnException(exception);
}
}
}
}TODO
First configure Appcenter. For guidance see https://docs.microsoft.com/en-us/appcenter/diagnostics/enabling-diagnostics
using Splat.AppCenter;
// then in your service locator initialisation
Locator.CurrentMutable.UseAppcenterApm();First configure Application Insights. For guidance see https://docs.microsoft.com/en-us/azure/azure-monitor/app/worker-service
using Splat.ApplicationInsights;
// then in your service locator initialisation
Locator.CurrentMutable.UseApplicationInsightsApm();First configure Exceptionless. For guidance see https://github.com/exceptionless/Exceptionless/wiki/Getting-Started
using Splat.Exceptionless;
// then in your service locator initialisation
Locator.CurrentMutable.UseExceptionlessApm();New Relic support isn't currently available.
OpenTrace support isn't currently available.
First configure Raygun. For guidance see TODO
using Splat.Raygun;
// then in your service locator initialisation
Locator.CurrentMutable.UseRaygunApm();The unit tests for this functionality do not generate activity to the relevant platform. The integration tests DO SEND TEST DATA to the relevant platforms, so they need to have the user-secrets configured. There is a script in the \scripts\inttestusersecrets.cmd that shows how to set the relevant secrets up.
Here is the comprehensive performance summary for the new InstanceGenericFirstDependencyResolver (referred to as the New Resolver) compared to the existing ModernDependencyResolver (referred to as the Legacy Resolver).
These statistics are based on the .NET 10.0 benchmark results provided.
The New Resolver is a massive upgrade over the legacy implementation. It is fully AOT-compatible and statistically superior in every critical metric:
It achieves this by replacing the old Dictionary-based lookup with a generic-first architecture that leverages static generic caching and ConditionalWeakTable for instance isolation.
Simulates a realistic application lifecycle: registering services, resolving them, and checking for existence.
| Workload | New Resolver | Legacy Resolver | Improvement |
|---|---|---|---|
| Realistic Usage | 431.1 μs | 1,513.1 μs | 3.5x Faster |
Why this matters: This is the metric that users will actually "feel" in their applications. The overhead of using Splat for dependency resolution effectively disappears.
Resolving services (GetService) or checking for them (HasRegistration).
| Operation | Scenario | New Resolver | Legacy Resolver | Improvement |
|---|---|---|---|---|
| Empty / Miss | Service not found | ~8.6 ns | ~31.4 ns | 3.6x Faster |
| Hit (Generic) | Service found | ~54 ns | ~71 ns | 1.3x Faster |
| Collection | GetServices | 168 μs | 581 μs | 3.4x Faster |
Why this matters: The "Empty/Miss" path is critical for app startup and unit testing where containers are often empty or checking for optional services. The optimizations we applied reduced this cost from ~7000ns to just ~9ns.
Registering new services into the container.
| Operation | New Resolver | Legacy Resolver | Improvement |
|---|---|---|---|
| Register (Generic) | 73 μs | 152 μs | 2.1x Faster |
| Register (Constant) | 92 μs | 173 μs | 1.9x Faster |
| Register (Contract) | 107 μs | 163 μs | 1.5x Faster |
Why this matters: Faster registration means faster application startup times, especially for apps with hundreds of services.
How much garbage (memory) is created during operations.
| Operation | New Resolver | Legacy Resolver | Improvement |
|---|---|---|---|
| Register (Constant) | 66 KB | 1,212 KB | 18x Less |
| Register (Generic) | 59 KB | 1,177 KB | 20x Less |
| Mixed Workload | 832 KB | 1,856 KB | 2.2x Less |
Why this matters: The Legacy resolver allocated over 1 MB just to register 300 simple services due to boxing and closure overhead. The New Resolver slashes this to just 60 KB, significantly reducing GC pressure and pauses in mobile and desktop apps.
| Feature | New Resolver (InstanceGenericFirst) | Legacy Resolver (Modern) |
|---|---|---|
| Storage | Generic Static Cache (ContainerCache<T>) | Dictionary<Type, List<Func>> |
| Isolation | ConditionalWeakTable (Per-Instance) | Dictionary Instance |
| Registration | readonly record struct (Zero Alloc) | Func<object> Delegate (High Alloc) |
| AOT Support | Native (No reflection on hot paths) | Poor (Requires reflection/boxing) |
| Concurrency | Lock-Free Reads (Volatile Snapshots) | Lock-Free Reads (Volatile Snapshots) |
By switching to the new resolver, consumers get a free performance boost and AOT compatibility without changing their code. The memory savings alone make this a mandatory upgrade for memory-constrained environments like mobile (MAUI/Xamarin) and WebAssembly (Blazor).
Splat is developed under an OSI-approved open source license, making it freely usable and distributable, even for commercial use. We ❤ the people who are involved in this project, and we’d love to have you on board, especially if you are just getting started or have never contributed to open-source before.
So here's to you, lovely person who wants to join us — this is how you can support us: