A Comprehensive Architectural Analysis of MediatR + Vertical Slice CQRS in .NET 10
Quick Review / TL;DR
- MediatR is not CQRS. It is a Mediator pattern implementation. CQRS is an architectural separation of read and write models. The two are commonly combined but are independent ideas.
- Vertical Slice Architecture organizes code by feature (axis of change), not by technical layer. Each slice owns its command, handler, validator, and endpoint end-to-end.
- MediatR fits vertical slices because it dispatches by type, lets pipeline behaviours apply cross-cutting policy uniformly, and removes the need for a shared application-service layer.
- Pipeline behaviours are typed middleware for
Send— they run perIMediator.Sendinvocation, are generic over request/response, and compose onion-style around the handler. - The 2024 licensing change affects only future major versions. MediatR 12.x remains Apache 2.0. Alternatives like
martinothamar/Mediator(source-generated, MIT, AOT-compatible) exist as drop-in replacements. - Pick vertical slices + MediatR when you have validation, logging, transactions, and tenant-scoping as cross-cutting policy. Skip MediatR when you only use
_mediator.Sendas a glorified method call.
Table of Contents
- Three Ideas That Are Commonly Confused
- Vertical Slice Architecture: Organizing by Change
- Why MediatR Fits Slices
- The Pipeline Behaviour Model
- Clean Architecture vs Vertical Slice
- The 2024 Licensing Change — Practical Implications
- Worked Example: A Booking Slice End-to-End
- Common Pitfalls & Anti-patterns
- Best Practices & Recommendations
- Conclusion
- References
1. Three Ideas That Are Commonly Confused
Before any code, three separate concepts need to be disentangled:
| Concept | What it is | What it is not |
|---|---|---|
| Mediator pattern | An object through which components communicate instead of referring to each other directly. | An architecture. A CQRS requirement. A .NET-specific thing. |
| CQRS | Separation of the model that writes (commands) from the model that reads (queries) [4]. | A library. A mediator. An event-sourcing requirement. |
| Vertical Slice Architecture | Organizing code so that a feature owns its full stack (endpoint → handler → data access) in one folder [1, 6]. | A synonym for microservices. A synonym for MediatR. |
MediatR [2] is a .NET implementation of the Mediator pattern. It is often used to dispatch commands and queries in a CQRS-shaped codebase — but MediatR works fine for non-CQRS scenarios (notifications, in-process events), and CQRS works fine with no library at all [5]. Vertical slice is an orthogonal organizing principle that can use MediatR, custom dispatchers, or direct calls.
The rest of this article is about the specific combination that dominates the .NET SaaS world in 2026: vertical slices, with MediatR as the dispatcher, handling CQRS-style commands and queries, under .NET 10.
2. Vertical Slice Architecture: Organizing by Change
The observation underlying vertical slice architecture, articulated by Jimmy Bogard [1] and Derek Comartin [6], is that most real changes to a codebase are feature-shaped, not layer-shaped. Adding a “cancel booking” capability touches an endpoint, a handler, a validator, a database write, and a notification — in the layered model, that’s five folders. In the vertical-slice model, it’s one.

