Mediatr
MediatR library for implementing CQRS, commands, queries, and pipeline behaviors in .NET
You are an expert in the MediatR library for implementing the CQRS (Command Query Responsibility Segregation) and Mediator patterns in .NET applications. ## Key Points - **Separate command and query models** — commands mutate state and can use a rich domain model; queries should project directly to DTOs for efficiency. - **One handler per file** — keeps each handler small and focused; name the file after the request (e.g., `CreateOrderHandler.cs`). - **Use marker interfaces** to scope behaviors — e.g., `ICommand` for transaction behavior, so queries are not wrapped in transactions. - **Keep handlers thin** — delegate complex domain logic to domain services or aggregate methods. The handler orchestrates, it does not contain business rules. - **Order behaviors deliberately** — validation should run before transaction, logging should wrap everything. - **Over-engineering simple CRUD** — if a handler just calls `db.Add()` and `SaveChanges()`, MediatR adds indirection without benefit. Use it when you need pipeline behaviors or decoupling. - **Circular dependencies in behaviors** — injecting services in behaviors that themselves depend on MediatR can cause resolution loops. Keep behavior dependencies minimal. - **Too many behaviors** — each behavior adds overhead. Profile the pipeline if handler latency becomes an issue.
skilldb get csharp-dotnet-skills/MediatrFull skill: 307 linesMediatR CQRS Pattern — C#/.NET
You are an expert in the MediatR library for implementing the CQRS (Command Query Responsibility Segregation) and Mediator patterns in .NET applications.
Core Philosophy
Overview
MediatR is an in-process messaging library that decouples request senders from handlers. It enables CQRS by separating commands (write operations) from queries (read operations), each handled by a dedicated handler class. Pipeline behaviors provide a clean way to layer cross-cutting concerns like validation, logging, and transactions.
Core Concepts
Commands and Queries
Commands represent intent to change state. Queries represent intent to read state.
// Command: changes state, returns a result
public record CreateOrderCommand(
string CustomerId,
List<OrderItemDto> Items) : IRequest<OrderDto>;
// Command: changes state, no return value
public record CancelOrderCommand(int OrderId) : IRequest;
// Query: reads state, never mutates
public record GetOrderByIdQuery(int OrderId) : IRequest<OrderDto>;
public record GetOrdersQuery(
int Page,
int PageSize,
string? Status) : IRequest<PagedResult<OrderDto>>;
Handlers
public class CreateOrderHandler(
AppDbContext db,
ILogger<CreateOrderHandler> logger)
: IRequestHandler<CreateOrderCommand, OrderDto>
{
public async Task<OrderDto> Handle(
CreateOrderCommand request, CancellationToken ct)
{
var order = new Order
{
CustomerId = request.CustomerId,
Items = request.Items.Select(i => new OrderItem
{
ProductId = i.ProductId,
Quantity = i.Quantity,
UnitPrice = i.UnitPrice
}).ToList(),
Status = OrderStatus.Pending
};
db.Orders.Add(order);
await db.SaveChangesAsync(ct);
logger.LogInformation("Created order {OrderId} for {CustomerId}",
order.Id, request.CustomerId);
return order.ToDto();
}
}
public class GetOrderByIdHandler(AppDbContext db)
: IRequestHandler<GetOrderByIdQuery, OrderDto>
{
public async Task<OrderDto> Handle(
GetOrderByIdQuery request, CancellationToken ct)
{
var order = await db.Orders
.AsNoTracking()
.Include(o => o.Items)
.Where(o => o.Id == request.OrderId)
.Select(o => o.ToDto())
.FirstOrDefaultAsync(ct);
return order ?? throw new NotFoundException($"Order {request.OrderId} not found");
}
}
Registration
builder.Services.AddMediatR(cfg =>
{
cfg.RegisterServicesFromAssemblyContaining<CreateOrderCommand>();
cfg.AddOpenBehavior(typeof(LoggingBehavior<,>));
cfg.AddOpenBehavior(typeof(ValidationBehavior<,>));
cfg.AddOpenBehavior(typeof(TransactionBehavior<,>));
});
Implementation Patterns
Pipeline Behaviors
Behaviors wrap every request and execute in registration order, forming a pipeline.
// Logging behavior
public class LoggingBehavior<TRequest, TResponse>(
ILogger<LoggingBehavior<TRequest, TResponse>> logger)
: IPipelineBehavior<TRequest, TResponse>
where TRequest : notnull
{
public async Task<TResponse> Handle(
TRequest request,
RequestHandlerDelegate<TResponse> next,
CancellationToken ct)
{
var requestName = typeof(TRequest).Name;
logger.LogInformation("Handling {Request}: {@Payload}", requestName, request);
var sw = Stopwatch.StartNew();
var response = await next();
sw.Stop();
logger.LogInformation("Handled {Request} in {ElapsedMs}ms",
requestName, sw.ElapsedMilliseconds);
return response;
}
}
Validation Behavior with FluentValidation
public class ValidationBehavior<TRequest, TResponse>(
IEnumerable<IValidator<TRequest>> validators)
: IPipelineBehavior<TRequest, TResponse>
where TRequest : notnull
{
public async Task<TResponse> Handle(
TRequest request,
RequestHandlerDelegate<TResponse> next,
CancellationToken ct)
{
if (!validators.Any())
return await next();
var context = new ValidationContext<TRequest>(request);
var failures = (await Task.WhenAll(
validators.Select(v => v.ValidateAsync(context, ct))))
.SelectMany(r => r.Errors)
.Where(f => f is not null)
.ToList();
if (failures.Count > 0)
throw new ValidationException(failures);
return await next();
}
}
// Validator
public class CreateOrderValidator : AbstractValidator<CreateOrderCommand>
{
public CreateOrderValidator()
{
RuleFor(x => x.CustomerId).NotEmpty();
RuleFor(x => x.Items).NotEmpty().WithMessage("Order must have at least one item");
RuleForEach(x => x.Items).ChildRules(item =>
{
item.RuleFor(i => i.Quantity).GreaterThan(0);
item.RuleFor(i => i.UnitPrice).GreaterThan(0);
});
}
}
Transaction Behavior
public class TransactionBehavior<TRequest, TResponse>(AppDbContext db)
: IPipelineBehavior<TRequest, TResponse>
where TRequest : ICommand // marker interface for commands only
{
public async Task<TResponse> Handle(
TRequest request,
RequestHandlerDelegate<TResponse> next,
CancellationToken ct)
{
await using var transaction = await db.Database.BeginTransactionAsync(ct);
try
{
var response = await next();
await transaction.CommitAsync(ct);
return response;
}
catch
{
await transaction.RollbackAsync(ct);
throw;
}
}
}
Notifications (Pub/Sub)
// Notification: broadcast to all handlers
public record OrderCreatedNotification(int OrderId, string CustomerId) : INotification;
// Multiple handlers can process the same notification
public class SendOrderConfirmationEmail(IEmailSender email)
: INotificationHandler<OrderCreatedNotification>
{
public async Task Handle(OrderCreatedNotification n, CancellationToken ct)
{
await email.SendOrderConfirmationAsync(n.OrderId, n.CustomerId);
}
}
public class UpdateInventory(IInventoryService inventory)
: INotificationHandler<OrderCreatedNotification>
{
public async Task Handle(OrderCreatedNotification n, CancellationToken ct)
{
await inventory.DeductStockAsync(n.OrderId, ct);
}
}
// Publishing from a command handler
public class CreateOrderHandler(AppDbContext db, IPublisher publisher)
: IRequestHandler<CreateOrderCommand, OrderDto>
{
public async Task<OrderDto> Handle(CreateOrderCommand request, CancellationToken ct)
{
// ... create order ...
await db.SaveChangesAsync(ct);
await publisher.Publish(new OrderCreatedNotification(order.Id, request.CustomerId), ct);
return order.ToDto();
}
}
Wiring to Minimal API Endpoints
public static class OrderEndpoints
{
public static RouteGroupBuilder MapOrderEndpoints(this RouteGroupBuilder group)
{
group.MapGet("/{id:int}", async (int id, ISender sender) =>
await sender.Send(new GetOrderByIdQuery(id)));
group.MapGet("/", async ([AsParameters] GetOrdersQuery query, ISender sender) =>
await sender.Send(query));
group.MapPost("/", async (CreateOrderCommand command, ISender sender) =>
{
var order = await sender.Send(command);
return TypedResults.Created($"/orders/{order.Id}", order);
});
group.MapDelete("/{id:int}", async (int id, ISender sender) =>
{
await sender.Send(new CancelOrderCommand(id));
return TypedResults.NoContent();
});
return group;
}
}
Best Practices
- Separate command and query models — commands mutate state and can use a rich domain model; queries should project directly to DTOs for efficiency.
- One handler per file — keeps each handler small and focused; name the file after the request (e.g.,
CreateOrderHandler.cs). - Use marker interfaces to scope behaviors — e.g.,
ICommandfor transaction behavior, so queries are not wrapped in transactions. - Keep handlers thin — delegate complex domain logic to domain services or aggregate methods. The handler orchestrates, it does not contain business rules.
- Order behaviors deliberately — validation should run before transaction, logging should wrap everything.
Common Pitfalls
- Over-engineering simple CRUD — if a handler just calls
db.Add()andSaveChanges(), MediatR adds indirection without benefit. Use it when you need pipeline behaviors or decoupling. - Notification handlers silently failing — by default, an exception in one notification handler does not stop others, but the exception may be swallowed. Configure
MediatRto use a custom publisher strategy if you need error handling. - Circular dependencies in behaviors — injecting services in behaviors that themselves depend on MediatR can cause resolution loops. Keep behavior dependencies minimal.
- Sending notifications inside a transaction — if a notification handler calls an external API and the transaction rolls back, the external side effect has already occurred. Publish notifications after commit or use an outbox pattern.
- Too many behaviors — each behavior adds overhead. Profile the pipeline if handler latency becomes an issue.
Anti-Patterns
Over-engineering for hypothetical scale. Building for millions of users when you have hundreds adds complexity without value. Solve today's problems first.
Ignoring the existing ecosystem. Reinventing functionality that mature libraries already provide well wastes time and introduces unnecessary risk.
Premature abstraction. Creating elaborate frameworks and utilities before you have enough concrete cases to know what the abstraction should look like produces the wrong abstraction.
Neglecting error handling at boundaries. Internal code can trust its inputs, but system boundaries (user input, APIs, file I/O) require defensive validation.
Skipping documentation for obvious code. What is obvious to you today will not be obvious to your colleague next month or to you next year.
Install this skill directly: skilldb add csharp-dotnet-skills
Related Skills
Aspnet Minimal API
ASP.NET Minimal APIs for building lightweight HTTP endpoints in .NET
Async Patterns
Async/await patterns, Task-based concurrency, and cancellation in C#/.NET
Blazor
Blazor component model, rendering modes, and interactive web UI development in .NET
Configuration
.NET configuration system, options pattern, and secrets management
Dependency Injection
Dependency injection patterns and service registration in .NET's built-in DI container
Entity Framework
Entity Framework Core ORM for data access, migrations, and query optimization in .NET