# EF Core Migrations Standards

> **Scope:** blazor-azure,nextjs-supabase
> **Layer:** 1 (on domain)
> **Keywords:** migration, schema, database update, ef migrations
> **Load When:** database work

Migration naming, creation, and deployment best practices

---

## Migration Naming

### Format

```
{Timestamp}_{DescriptiveAction}

Examples:
20260216103000_CreateUserTable
20260216104500_AddEmailIndexToUsers
20260216110000_AlterOrdersAddTotalColumn
```

### Naming Convention

- Use PascalCase
- Start with action verb: `Create`, `Add`, `Alter`, `Remove`, `Update`
- Be specific about what changed
- Keep under 50 characters

```bash
# ✅ GOOD
dotnet ef migrations add CreateUserTable
dotnet ef migrations add AddEmailIndexToUsers
dotnet ef migrations add AlterOrdersAddStatusColumn

# ❌ BAD
dotnet ef migrations add Update
dotnet ef migrations add Changes
dotnet ef migrations add Fix
```

---

## Creating Migrations

### Command

```bash
# Create migration
dotnet ef migrations add CreateUserTable --project Infrastructure --startup-project Web

# Create migration with output folder
dotnet ef migrations add AddOrderEntity --output-dir Data/Migrations
```

### Before Creating Migration

1. **Build project** - Ensure no compilation errors
2. **Review model changes** - Verify entity changes are correct
3. **Check DbContext** - Ensure DbSet and OnModelCreating are updated

---

## Migration Content

### Up Method (Apply Changes)

```csharp
protected override void Up(MigrationBuilder migrationBuilder)
{
    // Create table
    migrationBuilder.CreateTable(
        name: "Users",
        columns: table => new
        {
            Id = table.Column<int>(nullable: false)
                .Annotation("SqlServer:Identity", "1, 1"),
            Email = table.Column<string>(maxLength: 256, nullable: false),
            PasswordHash = table.Column<string>(nullable: false),
            CreatedAt = table.Column<DateTime>(nullable: false, defaultValueSql: "GETUTCDATE()")
        },
        constraints: table =>
        {
            table.PrimaryKey("PK_Users", x => x.Id);
        });

    // Create index
    migrationBuilder.CreateIndex(
        name: "IX_Users_Email",
        table: "Users",
        column: "Email",
        unique: true);
}
```

### Down Method (Rollback Changes)

```csharp
protected override void Down(MigrationBuilder migrationBuilder)
{
    // Drop in reverse order
    migrationBuilder.DropIndex(
        name: "IX_Users_Email",
        table: "Users");

    migrationBuilder.DropTable(
        name: "Users");
}
```

---

## Data Seeding in Migrations

### Seed Required Data

```csharp
protected override void Up(MigrationBuilder migrationBuilder)
{
    // Create table
    migrationBuilder.CreateTable(...);

    // Seed data
    migrationBuilder.InsertData(
        table: "Roles",
        columns: new[] { "Id", "Name" },
        values: new object[,]
        {
            { 1, "Admin" },
            { 2, "User" }
        });
}

protected override void Down(MigrationBuilder migrationBuilder)
{
    // Delete data
    migrationBuilder.DeleteData(
        table: "Roles",
        keyColumn: "Id",
        keyValues: new object[] { 1, 2 });

    migrationBuilder.DropTable("Roles");
}
```

---

## Applying Migrations

### Development

```bash
# Apply all pending migrations
dotnet ef database update

# Apply to specific migration
dotnet ef database update CreateUserTable

# Rollback to previous migration
dotnet ef database update PreviousMigrationName

# Rollback all migrations
dotnet ef database update 0
```

### Production

**Option 1: SQL Script (Recommended)**

```bash
# Generate SQL script for all pending migrations
dotnet ef migrations script --output migrate.sql

# Generate SQL from specific range
dotnet ef migrations script FromMigration ToMigration --output migrate.sql

# Idempotent script (can run multiple times)
dotnet ef migrations script --idempotent --output migrate.sql
```

**Option 2: Automatic (Startup)**