The principle, stated by Bogard: “Minimize coupling between slices, maximize coupling within a slice.” [1] A slice that reaches into another slice’s types is the vertical-slice equivalent of a layer violation — it means a feature is using another feature’s implementation as API. Shared kernel is the true domain (entities, value objects); everything else is private to the slice that owns it.
A concrete folder shape:
Features/
└── Appointments/
├── Book/
│ ├── BookAppointmentCommand.cs
│ ├── BookAppointmentHandler.cs
│ ├── BookAppointmentValidator.cs
│ ├── BookAppointmentEndpoint.cs
│ └── BookAppointmentResponse.cs
├── Cancel/
│ └── ... (same pattern)
└── List/
└── ... (same pattern)
Each folder is a deployable unit of thought. Diffs fit on one screen. PR review is scoped naturally. New developers read one slice and understand one feature.
3. Why MediatR Fits Slices
Vertical slices and MediatR are a natural pairing for four reasons:
3.1 Dispatch by type, not by reference. Endpoints depend on IMediator, not on a concrete service. Adding a slice never requires modifying a shared controller or service.
3.2 Compile-time handler discovery. services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(typeof(Program).Assembly)) auto-wires every IRequestHandler<,> in the assembly. A new slice is registered by existing — no DI plumbing.
3.3 Cross-cutting policy without per-slice boilerplate. Validation, logging, transaction management, and tenant scoping are implemented once as pipeline behaviours and apply to every handler uniformly. The slice itself stays focused on business logic [2].
3.4 No shared application-service layer. Clean Architecture [11] often ends up with IOrderService, IBookingService, IPaymentService — god-object aggregates that drift into bloat. MediatR handlers are a single focused unit per command or query. The surface area is small by construction.
The trade-off — articulated by Ardalis [8] and Chapsas [9] — is that MediatR adds a runtime indirection. If you only ever use _mediator.Send(cmd) and have zero pipeline behaviours, you have invested in a dependency whose value you are not extracting. The moment you add one real pipeline behaviour (validation, for instance), the case for MediatR flips from “unnecessary” to “cleaner than the alternatives.”
4. The Pipeline Behaviour Model
IPipelineBehavior<TRequest, TResponse> is the single most powerful feature of MediatR. It is structurally similar to ASP.NET middleware — an onion of Handle(request, next, ct) calls wrapping the handler — but it differs in three important ways:
- Scope. Middleware runs per HTTP request; pipeline behaviours run per
IMediator.Sendinvocation. A background job, a SignalR hub method, a message consumer all benefit identically. - Typing. Behaviours are generic over request and response. A
ValidationBehavior<TRequest, TResponse>can resolve FluentValidation validators for exactly the request type being dispatched. - Discoverability. Behaviours are DI-registered in order; their composition is visible in
Program.cs, not hidden in a request-pipeline DSL.

