LiteBus
Integrations

Outbox Entity Framework Core Storage

LiteBus.Outbox.Storage.EntityFrameworkCore provides EfCoreOutboxStore, which implements IOutboxStore, IOutboxLeaseStore, and IOutboxStateWriter on a single DbContext.

For EF transactional enqueue (ITransactionalOutbox<TContext>), see Transactional messaging writes.

Schema Ownership

LiteBus does not create or upgrade outbox tables for EF Core. Your application owns migrations. Apply OutboxEntityFrameworkCoreModelExtensions.GetModelBuilderConfiguration() in OnModelCreating so columns and indexes match EfCoreOutboxStore.

For Npgsql outbox storage and shared PostgreSQL schema helpers, see PostgreSQL Schema Management.

Registration

services.AddDbContextFactory<AppDbContext>(options => options.UseNpgsql(connectionString));

builder.Services.AddLiteBus(builder =>
{
    builder.AddMessaging(_ => { });
    builder.AddOutbox(outbox =>
    {
        outbox.Contracts.Register<OrderSubmitted>("orders.events.submitted", 1);
        outbox.UseEntityFrameworkCoreStorage(options => options.UseDbContext<AppDbContext>());
        outbox.UseInProcessDispatch();
        outbox.EnableOutboxProcessor(host => host.PollInterval = TimeSpan.FromSeconds(1));
    });
});

AppDbContext must implement IOutboxDbContext and expose DbSet<OutboxMessageEntity> OutboxMessages. The adapter requires IDbContextFactory<AppDbContext> and owns one context per store operation.

Transactional Writes

The default IOutboxStore registration creates a context through IDbContextFactory<AppDbContext> and calls SaveChanges during EnqueueAsync. That commits outbox rows in a separate unit of work from your application DbContext unless you opt in to one of the patterns below.

Participate in the Caller DbContext

var store = serviceProvider.GetRequiredService<EfCoreOutboxStore>();
var transactionalStore = store.UseExistingDbContext(appDbContext);

await transactionalStore.EnqueueAsync(envelope, cancellationToken);
await appDbContext.SaveChangesAsync(cancellationToken);

UseExistingDbContext stages the outbox entity on the supplied context. The caller owns SaveChanges and any explicit transaction.

SaveChanges Interceptor

Register the interceptor in LiteBus and on your DbContext options:

builder.AddOutbox(outbox =>
{
    outbox.UseEntityFrameworkCoreStorage(storage =>
    {
        storage.UseDbContext<AppDbContext>();
        storage.EnableSaveChangesInterceptor();
    });
});

// In AddDbContext / OnConfiguring:
options.AddLiteBusOutboxInterceptor(
    serviceProvider.GetRequiredService<LiteBusOutboxSaveChangesInterceptor>());

Queue envelopes before SaveChanges:

interceptor.Enqueue(envelope);
await appDbContext.SaveChangesAsync(cancellationToken);

LiteBusOutboxSaveChangesInterceptor copies queued envelopes into IOutboxDbContext.OutboxMessages during SavingChanges, so domain entities and outbox rows share one EF transaction.

Default Table and Columns

SettingDefault
Schemapublic
Tablelitebus_outbox_messages

GetModelBuilderConfiguration() maps:

ColumnRole
message_idPrimary key (Guid, application-assigned)
contract_name, contract_versionStable message contract
payloadJSON serialized event (store type depends on provider; see below)
topicOptional routing hint for external dispatchers
created_at, visible_after, attempt_count, statusScheduling and lifecycle
lease_owner, lease_expires_atProcessor leasing
last_errorLast failure message
correlation_id, causation_id, tenant_idOptional metadata
idempotency_keyOptional deduplication scoped per tenant_id (unique composite index with IS NOT NULL filter)

Indexes: unique filtered index on (tenant_id, idempotency_key); composite index on (status, visible_after, lease_expires_at, created_at); chronological index IX_LiteBus_Outbox_CreatedAt; filtered index on topic where topic IS NOT NULL. Null or whitespace tenant_id values normalize to an empty string for idempotency scope.

Override names with EntityFrameworkCoreOutboxStoreOptions when needed.

Provider-Specific Model Configuration

Pass EfCoreStorageProvider to GetModelBuilderConfiguration() so payload and trace columns use the correct store types. LiteBus maps payload as TEXT for every EF provider because protected payloads are opaque strings. PostgreSQL maps trace_context as nullable jsonb when EfCoreStorageProvider.PostgreSql is supplied. Omitting the provider leaves both properties for application-owned column configuration.

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    var outboxOptions = new EntityFrameworkCoreOutboxStoreOptions();

    if (Database.IsNpgsql())
    {
        modelBuilder.GetModelBuilderConfiguration(outboxOptions, EfCoreStorageProvider.PostgreSql);
        return;
    }

    modelBuilder.GetModelBuilderConfiguration(outboxOptions);
}

See Inbox Entity Framework Core Storage for the full provider column-type table. SQL Server apps should set SchemaName = "dbo" in store options so raw lease SQL matches your migrations.

Supported leasing providers match inbox EF storage: PostgreSQL, SQL Server, MySQL (Pomelo), SQLite, and the in-memory provider for tests. MySQL uses READ COMMITTED, FOR UPDATE SKIP LOCKED, and IX_LiteBus_Outbox_CreatedAt for ordered concurrent claims. SQLite stores durable timestamps as UTC ticks and serializes lease writers through its database transaction. See the inbox guide for the full provider table and SQLite throughput constraint.

Align Store Options with Your EF Model

Pass the same EntityFrameworkCoreOutboxStoreOptions schema and table names to GetModelBuilderConfiguration() in OnModelCreating as you register through UseEntityFrameworkCoreStorage. See Inbox Entity Framework Core Storage for the same pitfall on inbox storage.

Processing Notes

EfCoreOutboxStore reloads rows after insert when the store normalizes JSON payloads. Leasing uses the same multi-provider skip-locked SQL as EfCoreInboxStore.

Register dispatchers before EnableOutboxProcessor().

Idempotency on the EF Transactional Path

EfCoreOutboxStore and ITransactionalOutbox<TContext> honor Idempotency.Keyed on enqueue metadata:

IdempotencyConflictModeBehavior on duplicate key in one transaction
ReturnExisting (default)Returns the existing receipt without inserting a second row.
StrictThrows IdempotencyConflictException before SaveChanges.

The LiteBusOutboxSaveChangesInterceptor path does not dedupe at enqueue time. A duplicate (tenant_id, idempotency_key) pair in the same SaveChanges batch surfaces as a DbUpdateException from the unique index and aborts the caller transaction. Use ReturnExisting on IOutbox / ITransactionalOutbox when you need silent dedupe; reserve the interceptor for envelopes staged without idempotency keys or when duplicate keys should fail the unit of work.

Alignment with PostgreSQL CurrentSchemaVersion

PostgreSqlOutboxSchema.CurrentSchemaVersion is 3. The EF model includes lease_generation for fencing, opaque payload text, and nullable trace_context. Pass EfCoreStorageProvider.PostgreSql to GetModelBuilderConfiguration() so the fluent model maps trace_context as optional jsonb. Add the new column in your application-owned EF migration before running the v6 store.

Add the column in your migration when upgrading from an older table that predates LiteBus v1 DDL:

migrationBuilder.AddColumn<string>(
    name: "trace_context",
    schema: "public",
    table: "litebus_outbox_messages",
    type: "jsonb",
    nullable: true);

On this page