Cuando el CRUD normal deja de funcionar: Introducción a CQRS y Event Sourcing
\n\nImagine que está escribiendo un sistema para un banco. Cada segundo, cientos de usuarios verifican saldos, transfieren dinero, abren depósitos. Su aplicación en C# funciona con Entity Framework estándar y operaciones CRUD típicas. Y de repente — la base de datos falla. ¿Por qué? Porque un informe pesado bloqueó la tabla mientras se escribía una transacción. ¿Le suena familiar?
Los desarrolladores de sistemas complejos en C# tarde o temprano chocan con el techo de la arquitectura clásica. Dos potentes patrones vienen al rescate: CQRS (Command Query Responsibility Segregation) y Event Sourcing. Analicémoslos en la práctica — con ejemplos vivos, no teoría seca.
\n\n¿Qué es CQRS y por qué no es solo "separación"?
\n\nCQRS no se trata de poner métodos en carpetas diferentes. Es una filosofía: los comandos (cambian el estado) y las consultas (leen el estado) deben vivir separados. En el CRUD clásico, tiene un solo modelo de datos tanto para lectura como para escritura. En CQRS — al menos dos.
El problema del enfoque clásico
\nSupongamos que tiene un UserRepository con un método GetUserById. Cuando lee un usuario para mostrarlo en pantalla, necesita 3 campos. Cuando lo actualiza — 10 campos y validaciones. Como resultado, un modelo se convierte en un monstruo con campos null y mapeadores confusos.
En CQRS se separan las responsabilidades:
\n- \n
- Command — ejecuta una acción (por ejemplo,
CreateOrderCommand) \n - Query — devuelve datos (por ejemplo,
GetOrderQuery) \n - Read Model — DTO plano, optimizado para una pantalla específica \n
- Write Model — modelo de dominio rico con lógica de negocio \n
Escribiendo CQRS en C# con MediatR
\nLa forma más popular de implementar CQRS en C# es usar la biblioteca MediatR. Implementa el patrón Mediator y se adapta perfectamente al concepto de comandos y consultas.
Paso 1: Definir un comando
\npublic record CreateOrderCommand(\n Guid CustomerId,\n List<OrderItemDto> Items,\n string ShippingAddress\n) : IRequest<Guid>; // Devuelve el ID del pedido creado\n\nPaso 2: Manejador del comando
\npublic class CreateOrderCommandHandler : IRequestHandler<CreateOrderCommand, Guid>\n{\n private readonly IOrderRepository _orderRepository;\n private readonly IUnitOfWork _unitOfWork;\n\n public CreateOrderCommandHandler(IOrderRepository orderRepository, IUnitOfWork unitOfWork)\n {\n _orderRepository = orderRepository;\n _unitOfWork = unitOfWork;\n }\n\n public async Task<Guid> Handle(CreateOrderCommand request, CancellationToken cancellationToken)\n {\n // Lógica de negocio: verificación, validación, creación\n var order = new Order(request.CustomerId, request.ShippingAddress);\n \n foreach (var item in request.Items)\n {\n order.AddItem(item.ProductId, item.Quantity, item.Price);\n }\n\n await _orderRepository.AddAsync(order);\n await _unitOfWork.SaveChangesAsync(cancellationToken);\n \n return order.Id;\n }\n}\n\nPaso 3: Definir una consulta
\npublic record GetOrderSummaryQuery(Guid OrderId) : IRequest<OrderSummaryDto>;\n\nPaso 4: Manejador de la consulta (¡lee desde una BD separada!)
\npublic class GetOrderSummaryQueryHandler : IRequestHandler<GetOrderSummaryQuery, OrderSummaryDto>\n{\n private readonly IReadDbContext _readDbContext; // Contexto separado para lectura\n\n public GetOrderSummaryQueryHandler(IReadDbContext read