Migration Guides
v10.x → v10.2.0
💥 Breaking Change: Result removed from the mediator contract — errors are now exceptions
In 10.x, handlers and Send calls returned Result / Result<T>, and failures were expressed as failure results. In 10.2.0 the mediator contract is built on plain values and exceptions:
- Handlers return plain values —
Task<TResponse>for request/response handlers,Taskfor void request handlers.INotificationHandler<TNotification>is unchanged (Task). IMediator.Send<TRequest, TResponse>returnsTask<TResponse>;Send<TRequest>(void) returnsTask. Context overloads add aPipelineContext<TContext>? context = nullparameter.Publishis unchanged.- Errors are exceptions. Handlers throw typed exceptions from
RA.Utilities.Core.Exceptions(NotFoundException,BadRequestException,ConflictException,ForbiddenException,UnauthorizedException,UnprocessableException,GatewayTimeoutException,ServiceUnavailableException,TooManyRequestsException).ValidationBehaviornow throws aBadRequestExceptioninstead of returning a failureResult. - The API layer's
GlobalExceptionHandlermaps those exceptions to HTTP responses. - The
Result/Result<T>monad still exists inRA.Utilities.Core.Resultsfor callers who want it — it just no longer participates in the mediator contract.
Signature changes at a glance
| Member | Before (10.x) | After (10.2.0) |
|---|---|---|
IRequestHandler<TRequest, TResponse>.HandleAsync | Task<Result<TResponse>> | Task<TResponse> |
IRequestHandler<TRequest>.HandleAsync (void) | Task<Result> | Task |
IMediator.Send<TRequest, TResponse> | Task<Result<TResponse>> | Task<TResponse> |
IMediator.Send<TRequest> (void) | Task<Result> | Task |
RequestHandlerDelegate<TResponse> | Task<Result<TResponse>> | Task<TResponse> |
RequestHandlerDelegate | Task<Result> | Task |
RequestHandler<TRequest, TResponse> abstract member | Task<Result<TResponse>> HandleAsync(...) | Task<TResponse> HandleAsync(...) |
RequestHandler<TRequest> abstract member | Task<Result> HandleAsync(...) | Task HandleAsync(...) |
ValidationBehavior on failure | Returns Result.Failure | Throws BadRequestException |
Step 1: Change request/response types from Result<T> to plain T
Remove Result<...> from your request declarations and handler bases:
Before (10.x):
public record CreateProductCommand(string Name, decimal Price) : IRequest<Result<int>>;
public class CreateProductHandler : RequestHandler<CreateProductCommand, Result<int>>
{
public override async Task<Result<int>> HandleAsync(
CreateProductCommand command, CancellationToken ct)
{
var id = await _productRepository.AddAsync(new Product(command.Name, command.Price));
return id; // implicit Result.Success(id)
}
}
After (10.2.0):
public record CreateProductCommand(string Name, decimal Price) : IRequest<int>;
public class CreateProductHandler : RequestHandler<CreateProductCommand, int>
{
public override async Task<int> HandleAsync(
CreateProductCommand command, CancellationToken ct)
{
var id = await _productRepository.AddAsync(new Product(command.Name, command.Price));
return id;
}
}
Replace return Result.Success(x); with return x;. For void handlers, replace return Result.Success(); with a plain return; (or nothing):
Before (10.x):
public class DeleteProductHandler : RequestHandler<DeleteProductCommand>
{
public override async Task<Result> HandleAsync(
DeleteProductCommand command, CancellationToken ct)
{
await _repository.RemoveAsync(command.ProductId, ct);
return Result.Success();
}
}
After (10.2.0):
public class DeleteProductHandler : RequestHandler<DeleteProductCommand>
{
public override async Task HandleAsync(
DeleteProductCommand command, CancellationToken ct)
{
await _repository.RemoveAsync(command.ProductId, ct);
}
}
Step 2: Throw typed exceptions instead of returning failure results
Every return Result.Failure(...), return exception; (implicit Exception → Result conversion), or return Result.Success();-style failure path becomes a throw of the corresponding typed exception:
Before (10.x):
if (await _productRepository.DoesProductExistAsync(command.Name))
return new ConflictException(nameof(Product), command.Name);
var product = await _repository.FindByIdAsync(query.ProductId, ct);
if (product is null)
return new NotFoundException(nameof(Product), query.ProductId);
After (10.2.0):
if (await _productRepository.DoesProductExistAsync(command.Name))
throw new ConflictException(nameof(Product), command.Name);
var product = await _repository.FindByIdAsync(query.ProductId, ct);
if (product is null)
throw new NotFoundException(nameof(Product), query.ProductId);
Unhandled exceptions now propagate out of the mediator instead of being captured in a failed Result — make sure your API layer uses the GlobalExceptionHandler (it maps RaBaseException subclasses to their HTTP responses and every other exception to 500).
Step 3: Update call sites that unwrap results
Callers that checked result.IsSuccess, read result.Value, or chained OnFailure/Match/OnSuccess must switch to plain values plus try/catch:
Before (10.x):
var result = await mediator.Send<GetProductQuery, Result<Product>>(query);
if (result.IsSuccess)
return Results.Ok(result.Value);
return Results.Problem(...);
After (10.2.0):
var product = await mediator.Send<GetProductQuery, Product>(query);
return Results.Ok(product);
Failures are handled centrally by GlobalExceptionHandler — no per-call unwrapping needed.
Step 4: ValidationBehavior now throws BadRequestException
Nothing changes in how you register validators — .AddValidator<TValidator>() still wires the ValidationBehavior into the pipeline — but on failure the behavior now throws a BadRequestException (built from the collected ValidationFailure entries via ValidationUtilities.CreateValidationErrorResult) instead of returning a failed Result. The GlobalExceptionHandler converts it to 400 Bad Request with the structured ValidationError collection.
Step 5: RequestHandler base classes are simplified
The base classes no longer take an ILogger constructor argument, no longer log request start/finish, and no longer catch exceptions. They are pure passthroughs: a public abstract HandleAsync plus a protected virtual context-aware overload. If you relied on the base classes for logging, register the LoggingBehavior in the pipeline instead:
// Before (10.x): handler ctor took a logger and the base wrapped exceptions in Result.Failure
public class CreateProductHandler : RequestHandler<CreateProductCommand, Result<int>>
{
public CreateProductHandler(IProductRepository repo, ILogger<CreateProductHandler> logger)
: base(logger) { }
...
}
// After (10.2.0): no logger parameter — throw typed exceptions instead
public class CreateProductHandler : RequestHandler<CreateProductCommand, int>
{
public CreateProductHandler(IProductRepository repo) { }
...
}
Step 6: Remove redundant AddFeature / AddNotification calls — handlers auto-register
A source generator shipped in the package discovers every non-abstract IRequestHandler<,>, IRequestHandler<>, and INotificationHandler<> implementation and queues its registration; services.AddMediator() applies them (request handlers scoped, notification handlers transient).
Before (10.x):
builder.Services.AddMediator();
builder.Services
.AddFeature<CreateProductCommand, Result<int>, CreateProductHandler>()
.AddDecoration<LoggingBehavior<CreateProductCommand, Result<int>>>()
.AddValidator<CreateProductCommandValidator>();
After (10.2.0):
builder.Services.AddMediator(); // registers IMediator + all handlers in the assembly
// Explicit registration is only needed for validators, behaviors, and generic handlers.
builder.Services
.AddFeature<CreateProductCommand, int, CreateProductHandler>()
.AddValidator<CreateProductCommandValidator>()
.AddDecoration<LoggingBehavior<CreateProductCommand, int>>();
Note the assembly-loading guarantee: auto-registration covers the assembly that calls AddMediator() plus any assembly already loaded by then. Handlers in lazily-loaded class libraries may register too late — keep handlers in the entry/composition assembly, touch the handler assembly at startup (typeof(SomeHandler)), or register them explicitly. See the Auto Registration guide for the full details and generator diagnostics (FEAG001–FEAG005).
What still needs explicit registration
Auto-registration covers plain request and notification handlers only. Still register explicitly:
- Pipeline behaviors —
.AddDecoration<TBehavior>()(e.g.LoggingBehavior) - Validators —
.AddValidator<TValidator>() - Notification behaviors —
.AddDecoration<TBehavior>()on theAddNotificationbuilder - Generic handlers — the generator warns
FEAG002and skips them - Handlers needing custom lifetimes or setup
What is unchanged
PublishandINotificationHandler<TNotification>signatures — notifications never carried results.- The typed
PipelineContext<T>design and its behavior. AddMediator()is still required, and still a single call.
The guides below document earlier 10.x versions of the package and show 10.x-era signatures (handlers returning
Result<T>, explicit per-feature registration). When migrating to 10.2.0 follow the guide above.
v10.0.1 → v10.1.0
Breaking Change: PipelineContext is now generic PipelineContext<T>
The dictionary-based PipelineContext (with Set<T>()/Get<T>()/TryGetValue<T>()) has been replaced with a strongly-typed PipelineContext<T> where T is a user-defined class with a parameterless constructor.
If you were not using PipelineContext in v10.0.1, this change does not affect you — all existing code compiles unchanged.
Why this change?
- Compile-time safety — typos in context keys are caught by the compiler instead of silently returning
default - No boxing — value types don't need to be boxed/unboxed
- IntelliSense — context properties are discoverable via autocompletion
- Simpler API —
ctx.Data.CorrelationIdinstead ofctx.Get<string>("correlationId")
Migration Steps
1. Define a context class
Before (v10.0.1 — hypothetical, as PipelineContext didn't exist):
// No context API available in v10.0.1
After (v10.1.0):
public class MyPipelineContext
{
public string? CorrelationId { get; set; }
public int? UserId { get; set; }
public System.Diagnostics.Stopwatch? Timer { get; set; }
}
var ctx = new PipelineContext<MyPipelineContext>();
ctx.Data.CorrelationId = "abc-123";
var id = ctx.Data.CorrelationId;
2. Write context-aware behaviors
public class CorrelationIdBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
where TRequest : IRequest<TResponse>
{
// Required: implement the non-context method for backward compatibility
public Task<Result<TResponse>> HandleAsync(
TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken ct)
=> HandleAsync(request, _ => next(), new PipelineContext<MyPipelineContext>(), ct);
// Override the generic context-aware method
public async Task<Result<TResponse>> HandleAsync<TContext>(
TRequest request, RequestHandlerContextDelegate<TResponse, TContext> next,
PipelineContext<TContext> context, CancellationToken ct)
where TContext : class, new()
{
if (context is PipelineContext<MyPipelineContext> ctx)
ctx.Data.CorrelationId = Guid.NewGuid().ToString();
return await next(context);
}
}
3. Write context-aware handlers
public class MyHandler : RequestHandler<MyCommand, Result<Data>>
{
public MyHandler(ILogger<MyHandler> logger) : base(logger) { }
// Override the abstract method for basic use
public override Task<Result<Data>> HandleAsync(MyCommand request, CancellationToken ct)
{
// Business logic without context
}
// Override the virtual method to consume typed context
protected override async Task<Result<Data>> HandleAsync<TContext>(
MyCommand request, PipelineContext<TContext> context, CancellationToken ct)
where TContext : class, new()
{
if (context is PipelineContext<MyPipelineContext> ctx)
_logger.LogInformation("CorrelationId: {Id}", ctx.Data.CorrelationId);
return await base.HandleAsync(request, context, ct);
}
}
4. Send requests with typed context
// Without context — still works, unchanged from v10.0.1
var result = await mediator.Send<MyCommand, Result<Data>>(command);
// With typed context — new in v10.1.0
var ctx = new PipelineContext<MyPipelineContext>();
ctx.Data.UserId = 42;
var result = await mediator.Send<MyCommand, Result<Data>, MyPipelineContext>(command, ctx);
// Publish notifications with typed context
await mediator.Publish<OrderPlaced, MyPipelineContext>(notification, ctx);
What Else is New in v10.1.0
These changes are additive and do not require migration:
| Change | Impact |
|---|---|
RequestHandler base classes | Opt-in — inherit from RequestHandler<TRequest, TResponse> instead of implementing IRequestHandler directly to get free logging and exception handling |
Configurable NotificationRetryBehavior | Opt-in — constructor now accepts maxRetries and baseDelayMilliseconds; defaults match v10.0.1 behavior |
Per-handler exception isolation in Publish | Automatic — one failing handler no longer prevents others from executing |
Thread-safe NotificationMetricsBehavior | Automatic — Stopwatch moved from instance field to local variable |
ValidationBehavior null guard | Automatic — prevents NullReferenceException in edge cases |
Consistent LoggingBehavior messages | Automatic — no-response variant now logs consistently |
No changes needed for existing v10.0.1 code
Existing Send<TRequest, TResponse>(...) and Publish<TNotification>(...) calls compile unchanged. The context type parameter is entirely opt-in. Internally, the Mediator uses a private marker type when no user-defined context is provided.
Registration Pattern (unchanged)
The fluent builder registration API is unchanged from v10.0.1:
builder.Services.AddMediator();
builder.Services
.AddFeature<CreateProductCommand, Result<int>, CreateProductHandler>()
.AddDecoration<LoggingBehavior<CreateProductCommand, Result<int>>>()
.AddValidator<CreateProductCommandValidator>();
v10.0.0-rc.2 → v10.0.0 / v10.0.1
API Stability
Starting with v10.0.0, the package API is considered stable. The custom mediator, pipeline behaviors, notification system, and fluent builder registration API are the supported patterns.
Key differences from the rc.2 release notes (which incorrectly referenced MediatR):
| rc.2 Documentation (incorrect) | Actual API (v10.0.0+) |
|---|---|
builder.Services.AddMediatR(cfg => ...) | builder.Services.AddMediator() |
services.AddTransient(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>)) | .AddValidator<TValidator>() on the fluent builder |
services.AddValidatorsFromAssembly(...) | Validators are registered per-feature via AddValidator<T>() |
Registration Pattern (v10.0.0+)
builder.Services.AddMediator();
builder.Services
.AddFeature<CreateProductCommand, Result<int>, CreateProductHandler>()
.AddDecoration<LoggingBehavior<CreateProductCommand, Result<int>>>()
.AddValidator<CreateProductCommandValidator>();
Full Changelog
See the package release notes for a complete version history.