ZeroAlloc.Outbox.EfCore
2.5.0
dotnet add package ZeroAlloc.Outbox.EfCore --version 2.5.0
NuGet\Install-Package ZeroAlloc.Outbox.EfCore -Version 2.5.0
<PackageReference Include="ZeroAlloc.Outbox.EfCore" Version="2.5.0" />
<PackageVersion Include="ZeroAlloc.Outbox.EfCore" Version="2.5.0" />
<PackageReference Include="ZeroAlloc.Outbox.EfCore" />
paket add ZeroAlloc.Outbox.EfCore --version 2.5.0
#r "nuget: ZeroAlloc.Outbox.EfCore, 2.5.0"
#:package ZeroAlloc.Outbox.EfCore@2.5.0
#addin nuget:?package=ZeroAlloc.Outbox.EfCore&version=2.5.0
#tool nuget:?package=ZeroAlloc.Outbox.EfCore&version=2.5.0
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 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.EfCore # production — Entity Framework Core
dotnet add package ZeroAlloc.Outbox.InMemory # testing — in-process, no database
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;
})
.WithEfCore<AppDbContext>() // or .WithInMemoryStore()
.AddOrderPlacedOutbox(); // generated extension
3. 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.
4. Implement a dispatcher:
public class OrderPlacedDispatcher(IMessageBus bus) : IOutboxDispatcher<OrderPlaced>
{
public async Task 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.
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) |
| 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 |
| 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 |
IOptions<OutboxOptions> |
Full options support with hot-reload via standard Microsoft.Extensions.Options |
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 |
Documentation
Full docs live in docs/:
- Getting Started
- Outbox Pattern
- Message Types
- Dispatchers
- Store Adapters
- Background Worker
- Dependency Injection
- Diagnostics: ZO0001 · ZO0002 · ZO0003
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.EntityFrameworkCore (>= 9.0.4)
- Microsoft.EntityFrameworkCore.Relational (>= 9.0.4)
- ZeroAlloc.Outbox (>= 2.5.0)
- ZeroAlloc.ValueObjects.EfCore (>= 1.3.1)
-
net8.0
- Microsoft.EntityFrameworkCore (>= 9.0.4)
- Microsoft.EntityFrameworkCore.Relational (>= 9.0.4)
- ZeroAlloc.Outbox (>= 2.5.0)
- ZeroAlloc.ValueObjects.EfCore (>= 1.3.1)
-
net9.0
- Microsoft.EntityFrameworkCore (>= 9.0.4)
- Microsoft.EntityFrameworkCore.Relational (>= 9.0.4)
- ZeroAlloc.Outbox (>= 2.5.0)
- ZeroAlloc.ValueObjects.EfCore (>= 1.3.1)
NuGet packages
This package is not used by any NuGet packages.
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 2.5.0 | 35 | 5/14/2026 |
| 2.4.1 | 103 | 5/12/2026 |
| 2.4.0 | 369 | 5/4/2026 |
| 2.3.1 | 87 | 5/3/2026 |
| 2.3.0 | 91 | 5/1/2026 |
| 2.2.1 | 92 | 4/28/2026 |
| 2.2.0 | 92 | 4/28/2026 |
| 2.1.1 | 96 | 4/28/2026 |
| 2.1.0 | 95 | 4/26/2026 |
| 2.0.0 | 92 | 4/25/2026 |
| 1.3.0 | 92 | 4/25/2026 |
| 1.2.2 | 101 | 4/25/2026 |
| 1.2.1 | 99 | 4/24/2026 |
| 1.2.0 | 95 | 4/24/2026 |
| 1.1.2 | 102 | 4/23/2026 |
| 1.1.1 | 104 | 4/23/2026 |
| 1.1.0 | 99 | 4/22/2026 |
| 1.0.1 | 95 | 4/22/2026 |
| 1.0.0 | 104 | 4/20/2026 |
| 0.1.0 | 94 | 4/20/2026 |



