Migration Guide: MediatorLite v1 to v2
This guide helps you migrate from MediatorLite v1 (reflection + source-gen hybrid) to v2 (source-generation only).
Overview of Changes
MediatorLite v2 is a source-generation-only architecture. The reflection-based fallback dispatch has been removed in favor of compile-time generated dispatch code.
What Moved from Runtime to Compile-Time
| Feature | v1 | v2 |
|---|---|---|
| Handler discovery | Runtime reflection + optional source-gen | Compile-time only |
| Pipeline behavior wrapping | Runtime delegate chains | Generated unrolled pipelines |
| Handler resolution | ConcurrentDictionary caching | Compile-time type-pattern switch |
| Notification handler ordering | Runtime sorting | Compile-time sorting |
Breaking Changes
1. Source Generation is Required
v1: You could register handlers manually and rely on reflection fallback.
v2: You must use source generation. The reflection fallback has been removed.
// v1 - Manual registration worked without source-gen
services.AddTransient<IRequestHandler<MyRequest, MyResponse>, MyHandler>();
services.AddMediatorLite();
// v2 - Must call AddGeneratedHandlers(); it registers IMediator itself
services.AddGeneratedHandlers(); // Required!
services.AddMediatorLite(); // Optional — diagnostic fallback only (see below)
AddMediatorLite() is now optional: the generated AddGeneratedHandlers() registers IMediator directly (scoped lifetime — it captures the resolving scope’s IServiceProvider so scoped handler dependencies work; when resolved from the root provider it behaves like a singleton). AddMediatorLite() only registers a diagnostic fallback (TryAddScoped<IMediator, ThrowingMediator>) that throws a clear InvalidOperationException if the source generator never ran. The calling order of the two methods doesn’t matter.
2. SendAsync / PublishAsync Return ValueTask
v1: IMediator.SendAsync<TResponse> returned Task<TResponse>; IMediator.PublishAsync returned Task.
v2: SendAsync<TResponse> returns ValueTask<TResponse> and PublishAsync returns ValueTask. This enables zero-allocation dispatch: for a request with no behaviors, a synchronously completing handler allocates nothing at all.
Plain await works exactly as before:
var user = await _mediator.SendAsync(new GetUserQuery(id), ct);
await _mediator.PublishAsync(new UserCreatedNotification(user.Id), ct);
⚠️ A
ValueTaskmust be consumed exactly once —awaitit directly. ForTask.WhenAll, fan-out, storing the result, or awaiting more than once, convert it first with.AsTask():
var userTask = _mediator.SendAsync(new GetUserQuery(1), ct).AsTask();
var orderTask = _mediator.SendAsync(new GetOrderQuery(1), ct).AsTask();
await Task.WhenAll(userTask, orderTask);
3. Handler Registration Order
v1: Handlers were discovered at runtime in arbitrary order.
v2: Handlers are discovered at compile-time. Use [BehaviorOrder] to control execution order.
4. ISourceGeneratedMediator, RequestDispatcher, and NotificationPublisher Are Deleted
v2 has no runtime dispatch layer. The source generator emits a SourceGeneratedMediator class (namespace MediatorLite.Generated) that implements IMediator directly. Dispatch is a compile-time type-pattern switch — no Dictionary<Type,...> lookup, no Task<object> boxing, and no runtime Mediator wrapper class (that class is deleted too).
If you don’t call AddGeneratedHandlers(), the ThrowingMediator fallback registered by AddMediatorLite() throws on every dispatch:
InvalidOperationException: No source-generated mediator is registered. Reference the
MediatorLite.SourceGeneration analyzer package from the assembly that contains your
handlers and call services.AddGeneratedHandlers() so the generated mediator replaces
this fallback.
5. Notification Dispatch Matches the Runtime Type
v1: PublishAsync dispatched on typeof(TNotification), so publishing through a base- or interface-typed reference (e.g. a variable typed as INotification) silently no-oped.
v2: The generated type-pattern switch matches the notification’s runtime type, so base/interface-typed publishes now correctly dispatch to the concrete notification’s handlers.
6. Runtime Options Are Gone
v1: AddMediatorLite accepted a configure lambda that exposed a MediatorOptions instance (logging toggles, tracing toggles, lifetimes, open-behavior list, notification strategies).
v2: The entire options surface has moved to compile time:
MediatorOptionsis deleted.AddMediatorLiteno longer accepts a configure lambda — it is now a zero-argument call.EnableBuiltInLoggingis replaced by the absence/presence of[assembly: DisableMediatorLogging].EnableTracingis replaced by the absence/presence of[assembly: DisableMediatorTracing].DefaultLogLevelis dropped; useILoggingBuilder.AddFilter("MediatorLite.IMediator", LogLevel.X)orappsettings.json.HandlerLifetime/MediatorLifetimeare dropped; the mediator is alwaysTransient, and handler lifetimes remain controlled by the consumer at DI registration.MediatorLoggingAttribute(the per-classEnabled/IncludePayload/LogLevelattribute) has been deleted — it was never consumed.- Notification execution/error strategies moved from
MediatorOptionsto the compile-time[NotificationExecution]/[NotificationError](and their[assembly: Default...]counterparts).
// v1
services.AddMediatorLite(options =>
{
options.EnableBuiltInLogging = true;
options.EnableTracing = true;
options.DefaultLogLevel = LogLevel.Debug;
});
// v2
services.AddMediatorLite();
// Observability is on by default; opt out with [assembly: DisableMediatorLogging]
// / [assembly: DisableMediatorTracing]. Log level is controlled via MEL filters.
Step-by-Step Migration
Step 1: Ensure Source Generator Reference
Your project must reference MediatorLite.SourceGeneration:
<ItemGroup>
<PackageReference Include="MediatorLite" Version="2.0.0" />
<PackageReference Include="MediatorLite.SourceGeneration" Version="2.0.0"
OutputItemType="Analyzer"
ReferenceOutputAssembly="false" />
</ItemGroup>
Step 2: Update Service Registration
Replace manual handler registration with generated registration:
// Before (v1)
services.AddTransient<IRequestHandler<GetUserQuery, User>, GetUserHandler>();
services.AddTransient<IRequestHandler<CreateUserCommand, Unit>, CreateUserHandler>();
services.AddMediatorLite();
// After (v2)
services.AddGeneratedHandlers(); // Discovers all handlers at compile-time
services.AddMediatorLite();
For granular control, use the individual methods:
services.AddGeneratedRequestHandlers(); // Request handlers only
services.AddGeneratedNotificationHandlers(); // Notification handlers only
services.AddGeneratedBehaviors(); // Pipeline behaviors only
Step 3: Update Behavior Registration
v1: Open generic behaviors were registered manually.
v2: Behaviors are auto-discovered by the source generator — no options wiring required:
// v1 - Manual open behavior registration
services.AddTransient(typeof(IPipelineBehavior<,>), typeof(LoggingBehavior<,>));
services.AddMediatorLite();
// v2 - Behaviors are discovered automatically by the generator
services.AddGeneratedHandlers();
services.AddMediatorLite();
Control ordering with [BehaviorOrder] on the behavior class (see below).
Step 4: Use New Attributes
v2 provides attributes for compile-time configuration:
[NotificationExecution] / [NotificationError] - Per-Notification Strategies
[NotificationExecution(NotificationExecutionStrategy.Parallel)]
[NotificationError(NotificationErrorStrategy.ContinueAndAggregate)]
public record UserCreatedNotification(Guid UserId) : INotification;
Each attribute is independent. Omit one to fall back to the assembly-level default (or the library default if none is declared).
[assembly: DefaultNotificationExecution] / [assembly: DefaultNotificationError] - Assembly-Wide Defaults
[assembly: DefaultNotificationExecution(NotificationExecutionStrategy.Parallel)]
[assembly: DefaultNotificationError(NotificationErrorStrategy.ContinueAndAggregate)]
Per-notification attributes always win over these assembly-level defaults.
[NotificationHandlerOrder] - Handler Execution Order
[NotificationHandlerOrder(1)] // Runs first
public class AuditHandler : INotificationHandler<UserCreated> { }
[NotificationHandlerOrder(2)] // Runs second
public class EmailHandler : INotificationHandler<UserCreated> { }
[BehaviorOrder] - Pipeline Behavior Order
[BehaviorOrder(1)] // Runs first (outermost)
public class ValidationBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse> { }
[BehaviorOrder(2)] // Runs second (inner)
public class LoggingBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse> { }
[MediatorGeneration(Skip = true)] - Obsolete and Inert
The attribute type is retained for binary compatibility, but the v2 generator ignores it: discovery is unconditional, so a handler marked Skip = true is registered like any other. If you relied on Skip to keep a handler out of registration, move that handler to an assembly the source generator does not run on (for example, a test-support project).
Step 5: Verify Generated Code
After building, check the generated code in your IDE’s Analyzers section or in:
obj/Debug/net10.0/generated/MediatorLite.SourceGeneration/
Look for:
MediatorLiteRegistration.g.cs- Registration extension methodsSourceGeneratedMediator.g.cs- Dispatch implementation
You can also access diagnostic properties:
var requestCount = MediatorLiteRegistration.RequestHandlerCount;
var notificationCount = MediatorLiteRegistration.NotificationHandlerCount;
var behaviorCount = MediatorLiteRegistration.BehaviorCount;
Configuration in v2
All runtime configuration options have been removed. AddMediatorLite() now takes no arguments:
services
.AddGeneratedHandlers()
.AddMediatorLite();
The entire configuration surface has moved to compile time:
| v1 runtime option | v2 replacement |
|---|---|
MediatorOptions class | Deleted — AddMediatorLite() takes no lambda |
EnableBuiltInLogging | Presence/absence of [assembly: DisableMediatorLogging] |
EnableTracing | Presence/absence of [assembly: DisableMediatorTracing] |
DefaultLogLevel | ILoggingBuilder.AddFilter("MediatorLite.IMediator", LogLevel.X) (or appsettings.json) |
HandlerLifetime / MediatorLifetime | Mediator is always Transient; handler lifetimes remain controlled by you at DI registration |
AddOpenBehavior(...) | Behaviors are auto-discovered by the source generator |
NotificationExecutionStrategy / NotificationErrorStrategy | [NotificationExecution] / [NotificationError] per type, or [assembly: DefaultNotificationExecution] / [assembly: DefaultNotificationError] |
MediatorLoggingAttribute | Deleted — it was never consumed |
Example opt-out of observability:
// Any .cs file in the consuming assembly:
[assembly: DisableMediatorLogging]
[assembly: DisableMediatorTracing]
Example log-level configuration:
services.AddLogging(b => b.AddFilter("MediatorLite.IMediator", LogLevel.Information));
Common Migration Issues
Issue: “No handler registered” Exception
Cause: AddGeneratedHandlers() not called.
Fix: Ensure you call AddGeneratedHandlers() before AddMediatorLite().
Issue: Handlers Not Discovered
Cause: Handler not implementing the correct interface, or the handler is generic / nested inside a generic type (surfaced as warning MEDL1004).
Fix:
- Verify the handler implements
IRequestHandler<TRequest, TResponse> - Make the handler a non-generic class that is not nested inside a generic type
Issue: Behaviors Executing in Wrong Order
Cause: v1 used registration order; v2 uses [BehaviorOrder] attribute.
Fix: Add [BehaviorOrder(n)] attributes to control execution order.
Issue: Build Errors in Generated Code
Cause: Namespace or type conflicts.
Fix:
- Ensure all handler types are public
- Check for duplicate handler registrations
- Clean and rebuild the solution
Performance Improvements in v2
| Metric | v1 | v2 |
|---|---|---|
| Handler lookup | O(1) cached reflection | O(1) static dictionary |
| Pipeline wrapping | Runtime delegate chain | Compile-time unrolled |
| Memory per request | Delegate allocations | Zero allocations |
| Startup time | Reflection scanning | Immediate |
Validation → FluentValidation
The in-house validation model was removed. MediatorLite.Validation.IValidator<T>, DataAnnotationsValidator<T>, and ValidationResult no longer exist. Validation is now powered by FluentValidation via the opt-in MediatorLite.FluentValidation package.
- Add the package reference:
MediatorLite.FluentValidation(brings inFluentValidation). If the generator finds validators for handled requests but the package is missing, the build fails withMEDL1001. - Replace
IValidator<T>implementations with FluentValidationAbstractValidator<T>(RuleFor(...)). - Replace DataAnnotations (
[Required],[Range],[EmailAddress], …) with the equivalent FluentValidation rules (NotEmpty(),InclusiveBetween(...),EmailAddress(), …). ValidationExceptionandValidationErrorare unchanged — FluentValidation failures are mapped onto them, so existingcatch (ValidationException)blocks keep working.- Registration is still automatic via
AddGeneratedHandlers(); never callAddValidatorsFromAssembly(...). The generator emitsFluentValidationBehavior<,>as the outermost pipeline behavior. See validation.md.
Behavior Change: Cancellation Under ContinueAndAggregate (v2.x)
A handler-thrown OperationCanceledException is now treated as an ordinary fault when the publish CancellationToken is not cancelled:
- Parallel + ContinueAndAggregate: previously the first OCE was rethrown unwrapped and any sibling handler faults were silently dropped. Now all faults — OCEs included — arrive inside one
AggregateException. - Sequential / StopOnFirst + ContinueAndAggregate: previously any handler OCE aborted the publish and skipped the remaining handlers. Now remaining handlers keep running and the OCE is aggregated.
Genuine cancellation (the token you passed to PublishAsync is cancelled) still surfaces as an unwrapped OperationCanceledException. If you catch OperationCanceledException around PublishAsync to detect handler-internal cancellations, inspect AggregateException.InnerExceptions instead.
Summary
- Add source generator package reference
- Replace manual registration with
AddGeneratedHandlers() - Use attributes for ordering and configuration
- Remove reflection fallback code (it no longer exists)
- Build and verify generated code is correct
The v2 architecture is simpler, faster, and eliminates ~500 lines of reflection complexity.