CQRS and Event Sourcing in C#: Architecture for Complex Systems

Online Python Trainer for Beginners

Learn Python easily without overwhelming theory. Solve practical tasks with automatic checking, get hints in Russian, and write code directly in your browser — no installation required.

Start Course

When Regular CRUD Stops Working: Introduction to CQRS and Event Sourcing

\n\n

Imagine you are writing a system for a bank. Every second, hundreds of users check balances, transfer money, and open deposits. Your application in C# runs on standard Entity Framework with typical CRUD operations. And suddenly — the database crashes. Why? Because one heavy report locked the table while a transaction was being written. Sound familiar?

\n\n

Developers of complex systems in C# eventually hit the ceiling of classical architecture. Two powerful patterns come to the rescue: CQRS (Command Query Responsibility Segregation) and Event Sourcing. Let's break them down in practice — with real examples, not dry theory.

\n\n

What is CQRS and Why Is It Not "Just Separation"?

\n\n

CQRS is not about putting methods into different folders. It is a philosophy: commands (change state) and queries (read state) should live separately. In classic CRUD, you have one data model for both reading and writing. In CQRS — at least two.

\n\n

The Problem with the Classic Approach

\n

Suppose you have a UserRepository with a GetUserById method. When you read a user to display on the screen, you need 3 fields. When you update — 10 fields and validations. As a result, one model turns into a monster with null fields and confusing mappers.

\n\n

In CQRS, you separate responsibilities:

\n
    \n
  • Command — performs an action (e.g., CreateOrderCommand)
  • \n
  • Query — returns data (e.g., GetOrderQuery)
  • \n
  • Read Model — a flat DTO tailored to a specific screen
  • \n
  • Write Model — a rich domain model with business logic
  • \n
\n\n

Writing CQRS in C# with MediatR

\n

The most popular way to implement CQRS in C# is to use the MediatR library. It implements the Mediator pattern and fits perfectly with the concept of commands and queries.

\n\n

Step 1: Define the Command

\n
public record CreateOrderCommand(\n    Guid CustomerId,\n    List<OrderItemDto> Items,\n    string ShippingAddress\n) : IRequest<Guid>;  // Returns the ID of the created order
\n\n

Step 2: Command Handler

\n
public class CreateOrderCommandHandler : IRequestHandler<CreateOrderCommand, Guid>\n{\n    private readonly IOrderRepository _orderRepository;\n    private readonly IUnitOfWork _unitOfWork;\n\n    public CreateOrderCommandHandler(IOrderRepository orderRepository, IUnitOfWork unitOfWork)\n    {\n        _orderRepository = orderRepository;\n        _unitOfWork = unitOfWork;\n    }\n\n    public async Task<Guid> Handle(CreateOrderCommand request, CancellationToken cancellationToken)\n    {\n        // Business logic: validation, creation\n        var order = new Order(request.CustomerId, request.ShippingAddress);\n        \n        foreach (var item in request.Items)\n        {\n            order.AddItem(item.ProductId, item.Quantity, item.Price);\n        }\n\n        await _orderRepository.AddAsync(order);\n        await _unitOfWork.SaveChangesAsync(cancellationToken);\n        \n        return order.Id;\n    }\n}
\n\n

Step 3: Define the Query

\n
public record GetOrderSummaryQuery(Guid OrderId) : IRequest<OrderSummaryDto>;
\n\n

Step 4: Query Handler (reads from a separate database!)

\n
public class GetOrderSummaryQueryHandler : IRequestHandler<GetOrderSummaryQuery, OrderSummaryDto>\n{\n    private readonly IReadDbContext _readDbContext; // Separate context for reading\n\n    public GetOrderSummaryQueryHandler(IReadDbContext read                

Blogs

Book Recommendations