4.8 KiB
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
- 07-http-api.md - HTTP API endpoints
- Sagas