Excalibur.Saga
3.0.0-alpha.19
dotnet add package Excalibur.Saga --version 3.0.0-alpha.19
NuGet\Install-Package Excalibur.Saga -Version 3.0.0-alpha.19
<PackageReference Include="Excalibur.Saga" Version="3.0.0-alpha.19" />
<PackageVersion Include="Excalibur.Saga" Version="3.0.0-alpha.19" />
<PackageReference Include="Excalibur.Saga" />
paket add Excalibur.Saga --version 3.0.0-alpha.19
#r "nuget: Excalibur.Saga, 3.0.0-alpha.19"
#:package Excalibur.Saga@3.0.0-alpha.19
#addin nuget:?package=Excalibur.Saga&version=3.0.0-alpha.19&prerelease
#tool nuget:?package=Excalibur.Saga&version=3.0.0-alpha.19&prerelease
Excalibur.Saga
Saga state persistence and coordination for distributed transactions in .NET applications.
Overview
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.
Installation
dotnet add package Excalibur.Saga
For SQL Server persistence:
dotnet add package Excalibur.Saga.SqlServer
Features
| 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 |
Quick Start
1. Register Services
using Microsoft.Extensions.DependencyInjection;
services.AddExcaliburSaga(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);
2. Define Saga State
using Excalibur.Dispatch.Abstractions.Messaging.Delivery;
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; }
}
3. Implement Saga Orchestrator
using Excalibur.Dispatch.Abstractions.Messaging.Delivery;
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
}
Core Concepts
Saga State
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;
}
Saga Store
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;
}
Compensation Status
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
}
Configuration Options
SagaOptions
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);
}
Retry Policies
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
};
Saga Patterns
Orchestration Pattern
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:
- Complex workflows with conditional logic
- Need central visibility and monitoring
- Easier debugging requirements
Choreography Pattern
Services react to events without central coordination:
OrderPlaced ──► PaymentProcessed ──► InventoryReserved ──► ShipmentScheduled
│ │ │
▼ ▼ ▼
Payment Inventory Shipping
Service Service Service
Use when:
- Simple, linear workflows
- Loose coupling required
- High scalability needed
Compensation
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();
}
}
Timeout and Retry
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
}
Integration with Event Sourcing
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);
}
}
SQL Server Implementation
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
);
Monitoring
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;
}
}
}
Best Practices
DO
- Persist state before external calls - Recover from crashes
- Make steps idempotent - Handle duplicate deliveries
- Design compensations carefully - Not all actions can be undone
- Use correlation IDs - Track the entire distributed transaction
- Set appropriate timeouts - Prevent indefinite hanging
- Monitor saga health - Alert on stuck or failed sagas
DON'T
- Don't mix orchestration and choreography - Choose one per saga
- Don't store transient data in saga state - Only track step completion
- Don't make orchestrators do business logic - Delegate to services
- Don't forget compensation - Always plan for failure scenarios
- Don't use sagas for simple operations - Adds unnecessary complexity
Related Packages
| 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 |
License
This project is multi-licensed under:
See LICENSE for details.
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 is compatible. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. net9.0 is compatible. net9.0-android was computed. net9.0-browser was computed. net9.0-ios was computed. net9.0-maccatalyst was computed. net9.0-macos was computed. net9.0-tvos was computed. net9.0-windows was computed. net10.0 is compatible. net10.0-android was computed. net10.0-browser was computed. net10.0-ios was computed. net10.0-maccatalyst was computed. net10.0-macos was computed. net10.0-tvos was computed. net10.0-windows was computed. |
-
net10.0
- Ben.Demystifier (>= 0.4.1)
- CloudNative.CloudEvents (>= 2.8.0)
- CloudNative.CloudEvents.SystemTextJson (>= 2.8.0)
- Cronos (>= 0.11.1)
- Dapper (>= 2.1.66)
- Excalibur.Data.Abstractions (>= 3.0.0-alpha.19)
- Excalibur.Dispatch (>= 3.0.0-alpha.19)
- Excalibur.Dispatch.Abstractions (>= 3.0.0-alpha.19)
- Medo.Uuid7 (>= 1.4.0)
- MemoryPack (>= 1.21.4)
- Microsoft.ApplicationInsights (>= 2.23.0)
- Microsoft.AspNetCore.Authorization (>= 9.0.9)
- Microsoft.Extensions.Configuration (>= 10.0.0)
- Microsoft.Extensions.Configuration.Abstractions (>= 10.0.0)
- Microsoft.Extensions.Configuration.Binder (>= 10.0.0)
- Microsoft.Extensions.DependencyInjection (>= 10.0.0)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.0)
- Microsoft.Extensions.Diagnostics.HealthChecks (>= 10.0.0)
- Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions (>= 10.0.0)
- Microsoft.Extensions.Hosting.Abstractions (>= 10.0.0)
- Microsoft.Extensions.Http (>= 10.0.0)
- Microsoft.Extensions.Logging (>= 10.0.0)
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.0)
- Microsoft.Extensions.ObjectPool (>= 10.0.0)
- Microsoft.Extensions.Options (>= 10.0.0)
- Microsoft.Extensions.Options.ConfigurationExtensions (>= 10.0.0)
- Microsoft.Extensions.Options.DataAnnotations (>= 10.0.0)
- System.Threading.RateLimiting (>= 10.0.0)
-
net8.0
- Ben.Demystifier (>= 0.4.1)
- CloudNative.CloudEvents (>= 2.8.0)
- CloudNative.CloudEvents.SystemTextJson (>= 2.8.0)
- Cronos (>= 0.11.1)
- Dapper (>= 2.1.66)
- Excalibur.Data.Abstractions (>= 3.0.0-alpha.19)
- Excalibur.Dispatch (>= 3.0.0-alpha.19)
- Excalibur.Dispatch.Abstractions (>= 3.0.0-alpha.19)
- Medo.Uuid7 (>= 1.4.0)
- MemoryPack (>= 1.21.4)
- Microsoft.ApplicationInsights (>= 2.23.0)
- Microsoft.AspNetCore.Authorization (>= 9.0.9)
- Microsoft.Extensions.Configuration (>= 10.0.0)
- Microsoft.Extensions.Configuration.Abstractions (>= 10.0.0)
- Microsoft.Extensions.Configuration.Binder (>= 10.0.0)
- Microsoft.Extensions.DependencyInjection (>= 10.0.0)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.0)
- Microsoft.Extensions.Diagnostics.HealthChecks (>= 10.0.0)
- Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions (>= 10.0.0)
- Microsoft.Extensions.Hosting.Abstractions (>= 10.0.0)
- Microsoft.Extensions.Http (>= 10.0.0)
- Microsoft.Extensions.Logging (>= 10.0.0)
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.0)
- Microsoft.Extensions.ObjectPool (>= 10.0.0)
- Microsoft.Extensions.Options (>= 10.0.0)
- Microsoft.Extensions.Options.ConfigurationExtensions (>= 10.0.0)
- Microsoft.Extensions.Options.DataAnnotations (>= 10.0.0)
- System.Diagnostics.DiagnosticSource (>= 10.0.0)
- System.IO.Pipelines (>= 10.0.0)
- System.Threading.Channels (>= 10.0.0)
- System.Threading.RateLimiting (>= 10.0.0)
-
net9.0
- Ben.Demystifier (>= 0.4.1)
- CloudNative.CloudEvents (>= 2.8.0)
- CloudNative.CloudEvents.SystemTextJson (>= 2.8.0)
- Cronos (>= 0.11.1)
- Dapper (>= 2.1.66)
- Excalibur.Data.Abstractions (>= 3.0.0-alpha.19)
- Excalibur.Dispatch (>= 3.0.0-alpha.19)
- Excalibur.Dispatch.Abstractions (>= 3.0.0-alpha.19)
- Medo.Uuid7 (>= 1.4.0)
- MemoryPack (>= 1.21.4)
- Microsoft.ApplicationInsights (>= 2.23.0)
- Microsoft.AspNetCore.Authorization (>= 9.0.9)
- Microsoft.Extensions.Configuration (>= 10.0.0)
- Microsoft.Extensions.Configuration.Abstractions (>= 10.0.0)
- Microsoft.Extensions.Configuration.Binder (>= 10.0.0)
- Microsoft.Extensions.DependencyInjection (>= 10.0.0)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.0)
- Microsoft.Extensions.Diagnostics.HealthChecks (>= 10.0.0)
- Microsoft.Extensions.Diagnostics.HealthChecks.Abstractions (>= 10.0.0)
- Microsoft.Extensions.Hosting.Abstractions (>= 10.0.0)
- Microsoft.Extensions.Http (>= 10.0.0)
- Microsoft.Extensions.Logging (>= 10.0.0)
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.0)
- Microsoft.Extensions.ObjectPool (>= 10.0.0)
- Microsoft.Extensions.Options (>= 10.0.0)
- Microsoft.Extensions.Options.ConfigurationExtensions (>= 10.0.0)
- Microsoft.Extensions.Options.DataAnnotations (>= 10.0.0)
- System.Diagnostics.DiagnosticSource (>= 10.0.0)
- System.Threading.Channels (>= 10.0.0)
- System.Threading.RateLimiting (>= 10.0.0)
NuGet packages (3)
Showing the top 3 NuGet packages that depend on Excalibur.Saga:
| Package | Downloads |
|---|---|
|
Excalibur.Hosting
Core hosting infrastructure for Excalibur applications, including bootstrap and lifecycle integration for web and worker hosts. |
|
|
Excalibur.Hosting.AzureFunctions
Azure Functions hosting integration for the Excalibur framework. |
|
|
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. |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 3.0.0-alpha.19 | 38 | 2/26/2026 |