dotnet-cqrs/docs/tutorials/ecommerce-example/06-sagas.md

4.8 KiB

E-Commerce Example: Sagas

Implement the order fulfillment saga to coordinate order processing.

OrderFulfillmentSaga

The saga coordinates the entire order lifecycle:

public class OrderFulfillmentSaga : IWorkflow<OrderPlacedEvent>
{
    private readonly IEventStreamStore _eventStore;
    private readonly IInventoryService _inventory;
    private readonly IPaymentService _payment;
    private readonly IShippingService _shipping;
    private readonly ILogger<OrderFulfillmentSaga> _logger;

    public async Task HandleAsync(OrderPlacedEvent @event, CancellationToken ct)
    {
        var streamName = $"order-{@event.OrderId}";

        try
        {
            // Step 1: Reserve inventory
            _logger.LogInformation("Reserving inventory for order {OrderId}", @event.OrderId);

            var reservation = await _inventory.ReserveAsync(
                @event.OrderId,
                @event.Lines.Select(l => new InventoryItem
                {
                    ProductId = l.ProductId,
                    Quantity = l.Quantity
                }).ToList(),
                ct);

            if (!reservation.Success)
            {
                await _eventStore.AppendAsync(streamName, new InventoryUnavailableEvent
                {
                    OrderId = @event.OrderId,
                    UnavailableItems = reservation.UnavailableItems,
                    NotifiedAt = DateTimeOffset.UtcNow
                }, ct);

                await _eventStore.AppendAsync(streamName, new OrderCancelledEvent
                {
                    OrderId = @event.OrderId,
                    CancelledBy = "System",
                    Reason = "Inventory unavailable",
                    RefundIssued = false,
                    CancelledAt = DateTimeOffset.UtcNow
                }, ct);
                return;
            }

            await _eventStore.AppendAsync(streamName, new InventoryReservedEvent
            {
                OrderId = @event.OrderId,
                ReservationId = reservation.ReservationId,
                Items = @event.Lines.Select(l => new InventoryItemDto
                {
                    ProductId = l.ProductId,
                    Quantity = l.Quantity
                }).ToList(),
                ReservedAt = DateTimeOffset.UtcNow
            }, ct);

            // Step 2: Process payment
            _logger.LogInformation("Processing payment for order {OrderId}", @event.OrderId);

            var paymentResult = await _payment.ChargeAsync(
                @event.OrderId,
                @event.TotalAmount,
                ct);

            if (!paymentResult.Success)
            {
                // Compensation: Release inventory
                await _inventory.ReleaseAsync(reservation.ReservationId, ct);

                await _eventStore.AppendAsync(streamName, new PaymentFailedEvent
                {
                    OrderId = @event.OrderId,
                    Reason = paymentResult.ErrorMessage,
                    FailedAt = DateTimeOffset.UtcNow
                }, ct);

                await _eventStore.AppendAsync(streamName, new OrderCancelledEvent
                {
                    OrderId = @event.OrderId,
                    CancelledBy = "System",
                    Reason = "Payment failed",
                    RefundIssued = false,
                    CancelledAt = DateTimeOffset.UtcNow
                }, ct);
                return;
            }

            await _eventStore.AppendAsync(streamName, new PaymentProcessedEvent
            {
                PaymentId = paymentResult.PaymentId,
                OrderId = @event.OrderId,
                Amount = @event.TotalAmount,
                ProcessedAt = DateTimeOffset.UtcNow
            }, ct);

            await _eventStore.AppendAsync(streamName, new OrderPaidEvent
            {
                OrderId = @event.OrderId,
                PaymentId = paymentResult.PaymentId,
                PaymentMethod = "CreditCard",
                Amount = @event.TotalAmount,
                PaidAt = DateTimeOffset.UtcNow
            }, ct);

            _logger.LogInformation("Order {OrderId} fulfilled successfully", @event.OrderId);
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "Failed to fulfill order {OrderId}", @event.OrderId);

            await _eventStore.AppendAsync(streamName, new OrderFulfillmentFailedEvent
            {
                OrderId = @event.OrderId,
                ErrorMessage = ex.Message,
                FailedAt = DateTimeOffset.UtcNow
            }, ct);
        }
    }
}

Saga Registration

// In Program.cs
builder.Services.AddWorkflow<OrderPlacedEvent, OrderFulfillmentSaga>();

See Also