Mediatly 1.0.0
See the version list below for details.
dotnet add package Mediatly --version 1.0.0
NuGet\Install-Package Mediatly -Version 1.0.0
<PackageReference Include="Mediatly" Version="1.0.0" />
<PackageVersion Include="Mediatly" Version="1.0.0" />
<PackageReference Include="Mediatly" />
paket add Mediatly --version 1.0.0
#r "nuget: Mediatly, 1.0.0"
#:package Mediatly@1.0.0
#addin nuget:?package=Mediatly&version=1.0.0
#tool nuget:?package=Mediatly&version=1.0.0
Mediatly
A minimal, free (MIT-licensed), single-file CQRS mediator that is API-compatible with
MediatR 12.x. Migrating existing MediatR code is usually just a namespace swap
(using MediatR; → using Mediatly;).
- 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
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 (MediatR has the same
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 MediatR-style 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 | 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. |
-
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.0.0 — Initial public release. Minimal CQRS mediator API-compatible with MediatR 12.x: commands, queries, notifications, and pipeline behaviors, with reflection-based scanning plus a reflection-free / Native AOT registration path.