EF Core outbox and inbox
This guide shows how to configure the EF Core outbox and inbox integrations for reliable publish and consumer-side idempotency.
What you get
- Outbox: writes outgoing messages to your
DbContexttransaction, then forwards them asynchronously - Inbox: deduplicates consumer processing by message ID and persists consumer-published messages safely
- FIFO partitioning: outbox groups are keyed by
interbus-message-group-id, with ordering guaranteed at least per group key
1. Add EF Core entities to your DbContext model
Call AddInterBusOutbox() in OnModelCreating.
public sealed class AppDbContext(DbContextOptions<AppDbContext> options) : DbContext(options)
{
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.AddInterBusOutbox();
}
}
2. Register InterBus with EF Core outbox
Use AddEfCore<TDbContext>() plus UseEfCoreOutbox<TDbContext>().
InterBus does not register your
DbContextfor you.
Register it normally (for exampleservices.AddDbContext<AppDbContext>(...)) before configuring InterBus EF Core integration.
services.AddDbContext<AppDbContext>(options =>
{
options.UseNpgsql(connectionString); // or UseSqlServer(...)
});
services.AddInterBus(bus =>
{
bus.AddInMemoryTransport(); // or your transport
bus.AddEfCore<AppDbContext>();
bus.UseEfCoreOutbox<AppDbContext>(outbox => outbox
.UsePostgreSql() // or UseSqlServer()
.WithShardCount(128)
.WithMaxConcurrentWorkers(8)
.WithEmptyPollDelay(TimeSpan.FromMilliseconds(50)));
});
3. Create and apply migrations
The EF outbox/inbox tables are part of your model once AddInterBusOutbox() is added.
dotnet ef migrations add AddInterBusOutbox
dotnet ef database update
Consumer-side inbox + outbox
To make a consumer idempotent and safely publish follow-up messages in the same durable flow:
services.AddInterBus(bus =>
{
bus.AddEfCore<AppDbContext>();
bus.AddConsumer<OrderPlacedConsumer>(c =>
c.UseEfCoreInboxOutbox<AppDbContext>());
});
This enables:
- inbox dedupe by
(consumer, message-id) - safe persistence of consumer-published outbox messages
- retry-friendly behavior if downstream delivery fails
Ordering model
- Outbox publish middleware partitions buffered groups by
InterBusHeaders.MessageGroupId - Shard assignment is deterministic from
message-group-id - Processing is FIFO within a shard, therefore at least FIFO for a given message group ID
- There is no global FIFO guarantee when multiple shards/workers are used
Lock providers and multi-instance processing
For multi-instance deployments, use a DB lock provider:
- PostgreSQL:
UsePostgreSql()(FOR UPDATE SKIP LOCKED) - SQL Server:
UseSqlServer()(UPDLOCK, READPAST)
The default EF lock provider is for single-instance/test use only.
Key options
| Option | Meaning |
|---|---|
WithShardCount(int) | Number of FIFO shards (default: 128). Higher values increase parallelism and reduce global ordering scope |
WithMaxConcurrentWorkers(int) | Number of outbox workers in this process |
WithEmptyPollDelay(TimeSpan) | Backoff delay when no claimable outbox work is found |
DisableOutboxProcessor() | Disable background forwarding (for specialized scenarios/tests) |
Operational notes
- Outbox delivery is at-least-once; consumers should remain idempotent
- Keep
MessageGroupIdstable for workflows that require in-order processing - Use database-native lock providers for production throughput and safe multi-processor behavior
ShardCount in productionChanging shard count changes hash partitioning immediately. That means the same MessageGroupId can map to a different shard after deployment, which can change throughput/fairness characteristics during rollout and may affect ordering expectations if old/new versions are mixed.
Treat shard-count changes as an operational migration:
- Prefer a coordinated rollout (avoid long mixed-version windows)
- Consider draining or reducing outbox backlog before the change
- Monitor throughput, lag, and consumer behavior closely after rollout