| name | add-ef-migration |
| description | Use this skill when asked to add a database table, column, relationship, or other schema change via Entity Framework Core in a Cratis-based project. |
Add or update an Entity Framework Core schema change with a hand-written migration.
Always read .github/instructions/efcore.instructions.md first. It is the source of truth for project structure, base types, column helpers, and migration conventions.
Pre-flight — Understand the project split
Before writing anything, locate these three projects:
| Project | What lives there |
|---|
| Database | Migrations only — WellKnownTables.cs + versioned migration files |
| Core | Entities and feature DbContexts (co-located with features) |
| Infrastructure | DbContext registration, migration runner, cross-cutting EF setup |
Critical: Database must NEVER import from Core. Migrations reference only WellKnownTables constants and EF migration types.
Step 1 — Update or create the entity
Add, rename, or remove properties on the entity record in the Core project, co-located with its feature:
Features/Missions/StartupPhase/
├── StartupPhase.cs ← entity record
├── StartupPhaseDbContext.cs ← feature DbContext
└── ...
Step 2 — Update or create the feature DbContext
Use the Cratis Arc base types — never inherit directly from DbContext:
ReadOnlyDbContext — for all read model / projection contexts (the vast majority)
BaseDbContext — only for writable contexts that own state
public class StartupPhaseDbContext(DbContextOptions<StartupPhaseDbContext> options)
: ReadOnlyDbContext(options)
{
public DbSet<StartupPhase> StartupPhases => Set<StartupPhase>();
}
Create one focused DbContext per feature — never a "god context" with unrelated entities.
Step 3 — Add the table name to WellKnownTables
If this is a new table, add the constant to Database/WellKnownTables.cs before writing the migration:
public static class WellKnownTables
{
public const string StartupPhases = "StartupPhases";
}
Step 4 — Write the migration by hand
Migrations are hand-written, not generated by dotnet ef migrations add. Place the file in the Database project using versioned naming:
Database/
├── Missions/
│ ├── v1_0_0.cs
│ └── v1_1_0.cs
└── WellKnownTables.cs
Use the v{major}_{minor}_{patch}.cs naming pattern. The namespace must match the folder:
namespace Database.Missions;
public class v1_1_0 : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "Description",
table: WellKnownTables.Missions,
type: migrationBuilder.StringColumnType(maxLength: 1000),
nullable: true);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "Description",
table: WellKnownTables.Missions);
}
}
Cross-database column helpers (mandatory)
Always use the Cratis Arc MigrationBuilder extension helpers — never raw table.Column<T>():
| Helper | Use for |
|---|
table.StringColumn(migrationBuilder) | Text / varchar columns |
table.GuidColumn(migrationBuilder) | UUID / GUID columns |
table.NumberColumn<T>(migrationBuilder) | Integer, long, or numeric columns |
table.DateTimeOffsetColumn(migrationBuilder) | Timestamps with timezone |
Example CreateTable:
migrationBuilder.CreateTable(
name: WellKnownTables.StartupPhases,
columns: table => new
{
Id = table.StringColumn(migrationBuilder, nullable: false),
Title = table.StringColumn(migrationBuilder, maxLength: 200, nullable: false),
ResourceId = table.NumberColumn<int>(migrationBuilder, nullable: true),
DispatchTime = table.DateTimeOffsetColumn(migrationBuilder),
UrgencyId = table.GuidColumn(migrationBuilder)
},
constraints: table =>
{
table.PrimaryKey("PK_StartupPhases", x => x.Id);
});
Step 5 — Register the DbContext (if new)
In the Infrastructure project, ensure the context is registered. Read-only contexts are auto-discovered:
services.AddReadModelDbContextsWithConnectionStringFromAssemblies(
connectionString,
configureOptions,
[Assembly.GetExecutingAssembly()]);
Writable contexts need explicit registration:
services.AddDbContextWithConnectionString<DeviceStateDbContext>(connectionString);
Step 6 — Update specs
Integration specs using in-memory SQLite pick up schema changes automatically via context.Database.EnsureCreated(). If specs break, check that the fixture uses the correct connection string and that the new migration is in the Database assembly.
Step 7 — Validate
Run dotnet build and dotnet test. Fix all failures before completing.
Key rules
- Never use
dotnet ef migrations add — all migrations are hand-written
- Never use
dotnet ef database update — use the custom runner: await app.ApplyAllMigrations(connectionString)
- Never hardcode a provider (
UseSqlite, UseNpgsql) — use UseDatabaseFromConnectionString
- Never mutate state directly through a DbContext — all writes flow through Chronicle events and projections
- Always use
WellKnownTables constants — never magic strings for table names
- Always use cross-database column helpers — never raw
table.Column<T>()