EF Core 10 Migrations are more than a way to create tables — they are a vocabulary for changing a schema safely over time. A schema is never finished; it grows as features grow, and the gap between a painful upgrade and a seamless one is how you write the migrations that carry that growth. Good migrations are small, reviewable, and reversible.
This article focuses on the craft of writing EF Core 10 Migrations: planning changes before you generate them, understanding the model snapshot, injecting custom SQL when the conventions are not enough, and rolling back without breaking every other environment. These habits matter most in a codebase that is already running in production.
Plan EF Core 10 Migrations in Small, Reviewable Steps
The golden rule is small and frequent. A migration that adds a table, a column, and an index in one file is hard to review and impossible to partially roll back. Split schema changes into logical units, one per feature, named after the business intent.
- One migration per feature, not one per sprint.
- Every Up() should have a matching Down() that undoes it completely.
- Add columns as nullable first, backfill them, then enforce constraints in a later migration.
- Never edit a migration that has already shipped to another environment.
That last rule deserves emphasis. Once a migration runs anywhere outside your machine, it becomes part of history. Editing it desynchronizes the model snapshot from every environment that already applied it, and the next migration will produce a confusing, duplicated diff. If you need to change a shipped schema, ship a new migration.
Naming matters just as much as size. A migration called AddOrdersTable communicates intent in the history table; one called Migration1 forces every reviewer to open the file to learn anything. EF Core 10 lets you append a meaningful name to every add command, and that name is recorded in the migration ledger — free documentation every future developer reads before they touch your schema.
What the EF Core 10 Migrations Model Snapshot Actually Does
When you run dotnet ef migrations add AddTenantCode, EF Core compares your current entities against AppDbContextModelSnapshot.cs. That snapshot is a serialized view of your entire model — every entity, property, relationship, and index. The diff between the snapshot and your live model becomes the new migration.
Understanding the snapshot changes how you work with EF Core 10 Migrations. It explains why adding a property produces an AddColumn operation, why renaming a property produces a DropColumn plus AddColumn unless you configure it, and why rearranging properties can generate a noisy diff. When a generated migration looks wrong, the snapshot is the first place to look.
A healthy snapshot also catches model drift at review time. If a branch adds a property to an entity but never generates a migration, the pull request simply has no new migration file — a clear signal that the diff was missed. Making the migration part of the same commit as the entity change keeps that signal visible and keeps the snapshot honest, which is the cheapest insurance a schema can have.
Custom SQL Inside EF Core 10 Migrations
Some schema changes are too subtle for the code-first conventions. Check constraints, triggers, computed columns, and backfilling existing rows all belong in the migration as raw SQL. EF Core 10 Migrations give you a first-class hook for exactly this: migrationBuilder.Sql().
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "TenantCode",
table: "Orders",
nullable: false,
defaultValue: "");
migrationBuilder.Sql(
@"UPDATE ""Orders"" SET ""TenantCode"" = 'default'
WHERE ""TenantCode"" = ''");
migrationBuilder.CreateIndex(
name: "IX_Orders_TenantCode",
table: "Orders",
column: "TenantCode");
}
Custom SQL runs inside the same transaction as the rest of the migration, so a failure rolls everything back. That property is what keeps an evolution seamless: the migration either applies completely or not at all. There is no intermediate state where half the rows are backfilled and half are not.
Rolling Back EF Core 10 Migrations
A seamless evolution includes a way back. dotnet ef migrations remove deletes the last migration from the codebase before it has shipped, and the generated Down() method reverses the schema when you run dotnet ef database update <previous>. In production, you generate a script targeting the previous migration and run it in a maintenance window.
The practical rule: make every Down() complete, then treat rollback as a last resort. Backwards-compatible migrations — add a column, backfill it, move code to read it, then drop the old column in a later release — often make rollback unnecessary altogether. When every change is additive and reversible, your schema evolves the way your application does: continuously, without drama.
Key Takeaways
- Small, single-purpose migrations are easier to review and roll back.
- The model snapshot drives every diff; never edit or delete it.
- Use
migrationBuilder.Sql()for constraints, backfills, and other non-conventional changes. - Write complete Down() methods, then prefer backward-compatible migrations over rollbacks.
- Treat shipped migrations as append-only history.
FAQ
What exactly is the model snapshot?
AppDbContextModelSnapshot.cs is a serialized copy of your whole model. EF Core diffs your live entities against it to decide what the next migration must do.
Why does renaming a property generate drop and add instead of a rename?
EF Core cannot infer intent. It sees a removed column and an added one. Use migrationBuilder.RenameColumn() in the generated migration to produce a real ALTER TABLE RENAME.
Can custom SQL share a transaction with the migration?
Yes. migrationBuilder.Sql() runs inside the migration transaction, so schema operations and data backfills succeed or fail together.
What is the safest way to roll back in production?
Generate a script to the previous migration with dotnet ef migrations script --to <previous> and run it in a maintenance window, after backing up the database.