The happy path
In development you run two commands and move on. Most tutorials end here.
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.
# 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:
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:
// 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;
");
// 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.
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.