The canonical ValidationBehavior:
public sealed class ValidationBehavior<TRequest, TResponse>(
IEnumerable<IValidator<TRequest>> validators)
: IPipelineBehavior<TRequest, TResponse>
where TRequest : IRequest<TResponse>
{
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();
}
}
Registration:
services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(typeof(Program).Assembly));
services.AddScoped(typeof(IPipelineBehavior<,>), typeof(ValidationBehavior<,>));
services.AddValidatorsFromAssemblyContaining<Program>();
The TransactionBehavior is the other archetype: open a DbContext transaction, call next(), commit or rollback based on whether the handler returned a failed Result<T> or threw. Once you have these two, most of the “boilerplate” in Clean Architecture’s application layer disappears.
5. Clean Architecture vs Vertical Slice
The two patterns are frequently conflated because Jason Taylor’s popular template [11] uses MediatR inside Clean Architecture. The trade-offs:
| Dimension | Clean Architecture [11] | Vertical Slice [1, 6] |
|---|---|---|
| Primary organizing axis | Technical layer (Domain / Application / Infrastructure / Web) | Business feature |
| File locality | Low — one feature spans 4+ folders | High — one folder per feature |
| Shared abstractions | Many (services, repositories, DTOs) | Few (only true domain kernel) |
| Testability | Layer-level | Feature-level (slice end-to-end) |
| Best for | Large teams, long-lived domains, strong bounded contexts | Fast iteration, CRUD-heavy or evolving APIs |
| MediatR fit | Optional (often used to decouple Web from Application) | Natural (handlers are the slice) |
Neither is universally correct. A slow-moving core domain with complex business rules benefits from Clean Architecture’s explicit layering. A CRUD-ish SaaS product iterating weekly benefits from vertical slices’ change locality. Many codebases run a hybrid: Clean Architecture for the core domain, vertical slices for the features built on top.
6. The 2024 Licensing Change — Practical Implications
In January 2024, Jimmy Bogard announced that future major versions of MediatR (and AutoMapper) would transition to a commercial license under LuckyPennySoftware [3]. The community reaction was loud; the practical impact is narrower than the reaction suggested.
What changed:
– Future major versions (v13+) will require a paid commercial license for commercial use.
– New features ship in commercial versions.
What did not change:
– MediatR 12.x remains under Apache 2.0 indefinitely.
– You can pin <PackageReference Include="MediatR" Version="12.*" /> and not change anything.
Options matrix:
| Option | Cost | Migration effort | Trade-off |
|---|---|---|---|
| Stay on v12.x | $0 | None | Freeze at the feature set of late 2023 |
| Buy commercial v13+ | SaaS-style per-seat | Zero code change | Funds maintainer; supports continued evolution |
Migrate to martinothamar/Mediator [10] |
$0 (MIT) | Hours (near drop-in) | Faster (source-generated), AOT-compatible, smaller community |
| Roll your own 50-line dispatcher | $0 | Small | You give up pipeline-behaviour polish [8] |
For most production codebases the honest recommendation is: pin to MediatR 12.x, abstract IMediator behind a thin internal interface, and keep moving. If AOT or startup time matters, the source-generated alternative is a one-weekend migration. Panic is not warranted.
7. Worked Example: A Booking Slice End-to-End
A single slice, complete:
// Features/Appointments/Book/BookAppointmentCommand.cs
public sealed record BookAppointmentCommand(
Guid ClientId,
Guid ServiceId,
DateTimeOffset SlotUtc)
: IRequest<Result<AppointmentDto>>;
// Features/Appointments/Book/BookAppointmentValidator.cs
public sealed class BookAppointmentValidator : AbstractValidator<BookAppointmentCommand>
{
public BookAppointmentValidator()
{
RuleFor(x => x.ClientId).NotEmpty();
RuleFor(x => x.ServiceId).NotEmpty();
RuleFor(x => x.SlotUtc).GreaterThan(DateTimeOffset.UtcNow);
}
}
// Features/Appointments/Book/BookAppointmentHandler.cs
public sealed class BookAppointmentHandler(
IUnitOfWork uow,
ITenantContext tenant,
ILogger<BookAppointmentHandler> logger)
: IRequestHandler<BookAppointmentCommand, Result<AppointmentDto>>
{
public async Task<Result<AppointmentDto>> Handle(
BookAppointmentCommand request, CancellationToken ct)
{
logger.LogInformation("Booking {Service} for {Client} at {Slot}",
request.ServiceId, request.ClientId, request.SlotUtc);
var repo = uow.Repository<AppointmentDE>();
if (await repo.ExistsAsync(
a => a.ClientId == request.ClientId && a.SlotUtc == request.SlotUtc, ct))
{
return Result<AppointmentDto>.Failure("Slot already booked for this client.");
}
var appt = AppointmentDE.Create(
tenant.TenantId, request.ClientId, request.ServiceId, request.SlotUtc);
await repo.AddAsync(appt, ct);
await uow.CommitAsync(ct);
return Result<AppointmentDto>.Success(appt.ToDto());
}
}
// Features/Appointments/Book/BookAppointmentEndpoint.cs (Minimal API, .NET 10 [12])
public static class BookAppointmentEndpoint
{
public static IEndpointRouteBuilder MapBookAppointment(this IEndpointRouteBuilder app)
{
app.MapPost("/api/v1/appointments",
async (BookAppointmentCommand cmd, IMediator mediator, CancellationToken ct) =>
(await mediator.Send(cmd, ct)).ToHttpResult())
.WithTags("Appointments")
.RequireAuthorization();
return app;
}
}
Everything this feature needs lives in one folder. Validation runs automatically (ValidationBehavior picks up the validator by type). Logging runs automatically (LoggingBehavior). The transaction is managed by TransactionBehavior. The handler is 20 lines and testable without a web host.
8. Common Pitfalls & Anti-patterns
Pitfall 1: “MediatR is CQRS.”
It is not. CQRS is about model separation (read vs write); MediatR is about dispatch. You can use MediatR with a single shared model and no separation at all [4, 5].Pitfall 2: “Multiple handlers per request.”
IRequest<TResponse>has exactly one handler — duplicate registration throws at runtime. If you need many listeners, useINotification(publish) instead ofIRequest(send) [2].Pitfall 3: “Pipeline behaviours are middleware.”
Similar shape, different scope. Middleware is HTTP-bound; pipeline behaviours are per-Sendand work in any caller (background jobs, tests, message consumers).Pitfall 4: “Slices can freely call each other’s handlers.”
They can technically, but this is the slice-architecture equivalent of a layering violation. Inter-slice coupling should go through shared domain (entities, value objects) or explicit notifications, not through another slice’s handler type.Pitfall 5: “MediatR is dead post-2024.”
Overstated. v12 stays Apache 2.0 forever. The licensing change affects future major versions only [3].Pitfall 6: “Using MediatR purely as
Send.”
If you never add a pipeline behaviour, MediatR is a dependency that buys you indirection and nothing else [8]. Either use pipeline behaviours or remove MediatR.Pitfall 7: “Registering behaviours in the wrong order.”
Behaviours are invoked in registration order, onion-style. Logging first means logging sees the request before validation runs; validation first means validation sees the request before logging. Choose deliberately — order is the API.
9. Best Practices & Recommendations
- Default to vertical slices for feature-shaped codebases. Use Clean Architecture’s layering only where a rich core domain justifies it.
- Use pipeline behaviours for cross-cutting policy. Validation, logging, transactions, tenant scoping, caching — each is one behaviour, applied to every handler.
- Return
Result<T>from handlers, not exceptions, for predictable domain failures. Map to HTTP in the endpoint, not in the handler. - Pin MediatR to 12.x in new codebases until your commercial-license posture is decided. Abstract
IMediatorbehind an internal interface if you expect to swap. - Treat the slice folder as a deployable unit of thought. Everything the feature needs goes in it; nothing that is not the feature’s business lives there.
- Write one test per handler, one per validator, and smoke-test endpoints via Microsoft.AspNetCore.Mvc.Testing. The slice shape makes test organization obvious.
- Log handler names and timings via a
LoggingBehavior. The behaviour runs for every slice automatically; you never instrument a handler manually again.
10. Conclusion
MediatR + vertical slice CQRS is a combination that earns its popularity by answering three separate questions at once: how to organize code (feature slices), how to dispatch commands and queries (mediator), and how to apply cross-cutting policy (pipeline behaviours). The combination is not CQRS, is not Clean Architecture, and is not dependent on any specific library — but the specific library MediatR, in its 12.x incarnation, is the shortest path from “empty .NET 10 web API” to “shippable vertical-slice product” with the smallest number of moving parts and the largest amount of testable surface. The 2024 licensing change does not disrupt this — it changes who pays, not what works. Understanding that the pattern is the point, and the library is an implementation detail, is what lets a codebase evolve gracefully whether it stays on MediatR, moves to a source-generated alternative, or eventually graduates to its own internal dispatcher.
11. References
- Jimmy Bogard — “Vertical Slice Architecture”. https://www.jimmybogard.com/vertical-slice-architecture/
- Jimmy Bogard — MediatR repository and wiki. https://github.com/jbogard/MediatR
- Jimmy Bogard — “AutoMapper and MediatR going commercial” (Jan 2024). https://www.jimmybogard.com/automapper-and-mediatr-going-commercial/
- Greg Young — “CQRS Documents”. https://cqrs.files.wordpress.com/2010/11/cqrs_documents.pdf
- Greg Young — “CQRS and Event Sourcing” (Code on the Beach 2014). https://www.youtube.com/watch?v=JHGkaShoyNs
- Derek Comartin — “Vertical Slice Architecture! But how do you structure your project?” https://codeopinion.com/vertical-slice-architecture/
- Derek Comartin — “You probably don’t need MediatR”. https://codeopinion.com/you-probably-dont-need-mediatr/
- Steve Smith — “Do I need MediatR?” https://ardalis.com/do-i-need-mediatr/
- Nick Chapsas — “Why I stopped using MediatR” (2024). https://www.youtube.com/watch?v=baiH3f_TFfY
- Martin Othamar —
Mediatorsource-generated alternative. https://github.com/martinothamar/Mediator - Jason Taylor — Clean Architecture template. https://github.com/jasontaylordev/CleanArchitecture
- Microsoft Learn — “.NET 10 Minimal APIs and endpoint filters”. https://learn.microsoft.com/aspnet/core/fundamentals/minimal-apis/
Leave a Reply