147 lines
4.8 KiB
C#
147 lines
4.8 KiB
C#
using Svrnty.CQRS.Events.Abstractions.Sagas;
|
|
|
|
namespace Svrnty.Sample.Sagas;
|
|
|
|
/// <summary>
|
|
/// Sample saga demonstrating compensation pattern for order fulfillment.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// This saga orchestrates:
|
|
/// 1. Reserve Inventory
|
|
/// 2. Authorize Payment
|
|
/// 3. Ship Order
|
|
///
|
|
/// If any step fails, all completed steps are compensated in reverse order.
|
|
/// </remarks>
|
|
public sealed class OrderFulfillmentSaga : ISaga
|
|
{
|
|
public string SagaId { get; set; } = string.Empty;
|
|
public string CorrelationId { get; set; } = string.Empty;
|
|
public string SagaName { get; set; } = "order-fulfillment";
|
|
}
|
|
|
|
/// <summary>
|
|
/// Saga steps for order fulfillment.
|
|
/// </summary>
|
|
public static class OrderFulfillmentSteps
|
|
{
|
|
/// <summary>
|
|
/// Step 1: Reserve inventory for the order.
|
|
/// </summary>
|
|
public static async Task ReserveInventoryAsync(ISagaContext context, CancellationToken cancellationToken)
|
|
{
|
|
var orderId = context.Get<string>("OrderId");
|
|
var items = context.Get<List<OrderItem>>("Items") ?? new List<OrderItem>();
|
|
|
|
Console.WriteLine($"[SAGA] Reserving inventory for order {orderId}...");
|
|
|
|
// Simulate inventory reservation
|
|
await Task.Delay(100, cancellationToken);
|
|
|
|
var reservationId = Guid.NewGuid().ToString();
|
|
context.Set("ReservationId", reservationId);
|
|
|
|
Console.WriteLine($"[SAGA] Inventory reserved: {reservationId}");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Compensation: Release inventory reservation.
|
|
/// </summary>
|
|
public static async Task CompensateReserveInventoryAsync(ISagaContext context, CancellationToken cancellationToken)
|
|
{
|
|
var reservationId = context.Get<string>("ReservationId");
|
|
|
|
Console.WriteLine($"[SAGA] COMPENSATING: Releasing inventory reservation {reservationId}...");
|
|
|
|
// Simulate inventory release
|
|
await Task.Delay(100, cancellationToken);
|
|
|
|
Console.WriteLine($"[SAGA] COMPENSATING: Inventory released");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Step 2: Authorize payment for the order.
|
|
/// </summary>
|
|
public static async Task AuthorizePaymentAsync(ISagaContext context, CancellationToken cancellationToken)
|
|
{
|
|
var orderId = context.Get<string>("OrderId");
|
|
var amount = context.Get<decimal>("Amount");
|
|
|
|
Console.WriteLine($"[SAGA] Authorizing payment for order {orderId}: ${amount}...");
|
|
|
|
// Simulate payment authorization (could fail based on saga data)
|
|
await Task.Delay(100, cancellationToken);
|
|
|
|
var shouldFail = context.Get<bool>("FailPayment");
|
|
if (shouldFail)
|
|
{
|
|
throw new InvalidOperationException("Payment authorization failed: Insufficient funds");
|
|
}
|
|
|
|
var authorizationCode = Guid.NewGuid().ToString();
|
|
context.Set("AuthorizationCode", authorizationCode);
|
|
|
|
Console.WriteLine($"[SAGA] Payment authorized: {authorizationCode}");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Compensation: Void payment authorization.
|
|
/// </summary>
|
|
public static async Task CompensateAuthorizePaymentAsync(ISagaContext context, CancellationToken cancellationToken)
|
|
{
|
|
var authorizationCode = context.Get<string>("AuthorizationCode");
|
|
|
|
Console.WriteLine($"[SAGA] COMPENSATING: Voiding payment authorization {authorizationCode}...");
|
|
|
|
// Simulate payment void
|
|
await Task.Delay(100, cancellationToken);
|
|
|
|
Console.WriteLine($"[SAGA] COMPENSATING: Payment voided");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Step 3: Ship the order.
|
|
/// </summary>
|
|
public static async Task ShipOrderAsync(ISagaContext context, CancellationToken cancellationToken)
|
|
{
|
|
var orderId = context.Get<string>("OrderId");
|
|
var address = context.Get<string>("ShippingAddress");
|
|
|
|
Console.WriteLine($"[SAGA] Shipping order {orderId} to {address}...");
|
|
|
|
// Simulate shipping
|
|
await Task.Delay(100, cancellationToken);
|
|
|
|
var trackingNumber = $"TRACK-{Guid.NewGuid().ToString()[..8].ToUpper()}";
|
|
context.Set("TrackingNumber", trackingNumber);
|
|
|
|
Console.WriteLine($"[SAGA] Order shipped: Tracking #{trackingNumber}");
|
|
}
|
|
|
|
/// <summary>
|
|
/// Compensation: Cancel shipment.
|
|
/// </summary>
|
|
public static async Task CompensateShipOrderAsync(ISagaContext context, CancellationToken cancellationToken)
|
|
{
|
|
var trackingNumber = context.Get<string>("TrackingNumber");
|
|
|
|
Console.WriteLine($"[SAGA] COMPENSATING: Canceling shipment {trackingNumber}...");
|
|
|
|
// Simulate shipment cancellation
|
|
await Task.Delay(100, cancellationToken);
|
|
|
|
Console.WriteLine($"[SAGA] COMPENSATING: Shipment cancelled");
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Order item for saga.
|
|
/// </summary>
|
|
public sealed class OrderItem
|
|
{
|
|
public required string ProductId { get; init; }
|
|
public required string ProductName { get; init; }
|
|
public int Quantity { get; init; }
|
|
public decimal Price { get; init; }
|
|
}
|