{"id":49107088,"url":"https://github.com/zeroalloc-net/zeroalloc.outbox","last_synced_at":"2026-04-26T13:00:21.295Z","repository":{"id":352617897,"uuid":"1215884949","full_name":"ZeroAlloc-Net/ZeroAlloc.Outbox","owner":"ZeroAlloc-Net","description":"Source-generated transactional outbox for .NET — annotate a message type with [OutboxMessage] and get a typed writer and worker-backed dispatcher at compile time. No reflection, AOT-safe, EF Core and InMemory stores.","archived":false,"fork":false,"pushed_at":"2026-04-26T11:00:08.000Z","size":1063,"stargazers_count":0,"open_issues_count":1,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-04-26T13:00:17.594Z","etag":null,"topics":["csharp","dotnet","efcore","native-aot","outbox-pattern","roslyn","source-generator","zero-allocation"],"latest_commit_sha":null,"homepage":"https://outbox.zeroalloc.net","language":"C#","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/ZeroAlloc-Net.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-04-20T10:53:17.000Z","updated_at":"2026-04-26T11:00:02.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/ZeroAlloc-Net/ZeroAlloc.Outbox","commit_stats":null,"previous_names":["zeroalloc-net/zeroalloc.outbox"],"tags_count":1,"template":false,"template_full_name":null,"purl":"pkg:github/ZeroAlloc-Net/ZeroAlloc.Outbox","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ZeroAlloc-Net%2FZeroAlloc.Outbox","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ZeroAlloc-Net%2FZeroAlloc.Outbox/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ZeroAlloc-Net%2FZeroAlloc.Outbox/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ZeroAlloc-Net%2FZeroAlloc.Outbox/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/ZeroAlloc-Net","download_url":"https://codeload.github.com/ZeroAlloc-Net/ZeroAlloc.Outbox/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/ZeroAlloc-Net%2FZeroAlloc.Outbox/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":32297896,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-26T09:34:17.070Z","status":"ssl_error","status_checked_at":"2026-04-26T09:34:00.993Z","response_time":129,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["csharp","dotnet","efcore","native-aot","outbox-pattern","roslyn","source-generator","zero-allocation"],"created_at":"2026-04-21T02:13:21.715Z","updated_at":"2026-04-26T13:00:21.278Z","avatar_url":"https://github.com/ZeroAlloc-Net.png","language":"C#","readme":"# ZeroAlloc.Outbox\n\nSource-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.\n\n[![NuGet](https://img.shields.io/nuget/v/ZeroAlloc.Outbox.svg)](https://www.nuget.org/packages/ZeroAlloc.Outbox)\n[![NuGet](https://img.shields.io/nuget/v/ZeroAlloc.Outbox.Generator.svg)](https://www.nuget.org/packages/ZeroAlloc.Outbox.Generator)\n[![NuGet](https://img.shields.io/nuget/v/ZeroAlloc.Outbox.EfCore.svg)](https://www.nuget.org/packages/ZeroAlloc.Outbox.EfCore)\n[![NuGet](https://img.shields.io/nuget/v/ZeroAlloc.Outbox.InMemory.svg)](https://www.nuget.org/packages/ZeroAlloc.Outbox.InMemory)\n[![Build](https://github.com/ZeroAlloc-Net/ZeroAlloc.Outbox/actions/workflows/ci.yml/badge.svg)](https://github.com/ZeroAlloc-Net/ZeroAlloc.Outbox/actions/workflows/ci.yml)\n[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE)\n\n---\n\n## Install\n\n```bash\n# Core abstractions + source generator (always required)\ndotnet add package ZeroAlloc.Outbox\ndotnet add package ZeroAlloc.Outbox.Generator\n\n# Pick a store:\ndotnet add package ZeroAlloc.Outbox.EfCore    # production — Entity Framework Core\ndotnet add package ZeroAlloc.Outbox.InMemory  # testing — in-process, no database\n```\n\n---\n\n## Quick start\n\n**1. Annotate your message:**\n\n```csharp\nusing ZeroAlloc.Outbox;\n\n[OutboxMessage]\npublic sealed record OrderPlaced(int OrderId, decimal Amount);\n```\n\nThe generator emits `IOutboxWriter\u003cOrderPlaced\u003e` and its DI registration extension.\n\n**2. Register with DI:**\n\n```csharp\nbuilder.Services.AddOutbox(options =\u003e\n        {\n            options.PollingInterval = TimeSpan.FromSeconds(5);\n            options.BatchSize       = 50;\n            options.MaxAttempts     = 3;\n        })\n        .WithEfCore\u003cAppDbContext\u003e()      // or .WithInMemoryStore()\n        .AddOrderPlacedOutbox();         // generated extension\n```\n\n**3. Write in a transaction:**\n\n```csharp\npublic class OrderService(IOutboxWriter\u003cOrderPlaced\u003e writer, AppDbContext db)\n{\n    public async Task PlaceOrderAsync(Order order, CancellationToken ct)\n    {\n        db.Orders.Add(order);\n        await db.SaveChangesAsync(ct);\n        await writer.WriteAsync(new OrderPlaced(order.Id, order.Total), ct: ct);\n    }\n}\n```\n\n\u003e For atomic writes (both or neither commit), pass the `DbTransaction` explicitly. See [EF Core Transaction](docs/cookbook/01-ef-core-transaction.md).\n\n**4. Implement a dispatcher:**\n\n```csharp\npublic class OrderPlacedDispatcher(IMessageBus bus) : IOutboxDispatcher\u003cOrderPlaced\u003e\n{\n    public async Task DispatchAsync(OrderPlaced message, CancellationToken ct)\n        =\u003e await bus.PublishAsync(message, ct);\n}\n\n// Register the dispatcher\nbuilder.Services.AddTransient\u003cIOutboxDispatcher\u003cOrderPlaced\u003e, OrderPlacedDispatcher\u003e();\n```\n\n---\n\n## Dashboard\n\nOperate the outbox at runtime: inspect pending / retry / dead-lettered / dispatched\nmessages, watch a live throughput chart, and requeue or cancel individual messages.\n\nAdd the package, then register the event publisher and map the endpoints:\n\n```bash\ndotnet add package ZeroAlloc.Outbox.Dashboard\n```\n\n```csharp\n// Register the publisher (required for SSE live updates)\nbuilder.Services.AddOutbox().WithDashboardEvents();\n\n// Map the dashboard endpoints\napp.MapOutboxDashboard(\"/outbox\");\n\n// Optional: protect with auth\napp.MapOutboxDashboard(\"/outbox\").RequireAuthorization(\"AdminPolicy\");\n```\n\nThe mapped root (`/outbox`) serves the HTML dashboard; REST endpoints (`snapshot`,\n`throughput`, `requeue`, `cancel`, `force-dispatch`) and the SSE stream (`events`)\nlive under the same prefix.\n\n### Security\n\nThe dashboard exposes **write actions** (requeue, cancel, force-dispatch) as `POST` endpoints:\n\n- `POST /outbox/api/messages/{id}/requeue`\n- `POST /outbox/api/messages/{id}/cancel`\n- `POST /outbox/api/messages/{id}/force-dispatch`\n\n**Never mount the dashboard unauthenticated in a production environment.** Always apply\nauthentication/authorization:\n\n```csharp\napp.MapOutboxDashboard(\"/outbox\").RequireAuthorization(\"AdminPolicy\");\n```\n\nThe `IEndpointConventionBuilder` returned by `MapOutboxDashboard` supports all standard\nASP.NET Core auth middleware (`RequireAuthorization`, `AllowAnonymous`, route filters, etc.).\n\nCSRF protection is the host application's responsibility — the dashboard does not emit or\nvalidate anti-forgery tokens. If your authentication scheme is cookie-based, apply the\nstandard ASP.NET Core `[ValidateAntiForgeryToken]` or enable the antiforgery middleware\nas appropriate.\n\n**What the dashboard shows**\n\n- **Pending** — messages awaiting their first dispatch attempt\n- **Retry queue** — messages that have failed at least once and are scheduled for retry\n- **Dead-lettered** — messages that exceeded `MaxAttempts`, with the last failure reason\n- **Dispatched** — most-recently succeeded messages\n- **Throughput** — SVG chart of dispatched + failed counts per minute\n- **Actions** — `Requeue` a dead-lettered message · `Cancel` a pending one · `Force dispatch` to run it now\n\n| Tab | Screenshot |\n|-----|------------|\n| Pending — queue of messages awaiting first dispatch | ![Pending tab](docs/screenshots/pending-desktop.png) |\n| Retry — failed messages with back-off schedule | ![Retry tab](docs/screenshots/retry-desktop.png) |\n| Dead-lettered — exhausted retries with last error | ![Dead-lettered tab](docs/screenshots/dead-desktop.png) |\n| Dispatched — recently-succeeded history feeding the throughput chart | ![Dispatched tab](docs/screenshots/dispatched-desktop.png) |\n\nThe dashboard is fully responsive — tablet (768 × 1024) and mobile (375 × 812) captures live in [`docs/screenshots/`](docs/screenshots/).\n\n**Blazor component**\n\nFor apps already using Blazor, `ZeroAlloc.Outbox.Dashboard.Blazor` ships an\n`\u003cOutboxDashboard /\u003e` component that embeds the dashboard via `iframe`:\n\n```bash\ndotnet add package ZeroAlloc.Outbox.Dashboard.Blazor\n```\n\n```razor\n@* In any Razor page / component *@\n\u003cOutboxDashboard BaseUrl=\"/outbox\" /\u003e\n```\n\nYou still need `MapOutboxDashboard(\"/outbox\")` — the Blazor component is a thin wrapper\naround the mapped endpoints.\n\n---\n\n## Features\n\n| Feature | Notes |\n|---------|-------|\n| Source-generated writers | `[OutboxMessage]` triggers generator; typed `IOutboxWriter\u003cT\u003e` emitted at compile time |\n| Typed dispatchers | `IOutboxDispatcher\u003cT\u003e` — implement once, wire to any transport (bus, HTTP, email) |\n| EF Core store | Writes and reads via `DbContext`; enlist in ambient transaction for atomicity |\n| InMemory store | Thread-safe in-process store for unit and integration tests |\n| Polling worker | `OutboxWorkerService` (`IHostedService`) polls on configurable interval with scope isolation |\n| Exponential backoff | Retry delay = `RetryBaseDelay × 2^(attempt-1)`; configurable via `OutboxOptions` |\n| Dead-letter | Entries that exceed `MaxAttempts` are dead-lettered with the failure reason |\n| AOT / trimmer safe | All dispatch code is generated; no `Type.GetType`, no `MakeGenericType` |\n| `IOptions\u003cOutboxOptions\u003e` | Full options support with hot-reload via standard `Microsoft.Extensions.Options` |\n\n---\n\n## Diagnostics\n\n| ID | Severity | Description |\n|----|----------|-------------|\n| [ZO0001](docs/diagnostics/ZO0001.md) | Warning | `[OutboxMessage]` applied to an interface — code will not be generated |\n| [ZO0002](docs/diagnostics/ZO0002.md) | Warning | `[OutboxMessage]` applied to a static class — code will not be generated |\n| [ZO0003](docs/diagnostics/ZO0003.md) | Warning | `[OutboxMessage]` applied to a nested type — use a top-level type for a stable type discriminator |\n\n---\n\n## Documentation\n\nFull docs live in [`docs/`](docs/index.md):\n\n- [Getting Started](docs/getting-started.md)\n- [Outbox Pattern](docs/outbox-pattern.md)\n- [Message Types](docs/message-types.md)\n- [Dispatchers](docs/dispatchers.md)\n- [Store Adapters](docs/store-adapters.md)\n- [Background Worker](docs/background-worker.md)\n- [Dependency Injection](docs/dependency-injection.md)\n- Diagnostics: [ZO0001](docs/diagnostics/ZO0001.md) · [ZO0002](docs/diagnostics/ZO0002.md) · [ZO0003](docs/diagnostics/ZO0003.md)\n\n---\n\n## License\n\nMIT\n","funding_links":[],"categories":[],"sub_categories":[],"project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fzeroalloc-net%2Fzeroalloc.outbox","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fzeroalloc-net%2Fzeroalloc.outbox","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fzeroalloc-net%2Fzeroalloc.outbox/lists"}