OrionInbox 0.7.0
dotnet add package OrionInbox --version 0.7.0
NuGet\Install-Package OrionInbox -Version 0.7.0
<PackageReference Include="OrionInbox" Version="0.7.0" />
<PackageVersion Include="OrionInbox" Version="0.7.0" />
<PackageReference Include="OrionInbox" />
paket add OrionInbox --version 0.7.0
#r "nuget: OrionInbox, 0.7.0"
#:package OrionInbox@0.7.0
#addin nuget:?package=OrionInbox&version=0.7.0
#tool nuget:?package=OrionInbox&version=0.7.0
<p align="center"> <img src="docs/logo.png" alt="OrionInbox" width="150" /> </p>
OrionInbox
The consumer-side other half of OrionPatch: a transactional inbox that makes a message's effects exactly-once. An at-least-once broker plus an idempotent inbox equals a message that lands once.
"At-least-once" is the honest guarantee a broker offers — retries, redeliveries, and consumer restarts all mean a handler will eventually see the same message twice. That is fine for the transport and fatal for the effect: charge the card twice, send two shipping emails, double-apply a ledger entry. The fix is an inbox: record each message id, and inside the same transaction that runs the handler, refuse to process an id already committed. Hand-rolling it is where teams get it subtly wrong — they dedup after the side effect instead of atomically with it, store the processed-id in a different transaction than the business write, never prune the table, or mishandle two concurrent deliveries of the same id.
OrionInbox is the disciplined version: the dedup-row insert and the handler's writes commit together, or not at all. It is transport-agnostic and framework-free — it needs your DbContext and a message id, nothing more — so it drops into a broker consumer, a webhook receiver, or a polling worker identically.
The outbox → inbox loop
Producer service Broker (at-least-once) Consumer service
──────────────── ────────────────────── ────────────────
write + OrionPatch.Enqueue ──event──▶ (may redeliver) OrionInbox.ProcessAsync(messageId)
(state + outbox row: one txn) ├─ id seen? ─▶ yes ─▶ skip, ack (Duplicate)
│ └─ no ─▶ [ handler writes + dedup row ] one txn ─▶ ack
▼
OrionPatch dispatcher (≥1) ─────────────────────────────▶ duplicate deliveries absorbed here
Outbox (send ≥1) ∘ Inbox (effect ≤1) = exactly-once effects, end to end — the guarantee neither half gives alone.
Packages
OrionInbox— the framework-free core:IInboxHandler<T>,InboxMessage<T>,InboxResult,InboxOptions, and OpenTelemetry. AOT- and trim-clean.OrionInbox.EntityFrameworkCore— the EF Core store: the dedup table, the atomicProcessAsync,AddOrionInbox<TDbContext>wiring, and a background prune.
Install
dotnet add package OrionInbox.EntityFrameworkCore
Quick start
Map the dedup table onto your context, alongside your domain tables:
using Moongazing.OrionInbox.EntityFrameworkCore;
public sealed class AppDbContext : DbContext
{
public DbSet<Receipt> Receipts => Set<Receipt>();
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
// ... your entities ...
modelBuilder.ApplyOrionInboxConfiguration(); // adds the OrionInbox_Messages dedup table
}
}
Declare a handler. It adds its writes and returns — it must not call SaveChanges; the inbox owns the transaction:
using Moongazing.OrionInbox;
public sealed class OrderPaidHandler : IInboxHandler<OrderPaid>
{
private readonly AppDbContext _db;
public OrderPaidHandler(AppDbContext db) => _db = db;
public Task HandleAsync(InboxMessage<OrderPaid> msg, CancellationToken ct)
{
_db.Receipts.Add(new Receipt(msg.Payload.OrderId, msg.Payload.Amount));
return Task.CompletedTask; // no SaveChanges — the dedup row + these writes commit together
}
}
Wire it up:
services.AddDbContext<AppDbContext>(o => o.UseNpgsql(connectionString));
services.AddOrionInbox<AppDbContext>(o =>
{
o.DedupWindow = TimeSpan.FromDays(7); // how long a message id is remembered
o.PruneInterval = TimeSpan.FromHours(1); // background cleanup, off OrionClock
});
services.AddInboxHandler<OrderPaid, OrderPaidHandler>();
Process at the transport edge — a broker consumer, a webhook receiver, a polling worker. Run each delivery in its own DI scope (so the processor and handler share one DbContext):
using var scope = provider.CreateScope();
var inbox = scope.ServiceProvider.GetRequiredService<IOrionInbox>();
InboxResult result = await inbox.ProcessAsync(envelope.MessageId, payload, ct);
// First delivery : handler ran, dedup row + effects committed together -> Processed.
// Redelivery : handler skipped, no effect -> Duplicate.
// Either way, acknowledge the message to the broker.
What it guarantees
- Atomic dedup + effect. The dedup row and the handler's writes commit in one transaction. A crash between them is impossible: either both are durable or neither is, so a redelivery retries cleanly.
- Concurrency-safe. Two simultaneous deliveries of the same id race on the unique key; exactly one wins and runs the handler, the other returns
Duplicatebefore its handler runs. (Verified by a test that fires the same id 100× concurrently and asserts exactly one effect row and 99 duplicates.) - Bounded growth. A background service prunes dedup rows past
DedupWindow, in batches, on the family clock — so a fake clock fast-forwards retention in tests. - Provider-agnostic. Works on any relational EF Core provider; duplicate detection re-queries existence rather than parsing provider-specific error codes.
Observability
A Moongazing.OrionInbox meter and activity source: orion.inbox.processed, orion.inbox.duplicate, and orion.inbox.pruned, plus a per-delivery OrionInbox.process span. Built on the family's OrionInstrumentation spine, so multi-tenant / multi-region labels stamp every measurement.
Testing
Because retention runs on OrionClock, a FakeOrionClock fast-forwards the dedup window with no real waiting. The dedup and concurrency behaviour are exercised against a real relational store (SQLite) so the unique-constraint guarantee is genuinely tested, not mocked away.
Roadmap
Wave 1 (this release) ships the EF Core inbox store, the atomic dedup+effect ProcessAsync, concurrency safety, and background pruning. Later waves add the documented exactly-once loop with OrionPatch (a bridge package wiring message ids end to end), poison-message dead-lettering after a retry budget, a webhook-receiver inbox with a minimal-API filter, source-generated handler registration, and an optional Redis dedup store. See CHANGELOG.md.
OrionInbox is not a broker or transport (bring your own), does not provide exactly-once delivery (physically impossible — it provides exactly-once effects), and does not make a non-transactional external side effect idempotent for you.
Versioning
Follows Semantic Versioning. Multi-targets net8.0, net9.0, and net10.0. Binds to Orion.Abstractions 1.x and OrionClock 0.9.x; the EF Core store requires EF Core 8+. The framework-free core is AOT- and trim-clean (verified by a native-binary smoke test in CI); the EF Core store is not NativeAOT-published because EF Core itself is not AOT-clean.
Documentation
- CHANGELOG.md — release notes.
Contributing
Contributions are welcome. See CONTRIBUTING.md and the CODE_OF_CONDUCT.md.
More from the Orion family
Focused .NET libraries built to one quality bar. Each is usable on its own; several share the small Orion.Abstractions contracts spine, but there is no deep dependency web — pick only what you need:
- Orion.Abstractions — the shared contracts spine: telemetry, options, result, clock
- OrionClock — a
TimeProvider-based clock with TTL / deadline vocabulary - OrionPatch — transactional outbox for EF Core (the producing half of this loop)
- OrionResilience — retry, backoff, and timeout on OrionClock
- OrionGuard — validation, guard clauses, DDD primitives, domain events
- OrionAudit — automatic EF Core change-audit trail
- OrionBeacon — leader election with fencing tokens
- OrionGrant — permission / authorization checks
- OrionKey — source-generated strongly-typed IDs
- OrionLedger — API-key issuance, verification, and rotation
- OrionLens — ambient correlation-context propagation
- OrionLock — distributed locks with fencing tokens
- OrionOnce — idempotency keys for exactly-once request handling
- OrionRelay — outbound webhook delivery (HMAC, retries, backoff)
- OrionResult — Result/Option types and a shared error vocabulary
- OrionSaga — sagas / process managers for long-running workflows
- OrionShade — sensitive-data redaction for logs and telemetry
- OrionStream — server-sent events / streaming hub
- OrionVault — field-level encryption for EF Core
See it all working together in OrionShowcase, a production-shaped banking sample.
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 (>= 9.0.0)
- Microsoft.Extensions.Options (>= 9.0.0)
- Orion.Abstractions (>= 1.2.0)
- OrionClock (>= 0.9.0)
-
net8.0
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 9.0.0)
- Microsoft.Extensions.Options (>= 9.0.0)
- Orion.Abstractions (>= 1.2.0)
- OrionClock (>= 0.9.0)
-
net9.0
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 9.0.0)
- Microsoft.Extensions.Options (>= 9.0.0)
- Orion.Abstractions (>= 1.2.0)
- OrionClock (>= 0.9.0)
NuGet packages (1)
Showing the top 1 NuGet packages that depend on OrionInbox:
| Package | Downloads |
|---|---|
|
OrionInbox.EntityFrameworkCore
EF Core storage for OrionInbox. Adds the OrionInbox_Messages dedup table and an atomic ProcessAsync that inserts the dedup row and runs the handler's writes in one transaction, keyed by a unique constraint so concurrent redeliveries collapse to a single effect. Includes AddOrionInbox<TDbContext> wiring and a background prune of expired dedup rows off OrionClock. |
GitHub repositories
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 0.7.0 | 126 | 8/9/2026 |