CQRS is a pattern that decouples the read and write sides of your application, allowing you to optimize them independently.

Let’s see it in action. Imagine a simple e-commerce system. We’ll have commands to change the state of the system (like adding an item to a cart) and queries to retrieve data (like listing items in a cart).

First, we need to define our commands and queries. These are typically simple Plain Old C# Objects (POCOs) that represent the intent of an action.

// Command: Add an item to the shopping cart
public class AddItemToCartCommand : IRequest<Unit>
{
    public Guid CartId { get; set; }
    public Guid ProductId { get; set; }
    public int Quantity { get; set; }
}

// Query: Get the contents of a shopping cart
public class GetCartQuery : IRequest<CartDto>
{
    public Guid CartId { get; set; }
}

// Data Transfer Object for Cart
public class CartDto
{
    public Guid CartId { get; set; }
    public List<CartItemDto> Items { get; set; } = new List<CartItemDto>();
}

public class CartItemDto
{
    public Guid ProductId { get; set; }
    public string ProductName { get; set; }
    public int Quantity { get; set; }
    public decimal Price { get; set; }
}

Next, we introduce MediatR. MediatR is a simple, yet powerful, mediator pattern implementation that allows us to send commands and queries to their respective handlers without direct coupling.

// Command Handler: Processes the AddItemToCartCommand
public class AddItemToCartCommandHandler : IRequestHandler<AddItemToCartCommand, Unit>
{
    private readonly YourDbContext _context; // Assuming Entity Framework DbContext

    public AddItemToCartCommandHandler(YourDbContext context)
    {
        _context = context;
    }

    public async Task<Unit> Handle(AddItemToCartCommand request, CancellationToken cancellationToken)
    {
        // Logic to add item to cart in the database
        // This is the "write" side
        var cart = await _context.Carts.FindAsync(request.CartId);
        if (cart == null)
        {
            cart = new Cart { Id = request.CartId };
            _context.Carts.Add(cart);
        }

        var existingItem = cart.Items.FirstOrDefault(i => i.ProductId == request.ProductId);
        if (existingItem != null)
        {
            existingItem.Quantity += request.Quantity;
        }
        else
        {
            cart.Items.Add(new CartItem { ProductId = request.ProductId, Quantity = request.Quantity });
        }

        await _context.SaveChangesAsync(cancellationToken);
        return Unit.Value;
    }
}

// Query Handler: Processes the GetCartQuery
public class GetCartQueryHandler : IRequestHandler<GetCartQuery, CartDto>
{
    private readonly YourDbContext _context; // Assuming Entity Framework DbContext

    public GetCartQueryHandler(YourDbContext context)
    {
        _context = context;
    }

    public async Task<CartDto> Handle(GetCartQuery request, CancellationToken cancellationToken)
    {
        // Logic to retrieve cart data from the database
        // This is the "read" side
        var cart = await _context.Carts
            .Where(c => c.Id == request.CartId)
            .Select(c => new CartDto
            {
                CartId = c.Id,
                Items = c.Items.Select(i => new CartItemDto
                {
                    ProductId = i.ProductId,
                    // Assuming Product entity is linked and has Name and Price
                    ProductName = i.Product.Name,
                    Quantity = i.Quantity,
                    Price = i.Product.Price
                }).ToList()
            })
            .FirstOrDefaultAsync(cancellationToken);

        return cart ?? new CartDto { CartId = request.CartId };
    }
}

In your application’s startup (e.g., Startup.cs or Program.cs in .NET 6+), you register MediatR and your DbContext.

// In Startup.cs or Program.cs
services.AddMediatR(cfg => cfg.RegisterServicesFromAssembly(typeof(Program).Assembly));
services.AddDbContext<YourDbContext>(options =>
    options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));

To send a command or query, you inject IMediator and call SendAsync.

// In a controller or service
public class CartController : ControllerBase
{
    private readonly IMediator _mediator;

    public CartController(IMediator mediator)
    {
        _mediator = mediator;
    }

    [HttpPost("add-item")]
    public async Task<IActionResult> AddItem([FromBody] AddItemToCartCommand command)
    {
        await _mediator.Send(command);
        return Ok();
    }

    [HttpGet("{cartId}")]
    public async Task<ActionResult<CartDto>> GetCart([FromRoute] Guid cartId)
    {
        var query = new GetCartQuery { CartId = cartId };
        var cartDto = await _mediator.Send(query);
        return Ok(cartDto);
    }
}

The beauty of CQRS here is that the AddItemToCartCommand handler only cares about persisting the change, and the GetCartQuery handler only cares about retrieving data efficiently. You could, for example, have a separate, denormalized read-optimized database for your queries without impacting your write-heavy transactional database. Entity Framework is used here for simplicity on both sides, but in a more complex CQRS setup, you might use different data access technologies for reads and writes.

A common misconception is that CQRS requires separate databases. While it’s a powerful pattern when combined with separate data stores (e.g., a transactional SQL database for writes and a document database or search index for reads), you can implement CQRS with a single database as demonstrated. The primary benefit then becomes the clear separation of concerns between read and write operations within your application logic.

The next step in a more advanced CQRS implementation is often introducing event sourcing, where state changes are recorded as a sequence of immutable events rather than just the current state.

Want structured learning?

Take the full Cqrs course →