DEV Community

Cover image for Implementing CQRS and Event Sourcing in .NET Core 8
Paulo Torres
Paulo Torres

Posted on

Implementing CQRS and Event Sourcing in .NET Core 8

In the realm of complex software architectures, Command Query Responsibility Segregation (CQRS) and Event Sourcing (ES) are two powerful patterns that can help build scalable, maintainable, and robust applications. This article explores the implementation of CQRS and Event Sourcing in .NET Core 8, using real-world examples to illustrate these advanced techniques.

Understanding CQRS

Principle Overview

CQRS separates the read and write operations of a system into distinct models, allowing for optimized queries and commands. This separation can improve performance, scalability, and maintainability.

Command Model

The command model handles operations that modify the state of the application. This involves creating commands that encapsulate all the necessary information to perform the operation and handlers that execute the logic.

Example: E-commerce Order Management

Models/Commands/PlaceOrderCommand.cs

public class PlaceOrderCommand
{
    public Guid OrderId { get; set; }
    public Guid CustomerId { get; set; }
    public List<OrderItem> Items { get; set; }
}

public class OrderItem
{
    public Guid ProductId { get; set; }
    public int Quantity { get; set; }
    public decimal Price { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

Handlers/Commands/PlaceOrderHandler.cs

public class PlaceOrderHandler : ICommandHandler<PlaceOrderCommand>
{
    private readonly IEventStore _eventStore;

    public PlaceOrderHandler(IEventStore eventStore)
    {
        _eventStore = eventStore;
    }

    public async Task Handle(PlaceOrderCommand command)
    {
        var orderPlacedEvent = new OrderPlacedEvent
        {
            OrderId = command.OrderId,
            CustomerId = command.CustomerId,
            Items = command.Items
        };
        await _eventStore.SaveEventAsync(orderPlacedEvent);
    }
}
Enter fullscreen mode Exit fullscreen mode

Query Model

The query model is optimized for reading data. It involves creating queries that represent the data retrieval needs and handlers that perform the retrieval.

Example: E-commerce Order Management

Models/Queries/GetOrderDetailsQuery.cs

public class GetOrderDetailsQuery
{
    public Guid OrderId { get; set; }
}

public class OrderDetailsDto
{
    public Guid OrderId { get; set; }
    public Guid CustomerId { get; set; }
    public List<OrderItemDto> Items { get; set; }
}

public class OrderItemDto
{
    public Guid ProductId { get; set; }
    public int Quantity { get; set; }
    public decimal Price { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

Handlers/Queries/GetOrderDetailsHandler.cs

public class GetOrderDetailsHandler : IQueryHandler<GetOrderDetailsQuery, OrderDetailsDto>
{
    private readonly IReadOnlyRepository<Order> _orderReadRepository;

    public GetOrderDetailsHandler(IReadOnlyRepository<Order> orderReadRepository)
    {
        _orderReadRepository = orderReadRepository;
    }

    public async Task<OrderDetailsDto> Handle(GetOrderDetailsQuery query)
    {
        var order = await _orderReadRepository.GetByIdAsync(query.OrderId);
        return new OrderDetailsDto
        {
            OrderId = order.Id,
            CustomerId = order.CustomerId,
            Items = order.Items.Select(i => new OrderItemDto
            {
                ProductId = i.ProductId,
                Quantity = i.Quantity,
                Price = i.Price
            }).ToList()
        };
    }
}
Enter fullscreen mode Exit fullscreen mode

Understanding Event Sourcing

Principle Overview

Event Sourcing ensures that all changes to application state are stored as a sequence of events. This approach not only provides an audit trail but also allows for rebuilding state by replaying events.

Event Model

The event model captures the essential information about state changes. Each event represents a significant change that occurred in the system.

Example: E-commerce Order Management

Models/Events/OrderPlacedEvent.cs

public class OrderPlacedEvent
{
    public Guid OrderId { get; set; }
    public Guid CustomerId { get; set; }
    public List<OrderItem> Items { get; set; }
}
Enter fullscreen mode Exit fullscreen mode

Event Store

The event store is responsible for saving and retrieving events. It acts as the primary source of truth for the state of the application.

Infrastructure/EventStore.cs

public class EventStore : IEventStore
{
    private readonly List<IEvent> _events = new List<IEvent>();

    public async Task SaveEventAsync(IEvent @event)
    {
        _events.Add(@event);
        await Task.CompletedTask;
    }

    public async Task<List<IEvent>> GetEventsAsync(Guid aggregateId)
    {
        return await Task.FromResult(_events.Where(e => e.AggregateId == aggregateId).ToList());
    }
}
Enter fullscreen mode Exit fullscreen mode

Rebuilding State

By replaying events from the event store, you can rebuild the current state of an entity.

Models/Order.cs

public class Order
{
    public Guid Id { get; set; }
    public Guid CustomerId { get; set; }
    public List<OrderItem> Items { get; set; }

    public void Apply(OrderPlacedEvent @event)
    {
        Id = @event.OrderId;
        CustomerId = @event.CustomerId;
        Items = @event.Items;
    }

    public static Order Rebuild(IEnumerable<IEvent> events)
    {
        var order = new Order();
        foreach (var @event in events)
        {
            order.Apply((dynamic)@event);
        }
        return order;
    }
}
Enter fullscreen mode Exit fullscreen mode

Combining CQRS and Event Sourcing

Implementation Strategy

Combining CQRS and Event Sourcing allows the command model to update state by storing events, while the query model reads state by replaying events.

Handlers/Commands/PlaceOrderHandler.cs

public class PlaceOrderHandler : ICommandHandler<PlaceOrderCommand>
{
    private readonly IEventStore _eventStore;

    public PlaceOrderHandler(IEventStore eventStore)
    {
        _eventStore = eventStore;
    }

    public async Task Handle(PlaceOrderCommand command)
    {
        var orderPlacedEvent = new OrderPlacedEvent
        {
            OrderId = command.OrderId,
            CustomerId = command.CustomerId,
            Items = command.Items
        };
        await _eventStore.SaveEventAsync(orderPlacedEvent);
    }
}
Enter fullscreen mode Exit fullscreen mode

Handlers/Queries/GetOrderDetailsHandler.cs

public class GetOrderDetailsHandler : IQueryHandler<GetOrderDetailsQuery, OrderDetailsDto>
{
    private readonly IEventStore _eventStore;

    public GetOrderDetailsHandler(IEventStore eventStore)
    {
        _eventStore = eventStore;
    }

    public async Task<OrderDetailsDto> Handle(GetOrderDetailsQuery query)
    {
        var events = await _eventStore.GetEventsAsync(query.OrderId);
        var order = Order.Rebuild(events);
        return new OrderDetailsDto
        {
            OrderId = order.Id,
            CustomerId = order.CustomerId,
            Items = order.Items.Select(i => new OrderItemDto
            {
                ProductId = i.ProductId,
                Quantity = i.Quantity,
                Price = i.Price
            }).ToList()
        };
    }
}
Enter fullscreen mode Exit fullscreen mode

Conclusion

Implementing CQRS and Event Sourcing in .NET Core 8 can significantly enhance the scalability, maintainability, and robustness of your applications. By separating commands and queries and storing state changes as events, you create a system that is easier to manage, extend, and debug. Embrace these patterns to unlock the full potential of your .NET Core applications.

Top comments (1)

Collapse
 
jangelodev profile image
João Angelo

Hi Paulo Torres,
Top, very nice and helpful !
Thanks for sharing.