# E-Commerce Example: Queries Implement queries and projections for the e-commerce order system. ## Query Design Queries fetch data from read models (projections) optimized for querying: - ✅ `GetOrderQuery` - Fetch single order details - ✅ `ListOrdersQuery` - List orders with filtering - ✅ `GetOrderHistoryQuery` - Customer order history - ✅ `GetOrderAnalyticsQuery` - Analytics dashboard data ## GetOrderQuery ```csharp public record GetOrderQuery { public string OrderId { get; init; } = string.Empty; } public class GetOrderQueryHandler : IQueryHandler { private readonly IOrderRepository _repository; public async Task HandleAsync(GetOrderQuery query, CancellationToken ct) { var order = await _repository.GetByIdAsync(query.OrderId, ct); if (order == null) throw new NotFoundException($"Order {query.OrderId} not found"); return new OrderDto { OrderId = order.OrderId, CustomerId = order.CustomerId, CustomerName = order.CustomerName, Lines = order.Lines, TotalAmount = order.TotalAmount, Status = order.Status.ToString(), PlacedAt = order.PlacedAt, PaidAt = order.PaidAt, ShippedAt = order.ShippedAt, DeliveredAt = order.DeliveredAt }; } } public record OrderDto { public string OrderId { get; init; } = string.Empty; public string CustomerId { get; init; } = string.Empty; public string CustomerName { get; init; } = string.Empty; public List Lines { get; init; } = new(); public decimal TotalAmount { get; init; } public string Status { get; init; } = string.Empty; public DateTimeOffset PlacedAt { get; init; } public DateTimeOffset? PaidAt { get; init; } public DateTimeOffset? ShippedAt { get; init; } public DateTimeOffset? DeliveredAt { get; init; } } ``` ## ListOrdersQuery ```csharp public record ListOrdersQuery { public string? CustomerId { get; init; } public string? Status { get; init; } public DateTimeOffset? StartDate { get; init; } public DateTimeOffset? EndDate { get; init; } public int Page { get; init; } = 1; public int PageSize { get; init; } = 20; } public class ListOrdersQueryHandler : IQueryHandler> { private readonly IOrderRepository _repository; public async Task> HandleAsync(ListOrdersQuery query, CancellationToken ct) { var (orders, totalCount) = await _repository.ListAsync( customerId: query.CustomerId, status: query.Status, startDate: query.StartDate, endDate: query.EndDate, page: query.Page, pageSize: query.PageSize, ct); var dtos = orders.Select(o => new OrderSummaryDto { OrderId = o.OrderId, CustomerName = o.CustomerName, TotalAmount = o.TotalAmount, Status = o.Status.ToString(), PlacedAt = o.PlacedAt }).ToList(); return new PagedResult { Items = dtos, Page = query.Page, PageSize = query.PageSize, TotalCount = totalCount, TotalPages = (int)Math.Ceiling(totalCount / (double)query.PageSize) }; } } public record OrderSummaryDto { public string OrderId { get; init; } = string.Empty; public string CustomerName { get; init; } = string.Empty; public decimal TotalAmount { get; init; } public string Status { get; init; } = string.Empty; public DateTimeOffset PlacedAt { get; init; } } ``` ## Order Summary Projection Build an optimized read model for queries: ```csharp public class OrderSummaryProjection : IDynamicProjection, IResettableProjection { private readonly IEventStreamStore _eventStore; private readonly ICheckpointStore _checkpointStore; private readonly IOrderRepository _repository; public string ProjectionName => "order-summary"; public async Task RunAsync(CancellationToken ct) { var checkpoint = await _checkpointStore.GetCheckpointAsync(ProjectionName, ct); await foreach (var storedEvent in _eventStore.ReadStreamAsync( "orders", fromOffset: checkpoint + 1, cancellationToken: ct)) { await HandleEventAsync(storedEvent.Data, ct); await _checkpointStore.SaveCheckpointAsync(ProjectionName, storedEvent.Offset, ct); } } private async Task HandleEventAsync(object @event, CancellationToken ct) { switch (@event) { case OrderPlacedEvent e: await _repository.CreateAsync(new OrderSummary { OrderId = e.OrderId, CustomerId = e.CustomerId, CustomerName = e.CustomerName, CustomerEmail = e.CustomerEmail, Lines = e.Lines, TotalAmount = e.TotalAmount, Status = OrderStatus.Placed, PlacedAt = e.PlacedAt }, ct); break; case OrderPaidEvent e: var order = await _repository.GetByIdAsync(e.OrderId, ct); if (order != null) { order.Status = OrderStatus.Paid; order.PaidAt = e.PaidAt; await _repository.UpdateAsync(order, ct); } break; case OrderShippedEvent e: var shippedOrder = await _repository.GetByIdAsync(e.OrderId, ct); if (shippedOrder != null) { shippedOrder.Status = OrderStatus.Shipped; shippedOrder.ShippedAt = e.ShippedAt; shippedOrder.TrackingNumber = e.TrackingNumber; await _repository.UpdateAsync(shippedOrder, ct); } break; case OrderDeliveredEvent e: var deliveredOrder = await _repository.GetByIdAsync(e.OrderId, ct); if (deliveredOrder != null) { deliveredOrder.Status = OrderStatus.Delivered; deliveredOrder.DeliveredAt = e.DeliveredAt; await _repository.UpdateAsync(deliveredOrder, ct); } break; case OrderCancelledEvent e: var cancelledOrder = await _repository.GetByIdAsync(e.OrderId, ct); if (cancelledOrder != null) { cancelledOrder.Status = OrderStatus.Cancelled; cancelledOrder.CancelledAt = e.CancelledAt; await _repository.UpdateAsync(cancelledOrder, ct); } break; } } public async Task ResetAsync(CancellationToken ct) { await _repository.DeleteAllAsync(ct); await _checkpointStore.SaveCheckpointAsync(ProjectionName, 0, ct); } } ``` ## Customer Order History Projection Denormalized projection for fast customer queries: ```csharp public class CustomerOrderHistoryProjection : IDynamicProjection { public string ProjectionName => "customer-order-history"; private async Task HandleEventAsync(object @event, CancellationToken ct) { switch (@event) { case OrderPlacedEvent e: var customer = await _repository.GetCustomerAsync(e.CustomerId, ct) ?? new CustomerOrderHistory { CustomerId = e.CustomerId, Name = e.CustomerName }; customer.Orders.Add(new OrderSummary { OrderId = e.OrderId, TotalAmount = e.TotalAmount, Status = OrderStatus.Placed, PlacedAt = e.PlacedAt }); customer.TotalSpent += e.TotalAmount; customer.OrderCount++; await _repository.SaveCustomerAsync(customer, ct); break; } } } public class CustomerOrderHistory { public string CustomerId { get; set; } = string.Empty; public string Name { get; set; } = string.Empty; public List Orders { get; set; } = new(); public decimal TotalSpent { get; set; } public int OrderCount { get; set; } } ``` ## Query Registration ```csharp // In Program.cs builder.Services.AddQuery(); builder.Services.AddQuery, ListOrdersQueryHandler>(); // Register projections builder.Services.AddSingleton(); builder.Services.AddSingleton(); // Auto-start projections builder.Services.AddProjectionRunner(); ``` ## See Also - [05-projections.md](05-projections.md) - More projection examples - [07-http-api.md](07-http-api.md) - HTTP endpoints for queries - [Query Design Best Practices](../../best-practices/query-design.md)