# Outbox The outbox stores events for publication after a state change commits. `IEventPublisher.PublishAsync` remains an in-process notification API. Outbox publication is explicit through `IOutboxWriter` or `IIntegrationOutbox`. ## Contract Use `IIntegrationOutbox` for events that cross a bounded-context or process boundary. ```csharp public sealed record OrderSubmittedIntegrationEvent : IIntegrationEvent { public required Guid OrderId { get; init; } } ``` ```csharp var receipt = await integrationOutbox.AddAsync( new OrderSubmittedIntegrationEvent { OrderId = orderId }, new OutboxOptions { MessageId = eventId, Topic = "orders", CorrelationId = correlationId }, cancellationToken); ``` `IOutboxWriter` accepts any non-null event type. `IIntegrationOutbox` narrows the API to `IIntegrationEvent` so external publication is opt-in at the type level. Use `OutboxOptions.MessageId` when the application already has a stable event id. v5 does not put that id on a marker interface; the event contract stays focused on domain data while the outbox envelope owns storage identity. Use `OutboxOptions.VisibleAfter` for delayed first publication. Retry visibility after failures is still controlled by the processor and store state. When `UseLiteBusEventDispatcher()` is registered, the dispatcher copies stored correlation, causation, and tenant values into `MessageTraceContextKeys` on `EventMediationSettings.Items` before `PublishAsync`. ## Registration Register contracts and an outbox store. ```csharp builder.Services.AddLiteBus(liteBus => { liteBus.AddOutboxModule(outbox => { outbox.Contracts.Register( name: "orders.events.order-submitted", version: 1); }); }); ``` The processor dispatches through `IOutboxDispatcher`. To dispatch through LiteBus event handlers in the current process, opt in to the LiteBus event dispatcher and register the event module. ```csharp builder.Services.AddLiteBus(liteBus => { liteBus.AddEventModule(events => { events.Register(); }); liteBus.AddOutboxModule(outbox => { outbox.Contracts.Register( "orders.events.order-submitted", version: 1); outbox.UseLiteBusEventDispatcher(); }); }); ``` External broker dispatchers can implement `IOutboxDispatcher` without changing the writer or processor. ## PostgreSQL Store The PostgreSQL package provides a raw Npgsql store. ```csharp var dataSource = NpgsqlDataSource.Create(connectionString); builder.Services.AddLiteBus(liteBus => { liteBus.AddOutboxModule(outbox => { outbox.Contracts.Register( "orders.events.order-submitted", version: 1); }); liteBus.AddPostgreSqlOutboxStore(postgres => { postgres.UseDataSource(dataSource); postgres.UseOptions(new PostgreSqlOutboxStoreOptions { SchemaName = "app", TableName = "litebus_outbox_messages" }); }); }); ``` When you do not already have an `NpgsqlDataSource`, pass a connection string and the module creates one for you: ```csharp liteBus.AddPostgreSqlOutboxStore(postgres => { postgres.UseConnectionString(configuration.GetConnectionString("OrdersDb")!); }); ``` See [PostgreSQL Schema Management](PostgreSQL-Schema-Management.md) for migration-owned SQL files, explicit bootstrap, and opt-in host schema creation. ```csharp // Option A: explicit bootstrap during startup or a deploy job await PostgreSqlOutboxSchema.EnsureAsync(dataSource, outboxOptions, cancellationToken); // Option B: opt-in host bootstrap (register AddPostgreSqlOutboxSchemaHosting before processor hosting) liteBus.AddPostgreSqlOutboxStore(postgres => { postgres.UseDataSource(dataSource); postgres.EnsureSchemaCreationOnStartup(); }); liteBus.AddPostgreSqlOutboxSchemaHosting(); ``` `CreateIfNotExistsAsync` remains available as an alias for `EnsureAsync`. The store uses `jsonb` payloads and `FOR UPDATE SKIP LOCKED` leases. It does not depend on EF Core. ## Processing Flow 1. Application code stores an event through `IOutboxWriter` or `IIntegrationOutbox` in the same transaction as the state change. 2. The writer resolves a stable contract name and version. 3. The writer serializes the event and stores an `OutboxMessageEnvelope`. 4. `IOutboxProcessor.ProcessPendingAsync` leases due messages. 5. The processor calls `IOutboxDispatcher.DispatchAsync` for each message. 6. The processor marks the message published, failed for retry, or dead-lettered. ## Store Roles | Interface | Used by | Responsibility | | --- | --- | --- | | `IOutboxMessageWriter` | `IOutboxWriter` | Append a pending outbox envelope and return the stored row | | `IOutboxMessageLeaseStore` | `IOutboxProcessor` | Atomically claim due messages for one publisher | | `IOutboxMessageStateStore` | `IOutboxProcessor` | Record published, failed, or dead-lettered publication results | The role split keeps event writers from depending on publisher-only operations. A single database implementation can still implement all roles when one table owns the transaction boundary. ## Generic Events Closed generic events are supported when each closed event type is registered with its own stable contract. ```csharp public sealed record ExportCompletedEvent : IIntegrationEvent { public required TPayload Payload { get; init; } } outbox.Contracts.Register>( "exports.events.customer-completed", version: 1); ``` Open generic outbox contracts are not supported. Do not register `ExportCompletedEvent<>` as a persisted contract because the processor or dispatcher must deserialize each row into one concrete CLR type. ## Storage Record | Field | Purpose | | --- | --- | | message id | Unique outbox message identity | | contract name | Stable persisted event name | | contract version | Payload version used by dispatchers | | serialized payload | Event data | | topic | Optional publication target | | created timestamp | Storage time | | visible-after timestamp | Retry visibility | | status | pending, publishing, published, failed, dead-lettered | | attempt count | Retry policy input | | lease owner | Publisher identity | | lease expiration | Crash recovery | | last error | Failure diagnostics | | correlation id | Trace grouping | | causation id | Parent operation tracking | | tenant id | Tenant isolation when needed | ## Retry and Dead Letter `OutboxProcessorOptions` uses the same `RetryOptions` type as the inbox. Failed messages are retried after a delay, then moved to `DeadLettered` when attempts are exhausted. ```csharp outbox.UseProcessorOptions(new OutboxProcessorOptions { BatchSize = 100, LeaseDuration = TimeSpan.FromMinutes(2), Retry = new RetryOptions { MaxAttempts = 12, InitialDelay = TimeSpan.FromSeconds(10), MaxDelay = TimeSpan.FromMinutes(10), Backoff = RetryBackoff.Exponential, UseJitter = true } }); ``` ## Boundary Rule The outbox is not an event mediator replacement. Use `PublishAsync` when the current process should notify handlers now. Use `IIntegrationOutbox.AddAsync` when a fact must survive process failure and be published after commit. ## Processor hosting `IOutboxProcessor.ProcessPendingAsync` runs one pass. For a generic-host background loop, reference `LiteBus.Outbox.Extensions.Microsoft.Hosting`: ```csharp liteBus.AddOutboxModule(outbox => { outbox.UseProcessorOptions(new OutboxProcessorOptions { BatchSize = 100 }); }); liteBus.AddOutboxProcessorHosting(host => host.PollInterval = TimeSpan.FromSeconds(1)); ``` Optional: `AddHealthChecks().AddLiteBusOutboxProcessor()`. Inbox and outbox hosts are registered separately. See [Processor Hosting](Processor-Hosting.md).