MDator 0.6.0

dotnet add package MDator --version 0.6.0
                    
NuGet\Install-Package MDator -Version 0.6.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="MDator" Version="0.6.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="MDator" Version="0.6.0" />
                    
Directory.Packages.props
<PackageReference Include="MDator" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add MDator --version 0.6.0
                    
#r "nuget: MDator, 0.6.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package MDator@0.6.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=MDator&version=0.6.0
                    
Install as a Cake Addin
#tool nuget:?package=MDator&version=0.6.0
                    
Install as a Cake Tool

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, and RegisterServicesFromAssemblyContaining<T> exist on MDatorConfiguration for MediatR source compatibility but have no effect — MDator's source generator scans the consuming compilation directly. The analyzer MDATOR0001 flags these calls so they can be removed.

Migrating from MediatR

  1. Replace the MediatR package reference with MDator.
  2. Find-replace using MediatR; with using MDator;.
  3. Replace services.AddMediatR(cfg => ...) with services.AddMDator(cfg => ...).
  4. Convert open behavior registrations from fluent to attribute:
- cfg.AddOpenBehavior(typeof(LoggingBehavior<,>));
+ [assembly: MDator.OpenBehavior(typeof(LoggingBehavior<,>), Order = 0)]
  1. 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 with IServiceCollection.
  • GeneratedMediator -- a sealed IMediator implementation with:
    • A compile-time switch in Send<TResponse>() dispatching each known request type to a strongly-typed SendCore_* 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_*, and SendVoidCore_* methods.

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:

  1. The handler assembly's generator emits [assembly: KnownRequest(typeof(MyRequest))] for every handler it discovers.
  2. The consuming assembly's generator reads these attributes from its referenced assemblies and includes those types in its compile-time switch — generating strongly-typed SendCore_* pipeline methods with fused open behaviors, just like same-assembly requests.
  3. A RuntimeDispatch fallback 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 through RuntimeDispatch.PublishFallback resolve handlers from DI for the runtime type and dispatch through the active INotificationPublisher — 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

MIT

Product 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

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
Loading failed