Implementing Event Sourcing and CQRS with ASP.NET Core in Microservices

Event Sourcing and CQRS with ASP.NET Core

Crafting-Code
9 min readAug 26, 2023

Event Sourcing and Command Query Responsibility Segregation (CQRS) have emerged as powerful architectural patterns to address the complexities of microservices design.

Diagram illustrating the components and relationships of Event Sourcing and Command Query Responsibility Segregation (CQRS) in an ASP.NET Core application.
Basic CQRS Representation

In this article, we’ll explore how ASP.NET Core empowers you to integrate Event Sourcing and CQRS seamlessly into your microservices ecosystem. By understanding their fundamentals, practical implementation, and tools available, you’ll be well-equipped to architect robust and efficient microservices solutions.

Introduction to Event Sourcing

Event Sourcing, at its core, is a data storage pattern that captures every change to an application’s state as a sequence of immutable events. Unlike traditional approaches that store only the current state, Event Sourcing maintains a full history of state changes. This technique not only enables you to reconstruct the application’s past states but also provides an audit trail of how and why the system arrived at its current state.

What is CQRS?

Command Query Responsibility Segregation (CQRS) is a pattern that separates the read and write operations of a system into distinct paths. In a CQRS architecture, commands represent requests to change the system’s state, while queries fetch data for reading purposes. By segregating these concerns, CQRS allows optimization of each path independently, enabling efficient scaling, performance tuning, and enhanced user experiences.

Benefits of Event Sourcing and CQRS in Microservices

The combination of Event Sourcing and CQRS offers several advantages when applied to microservices-based applications:

  1. Historical Transparency: Event Sourcing ensures a comprehensive record of all state changes, providing historical transparency for auditing, compliance, and debugging purposes.
  2. Flexibility in Query Optimization: CQRS empowers you to optimize read and write paths differently. This means you can tailor the read path for query performance while optimizing the write path for high throughput.
  3. Scalability and Performance: Microservices architectures demand efficient scaling. Event Sourcing and CQRS allow you to scale different parts of your application independently, enhancing overall performance.
  4. Resilience and Fault Tolerance: Event Sourcing enhances resilience by allowing the reconstruction of application state after a failure. CQRS helps isolate errors and failures in the write path from affecting the read path.
  5. Support for Complex Domains: Event Sourcing allows you to capture complex domain behavior accurately by recording fine-grained events. CQRS then enables tailored views of this data for various use cases.

Getting Started with ASP.NET Core Microservices

Setting the Foundation: Creating an ASP.NET Core Microservices Solution

To implement Event Sourcing and Command Query Responsibility Segregation (CQRS) in ASP.NET Core microservices, you’ll first need to set up your development environment and create the necessary solution structure. Here’s a step-by-step guide to get you started:

1. Install .NET Core SDK: Make sure you have the latest .NET Core SDK installed on your machine. You can download it from the official Microsoft website.

2. Create a New Solution: Open your command-line interface and navigate to the directory where you want to create your solution. Use the following command to create a new ASP.NET Core solution:

dotnet new sln -n MicroservicesSolution

3. Create Microservices Projects: Inside your solution folder, create individual projects for each microservice. For example, you can create projects named OrderService, PaymentService, and NotificationService:

dotnet new webapi -n OrderService
dotnet new webapi -n PaymentService
dotnet new webapi -n NotificationService

4. Add Projects to Solution: Add the microservices projects to the solution using the following command:

dotnet sln MicroservicesSolution.sln add OrderService\OrderService.csproj
dotnet sln MicroservicesSolution.sln add PaymentService\PaymentService.csproj
dotnet sln MicroservicesSolution.sln add NotificationService\NotificationService.csproj

Defining Domain Events and Commands

With your solution structure in place, you’re ready to define the core building blocks of Event Sourcing and CQRS: domain events and commands.

  • Create a Shared Library: To avoid duplicating domain-related code, create a shared library that contains common classes like domain events, commands, and value objects. Use the following command to create a class library project:
dotnet new classlib -n SharedDomain
  • Add the Shared Library to Solution: Add the shared library project to the solution:
dotnet sln MicroservicesSolution.sln add SharedDomain\SharedDomain.csproj
  • Define Domain Events and Commands: Inside the SharedDomain project, define your domain events and commands as C# classes. For example:
public class OrderPlacedEvent
{
public Guid OrderId { get; set; }
public DateTime Timestamp { get; set; }
}

public class ProcessPaymentCommand
{
public Guid OrderId { get; set; }
public decimal Amount { get; set; }
}
  • Reference the Shared Library: In each microservice project, add a reference to the SharedDomain library:
dotnet add OrderService\OrderService.csproj reference SharedDomain\SharedDomain.csproj
dotnet add PaymentService\PaymentService.csproj reference SharedDomain\SharedDomain.csproj
dotnet add NotificationService\NotificationService.csproj reference SharedDomain\SharedDomain.csproj

