.NET EF Core SQL Server Migrations

EF Core migrations in production - what nobody tells you

The migration commands you already know, plus the ones you need when the first deploy fails and you are staring at a locked database at 2am.

By admin Apr 16, 2026 1

The happy path

In development you run two commands and move on. Most tutorials end here.
bash
dotnet ef migrations add AddBlogTables
dotnet ef database update

Production is not development

On a live server you usually cannot run ef tool commands (no SDK, no access, or CI/CD in the middle). Generate an idempotent SQL script instead and hand it to DBAs or run it from your pipeline.
bash
# from InitialCreate up to the latest migration, safe to re-run
dotnet ef migrations script --idempotent --output migrate.sql

# or only the delta between two specific migrations
dotnet ef migrations script AddBlogTables AddBlogViewCounts     --idempotent --output 2026-04-16_delta.sql

The three rules that save you

1. Never edit a migration that has already been applied to any shared environment. Add a new one instead.\n2. Every migration must be reversible - test the Down method locally before you merge.\n3. Separate schema changes from data fixes. Mixing them makes a half-failed deploy almost impossible to recover.
A good migration does one small thing and does it atomically. A bad migration is a 400-line script that renames columns, seeds data, and "also backfills a lookup table - should be fine".

Rename a column without data loss

EF treats a property rename as drop + add by default, which silently loses the data. Tell it explicitly:
csharp
protected override void Up(MigrationBuilder migrationBuilder)
{
    migrationBuilder.RenameColumn(
        name: "CoverImage",
        table: "BLOG_POSTS",
        newName: "COVER_IMAGE");
}

protected override void Down(MigrationBuilder migrationBuilder)
{
    migrationBuilder.RenameColumn(
        name: "COVER_IMAGE",
        table: "BLOG_POSTS",
        newName: "CoverImage");
}

Add a NOT NULL column on a table with rows

SQL Server will reject an ADD COLUMN NOT NULL unless you give it a default. Do it in two migrations if the column should eventually be mandatory with no default:
csharp
// Migration 1 - nullable, backfill, then tighten later
migrationBuilder.AddColumn<string>(
    name: "SLUG",
    table: "BLOG_POSTS",
    nullable: true);

migrationBuilder.Sql(@"
    UPDATE BLOG_POSTS
    SET SLUG = LOWER(REPLACE(TITLE, ' ', '-'))
    WHERE SLUG IS NULL;
");
csharp
// Migration 2 (next deploy) - now safely NOT NULL
migrationBuilder.AlterColumn<string>(
    name: "SLUG",
    table: "BLOG_POSTS",
    nullable: false,
    defaultValue: "");

Apply the script at startup (optional)

For small apps I run pending migrations on boot. Do not do this in a multi-instance deployment - you will race. Use a job or CI step instead.
csharp
using (var scope = app.Services.CreateScope())
{
    var db = scope.ServiceProvider.GetRequiredService<PortfolioDbContext>();
    await db.Database.MigrateAsync();
}

Takeaway

Script your migrations, keep each one small and reversible, and treat data fixes as their own commits. Boring migrations are the good ones.