EF Core Migrations: A Detailed Beginner Guide for .NET
Learn EF Core migrations step by step. Add, apply, revert, and ship database changes safely with simple examples, diagrams, tables, and best practices for .NET 10.
A diary for your database
Imagine you are building a house, room by room. Each time you add a room, you write a short note in a diary: "Day 1: built the kitchen. Day 2: added a bedroom. Day 3: put a window in the bedroom." If a friend wants to build the exact same house, they just follow your diary from Day 1 to the end. They get the same house, in the same order, with nothing missed.
EF Core migrations are that diary, but for your database. Every time you change your C# model — add a new property, a new table, a new relationship — EF Core writes a new diary page (a migration) that says exactly how to update the database. Your teammate, your test server, and your live production server all follow the same pages in the same order. Everyone ends up with the same database shape.
This guide will teach you, slowly and clearly, how this diary works: how to write a page, how to apply it, how to undo a mistake, and how to ship pages safely to a real server. Let us start from zero.
Why migrations exist
In EF Core, your C# classes describe your data. A Customer class with Name and Email properties usually maps to a Customers table with Name and Email columns. This is called code-first: the code comes first, and the database follows.
But databases do not change themselves. If you add an Email property to the Customer class, the real table still has no Email column until someone runs the right SQL. Migrations are how EF Core writes and runs that SQL for you, in a way that is repeatable and safe.
Without migrations, you would have to write every ALTER TABLE by hand and remember which servers already had it. That is slow and full of mistakes. Migrations turn schema changes into ordered, version-controlled files that live next to your code.
Setting up the tools
Migrations need two things: a NuGet package and a command-line tool.
The package goes in your project. The Design package contains the logic that builds migration files:
// In your project folder, run:
// dotnet add package Microsoft.EntityFrameworkCore.Design
// You also need a database provider, for example SQL Server:
// dotnet add package Microsoft.EntityFrameworkCore.SqlServerThe tool is dotnet-ef. Install it once on your machine as a global tool:
// Install the EF Core command-line tool globally:
// dotnet tool install --global dotnet-ef
// Check it works:
// dotnet ef --versionNow you can run dotnet ef commands from your project folder. If you prefer Visual Studio, the Package Manager Console offers the same features with names like Add-Migration and Update-Database. This guide uses the cross-platform dotnet ef commands.
Your first model and DbContext
Here is a tiny model and a DbContext. The DbContext is the bridge between your classes and the database.
public class Customer
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
}
public class AppDbContext : DbContext
{
public DbSet<Customer> Customers => Set<Customer>();
protected override void OnConfiguring(DbContextOptionsBuilder options)
{
options.UseSqlServer(
"Server=localhost;Database=ShopDb;Trusted_Connection=True;TrustServerCertificate=True;");
}
}This says: "I have a Customers table with an Id and a Name." EF Core does not change the database yet. We need a migration for that.
Creating your first migration
Run this command in the project folder:
// dotnet ef migrations add InitialCreateEF Core looks at your model, compares it to its saved snapshot (empty the first time), and writes new files in a Migrations folder. The main file has three important parts.
| File or part | What it contains | Should you edit it? |
|---|---|---|
<timestamp>_InitialCreate.cs | The Up() and Down() methods with the actual changes | Sometimes, carefully |
Up() method | Steps to apply the change (create table, add column) | Yes, if you know what you are doing |
Down() method | Steps to undo the change (drop table, remove column) | Yes, to keep undo correct |
AppDbContextModelSnapshot.cs | EF Core's picture of the full current model | No — EF Core manages it |
The timestamp at the front of the file name is what gives migrations their order. EF Core always applies them oldest first. Never rename that timestamp.
Applying the migration
The migration file is just a plan. To run it, use:
// dotnet ef database updateThis creates the database if it does not exist, then runs every migration that has not been applied yet, oldest first. After it finishes, your Customers table exists.
How does EF Core know which migrations already ran? It keeps a special table in your database called __EFMigrationsHistory. Each time a migration is applied, EF Core writes a row with that migration's name. Next time you run database update, EF Core reads this table and skips anything already listed. This is the heart of how migrations stay safe and repeatable.
What happens during 'database update'
Steps
Read history
Look at __EFMigrationsHistory
Find pending
Which migrations are not listed yet
Apply in order
Run Up() oldest first
Record each
Insert a row per applied migration
Making a second change
Let us add an Email column. Edit the model:
public class Customer
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public string Email { get; set; } = string.Empty; // new
}Then create a new migration and apply it:
// dotnet ef migrations add AddCustomerEmail
// dotnet ef database updateEF Core compares your model to the snapshot, sees only the new Email property, and writes a migration that adds just that one column. It does not rebuild the whole table. Each migration is a small, focused step. This is the diary idea in action: one page per change.
Undoing things safely
Mistakes happen. EF Core gives you clear ways to step back, and the right choice depends on whether you already applied the migration.
| Situation | Command | What it does |
|---|---|---|
| Created a migration, not applied yet | dotnet ef migrations remove | Deletes the last migration file and rewinds the snapshot |
| Already applied, want to go back | dotnet ef database update <PreviousName> | Runs Down() to roll the database back to that point |
| Want to throw away all changes | dotnet ef database update 0 | Reverts every migration (empties the schema) |
The safe order to fully undo an applied migration is: first roll the database back, then remove the file.
// Roll the database back to the migration BEFORE the bad one:
// dotnet ef database update InitialCreate
// Now the bad migration is unapplied, so remove its file:
// dotnet ef migrations removeReverting an applied migration
Steps
Bad migration applied
It is in the history table
Update to previous
database update <PreviousName>
File now unapplied
Down() has run
Remove file
migrations remove is now safe
Golden rule: never edit or delete a migration that has already been applied on a server other people share, especially production. Once a page is in the shared diary, fix problems by adding a new page, not by tearing out an old one.
Seeing the SQL before you trust it
Sometimes you want to read the exact SQL a migration will run, without touching any database. Generate a script:
// Print SQL for all migrations:
// dotnet ef migrations script
// Print SQL only between two migrations:
// dotnet ef migrations script InitialCreate AddCustomerEmailThis is great for code review and for handing changes to a database administrator. It is also the first step toward production-grade deployments, which we cover next.
Shipping migrations to production
On your own machine, dotnet ef database update is perfect. On a real production server, it is usually the wrong tool. Two reasons: your running app would need permission to change the database shape (dangerous), and if two copies of your app start at once, they might both try to migrate and clash.
The community and Microsoft agree on safer patterns. Here are the common ways to apply migrations, from least to most production-ready.
Idempotent SQL scripts
An idempotent script is one you can run many times safely. EF Core can generate a script that checks the history table itself and applies only the missing migrations:
// dotnet ef migrations script --idempotent --output migrate.sqlYou hand migrate.sql to whoever runs your deployment, or to a DBA for review. If a deploy fails halfway and you run it again, it simply skips what already succeeded. This is the favorite of regulated teams who want a human to read the SQL before it runs.
Migration bundles
A migration bundle is a single small executable that contains your migrations. Microsoft introduced these for DevOps-friendly deployments. You build the bundle once in your CI pipeline:
// dotnet ef migrations bundle --output ./efbundle
// Then on the server (no SDK or source code needed):
// ./efbundle --connection "Server=...;Database=...;"The bundle does not need the .NET SDK, the EF tool, or your project's source code to run. That makes it clean and predictable for automated pipelines. It is the recommended approach for CI/CD.
Here is a quick comparison to help you choose.
| Approach | Best for | Needs app source/SDK? | Safe to re-run? |
|---|---|---|---|
database update | Local development, learning | Yes | Yes |
| Idempotent script | DBA review, regulated teams | No (script is plain SQL) | Yes |
| Migration bundle | CI/CD pipelines | No | Yes |
| Migrate on app startup | Tiny demos only | Yes | Risky with many instances |
A widely shared rule of thumb: deploy the database change before the new app version, and give your migration step a different database identity than your running app. The app should be able to read and write data, but should not be allowed to change the schema.
A clean migration workflow
Putting it together, here is a simple loop you can repeat for every change.
The everyday migration loop
Steps
Change model
Edit C# classes
Add migration
dotnet ef migrations add Name
Review SQL
Read Up/Down or script it
Update local DB
database update on dev
Commit + ship
Bundle or script in CI/CD
Following this every time keeps your team's diary tidy and your servers in sync.
Common mistakes to avoid
A few traps catch almost every beginner. Knowing them early saves hours later.
- Editing an applied migration. Once a migration has run on a shared server, treat it as frozen. Fix issues with a new migration.
- Deleting the snapshot file.
AppDbContextModelSnapshot.csis EF Core's memory of your model. If you delete it, the next migration will think your whole database is new and try to recreate everything. - Forgetting to commit migration files. Migrations belong in version control next to your code. A teammate who pulls your code must get your migrations too.
- Two people adding migrations at the same time. If both branches add a migration, the timestamps and snapshot can conflict. Coordinate, and rebuild one migration after merging if needed.
- Giving production data scripts and schema scripts the same identity. Keep schema-change power separate from everyday app access.
Avoiding these keeps migrations boring — and boring is exactly what you want from a database tool.
Bringing it together
Migrations are not magic. They are an ordered set of small files, each describing one change, with a history table in the database remembering what already ran. You add a page with migrations add, you apply pages with database update, you undo with migrations remove or by updating to a previous migration, and for production you ship a reviewed idempotent script or a self-contained bundle. Master this loop and your database will always match your code, on every machine, without guesswork.
Quick recap
- A migration is one diary page describing one change to your database shape.
- Install
Microsoft.EntityFrameworkCore.Designand thedotnet-eftool to get started. dotnet ef migrations add <Name>writes the file;dotnet ef database updateapplies it.- EF Core uses the
__EFMigrationsHistorytable to apply only what is missing and skip the rest. - Undo unapplied work with
migrations remove; undo applied work by updating to a previous migration first. - Never edit or delete a migration that already shipped — add a new one instead.
- For production, prefer idempotent SQL scripts or migration bundles in a separate deploy step, not migrate-on-startup.
- Keep migration files and the model snapshot in version control, and apply the database change before the new app version.
References and further reading
- Migrations Overview — EF Core (Microsoft Learn)
- Managing Migrations — EF Core (Microsoft Learn)
- Applying Migrations — EF Core (Microsoft Learn)
- Introducing DevOps-friendly EF Core Migration Bundles (.NET Blog)
- EF Core Migrations: A Detailed Guide — Milan Jovanović
Related Posts
Soft Delete with EF Core: Delete Data Without Losing It
Learn soft delete in EF Core the right way. Use an interceptor and global query filters to hide deleted rows automatically, with simple examples, diagrams, code, and best practices for .NET 10.
EF Core Query Splitting: Fix Slow Queries and Cartesian Explosion
Learn how EF Core query splitting (AsSplitQuery) fixes the cartesian explosion problem with simple examples, diagrams, and real performance numbers. Know when to split and when not to.
5 EF Core Features You Need to Know (Beginner Friendly)
Learn 5 must-know EF Core features with simple examples: change tracking, AsNoTracking, eager loading, bulk ExecuteUpdate, and query filters.
Multi-Tenant Applications With EF Core: A Beginner's Guide
Learn multi-tenancy in EF Core the simple way. Isolate tenant data with global query filters, ITenantService, and named filters in .NET 10, with diagrams and code.
Using Multiple EF Core DbContext in a Single Application
Learn how to use multiple EF Core DbContext classes in one .NET app. See when to split, how to register, migrate, and coordinate them with simple examples.
5 Hidden EF Core NuGet Packages That Make Your .NET Code Better
Five lesser-known EF Core NuGet packages for clean exceptions, naming conventions, bulk speed, dynamic queries, and auditing — with simple examples and diagrams.