Skip to content

Migrating from MediatR

Mediant is a drop-in-shaped replacement: IRequest<T>, IRequestHandler<,>, INotification, IStreamRequest<T> and IPipelineBehavior<,> all exist with the same semantics. You can migrate incrementally — swap the package first, adopt Result<T> and explicit CQRS types later.

1. Replace the packages

Terminal window
dotnet remove package MediatR
dotnet add package Mediant
dotnet add package Mediant.Behaviors # optional: the 11 built-in behaviors
dotnet add package Mediant.FluentValidation # optional: FluentValidation integration
dotnet add package Mediant.AspNetCore # optional: [HttpEndpoint] mapping

2. Change the registration

// Before
services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(typeof(Program).Assembly));
// After
services.AddMediant(cfg => cfg.RegisterServicesFromAssembly(typeof(Program).Assembly));

3. Update the namespaces

// Before
using MediatR;
// After
using Mediant.Abstractions;
using Mediant.Results;

4. Handlers return ValueTask

The one required code change. The shape is otherwise identical.

// Before (MediatR)
public class CreateOrderHandler : IRequestHandler<CreateOrder, OrderDto>
{
public Task<OrderDto> Handle(CreateOrder request, CancellationToken ct)
=> Task.FromResult(dto);
}
// After (Mediant)
public class CreateOrderHandler : IRequestHandler<CreateOrder, OrderDto>
{
public ValueTask<OrderDto> Handle(CreateOrder request, CancellationToken ct)
=> new(dto);
}

Pipeline behaviors change the same way:

public class LoggingBehavior<TReq, TResp> : IPipelineBehavior<TReq, TResp>
where TReq : IRequest<TResp>
{
public async ValueTask<TResp> Handle(TReq req, RequestHandlerDelegate<TResp> next, CancellationToken ct)
=> await next();
}

5. Adopt explicit CQRS (optional, gradual)

IRequest<T> keeps working. Migrate types when you’re ready:

// Before
public record CreateOrder(string Name) : IRequest<OrderDto>;
// After — the intent is now in the type
public record CreateOrder(string Name) : ICommand<Result<OrderDto>>;
public record GetOrderById(Guid Id) : IQuery<Result<Order>>;

6. Adopt the Result pattern (optional)

Stop throwing for expected failures:

public class CreateOrderHandler : ICommandHandler<CreateOrder, Result<OrderDto>>
{
public async ValueTask<Result<OrderDto>> Handle(CreateOrder cmd, CancellationToken ct)
{
if (await _repo.ExistsAsync(cmd.Name, ct))
return Error.Conflict("Order.Duplicate", "An order with that name exists.");
return Result<OrderDto>.Success(dto);
}
}

See The Result pattern.

7. Turn on the behaviors (optional)

services.AddMediantValidation(typeof(Program).Assembly);
services.AddMediantAllBehaviors();

8. Map endpoints without controllers (optional)

Program.cs
[HttpEndpoint("POST", "/api/orders")]
public record CreateOrder : ICommand<Result<Guid>> { /* ... */ }
app.MapMediantEndpoints(typeof(Program).Assembly);

API map

MediatRMediant
IRequest<T>IRequest<T>, ICommand<T>, IQuery<T>
IRequestHandler<TReq, TResp>IRequestHandler<,>, ICommandHandler<,>, IQueryHandler<,>
INotificationINotification, IDomainEvent
INotificationHandler<T>INotificationHandler<T>
IStreamRequest<T>IStreamRequest<T>
IPipelineBehavior<T, R>IPipelineBehavior<T, R> (+ IStreamPipelineBehavior)
Task<T>ValueTask<T>
AddMediatR(...)AddMediant(...)
Result<T>, Error, ErrorType
[HttpEndpoint], [Transactional], [Cacheable], …

What you gain

  • MIT license — free for any use.
  • Up to 65% faster notification dispatch, 4.7× less memory — see Benchmarks.
  • Behaviors ordered explicitly (IBehaviorOrder), not by registration order.
  • ValidateOnStartup catches a missing handler at boot, not in production.
  • Native AOT via a source generator, and built-in OpenTelemetry.