```csharp
// Program.cs
using (var scope = app.Services.CreateScope())
{
    var context = scope.ServiceProvider.GetRequiredService<AppDbContext>();

    if (builder.Environment.IsDevelopment())
    {
        await context.Database.MigrateAsync(); // ✅ Dev only
    }
}
```

---

## Handling Production Data

### Adding Column with Default

```csharp
protected override void Up(MigrationBuilder migrationBuilder)
{
    // Add column with default value (safe for existing rows)
    migrationBuilder.AddColumn<string>(
        name: "Status",
        table: "Orders",
        nullable: false,
        defaultValue: "Pending"); // ✅ Default prevents NULL for existing rows
}
```

### Renaming Column (Preserve Data)

```csharp
protected override void Up(MigrationBuilder migrationBuilder)
{
    // Rename column (data preserved)
    migrationBuilder.RenameColumn(
        name: "OldName",
        table: "Users",
        newName: "NewName");
}

protected override void Down(MigrationBuilder migrationBuilder)
{
    migrationBuilder.RenameColumn(
        name: "NewName",
        table: "Users",
        newName: "OldName");
}
```

### Altering Column Type (Risky)

```csharp
protected override void Up(MigrationBuilder migrationBuilder)
{
    // ⚠️ RISKY: May lose data if not compatible
    // Example: VARCHAR(50) → VARCHAR(100) is safe
    //          VARCHAR(100) → VARCHAR(50) may truncate
    //          NVARCHAR → INT will fail

    migrationBuilder.AlterColumn<string>(
        name: "Email",
        table: "Users",
        maxLength: 512, // Increased from 256
        nullable: false,
        oldClrType: typeof(string),
        oldMaxLength: 256);
}
```

---

## Best Practices

### DO

✅ **Review generated migration before applying**

```bash
dotnet ef migrations add AddUserEntity
# Review Migrations/xxxxx_AddUserEntity.cs file
# Verify Up and Down methods are correct
dotnet ef database update
```

✅ **Create migrations for each logical change**

```bash
# ✅ GOOD (separate migrations)
dotnet ef migrations add CreateUserTable
dotnet ef migrations add CreateOrderTable

# ❌ BAD (one big migration)
dotnet ef migrations add CreateAllTables
```

✅ **Test migrations in dev before production**

```bash
# Test migration
dotnet ef database update

# Test rollback
dotnet ef database update PreviousMigration

# Test re-apply
dotnet ef database update LatestMigration
```

✅ **Use idempotent scripts for production**

```bash
dotnet ef migrations script --idempotent --output deploy.sql
```

### DON'T

❌ **Don't modify applied migrations**

```csharp
// ❌ WRONG (migration already applied in prod)
protected override void Up(MigrationBuilder migrationBuilder)
{
    // Changed this after already deployed - will cause issues!
    migrationBuilder.CreateTable(...);
}

// ✅ CORRECT (create new migration)
dotnet ef migrations add FixUserTable
```

❌ **Don't delete migrations already in production**

```bash
# ❌ WRONG (will break production)
rm Migrations/20260216_CreateUserTable.cs

# ✅ CORRECT (rollback properly)
dotnet ef migrations remove
```

❌ **Don't use database.EnsureCreated() with migrations**

```csharp
// ❌ WRONG (conflicts with migrations)
context.Database.EnsureCreated();

// ✅ CORRECT
context.Database.Migrate();
```

---

## Troubleshooting

### Migration Pending Error

```
Error: Pending model changes detected
```

**Solution:**

```bash
dotnet ef migrations add FixPendingChanges
dotnet ef database update
```

### Migration Already Applied

```
Error: Migration 'xxx' has already been applied
```

**Solution:**

```bash
# Remove last migration (if not in prod)
dotnet ef migrations remove

# Or create new migration
dotnet ef migrations add AdditionalChanges
```

### Rollback Failed

```
Error: Could not rollback migration
```

**Solution:**

```bash
# Manually fix database state, then:
dotnet ef database update LastGoodMigration --force
```

---

## Related Standards

- [EF Core Standards](./ef-core.md)
- [Database Performance](./performance.md)

---

*MORPH-SPEC by Polymorphism Tech*
