CQRS: commands change state through the aggregate.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 | // ====================================================================== // Q: Explain Mediator / CQRS and Observer (domain events). // // CQRS: commands change state through the aggregate. Queries read directly with // AsNoTracking() and projection. You don't need two databases: separation in the // code is enough. Mediator: the controller doesn't know who executes the // command, it just sends it. // Observer (domain events): many handlers react to the same event, without the // Order knowing about them. // - Events are dispatched in SaveChangesAsync (e.g. with an EF Core // SaveChangesInterceptor). // - Anything that leaves the process (ERP, another service) goes through the // outbox (Q10), not in-memory, so it isn't lost. // - MediatR became commercial (commercial license) in 2025. Alternatives: your // own simple dispatcher as above, Wolverine, or a source-generated mediator. // ====================================================================== // CQRS command, handler and thin controller public record PlaceOrder(int OrderId); public interface ICommandHandler<in TCommand> { Task Handle(TCommand command, CancellationToken ct); } public class PlaceOrderHandler(IOrderRepository orders, IUnitOfWork uow) : ICommandHandler<PlaceOrder> { public async Task Handle(PlaceOrder cmd, CancellationToken ct) { var order = await orders.GetAsync(cmd.OrderId, ct) ?? throw new NotFoundException(); order.Place(); // the business logic lives in the aggregate await uow.SaveChangesAsync(ct); // domain events are dispatched here } } // Controller: thin [HttpPost("{id:int}/place")] public async Task<IActionResult> Place(int id, ICommandHandler<PlaceOrder> handler, CancellationToken ct) { await handler.Handle(new PlaceOrder(id), ct); return NoContent(); } // Observer: multiple domain event handlers for OrderPlaced public interface IDomainEventHandler<in TEvent> where TEvent : IDomainEvent { Task Handle(TEvent e, CancellationToken ct); } public class SendOrderConfirmation(IEmailSender email) : IDomainEventHandler<OrderPlaced> { /* ... */ } public class WriteErpOutboxMessage(AppDbContext db) : IDomainEventHandler<OrderPlaced> { /* ... */ } builder.Services.AddScoped<IDomainEventHandler<OrderPlaced>, SendOrderConfirmation>(); builder.Services.AddScoped<IDomainEventHandler<OrderPlaced>, WriteErpOutboxMessage>(); |