Skip to content

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

Terminal window
dotnet add package Emit.Persistence.EntityFrameworkCore

Registration

Program.cs
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:

AppDbContext.cs
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:

Terminal window
dotnet ef migrations add AddEmitTables
dotnet ef database update

Run 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.

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.