OrionInbox.EntityFrameworkCore 0.7.0

dotnet add package OrionInbox.EntityFrameworkCore --version 0.7.0
                    
NuGet\Install-Package OrionInbox.EntityFrameworkCore -Version 0.7.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="OrionInbox.EntityFrameworkCore" Version="0.7.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="OrionInbox.EntityFrameworkCore" Version="0.7.0" />
                    
Directory.Packages.props
<PackageReference Include="OrionInbox.EntityFrameworkCore" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add OrionInbox.EntityFrameworkCore --version 0.7.0
                    
#r "nuget: OrionInbox.EntityFrameworkCore, 0.7.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package OrionInbox.EntityFrameworkCore@0.7.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=OrionInbox.EntityFrameworkCore&version=0.7.0
                    
Install as a Cake Addin
#tool nuget:?package=OrionInbox.EntityFrameworkCore&version=0.7.0
                    
Install as a Cake Tool

<p align="center"> <img src="docs/logo.png" alt="OrionInbox" width="150" /> </p>

OrionInbox

CI/CD NuGet

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 atomic ProcessAsync, 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 Duplicate before 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

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 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. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

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
0.7.0 116 8/9/2026