Avoiding EF Core Column Rename Data Loss
You renamed Customer.Phone to Customer.PhoneNumber in the entity class, ran dotnet ef migrations add RenamePhone, and EF Core printed a yellow line you nearly missed: An operation was scaffolded that may result in the loss of data. Please review the migration for accuracy. The generated Up method drops the Phone column and adds an empty PhoneNumber column. Applied to production, that migration deletes every customer’s phone number. Even the correct fix — a RenameColumn — has a second trap: during a rolling deploy, old instances still query Phone, which no longer exists. This guide covers both: how to make EF generate a rename instead of a drop, and how to rename a column without breaking the code that is still running. It is part of Entity Framework Core Migrations.
Symptom / Error Signatures
At scaffolding time:
An operation was scaffolded that may result in the loss of data. Please review the migration for accuracy.
In the generated migration:
migrationBuilder.DropColumn(name: "Phone", table: "Customers");
migrationBuilder.AddColumn<string>(name: "PhoneNumber", table: "Customers", type: "nvarchar(max)", nullable: true);
After a plain RenameColumn ships during a rolling deploy, old instances fail with Microsoft.Data.SqlClient.SqlException: Invalid column name 'Phone' (SQL Server) or Npgsql.PostgresException: 42703: column c.Phone does not exist (PostgreSQL) until they are replaced.
Root Cause Analysis
EF Core generates migrations by diffing the current model against the model snapshot. A renamed property appears in that diff as one property removed and another added, with the same type. EF’s differ can sometimes pair them up as a rename, but it cannot know your intent in general — two independent changes (drop one column, add another) look identical to a rename — so it often falls back to the literal diff and warns. The warning is the only signal, and it is easy to miss in CI output.
A correct RenameColumn fixes the data problem but not the deployment problem. Renaming a column is instant, but it changes the name that every query uses at the moment the migration commits. Instances still running the previous release map Customer.Phone to column Phone and fail. That is the same backward-compatibility constraint every framework faces, and the answer is the same: either keep the physical column name and rename only the property, or perform an expand-and-contract rename across releases, as described in renaming a column with expand and contract.
| Option | Data | Old instances during rollout | Releases |
|---|---|---|---|
| scaffolded drop + add | lost | fail (column missing) | 1 |
RenameColumn |
kept | fail (old name gone) | 1 |
rename property only, HasColumnName("Phone") |
kept | work | 1 (no DDL) |
| expand/contract: add, dual-write, backfill, switch, drop | kept | work | 3 |
Immediate Mitigation
1. Never apply a migration that carries the data-loss warning without editing it. If the migration is not yet applied anywhere shared, remove it and redo it deliberately:
# Shell · developer workstation · removes the last, unapplied migration and reverts the snapshot
dotnet ef migrations remove --project src/Shop.Data --startup-project src/Shop.Api
2. If the column name does not need to change, map the new property to the old column. This produces no DDL at all and is completely safe during a rollout.
// C# · Data/Configurations/CustomerConfiguration.cs · model configuration only
// WARNING: after this, `dotnet ef migrations add` should scaffold an empty migration — confirm it does.
builder.Property(c => c.PhoneNumber).HasColumnName("Phone");
3. If the column must be renamed and you have a window with no overlapping versions, edit the migration to use RenameColumn.
// C# · Migrations/20260918120000_RenamePhone.cs · safe for data; breaks instances still on the old name
// WARNING: apply only when no instance of the previous release is running.
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.RenameColumn(name: "Phone", table: "Customers", newName: "PhoneNumber");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.RenameColumn(name: "PhoneNumber", table: "Customers", newName: "Phone");
}
// ROLLBACK PATH: the Down method renames the column back; no data is lost either way.
Permanent Fix / Long-Term Pattern
For renames that must change the physical column while the service stays up, split the change across releases. Release 1 adds the new column (nullable), maps it with a shadow or second property, and writes both columns on every save. A backfill copies old values in batches, as covered in Backfill Optimization. Release 2 switches reads to the new column while still writing both. Release 3 stops writing the old column, unmaps it, and a final migration drops it. At every point, the running and previous releases can both operate.
// C# · release 1 migration · additive only · PostgreSQL or SQL Server
// WARNING: the backfill runs separately in batches; do not UPDATE the whole table here.
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(name: "PhoneNumber", table: "Customers", nullable: true);
}
// ROLLBACK PATH: DropColumn("PhoneNumber") is safe until release 2 starts reading it.
Make the data-loss warning impossible to ignore. Add a CI step that generates the idempotent script and fails on DROP COLUMN unless the pull request is labelled as an intentional contract step, as described in generating idempotent SQL scripts from EF Core. Treat any DropColumn in review as a question — “which release stopped using this?” — rather than as routine output.
Verification Checklist
Frequently Asked Questions
Why doesn’t EF Core always detect renames? Because a rename and a removal plus an unrelated addition produce the same model difference. EF sometimes infers a rename, but when it cannot be sure it scaffolds the literal drop and add and warns about possible data loss, leaving the decision to you.
Is RenameColumn safe on a large table?
The DDL is metadata-only on SQL Server, PostgreSQL and MySQL 8.0, so it is instant. The risk is not the lock but the running code: any instance still using the old name fails as soon as the rename commits.
What is the least risky way to rename a property?
Rename it in C# and keep the database column as it is with HasColumnName. No DDL runs, nothing breaks, and you can rename the physical column later with an expand-and-contract sequence if it ever matters.
Does the same problem apply to table renames?
Yes. Renaming an entity or its table name produces RenameTable at best and a drop-and-create at worst. The same options apply: map the new entity name to the old table with ToTable, or stage the rename across releases, for example behind a view as described in renaming a table with an updatable view.