Skip to main content
SEMastery
Data Accessbeginner

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.

13 min readUpdated April 5, 2026

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.

Figure 1: Your C# model changes, EF Core compares it to a saved snapshot, and writes a migration that updates the database to match.

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.SqlServer

The 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 --version

Now 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 InitialCreate

EF 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 partWhat it containsShould you edit it?
<timestamp>_InitialCreate.csThe Up() and Down() methods with the actual changesSometimes, carefully
Up() methodSteps to apply the change (create table, add column)Yes, if you know what you are doing
Down() methodSteps to undo the change (drop table, remove column)Yes, to keep undo correct
AppDbContextModelSnapshot.csEF Core's picture of the full current modelNo — 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.

Figure 2: The two main commands. 'migrations add' writes the file. 'database update' runs it against the real database.

Applying the migration

The migration file is just a plan. To run it, use:

// dotnet ef database update

This 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'

Read history
Find pending
Apply in order
Record each

Steps

1

Read history

Look at __EFMigrationsHistory

2

Find pending

Which migrations are not listed yet

3

Apply in order

Run Up() oldest first

4

Record each

Insert a row per applied migration

EF Core uses the history table to apply only what is missing.

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 update

EF 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.

SituationCommandWhat it does
Created a migration, not applied yetdotnet ef migrations removeDeletes the last migration file and rewinds the snapshot
Already applied, want to go backdotnet ef database update <PreviousName>Runs Down() to roll the database back to that point
Want to throw away all changesdotnet ef database update 0Reverts 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 remove

Reverting an applied migration

Bad migration applied
Update to previous
File now unapplied
Remove file

Steps

1

Bad migration applied

It is in the history table

2

Update to previous

database update <PreviousName>

3

File now unapplied

Down() has run

4

Remove file

migrations remove is now safe

Roll the database back first, then delete the file. Order matters.

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 AddCustomerEmail

This 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.

Figure 3: Choosing how to apply migrations. Local development is simple; production should use scripts or bundles in a separate deploy step.

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.sql

You 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.

ApproachBest forNeeds app source/SDK?Safe to re-run?
database updateLocal development, learningYesYes
Idempotent scriptDBA review, regulated teamsNo (script is plain SQL)Yes
Migration bundleCI/CD pipelinesNoYes
Migrate on app startupTiny demos onlyYesRisky 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

Change model
Add migration
Review SQL
Update local DB
Commit + ship

Steps

1

Change model

Edit C# classes

2

Add migration

dotnet ef migrations add Name

3

Review SQL

Read Up/Down or script it

4

Update local DB

database update on dev

5

Commit + ship

Bundle or script in CI/CD

Change model, add migration, review, apply locally, commit. Ship with a bundle or script.

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.cs is 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.Design and the dotnet-ef tool to get started.
  • dotnet ef migrations add <Name> writes the file; dotnet ef database update applies it.
  • EF Core uses the __EFMigrationsHistory table 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

Related Posts