CQRS, or Command Query Responsibility Segregation, is a design pattern that separates read (query) and write (command) operations in an application. This separation allows you to independently optimize how the system handles state-changing commands and data-retrieval queries, improving scalability, performance, and flexibility.
This article will explore how to implement CQRS in .NET 8 using the MediatR library and provide insights into other libraries you can use for similar purposes. Additionally, we'll cover how to integrate middleware into MediatR for pre- and post-processing operations to enhance the pipeline.
Key Benefits of CQRS
Performance Optimization:
By separating commands and queries, each path can be optimized independently. You can use caching or denormalized data for queries, while commands focus on ensuring data consistency and executing business logic.Scalability:
CQRS allows for independent scaling of the read and write sides of your system. You can horizontally scale reads (queries) across many nodes, while commands (writes) can be scaled separately, ensuring consistency.Simplified Codebase:
Since commands and queries are separated, each can be designed without affecting the other. This simplifies code and makes the system easier to maintain.Flexibility in Data Models:
Different data models can be used for reading and writing, allowing denormalized models for fast queries and normalized models for consistency during writes.Testability:
You can unit test the command and query sides independently, making testing easier and more focused.
Implementing CQRS in .NET 8 with MediatR
MediatR is a lightweight library in .NET that facilitates CQRS by handling commands and queries through request handlers. It promotes clean separation of concerns and decouples the sender of a request from its receiver.
Let’s break down the implementation into commands, queries, and handlers.
1. Command Side: Handling State Changes
Commands in CQRS are responsible for state-changing operations. These include creating, updating, or deleting entities in the system. Commands should encapsulate all the necessary data and business rules to ensure valid state transitions.
Here’s an example of a CreateOrderCommand
in .NET 8:
public class CreateOrderCommand : IRequest<Guid>
{
public string CustomerName { get; }
public List<OrderItem> Items { get; }
public CreateOrderCommand(string customerName, List<OrderItem> items)
{
CustomerName = customerName;
Items = items;
}
}
To handle this command, we’ll create a CreateOrderCommandHandler
using MediatR’s IRequestHandler
:
public class CreateOrderCommandHandler : IRequestHandler<CreateOrderCommand, Guid>
{
private readonly IOrderRepository _orderRepository;
public CreateOrderCommandHandler(IOrderRepository orderRepository)
{
_orderRepository = orderRepository;
}
public async Task<Guid> Handle(CreateOrderCommand request, CancellationToken cancellationToken)
{
var order = new Order(Guid.NewGuid(), request.CustomerName, request.Items);
await _orderRepository.SaveAsync(order);
return order.Id;
}
}
Here, the CreateOrderCommandHandler
processes the command and persists the new order using a repository. The command handler is responsible only for the write logic, keeping it simple and focused.
2. Query Side: Optimizing Data Retrieval
The query side is used to retrieve data. Queries do not modify the state of the system, and you can optimize them for performance by using caching, different data stores, or denormalized data models.
Here’s an example of a query to fetch an order by its ID:
public class GetOrderByIdQuery : IRequest<OrderDto>
{
public Guid OrderId { get; }
public GetOrderByIdQuery(Guid orderId)
{
OrderId = orderId;
}
}
A query handler for this query will look like this:
public class GetOrderByIdQueryHandler : IRequestHandler<GetOrderByIdQuery, OrderDto>
{
private readonly IOrderReadRepository _orderReadRepository;
public GetOrderByIdQueryHandler(IOrderReadRepository orderReadRepository)
{
_orderReadRepository = orderReadRepository;
}
public async Task<OrderDto> Handle(GetOrderByIdQuery request, CancellationToken cancellationToken)
{
var order = await _orderReadRepository.GetByIdAsync(request.OrderId);
return new OrderDto
{
Id = order.Id,
CustomerName = order.CustomerName,
TotalAmount = order.Items.Sum(i => i.Price * i.Quantity)
};
}
}
This handler retrieves the data from a read-optimized repository and returns a DTO (Data Transfer Object) that contains only the necessary information for the client.
3. Using MediatR Middleware for Pre- and Post-Processing
MediatR provides support for adding middleware into the request pipeline, allowing you to perform operations before or after a command or query is handled. This is useful for logging, validation, performance monitoring, or modifying requests/responses.
Here’s an example of adding pre- and post-processing behavior with MediatR:
Step 1: Create a Middleware Behavior
public class LoggingBehavior<TRequest, TResponse> : IPipelineBehavior<TRequest, TResponse>
{
private readonly ILogger<LoggingBehavior<TRequest, TResponse>> _logger;
public LoggingBehavior(ILogger<LoggingBehavior<TRequest, TResponse>> logger)
{
_logger = logger;
}
public async Task<TResponse> Handle(TRequest request, RequestHandlerDelegate<TResponse> next, CancellationToken cancellationToken)
{
_logger.LogInformation($"Handling {typeof(TRequest).Name}");
// Pre-processing
var response = await next(); // Call the next handler in the pipeline
// Post-processing
_logger.LogInformation($"Handled {typeof(TResponse).Name}");
return response;
}
}
Step 2: Register the Middleware
You can register the middleware in your Program.cs
or Startup.cs
by adding the following:
builder.Services.AddTransient(typeof(IPipelineBehavior<,>), typeof(LoggingBehavior<,>));
This middleware will log the handling of every command and query in your application, providing pre- and post-execution logging.
4. Alternative Libraries for CQRS in .NET 8
While MediatR is widely used, other libraries can be employed for implementing CQRS in .NET 8:
Brighter: Brighter is a command dispatcher and handler library that supports CQRS, including features like outbox pattern and distributed task scheduling. It’s well-suited for more complex systems requiring event sourcing and message dispatching.
SimpleCQRS: A minimalistic library for implementing CQRS in .NET, focused on simplifying the CQRS pattern while providing extensibility for more complex use cases.
FluentValidation: Although not a CQRS library per se, FluentValidation is often used in combination with MediatR for validating commands before they are processed.
Unit Testing CQRS with Xunit and Moq
Unit testing CQRS implementations ensures that both the command and query sides work independently and correctly. Using Xunit and Moq, we can mock dependencies such as repositories to isolate the logic inside command and query handlers. Below, we explore how to test both handlers effectively.
Unit Testing the Command Handler
For the CreateOrderCommandHandler
, we want to verify that the command handler saves a new order when a valid command is issued. Using Moq, we mock the IOrderRepository
to avoid actual database interactions:
public class CreateOrderCommandHandlerTests
{
[Fact]
public async Task Handle_ShouldSaveOrder_WhenCommandIsValid()
{
// Arrange
var mockOrderRepository = new Mock<IOrderRepository>();
var handler = new CreateOrderCommandHandler(mockOrderRepository.Object);
var command = new CreateOrderCommand("Test Customer", new List<OrderItem>());
// Act
var result = await handler.Handle(command, CancellationToken.None);
// Assert
Assert.NotEqual(Guid.Empty, result); // Verify a valid order ID is returned
mockOrderRepository.Verify(r => r.SaveAsync(It.IsAny<Order>()), Times.Once); // Verify the order was saved once
}
}
This test checks that the SaveAsync
method is called when the command is handled and that a valid Guid
is returned.
Unit Testing the Query Handler
For the GetOrderByIdQueryHandler
, we need to verify that it returns the correct OrderDto
when querying by order ID. Again, we mock the repository to simulate data retrieval:
public class GetOrderByIdQueryHandlerTests
{
[Fact]
public async Task Handle_ShouldReturnOrderDto_WhenOrderExists()
{
// Arrange
var mockOrderReadRepository = new Mock<IOrderReadRepository>();
var orderId = Guid.NewGuid();
var order = new Order(orderId, "Test Customer", new List<OrderItem>());
mockOrderReadRepository.Setup(r => r.GetByIdAsync(orderId)).ReturnsAsync(order);
var handler = new GetOrderByIdQueryHandler(mockOrderReadRepository.Object);
var query = new GetOrderByIdQuery(orderId);
// Act
var result = await handler.Handle(query, CancellationToken.None);
// Assert
Assert.Equal(orderId, result.Id); // Ensure correct order ID is returned
Assert.Equal("Test Customer", result.CustomerName); // Ensure correct customer name
mockOrderReadRepository.Verify(r => r.GetByIdAsync(orderId), Times.Once); // Verify repository call
}
}
This test confirms that the query handler returns the expected data and that the repository's GetByIdAsync
method is called once.
By using Moq to mock the dependencies, these tests ensure that the business logic for both command and query handlers behaves correctly without requiring real data access.
Summary
CQRS is a powerful architectural pattern that separates the concerns of handling commands and queries, leading to more scalable, maintainable, and performant systems. In .NET 8, MediatR makes implementing CQRS straightforward by handling the separation of requests through command and query handlers.
By integrating middleware, you can further enhance the system by adding pre- and post-processing behaviors, such as logging or validation, into the request pipeline. Additionally, libraries like Brighter or SimpleCQRS offer more advanced CQRS capabilities for distributed systems or event-driven architectures.
Top comments (0)