Implementing Event Sourcing: Designing Aggregates and Events

  1. Define Aggregates: Start by identifying aggregates within your microservices. Aggregates represent a consistency boundary for domain entities. For instance, in an e-commerce application, an “Order” can be an aggregate that encapsulates line items, customer details, and other related information.
  2. Create Domain Events: Define domain events as plain C# classes. Each event should encapsulate a specific change in the state of an aggregate. For example:
public class OrderPlacedEvent
{
public Guid OrderId { get; set; }
public DateTime Timestamp { get; set; }
}

Creating Event Stores and Repositories

  1. Set Up Event Store: Create an event store to persist domain events. This store is responsible for storing events related to each aggregate. You can use a relational database, a NoSQL database, or specialized event store solutions.
  2. Implement Repositories: Repositories provide a way to retrieve aggregates from the event store and manage their lifecycle. A repository loads events from the event store and reconstructs aggregates by applying events in the correct order.
public class OrderRepository
{
private readonly IEventStore _eventStore;

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

public Order GetOrder(Guid orderId)
{
var events = _eventStore.GetEventsForAggregate(orderId);
var order = new Order(orderId);
order.Apply(events);
return order;
}
}

Publishing and Handling Domain Events

  1. Publish Domain Events: When an aggregate undergoes a state change, it emits one or more domain events. These events need to be published to notify other parts of the system about the changes. Implement a mechanism to publish domain events to event buses or queues.
  2. Handle Domain Events: Event handlers subscribe to specific domain events and take actions accordingly. For instance, an “OrderPlacedEventHandler” might send a notification email to the customer when an order is placed.
using System;
using YourProjectName.Events; // Import the namespace where your events are defined

namespace YourProjectName.EventHandlers
{
public class OrderPlacedEventHandler : IEventHandler<OrderPlacedEvent>
{
public void Handle(OrderPlacedEvent @event)
{
// Send notification email to the customer
SendEmailToCustomer(@event.OrderId);
}

private void SendEmailToCustomer(Guid orderId)
{
// Implement the logic to send an email to the customer
// You can use email libraries like SendGrid, SMTP, etc.
Console.WriteLine($"Notification email sent for order {orderId}");
}
}
}

By implementing Event Sourcing, you’re capturing the history of changes in your application’s domain, enabling you to reconstruct past states and providing a robust audit trail.

Building Command Handlers and Command Models

  • Define Command Models: Command models represent the intention to change the state of the application. They encapsulate the necessary data for an operation. For instance, an “PlaceOrderCommand” can encapsulate the details of an order placement.
public class PlaceOrderCommand
{
public Guid OrderId { get; set; }
// Other relevant properties...
}
  • Implement Command Handlers: Command handlers are responsible for processing and executing commands. They validate the command, perform necessary actions, and raise domain events if needed.
public class PlaceOrderCommandHandler : ICommandHandler<PlaceOrderCommand>
{
public void Handle(PlaceOrderCommand command)
{
// Validate the command, perform actions, and raise events
var order = new Order(command.OrderId);
// Perform actions like adding line items, calculating totals, etc.
// Raise domain events like OrderPlacedEvent
}
}

Creating Query Models and Query Handlers

  • Define Query Models: Query models represent the data structure used for fetching information from the read side of your application. They are tailored for efficient querying and retrieval.
public class OrderQueryModel
{
public Guid OrderId { get; set; }
// Other relevant properties...
}
  • Implement Query Handlers: Query handlers retrieve data from the read store based on user queries. They fetch data from query models and return the requested information.
public class OrderQueryHandler : IQueryHandler<OrderQueryModel>
{
public OrderQueryModel Handle()
{
// Fetch data from the read store and return the query model
var orderData = ReadStore.GetOrderData();
var orderQueryModel = MapToOrderQueryModel(orderData);
return orderQueryModel;
}
}

Separating Write and Read Concerns

One of the key tenets of CQRS is separating the write and read concerns of your application. This separation allows you to optimize the architecture for performance, scalability, and maintainability.

By embracing CQRS, you’re structuring your microservices to handle commands and queries separately, optimizing each part of your application for its specific requirements.

Choosing the Right Database for Event Sourcing and CQRS

  • Selecting a Database: When choosing a database, consider the characteristics of your application’s data. For event sourcing, you might opt for databases that excel at handling write-heavy workloads, such as NoSQL databases like MongoDB or event store databases like EventStoreDB.
// Example of using MongoDB as the event store
services.AddEventStore(options =>
{
options.UseMongoDb(Configuration.GetConnectionString("EventStore"));
});

Event Store and Read Models Storage Strategies

  • Storing Event Store Data: The event store is the heart of your event sourcing architecture. It stores domain events that represent changes to the application’s state. Implement an event store, whether using a dedicated database or a specialized solution.
using System;
using System.Collections.Generic;
using System.Threading.Tasks;

