ASP.NET - CQRS and MediatR Pattern in ASP.NET Core
CQRS stands for Command Query Responsibility Segregation. It is a software design pattern that separates the operations that modify data from the operations that read data. In traditional application architecture, the same model and service classes are often used for both reading and writing data. CQRS divides these responsibilities into two distinct parts:
-
Commands: Used for creating, updating, or deleting data.
-
Queries: Used for retrieving data.
This separation improves scalability, maintainability, readability, and performance in complex enterprise applications.
In ASP.NET Core, CQRS is commonly implemented along with the MediatR library. MediatR is a lightweight library that implements the Mediator design pattern. It helps reduce direct dependencies between controllers and business logic by acting as an intermediary for handling commands and queries.
Why CQRS is Important
As applications grow, combining all operations into a single service layer creates several problems:
-
Large and difficult-to-maintain service classes
-
Tight coupling between components
-
Difficulty in testing
-
Poor scalability
-
Complex business logic handling
CQRS solves these issues by organizing code into separate command and query operations.
For example:
-
A user registration operation belongs to the command side because it modifies data.
-
A user profile retrieval operation belongs to the query side because it only fetches data.
This separation allows developers to optimize read and write operations independently.
Basic Architecture of CQRS
CQRS architecture mainly contains:
Command Side
Responsible for write operations.
Examples:
-
Create Product
-
Update Employee
-
Delete Order
Commands do not return large datasets. Usually they return:
-
Success status
-
ID of created record
-
Simple acknowledgement
Query Side
Responsible for read operations.
Examples:
-
Get Product List
-
Search Customers
-
Fetch Order Details
Queries never modify data.
Introduction to MediatR
MediatR acts as a mediator between different application components. Instead of controllers directly calling services, controllers send commands or queries to MediatR, and MediatR forwards them to the appropriate handlers.
Without MediatR:
-
Controller directly depends on service classes
With MediatR:
-
Controller only depends on IMediator
-
Business logic stays inside handlers
This creates loose coupling and cleaner architecture.
Installing MediatR in ASP.NET Core
The MediatR package can be installed using NuGet Package Manager.
Common packages:
-
MediatR
-
MediatR.Extensions.Microsoft.DependencyInjection
Package Manager Console command:
Install-Package MediatR
Install-Package MediatR.Extensions.Microsoft.DependencyInjection
Registering MediatR in ASP.NET Core
In Program.cs:
builder.Services.AddMediatR(typeof(Program));
This registers all handlers automatically.
Creating a Command
Suppose we want to create a new employee.
CreateEmployeeCommand.cs
using MediatR;
public class CreateEmployeeCommand : IRequest<int>
{
public string Name { get; set; }
public string Department { get; set; }
public decimal Salary { get; set; }
}
Explanation:
-
IRequest means the command returns an integer value.
-
Usually this integer represents the created employee ID.
Creating a Command Handler
CreateEmployeeCommandHandler.cs
using MediatR;
public class CreateEmployeeCommandHandler
: IRequestHandler<CreateEmployeeCommand, int>
{
public async Task<int> Handle(
CreateEmployeeCommand request,
CancellationToken cancellationToken)
{
// Database save logic
int employeeId = 101;
return employeeId;
}
}
Explanation:
-
IRequestHandler processes the command.
-
Handle method contains business logic.
-
Command handler performs write operations.
Creating a Query
Now create a query to fetch employee details.
GetEmployeeQuery.cs
using MediatR;
public class GetEmployeeQuery : IRequest<Employee>
{
public int Id { get; set; }
public GetEmployeeQuery(int id)
{
Id = id;
}
}
Creating a Query Handler
GetEmployeeQueryHandler.cs
using MediatR;
public class GetEmployeeQueryHandler
: IRequestHandler<GetEmployeeQuery, Employee>
{
public async Task<Employee> Handle(
GetEmployeeQuery request,
CancellationToken cancellationToken)
{
return new Employee
{
Id = request.Id,
Name = "John",
Department = "IT",
Salary = 50000
};
}
}
Explanation:
-
Query handler only retrieves data.
-
No update or insert operation should occur here.
Using MediatR in Controller
EmployeeController.cs
using MediatR;
using Microsoft.AspNetCore.Mvc;
[ApiController]
[Route("api/[controller]")]
public class EmployeeController : ControllerBase
{
private readonly IMediator _mediator;
public EmployeeController(IMediator mediator)
{
_mediator = mediator;
}
[HttpPost]
public async Task<IActionResult> CreateEmployee(
CreateEmployeeCommand command)
{
var employeeId = await _mediator.Send(command);
return Ok(employeeId);
}
[HttpGet("{id}")]
public async Task<IActionResult> GetEmployee(int id)
{
var employee = await _mediator.Send(
new GetEmployeeQuery(id));
return Ok(employee);
}
}
Explanation:
-
Controller does not contain business logic.
-
Controller only sends requests to MediatR.
-
MediatR routes requests to proper handlers.
Advantages of CQRS with MediatR
Clean Separation of Concerns
Read and write logic remain independent.
Better Maintainability
Each handler contains focused functionality.
Improved Scalability
Read operations can be optimized separately from write operations.
Easier Testing
Handlers can be unit tested independently.
Reduced Coupling
Controllers do not directly depend on service implementations.
Better Organization
Large applications become more structured and manageable.
Pipeline Behaviors in MediatR
MediatR supports pipeline behaviors similar to middleware.
They allow execution of logic before or after handlers.
Common uses:
-
Logging
-
Validation
-
Exception handling
-
Performance monitoring
-
Authentication
Example:
public class LoggingBehavior<TRequest, TResponse>
: IPipelineBehavior<TRequest, TResponse>
{
public async Task<TResponse> Handle(
TRequest request,
RequestHandlerDelegate<TResponse> next,
CancellationToken cancellationToken)
{
Console.WriteLine("Request received");
var response = await next();
Console.WriteLine("Response sent");
return response;
}
}
CQRS in Real-World Applications
CQRS is widely used in:
-
Banking systems
-
E-commerce platforms
-
ERP applications
-
Healthcare systems
-
Microservices architecture
-
Large enterprise applications
These systems usually have:
-
Complex business rules
-
High scalability requirements
-
Large volumes of data
-
Multiple independent services
CQRS vs Traditional CRUD
| Feature | Traditional CRUD | CQRS |
|---|---|---|
| Model Usage | Same model for read/write | Separate models |
| Complexity | Simple | Moderate to High |
| Scalability | Limited | High |
| Maintenance | Difficult in large apps | Easier |
| Performance Optimization | Limited | Better |
| Suitable For | Small applications | Enterprise systems |
Challenges of CQRS
Although CQRS provides many benefits, it also introduces complexity.
Increased Code Files
Each operation may require:
-
Command/query
-
Handler
-
Validator
-
DTO
Learning Curve
Developers must understand:
-
Mediator pattern
-
Separation principles
-
Messaging concepts
Overengineering Risk
CQRS may not be suitable for very small applications.
For small CRUD applications, traditional architecture may be simpler and more efficient.
Best Practices
Keep Commands Focused
Each command should perform one operation only.
Avoid Business Logic in Controllers
Place all business rules inside handlers.
Use Validation Pipelines
Validate requests before processing.
Separate Read Models and Write Models
Optimize queries independently.
Use Async Operations
Improve scalability using asynchronous programming.
Organize by Features
Structure folders by feature rather than technical layers.
Example:
Features
├── Employees
│ ├── Commands
│ ├── Queries
│ ├── Handlers
│ └── Validators
Conclusion
CQRS with MediatR is a powerful architectural approach in ASP.NET Core applications. It separates read and write responsibilities, improves maintainability, simplifies testing, and creates a cleaner project structure. MediatR further enhances the architecture by reducing dependencies and centralizing request handling.
While CQRS may introduce additional complexity, it becomes extremely valuable in medium and large-scale enterprise applications where scalability, maintainability, and clean architecture are important.