Migrations
By the end of this lesson
Evolve a schema safely, with changes reviewable in version control.
Your model changes as the application grows. A migration is the record of one such change: a C# file describing how to move the database from the previous shape to the new one, and how to go back.
The alternative is a folder of loose SQL scripts and a shared understanding of which have been run. That works until two people apply them in different orders, or a script is run twice, or someone changes a column by hand on a Friday. A migration removes the guesswork by recording the sequence and letting the database remember where it has reached.
Migrations are ordinary source files. They belong in version control alongside the model change that required them, they show up in a pull request, and a colleague can read one before it touches a database.
# Create a migration from the difference between your model and the last one
dotnet ef migrations add AddDepartmentBudget
# Apply everything not yet applied to the database in your connection string
dotnet ef database update
# See what exists and what has been applied
dotnet ef migrations list
# Remove the newest migration -- only if it has NOT been applied anywhere
dotnet ef migrations remove
# Produce a SQL script instead of connecting to the database
dotnet ef migrations script --idempotent --output deploy/schema.sql- The name after add is yours to choose and it is permanent. AddDepartmentBudget tells a future reader what the file does; Migration7 tells them nothing.
- database update applies every pending migration in order. Run it twice and the second run does nothing, because the database records what it has already applied.
- migrations remove deletes the newest migration file. It is safe only while that migration exists on your machine alone. Once it has been applied or pushed, removing it means other databases and other developers disagree about history.
- The script command is how production deployments usually work: generate SQL, have someone review it, run it through your normal release process. The idempotent option makes the script safe to run against a database at any point in the sequence.
- Add --project and --startup-project when your context lives in a different project from the entry point, which is common once a solution has more than one project.
public partial class AddDepartmentBudget : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<decimal>(
name: "AnnualBudget",
table: "Departments",
type: "decimal(18,2)",
nullable: false,
defaultValue: 0m);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(
name: "AnnualBudget",
table: "Departments");
}
}- Up moves the database forward. Down undoes it. Both are generated for you, and both are plain C# you can read and change before anything is applied.
- The timestamp at the front of the filename fixes the order. This is why two developers adding migrations on the same day usually get a clean sequence, and why merging two branches that each added one needs a look rather than a blind accept.
- A non-nullable column added to a table with existing rows needs a default, or the statement fails. EF Core supplied 0m here. That default applies to existing rows only — it is a migration detail, not a rule your model enforces afterwards.
- Notice that Down for this migration drops the column, and therefore discards every value stored in it. Reversible in schema terms; not reversible in data terms.
The working loop, once per schema change:
Change the model
Add the property, the entity or the configuration. Nothing has happened to any database yet, and your application will fail against the old schema if you run it now.
Add a migration
dotnet ef migrations add with a name that describes the change. EF Core compares your model against a snapshot of the last migration and writes the difference. The snapshot file is updated too, so include it in the commit.
Read what it generated
Open the file. Confirm it does what you meant, and nothing else. This step takes under a minute and is the one that prevents the expensive mistakes.
Apply it locally
dotnet ef database update against your development database. Then run the application and exercise the affected feature, because a migration that applies cleanly can still map incorrectly.
Commit the model change and the migration together
They are one change. Committing the model without the migration gives your colleagues code that cannot run; committing the migration without the model gives them a schema nothing uses.
Deploy with a reviewed script
For production, generate SQL with migrations script rather than pointing dotnet ef at a live database. The script can be reviewed, kept as a record of what was run, and applied by whoever normally applies changes.
ALTER TABLE [Departments] ADD [AnnualBudget] decimal(18,2) NOT NULL DEFAULT 0.0;
GO
INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion])
VALUES (N'20260214093000_AddDepartmentBudget', N'9.0.0');
GO- One ALTER TABLE, which is what the migration described. Reading the SQL is the most direct way to know what will happen, especially for a change involving existing data.
- The second statement is how the database remembers. __EFMigrationsHistory is a small table EF Core creates, holding one row per applied migration.
- That table is why database update is safe to run repeatedly: EF Core reads the history, compares it with the migration files, and applies only what is missing.
- It is also why editing an applied migration does not help. The history records the identifier, not the contents, so a database that has the row will never run your edited version.
Summary
- A migration is generated C# recording one schema change, with an Up and a Down
- Migrations belong in version control, committed with the model change that needed them
- __EFMigrationsHistory lets the database track what it has applied, so update is safe to repeat
- Read every migration before applying it — a rename generates a drop and an add
- Never edit an applied migration; add a new one. Dropping a column loses its data for good
Practice
Attempt each one before opening the solution. Getting it wrong first is how the idea sticks.
Try it yourself
Add a column, read the migration, apply it
Add a nullable DateOnly property called LastOrderedOn to Product. Create a migration named AddProductLastOrderedOn and open the generated file before applying anything.
Identify the AddColumn call, confirm the nullability matches what you wrote, then apply it and check the Products table.
Show solution
The generated Up contains one AddColumn with nullable: true, because the C# property is nullable. Had you written a non-nullable DateOnly, EF Core would have needed a default value for the existing rows, and the difference would be visible in the file.
Reading the file first is the habit the exercise is for. On a change this small it feels unnecessary, which is exactly why it is worth practising now — you want the habit established before the migration that renames a column and quietly drops its data.
Down drops the column. Applying and reverting this migration loses nothing today, because the column is empty. Repeat the same revert in three months and you have deleted every date recorded in it.
dotnet ef migrations add AddProductLastOrderedOn
# open Migrations/*_AddProductLastOrderedOn.cs and read it
dotnet ef database update
dotnet ef migrations listChallenge
Rename without losing data
Rename Customer.CompanyName to Customer.OrganisationName, then generate a migration and read it.
Explain what EF Core produced, why it is dangerous on a table with real rows, and how you would change the file so that no data is lost.
Show solution
EF Core generates DropColumn for CompanyName and AddColumn for OrganisationName. It compares two models and sees one column gone and one arrived; a rename is not something it can detect.
Applied to a populated table, that pair of statements deletes every company name and creates an empty column in its place. The migration succeeds, the application runs, and the data is gone.
Replace both calls with a single migrationBuilder.RenameColumn, giving the table, the old name and the new name. The database renames the column in place and the values stay where they are. Add the matching RenameColumn to Down, reversed, so the migration is still revertible.
The wider point is that a generated migration is a proposal. EF Core knows the difference between two models; only you know the intent behind it. This is the single most valuable reason to read what was generated.
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.RenameColumn(
name: "CompanyName",
table: "Customers",
newName: "OrganisationName");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.RenameColumn(
name: "OrganisationName",
table: "Customers",
newName: "CompanyName");
}Knowledge check
Nothing is recorded and there is no score. The explanation appears either way.
Saved in this browser only.