EF Core (PostgreSQL)
The Emit.Persistence.EntityFrameworkCore package provides EF Core-backed implementations of the outbox repository and distributed lock provider. PostgreSQL is the supported database.
Installation
dotnet add package Emit.Persistence.EntityFrameworkCoreRegistration
builder.Services.AddDbContextFactory<AppDbContext>(options => options.UseNpgsql(connectionString));
builder.Services.AddEmit(emit =>{ emit.AddEntityFrameworkCore<AppDbContext>(ef => { ef.UseNpgsql() .UseOutbox() .UseDistributedLock(); });});You register IDbContextFactory<TDbContext> yourself using the standard EF Core pattern. Emit resolves the factory internally to access the database without interfering with your application’s DbContext lifetimes.
UseNpgsql() is required: it configures PostgreSQL-specific type mappings. UseOutbox() and UseDistributedLock() are independent. When UseOutbox() is called, the background delivery worker starts and all producers route through the outbox by default. See Producers for how to opt out with UseDirect().
Model configuration
Add Emit’s entity model to your DbContext:
public class AppDbContext(DbContextOptions<AppDbContext> options) : DbContext(options){ protected override void OnModelCreating(ModelBuilder modelBuilder) { modelBuilder.AddEmitModel(emit => emit.UseNpgsql());
// your own model configuration }}AddEmitModel registers all Emit entity types with PostgreSQL-appropriate column types. Run a migration after adding it.
Migrations
Emit does not apply migrations automatically. Your migration history stays under your control:
dotnet ef migrations add AddEmitTablesdotnet ef database updateRun these after adding AddEmitModel to your OnModelCreating override. If you update to a new version of Emit that adds or changes schema, add another migration the same way.
Transactional outbox
EF Core supports two transaction approaches for coordinating outbox writes with your business data. For the full explanation, including the [Transactional] attribute and when to use each approach, see Transactional Outbox.
Implicit (recommended for most cases)
The outbox entry is tracked by the same DbContext as your business entities. A single SaveChangesAsync call flushes both atomically in one implicit database transaction. No transaction boilerplate needed:
public async Task HandleAsync(PlacePizzaOrder request, CancellationToken ct){ dbContext.PizzaOrders.Add(new PizzaOrder(request)); await producer.ProduceAsync( new EventMessage<string, PizzaOrdered>(request.PizzaId.ToString(), new PizzaOrdered(request)), ct); await dbContext.SaveChangesAsync(ct);}IUnitOfWork
For hosted services, background jobs, or anywhere you need explicit control over when to commit or roll back:
await using var tx = await unitOfWork.BeginAsync(ct);dbContext.PizzaOrders.Add(new PizzaOrder(request));await producer.ProduceAsync(..., ct);await tx.CommitAsync(ct);CommitAsync calls SaveChangesAsync internally before committing the database transaction. IUnitOfWork is registered automatically as a scoped service when UseOutbox() is called.
Lock cleanup
The EF Core provider includes a background worker that periodically deletes expired lock rows. It runs automatically alongside the distributed lock provider. The default cleanup interval is 5 minutes, configurable via LockCleanupOptions.