Auto Registration
Starting with v10.2.0 handlers no longer need to be registered one by one.
A source generator shipped inside the RA.Utilities.Feature NuGet package discovers handler and behavior implementations at compile time and emits the registration code for you — plain handlers are wired up with nothing more than services.AddMediator(), and since v11.1.0 closed pipeline behaviors are too.
🎯 How It Works
The generator ships as an analyzer inside the RA.Utilities.Feature package (analyzers/dotnet/cs), so every consumer of the NuGet package gets it automatically — no extra package reference, attribute, or configuration required.
For each assembly that references the package, the generator:
- Scans the assembly for every non-abstract, non-static class implementing one of the handler or behavior contracts:
IRequestHandler<TRequest, TResponse>(request with response)IRequestHandler<TRequest>(request without response)INotificationHandler<TNotification>(notification subscriber)IPipelineBehavior<TRequest, TResponse>/IPipelineBehavior<TRequest>(request pipeline behaviors)INotificationBehavior<TNotification>(notification pipeline behaviors)
- Validates each candidate (see diagnostics below).
- Emits a module initializer into that assembly that queues the registrations on a static queue in the
RA.Utilities.Featureruntime (RA.Utilities.Feature.Generated.HandlerRegistrations): handlers under their interfaces and as themselves (so the generated mediator can inject them by concrete type), behaviors under their behavior interfaces. - When your startup code calls
services.AddMediator(), all queued registrations are applied to the service collection in one pass.
var builder = WebApplication.CreateBuilder(args);
// Registers IMediator AND applies every compile-time handler registration
// queued by the source generator.
builder.Services.AddMediator();
No AddFeature<...>() or AddNotification<...>() calls are needed for plain handlers.
🔍 Discovery Rules
The generator inspects the whole class hierarchy: matching interfaces are detected through AllInterfaces, so a concrete class that inherits from an abstract base (which itself implements the interface) is registered as itself, not as the base.
| Scenario | Behavior |
|---|---|
| Abstract class implementing a handler interface | Skipped — it is a base class, not a registrable handler |
| Concrete class implementing a handler interface directly | Auto-registered |
| Concrete class deriving from an abstract handler base | Auto-registered as the concrete class |
| Concrete class implementing multiple handler interfaces | One registration per implemented interface |
| Partial class | Single registration (parts are de-duplicated) |
Generic handler class (MyHandler<T>) | FEAG002 warning — skipped; register explicitly |
private / file-local handler (or contained in one) | FEAG003 error |
struct handler | FEAG004 error |
| Two handlers for the same request pair in one assembly | FEAG005 error |
| Multiple notification handlers for the same notification | Allowed — fan-out is their purpose |
| Closed (non-generic) behavior class | Auto-registered under its behavior interface (transient) — resolved by the generated mediator through DI |
Generic behavior class (MyBehavior<TRequest, TResponse>) | FEAG010 warning — skipped; register explicitly with AddDecoration |
| Multiple behaviors for the same message | Allowed — composed in registration order |
🛠️ Diagnostics
All diagnostics use the category RA.Utilities.Feature.Generators:
| ID | Severity | Meaning |
|---|---|---|
FEAG001 | Warning | Compilation targets a framework before .NET 5, where ModuleInitializerAttribute does not exist, so handlers cannot be auto-registered. Register handlers explicitly with AddFeature / AddNotification. |
FEAG002 | Warning | Handler is generic, so the generated registration code cannot supply a type argument. Register it explicitly. |
FEAG003 | Error | Handler (or one of its containing types) is private or file-local, so the generated code cannot reference it. Make it internal/public (or register explicitly). |
FEAG004 | Error | Handler is a struct. Handlers are registered and resolved as class services. |
FEAG005 | Error | Two or more handlers implement the same request handler pair (IRequestHandler<TRequest, TResponse> or IRequestHandler<TRequest>) in one assembly — the winning registration would be load-order dependent. A request may have exactly one handler; remove one of the implementations. |
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. |
The generator reports additional message and handler diagnostics — FEAG006–FEAG009 — see Generated Mediator diagnostics.
⏳ Lifetimes
Generated registrations use the same lifetimes as the explicit fluent builders:
| Registration | Lifetime |
|---|---|
IRequestHandler<TRequest, TResponse> → handler | Scoped |
IRequestHandler<TRequest> → handler | Scoped |
INotificationHandler<TNotification> → handler | Transient |
| Handler as its concrete type (for the generated mediator's direct injection) | Same as the interface registration |
Behavior under its interface (IPipelineBehavior<,> / IPipelineBehavior<> / INotificationBehavior<>) | Transient |
Registering handlers as themselves is what lets the generated Send resolve PingHandler directly and call it without interface dispatch — see Generated Mediator.
📥 Assembly-Loading Guarantee
Module initializers run when their assembly is loaded by the CLR. The registrations are queued at that moment and applied later, when AddMediator() executes.
The practical guarantee is:
Auto-registration covers the assembly that calls
AddMediator()plus any assembly that was already loaded before that call.
// Program.cs — entry assembly: handlers here are always registered ✓
builder.Services.AddMediator();
Referenced class libraries load lazily on the CLR — a library's module initializer does not run just because the library is referenced. If a handler lives in a library whose types are first touched only when a request arrives (i.e., after AddMediator() has already applied the queue), that handler may be registered too late.
Escape hatches
Keep handlers where they are guaranteed to be registered, or force the assembly to load early:
- Keep handlers in the entry / composition assembly — the assembly that calls
AddMediator(). This is the simplest and recommended layout. - Touch the handler assembly at startup — reference one of its types before (or when) calling
AddMediator(), forcing the CLR to load it:
_ = typeof(SomeHandlerInTheLibrary).Assembly; // force assembly load
builder.Services.AddMediator();
- Register explicitly — handlers in lazily-loaded libraries can always be registered the classic way:
builder.Services.AddFeature<MyCommand, MyResponse, MyHandlerInAnotherLibrary>();
builder.Services.AddNotification<OrderPlaced>().AddHandler<MyNotificationHandlerInAnotherLibrary>();
🧩 What Still Needs Explicit Registration
The generator discovers handlers and closed behaviors. Everything else is registered explicitly:
| Component | Registration |
|---|---|
| Request handlers (plain, non-generic, in an early-loaded assembly) | Automatic |
| Notification handlers (plain, non-generic, in an early-loaded assembly) | Automatic |
Closed pipeline behaviors (IPipelineBehavior<,> / IPipelineBehavior<> / INotificationBehavior<>) | Automatic — under their interfaces, resolved by the generated mediator through DI |
| Generic handlers | AddFeature<TRequest, TResponse, THandler>() / AddNotification<TNotification>().AddHandler<THandler>() (generator emits FEAG002) |
| Generic behaviors | .AddDecoration<TBehavior>() (generator emits FEAG010) |
Validators (IValidator<T>) | .AddValidator<TValidator>() (also wires the ValidationBehavior) |
| Handlers with custom lifetimes or setup | Manual DI registration |
If a request handler is registered both automatically and explicitly, the registrations simply stack; keep only the explicit one when you need a custom setup.