CQRS Pattern in .NET: The Way It Should Have Been From the Start
A friendly, hands-on guide to the CQRS pattern in .NET 10. Learn commands, queries, handlers, diagrams, and when to actually use it.
A kitchen story to start with
Imagine a busy restaurant in your town. You walk in, sit down, and a waiter comes to take your order. You say, "One masala dosa and one tea, please." The waiter writes it down and walks to the kitchen.
Now think about who does what here.
The waiter who takes your order is doing one job: accepting a request to change something. A new order must be made. This is an action. It has rules. The kitchen must have dosa batter. The stove must be free.
The menu card on your table is doing a completely different job. It only shows you what is available. It never cooks anything. It never changes the kitchen. It just reads out information in a way that is easy for you to understand.
A good restaurant keeps these two jobs apart. The person taking orders is not the same as the printed menu. Mixing them would be slow and confusing.
CQRS is exactly this idea, but for software. We keep the order-taking side (writing, changing data) away from the menu side (reading, showing data). That is the whole heart of it. Everything else is detail.
What does CQRS stand for?
CQRS stands for Command Query Responsibility Segregation. That is a big mouthful, so let us break it gently.
- Command = a request to do something or change something. Like "Book this hotel room."
- Query = a request to get information. Like "Show me my bookings."
- Responsibility Segregation = a fancy way of saying "keep the two jobs separate."
So CQRS just says: the part of your code that changes data should be separate from the part that reads data. Two paths. Two models. One clear rule.
Why was the old way painful?
For years, many of us built apps in one common style. We had one big "Service" class for each thing. A ProductService would have GetProduct, GetAllProducts, CreateProduct, UpdateProduct, and DeleteProduct, all in one place. We used the same Product model for everything.
This feels fine at the start. But it grows into a mess.
Reads and writes have very different needs:
| Concern | Writing data (commands) | Reading data (queries) |
|---|---|---|
| Main goal | Protect business rules | Be fast and easy to show |
| Validation | Heavy, strict | Almost none |
| Shape of data | Full domain model | Flat DTO for the screen |
| How often it runs | Less often | Very often |
| Scaling needs | Careful, consistent | Cheap to scale wide |
When you force both jobs through one model, you get problems. Your read screens drag in heavy validation they do not need. Your write code returns big objects shaped for screens instead of for rules. Adding a new report means touching code that also saves orders. One change risks breaking the other.
CQRS is the calm answer. Give each job its own model and its own path. They stop fighting.
The three small pieces
A simple CQRS setup in .NET has three small building blocks. Do not overthink them.
- A Command or Query — a small message object that says what you want.
- A Handler — a class that knows how to do that one job.
- A Dispatcher (or mediator) — a tiny helper that finds the right handler for each message.
Let us see each one.
A command
A command is just a plain object holding the data needed to change something. Notice it asks for an action, not a low-level field edit. Microsoft Learn gives good advice here: name commands after the business task, like "Book hotel room," not "Set status to reserved."
// A command: a request to change something.
public sealed record CreateOrderCommand(
int CustomerId,
string ProductName,
int Quantity) : IRequest<int>;A command handler
The handler does the real work. It checks rules, then saves. It is the only place that "knows how" to create an order.
public sealed class CreateOrderHandler
: IRequestHandler<CreateOrderCommand, int>
{
private readonly AppDbContext _db;
public CreateOrderHandler(AppDbContext db) => _db = db;
public async Task<int> Handle(
CreateOrderCommand cmd,
CancellationToken ct)
{
if (cmd.Quantity <= 0)
throw new ArgumentException("Quantity must be at least 1.");
var order = new Order(cmd.CustomerId, cmd.ProductName, cmd.Quantity);
_db.Orders.Add(order);
await _db.SaveChangesAsync(ct);
return order.Id; // return the new id, nothing more
}
}A query and its handler
A query never changes anything. It reads, and it returns a flat DTO shaped for the screen. No domain rules. No saving.
// A query: a request to read something.
public sealed record GetOrderByIdQuery(int OrderId)
: IRequest<OrderSummaryDto?>;
public sealed record OrderSummaryDto(
int Id, string ProductName, int Quantity, string Status);
public sealed class GetOrderByIdHandler
: IRequestHandler<GetOrderByIdQuery, OrderSummaryDto?>
{
private readonly AppDbContext _db;
public GetOrderByIdHandler(AppDbContext db) => _db = db;
public async Task<OrderSummaryDto?> Handle(
GetOrderByIdQuery query,
CancellationToken ct)
{
// Read-only, no tracking, projected straight to a DTO.
return await _db.Orders
.AsNoTracking()
.Where(o => o.Id == query.OrderId)
.Select(o => new OrderSummaryDto(
o.Id, o.ProductName, o.Quantity, o.Status))
.FirstOrDefaultAsync(ct);
}
}See the difference? The command handler protects rules. The query handler just shapes data for the screen. They never get in each other's way.
How a request flows through CQRS
Here is the journey of one request, step by step, when a user creates an order.
A command travelling through the system
Steps
Controller
Builds a CreateOrderCommand from the request.
Dispatcher
Finds the one handler for that command.
Handler
Checks rules, then saves the order.
Database
Stores the new order row.
Response
Returns the new order id to the user.
And here is the read side, which is shorter because it does less work.
A query travelling through the system
Steps
Controller
Builds a GetOrderByIdQuery.
Dispatcher
Routes it to the query handler.
Handler
Reads with AsNoTracking, projects to DTO.
DTO
Returns a flat object for the screen.
The controller stays thin. It does not hold business logic. It just creates a message and sends it.
[ApiController]
[Route("api/orders")]
public sealed class OrdersController : ControllerBase
{
private readonly IMediator _mediator;
public OrdersController(IMediator mediator) => _mediator = mediator;
[HttpPost]
public async Task<IActionResult> Create(CreateOrderCommand cmd)
{
int id = await _mediator.Send(cmd);
return CreatedAtAction(nameof(GetById), new { id }, null);
}
[HttpGet("{id:int}")]
public async Task<IActionResult> GetById(int id)
{
var dto = await _mediator.Send(new GetOrderByIdQuery(id));
return dto is null ? NotFound() : Ok(dto);
}
}A quick word about MediatR and the licence change
For years, most .NET teams reached for a library called MediatR to wire up commands, queries, and handlers. It is well loved and easy to use.
But there is important news. As of July 2025, MediatR moved to a commercial model under Lucky Penny Software. Version 12 and earlier stay under their old open-source licence, but version 13 and newer need a paid commercial tier for many business uses, with a free community tier for individuals and non-commercial use. The popular messaging library MassTransit has made a similar commercial move too.
This is not a disaster. CQRS is an idea, not a library. You have good free choices:
| Option | Licence | Good to know |
|---|---|---|
| MediatR 12.x | Open source | Still free; pinned older version |
| MediatR 13+ | Commercial | Paid for most business use |
| Wolverine | Open source | Uses source generators, fast, adds messaging |
| Cortex.Mediator | MIT (free) | Drop-in style, modern features |
| LiteBus | Free | Lightweight, focused on CQS |
| Your own dispatcher | Yours | ~100 lines, no dependency, fully in your control |
Writing your own tiny dispatcher is honestly not hard. Here is a small version that resolves a handler from the .NET dependency injection container and calls it.
public interface IRequest<TResponse> { }
public interface IRequestHandler<TRequest, TResponse>
where TRequest : IRequest<TResponse>
{
Task<TResponse> Handle(TRequest request, CancellationToken ct);
}
public sealed class Mediator : IMediator
{
private readonly IServiceProvider _provider;
public Mediator(IServiceProvider provider) => _provider = provider;
public async Task<TResponse> Send<TResponse>(
IRequest<TResponse> request,
CancellationToken ct = default)
{
var handlerType = typeof(IRequestHandler<,>)
.MakeGenericType(request.GetType(), typeof(TResponse));
dynamic handler = _provider.GetRequiredService(handlerType);
return await handler.Handle((dynamic)request, ct);
}
}That is the same shape MediatR gives you, but free and under your own roof. You register each handler in Program.cs and you are done.
Simple CQRS vs full CQRS
People sometimes get scared of CQRS because they think it forces two databases. It does not. There are levels.
The most common and most useful level is the first one: one shared database, but separate command and query code. Microsoft calls this "simplified CQRS," and they use it in the eShopOnContainers sample. You get most of the benefit with very little extra cost.
You only climb to the higher levels when you truly need them. For example, a separate read database that is tuned for fast reports, kept up to date by messages. That is powerful, but it brings a new challenge called eventual consistency — the read side may be a tiny bit behind the write side for a short moment.
If you do not need that, do not build it. Keeping things simple is a feature, not a weakness.
When should you actually use CQRS?
CQRS is a tool, not a rule. Microsoft Learn is clear about this: do not force CQRS and DDD everywhere. Many parts of an app are simple and a plain CRUD service is perfectly fine.
Good signs that CQRS will help:
- Your reads and writes have very different shapes and rules.
- Your read traffic is far heavier than your write traffic, and you want to scale them apart.
- You have rich business logic on the write side that you want to keep clean and protected.
- Many people work on the same area, and clear separation reduces accidents.
Signs you probably do not need it:
- A small app or a simple admin screen.
- Basic create-read-update-delete with no real business rules.
- A team that is new and just needs to ship something this week.
A nice middle path is to use CQRS only inside the complex parts of your system, and use plain CRUD for the easy parts. You are allowed to mix. Good engineers pick the right tool for each corner of the app.
Common mistakes to avoid
A few friendly warnings, learned the hard way by many teams.
- Do not let queries change data. A query must be read-only. If it saves anything, the whole idea breaks.
- Do not share one fat model for both sides. That defeats the point. Let the read side have its own slim DTOs.
- Do not add two databases on day one. Start simple, with one database. Climb only when pain appears.
- Do not skip naming. Name commands after real business actions.
CancelOrderCommandis clearer thanUpdateOrderStatus. - Do not over-engineer. If a screen just lists ten rows, you do not need handlers, dispatchers, and DTOs for it. Be honest about complexity.
Putting it all together
Here is how the whole thing fits, from request to response, across both sides.
The full CQRS picture
Steps
Request
Controller builds a command or a query.
Dispatcher
Sends the message to its single handler.
Command path
Checks rules and writes data.
Query path
Reads and projects to a DTO.
Storage
One database, or tuned read and write stores.
When you read your own code months later, this shape is a gift. You open a folder of commands and instantly know "this is where data changes." You open a folder of queries and know "this is read-only, safe to touch." That clarity is the real prize of CQRS.
Quick recap
- CQRS = Command Query Responsibility Segregation: keep writing code separate from reading code.
- Like a restaurant, the order-taker (command) is not the menu (query). Two jobs, kept apart.
- The three small pieces are the message (command or query), the handler, and a tiny dispatcher.
- Commands change data and protect business rules. Queries only read and return flat DTOs.
- Name commands after real business actions, like "Book hotel room," not low-level field edits.
- MediatR went commercial in July 2025 (v13+). Free choices include Wolverine, Cortex.Mediator, LiteBus, or your own ~100-line dispatcher.
- CQRS has levels. Start with one database and split code only. Add separate read stores or event sourcing only when you truly need them.
- Do not force CQRS everywhere. Use it for the complex parts and plain CRUD for the simple parts.
References and further reading
- CQRS Pattern - Azure Architecture Center (Microsoft Learn)
- Applying simplified CQRS and DDD patterns in a microservice (Microsoft Learn)
- Implementing reads/queries in a CQRS microservice (Microsoft Learn)
- Applying CQRS and CQS in eShopOnContainers (Microsoft Learn)
- Build Your Own CQRS Dispatcher in .NET 10 (codewithmukesh)
- MediatR Alternative - Wolverine (TheCodeMan)
Related Patterns
The Repository Pattern in .NET: A Friendly, Complete Guide
Learn the Repository Pattern in .NET with simple real-life examples, EF Core code, diagrams, and honest advice on when to use it and when to skip it.
Event Sourcing for .NET Developers: A Simple Introduction
Learn event sourcing in .NET from scratch. Store every change as an event instead of just the current state, with a real-life bank-passbook analogy, diagrams, code, aggregates, projections, and when to use it.
Building a Custom Domain Events Dispatcher in .NET (No MediatR Needed)
Build your own domain events dispatcher in .NET with EF Core. Simple analogy, full C# code, diagrams, and timing tips — no paid MediatR license required.
Refactoring a Modular Monolith Without MediatR in .NET
Learn to remove MediatR from a .NET modular monolith using plain handlers and a tiny dispatcher, with CQRS, pipeline behaviors, and clear module boundaries.
Stop Conflating CQRS and MediatR: They Are Not the Same Thing
CQRS and MediatR are two different ideas. Learn what each one really does, why people mix them up, and how to use CQRS in .NET with or without MediatR.
Value Objects in .NET: DDD Fundamentals Made Simple
Learn value objects in .NET with simple examples. Understand equality, immutability, records vs base class, and EF Core mapping in domain-driven design.