The incident
Here's a common way this happens: a routine refactor renames a C# property on an entity — say CustomerName to ClientName — and a teammate runs dotnet ef migrations add to keep the database in sync. The generated migration compiles cleanly and reads like any other schema change, so it can pass review without a second look. It only gets caught if someone scrolls through the generated Up() method before it's applied and notices a DropColumn followed by an unrelated AddColumn, rather than the RenameColumn a simple rename should have produced — catching it before it ever reaches a database with real rows in that column.
Why a rename doesn't look like a rename to EF Core
When you run dotnet ef migrations add, EF Core compares your current DbContext model against a snapshot of what the model looked like the last time a migration was generated. That comparison happens property by property, matched by name. If you rename a C# property — say, CustomerName to ClientName — from EF Core's perspective, one property that used to exist is now gone, and a completely unrelated property with a different name has appeared. It has no way to infer that these are "the same column, just renamed," because nothing in the model tells it that. The result is a migration that drops the old column and adds a new one:
public partial class RenameCustomerNameToClientName : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "CustomerName",
table: "Orders");
migrationBuilder.AddColumn<string>(
name: "ClientName",
table: "Orders",
type: "nvarchar(max)",
nullable: true);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "ClientName",
table: "Orders");
migrationBuilder.AddColumn<string>(
name: "CustomerName",
table: "Orders",
type: "nvarchar(max)",
nullable: true);
}
}
This migration is completely valid C#, compiles without warnings, and runs without error. It also deletes every existing value in CustomerName for every row in the Orders table the moment it applies — DROP COLUMN in SQL Server removes the column and its data outright; the subsequent ADD COLUMN creates a new, empty one that happens to share a table. There is no data migration step connecting the two.
Why this is easy to miss in code review
The generated migration file reads as ordinary, mechanical EF Core boilerplate — DropColumn followed by AddColumn is a completely normal-looking pair of calls, indistinguishable in shape from any other schema change. Unless a reviewer specifically recognizes that this particular pair represents a rename that lost its identity, and specifically checks whether the table already has production data that depends on the dropped column, the migration reads as unremarkable. The C# compiles, the SQL is syntactically ordinary, and nothing about the diff itself signals "this deletes data."
The fix: tell EF Core it's a rename, not two unrelated changes
Once you know the intent was a rename, the fix is a small manual edit to the generated migration, replacing the drop/add pair with MigrationBuilder.RenameColumn, which preserves every existing value:
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.RenameColumn(
name: "CustomerName",
table: "Orders",
newName: "ClientName");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.RenameColumn(
name: "ClientName",
table: "Orders",
newName: "CustomerName");
}
The same principle applies to renaming a table — DropTable + CreateTable generated by the scaffolder should become MigrationBuilder.RenameTable if the intent was a rename rather than a genuine drop-and-recreate. In both cases, EF Core's own scaffolding tool has no way to know your intent; it only knows what changed, not why, and it always assumes the more mechanical, more destructive interpretation by default.
The generalizable check: read every generated migration before it ships
Any migration containing DropColumn, DropTable, or DropForeignKey against a table that already holds production data deserves a specific, deliberate second look before it's allowed to run — not because these operations are always wrong, but because a genuine "remove this column, we no longer need the data" and an accidental "this was actually just a rename" produce byte-for-byte identical-looking migration code. The EF Core Migration SQL Explainer exists specifically to make that distinction visible without reading raw Up()/Down() C# line by line — paste a migration script and every destructive operation is called out explicitly, grouped by table, before it ever reaches a production database.