public interface IEventStoreRepository
{
Task<IEnumerable<DomainEvent>> GetEventsAsync(Guid aggregateId);
Task SaveEventsAsync(Guid aggregateId, IEnumerable<DomainEvent> events);
}

public class EventStoreRepository : IEventStoreRepository
{
private readonly IEventStoreDbContext _dbContext;

public EventStoreRepository(IEventStoreDbContext dbContext)
{
_dbContext = dbContext;
}

public async Task<IEnumerable<DomainEvent>> GetEventsAsync(Guid aggregateId)
{
// Retrieve events for the specified aggregate from the event store
var events = await _dbContext.Events
.Where(e => e.AggregateId == aggregateId)
.OrderBy(e => e.SequenceNumber)
.ToListAsync();

return events.Select(e => e.DomainEvent);
}

public async Task SaveEventsAsync(Guid aggregateId, IEnumerable<DomainEvent> events)
{
// Store events for the specified aggregate in the event store
var sequenceNumber = await _dbContext.Events
.Where(e => e.AggregateId == aggregateId)
.MaxAsync(e => (int?)e.SequenceNumber) ?? 0;

var eventEntities = events.Select(domainEvent => new EventEntity
{
AggregateId = aggregateId,
SequenceNumber = ++sequenceNumber,
DomainEvent = domainEvent
});

await _dbContext.Events.AddRangeAsync(eventEntities);
await _dbContext.SaveChangesAsync();
}
}

In this example, the EventStoreRepository class implements the IEventStoreRepository interface with methods to retrieve and store events. It interacts with an IEventStoreDbContext that represents the database context for the event store.

  • Storing Read Models: Read models contain the data tailored for queries. Choose a suitable database for storing read models based on the querying patterns. For example, use SQL databases like SQL Server or PostgreSQL for relational data, and NoSQL databases like MongoDB for flexible and schema-less data.
// Example of defining a read model using Entity Framework Core
public class OrderReadModel
{
public Guid OrderId { get; set; }
// Other relevant properties...
}

By using the repository pattern, you can encapsulate the interactions with the event store, making it easier to manage and maintain your Event Sourcing and CQRS architecture.

Remember that this is a simplified example, and in a real-world scenario, you might incorporate error handling, concurrency control, and other considerations.

Tools and Libraries for ASP.NET Core Event Sourcing and CQRS

Event Store Providers

When implementing Event Sourcing and CQRS in your ASP.NET Core microservices, you can take advantage of various event store providers that simplify the management of events and aggregates. These providers offer features such as storage, retrieval, and querying of events. Let’s explore a few popular event store providers:

1. EventStoreDB: EventStoreDB is a robust and open-source event store designed for handling high volumes of events efficiently. It provides features like stream-based storage, event projections, and built-in support for Event Sourcing and CQRS.

// Example of connecting to an EventStoreDB instance
var settings = new EventStoreClientSettings { ConnectionString = "esdb://localhost:21XX" };
var eventStoreClient = new EventStoreClient(settings);

2. NEventStore: NEventStore is another well-known event store library that supports various storage backends and provides integration with ASP.NET Core.

// Example of configuring NEventStore
var store = Wireup.Init()
.UsingSqlPersistence("ConnectionStringName")
.WithDialect(new MsSqlDialect())
.InitializeStorageEngine()
.UsingJsonSerialization()
.Compress()
.Build();

CQRS Frameworks and Libraries

To streamline the implementation of the Command Query Responsibility Segregation (CQRS) pattern, several frameworks and libraries can assist you in building the necessary components:

1. MediatR: MediatR is a popular open-source library that simplifies the implementation of the mediator pattern. It enables loose coupling between command handlers, query handlers, and domain logic.

// Example of using MediatR for command handling
public class CreateOrderCommandHandler : IRequestHandler<CreateOrderCommand, Guid>
{
public async Task<Guid> Handle(CreateOrderCommand request, CancellationToken cancellationToken)
{
// Create order logic
return orderId;
}
}

2. SimpleInjector: SimpleInjector is an efficient dependency injection container that can be utilized to manage the lifetime of your CQRS command handlers and query handlers within your ASP.NET Core application.

// Example of configuring SimpleInjector for dependency injection
services.UseSimpleInjector(container, options =>
{
options.AddAspNetCore()
.AddControllerActivation();
});

Conclusion:

By now, you’ve explored the intricacies of Event Sourcing and CQRS and their pivotal role in crafting robust and scalable microservices with ASP.NET Core.

As you work on implementing these patterns, remember to consider the unique requirements of your project, select suitable tools, and continuously refine your practices. Your journey to mastering Event Sourcing and CQRS has just begun, and the road ahead promises a more resilient and responsive microservices architecture.

If you found this article beneficial and wish to contribute to the growth of this platform, there’s a simple yet meaningful way you can show Your Support.

Also Feel free to reach out to me at toshiah213@gmail.com if you’re interested in collaborating, sponsoring, or discussing business opportunities. I’m always open to exciting ventures and partnerships. Looking forward to hearing from you!

--

--