Implementing CQRS and Mediator Design Patterns in Web API .Net 8
What is CQRS?
CQRS, short for Command Query Responsibility Segregation, is a design pattern that separates the application’s read and write operations into two parts – commands (write operations) and queries (read operations). This segregation allows for more flexible and scalable systems, particularly in complex applications or rapidly changing business requirements, where the read & write operations differ significantly.
1) Commands are actions that change the state of the system, by performing Create(), Update() and Delete() operations.
The command pattern consists of two objects:
- Command – Defines which methods should be executed.
- Command Handler – responsible for processing commands, which are requests to perform actions that change the state of the system.
2) Queries are actions that retrieve read-only data from the system without changing its state, for example, Get() operation. It doesn’t alter the data, just fetches it.
The query pattern consists of two objects:
- Query – Defines the objects to be returned.
- Query Handler – responsible for processing queries, which are requests to retrieve data from the system.
Why Use CQRS?
It’s common to see traditional architectural patterns that use the same data model or DTO for querying and persisting data and handle a significant volume of read-and-write operations. This approach works well for simple CRUD operations, but as the number of users, the volume of these operations and the complexity of the system increases, the application may encounter performance, scalability, and flexibility challenges.
The primary goal of CQRS is to use different data models effectively, providing flexibility in scenarios that require a complex structure. It allows you to create multiple DTOs without breaking architectural principles or risking data loss.
You can implement CQRS using Mediator (MediatR library) in .Net.
What is Mediator?
The Mediator is a behavioral design pattern that facilitates communication between different components or objects in a system by centralizing this interaction through a mediator object. Instead of components communicating directly with each other, which can lead to a tightly coupled and complex system, they interact with the mediator, which handles the communication and coordination between them.
In short, Mediator makes a bridge between different objects, which eliminates the dependency between them as they do not communicate directly.
In .NET Core, the Mediator pattern is often implemented using the MediatR library, which is a popular library for implementing this pattern. When using MediatR, you create a request (like a command or query) and a corresponding handler. The request is sent to the mediator, which then routes the requests to their respective handlers.
Pros: Independence between different objects, Centralized communication, and Easy maintenance.
Cons: Greater complexity, and it can become a bottleneck in an application if it has to handle a large amount of data.
CQRS and Mediator Implementation
Let’s create a simple Web API project and understand the basic implementation of CQRS design pattern using MediatR library, step-by-step.
1) Create a new ASP.NET Core Web API project in Visual Studio.
2) Add project Dependencies in .csproj file or via NuGet package manager.
<PackageReference Include="MediatR" Version="12.4.0" />
<PackageReference Include="Microsoft.EntityFrameworkCore" Version="8.0.8" />
<PackageReference Include="Microsoft.EntityFrameworkCore.SqlServer" Version="8.0.8" />
<PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="8.0.8">
3) Create the Entity Model: For demonstration purposes, create a Model class called “EmployeeModel” and add these 5 fields – Id(primary key), Name, Gender, RoldId, EmailId. This entity model will be passed to Entity Framework to make database changes.
4) Create DTO (Data Transfer Object) for “EmployeeModel” model, and name that class as “EmployeeDTO“. DTOs usually include only the data required by the client, excluding any unnecessary or sensitive information.
5) Create the Database Context: Add new class called “ApplicationDbContext” and add the following code:
using Microsoft.EntityFrameworkCore;
namespace Infrastructure;
public sealed class ApplicationDbContext : DbContext
{
public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
: base(options)
{
}
public DbSet<EmployeeModel> Employees { get; set; }
}
6) Add required services to the Program.cs file.
a. Register Mediator service:
// Adding the MediatR dependency
builder.Services.AddMediatR(m=> m.RegisterServicesFromAssemblies(typeof(Program).Assembly));
b. Add Database Context:
builder.Services.AddDbContext<ApplicationDbContext>(options =>
options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection"))
);
7) Run EF Core Commands (Add-Migration)
You can run the following commands from the ‘Package Manager Console’ in Visual Studio:
- Add-Migration InitialModel
- Update-Database
Alternatively, you can run the commands below in a project root terminal.
- dotnet ef migrations add InitialModel
- dotnet ef database update
8) Implement the CQRS design pattern
We will follow the Vertical Slice Architecture approach to organize our folder structure by feature.
Create a new folder called “Employee” under “Features” folder and inside it create two new folders “Commands” and “Queries.”
Inside the Commands and Queries folders, create a folder for each function you need to execute like Create, Read, Update, and Delete operations. See the folder structure below, organized by feature-wise.

