![]() |
VOOZH | about |
dotnet add package Excalibur.Saga --version 3.0.0-alpha.208
NuGet\Install-Package Excalibur.Saga -Version 3.0.0-alpha.208
<PackageReference Include="Excalibur.Saga" Version="3.0.0-alpha.208" />
<PackageVersion Include="Excalibur.Saga" Version="3.0.0-alpha.208" />Directory.Packages.props
<PackageReference Include="Excalibur.Saga" />Project file
paket add Excalibur.Saga --version 3.0.0-alpha.208
#r "nuget: Excalibur.Saga, 3.0.0-alpha.208"
#:package Excalibur.Saga@3.0.0-alpha.208
#addin nuget:?package=Excalibur.Saga&version=3.0.0-alpha.208&prereleaseInstall as a Cake Addin
#tool nuget:?package=Excalibur.Saga&version=3.0.0-alpha.208&prereleaseInstall as a Cake Tool
Saga state persistence and coordination for distributed transactions in .NET applications.
Excalibur.Saga provides the foundation for implementing the Saga pattern - a way to manage distributed transactions across multiple services without requiring two-phase commit (2PC). It ensures data consistency in microservices architectures by coordinating compensating transactions when failures occur.
dotnet add package Excalibur.Saga
For SQL Server persistence:
dotnet add package Excalibur.Saga.SqlServer
| Feature | Description |
|---|---|
| ISagaStore | Saga state persistence abstraction |
| SagaOptions | Configuration for timeouts, retries, concurrency |
| Compensation Tracking | Track compensation status per step |
| Retry Policies | Built-in exponential backoff and fixed delay |
| AOT Compatible | Full Native AOT support |
| Provider Agnostic | Pluggable storage backends |
using Microsoft.Extensions.DependencyInjection;
services.AddExcalibur(x => x.AddSagas(options =>
{
options.MaxConcurrency = 10;
options.DefaultTimeout = TimeSpan.FromMinutes(30);
options.MaxRetryAttempts = 3;
options.RetryDelay = TimeSpan.FromMinutes(1);
}));
// Add SQL Server persistence (optional)
services.AddExcaliburSagaSqlServer(connectionString);
using Excalibur.Dispatch.Messaging;
public class OrderFulfillmentState : SagaState
{
public Guid OrderId { get; set; }
public Guid CustomerId { get; set; }
public decimal TotalAmount { get; set; }
// Step completion tracking
public bool OrderCreated { get; set; }
public bool PaymentProcessed { get; set; }
public bool InventoryReserved { get; set; }
public bool ShipmentScheduled { get; set; }
// Compensation tracking
public bool PaymentRefunded { get; set; }
public bool InventoryReleased { get; set; }
// Error information
public string? FailureReason { get; set; }
public DateTimeOffset? FailedAt { get; set; }
}
using Excalibur.Dispatch.Messaging;
public class OrderFulfillmentSaga : Saga<OrderFulfillmentState>
{
public OrderFulfillmentSaga(
OrderFulfillmentState state,
IDispatcher dispatcher,
ILogger<OrderFulfillmentSaga> logger)
: base(state, dispatcher, logger)
{
}
public override bool HandlesEvent(object eventMessage)
{
return eventMessage is StartOrderFulfillment
or OrderCreated
or PaymentProcessed
or PaymentFailed
or InventoryReserved
or ShipmentScheduled;
}
public override async Task HandleAsync(
object eventMessage,
CancellationToken cancellationToken)
{
switch (eventMessage)
{
case StartOrderFulfillment start:
await HandleStart(start, cancellationToken);
break;
case OrderCreated created:
await HandleOrderCreated(created, cancellationToken);
break;
case PaymentProcessed processed:
await HandlePaymentProcessed(processed, cancellationToken);
break;
case PaymentFailed failed:
await HandlePaymentFailed(failed, cancellationToken);
break;
// ... additional handlers
}
}
private async Task HandleStart(
StartOrderFulfillment start,
CancellationToken cancellationToken)
{
State.OrderId = start.OrderId;
State.CustomerId = start.CustomerId;
State.TotalAmount = start.TotalAmount;
Logger.LogInformation("Starting saga for order {OrderId}", start.OrderId);
await Dispatcher.DispatchAsync(
new CreateOrder(start.OrderId, start.CustomerId, start.TotalAmount),
cancellationToken);
}
// ... additional handler methods
}
The SagaState base class provides essential tracking:
public abstract class SagaState
{
/// <summary>
/// Unique identifier for this saga instance.
/// </summary>
public Guid SagaId { get; set; } = Guid.NewGuid();
/// <summary>
/// Whether the saga has completed (successfully or via compensation).
/// </summary>
public bool Completed { get; set; }
/// <summary>
/// Current step in the saga workflow.
/// </summary>
public int CurrentStep { get; set; }
/// <summary>
/// When the saga was started.
/// </summary>
public DateTimeOffset StartedAt { get; set; } = DateTimeOffset.UtcNow;
}
ISagaStore persists saga state across process restarts:
public interface ISagaStore
{
Task<TSagaState?> GetAsync<TSagaState>(
Guid sagaId,
CancellationToken cancellationToken)
where TSagaState : SagaState;
Task SaveAsync<TSagaState>(
TSagaState state,
CancellationToken cancellationToken)
where TSagaState : SagaState;
Task DeleteAsync(
Guid sagaId,
CancellationToken cancellationToken);
Task<IReadOnlyList<TSagaState>> GetPendingAsync<TSagaState>(
CancellationToken cancellationToken)
where TSagaState : SagaState;
}
Track compensation state per step:
public enum CompensationStatus
{
/// <summary>
/// Step completed successfully, no compensation needed.
/// </summary>
NotRequired,
/// <summary>
/// Compensation is pending.
/// </summary>
Pending,
/// <summary>
/// Compensation is currently executing.
/// </summary>
Running,
/// <summary>
/// Compensation completed successfully.
/// </summary>
Succeeded,
/// <summary>
/// Compensation failed.
/// </summary>
Failed,
/// <summary>
/// Step cannot be compensated.
/// </summary>
NotCompensable
}
public class SagaOptions
{
/// <summary>
/// Maximum concurrent saga executions.
/// Default: 10
/// </summary>
public int MaxConcurrency { get; set; } = 10;
/// <summary>
/// Default timeout for saga steps.
/// Default: 30 minutes
/// </summary>
public TimeSpan DefaultTimeout { get; set; } = TimeSpan.FromMinutes(30);
/// <summary>
/// Maximum retry attempts before dead letter.
/// Default: 3
/// </summary>
public int MaxRetryAttempts { get; set; } = 3;
/// <summary>
/// Delay between retry attempts.
/// Default: 1 minute
/// </summary>
public TimeSpan RetryDelay { get; set; } = TimeSpan.FromMinutes(1);
}
Built-in retry strategies for transient failures:
// Exponential backoff: 1s, 2s, 4s...
var exponential = RetryPolicy.ExponentialBackoff(
maxAttempts: 3,
initialDelay: TimeSpan.FromSeconds(1));
// Fixed delay between retries
var fixed = RetryPolicy.FixedDelay(
maxAttempts: 3,
delay: TimeSpan.FromSeconds(5));
// Custom policy
var custom = new RetryPolicy
{
MaxAttempts = 5,
InitialDelay = TimeSpan.FromMilliseconds(500),
MaxDelay = TimeSpan.FromSeconds(30),
BackoffMultiplier = 2.0,
UseJitter = true // Prevents thundering herd
};
A central coordinator manages the saga workflow:
┌─────────────┐
│ Orchestrator│
└──────┬──────┘
│
├──► Step 1: Create Order
├──► Step 2: Process Payment
├──► Step 3: Reserve Inventory
└──► Step 4: Schedule Shipment
Use when:
Services react to events without central coordination:
OrderPlaced ──► PaymentProcessed ──► InventoryReserved ──► ShipmentScheduled
│ │ │
▼ ▼ ▼
Payment Inventory Shipping
Service Service Service
Use when:
When a step fails, previous steps must be compensated (rolled back):
public class ProcessPaymentStep : ISagaStep<OrderSagaData>
{
public string Name => "ProcessPayment";
public TimeSpan Timeout => TimeSpan.FromSeconds(60);
public RetryPolicy? RetryPolicy => RetryPolicy.ExponentialBackoff(3);
public bool CanCompensate => true;
public async Task<StepResult> ExecuteAsync(
SagaExecutionContext<OrderSagaData> context,
CancellationToken cancellationToken)
{
var paymentId = await _gateway.ChargeAsync(
context.Data.CustomerId,
context.Data.Amount,
cancellationToken);
context.Data.PaymentId = paymentId;
return StepResult.Success();
}
public async Task<StepResult> CompensateAsync(
SagaExecutionContext<OrderSagaData> context,
CancellationToken cancellationToken)
{
// Refund the payment
await _gateway.RefundAsync(
context.Data.PaymentId,
cancellationToken);
return StepResult.Success();
}
}
Configure timeouts and retries per step:
public class ExternalApiStep : ISagaStep<OrderSagaData>
{
public string Name => "CallExternalApi";
// Step times out after 45 seconds
public TimeSpan Timeout => TimeSpan.FromSeconds(45);
// Retry with exponential backoff + jitter
public RetryPolicy? RetryPolicy => new RetryPolicy
{
MaxAttempts = 4,
InitialDelay = TimeSpan.FromSeconds(1),
BackoffMultiplier = 2.0,
UseJitter = true
};
public bool CanCompensate => true;
// ... implementation
}
Sagas work seamlessly with event-sourced aggregates:
public class OrderSagaEventHandler : IEventHandler<OrderPlaced>
{
private readonly ISagaCoordinator _coordinator;
public async Task HandleAsync(
OrderPlaced @event,
CancellationToken cancellationToken)
{
// Start saga when order is placed
await _coordinator.StartAsync(new StartOrderFulfillment
{
SagaId = Guid.NewGuid().ToString(),
OrderId = @event.OrderId,
CustomerId = @event.CustomerId,
TotalAmount = @event.TotalAmount
}, cancellationToken);
}
}
For production use, add SQL Server persistence:
services.AddExcaliburSagaSqlServer(options =>
{
options.ConnectionString = connectionString;
options.SchemaName = "saga";
options.TableName = "SagaState";
});
This creates a table to store saga state:
CREATE TABLE [saga].[SagaState] (
[SagaId] UNIQUEIDENTIFIER NOT NULL PRIMARY KEY,
[SagaType] NVARCHAR(256) NOT NULL,
[State] NVARCHAR(MAX) NOT NULL,
[Status] INT NOT NULL,
[CurrentStep] INT NOT NULL,
[StartedAt] DATETIMEOFFSET NOT NULL,
[CompletedAt] DATETIMEOFFSET NULL,
[Version] INT NOT NULL
);
Track saga execution with logging and metrics:
public class MonitoredSaga : Saga<OrderFulfillmentState>
{
public override async Task HandleAsync(
object eventMessage,
CancellationToken cancellationToken)
{
using var activity = ActivitySource.StartActivity("Saga.HandleEvent");
activity?.SetTag("saga.id", State.SagaId);
activity?.SetTag("event.type", eventMessage.GetType().Name);
try
{
await base.HandleAsync(eventMessage, cancellationToken);
SagaMetrics.EventsProcessed.Inc();
}
catch (Exception ex)
{
SagaMetrics.EventsFailed.Inc();
activity?.SetStatus(ActivityStatusCode.Error, ex.Message);
throw;
}
}
}
| Package | Purpose |
|---|---|
Excalibur.Saga.SqlServer |
SQL Server saga store implementation |
Excalibur.Dispatch.Patterns |
Saga step abstractions and orchestration |
Excalibur.Dispatch.Abstractions |
Core saga interfaces |
Dispatch |
Message dispatching for saga events |
This project is multi-licensed under:
See for details.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 net10.0 is compatible. net10.0-android net10.0-android was computed. net10.0-browser net10.0-browser was computed. net10.0-ios net10.0-ios was computed. net10.0-maccatalyst net10.0-maccatalyst was computed. net10.0-macos net10.0-macos was computed. net10.0-tvos net10.0-tvos was computed. net10.0-windows net10.0-windows was computed. |
Showing the top 5 NuGet packages that depend on Excalibur.Saga:
| Package | Downloads |
|---|---|
|
Excalibur.Data.MongoDB
MongoDB database provider implementation for Excalibur data access layer. |
|
|
Excalibur.Data.DynamoDb
AWS DynamoDB data provider implementation for Excalibur cloud-native data access. |
|
|
Excalibur.Data.Firestore
Google Cloud Firestore data provider implementation for Excalibur cloud-native data access. |
|
|
Excalibur.Data.CosmosDb
Azure Cosmos DB data provider implementation for Excalibur cloud-native data access. |
|
|
Excalibur.Saga.SqlServer
SQL Server implementation of the saga pattern for Excalibur, providing durable saga state persistence with optimistic concurrency control using SQL Server as the backing store. |
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 3.0.0-alpha.208 | 178 | 6/11/2026 |
| 3.0.0-alpha.207 | 185 | 6/11/2026 |
| 3.0.0-alpha.205 | 208 | 6/10/2026 |
| 3.0.0-alpha.204 | 205 | 6/8/2026 |
| 3.0.0-alpha.203 | 182 | 6/8/2026 |
| 3.0.0-alpha.202 | 200 | 6/8/2026 |
| 3.0.0-alpha.201 | 199 | 6/8/2026 |
| 3.0.0-alpha.199 | 186 | 6/8/2026 |
| 3.0.0-alpha.198 | 189 | 5/28/2026 |
| 3.0.0-alpha.197 | 199 | 5/28/2026 |
| 3.0.0-alpha.194 | 203 | 5/20/2026 |
| 3.0.0-alpha.193 | 190 | 5/13/2026 |
| 3.0.0-alpha.192 | 182 | 5/13/2026 |
| 3.0.0-alpha.191 | 197 | 5/13/2026 |
| 3.0.0-alpha.189 | 184 | 5/12/2026 |
| 3.0.0-alpha.187 | 202 | 5/8/2026 |
| 3.0.0-alpha.185 | 208 | 5/7/2026 |
| 3.0.0-alpha.183 | 207 | 5/7/2026 |
| 3.0.0-alpha.182 | 201 | 5/6/2026 |
| 3.0.0-alpha.181 | 221 | 5/6/2026 |