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.

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

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.