CQRS Simplified - Explained and Implemented

Introduction

The Command Query Responsibility Segregation (CQRS) pattern is a powerful architectural pattern in software design that separates read and write operations for a data store. This separation can provide several benefits, including scalability, performance, and maintainability. This article will demystify CQRS and demonstrate how to implement it using .NET Core.

What is CQRS?

CQRS stands for Command Query Responsibility Segregation. It's a pattern that separates the responsibility of handling commands (operations that change the state of the system) from queries (operations that retrieve data). In traditional CRUD (Create, Read, Update, Delete) systems, both reads and writes are handled by the same model. CQRS, however, suggests using different models for updating information and reading information.

Why Use CQRS?

  1. Performance: By separating read and write operations, you can optimize each independently. Read operations can be highly optimized using techniques like caching, while write operations can be made transactional.
  2. Scalability: CQRS allows for the independent scaling of read and write workloads. This is especially useful in systems with a high read-to-write ratio.
  3. Security: CQRS can help in implementing more granular security controls. Different models for reading and writing allow for stricter control over who can perform certain operations.
  4. Complexity Management: By clearly separating read and write concerns, the system can be easier to maintain and evolve.

Implementing CQRS in .NET Core

Let's implement a simple CQRS example in a .NET Core application. We'll create a basic application to manage a list of products.

Setting Up the Project

First, create a new .NET Core project.

dotnet new webapi -n CQRSExample
cd CQRSExample

Define Models

Create separate models for commands and queries. In this example, we'll have a Product model, a ProductCommandModel for write operations, and a ProductQueryModel for read operations.

public class Product
{
    public int Id { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }
}
public class ProductCommandModel
{
    public string Name { get; set; }
    public decimal Price { get; set; }
}
public class ProductQueryModel
{
    public int Id { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }
}

Create Command and Query Handlers

Handlers are responsible for processing commands and queries. Define interfaces for command and query handlers.

public interface ICommandHandler<TCommand>
{
    void Handle(TCommand command);
}
public interface IQueryHandler<TResult>
{
    TResult Handle();
}

Now, implement these interfaces for product-related operations.

public class CreateProductCommandHandler : ICommandHandler<ProductCommandModel>
{
    private readonly List<Product> _products;
    public CreateProductCommandHandler(List<Product> products)
    {
        _products = products;
    }
    public void Handle(ProductCommandModel command)
    {
        var product = new Product
        {
            Id = _products.Count + 1,
            Name = command.Name,
            Price = command.Price
        };
        _products.Add(product);
    }
}
public class GetAllProductsQueryHandler : IQueryHandler<List<ProductQueryModel>>
{
    private readonly List<Product> _products;
    public GetAllProductsQueryHandler(List<Product> products)
    {
        _products = products;
    }
    public List<ProductQueryModel> Handle()
    {
        return _products.Select(p => new ProductQueryModel
        {
            Id = p.Id,
            Name = p.Name,
            Price = p.Price
        }).ToList();
    }
}

Setup Dependency Injection

Register your command and query handlers in the Startup.cs file.

public void ConfigureServices(IServiceCollection services)
{
    services.AddSingleton<List<Product>>();
    services.AddScoped<ICommandHandler<ProductCommandModel>, CreateProductCommandHandler>();
    services.AddScoped<IQueryHandler<List<ProductQueryModel>>, GetAllProductsQueryHandler>();
    services.AddControllers();
}

Create Controllers

Create controllers to handle incoming HTTP requests for commands and queries.

[ApiController]
[Route("api/products")]
public class ProductsController : ControllerBase
{
    private readonly ICommandHandler<ProductCommandModel> _createProductHandler;
    private readonly IQueryHandler<List<ProductQueryModel>> _getAllProductsHandler;
    public ProductsController(
        ICommandHandler<ProductCommandModel> createProductHandler,
        IQueryHandler<List<ProductQueryModel>> getAllProductsHandler)
    {
        _createProductHandler = createProductHandler;
        _getAllProductsHandler = getAllProductsHandler;
    }
    [HttpPost]
    public IActionResult CreateProduct([FromBody] ProductCommandModel command)
    {
        _createProductHandler.Handle(command);
        return Ok();
    }
    [HttpGet]
    public IActionResult GetAllProducts()
    {
        var products = _getAllProductsHandler.Handle();
        return Ok(products);
    }
}

Testing the Implementation

Run the application and test the endpoints using a tool like Postman or Curl.

To create a new product

curl -X POST "https://localhost:5001/api/products" -H "Content-Type: application/json" -d '{"name": "Product1", "price": 10.0}'

To get all the products

curl -X GET "https://localhost:5001/api/products"

Conclusion

CQRS is a powerful pattern that can help improve your applications' scalability, performance, and maintainability by separating read and write operations. This simplified implementation in .NET Core demonstrates the core concepts and benefits of using CQRS. As your application grows, you can further enhance the CQRS pattern by integrating it with other architectural patterns like Event Sourcing and Domain-Driven Design.