# E-Commerce Example: Projections Build analytics and reporting projections for the e-commerce system. ## Analytics Projection Create real-time analytics from order events: ```csharp public class OrderAnalyticsProjection : IDynamicProjection, IResettableProjection { private readonly IEventStreamStore _eventStore; private readonly ICheckpointStore _checkpointStore; private readonly IAnalyticsRepository _repository; public string ProjectionName => "order-analytics"; 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) { var analytics = await _repository.GetOrCreateAsync(ct); switch (@event) { case OrderPlacedEvent e: analytics.TotalOrders++; analytics.TotalRevenue += e.TotalAmount; analytics.OrdersByStatus["Placed"]++; foreach (var line in e.Lines) { if (!analytics.ProductSales.ContainsKey(line.ProductId)) { analytics.ProductSales[line.ProductId] = new ProductSales { ProductId = line.ProductId, ProductName = line.ProductName, UnitsSold = 0, Revenue = 0 }; } analytics.ProductSales[line.ProductId].UnitsSold += line.Quantity; analytics.ProductSales[line.ProductId].Revenue += line.LineTotal; } break; case OrderCancelledEvent e: analytics.OrdersByStatus["Placed"]--; analytics.OrdersByStatus["Cancelled"]++; analytics.CancellationRate = (double)analytics.OrdersByStatus["Cancelled"] / analytics.TotalOrders; break; case OrderPaidEvent e: analytics.OrdersByStatus["Placed"]--; analytics.OrdersByStatus["Paid"]++; break; case OrderShippedEvent e: analytics.OrdersByStatus["Paid"]--; analytics.OrdersByStatus["Shipped"]++; break; case OrderDeliveredEvent e: analytics.OrdersByStatus["Shipped"]--; analytics.OrdersByStatus["Delivered"]++; analytics.FulfillmentRate = (double)analytics.OrdersByStatus["Delivered"] / analytics.TotalOrders; break; } await _repository.SaveAsync(analytics, ct); } public async Task ResetAsync(CancellationToken ct) { await _repository.DeleteAllAsync(ct); await _checkpointStore.SaveCheckpointAsync(ProjectionName, 0, ct); } } public class OrderAnalytics { public int TotalOrders { get; set; } public decimal TotalRevenue { get; set; } public Dictionary OrdersByStatus { get; set; } = new(); public Dictionary ProductSales { get; set; } = new(); public double CancellationRate { get; set; } public double FulfillmentRate { get; set; } } public class ProductSales { public string ProductId { get; set; } = string.Empty; public string ProductName { get; set; } = string.Empty; public int UnitsSold { get; set; } public decimal Revenue { get; set; } } ``` ## Top Products Projection Track top-selling products: ```csharp public class TopProductsProjection : IDynamicProjection { public string ProjectionName => "top-products"; private async Task HandleEventAsync(object @event, CancellationToken ct) { if (@event is OrderPlacedEvent e) { foreach (var line in e.Lines) { await _repository.IncrementSalesAsync(line.ProductId, line.Quantity, line.LineTotal, ct); } } } } public class TopProductsQuery : IQueryHandler> { private readonly ITopProductsRepository _repository; public async Task> HandleAsync(GetTopProductsQuery query, CancellationToken ct) { var topProducts = await _repository.GetTopAsync(query.Limit, ct); return topProducts.Select(p => new ProductSalesDto { ProductId = p.ProductId, ProductName = p.ProductName, UnitsSold = p.UnitsSold, Revenue = p.Revenue }).ToList(); } } ``` ## See Also - [06-sagas.md](06-sagas.md) - Order fulfillment workflow - [Projections](../../event-streaming/projections/README.md)