Skip to main content

Command Palette

Search for a command to run...

CQRS and MediatR in ASP.NET Core: Enterprise Decision Guide

Published
โ€ข11 min read

CQRS and MediatR have become near-synonymous in the ASP.NET Core world. Teams adopt them for the same reason they adopt most patterns โ€” because they read that Netflix or some other high-scale engineering org uses them. The problem is that pattern adoption without architectural intent is how you end up with 200 handler classes for a CRUD application that should have stayed a CRUD application. This article is not about how to implement CQRS with MediatR. It is about whether your team should, and what you actually get when you do.

Want implementation-ready .NET source code you can adapt fast? Join Coding Droplets on Patreon. ๐Ÿ‘‰ https://www.patreon.com/CodingDroplets

What CQRS Actually Means in .NET Context

Command Query Responsibility Segregation is a principle that separates the read model from the write model. In its minimal form, it simply means you do not use the same method or service class to both mutate state and return data. In its maximal form โ€” which most .NET teams never actually need โ€” it means separate databases, separate deployment units, and event sourcing.

MediatR is a mediator library that lets you decouple the caller from the handler through an in-process message bus. It is not a CQRS library. It is a general-purpose mediator. CQRS is the pattern. MediatR is one of many tools you can use to structure it.

The conflation of the two is the first thing enterprise architects need to untangle. You can implement CQRS without MediatR. You can use MediatR without implementing CQRS. Most teams do both together as a bundle, which is fine โ€” but understanding the separation helps you make better decisions about scope and complexity.

The Core Architectural Trade-Off

CQRS introduces explicit command and query objects in place of direct service method calls. The trade-off is upfront verbosity in exchange for long-term separation of concerns.

The verbosity is real. A simple "create order" operation that might live in a single OrderService.CreateOrder() method becomes a CreateOrderCommand, a CreateOrderCommandHandler, optionally a CreateOrderCommandValidator, and a separate GetOrderQuery with its own handler for reading the result back. You have created four to six files to do what one method did.

The separation of concerns is also real. When your read path and write path have genuinely different performance characteristics โ€” different indexes, different projections, different caching strategies โ€” having them structurally separate in code makes that separation manageable. When audit logging, validation, and cross-cutting concerns need to be applied consistently across every command, pipeline behaviors give you a clean interception point.

The question every enterprise team must answer is whether their domain complexity justifies that trade-off.

When CQRS with MediatR Is the Right Call

CQRS earns its complexity budget when several conditions are true simultaneously.

High write contention with read-heavy traffic. When your API serves significantly more reads than writes, and those reads can be served from projections or read-optimized stores without going through your write model, CQRS lets you optimize each path independently. A warehouse management system that processes inventory commands from warehouse workers while serving product availability queries to millions of e-commerce users is a textbook candidate.

Complex domain logic with audit requirements. When every state change needs to be recorded, validated against domain rules, and potentially reversed, the command model gives you a clean envelope to attach that behavior to. Command pipelines in MediatR make it straightforward to run validators, log audit trails, and publish domain events without polluting business logic.

Multi-team ownership. When separate teams own read APIs and write APIs, CQRS gives them a clean contract boundary. The team owning inventory commands does not need to coordinate with the team building product catalog reads. Each side evolves independently within the mediator contract.

Gradual event sourcing adoption. If your roadmap includes event sourcing โ€” storing the sequence of events that produced state rather than current state โ€” CQRS is a natural precursor. The command side emits events; the query side projects them. Adopting CQRS first means your team learns the conceptual model before layering in event store complexity.

When CQRS with MediatR Is the Wrong Call

Most internal tooling, admin dashboards, and SaaS applications with moderate domain complexity are not CQRS candidates. The pattern adds coordination overhead that outweighs the benefit when the following is true.

Your reads and writes use the same EF Core DbContext with the same entities. If the read model and write model are identical โ€” same database, same schema, same entities โ€” you are not getting the separation CQRS was designed for. You have added handler indirection without gaining independent scalability. This is the most common anti-pattern.

Your team is small and velocity matters. A five-person team shipping a SaaS product will feel the CQRS overhead in every feature. Adding a new field to a form means updating the command, the validator, the handler, the query, the query handler, and the DTO. For teams where time-to-market is the primary constraint, this overhead is not justified.

Your domain is genuinely CRUD. If your business operations are fundamentally create, read, update, delete with minimal business rules in the write path, a well-organized service layer is a better fit. CQRS does not make CRUD more correct โ€” it makes it more complicated.

You are building microservices that already have bounded context isolation. If each microservice owns a single aggregate with its own database, the command/query boundary within that service may not justify MediatR's overhead. The service boundary itself provides the separation. Adding MediatR inside a microservice that already has a focused responsibility can be architectural gilding.

The MediatR Pipeline Behaviors Decision

One of the most genuinely valuable parts of using MediatR in enterprise ASP.NET Core applications is the pipeline behavior system. Pipeline behaviors are middleware for your command and query handlers. They wrap every handler invocation and let you attach cross-cutting concerns.

Common behaviors enterprise teams implement include validation (running FluentValidation before the handler executes), logging (structured logging of command names and execution times), authorization (fine-grained pre-handler authorization checks), caching (returning cached results for queries without invoking the handler), and transaction management (wrapping command handlers in database transactions).

The architectural decision here is whether to implement these as MediatR behaviors or as ASP.NET Core middleware. The answer depends on where the cross-cutting concern belongs.

