![]() |
VOOZH | about |
dotnet add package ZeroAlloc.Outbox.Telemetry --version 2.5.0
NuGet\Install-Package ZeroAlloc.Outbox.Telemetry -Version 2.5.0
<PackageReference Include="ZeroAlloc.Outbox.Telemetry" Version="2.5.0" />
<PackageVersion Include="ZeroAlloc.Outbox.Telemetry" Version="2.5.0" />Directory.Packages.props
<PackageReference Include="ZeroAlloc.Outbox.Telemetry" />Project file
paket add ZeroAlloc.Outbox.Telemetry --version 2.5.0
#r "nuget: ZeroAlloc.Outbox.Telemetry, 2.5.0"
#:package ZeroAlloc.Outbox.Telemetry@2.5.0
#addin nuget:?package=ZeroAlloc.Outbox.Telemetry&version=2.5.0Install as a Cake Addin
#tool nuget:?package=ZeroAlloc.Outbox.Telemetry&version=2.5.0Install as a Cake Tool
๐ NuGet
๐ Build
๐ AOT
๐ GitHub Sponsors
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.
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.
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>();
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.
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-dispatchNever 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
MaxAttempts, with the last failure reasonRequeue a dead-lettered message ยท Cancel a pending one ยท Force dispatch to run it now| Tab | Screenshot |
|---|---|
| Pending โ queue of messages awaiting first dispatch | ๐ Pending tab |
| Retry โ failed messages with back-off schedule | ๐ Retry tab |
| Dead-lettered โ exhausted retries with last error | ๐ Dead-lettered tab |
| Dispatched โ recently-succeeded history feeding the throughput chart | ๐ Dispatched tab |
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.
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.
| 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 |
| 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 |
Full docs live in docs/:
MIT
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net8.0 net8.0 is compatible. net8.0-android net8.0-android was computed. net8.0-browser net8.0-browser was computed. net8.0-ios net8.0-ios was computed. net8.0-maccatalyst net8.0-maccatalyst was computed. net8.0-macos net8.0-macos was computed. net8.0-tvos net8.0-tvos was computed. net8.0-windows net8.0-windows was computed. net9.0 net9.0 is compatible. net9.0-android net9.0-android was computed. net9.0-browser net9.0-browser was computed. net9.0-ios net9.0-ios was computed. net9.0-maccatalyst net9.0-maccatalyst was computed. net9.0-macos net9.0-macos was computed. net9.0-tvos net9.0-tvos was computed. net9.0-windows net9.0-windows was computed. net10.0 net10.0 is compatible. net10.0-android net10.0-android was computed. net10.0-browser net10.0-browser was computed. net10.0-ios net10.0-ios was computed. net10.0-maccatalyst net10.0-maccatalyst was computed. net10.0-macos net10.0-macos was computed. net10.0-tvos net10.0-tvos was computed. net10.0-windows net10.0-windows was computed. |
This package is not used by any NuGet packages.
This package is not used by any popular GitHub repositories.