Generated Mediator
The source generator in the RA.Utilities.Feature package generates the IMediator implementation itself — the generated Mediator is the only mediator in the package.
Every assembly referencing the package gets its own Mediator class — a complete, compile-time implementation of IMediator with zero runtime reflection — and services.AddMediator() registers it as the IMediator implementation automatically.
The generated implementation covers every message type visible to the compilation, including messages declared in referenced class libraries.
🚀 Why It Is Faster
- Direct handler injection. The generator knows each message's handler: it registers handlers under their interfaces and as themselves, and the generated
Sendresolves the known handler by its concrete type and callsHandleAsyncdirectly — no interface resolution, and the call devirtualizes. - Monomorphized
Sendmethods. One typedSendoverload per message (Send(Ping)) — calls bind directly with no interface or generic dispatch, and the genericIMediatormethods carry the same bodies, monomorphized by the JIT. - No delegate allocation without behaviors. When a request has no pipeline behaviors, the handler is invoked directly instead of building a delegate chain. Benchmarks: 12-18% faster sends and 9-14% fewer allocations than MediatR (with MediatR handlers registered scoped, matching this package's lifetime) across the send shapes — see the Benchmarks page for the full comparison, methodology, and how to run them.
- Fast dictionary lookups for object dispatch.
Send(object)/Publish(object)— where the static type is erased — use a chain oftypeofcomparisons in small projects and switch to staticDictionary<Type, Delegate>lookups once the project defines more than 8 messages, a threshold above which a dictionary pays for itself. Unknown message types throwHandlerNotFoundException. - Both
IMediatorand the concrete class are available. InjectIMediatoror the concreteMediatorfor the best performance:
using RA.Utilities.Feature.Generated;
public class OrdersService
{
private readonly Mediator _mediator;
public OrdersService(Mediator mediator) => _mediator = mediator;
public Task<CreateOrderResponse> CreateAsync(CreateOrderCommand command, CancellationToken ct) =>
_mediator.Send(command, ct); // binds the monomorphized Send(CreateOrderCommand) overload
}
See the Benchmarks page for the full side-by-side comparison with MediatR (equal scoped lifetimes), the interface-typed and object-dispatch shapes, and the benchmark harness.
📦 How It Is Registered
The generator emits a module initializer (per assembly) that queues the registration of that assembly's Mediator on RA.Utilities.Feature.Generated.MediatorRegistrations.
services.AddMediator() applies the queued handler, behavior, and mediator registrations in one pass:
IMediatorresolves to the generatedMediator.Mediatoris registered as itself, so it can be injected directly.
builder.Services.AddMediator();
The same assembly-loading guarantee as auto registration applies: the IMediator registration is queued by each assembly's module initializer, so the generated mediator of the last loaded assembly wins. Every assembly's generated mediator covers the messages visible to that assembly's compilation, including its referenced libraries.
🧭 Dispatch Matrix
| Call | How it dispatches |
|---|---|
Mediator.Send(ping) (concrete, statically typed message) | Monomorphized overload — concrete handler injection + DI behavior resolution, direct calls |
IMediator.Send<Ping, Pong>(ping) | Generated generic body — handler and behaviors resolved from DI (the JIT monomorphizes the instantiation) |
Mediator.Send((object)ping) | GetType() chain (≤ 8 messages) or static dictionary lookup, then the monomorphized overload |
IMediator.Send<TRequest, TContext>(...) / Publish context variants | Generated generic bodies with the same fast paths |
| Unknown object message | RA.Utilities.Feature.Exceptions.HandlerNotFoundException |
🧳 Typed Pipeline Context
The generated mediator carries the typed PipelineContext<T> through the pipeline exactly like Send/Publish without a context do — every message gets a generated context overload:
// Send with a typed context:
string greeting = await mediator.Send<GreetingContext>(new ContextGreetingRequest("context"));
// Publish with a typed context:
await mediator.Publish<GreetingContext>(new OrderPlacedNotification("order-1"));
The generated context overloads create the PipelineContext<T> when you don't pass one and hand it to every pipeline member — behaviors stamp it (HandleAsync<TContext>), handlers read it back. Behaviors and handlers that don't care about the context use the plain overloads; the default interface implementations bridge the two. Calls without a context type parameter allocate no context at all.
⚙️ Object-Based Dispatch
Both the concrete class and the interface expose object-based overloads on the concrete class (they are not part of the IMediator interface, so existing IMediator implementors are unaffected):
Mediator mediator = /* ... */;
object? pong = await mediator.Send((object)new Ping("hello"));
await mediator.Publish((object)new OrderPlaced("order-1"));
- Dispatch by runtime type with the same chain/dictionary strategy (no reflection).
- Unknown message types throw
RA.Utilities.Feature.Exceptions.HandlerNotFoundException.
🛠️ Message and Handler Diagnostics
The generator reports problems with messages and message handlers at compile time (category RA.Utilities.Feature.Generators):
| ID | Severity | Meaning |
|---|---|---|
FEAG006 | Error | Message implements multiple message contracts (two distinct IRequest<TResponse> interfaces, or a request contract together with INotification). It is excluded from the generated dispatch. |
FEAG007 | Warning | Request message has no handler anywhere in the compilation — sending it would fail at runtime. |
FEAG008 | Warning | Message is generic or inaccessible from the generated code — it dispatches through the generic interface methods instead. |
FEAG009 | Warning | Handler implements both IRequestHandler<TRequest> and IRequestHandler<TRequest, TResponse> for the same message — split them into separate classes. |
FEAG010 | Warning | Pipeline behavior is generic — it is not auto-registered. Register it explicitly with AddDecoration; the generated mediator resolves explicitly registered behaviors from DI. |
See Auto Registration diagnostics for FEAG001–FEAG005 and the behavior registration model.
Behavior registration
Closed (non-generic) pipeline behaviors — IPipelineBehavior<TRequest>, IPipelineBehavior<TRequest, TResponse>, INotificationBehavior<TNotification> — are auto-registered under their interfaces (transient) and resolved by the generated mediator through DI, in registration order. Explicitly registered behaviors (AddDecoration, including generic behavior closures) resolve through the same channel, so both auto-registered and explicit behaviors compose into the same pipeline.