Concerns that apply to all HTTP requests โ€” authentication, rate limiting, global error handling โ€” belong in ASP.NET Core middleware. Concerns that apply to domain operations regardless of transport โ€” validation of business rules, domain event publishing, audit logging โ€” belong in MediatR pipeline behaviors. The distinction matters because MediatR commands can be dispatched from background jobs, message consumers, and scheduled tasks, not just HTTP handlers. A pipeline behavior that validates a domain invariant will protect it regardless of how the command enters the system.

Scaling Considerations and Read Model Architecture

In a full CQRS implementation, the read model is separately maintained. In practice, enterprise .NET teams implement this on a spectrum.

At the lightweight end, queries hit the same database as commands but use separate, read-optimized queries โ€” often raw SQL via Dapper or compiled EF Core queries with explicit projections. The DbContext write model is never used for reads. This is practical CQRS that delivers real benefits without a second database.

At the middle of the spectrum, a read-optimized secondary store is maintained โ€” a denormalized table or a read replica โ€” that is updated by domain events published from the command side. This requires an eventually-consistent read model and the infrastructure to synchronize it.

At the full end, event sourcing provides the write model and projections build the read model from the event log. This is appropriate for financial systems, compliance-heavy domains, and systems where the audit trail is itself a primary output, not a secondary concern.

Most enterprise teams building with ASP.NET Core in 2026 belong in the lightweight-to-middle tier. The full event sourcing implementation is a significant infrastructure commitment that requires careful evaluation of organizational readiness, not just technical capability.

Versioning Commands and Queries in Production

One operational reality that most CQRS tutorials skip is command versioning. Once your system dispatches commands โ€” especially if they cross process boundaries through a message bus โ€” you will need to handle multiple versions of the same command schema.

A CreateOrderCommand that gains a new required field is a breaking change if any consumer still sends the old version. Enterprise systems solve this with a versioning envelope, tolerant reader patterns in handlers, or explicit command version numbers in the message contract.

In MediatR's in-process model, this is less acute because you control all callers. But in hybrid architectures where some commands are published to a queue and picked up by other services, versioning becomes a first-class concern from day one.

Observability Patterns for CQRS Systems

CQRS systems โ€” especially those with separate read and write paths โ€” require deliberate observability design. The command side needs tracing that follows a write operation from HTTP receipt through handler execution, domain event publishing, and downstream projection updates. The query side needs cache hit/miss metrics and query latency histograms broken out by query type.

In ASP.NET Core, this integrates naturally with OpenTelemetry. Each MediatR pipeline behavior can emit spans and metrics to the OpenTelemetry pipeline. Command handlers emit a write span. Query handlers emit a read span. Pipeline behaviors emit child spans for validation and authorization. This gives you a complete picture of where time is spent across the domain operation lifecycle.

The operational recommendation for enterprise teams is to instrument the MediatR pipeline with OpenTelemetry from the start, not as an afterthought. Command and query names make naturally meaningful span names. Handler execution time is a leading indicator of domain model performance degradation before it appears in end-to-end latency.

Frequently Asked Questions

Is MediatR required for CQRS in ASP.NET Core? No. MediatR is a convenience that structures CQRS with a mediator pattern and provides a pipeline behavior system. You can implement CQRS with plain interfaces and manual dependency injection โ€” some teams prefer this for the explicit wiring it produces. MediatR reduces the boilerplate of that wiring and adds the behavior pipeline, but it is not architecturally required.

Does CQRS mean two databases? Not necessarily. The read and write model separation is conceptual before it is physical. Many production CQRS implementations use a single database with separate query and command paths. Two-database implementations โ€” where the read model is a separate store updated by events โ€” are appropriate when read and write performance requirements diverge significantly.

How does CQRS interact with Domain-Driven Design? CQRS complements DDD by mapping cleanly to aggregate boundaries. Commands are dispatched to aggregates, which enforce invariants and emit domain events. The query side reads from projections optimized for reporting and display. DDD provides the modeling discipline; CQRS provides the structural separation that keeps the aggregate write model clean.

What is the right granularity for commands? Commands should map to business operations, not technical operations. ApproveInvoice is a command. UpdateInvoiceStatus is a database operation. The difference matters because ApproveInvoice encodes business intent โ€” it can trigger notifications, update credit limits, and log approval history. Designing commands at the wrong granularity is the most common source of handler bloat.

How does CQRS affect API design? A well-designed CQRS API exposes commands as POST/PUT/PATCH endpoints that return a command identifier or a minimal acknowledgment, and separate GET endpoints that query the read model. This maps cleanly to HTTP semantics. The common anti-pattern is returning the full updated resource from a command endpoint โ€” which couples your write model to your read model and undermines the separation.

Should background jobs dispatch commands through MediatR? Yes, and this is one of the strongest arguments for MediatR in enterprise systems. When background jobs, message consumers, and HTTP handlers all dispatch commands through the same MediatR pipeline, your validation, logging, and authorization behaviors apply uniformly regardless of invocation source. This consistency is harder to achieve when command logic lives directly in service classes.

How does CQRS affect unit testability? CQRS improves unit testability because handlers have a single input type and a single output type. Testing a CreateOrderCommandHandler means constructing the handler with mocked dependencies, calling Handle(), and asserting on the result or side effects. There is no need to mock a multi-method service interface. This isolation is one of the legitimate productivity benefits of the pattern for teams that write unit tests consistently.

More from this blog

C

Coding Droplets

158 posts

CQRS & MediatR in ASP.NET Core: Enterprise Guide