144 lines
4.8 KiB
Markdown
144 lines
4.8 KiB
Markdown
# E-Commerce Example: Sagas
|
|
|
|
Implement the order fulfillment saga to coordinate order processing.
|
|
|
|
## OrderFulfillmentSaga
|
|
|
|
The saga coordinates the entire order lifecycle:
|
|
|
|
```csharp
|
|
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
|
|
|
|
```csharp
|
|
// In Program.cs
|
|
builder.Services.AddWorkflow<OrderPlacedEvent, OrderFulfillmentSaga>();
|
|
```
|
|
|
|
## See Also
|
|
|
|
- [07-http-api.md](07-http-api.md) - HTTP API endpoints
|
|
- [Sagas](../../event-streaming/sagas/README.md)
|