Implementing CQRS and Event Sourcing in .NET Core
In software design, Command Query Responsibility Segregation (CQRS) and Event Sourcing (ES) are 2 patterns which could significantly increase the scalability, maintainability & functionality of your programs. While CQRS allows you to decouple your read and write operations, Event Sourcing stores all changes to the application state as a sequence of events. Together, these patterns can help you develop robust and scalable applications. This write-up will discuss the concepts of CQRS and Event Sourcing and show their implementation for a .NET Core program. For those looking to enhance their projects further, leveraging professional ASP.NET development services can provide additional expertise and support.
Understanding CQRS
Command Query Responsibility Segregation (CQRS) is a pattern that separates the read and write operations of an application into distinct models. The primary objective is to optimize and scale these operations independently.
- Command Model (Write Model):
- Handles operations that modify the application state.
- Encompasses complex business logic and validations.
- Query Model (Read Model):
- Manages operations that retrieve data.
- Optimized for performance and simplicity, often bypassing complex business logic.
Benefits of CQRS
- Scalability: Read and write operations can be scaled independently based on their specific requirements.
- Performance: Read models can be optimized for fast query performance, while write models focus on maintaining data consistency.
- Maintainability: Separation of concerns makes the codebase easier to manage and understand.
Implementing CQRS in .NET Core
Setting Up the Project
Create a new .NET Core project using the following command:
dotnet new webapi -n CQRSExample
cd CQRSExample
Defining the Domain Models
For this example, let’s consider a simple e-commerce domain with Order
and Product
entities.
public class Order
{
public Guid Id { get; set; }
public DateTime OrderDate { get; set; }
public List<OrderItem> Items { get; set; }
public decimal TotalAmount => Items.Sum(i => i.Quantity * i.UnitPrice);
}
public class OrderItem
{
public Guid ProductId { get; set; }
public int Quantity { get; set; }
public decimal UnitPrice { get; set; }
}
Implementing the Command Model
Commands represent actions that modify the state of the system. Define a command for creating an order:
public class CreateOrderCommand
{
public DateTime OrderDate { get; set; }
public List<OrderItemDto> Items { get; set; }
}
public class OrderItemDto
{
public Guid ProductId { get; set; }
public int Quantity { get; set; }
public decimal UnitPrice { get; set; }
}
Handling Commands
Create a command handler to process the CreateOrderCommand
:
public class CreateOrderCommandHandler
{
private readonly ApplicationDbContext _context;
public CreateOrderCommandHandler(ApplicationDbContext context)
{
_context = context;
}
public async Task Handle(CreateOrderCommand command)
{
var order = new Order
{
Id = Guid.NewGuid(),
OrderDate = command.OrderDate,
Items = command.Items.Select(i => new OrderItem
{
ProductId = i.ProductId,
Quantity = i.Quantity,
UnitPrice = i.UnitPrice
}).ToList()
};
_context.Orders.Add(order);
await _context.SaveChangesAsync();
}
}
Implementing the Query Model
Queries are responsible for retrieving data. Define a query to get order details:
public class GetOrderQuery
{
public Guid OrderId { get; set; }
}
public class OrderDto
{
public Guid Id { get; set; }
public DateTime OrderDate { get; set; }
public List<OrderItemDto> Items { get; set; }
public decimal TotalAmount { get; set; }
}
Handling Queries
Create a query handler to process the GetOrderQuery
:
public class GetOrderQueryHandler
{
private readonly ApplicationDbContext _context;
public GetOrderQueryHandler(ApplicationDbContext context)
{
_context = context;
}
public async Task<OrderDto> Handle(GetOrderQuery query)
{
var order = await _context.Orders
.Include(o => o.Items)
.FirstOrDefaultAsync(o => o.Id == query.OrderId);
if (order == null)
{
return null;
}
return new OrderDto
{
Id = order.Id,
OrderDate = order.OrderDate,
Items = order.Items.Select(i => new OrderItemDto
{
ProductId = i.ProductId,
Quantity = i.Quantity,
UnitPrice = i.UnitPrice
}).ToList(),
TotalAmount = order.TotalAmount
};
}
}
Understanding Event Sourcing
Principles of Event Sourcing
Event Sourcing (ES) is a pattern where state changes are represented as a sequence of events. Instead of storing the current state, the system stores all events that have led to the current state.
Benefits of Event Sourcing
- Auditability: Complete history of changes is maintained, providing a clear audit trail.
- Consistency: Ensures that the application state is derived from a series of immutable events.
- Scalability: Enables rebuilding of the application state from events, which can be distributed and processed independently.
Implementing Event Sourcing in .NET Core
Defining Events
Events represent state changes in the application. Define events for order creation and item addition:
public class OrderCreatedEvent
{
public Guid OrderId { get; set; }
public DateTime OrderDate { get; set; }
}
public class OrderItemAddedEvent
{
public Guid OrderId { get; set; }
public Guid ProductId { get; set; }
public int Quantity { get; set; }
public decimal UnitPrice { get; set; }
}
Storing Events
Create an event store to persist events:
public class EventStore
{
private readonly List<object> _events = new List<object>();
public void SaveEvent(object @event)
{
_events.Add(@event);
}
public IEnumerable<object> GetEvents()
{
return _events;
}
}
Handling Events
Create event handlers to apply events to the domain model:
public class OrderEventHandler
{
private readonly Dictionary<Guid, Order> _orders = new Dictionary<Guid, Order>();
public void Handle(OrderCreatedEvent @event)
{
var order = new Order
{
Id = @event.OrderId,
OrderDate = @event.OrderDate,
Items = new List<OrderItem>()
};
_orders[@event.OrderId] = order;
}
public void Handle(OrderItemAddedEvent @event)
{
if (_orders.TryGetValue(@event.OrderId, out var order))
{
var item = new OrderItem
{
ProductId = @event.ProductId,
Quantity = @event.Quantity,
UnitPrice = @event.UnitPrice
};
order.Items.Add(item);
}
}
public Order GetOrder(Guid orderId)
{
return _orders.TryGetValue(orderId, out var order) ? order : null;
}
}
Integrating CQRS and Event Sourcing
Modify the command handler to emit events instead of directly modifying the state:
public class CreateOrderCommandHandler
{
private readonly EventStore _eventStore;
public CreateOrderCommandHandler(EventStore eventStore)
{
_eventStore = eventStore;
}
public async Task Handle(CreateOrderCommand command)
{
var orderCreatedEvent = new OrderCreatedEvent
{
OrderId = Guid.NewGuid(),
OrderDate = command.OrderDate
};
_eventStore.SaveEvent(orderCreatedEvent);
foreach (var item in command.Items)
{
var orderItemAddedEvent = new OrderItemAddedEvent
{
OrderId = orderCreatedEvent.OrderId,
ProductId = item.ProductId,
Quantity = item.Quantity,
UnitPrice = item.UnitPrice
};
_eventStore.SaveEvent(orderItemAddedEvent);
}
}
}
Rebuilding State from Events
Implement a mechanism to rebuild the application state from stored events:
public class OrderService
{
private readonly EventStore _eventStore;
private readonly OrderEventHandler _eventHandler;
public OrderService(EventStore eventStore, OrderEventHandler eventHandler)
{
_eventStore = eventStore;
_eventHandler = eventHandler;
}
public void RebuildState()
{
foreach (var @event in _eventStore.GetEvents())
{
switch (@event)
{
case OrderCreatedEvent e:
_eventHandler.Handle(e);
break;
case OrderItemAddedEvent e:
_eventHandler.Handle(e);
break;
}
}
}
public Order GetOrder(Guid orderId)
{
return _eventHandler.GetOrder(orderId);
}
}
For those interested in learning more about .NET development, check out our .NET Development blogs. Stay updated with the latest insights and best practices!
Conclusion
CQRS and Event Sourcing in .NET Core could enhance the scalability, maintainability and performance of your programs. Decoupling the read & write operations and saving state changes as a sequence of events offers a far more powerful and flexible architecture. The setup and learning curve for utilizing these patterns is steep in the beginning but the long term advantages are worth the effort. With all the supplied code samples and explanations you can begin implementing CQRS and Event Sourcing in your .NET Core applications and experience the advantages firsthand.
From initial concept to final deployment, WireFuture is your partner in software development. Our holistic approach ensures your project not only launches successfully but also thrives in the competitive digital ecosystem.
No commitment required. Whether you’re a charity, business, start-up or you just have an idea – we’re happy to talk through your project.
Embrace a worry-free experience as we proactively update, secure, and optimize your software, enabling you to focus on what matters most – driving innovation and achieving your business goals.