ZeroAlloc.Outbox
4.4.4
dotnet add package ZeroAlloc.Outbox --version 4.4.4
NuGet\Install-Package ZeroAlloc.Outbox -Version 4.4.4
<PackageReference Include="ZeroAlloc.Outbox" Version="4.4.4" />
<PackageVersion Include="ZeroAlloc.Outbox" Version="4.4.4" />
<PackageReference Include="ZeroAlloc.Outbox" />
paket add ZeroAlloc.Outbox --version 4.4.4
#r "nuget: ZeroAlloc.Outbox, 4.4.4"
#:package ZeroAlloc.Outbox@4.4.4
#addin nuget:?package=ZeroAlloc.Outbox&version=4.4.4
#tool nuget:?package=ZeroAlloc.Outbox&version=4.4.4
ZeroAlloc.Outbox
Source-generated transactional outbox for .NET. Annotate a message type with [OutboxMessage] and a Roslyn source generator emits a typed writer and dispatcher bridge — no reflection, no boxing, AOT-safe. Backed by ZeroAlloc.ORM or EF Core (production) or in-memory (tests), with a built-in polling worker, exponential-backoff retry, and dead-letter support.
Multiple packages in this family — see Documentation or NuGet for the full list.
Install
The source generator is bundled into the main package — a single PackageReference is all you need:
# Core abstractions + source generator (always required)
dotnet add package ZeroAlloc.Outbox
# Pick a store:
dotnet add package ZeroAlloc.Outbox.Orm # production — ZeroAlloc.ORM raw SQL, Native AOT (net10.0)
dotnet add package ZeroAlloc.Outbox.EfCore # production — Entity Framework Core
dotnet add package ZeroAlloc.Outbox.InMemory # testing — in-process, no database
# Optional bridges:
dotnet add package ZeroAlloc.Outbox.Mediator # dispatch through ZeroAlloc.Mediator IMediator.Publish
dotnet add package ZeroAlloc.Outbox.Resilience # wrap dispatchers in a ZeroAlloc.Resilience proxy
dotnet add package ZeroAlloc.Outbox.Telemetry # OpenTelemetry instrumentation around dispatch
See Mediator Integration, Resilience Bridge and OpenTelemetry integration.
The standalone
ZeroAlloc.Outbox.Generatorpackage is still published for backwards compatibility with existing direct PackageReferences, but new consumers should reference onlyZeroAlloc.Outbox.
Quick start
1. Annotate your message:
using ZeroAlloc.Outbox;
[OutboxMessage]
public sealed record OrderPlaced(int OrderId, decimal Amount);
The generator emits IOutboxWriter<OrderPlaced> and its DI registration extension.
2. Register with DI:
builder.Services.AddOutbox(options =>
{
options.PollingInterval = TimeSpan.FromSeconds(5);
options.BatchSize = 50;
options.MaxAttempts = 3;
options.LeaseDuration = TimeSpan.FromMinutes(2); // must exceed your slowest dispatch
})
.WithEfCore<AppDbContext>() // or .WithOrm() / .WithInMemoryStore()
.AddOrderPlacedOutbox(); // generated extension
3. Choose a serializer. AddOutbox() has no implicit serializer since 4.0; pick one:
// AOT-safe: annotate OrderPlaced with [ZeroAllocSerializable] (ZeroAlloc.Serialisation)
builder.Services.AddSerializerDispatcher();
// or reflection-based System.Text.Json, not trim- or AOT-safe:
builder.Services.AddOutbox().WithSystemTextJsonSerializer();
Without either, the host fails to start with an error that names both options. See AOT-Safe Serialisation.
4. Write in a transaction:
public class OrderService(IOutboxWriter<OrderPlaced> writer, AppDbContext db)
{
public async Task PlaceOrderAsync(Order order, CancellationToken ct)
{
db.Orders.Add(order);
await db.SaveChangesAsync(ct);
await writer.WriteAsync(new OrderPlaced(order.Id, order.Total), ct: ct);
}
}
For atomic writes (both or neither commit), pass the
DbTransactionexplicitly. See EF Core Transaction.
5. Implement a dispatcher:
public class OrderPlacedDispatcher(IMessageBus bus) : IOutboxDispatcher<OrderPlaced>
{
public async ValueTask DispatchAsync(OrderPlaced message, CancellationToken ct)
=> await bus.PublishAsync(message, ct);
}
// Register the dispatcher
builder.Services.AddTransient<IOutboxDispatcher<OrderPlaced>, OrderPlacedDispatcher>();
Dashboard
Operate the outbox at runtime: inspect pending / retry / dead-lettered / dispatched messages, watch a live throughput chart, and requeue or cancel individual messages.
Add the package, then register the event publisher and map the endpoints:
dotnet add package ZeroAlloc.Outbox.Dashboard
// Register the publisher (required for SSE live updates)
builder.Services.AddOutbox().WithDashboardEvents();
// Map the dashboard endpoints
app.MapOutboxDashboard("/outbox");
// Optional: protect with auth
app.MapOutboxDashboard("/outbox").RequireAuthorization("AdminPolicy");
The mapped root (/outbox) serves the HTML dashboard; REST endpoints (snapshot,
throughput, requeue, cancel, force-dispatch) and the SSE stream (events)
live under the same prefix.
Security
The dashboard exposes write actions (requeue, cancel, force-dispatch) as POST endpoints:
POST /outbox/api/messages/{id}/requeuePOST /outbox/api/messages/{id}/cancelPOST /outbox/api/messages/{id}/force-dispatch
Never mount the dashboard unauthenticated in a production environment. Always apply authentication/authorization:
app.MapOutboxDashboard("/outbox").RequireAuthorization("AdminPolicy");
The IEndpointConventionBuilder returned by MapOutboxDashboard supports all standard
ASP.NET Core auth middleware (RequireAuthorization, AllowAnonymous, route filters, etc.).
CSRF protection is the host application's responsibility — the dashboard does not emit or
validate anti-forgery tokens. If your authentication scheme is cookie-based, apply the
standard ASP.NET Core [ValidateAntiForgeryToken] or enable the antiforgery middleware
as appropriate.
What the dashboard shows
- Pending — messages awaiting their first dispatch attempt
- Retry queue — messages that have failed at least once and are scheduled for retry
- Dead-lettered — messages that exceeded
MaxAttempts, with the last failure reason - Dispatched — most-recently succeeded messages
- Throughput — SVG chart of dispatched + failed counts per minute
- Actions —
Requeuea dead-lettered message ·Cancela pending one ·Force dispatchto run it now
| Tab | Screenshot |
|---|---|
| Pending — queue of messages awaiting first dispatch | ![]() |
| Retry — failed messages with back-off schedule | ![]() |
| Dead-lettered — exhausted retries with last error | ![]() |
| Dispatched — recently-succeeded history feeding the throughput chart | ![]() |
The dashboard is fully responsive — tablet (768 × 1024) and mobile (375 × 812) captures live in docs/screenshots/.
Blazor component
For apps already using Blazor, ZeroAlloc.Outbox.Dashboard.Blazor ships an
<OutboxDashboard /> component that embeds the dashboard via iframe:
dotnet add package ZeroAlloc.Outbox.Dashboard.Blazor
@* In any Razor page / component *@
<OutboxDashboard BaseUrl="/outbox" />
You still need MapOutboxDashboard("/outbox") — the Blazor component is a thin wrapper
around the mapped endpoints.
Performance
Correctness-matched overhead vs a hand-rolled SQLite outbox (same connection, both transactional). .NET 10.0.7, i9-12900HK, BenchmarkDotNet v0.15.4.
| Operation | Hand-rolled | ZA.Outbox | Overhead |
|---|---|---|---|
| Enqueue (1 message) | 6.86 µs / 2.08 KB | 6.99 µs / 2.13 KB | +2% time, +2% alloc |
| Dispatch tick (10 messages) | 105.4 µs / 11.9 KB | 115.0 µs / 11.09 KB | +9% time, −7% alloc |
Near-zero abstraction overhead vs writing the same outbox by hand — the 2–9% delta is IOutboxWriter<T> + IOutboxStore interface dispatch. The value of ZA.Outbox is the [OutboxMessage] attribute + typed writer + ecosystem composability (resilience / telemetry / dispatcher bridges) at this cost.
Serializing a message for enqueue allocates only its exact-size payload array: 72 B for a small struct message, 64 B for a small class message, with the generated SerializerDispatcher. The ORM store persists that array without copying it. Reading a payload back for dispatch does not box a struct message: a MemoryPack struct dispatches with 0 B allocated.
Full methodology: docs/performance.md.
Features
| Feature | Notes |
|---|---|
| Source-generated writers | [OutboxMessage] triggers generator; typed IOutboxWriter<T> emitted at compile time |
| Typed dispatchers | IOutboxDispatcher<T> — implement once, wire to any transport (bus, HTTP, email) |
| ORM store | Raw SQL through ZeroAlloc.ORM on the caller's transaction; SQLite, PostgreSQL or SQL Server |
| EF Core store | Writes and reads via DbContext; enlist in ambient transaction for atomicity |
| InMemory store | Thread-safe in-process store for unit and integration tests |
| Polling worker | OutboxWorkerService (IHostedService) polls on configurable interval with scope isolation |
| Lease-based claim | ClaimPendingAsync atomically leases a batch to one host, so scaling out never double-dispatches a row |
| Exponential backoff | Retry delay = RetryBaseDelay × 2^(attempt-1); configurable via OutboxOptions |
| Dead-letter | Entries that exceed MaxAttempts are dead-lettered with the failure reason |
| AOT / trimmer safe | All dispatch code is generated; no Type.GetType, no MakeGenericType. AddOutbox() is trim- and AOT-safe; reflection JSON is an explicit opt-in |
IOptions<OutboxOptions> |
Configured through standard Microsoft.Extensions.Options; the worker reads its options once when it starts, so a change takes effect after a restart |
Diagnostics
| ID | Severity | Description |
|---|---|---|
| ZO0001 | Warning | [OutboxMessage] applied to an interface — code will not be generated |
| ZO0002 | Warning | [OutboxMessage] applied to a static class — code will not be generated |
| ZO0003 | Warning | [OutboxMessage] applied to a nested type — use a top-level type for a stable type discriminator |
| ZO0004 | Warning | Generic [OutboxMessage] type has no closed usage |
| ZO0005 | Error | Stored type name is longer than 256 characters |
| ZO0006 | Error | [OutboxMessage] declaration of a closed generic type is invalid |
| ZO0007 | Error | Outbox message would get the same generated names as another message |
Documentation
Full docs live in docs/:
- Getting Started
- Outbox Pattern
- Message Types
- Dispatchers
- Store Adapters
- Background Worker
- Dependency Injection
- Migrating to v3
- Migrating to v4
- Dashboard · Testing · Performance
- Cookbook: EF Core transaction, custom dispatcher, Mediator integration, dead-letter handling, testing with a host, AOT-safe serialisation, Resilience bridge, telemetry
- Diagnostics: ZO0001 · ZO0002 · ZO0003 · ZO0004 · ZO0005 · ZO0006 · ZO0007
License
MIT
| 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
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.12)
- Microsoft.Extensions.Hosting.Abstractions (>= 10.0.12)
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.12)
- Microsoft.Extensions.Options (>= 10.0.12)
- ZeroAlloc.Resilience (>= 3.3.3)
- ZeroAlloc.Serialisation (>= 2.8.1)
- ZeroAlloc.StateMachine (>= 1.6.3)
- ZeroAlloc.ValueObjects (>= 2.2.0)
-
net8.0
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.12)
- Microsoft.Extensions.Hosting.Abstractions (>= 10.0.12)
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.12)
- Microsoft.Extensions.Options (>= 10.0.12)
- ZeroAlloc.Resilience (>= 3.3.3)
- ZeroAlloc.Serialisation (>= 2.8.1)
- ZeroAlloc.StateMachine (>= 1.6.3)
- ZeroAlloc.ValueObjects (>= 2.2.0)
-
net9.0
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.12)
- Microsoft.Extensions.Hosting.Abstractions (>= 10.0.12)
- Microsoft.Extensions.Logging.Abstractions (>= 10.0.12)
- Microsoft.Extensions.Options (>= 10.0.12)
- ZeroAlloc.Resilience (>= 3.3.3)
- ZeroAlloc.Serialisation (>= 2.8.1)
- ZeroAlloc.StateMachine (>= 1.6.3)
- ZeroAlloc.ValueObjects (>= 2.2.0)
NuGet packages (12)
Showing the top 5 NuGet packages that depend on ZeroAlloc.Outbox:
| Package | Downloads |
|---|---|
|
ZeroAlloc.Scheduling.EfCore
EF Core job store adapter for ZeroAlloc.Scheduling. |
|
|
ZeroAlloc.Outbox.EfCore
EF Core store adapter for ZeroAlloc.Outbox. |
|
|
ZeroAlloc.Outbox.InMemory
In-memory store adapter for ZeroAlloc.Outbox (testing). |
|
|
ZeroAlloc.Outbox.Orm
ZeroAlloc.ORM store adapter for ZeroAlloc.Outbox. Raw-SQL persistence with compile-time parameter binding, transactional enqueue on the caller's transaction, and MigrationRunner-owned schema. Zero runtime reflection; Native AOT compatible. |
|
|
ZeroAlloc.Outbox.Telemetry
ZeroAlloc.Telemetry bridge for ZeroAlloc.Outbox — wraps IOutboxTypeDispatcher with a source-generated OpenTelemetry proxy. |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 4.4.4 | 0 | 10/15/2026 |
| 4.4.3 | 618 | 10/12/2026 |
| 4.4.2 | 458 | 10/11/2026 |
| 4.4.1 | 397 | 10/11/2026 |
| 4.4.0 | 992 | 10/10/2026 |
| 4.3.0 | 383 | 10/10/2026 |
| 4.2.0 | 1,244 | 9/30/2026 |
| 4.1.0 | 431 | 9/30/2026 |
| 4.0.1 | 563 | 9/28/2026 |
| 4.0.0 | 488 | 9/27/2026 |
| 3.0.1 | 1,166 | 9/26/2026 |
| 3.0.0 | 209 | 9/26/2026 |
| 2.7.2 | 472 | 9/25/2026 |
| 2.7.1 | 791 | 9/22/2026 |
| 2.7.0 | 197 | 9/22/2026 |
| 2.6.0 | 265 | 9/20/2026 |
| 2.5.5 | 218 | 9/20/2026 |
| 2.5.4 | 528 | 9/20/2026 |
| 2.5.3 | 824 | 9/19/2026 |
| 2.5.2 | 927 | 8/8/2026 |



