Excalibur.Saga 10.0.0-alpha.21

This is a prerelease version of Excalibur.Saga.
dotnet add package Excalibur.Saga --version 10.0.0-alpha.21
                    
NuGet\Install-Package Excalibur.Saga -Version 10.0.0-alpha.21
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Excalibur.Saga" Version="10.0.0-alpha.21" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Excalibur.Saga" Version="10.0.0-alpha.21" />
                    
Directory.Packages.props
<PackageReference Include="Excalibur.Saga" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Excalibur.Saga --version 10.0.0-alpha.21
                    
#r "nuget: Excalibur.Saga, 10.0.0-alpha.21"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Excalibur.Saga@10.0.0-alpha.21
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Excalibur.Saga&version=10.0.0-alpha.21&prerelease
                    
Install as a Cake Addin
#tool nuget:?package=Excalibur.Saga&version=10.0.0-alpha.21&prerelease
                    
Install as a Cake Tool

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.AddExcalibur(x => x.AddSagas(options =>
{
    options.MaxConcurrency = 10;
    options.DefaultTimeout = TimeSpan.FromMinutes(30);
    options.MaxAttempts = 3;
    options.RetryDelay = TimeSpan.FromMinutes(1);
}));

// Add SQL Server persistence (optional)
services.AddExcalibur(excalibur => excalibur.AddSagas(saga =>
    saga.UseSqlServer(sql => sql.ConnectionString(connectionString))));

2. Define Saga State

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; }
}

3. Implement Saga Orchestrator

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
}

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 MaxAttempts { 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.AddExcalibur(excalibur => excalibur.AddSagas(saga =>
    saga.UseSqlServer(sql => sql.ConnectionString(connectionString))));

// Optional. These are the defaults, and they must match the table you provisioned:
//   sql.SchemaName("dispatch")
//   sql.TableName("sagas")

Provisioning the schema

The store does not create its table at runtime. Provision it before the first saga is started, by running the script shipped inside the Excalibur.Saga.SqlServer package:

scripts/01-SagaSchema.sql

That script creates dispatch.sagas and its indexes, and it is the authoritative definition of the table's shape. It is safe to re-run: each statement is guarded, and it also upgrades a table created by an earlier version.

The DDL is deliberately not reproduced here. A copy in a README drifts out of step with the script the moment a column is added, and a consumer who follows the drifted copy gets a table the store cannot read.

Two properties of that schema are worth knowing before you provision it, because they affect how you operate the table:

  • The tenant discriminator is part of the primary key, which is (TenantId, SagaId). Sagas are correlated by a business key such as an order id, so two tenants can legitimately hold the same SagaId; keyed on SagaId alone, one tenant's saga would overwrite the other's. A row that is genuinely not tenant-scoped stores a reserved sentinel rather than NULL.
  • CompletedAt drives retention. The purge deletes on that column, so it is indexed, and it is stored with its UTC offset.

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.RecordSagaCompleted(nameof(MonitoredSaga));
        }
        catch (Exception ex)
        {
            SagaMetrics.RecordSagaFailed(nameof(MonitoredSaga));
            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
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 Compatible and additional computed target framework versions.
.NET 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (14)

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. PRE-RELEASE: the public API is not frozen and may change between pre-release builds. Not recommended for production use. Known issues: https://docs.excalibur-dispatch.dev/docs/known-issues

Excalibur.Data.DynamoDb

AWS DynamoDB data provider implementation for Excalibur cloud-native data access. PRE-RELEASE: the public API is not frozen and may change between pre-release builds. Not recommended for production use. Known issues: https://docs.excalibur-dispatch.dev/docs/known-issues

Excalibur.Data.Firestore

Google Cloud Firestore data provider implementation for Excalibur cloud-native data access. PRE-RELEASE: the public API is not frozen and may change between pre-release builds. Not recommended for production use. Known issues: https://docs.excalibur-dispatch.dev/docs/known-issues

Excalibur.Data.CosmosDb

Azure Cosmos DB data provider implementation for Excalibur cloud-native data access. PRE-RELEASE: the public API is not frozen and may change between pre-release builds. Not recommended for production use. Known issues: https://docs.excalibur-dispatch.dev/docs/known-issues

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. PRE-RELEASE: the public API is not frozen and may change between pre-release builds. Not recommended for production use. Known issues: https://docs.excalibur-dispatch.dev/docs/known-issues

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
10.0.0-alpha.21 204 11/1/2026
10.0.0-alpha.20 206 10/30/2026
10.0.0-alpha.19 206 10/27/2026
10.0.0-alpha.18 203 10/25/2026
10.0.0-alpha.17 210 10/23/2026
10.0.0-alpha.16 220 10/21/2026
10.0.0-alpha.15 222 10/11/2026
10.0.0-alpha.14 223 10/2/2026
10.0.0-alpha.13 203 9/29/2026
10.0.0-alpha.12 227 9/25/2026
10.0.0-alpha.11 233 9/15/2026
10.0.0-alpha.10 249 9/6/2026
10.0.0-alpha.9 235 8/31/2026
10.0.0-alpha.8 276 8/14/2026
10.0.0-alpha.7 279 8/13/2026
10.0.0-alpha.6 265 8/11/2026
10.0.0-alpha.5 280 8/10/2026
10.0.0-alpha.4 270 8/10/2026
3.0.0-alpha.216 242 6/30/2026
3.0.0-alpha.215 226 6/23/2026
Loading failed