CQRS and Event Sourcing in C# .NET (Part 1)

Implementing the Command Query Responsibility Segregation (CQRS) pattern in real-life scenarios.

CQRS and Event Sourcing in C# .NET (Part 1)

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.

  1. Create a new .NET web API project

  2. 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">
    
  3. Add the following files and folder structure to the just-created project

  4. 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 and IWriteOrdersRepository, 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 and AddQueryHandlers 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.

  1. Run the application from the terminal or by pressing F5 in your IDE.

    You should see this swagger page visible.

  2. Initiate a command by calling the POST request

    Below is our result.

  3. 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.