Published on

CQRS Architecture

Authors
CQRS

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

  1. 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.

  2. 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.

  3. 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.

  4. 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.

  5. 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.