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; }
}
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);
}
}
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; }
}
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()
};
}
}
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; }
}
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());
}
}
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;
}
}
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);
}
}
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()
};
}
}
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)
Hi Paulo Torres,
Top, very nice and helpful !
Thanks for sharing.