# Add Ef Migration

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

- Skill: `cratis/add-ef-migration` (Agent Skill)
- Install (CLI): `npx skillmds@latest add cratis/add-ef-migration`
- Raw SKILL.md: https://api.skillmd.com/api/skills/cratis/add-ef-migration/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: Cratis (https://skillmd.com/u/cratis)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/cratis/add-ef-migration

---


Add or update an Entity Framework Core schema change with a hand-written migration.

> **Always read the [efcore.md](https://github.com/Cratis/AI/blob/main/.ai/rules/efcore.md) rule 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:

```
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

```csharp
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:

```csharp
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:

```csharp
namespace Database.Missions;

public class v1_1_0 : Migration
{
    /// <summary>
    /// Apply the schema change.
    /// </summary>
    /// <param name="migrationBuilder">The migration builder.</param>
    protected override void Up(MigrationBuilder migrationBuilder)
    {
        migrationBuilder.AddColumn<string>(
            name: "Description",
            table: WellKnownTables.Missions,
            type: migrationBuilder.StringColumnType(maxLength: 1000),
            nullable: true);
    }

    /// <summary>
    /// Reverse the schema change.
    /// </summary>
    /// <param name="migrationBuilder">The migration builder.</param>
    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`:

```csharp
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:

```csharp
services.AddReadModelDbContextsWithConnectionStringFromAssemblies(
    connectionString,
    configureOptions,
    [Assembly.GetExecutingAssembly()]);
```

Writable contexts need explicit registration:

```csharp
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>()`

