Move a domain data context to EF migrations
Source DomainContext/
A DomainDataContext takes ordinary EF Core migrations. Trax's own tables are migrated separately, by UsePostgres during service registration and tracked in trax.migrations, so the two never touch each other's history. This page assumes a context set up as in Add a domain data context.
1. Add the EF tools
The design package goes in the project that holds the context, and the dotnet-ef tool at the same EF Core version as the rest of the app:
dotnet add package Microsoft.EntityFrameworkCore.Design
dotnet new tool-manifest
dotnet tool install dotnet-ef2. Keep the provider options in one place
The app and the tools must agree on them. Put the migrations history table in the domain's own schema; without this it lands in public."__EFMigrationsHistory", shared by every context that does the same:
using Npgsql.EntityFrameworkCore.PostgreSQL.Infrastructure;
public static class ShopDatabase
{
public static void Npgsql(NpgsqlDbContextOptionsBuilder npgsql) =>
npgsql.MigrationsHistoryTable("__EFMigrationsHistory", "shop");
}3. Add a design-time factory
dotnet ef builds the context from it instead of starting your host, which would run Trax's migrations and the rest of Program.cs. The base's sealed OnModelCreating is no obstacle: the tools build the model through it, schema and UTC converter included, so the migrations carry EnsureSchema("shop") and every table in that schema.
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;
public class ShopDbContextFactory : IDesignTimeDbContextFactory<ShopDbContext>
{
public ShopDbContext CreateDbContext(string[] args) =>
new(new DbContextOptionsBuilder<ShopDbContext>()
.UseNpgsql("Host=localhost;Database=shop;Username=app;Password=app", ShopDatabase.Npgsql)
.Options);
}dotnet ef migrations add builds the model offline and does not connect to that database.
4. Apply migrations at startup
Apply them at startup, or from a deployment step, in place of EnsureSchemaCreatedAsync:
using Microsoft.EntityFrameworkCore;
using Trax.Effect.Data.Postgres.Extensions;
using Trax.Effect.Data.Services.DomainContext;
using Trax.Effect.Extensions;
builder.Services.AddDomainDataContext<IShopDbContext, ShopDbContext>(o =>
o.UseNpgsql(connectionString, ShopDatabase.Npgsql));
builder.Services.AddTrax(trax => trax.AddEffects(e => e.UsePostgres(connectionString)));
var app = builder.Build();
await using (var scope = app.Services.CreateAsyncScope())
{
var factory = scope.ServiceProvider.GetRequiredService<IDbContextFactory<ShopDbContext>>();
await using var db = await factory.CreateDbContextAsync();
await db.Database.MigrateAsync();
}5. Generate the first migration
dotnet ef migrations add Initial -o Data/MigrationsOn a fresh database the first MigrateAsync logs one Error from EF Core, a failed SELECT ... FROM shop."__EFMigrationsHistory" before it creates that table; later starts log none. MigrateAsync refuses to start the host (PendingModelChangesWarning) when the model has changed without a migration. The NoPendingModelChanges architecture guard catches that in CI: list the context in MigrationContexts.
A database EnsureSchemaCreatedAsync already created
It has the tables and no history, so the first MigrateAsync fails with 42P07: relation "Orders" already exists. Generate Initial from the model exactly as deployed, then record it as applied once, before the new code starts:
CREATE TABLE IF NOT EXISTS shop."__EFMigrationsHistory" (
"MigrationId" character varying(150) NOT NULL PRIMARY KEY,
"ProductVersion" character varying(32) NOT NULL);
INSERT INTO shop."__EFMigrationsHistory" VALUES ('20261003182101_Initial', '10.0.12');Use the migration's full id (its file name) and your EF Core version. Every later migration then applies normally.