Mediatly 1.1.0
dotnet add package Mediatly --version 1.1.0
NuGet\Install-Package Mediatly -Version 1.1.0
<PackageReference Include="Mediatly" Version="1.1.0" />
<PackageVersion Include="Mediatly" Version="1.1.0" />
<PackageReference Include="Mediatly" />
paket add Mediatly --version 1.1.0
#r "nuget: Mediatly, 1.1.0"
#:package Mediatly@1.1.0
#addin nuget:?package=Mediatly&version=1.1.0
#tool nuget:?package=Mediatly&version=1.1.0
Mediatly
Mediatly is a lightweight, MIT-licensed, single-file CQRS mediator for .NET. It provides a simple, dependency-free implementation with a familiar API, making it easy to integrate into existing CQRS applications.
- Zero dependencies beyond
Microsoft.Extensions.DependencyInjection.Abstractions. - Commands, queries, notifications (many handlers, sequential), and pipeline behaviors.
- Reflection at most once per request type, then a cached dictionary lookup.
- Handlers and mediator registered scoped (safe with EF Core
DbContext). - Optional reflection-free / Native AOT registration path.
Install
dotnet add package Mediatly
Target frameworks
| TFM | Runtimes | Notes |
|---|---|---|
netstandard2.0 |
.NET Framework 4.6.2+, .NET Core 2.x, Mono/Xamarin/Unity/Tizen | Reflection dispatch only — no trimming/AOT toolchain exists for this target |
net8.0 |
.NET 8+ | Full trim/AOT annotations |
net9.0 |
.NET 9+ | Full trim/AOT annotations |
net10.0 |
.NET 10+ | Full trim/AOT annotations |
The public API is identical on every target (enforced at pack time by
EnablePackageValidation). On netstandard2.0 the trim/AOT attributes are compiled
from internal polyfills and are inert, and the reflection-free AddHandler<...>
registration APIs still work — they are simply not required, since every runtime
that consumes netstandard2.0 is JIT-based.
Quick start
using Mediatly;
// 1) Define a request + handler
public record GetUser(int Id) : IRequest<string>;
public class GetUserHandler : IRequestHandler<GetUser, string>
{
public Task<string> Handle(GetUser request, CancellationToken ct)
=> Task.FromResult($"User#{request.Id}");
}
// 2) Register (reflection scan) + an open-generic behavior
services.AddMediatly(typeof(GetUserHandler).Assembly);
services.AddPipelineBehavior(typeof(LoggingBehavior<,>));
// 3) Send
var mediator = provider.GetRequiredService<IMediator>();
string user = await mediator.Send(new GetUser(42));
Native AOT / trimming
The convenient reflection APIs — assembly scanning (AddMediatly(assembly),
cfg.AddHandlersFromAssembly...) and open-generic behaviors
(AddPipelineBehavior(typeof(X<,>)), cfg.AddBehavior(typeof(X<,>))) — are not
Native-AOT compatible, and this is fundamental to AOT (any reflection-based mediator
shares this restriction), not a Mediatly limitation:
| Reflection-style call | Why it can't be AOT |
|---|---|
| assembly scanning | reflects over types the trimmer can't see → [RequiresUnreferencedCode] (IL2026) |
open-generic behavior typeof(X<,>) |
closed at runtime via MakeGenericType → [RequiresDynamicCode] (IL3050) |
For trimmed / Native AOT builds, register everything explicitly and closed — no reflection at dispatch time:
services.AddHandler<GetUser, string, GetUserHandler>();
services.AddHandler<DoThing, DoThingHandler>(); // void (Unit) request
services.AddNotificationHandler<OrderPlaced, EmailHandler>();
services.AddBehavior<GetUser, string, LoggingBehavior<GetUser, string>>(); // closed behavior
Each of these builds its dispatch wrapper with a plain new at startup (both
generic arguments known at compile time). If a request reaches the mediator without
a registered wrapper under AOT, you get an InvalidOperationException naming the
exact AddHandler<...> call to add.
AOT-safe equivalent of a "scan + AddOpenBehavior ×N" config
A scan-based configuration like this is not AOT-compatible:
// ❌ Not AOT: reflection scan + open-generic behaviors
services.AddMediatly(cfg =>
{
cfg.AddHandlersFromAssemblies(AppDomain.CurrentDomain.GetAssemblies()); // IL2026
cfg.AddBehavior(typeof(AuthenticationBehavior<,>)); // IL3050
cfg.AddBehavior(typeof(ValidationBehavior<,>));
cfg.AddBehavior(typeof(LoggingBehavior<,>));
});
The AOT-safe form registers each request explicitly and closes the same behavior stack per request type. A one-line helper keeps it ergonomic and preserves order (first registered = outermost):
public static class Registration
{
public static IServiceCollection AddRequestPipeline<TRequest, TResponse,
[DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicConstructors)] THandler>(
this IServiceCollection services)
where TRequest : IRequest<TResponse>
where THandler : class, IRequestHandler<TRequest, TResponse>
{
services.AddHandler<TRequest, TResponse, THandler>();
services.AddBehavior<TRequest, TResponse, AuthenticationBehavior<TRequest, TResponse>>(); // outermost
services.AddBehavior<TRequest, TResponse, ValidationBehavior<TRequest, TResponse>>();
services.AddBehavior<TRequest, TResponse, LoggingBehavior<TRequest, TResponse>>(); // innermost
return services;
}
}
// One line per request type — no scanning, no open generics:
services.AddRequestPipeline<GetUser, string, GetUserHandler>();
services.AddRequestPipeline<PlaceOrder, string, PlaceOrderHandler>();
Trade-off: you list each request type once (O(number of requests) instead of a
single scan), and behaviors are closed per request type so each gets its own
correctly-ordered pipeline. For a large app, a source generator can emit these
AddRequestPipeline<...> calls at build time to restore scan-like ergonomics while
staying AOT-safe.
Publishing
dotnet publish -c Release -r win-x64 # requires the "Desktop development with C++" workload
Performance
Vs MediatR 12.4.1 (last Apache-2.0 release). BenchmarkDotNet v0.15.8, .NET 8.0.24,
AMD Ryzen 7 5800U (16 logical / 8 physical cores), Windows 10; MediatR is the baseline
in each group. Reproduce with the Mediatly.Benchmarks project.
Warm dispatch — Send / Publish / dynamic Send(object)
| Method | Mean | Ratio | Allocated | Alloc Ratio |
|---|---|---|---|---|
| Send_Mediatly | 89.95 ns | 1.72× faster | 64 B | 4.13× less |
| Send_MediatR | 154.56 ns | baseline | 264 B | baseline |
| Publish_Mediatly | 105.05 ns | 1.69× faster | 56 B | 11.0× less |
| Publish_MediatR | 177.46 ns | baseline | 616 B | baseline |
| SendObject_Mediatly | 97.60 ns | 1.99× faster | 160 B | 2.25× less |
| SendObject_MediatR | 194.68 ns | baseline | 360 B | baseline |
Send = plain request/response; Publish = notification to 2 handlers;
Send(object) = the non-generic dynamic-dispatch path.
Startup — register + BuildServiceProvider + first (cold) dispatch
| Method | Mean | Allocated | Ratio |
|---|---|---|---|
| Startup_Mediatly | 6.92 µs | 13.28 KB | 12.96× faster |
| Startup_MediatR | 89.67 µs | 196.75 KB | baseline |
100,000 requests — batch throughput + memory
| Library | Time for 100k | Memory for 100k | req/s |
|---|---|---|---|
| Mediatly | 9.56 ms | 12.97 MB | 10,461,997/s |
| MediatR | 20.01 ms | 32.04 MB | 4,997,573/s |
Notification fan-out — Publish to N handlers
| Handlers | Mediatly | MediatR | Alloc ratio |
|---|---|---|---|
| 1 | 107.5 ns / 56 B | 174.7 ns / 256 B | 4.6× less |
| 5 | 138.8 ns / 56 B | 364.3 ns / 736 B | 13× less |
| 20 | 344.6 ns / 56 B | 927.5 ns /2536 B | 45× less |
Mediatly's per-publish allocation stays flat at 56 B regardless of handler count; MediatR's grows with the number of handlers.
Pipeline scaling — Send through N pass-through behaviors
| Behaviors | Mediatly | MediatR |
|---|---|---|
| 0 | 91.8 ns / 64 B | 154.1 ns / 264 B |
| 1 | 158.9 ns /232 B | 184.2 ns / 456 B |
| 3 | 359.6 ns /440 B | 258.0 ns / 744 B |
| 5 | 253.3 ns /648 B | 314.9 ns /1032 B |
Per-behavior overhead is similar for both, and Mediatly allocates less at every stage. On a shared machine the 3-behavior Mediatly figure showed high run-to-run variance (±42 ns, P95 411 ns), so treat the ordering at that single point as noise.
Multi-threaded throughput — scope per request, 16 logical cores
| Library | 1-thread req/s | all-core req/s | speedup |
|---|---|---|---|
| Mediatly | 3,940,561/s | 12,724,068/s | 3.2× |
| MediatR | 3,239,471/s | 11,095,351/s | 3.4× |
Summary: ~1.7× faster on warm dispatch, ~13× faster to cold-start, and 4–45× less
allocation depending on scenario. The gains come from Mediatly doing less (no streaming
/ processors / publish strategies). Numbers are indicative — rerun with the
Mediatly.Benchmarks project; sub-microsecond benchmarks vary on a shared machine.
Not included (by design)
IStreamRequest/streaming behaviors, request pre/post processors, and custom
publish strategies are intentionally omitted to keep the library tiny. They can be
added as extensions.
License
MIT.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net5.0 was computed. net5.0-windows was computed. net6.0 was computed. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 is compatible. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
| .NET Core | netcoreapp2.0 was computed. netcoreapp2.1 was computed. netcoreapp2.2 was computed. netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
| .NET Standard | netstandard2.0 is compatible. netstandard2.1 was computed. |
| .NET Framework | net461 was computed. net462 was computed. net463 was computed. net47 was computed. net471 was computed. net472 was computed. net48 was computed. net481 was computed. |
| MonoAndroid | monoandroid was computed. |
| MonoMac | monomac was computed. |
| MonoTouch | monotouch was computed. |
| Tizen | tizen40 was computed. tizen60 was computed. |
| Xamarin.iOS | xamarinios was computed. |
| Xamarin.Mac | xamarinmac was computed. |
| Xamarin.TVOS | xamarintvos was computed. |
| Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETStandard 2.0
-
net10.0
-
net8.0
-
net9.0
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
1.1.0 — Added a netstandard2.0 target, so the package now also supports .NET Framework 4.6.2+, .NET Core 2.x and Mono/Xamarin/Unity/Tizen alongside net8.0/net9.0/net10.0. No API changes: the public surface is identical on every target (enforced by package validation). On netstandard2.0 the trim/AOT annotations are inert internal polyfills and dispatch is always reflection-based. 1.0.1 — Packaging/metadata update (repository URL, description). No API or behavior changes. 1.0.0 — Initial public release: lightweight, single-file CQRS mediator with a familiar API — commands, queries, notifications, and pipeline behaviors, with reflection-based scanning plus a reflection-free / Native AOT registration path.