9) Create the Query/Command class: Create a Handler for each Query/Command class, follow the code given below:
Create Queries
a. Get All Employees:
public sealed record GetAllEmployeeQuery : IRequest<List<EmployeeDTO>>
{
}
public sealed record GetAllEmployeeQueryHandler : IRequestHandler<GetAllEmployeeQuery, List<EmployeeDTO>>
{
public GetAllEmployeeQueryHandler(ApplicationDbContext dbContext)
{
_dbContext = dbContext;
}
private readonly ApplicationDbContext _dbContext;
public async Task<List<EmployeeDTO>> Handle(GetAllEmployeeQuery request, CancellationToken cancellationToken)
{
return await _dbContext.Employees
.AsNoTracking()
.Select(s=> new EmployeeDTO
{
Id = s.Id,
Name = s.Name,
EmailId = s.EmailId,
Gender = s.Gender,
RoleId = s.RoleId
})
.ToListAsync(cancellationToken);
}
}
b. Get Employee By Id:
public sealed record GetEmployeeByIdQuery(int Id) : IRequest<EmployeeDTO>
{
}
public sealed record GetEmployeeByIdQueryHandler : IRequestHandler<GetEmployeeByIdQuery, EmployeeDTO>
{
public GetEmployeeByIdQueryHandler(ApplicationDbContext dbContext)
{
_dbContext = dbContext;
}
private readonly ApplicationDbContext _dbContext;
public async Task<EmployeeDTO> Handle(GetEmployeeByIdQuery request, CancellationToken cancellationToken)
{
var result = await _dbContext.Employees
.AsNoTracking()
.FirstOrDefaultAsync(w => w.Id == request.Id, cancellationToken);
return new EmployeeDTO
{
Id = result.Id,
Name = result.Name,
EmailId = result.EmailId,
Gender = result.Gender,
RoleId = result.RoleId
};
}
}
Create Commands
a. Create Employee:
public sealed record CreateEmployeeCommand(EmployeeDTO EmployeeDTO) : IRequest<EmployeeDTO>
{
}
public sealed record CreateEmployeeCommandHandler : IRequestHandler<CreateEmployeeCommand, EmployeeDTO>
{
public CreateEmployeeCommandHandler(ApplicationDbContext dbContext)
{
_dbContext = dbContext;
}
private readonly ApplicationDbContext _dbContext;
public async Task<EmployeeDTO> Handle(CreateEmployeeCommand request, CancellationToken cancellationToken)
{
var employee = new EmployeeModel
{
Name = request.EmployeeDTO.Name,
Gender = request.EmployeeDTO.Gender,
EmailId = request.EmployeeDTO.EmailId,
RoleId = request.EmployeeDTO.RoleId,
};
_dbContext.Employees.Add(employee);
await _dbContext.SaveChangesAsync(cancellationToken);
return request.EmployeeDTO;
}
}
b. Update Employee:
public sealed record UpdateEmployeeCommand(EmployeeDTO EmployeeDTO) : IRequest<EmployeeDTO>
{
}
public sealed record UpdateEmployeeCommandHandler : IRequestHandler<UpdateEmployeeCommand, EmployeeDTO>
{
public UpdateEmployeeCommandHandler(ApplicationDbContext dbContext)
{
_dbContext = dbContext;
}
private readonly ApplicationDbContext _dbContext;
public async Task<EmployeeDTO> Handle(UpdateEmployeeCommand request, CancellationToken cancellationToken)
{
var employee = new EmployeeModel
{
Id = request.EmployeeDTO.Id,
Name = request.EmployeeDTO.Name,
Gender = request.EmployeeDTO.Gender,
EmailId = request.EmployeeDTO.EmailId,
RoleId = request.EmployeeDTO.RoleId,
};
_dbContext.Employees.Update(employee);
await _dbContext.SaveChangesAsync(cancellationToken);
return request.EmployeeDTO;
}
}
c. Delete Employee:
public sealed record DeleteEmployeeCommand(int id) : IRequest
{
}
public class DeleteEmployeeCommandHandler : IRequestHandler<DeleteEmployeeCommand>
{
public DeleteEmployeeCommandHandler(ApplicationDbContext dbContext)
{
_dbContext = dbContext;
}
private readonly ApplicationDbContext _dbContext;
public async Task Handle(DeleteEmployeeCommand request, CancellationToken cancellationToken)
{
var employee = await _dbContext.Employees.
Where(w=> w.Id == request.id).
FirstOrDefaultAsync(cancellationToken);
if (employee != null)
{
_dbContext.Employees.Remove(employee);
await _dbContext.SaveChangesAsync(cancellationToken);
}
}
}
10) Create API endpoints: Now we have all the required commands/queries and handlers in place, let’s wire them up with APIs. For this demonstration, we will use a controller named “EmployeeController” to implement CRUD operations, each of which will execute the corresponding query or command handler via MediatR. You can use other mechanisms like Minimal API to configure endpoints.
public class EmployeeController : Controller
{
private IMediator _mediator;
protected IMediator Mediator => _mediator ??= HttpContext.RequestServices.GetService<IMediator>();
[HttpGet("GetAll")]
public async Task<IActionResult> GetAllEmployee(CancellationToken cancellationToken)
{
var query = new GetAllEmployeeQuery();
return Ok(await Mediator.Send(query, cancellationToken));
}
[HttpGet("GetById/{id}")]
public async Task<IActionResult> GetEmployeeById(int id, CancellationToken cancellationToken)
{
var query = new GetEmployeeByIdQuery(id);
return Ok(await Mediator.Send(query, cancellationToken));
}
[HttpPost("Create")]
public async Task<IActionResult> CreateEmployee([FromBody] EmployeeDTO request, CancellationToken cancellationToken)
{
if (!ModelState.IsValid) {
return BadRequest(string.Join(Environment.NewLine, ModelState.Values
.SelectMany(x => x.Errors)
.Select(x => x.ErrorMessage))); }
var command = new UpdateEmployeeCommand(request);
return Ok(await Mediator.Send(command, cancellationToken));
}
[HttpPut("Update")]
public async Task<IActionResult> UpdateEmployee([FromBody] EmployeeDTO request, CancellationToken cancellationToken)
{
if (!ModelState.IsValid)
{
return BadRequest(string.Join(Environment.NewLine, ModelState.Values
.SelectMany(x => x.Errors)
.Select(x => x.ErrorMessage)));
}
var command = new UpdateEmployeeCommand(request);
return Ok(await Mediator.Send(command, cancellationToken));
}
[HttpDelete("DeleteById/{id}")]
public async Task<IActionResult> DeleteEmployee(int id, CancellationToken cancellationToken)
{
var command = new DeleteEmployeeCommand(id);
await Mediator.Send(command, cancellationToken);
return Ok();
}
}
11) Test the Endpoints via Swagger: The implementation is complete. Now, let’s test it using Swagger! Build and run your ASP.NET Core application and open the Swagger UI. The screenshots below display the API test results from Swagger. You can also use other Web API testing tools like Postman, Fiddler, etc.
adding a new record – screenshot

getting a record by id – screenshot

Conclusion
We learned how CQRS and the Mediator pattern work, explored how to implement them using the MediatR library, and also covered Entity Framework Core and the Vertical Slice Architecture approach in a .NET 8 Web API.