CQRS and Event Sourcing in C# .NET (Part 1)
Implementing the Command Query Responsibility Segregation (CQRS) pattern in real-life scenarios.
Introduction
Command Query Responsibility Segregation (CQRS) and Event Sourcing are two architectural patterns often used in designing complex software systems, particularly in distributed and event-driven systems.
In this article, we are exploring the CQRS pattern. The topic of event sourcing will be in the following article. CQRS can be used without event-sourcing, but the event-sourcing pattern requires using CQRS.
CQRS (Command Query Responsibility Segregation)
CQRS is an architectural pattern that separates the responsibilities for reading data (queries) from those for writing data (commands) in a software application. In a CQRS-based system, these responsibilities are segregated into two distinct parts:
Command Side: This side handles operations that modify the system's state. It processes commands sent by clients or external systems, performs validation, and updates the data store accordingly.
Query Side: This side is responsible for reading data from the system. It serves queries from clients or external systems by fetching data from the appropriate data stores.
Key benefits of CQRS include:
Scalability: You can independently scale your system's read and write components based on their respective workloads.
Flexibility: Different data stores and optimization techniques can be used for reads and writes, allowing you to choose the best tool for each job.
Event-Driven: CQRS often works well with event-driven architectures, as changes to the system's state are often captured as events.
Implementation
Let's implement this practically in C#.
We would be building the Ordering API of an e-shop platform for our implementation.
Create a new .NET web API project
Install necessary libraries.
Add the following lines of code to your *.csproj file. (You can also install the respective packages via the Nuggets package manager.)
<PackageReference Include="Ardalis.GuardClauses" Version="4.1.1" /> <PackageReference Include="AutoMapper.Extensions.Microsoft.DependencyInjection" Version="12.0.1" /> <PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="7.0.5" /> <PackageReference Include="Microsoft.EntityFrameworkCore" Version="7.0.11" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="7.0.11" /> <PackageReference Include="Microsoft.EntityFrameworkCore.Tools" Version="7.0.11">
Add the following files and folder structure to the just-created project
Add the following lines of code to the respective files.
Create our Order entity in
Order.cs
namespace Orders.Api.Entities; public class Order { public Guid Id { get; set; } public decimal Amount { get; set; } public string Currency { get; set; } public string CustomerEmail { get; set; } }
Create our commands marker interface in
ICommand.cs
, and our commands would implement this interface.namespace Orders.Api.Commands; public interface ICommand { }
using Orders.Api.Entities; namespace Orders.Api.Commands; public class CreateOrderCommand : ICommand { public Order Order { get; } public CreateOrderCommand(Order order) { Order = order; } }
using Orders.Api.Entities; namespace Orders.Api.Commands; public class UpdateOrderCommand : ICommand { public Order Order { get; } public UpdateOrderCommand(Order order) { Order = order; } }
Create our command handler abstraction in
ICommandHandler.cs
and implement our command handlers from this interface.namespace Orders.Api.Commands.CommandHandlers; public interface ICommandHandler<in TCommand> where TCommand: ICommand { Task HandleAsync(TCommand command); }
using Ardalis.GuardClauses; using Orders.Api.Repositories; namespace Orders.Api.Commands.CommandHandlers; public class CreateOrderCommandHandler : ICommandHandler<CreateOrderCommand> { private readonly IWriteOrdersRepository _writeOrdersRepository; public CreateOrderCommandHandler(IWriteOrdersRepository writeOrdersRepository) { _writeOrdersRepository = writeOrdersRepository; } public async Task HandleAsync(CreateOrderCommand command) { Guard.Against.Null(command); await _writeOrdersRepository.CreateOrderAsync(command.Order); } }
using Ardalis.GuardClauses; using Orders.Api.Repositories; namespace Orders.Api.Commands.CommandHandlers; public class UpdateOrderCommandHandler : ICommandHandler<UpdateOrderCommand> { private readonly IWriteOrdersRepository _writeOrdersRepository; public UpdateOrderCommandHandler(IWriteOrdersRepository writeOrdersRepository) { _writeOrdersRepository = writeOrdersRepository; } public async Task HandleAsync(UpdateOrderCommand command) { Guard.Against.Null(command?.Order); await _writeOrdersRepository.UpdateOrderAsync(command.Order); } }
Create our query marker interface in
IQuery.cs
, and our queries would implement this interface.namespace Orders.Api.Queries; public interface IQuery { }
namespace Orders.Api.Queries; public class GetOrderByIdQuery:IQuery { public Guid OrderId { get; } public GetOrderByIdQuery(Guid orderId) { OrderId = orderId; } }
namespace Orders.Api.Queries; public class GetOrderQuery : IQuery { }
Create our query handler abstraction in
IQueryHandler.cs
and implement our query handlers from this interface.using Orders.Api.Entities; namespace Orders.Api.Queries.QueryHandlers; public interface IQueryHandler<in TQuery, TResponse> where TQuery : IQuery { Task<TResponse> HandleAsync(TQuery input); }
using Ardalis.GuardClauses; using Orders.Api.Entities; using Orders.Api.Repositories; namespace Orders.Api.Queries.QueryHandlers; public class GetOrdersByIdQueryHandler : IQueryHandler<GetOrderByIdQuery, Order?> { private readonly IReadOrdersRepository _readOrdersRepository; public GetOrdersByIdQueryHandler(IReadOrdersRepository readOrdersRepository) { _readOrdersRepository = readOrdersRepository; } public async Task<Order?> HandleAsync(GetOrderByIdQuery input) { Guard.Against.Default(input?.OrderId); return await _readOrdersRepository.GetOrderByIdAsync(input.OrderId); } }
using Ardalis.GuardClauses; using Orders.Api.Entities; using Orders.Api.Repositories; namespace Orders.Api.Queries.QueryHandlers; public class GetOrdersQueryHandler : IQueryHandler<GetOrderQuery, IEnumerable<Order>> { private readonly IReadOrdersRepository _readOrdersRepository; public GetOrdersQueryHandler(IReadOrdersRepository readOrdersRepository) { _readOrdersRepository = readOrdersRepository; } public async Task<IEnumerable<Order>> HandleAsync(GetOrderQuery input) { Guard.Against.Null(input); return await _readOrdersRepository.GetOrdersAsync(); } }
Create our repositories. We would be segregating it according to read and write operations:
IReadOrdersRepository.cs
andIWriteOrdersRepository
, and respective implementations.using Orders.Api.Entities; namespace Orders.Api.Repositories; public interface IReadOrdersRepository { Task<IEnumerable<Order>> GetOrdersAsync(); Task<Order?> GetOrderByIdAsync(Guid orderId); }
using Orders.Api.Entities; namespace Orders.Api.Repositories; public interface IWriteOrdersRepository { Task<Order> CreateOrderAsync(Order entity); Task<Order> UpdateOrderAsync(Order entity); Task<bool> DeleteOrderByIdAsync(Order entity); }
Create our Data-transfer objects (Dtos).
namespace Orders.Api.Dtos; public class OrderForCreateDto { public decimal Amount { get; set; } public string Currency { get; set; } public string CustomerEmail { get; set; } }
namespace Orders.Api.Dtos; public class OrderForReturnDto { public Guid Id { get; set; } public decimal Amount { get; set; } public string Currency { get; set; } public string CustomerEmail { get; set; } }
namespace Orders.Api.Dtos; public class OrderForUpdateDto { public decimal Amount { get; set; } public string Currency { get; set; } public string CustomerEmail { get; set; } }
Create our auto-mapper profile to map from our DTOs to entities in
MappingProfile.cs
.using AutoMapper; using Orders.Api.Dtos; using Orders.Api.Entities; namespace Orders.Api; public class MappingProfile : Profile { public MappingProfile() { CreateMap<Order, OrderForCreateDto>().ReverseMap(); CreateMap<Order, OrderForUpdateDto>().ReverseMap(); CreateMap<Order, OrderForReturnDto>().ReverseMap(); } }
Create our Database context in
AppDbContext.cs
using Microsoft.EntityFrameworkCore; using Orders.Api.Entities; namespace Orders.Api.Data; public class AppDbContext : DbContext { public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { } public DbSet<Order> Orders { get; set; } }
Finally, we set up our dependency injection pipeline and created API endpoints using asp.net minimal apis.
using AutoMapper; using Microsoft.AspNetCore.Http.HttpResults; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using Orders.Api; using Orders.Api.Commands; using Orders.Api.Commands.CommandHandlers; using Orders.Api.Data; using Orders.Api.Dtos; using Orders.Api.Entities; using Orders.Api.Queries; using Orders.Api.Queries.QueryHandlers; using Orders.Api.Repositories; var builder = WebApplication.CreateBuilder(args); builder.Services.AddAutoMapper(typeof(Program)); builder.Services.AddControllers(); builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); builder.Services.AddScoped<IReadOrdersRepository, ReadOrdersRepository>(); builder.Services.AddScoped<IWriteOrdersRepository, WriteOrdersRepository>(); builder.Services.AddCommandHandlers(typeof(Program)); builder.Services.AddQueryHandlers(typeof(Program)); builder.Services.AddDbContext<AppDbContext>(options => { options.UseSqlite(builder.Configuration.GetConnectionString("DefaultConnection")); }); var app = builder.Build(); using var serviceScope = app.Services.CreateScope(); var dbContext = serviceScope.ServiceProvider.GetRequiredService<AppDbContext>(); await dbContext.Database.MigrateAsync(); if (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); } app.UseHttpsRedirection(); app.MapGet("/orders/{orderId:guid}", async (Guid orderId, [FromServices] IQueryHandler<GetOrderByIdQuery, Order> queryHandler) => { var result = await queryHandler.HandleAsync(new GetOrderByIdQuery(orderId)); return result is not { } ? Results.NotFound() : Results.Ok(result); }); app.MapGet("/orders", async ([FromServices] IQueryHandler<GetOrderQuery, Order> queryHandler) => { var result = await queryHandler.HandleAsync(new GetOrderQuery()); return result is not { } ? Results.NotFound() : Results.Ok(result); }); app.MapPut("/orders/{orderId:guid}", async (Guid orderId, [FromBody] OrderForUpdateDto orderForUpdateDto, [FromServices] IMapper mapper, [FromServices] ICommandHandler<UpdateOrderCommand> commandHandler) => { var order = mapper.Map<Order>(orderForUpdateDto); order.Id = orderId; await commandHandler.HandleAsync(new UpdateOrderCommand(order)); return Results.Ok(); }).WithName("GetOrderById"); app.MapPost("/orders", async ([FromBody] OrderForCreateDto orderForCreateDto, [FromServices] IMapper mapper, [FromServices] ICommandHandler<CreateOrderCommand> commandHandler) => { var order = mapper.Map<Order>(orderForCreateDto); order.Id = Guid.NewGuid(); await commandHandler.HandleAsync(new CreateOrderCommand(order)); return Results.CreatedAtRoute("GetOrderById", new { orderId = order.Id }); }); app.Run();
The method
AddCommandHandlers
andAddQueryHandlers
are extension methods to simplify registering our command and query handlers.using System.Reflection; using Orders.Api.Commands; using Orders.Api.Commands.CommandHandlers; using Orders.Api.Queries.QueryHandlers; namespace Orders.Api; public static class Extensions { public static IServiceCollection AddCommandHandlers(this IServiceCollection collection, Type assemblyType) { if (assemblyType == null) throw new ArgumentNullException(nameof(assemblyType)); var assembly = assemblyType.Assembly; var scanType = typeof(ICommandHandler<>); RegisterScanTypeWithImplementations(collection, assembly, scanType); return collection; } public static IServiceCollection AddQueryHandlers(this IServiceCollection collection, Type assemblyType) { if (assemblyType == null) throw new ArgumentNullException(nameof(assemblyType)); var assembly = assemblyType.Assembly; var scanType = typeof(IQueryHandler<,>); RegisterScanTypeWithImplementations(collection, assembly, scanType); return collection; } private static void RegisterScanTypeWithImplementations(IServiceCollection collection, Assembly assembly, Type scanType) { var commandHandlers = ScanTypes(assembly, scanType); foreach (var handler in commandHandlers) { var abstraction = handler.GetTypeInfo().ImplementedInterfaces .First(type => type.IsGenericType && type.GetGenericTypeDefinition() == scanType); collection.AddScoped(abstraction, handler); } } private static IEnumerable<Type> ScanTypes(Assembly assembly, Type typeToScanFor) { return assembly.GetTypes() .Where(type => type is { IsClass: true, IsAbstract: false } && type.GetInterfaces() .Any(i=>i.IsGenericType && i.GetGenericTypeDefinition() == typeToScanFor)); } }
Now, let's set up our database. Navigate to the
Orders.Api
project and run the following entity framework commands to create and update our database with the latest migrations.cd Orders.Api
dotnet ef migrations add InitialCommit
dotnet ef database update
Testing our application.
Run the application from the terminal or by pressing F5 in your IDE.
You should see this swagger page visible.
Initiate a command by calling the POST request
Below is our result.
Initiate a query by calling any of the GET methods
Conclusion
This article explored the concept of Command Query Responsibility Segregation (CQRS) and its application in contemporary software development. The tutorial's corresponding source code can be accessed through this link.