Skip to main content

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, Task for void request handlers. INotificationHandler<TNotification> is unchanged (Task).
  • IMediator.Send<TRequest, TResponse> returns Task<TResponse>; Send<TRequest> (void) returns Task. Context overloads add a PipelineContext<TContext>? context = null parameter. Publish is unchanged.
  • Errors are exceptions. Handlers throw typed exceptions from RA.Utilities.Core.Exceptions (NotFoundException, BadRequestException, ConflictException, ForbiddenException, UnauthorizedException, UnprocessableException, GatewayTimeoutException, ServiceUnavailableException, TooManyRequestsException). ValidationBehavior now throws a BadRequestException instead of returning a failure Result.
  • The API layer's GlobalExceptionHandler maps those exceptions to HTTP responses.
  • The Result / Result<T> monad still exists in RA.Utilities.Core.Results for callers who want it — it just no longer participates in the mediator contract.

Signature changes at a glance​

MemberBefore (10.x)After (10.2.0)
IRequestHandler<TRequest, TResponse>.HandleAsyncTask<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>
RequestHandlerDelegateTask<Result>Task
RequestHandler<TRequest, TResponse> abstract memberTask<Result<TResponse>> HandleAsync(...)Task<TResponse> HandleAsync(...)
RequestHandler<TRequest> abstract memberTask<Result> HandleAsync(...)Task HandleAsync(...)
ValidationBehavior on failureReturns Result.FailureThrows 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 the AddNotification builder
  • Generic handlers — the generator warns FEAG002 and skips them
  • Handlers needing custom lifetimes or setup

What is unchanged​

  • Publish and INotificationHandler<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.CorrelationId instead of ctx.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:

ChangeImpact
RequestHandler base classesOpt-in — inherit from RequestHandler<TRequest, TResponse> instead of implementing IRequestHandler directly to get free logging and exception handling
Configurable NotificationRetryBehaviorOpt-in — constructor now accepts maxRetries and baseDelayMilliseconds; defaults match v10.0.1 behavior
Per-handler exception isolation in PublishAutomatic — one failing handler no longer prevents others from executing
Thread-safe NotificationMetricsBehaviorAutomatic — Stopwatch moved from instance field to local variable
ValidationBehavior null guardAutomatic — prevents NullReferenceException in edge cases
Consistent LoggingBehavior messagesAutomatic — 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.