Your entity classes describe the schema you want. A migration is the recorded, reviewable, repeatable set of steps that gets an actual database from where it is to where your classes say it should be.
You've written a Product entity and configured it with relationships and conventions — but nothing in a real database matches it yet. Now imagine three months from now, you add a Category column to Product. Your local database needs that new column. So does the tester's copy. So does staging. So does production, with actual customer data already sitting in it. Do you remember to write the same ALTER TABLE Products ADD Category nvarchar(50) statement, correctly, on every single one of those, in the right order relative to every other change anyone else on the team has made? That's the exact problem migrations exist to solve.
In this lesson, you'll learn what a migration actually is, how to generate one with dotnet ef migrations add, how to apply it with dotnet ef database update, and why this workflow replaces hand-editing schema entirely.
A migration is a small, versioned piece of C# code that describes one incremental change to your database schema — "add this table," "add this column," "add this index" — generated automatically by comparing your current entity model against the last schema EF Core knows about. Migrations are meant to be applied in order, one after another, so that any database — no matter how far behind — can be brought up to date by simply running through the ones it's missing.
A migration is a class deriving from Migration, placed in a Migrations folder in your project, with two methods: Up(), which applies the change (e.g. CreateTable, AddColumn), and Down(), which reverses it. EF Core also maintains a special table inside your actual database — __EFMigrationsHistory — listing exactly which migrations have already been applied, which is how it knows what's missing the next time you run an update.
Say you manually run ALTER TABLE Products ADD Category nvarchar(50) against your local database while building a feature. You remember to also run it against the shared dev database. Two weeks later, someone else on the team pulls your code, and their local database is now missing that column — nothing tells them it's needed, and the application just throws confusing SQL errors the moment it tries to read or write Category. Multiply this by every environment (local, dev, staging, production) and every schema change anyone on the team ever makes, and you get schema drift — a mess where no one is quite sure what the actual current schema is anywhere, or how it got that way.
Migrations turn every schema change into a file, checked into source control, right alongside the entity class change that caused it. The migration is the record of what changed and why — reviewable in a pull request like any other code, applied identically and in the same order on every machine, and permanently recorded in __EFMigrationsHistory so any database can be asked "what do I still need?" and get a correct answer. Schema evolution stops being something you remember to do by hand, and becomes something the codebase itself tracks.
Generates a new migration by diffing your current entity model against the previous migration's snapshot of the schema:
dotnet ef migrations add InitialCreateThis creates three things in your Migrations folder: a timestamped migration file (e.g. 20260830120000_InitialCreate.cs) with Up()/Down() methods, a matching .Designer.cs metadata file, and an updated ModelSnapshot.cs — a complete picture of what the schema looks like after this migration, used as the baseline for the next diff.
Applies any migrations not yet recorded in the target database's __EFMigrationsHistory table:
dotnet ef database updateRun with no arguments, it brings the database fully up to date — applying every pending migration's Up() method, in order. Passing a specific migration name rolls the database forward or backward to exactly that point:
dotnet ef database update InitialCreate # roll back to right after InitialCreateDeletes the most recently added migration — but only if it hasn't been applied to any database yet. This is for the common "oops, I need to tweak my entity change before generating the migration" loop, not for undoing something already shipped:
dotnet ef migrations removeStarting from a fresh Product entity and ShopDbContext with no database yet:
public class Product
{
public int Id { get; set; }
public required string Name { get; set; }
public decimal Price { get; set; }
}
public class ShopDbContext(DbContextOptions<ShopDbContext> options) : DbContext(options)
{
public DbSet<Product> Products => Set<Product>();
}dotnet ef migrations add InitialCreate
dotnet ef database updateThe generated migration's Up() method looks roughly like this — plain, readable C# describing exactly one CREATE TABLE:
public partial class InitialCreate : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.CreateTable(
name: "Products",
columns: table => new
{
Id = table.Column<int>(nullable: false)
.Annotation("SqlServer:Identity", "1, 1"),
Name = table.Column<string>(nullable: false),
Price = table.Column<decimal>(nullable: false)
},
constraints: table =>
{
table.PrimaryKey("PK_Products", x => x.Id);
});
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropTable(name: "Products");
}
}Now add a Category property to Product and generate a second migration — EF Core diffs against the snapshot from InitialCreate and produces only the incremental change:
dotnet ef migrations add AddCategoryToProduct
dotnet ef database updateThis second migration's Up() contains a single AddColumn call, not a full table recreation — migrations are strictly incremental, each one building on the schema state left by the last.
A product catalog team ships a new feature: products can now be marked "featured" for the homepage. The full workflow, exactly as it plays out day to day:
public bool IsFeatured { get; set; }
dotnet ef migrations add AddIsFeaturedToProduct
Migrations are to your database schema what commits are to your source code. Each migration is a small, named, ordered change — just like a commit is a small, named, ordered change to your files. __EFMigrationsHistory is like a branch's commit log: it tells you exactly which changes this particular database has already received. Applying pending migrations is like pulling and checking out the latest commit — you end up at the same known state as everyone else, no matter where you started from.
And just as you'd never hand-edit a teammate's committed file directly on the shared server, you don't hand-edit a shared database's schema directly either — the change belongs in a migration, reviewed and applied the same way everywhere.
It doesn't touch any database at all — it only writes C# files to your project, based on comparing your model against the stored snapshot. Nothing happens to any actual schema until you separately run dotnet ef database update (or the equivalent programmatic call). It's entirely normal, and expected, to generate several migrations locally before applying any of them.
It's the reverse: migrations are generated from your entity classes, compared against the last recorded snapshot — never by inspecting an actual live database. This is exactly why manually altering a database's schema outside of a migration is dangerous: EF Core has no way to see that change, so its snapshot silently disagrees with reality, and the next migration it generates won't account for the manual change at all.
Running ALTER TABLE directly against dev or production to fix something quickly, bypassing migrations entirely. The moment you do this, your migration snapshot and the real database schema disagree, and the next dotnet ef migrations add either misses the manual change or tries to redo it and fails. Always make the change through the entity classes and a generated migration, even for "small" fixes — that's the entire point of using migrations at all.
Going back into 20260830120000_InitialCreate.cs weeks later and changing its Up() method to fix a mistake — databases that already ran the original version have no idea anything changed, since __EFMigrationsHistory only recorded that the migration by that name ran, not its exact contents. Generate a brand-new migration that corrects the mistake — migrations are meant to be append-only history, like commits, not edited retroactively once applied anywhere.
Blindly trusting that dotnet ef migrations add got it exactly right, especially for renames (which EF Core can't always distinguish from "drop one column, add a different one" — silently losing data if applied that way) or precision-sensitive changes. Always open and read the generated Up()/Down() before committing — it's ordinary, readable C#, generated code you're still responsible for.
You've seen the full migration workflow, from an entity class change to an updated database. Let's confirm the reasoning stuck.
1. What actually happens when you run dotnet ef migrations add AddCategoryToProduct?
Correct: B
Why B is correct: migrations add only writes files to your project — a migration class plus an updated model snapshot — based on comparing your current entity model to the previous snapshot. It never connects to or modifies any actual database.
Why A is incorrect: Applying the change to a real database is a separate step — dotnet ef database update — not something migrations add does.
Why C is incorrect: Migrations are incremental by design — they add exactly the diff, not a full schema recreation, precisely so existing data isn't destroyed.
Why D is incorrect: EF Core generates the migration's SQL-equivalent operations automatically from the model diff — you don't hand-write SQL for a standard property addition.
Reinforcement: Keep migrations add (generates files) and database update (applies them) mentally separate — they are two distinct, independently-run steps.
2. Two developers each generate migrations locally without applying them to any shared database yet. Why doesn't this cause a problem, even though neither database has actually changed?
Correct: B
Why B is correct: Generating a migration only produces files — it's ordinary code, reviewable and mergeable like any other change. The actual database only changes when database update runs against it, so two developers generating migrations independently is no different from two developers writing any other code independently, until it's time to merge and apply.
Why A is incorrect: There's no requirement to apply a migration immediately after generating it — many are generated and reviewed locally well before ever being applied anywhere.
Why C is incorrect: EF Core doesn't automatically merge conflicting migrations — if two developers' migrations genuinely conflict, that's resolved like any merge conflict, through normal source control and communication.
Why D is incorrect: Any developer can generate migrations — there's no such restriction in EF Core.
Reinforcement: A migration file is inert until database update actually applies it — that separation is what makes migrations safe to generate, review, and commit like ordinary code.
3. A developer directly runs ALTER TABLE Products ADD Category nvarchar(50) against the shared dev database to save time, instead of creating a migration. What problem does this cause?
Correct: B
Why B is correct: Migrations are generated by diffing entity classes against a stored snapshot file — never by inspecting the live database. A manual ALTER TABLE is invisible to that snapshot, so it silently drifts out of sync with reality, and every other developer's or environment's database still lacks the column since it was never recorded in a migration.
Why A is incorrect: EF Core has no mechanism to detect manual schema changes automatically — the snapshot only updates when you explicitly generate a migration from the entity classes.
Why C is incorrect: The application will typically keep running against the manually-altered database without any error at all — the danger is silent, not a loud failure.
Why D is incorrect: database update only applies pending migrations — it has no awareness of, and doesn't compare against, changes made outside the migration system.
Reinforcement: This is exactly why manual schema changes to any shared database are a core mistake to avoid — always route schema changes through the entity classes and a generated migration.
You now understand how migrations keep every environment's schema in sync, reviewably and repeatably. Next up: actually querying your data with LINQ against EF Core.
dotnetmadeeasy.com — Learn C# and .NET, the right way.