MDator 0.6.0
dotnet add package MDator --version 0.6.0
NuGet\Install-Package MDator -Version 0.6.0
<PackageReference Include="MDator" Version="0.6.0" />
<PackageVersion Include="MDator" Version="0.6.0" />
<PackageReference Include="MDator" />
paket add MDator --version 0.6.0
#r "nuget: MDator, 0.6.0"
#:package MDator@0.6.0
#addin nuget:?package=MDator&version=0.6.0
#tool nuget:?package=MDator&version=0.6.0
MDator
A source-generated, MediatR-compatible mediator for .NET.
MDator replaces MediatR's runtime reflection with a C# incremental source
generator that discovers handlers, composes pipelines, and emits a strongly-typed
IMediator implementation at compile time. The public API mirrors MediatR v12
exactly -- migrating is a namespace find-replace:
- using MediatR;
+ using MDator;
Why MDator?
| MediatR v12 | MDator | |
|---|---|---|
| Handler dispatch | Reflection + MakeGenericType + wrapper cache |
Compile-time switch per request type |
| Pipeline composition | Runtime closure chain, services resolved reflectively | Generated per-request pipeline, behaviors fused at compile time |
| Open generic behaviors | Closed at runtime via DI's open-generic support | Fused by the generator into each concrete pipeline |
| Registration | Assembly scanning at startup | Source-generated [ModuleInitializer], zero startup cost |
| License | Commercial (v13+) | MIT |
Quick start
1. Reference MDator (the generator ships alongside the runtime library):
<PackageReference Include="MDator" Version="..." />
2. Define a request and handler -- identical to MediatR:
using MDator;
public record GetUser(int Id) : IRequest<User>;
public sealed class GetUserHandler : IRequestHandler<GetUser, User>
{
public Task<User> Handle(GetUser request, CancellationToken ct)
=> Task.FromResult(new User(request.Id, $"user-{request.Id}"));
}
3. Register in your composition root:
services.AddMDator();
4. Inject and use IMediator, ISender, or IPublisher:
var user = await mediator.Send(new GetUser(42));
At build time the generator emits a GeneratedMediator class that dispatches
GetUser directly to GetUserHandler -- no reflection, no dictionary lookups.
Features
Request / Response
// With response
public record Ping(string Message) : IRequest<string>;
public class PingHandler : IRequestHandler<Ping, string> { ... }
var pong = await mediator.Send(new Ping("hello"));
// Void (no response)
public record FireAndForget() : IRequest;
public class FireHandler : IRequestHandler<FireAndForget> { ... }
await mediator.Send(new FireAndForget());
// Object-typed dispatch (runtime type switch, no reflection)
object request = new Ping("hi");
object? result = await mediator.Send(request);
Notifications
public record OrderPlaced(int OrderId) : INotification;
public class EmailNotifier : INotificationHandler<OrderPlaced> { ... }
public class AuditNotifier : INotificationHandler<OrderPlaced> { ... }
await mediator.Publish(new OrderPlaced(7)); // fans out to both handlers
Configure the publisher strategy at registration time:
services.AddMDator(cfg =>
{
// Sequential, stop on first error (default, matches MediatR):
cfg.NotificationPublisher = new ForEachAwaitPublisher();
// Parallel, aggregate all errors:
cfg.NotificationPublisher = new TaskWhenAllPublisher();
// Parallel via Task.Run, aggregate:
cfg.NotificationPublisher = new TaskWhenAllContinuationPublisher();
});
Open generic pipeline behaviors
The key differentiator. Declare behaviors with an assembly-level attribute so the source generator can see them at compile time:
[assembly: MDator.OpenBehavior(typeof(LoggingBehavior<,>), Order = 0)]
[assembly: MDator.OpenBehavior(typeof(ValidationBehavior<,>), Order = 1)]
public class LoggingBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
where TRequest : notnull
{
public async Task<TResponse> Handle(
TRequest request,
RequestHandlerDelegate<TResponse> next,
CancellationToken ct)
{
Console.WriteLine($"Handling {typeof(TRequest).Name}");
var response = await next();
Console.WriteLine($"Handled {typeof(TRequest).Name}");
return response;
}
}
The generator fuses LoggingBehavior<GetUser, User> directly into the
SendCore_GetUser pipeline -- no runtime generic closure, no
MakeGenericType, no allocation beyond the behavior's own closure captures.
Order controls nesting: lower values run outermost (closest to the caller).
Closed pipeline behaviors
Non-generic behaviors targeting a specific (TRequest, TResponse) pair are
discovered by the generator's class scan and registered under
IPipelineBehavior<TRequest, TResponse>. They are picked up at runtime via
IServiceProvider.GetServices<>():
public class CacheBehavior : IPipelineBehavior<GetUser, User>
{
public async Task<User> Handle(
GetUser request, RequestHandlerDelegate<User> next, CancellationToken ct)
{
// check cache, call next(), populate cache
}
}
FuseOnly mode
For maximum performance, suppress the runtime enumeration fallback entirely:
services.AddMDator(cfg => cfg.FuseOnly = true);
With FuseOnly = true, only [assembly: OpenBehavior]-declared behaviors
execute. Runtime-added behaviors are ignored, and the per-request
GetServices<IPipelineBehavior<,>>() call is eliminated.
Pre / post processors
public class AuditPreProcessor : IRequestPreProcessor<SaveItem>
{
public Task Process(SaveItem request, CancellationToken ct) { ... }
}
public class AuditPostProcessor : IRequestPostProcessor<SaveItem, string>
{
public Task Process(SaveItem request, string response, CancellationToken ct) { ... }
}
Pre-processors run before the handler; post-processors run after a successful response, inside the innermost pipeline scope.
Exception handlers and actions
// Can convert an exception into a response:
public class RecoverHandler
: IRequestExceptionHandler<SaveItem, string, InvalidOperationException>
{
public Task Handle(
SaveItem request,
InvalidOperationException exception,
RequestExceptionHandlerState<string> state,
CancellationToken ct)
{
state.SetHandled("fallback-value");
return Task.CompletedTask;
}
}
// Observes exceptions without converting them (e.g. logging):
public class LogExceptionAction : IRequestExceptionAction<SaveItem, Exception>
{
public Task Execute(SaveItem request, Exception exception, CancellationToken ct) { ... }
}
Exception handlers are ordered by type specificity at compile time (most-derived first). Actions always run, even if a handler marks the exception as handled.
Streaming
public record CountStream(int To) : IStreamRequest<int>;
public class CountHandler : IStreamRequestHandler<CountStream, int>
{
public async IAsyncEnumerable<int> Handle(
CountStream request,
[EnumeratorCancellation] CancellationToken ct)
{
for (var i = 1; i <= request.To; i++)
yield return i;
}
}
await foreach (var n in mediator.CreateStream(new CountStream(5)))
Console.WriteLine(n); // 1, 2, 3, 4, 5
Stream pipeline behaviors (IStreamPipelineBehavior<TRequest, TResponse>) and
open stream behaviors via [assembly: OpenBehavior] are also supported.
Configuration reference
services.AddMDator(cfg =>
{
// Lifetime for all generated registrations (default: Transient)
cfg.Lifetime = ServiceLifetime.Scoped;
// Notification publisher strategy (default: ForEachAwaitPublisher)
cfg.NotificationPublisher = new TaskWhenAllPublisher();
// Suppress runtime behavior enumeration for pure compile-time pipelines
cfg.FuseOnly = true;
// Add a closed behavior at runtime (e.g. from an unscanned assembly)
cfg.AddBehavior<MyClosedBehavior>();
});
Note for migrators:
RegisterServicesFromAssembly,RegisterServicesFromAssemblies, andRegisterServicesFromAssemblyContaining<T>exist onMDatorConfigurationfor MediatR source compatibility but have no effect — MDator's source generator scans the consuming compilation directly. The analyzerMDATOR0001flags these calls so they can be removed.
Migrating from MediatR
- Replace the
MediatRpackage reference withMDator. - Find-replace
using MediatR;withusing MDator;. - Replace
services.AddMediatR(cfg => ...)withservices.AddMDator(cfg => ...). - Convert open behavior registrations from fluent to attribute:
- cfg.AddOpenBehavior(typeof(LoggingBehavior<,>));
+ [assembly: MDator.OpenBehavior(typeof(LoggingBehavior<,>), Order = 0)]
- Build. The generator discovers your handlers and emits the mediator.
All interface shapes (IRequest<T>, IRequestHandler<,>, INotification,
INotificationHandler<>, IPipelineBehavior<,>, IStreamRequest<>,
IStreamRequestHandler<,>, IRequestPreProcessor<>,
IRequestPostProcessor<,>, IRequestExceptionHandler<,,>,
IRequestExceptionAction<,>, Unit, RequestHandlerDelegate<T>,
StreamHandlerDelegate<T>) are source-level identical to MediatR v12.
How it works
MDator ships three assemblies:
| Assembly | TFM | Purpose |
|---|---|---|
MDator.Abstractions |
netstandard2.0 | Interfaces, Unit, attributes. Reference this from handler libraries. |
MDator |
net9.0, net10.0 | Runtime shell: AddMDator, MDatorConfiguration, notification publishers. |
MDator.SourceGenerator |
netstandard2.0 | Roslyn incremental source generator, shipped as an analyzer. |
When you reference MDator, the generator activates in the consuming project
and emits a single file (MDatorGenerated.g.cs) containing:
MDatorGeneratedRegistration-- a static class with a[ModuleInitializer]that registers every discovered handler, behavior, pre/post processor, and exception handler withIServiceCollection.GeneratedMediator-- a sealedIMediatorimplementation with:- A compile-time
switchinSend<TResponse>()dispatching each known request type to a strongly-typedSendCore_*method. - Per-request pipeline methods that chain pre-processors, the handler, post-processors, fused open behaviors, runtime behaviors, and exception wrappers -- all as generated C# with zero reflection.
- Analogous
PublishCore_*,StreamCore_*, andSendVoidCore_*methods.
- A compile-time
Multiple projects can each host handlers. Each project's generator emits its
own module initializer that appends to MDatorGeneratedHook.Registrations.
When AddMDator() runs in the composition root, all callbacks fire.
Cross-assembly dispatch
When a handler lives in a different assembly from the mediator consumer, the generator automatically propagates the request type across the project boundary:
- The handler assembly's generator emits
[assembly: KnownRequest(typeof(MyRequest))]for every handler it discovers. - The consuming assembly's generator reads these attributes from its
referenced assemblies and includes those types in its compile-time
switch— generating strongly-typedSendCore_*pipeline methods with fused open behaviors, just like same-assembly requests. - A
RuntimeDispatchfallback covers requests, streams, and notifications for plugin-loaded or otherwise dynamic types that no assembly advertises at compile time. The fallback caches a compiled, strongly-typed delegate per runtime type, so the warm path is close to the compile-time switch in cost. Notifications routed throughRuntimeDispatch.PublishFallbackresolve handlers from DI for the runtime type and dispatch through the activeINotificationPublisher— no silent drops.
You can also apply [assembly: KnownRequest(typeof(...))] manually for
requests whose closed generic form is never syntactically referenced in the
consuming assembly.
Project structure
MDator.slnx
src/
MDator.Abstractions/ netstandard2.0 -- interfaces, Unit, attributes
MDator/ net9.0;net10.0 -- runtime, DI extensions, publishers
MDator.SourceGenerator/ netstandard2.0 -- incremental generator (analyzer)
tests/
MDator.Tests/ net10.0, xUnit -- integration tests
MDator.Tests.CrossAssembly/ net10.0 -- cross-assembly test handlers
Building
dotnet build MDator.slnx
dotnet test tests/MDator.Tests/MDator.Tests.csproj
License
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | 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
- MDator.Abstractions (>= 0.6.0)
- Microsoft.Bcl.AsyncInterfaces (>= 10.0.7)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.7)
-
net9.0
- MDator.Abstractions (>= 0.6.0)
- Microsoft.Bcl.AsyncInterfaces (>= 10.0.7)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.7)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 0.6.0 | 141 | 5/18/2026 |
| 0.5.0 | 93 | 5/6/2026 |
| 0.4.0 | 94 | 4/26/2026 |
| 0.3.0 | 123 | 4/15/2026 |
| 0.2.22 | 105 | 4/14/2026 |
| 0.2.22-g1d20b9cd07 | 92 | 4/14/2026 |
| 0.2.21 | 100 | 4/14/2026 |
| 0.2.20-g832a3f2f63 | 101 | 4/14/2026 |
| 0.2.17 | 97 | 4/14/2026 |
| 0.2.16 | 95 | 4/14/2026 |
| 0.2.15 | 101 | 4/14/2026 |
| 0.2.13 | 106 | 4/13/2026 |
| 0.2.11 | 100 | 4/13/2026 |
| 0.2.10 | 90 | 4/13/2026 |
| 0.2.9 | 96 | 4/13/2026 |
| 0.2.8 | 100 | 4/13/2026 |
| 0.2.7 | 101 | 4/13/2026 |
| 0.2.5 | 104 | 4/13/2026 |
| 0.2.4 | 248 | 4/13/2026 |
| 0.2.2 | 107 | 4/13/2026 |