# E-Commerce Example: Commands Implement commands and handlers for the e-commerce order system. ## Command Design Commands represent user intentions and are named using imperative verbs: - ✅ `PlaceOrderCommand` - Place a new order - ✅ `ProcessPaymentCommand` - Process payment for order - ✅ `ShipOrderCommand` - Ship an order - ✅ `CancelOrderCommand` - Cancel an order ## PlaceOrderCommand ```csharp public record PlaceOrderCommand { public string CustomerId { get; init; } = string.Empty; public List Lines { get; init; } = new(); public string ShippingAddress { get; init; } = string.Empty; } public class PlaceOrderCommandValidator : AbstractValidator { public PlaceOrderCommandValidator() { RuleFor(x => x.CustomerId).NotEmpty(); RuleFor(x => x.Lines).NotEmpty().WithMessage("Order must have at least one item"); RuleFor(x => x.ShippingAddress).NotEmpty(); RuleForEach(x => x.Lines).ChildRules(line => { line.RuleFor(l => l.ProductId).NotEmpty(); line.RuleFor(l => l.Quantity).GreaterThan(0); }); } } public class PlaceOrderCommandHandler : ICommandHandler { private readonly IEventStreamStore _eventStore; private readonly IProductRepository _products; private readonly ICustomerRepository _customers; public async Task HandleAsync(PlaceOrderCommand command, CancellationToken ct) { // 1. Validate customer exists var customer = await _customers.GetByIdAsync(command.CustomerId, ct); if (customer == null) throw new InvalidOperationException("Customer not found"); // 2. Load products and calculate total var orderLines = new List(); decimal totalAmount = 0; foreach (var line in command.Lines) { var product = await _products.GetByIdAsync(line.ProductId, ct); if (product == null) throw new InvalidOperationException($"Product {line.ProductId} not found"); if (product.AvailableStock < line.Quantity) throw new InvalidOperationException($"Insufficient stock for {product.Name}"); var orderLine = new OrderLineDto { ProductId = product.ProductId, ProductName = product.Name, Quantity = line.Quantity, UnitPrice = product.Price, LineTotal = line.Quantity * product.Price }; orderLines.Add(orderLine); totalAmount += orderLine.LineTotal; } // 3. Create order and emit event var orderId = Guid.NewGuid().ToString(); var streamName = $"order-{orderId}"; await _eventStore.AppendAsync(streamName, new OrderPlacedEvent { OrderId = orderId, CustomerId = customer.CustomerId, CustomerName = customer.Name, CustomerEmail = customer.Email, Lines = orderLines, TotalAmount = totalAmount, PlacedAt = DateTimeOffset.UtcNow }, ct); return orderId; } } ``` ## ProcessPaymentCommand ```csharp public record ProcessPaymentCommand { public string OrderId { get; init; } = string.Empty; public string PaymentMethod { get; init; } = string.Empty; public string PaymentToken { get; init; } = string.Empty; } public class ProcessPaymentCommandHandler : ICommandHandler { private readonly IAggregateRepository _repository; private readonly IPaymentService _paymentService; private readonly IEventStreamStore _eventStore; public async Task HandleAsync(ProcessPaymentCommand command, CancellationToken ct) { // 1. Load order aggregate var order = await _repository.LoadAsync(command.OrderId, ct); if (order.Status != OrderStatus.Placed) throw new InvalidOperationException("Order must be in 'Placed' status to process payment"); // 2. Process payment var paymentResult = await _paymentService.ChargeAsync( command.OrderId, order.TotalAmount, command.PaymentMethod, command.PaymentToken, ct); if (!paymentResult.Success) { await _eventStore.AppendAsync($"order-{command.OrderId}", new PaymentFailedEvent { OrderId = command.OrderId, Reason = paymentResult.ErrorMessage, FailedAt = DateTimeOffset.UtcNow }, ct); throw new InvalidOperationException($"Payment failed: {paymentResult.ErrorMessage}"); } // 3. Emit payment events await _eventStore.AppendAsync($"order-{command.OrderId}", new PaymentProcessedEvent { PaymentId = paymentResult.PaymentId, OrderId = command.OrderId, Amount = order.TotalAmount, ProcessedAt = DateTimeOffset.UtcNow }, ct); await _eventStore.AppendAsync($"order-{command.OrderId}", new OrderPaidEvent { OrderId = command.OrderId, PaymentId = paymentResult.PaymentId, PaymentMethod = command.PaymentMethod, Amount = order.TotalAmount, PaidAt = DateTimeOffset.UtcNow }, ct); } } ``` ## ShipOrderCommand ```csharp public record ShipOrderCommand { public string OrderId { get; init; } = string.Empty; public string Carrier { get; init; } = string.Empty; } public class ShipOrderCommandHandler : ICommandHandler { private readonly IAggregateRepository _repository; private readonly IShippingService _shippingService; private readonly IEventStreamStore _eventStore; public async Task HandleAsync(ShipOrderCommand command, CancellationToken ct) { var order = await _repository.LoadAsync(command.OrderId, ct); if (order.Status != OrderStatus.Paid) throw new InvalidOperationException("Order must be paid before shipping"); // Create shipment var shipment = await _shippingService.CreateShipmentAsync( command.OrderId, order.Lines, command.Carrier, ct); // Emit event await _eventStore.AppendAsync($"order-{command.OrderId}", new OrderShippedEvent { OrderId = command.OrderId, ShipmentId = shipment.ShipmentId, TrackingNumber = shipment.TrackingNumber, Carrier = command.Carrier, EstimatedDelivery = shipment.EstimatedDelivery, ShippedAt = DateTimeOffset.UtcNow }, ct); } } ``` ## CancelOrderCommand ```csharp public record CancelOrderCommand { public string OrderId { get; init; } = string.Empty; public string Reason { get; init; } = string.Empty; } public class CancelOrderCommandHandler : ICommandHandler { private readonly IAggregateRepository _repository; private readonly IEventStreamStore _eventStore; public async Task HandleAsync(CancelOrderCommand command, CancellationToken ct) { var order = await _repository.LoadAsync(command.OrderId, ct); if (order.Status == OrderStatus.Shipped || order.Status == OrderStatus.Delivered) throw new InvalidOperationException("Cannot cancel shipped or delivered orders"); if (order.Status == OrderStatus.Cancelled) throw new InvalidOperationException("Order is already cancelled"); // Emit cancellation event await _eventStore.AppendAsync($"order-{command.OrderId}", new OrderCancelledEvent { OrderId = command.OrderId, CancelledBy = "Customer", // Or get from auth context Reason = command.Reason, RefundIssued = order.Status == OrderStatus.Paid, CancelledAt = DateTimeOffset.UtcNow }, ct); } } ``` ## Command Registration ```csharp // In Program.cs builder.Services.AddCommand(); builder.Services.AddCommand(); builder.Services.AddCommand(); builder.Services.AddCommand(); // Register validators builder.Services.AddTransient, PlaceOrderCommandValidator>(); ``` ## See Also - [04-queries.md](04-queries.md) - Query the order data - [06-sagas.md](06-sagas.md) - Order fulfillment workflow - [Command Design Best Practices](../../best-practices/command-design.md)