ZeroAlloc.Saga.Outbox
4.1.4
See the version list below for details.
dotnet add package ZeroAlloc.Saga.Outbox --version 4.1.4
NuGet\Install-Package ZeroAlloc.Saga.Outbox -Version 4.1.4
<PackageReference Include="ZeroAlloc.Saga.Outbox" Version="4.1.4" />
<PackageVersion Include="ZeroAlloc.Saga.Outbox" Version="4.1.4" />
<PackageReference Include="ZeroAlloc.Saga.Outbox" />
paket add ZeroAlloc.Saga.Outbox --version 4.1.4
#r "nuget: ZeroAlloc.Saga.Outbox, 4.1.4"
#:package ZeroAlloc.Saga.Outbox@4.1.4
#addin nuget:?package=ZeroAlloc.Saga.Outbox&version=4.1.4
#tool nuget:?package=ZeroAlloc.Saga.Outbox&version=4.1.4
ZeroAlloc.Saga
Source-generated sagas for .NET: declare steps and their compensation on a partial class, and get a state machine with no reflection that runs under NativeAOT, on in-memory, EF Core, ZeroAlloc.ORM or Redis storage.
Why ZeroAlloc.Saga
- A workflow is one partial class. Steps, correlation keys and compensation are attributes. The generator emits the state machine, the event handlers and the command dispatch, and compensation runs in reverse automatically when a failure event arrives.
- Works under NativeAOT and trimming. CI publishes and runs AOT samples for the core, the outbox bridge, the ORM store and the resilience bridge. The EF Core store runs on JIT hosts only, blocked upstream.
- Compile-time diagnostics. 16 generator diagnostics catch authoring mistakes such as gaps in step order, missing correlation keys or a misshaped compensation method, three of them with code fixes.
- No double dispatch under retry. With the outbox bridge on the EF Core, ORM or Redis store, each step command is committed atomically with the saga state, so an optimistic-concurrency retry cannot send a command twice.
Install
dotnet add package ZeroAlloc.Saga
dotnet add package ZeroAlloc.Mediator.Generator
ZeroAlloc.Saga carries its own generator. Saga commands and events flow through ZeroAlloc.Mediator, whose generator you reference directly.
Quick start
using Microsoft.Extensions.DependencyInjection;
using ZeroAlloc.Mediator;
using ZeroAlloc.Saga;
var services = new ServiceCollection().AddLogging().AddTransient<Warehouse>().AddTransient<Payments>();
services.AddSaga().WithCheckoutSaga(); // generated for the [Saga] class
await using var provider = services.BuildServiceProvider();
var mediator = provider.GetRequiredService<IMediator>();
await mediator.Publish(new OrderPlaced(42, 9.95m), default); // step 1 sends ReserveStock
await mediator.Publish(new StockReserved(42), default); // step 2 sends ChargeCard
await mediator.Publish(new PaymentDeclined(42), default); // compensation sends ReleaseStock
public sealed record OrderPlaced(int OrderId, decimal Total) : INotification;
public sealed record StockReserved(int OrderId) : INotification;
public sealed record PaymentCharged(int OrderId) : INotification;
public sealed record PaymentDeclined(int OrderId) : INotification;
public readonly record struct ReserveStock(int OrderId) : IRequest;
public readonly record struct ReleaseStock(int OrderId) : IRequest;
public readonly record struct ChargeCard(int OrderId, decimal Total) : IRequest;
public readonly record struct ShipOrder(int OrderId) : IRequest;
[Saga]
public partial class CheckoutSaga
{
public int OrderId { get; private set; }
public decimal Total { get; private set; }
[CorrelationKey] public int Key(OrderPlaced e) => e.OrderId;
[CorrelationKey] public int Key(StockReserved e) => e.OrderId;
[CorrelationKey] public int Key(PaymentCharged e) => e.OrderId;
[CorrelationKey] public int Key(PaymentDeclined e) => e.OrderId;
[Step(Order = 1, Compensate = nameof(Release))]
public ReserveStock Reserve(OrderPlaced e) { (OrderId, Total) = (e.OrderId, e.Total); return new(OrderId); }
[Step(Order = 2, CompensateOn = typeof(PaymentDeclined))]
public ChargeCard Charge(StockReserved e) => new(OrderId, Total);
[Step(Order = 3)]
public ShipOrder Ship(PaymentCharged e) => new(OrderId);
public ReleaseStock Release() => new(OrderId);
}
public sealed class Warehouse : IRequestHandler<ReserveStock, Unit>, IRequestHandler<ReleaseStock, Unit>, IRequestHandler<ShipOrder, Unit>
{
public ValueTask<Unit> Handle(ReserveStock c, CancellationToken ct) => Log($"Reserved stock for {c.OrderId}");
public ValueTask<Unit> Handle(ReleaseStock c, CancellationToken ct) => Log($"Released stock for {c.OrderId}");
public ValueTask<Unit> Handle(ShipOrder c, CancellationToken ct) => Log($"Shipped {c.OrderId}");
internal static ValueTask<Unit> Log(string what) { Console.WriteLine(what); return new(Unit.Value); }
}
public sealed class Payments : IRequestHandler<ChargeCard, Unit>
{
public ValueTask<Unit> Handle(ChargeCard c, CancellationToken ct) => Warehouse.Log($"Charging {c.Total}");
}
It prints Reserved stock for 42, Charging 9.95 and Released stock for 42. A saga that reaches a terminal state is removed from the store automatically. Concepts explains the lifecycle and what the generator emits.
What you get
- Correlation by any key type, including strongly typed IDs and composite keys.
- Automatic compensation on a failure event, and manual compensation through
ISagaManager.CompensateAsync. - Optimistic concurrency with retry on conflict, each attempt in a fresh DI scope.
- In-memory storage by default; switch to a durable store with one fluent call.
Optional packages:
ZeroAlloc.Saga.EfCore: durable store on your EF CoreDbContext, with row-version concurrency.ZeroAlloc.Saga.Orm: durable store over ZeroAlloc.ORM, NativeAOT-ready.ZeroAlloc.Saga.Redis: durable store on Redis withWATCH/MULTI/EXECconcurrency.ZeroAlloc.Saga.Outbox: step commands dispatched through ZeroAlloc.Outbox, committed with the saga state.ZeroAlloc.Saga.Outbox.Orm: outbox rows in the ORM saga store's transaction.ZeroAlloc.Saga.Outbox.Redis: outbox rows in the Redis saga store'sMULTI/EXEC.ZeroAlloc.Saga.Resilience: retry, timeout, circuit-breaker and rate-limit around step dispatch.
Performance
Saga state is persisted through generated Snapshot() and Restore() methods. Exact budgets, asserted by SagaStateAllocationTests:
| Call | Allocation |
|---|---|
SagaStateWriter, every supported shape |
0 B |
Generated Restore() |
0 B |
Generated Snapshot(), a 21-byte state |
48 B, the returned array |
There is no published head-to-head against another saga library yet. Details: docs/performance.md.
Learn more
- Documentation: docs/, starting with concepts, correlation and compensation
- Reference: packages and dependencies, diagnostics, samples, limitations and roadmap
- Upgrading: migrating to v4, release highlights, CHANGELOG
- Related: ZeroAlloc.Mediator, ZeroAlloc.Outbox, ZeroAlloc.Resilience, ZeroAlloc.EventSourcing
- Contributing: issues and pull requests are welcome on GitHub
- License: MIT, see LICENSE
| 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 was computed. 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
- Microsoft.Extensions.DependencyInjection (>= 10.0.12)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.12)
- Microsoft.Extensions.Hosting.Abstractions (>= 10.0.12)
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.12)
- ZeroAlloc.Mediator (>= 5.1.5)
- ZeroAlloc.Outbox (>= 4.4.4)
- ZeroAlloc.Pipeline (>= 1.4.1)
- ZeroAlloc.Resilience (>= 3.3.5)
- ZeroAlloc.Saga (>= 4.1.4)
- ZeroAlloc.Serialisation (>= 2.8.2)
-
net8.0
- Microsoft.Extensions.DependencyInjection (>= 10.0.12)
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.12)
- Microsoft.Extensions.Hosting.Abstractions (>= 10.0.12)
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.12)
- ZeroAlloc.Mediator (>= 5.1.5)
- ZeroAlloc.Outbox (>= 4.4.4)
- ZeroAlloc.Pipeline (>= 1.4.1)
- ZeroAlloc.Resilience (>= 3.3.5)
- ZeroAlloc.Saga (>= 4.1.4)
- ZeroAlloc.Serialisation (>= 2.8.2)
NuGet packages (2)
Showing the top 2 NuGet packages that depend on ZeroAlloc.Saga.Outbox:
| Package | Downloads |
|---|---|
|
ZeroAlloc.Saga.Outbox.Redis
Redis-native outbox storage + transactional unit-of-work for ZeroAlloc.Saga.Outbox + ZeroAlloc.Saga.Redis. Closes the cross-backend atomic-dispatch story: saga state save and outbox row commit together (or roll back together) inside a single Redis MULTI/EXEC. Native AOT compatible. |
|
|
ZeroAlloc.Saga.Outbox.Orm
Transactional outbox unit of work for ZeroAlloc.Saga.Outbox + ZeroAlloc.Saga.Orm. A saga step's outbox rows commit in the same database transaction as the saga state, through ZeroAlloc.Outbox.Orm, or roll back with it. Native AOT compatible. |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 4.1.6 | 83 | 10/16/2026 |
| 4.1.5 | 62 | 10/16/2026 |
| 4.1.4 | 60 | 10/16/2026 |
| 4.1.3 | 83 | 10/15/2026 |
| 4.1.2 | 175 | 10/11/2026 |
| 4.1.1 | 109 | 10/11/2026 |
| 4.1.0 | 169 | 9/30/2026 |
| 4.0.2 | 176 | 9/30/2026 |
| 4.0.1 | 115 | 9/28/2026 |
| 4.0.0 | 149 | 9/27/2026 |
| 3.1.1 | 162 | 9/25/2026 |
| 3.1.0 | 136 | 9/22/2026 |
| 3.0.0 | 109 | 9/20/2026 |
| 2.0.4 | 112 | 9/20/2026 |
| 2.0.3 | 109 | 9/20/2026 |
| 2.0.2 | 108 | 9/19/2026 |
| 2.0.1 | 107 | 9/19/2026 |
| 2.0.0 | 124 | 8/7/2026 |
| 1.5.0 | 136 | 5/4/2026 |
| 1.4.0 | 116 | 5/4/2026 |