Implementing CQRS and Event Sourcing with .NET Core

Introduction

In modern software architecture, Command Query Responsibility Segregation (CQRS) and Event Sourcing are powerful patterns that can enhance scalability, maintainability, and performance. This article explores how to implement CQRS and Event Sourcing with .NET Core, providing a comprehensive guide for developers applying these patterns in their applications.

What is CQRS?

CQRS is a pattern that separates an application's read (query) and write (command) responsibilities. This separation allows you to optimize each side independently, improving performance and scalability.

  • Commands: Represent actions that change the state of the application. Command handlers typically handle them.
  • Queries: Represent requests for information and do not change the state of the application. Query handlers typically handle them.

What is Event Sourcing?

Event Sourcing is a pattern where state changes are stored as a sequence of events rather than as a direct snapshot of the state. Each event represents a state change, and the current state can be reconstructed by replaying these events.

Benefits of CQRS and Event Sourcing

  • Scalability: CQRS allows you to scale the read and write sides independently. Event Sourcing enables scalable event replay and state reconstruction.
  • Performance: Optimized read models (projections) can be tailored for specific queries, improving performance.
  • Auditability: Event Sourcing provides a complete audit trail of changes, which can be useful for compliance and debugging.
  • Flexibility: CQRS and Event Sourcing together allow you to adapt to changing requirements by modifying read models or event processing logic without affecting the command side.

Implementing CQRS and Event Sourcing in .NET Core
 

1. Setting Up the Project

Start by creating a new .NET Core project.

dotnet new webapi -n CQRSExample
cd CQRSExample

Add necessary packages.

dotnet add package MediatR
dotnet add package AutoMapper
dotnet add package EventStore.Client

2. Defining Commands and Queries

Define commands and queries as simple classes.

Commands

public class CreateOrderCommand : IRequest<Guid>
{
    public string ProductName { get; set; }
    public int Quantity { get; set; }
}

Queries

public class GetOrderByIdQuery : IRequest<OrderDto>
{
    public Guid Id { get; set; }
}

3. Implementing Command Handlers and Query Handlers

Command Handler

public class CreateOrderCommandHandler : IRequestHandler<CreateOrderCommand, Guid>
{
    private readonly IEventStore _eventStore;
    public CreateOrderCommandHandler(IEventStore eventStore)
    {
        _eventStore = eventStore;
    }
    public async Task<Guid> Handle(CreateOrderCommand request, CancellationToken cancellationToken)
    {
        var orderId = Guid.NewGuid();
        var orderCreatedEvent = new OrderCreatedEvent
        {
            OrderId = orderId,
            ProductName = request.ProductName,
            Quantity = request.Quantity
        };        
        await _eventStore.SaveEvent(orderCreatedEvent);
        return orderId;
    }
}

Query Handler

public class GetOrderByIdQueryHandler : IRequestHandler<GetOrderByIdQuery, OrderDto>
{
    private readonly IOrderRepository _orderRepository;
    public GetOrderByIdQueryHandler(IOrderRepository orderRepository)
    {
        _orderRepository = orderRepository;
    }
    public async Task<OrderDto> Handle(GetOrderByIdQuery request, CancellationToken cancellationToken)
    {
        var order = await _orderRepository.GetByIdAsync(request.Id);
        return new OrderDto
        {
            Id = order.Id,
            ProductName = order.ProductName,
            Quantity = order.Quantity
        };
    }
}

4. Setting Up Event Sourcing

Define your events.

public class OrderCreatedEvent
{
    public Guid OrderId { get; set; }
    public string ProductName { get; set; }
    public int Quantity { get; set; }
}

Implement an event store interface and class.

public interface IEventStore
{
    Task SaveEvent(object @event);
    Task<IEnumerable<object>> GetEvents(Guid aggregateId);
}
public class EventStore : IEventStore
{
    private readonly EventStoreClient _client;
    public EventStore(EventStoreClient client)
    {
        _client = client;
    }
    public async Task SaveEvent(object @event)
    {
        // Implementation for saving events
    }
    public async Task<IEnumerable<object>> GetEvents(Guid aggregateId)
    {
        // Implementation for retrieving events
    }
}

5. Applying Event Handlers

Event handlers apply changes to the state based on events.

public class OrderEventHandler
{
    private readonly IOrderRepository _orderRepository;
    public OrderEventHandler(IOrderRepository orderRepository)
    {
        _orderRepository = orderRepository;
    }
    public async Task Handle(OrderCreatedEvent @event)
    {
        var order = new Order
        {
            Id = @event.OrderId,
            ProductName = @event.ProductName,
            Quantity = @event.Quantity
        };
        await _orderRepository.AddAsync(order);
    }
}

6. Setting Up the Application

Configure services in Startup. cs.

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers();
    services.AddMediatR(typeof(Startup).Assembly);
    services.AddAutoMapper(typeof(Startup));
    services.AddSingleton<IEventStore, EventStore>();
    services.AddSingleton<IOrderRepository, OrderRepository>();
}

Conclusion

CQRS and Event Sourcing offer robust solutions for complex applications by separating concerns and maintaining an audit trail of state changes. Implementing these patterns in .NET Core involves defining commands, queries, handlers, events, and repositories, and configuring the application to support these components. By leveraging CQRS and Event Sourcing, you can build scalable, maintainable, and flexible applications.