Code First and Migrations
Before you start
You need: configured entities (Article 02).
In Visual Studio: everything here runs in the Package Manager Console — Tools → NuGet Package Manager → Package Manager Console.
Time: about 55 minutes, plus the practice.
Learning objective
Add, review, apply and reverse a migration, and make a schema change to a populated database without losing data.
Topics
- Code first versus database first
- Adding and reviewing a migration
- Applying and reverting
- The
__EFMigrationsHistorytable - Data-losing changes
- Custom SQL in a migration
- Seeding
- Applying migrations in production
- Diagnosing a model mismatch
Code first versus database first
Code first: the C# model is the source of truth; migrations generate the schema. Good when you own the database and it evolves with the application.
Database first: the database is the source of truth; classes are scaffolded from it.
Scaffold-DbContext "Server=.;Database=NexCodingSchool;Integrated Security=True;TrustServerCertificate=True;" Microsoft.EntityFrameworkCore.SqlServer -OutputDir Models
Microsoft.EntityFrameworkCore.SqlServer \
--output-dir Models --context SchoolDbContext --force
Database first suits an existing database you do not control, or one a DBA team manages. --force overwrites, so any hand-edits to scaffolded classes are lost — put customisations in partial classes or separate configuration files.
Do not mix them. Scaffolding over a migration-managed database, or hand-editing a scaffolded model and then adding a migration, produces a model and a schema that disagree in ways that are painful to reconcile.
Adding a migration
All of these run in the Package Manager Console — Tools → NuGet Package Manager → Package Manager Console. Set the Default project dropdown to the project holding your DbContext first; that one setting causes most "No DbContext was found" reports.
Add-Migration InitialCreate
Add-Migration AddStudentAddress
Add-Migration AddFeePaymentCancelledFlag
Each produces three files:
Migrations/
├── 20260827093015_AddStudentAddress.cs Up and Down
├── 20260827093015_AddStudentAddress.Designer.cs model snapshot at this point
└── SchoolDbContextModelSnapshot.cs current model — regenerated each time
public partial class AddStudentAddress : Migration
{
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.AddColumn<string>(
name: "Address",
table: "Student",
type: "nvarchar(250)",
maxLength: 250,
nullable: true);
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.DropColumn(name: "Address", table: "Student");
}
}
Read every generated migration before applying it. EF Core infers intent from a model diff and sometimes infers wrongly — a renamed property looks like a drop plus an add, which loses the data. The migration file is the last point at which that is cheap to fix.
Name migrations for what they do. Migration1, Fix, Update2 tell the next person nothing.
Applying
Update-Database # apply everything pending
Update-Database AddStudentAddress # apply up to a specific one
Get-Migration # what is applied and what is pending
EF Core records applied migrations in __EFMigrationsHistory:
SELECT MigrationId, ProductVersion FROM dbo.__EFMigrationsHistory ORDER BY MigrationId;
That table is how EF Core knows what to run. Deleting rows from it, or restoring a database without it, causes migrations to be re-applied and fail on objects that already exist.
Reverting
Update-Database AddStudentAddress # roll back to just after this migration
Update-Database 0 # revert everything
Remove-Migration # delete the last migration (if unapplied)
migrations remove deletes the migration files and rewinds the snapshot. It refuses if the migration has been applied — revert the database first.
Down is frequently wrong. EF Core generates it mechanically, and a Down that drops a column discards the data in it. Treat rollback as a last resort on production; the safer path is usually a new forward migration that undoes the change.
Data-losing changes
EF Core warns when a migration will lose data, then generates it anyway:
An operation was scaffolded that may result in the loss of data. Please review the migration for accuracy.
Renaming a property
// Before: public string ParentPhone { get; set; }
// After: public string GuardianPhone { get; set; }
EF Core sees a dropped column and a new one:
migrationBuilder.DropColumn(name: "ParentPhone", table: "Student");
migrationBuilder.AddColumn<string>(name: "GuardianPhone", table: "Student", nullable: false, defaultValue: "");
Every phone number is gone. Replace it by hand:
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.RenameColumn(
name: "ParentPhone", table: "Student", newName: "GuardianPhone");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.RenameColumn(
name: "GuardianPhone", table: "Student", newName: "ParentPhone");
}
This is the single most valuable reason to read migrations. Nothing fails; the column is simply empty afterwards.
Adding a required column to a populated table
migrationBuilder.AddColumn<string>(
name: "AcademicYear", table: "FeeAccount", nullable: false, defaultValue: "");
Existing rows get "", which is valid to the database and meaningless to the application. Do it in three steps:
protected override void Up(MigrationBuilder migrationBuilder)
{
// 1. Add as nullable
migrationBuilder.AddColumn<string>(
name: "AcademicYear", table: "FeeAccount", type: "nvarchar(10)",
maxLength: 10, nullable: true);
// 2. Populate with something meaningful
migrationBuilder.Sql(@"
UPDATE dbo.FeeAccount
SET AcademicYear = '2024-25'
WHERE AcademicYear IS NULL;");
// 3. Make it required
migrationBuilder.AlterColumn<string>(
name: "AcademicYear", table: "FeeAccount", type: "nvarchar(10)",
maxLength: 10, nullable: false);
}
Narrowing a type
NVARCHAR(250) to NVARCHAR(100) silently truncates anything longer. Check first:
SELECT COUNT(*) FROM dbo.Student WHERE LEN(Address) > 100;
Custom SQL
protected override void Up(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql(@"
CREATE OR ALTER VIEW dbo.vw_StudentFeeSummary
AS
SELECT s.SchoolId, s.RollNumber, s.Name,
fa.TotalFees - fa.DiscountAmount - fa.PaidAmount AS Outstanding
FROM dbo.Student AS s
JOIN dbo.FeeAccount AS fa ON fa.StudentId = s.Id;");
migrationBuilder.Sql(@"
CREATE NONCLUSTERED INDEX IX_FeePayment_Account_Active
ON dbo.FeePayment (FeeAccountId)
INCLUDE (Amount)
WHERE IsCancelled = 0;");
}
protected override void Down(MigrationBuilder migrationBuilder)
{
migrationBuilder.Sql("DROP INDEX IX_FeePayment_Account_Active ON dbo.FeePayment;");
migrationBuilder.Sql("DROP VIEW IF EXISTS dbo.vw_StudentFeeSummary;");
}
migrationBuilder.Sql is how views, stored procedures, filtered indexes and data fixes get into a migration. Anything EF Core cannot model goes here — and it still gets versioned and applied in order with everything else.
For a long script, keep it as an embedded resource rather than a giant string literal.
Seeding
HasData — model-managed reference data
builder.HasData(
new School { Id = 1, Name = "NexCoding Academy", Code = "NCA", IsActive = true });
EF Core generates inserts in the migration and tracks changes to the seed data across migrations.
Three constraints: keys must be supplied explicitly, values must be constant (no Guid.NewGuid() or DateTime.Now, which change on every scaffold and produce a spurious migration), and it suits small fixed reference data only.
Runtime seeding — test data
public static async Task SeedAsync(SchoolDbContext context, CancellationToken ct)
{
if (await context.Students.AnyAsync(ct))
{
return;
}
context.Students.AddRange(
new Student { SchoolId = 1, Name = "Ravi Kumar", RollNumber = "NCA-2024-0012",
ClassName = "10th", Section = "A", ParentName = "Suresh Kumar",
ParentPhone = "9951510727", DateOfBirth = new DateTime(2009, 5, 14) },
new Student { SchoolId = 1, Name = "Priya Sharma", RollNumber = "NCA-2024-0018",
ClassName = "10th", Section = "A", ParentName = "Anil Sharma",
ParentPhone = "9848012345", DateOfBirth = new DateTime(2009, 8, 2) });
await context.SaveChangesAsync(ct);
}
The AnyAsync guard makes it idempotent. Use HasData for reference data the application depends on, runtime seeding for development and test data.
Applying migrations in production
// Convenient, and wrong for production
await context.Database.MigrateAsync();
Three problems: several instances starting together race each other; the application needs schema-altering permissions permanently; and a failure leaves the schema half-changed with the application already serving traffic.
Generate a script instead and let it go through the same review as any other change:
# From the last applied migration to the latest
Script-Migration AddStudentAddress AddFeePaymentCancelledFlag -Output migrate.sql
# Idempotent — safe to run against any state
Script-Migration -Idempotent -Output migrate.sql
The idempotent script wraps each migration in a check against __EFMigrationsHistory, so running it twice is harmless. That is the one to hand to whoever deploys.
Read the script before it runs on production data. A DROP COLUMN in a deployment script is not recoverable from the application side.
Diagnosing a model mismatch
"The model backing the context has changed." Migrations are pending. Run Get-Migration and apply them with Update-Database.
"There is already an object named 'Student' in the database." __EFMigrationsHistory is out of step with the schema — commonly after restoring a database without it, or after someone created tables by hand. Fix by baselining: generate an initial migration and mark it applied without running it.
Add-Migration InitialCreate
Script-Migration -Idempotent -Output baseline.sql
# then insert the migration row manually, or apply against an empty database
"Unable to create a 'DbContext'." The CLI cannot construct your context. Add an IDesignTimeDbContextFactory, or check the Design package is referenced.
A migration was generated with no changes made. A non-deterministic value in HasData — Guid.NewGuid() or DateTime.Now — makes the snapshot differ every time. Replace it with a constant.
Two developers generated migrations in parallel. Both edited SchoolDbContextModelSnapshot.cs, so it conflicts in Git. Resolve by keeping one, reverting the other's migration, and regenerating it on top. Merging the snapshot by hand does not work reliably.
Errors you will hit
| Message | Cause | Fix |
|---|---|---|
The model backing the context has changed | Migrations pending | Update-Database |
There is already an object named 'Student' in the database | Migration applied to a database that already had the tables | Baseline it, or drop and rebuild |
Add-Migration produces an empty migration | No model change since the last one | Expected |
Remove-Migration refuses | The migration is already applied | Update-Database to the previous one first |
Column names in each table must be unique | A rename modelled as add plus drop | Edit the migration to use RenameColumn |
| Data disappears after a migration | A drop-and-add where a rename was meant | Always read the generated file before applying |
Read every generated migration before applying it. EF Core models a rename as a drop plus an add unless told otherwise, and that silently discards the column's data.
Common mistakes
- Applying a migration without reading it
- Letting a rename generate a drop plus an add, losing the column's data
- Adding a required column with a meaningless default to populated rows
- Narrowing a type without checking existing lengths
- Trusting
Downto be correct - Editing an applied migration instead of adding a new one
- Deleting rows from
__EFMigrationsHistory Database.MigrateAsync()on startup in production- Non-deterministic values in
HasData - Mixing code first and database first
- Merging a model snapshot conflict by hand
Practice
The course exercises here are create a Code First model and run migrations.
- Generate
InitialCreateand read the whole file. List anything you did not intend. - Apply it and inspect
__EFMigrationsHistory. - Add an
Addressproperty, generate a migration, apply it. - Rename
ParentPhonetoGuardianPhone. Read the generated migration. Confirm it drops and adds. Apply it against seeded data and confirm the numbers are gone. - Revert, rewrite the migration with
RenameColumn, apply it, and confirm the data survives. - Add a required
AcademicYearto a populatedFeeAccounttable in one step. Inspect the resulting values. Then do it properly in three steps. - Narrow
Addressfrom 250 to 100 with a row longer than 100. Record what happens. - Add a view and a filtered index with
migrationBuilder.Sql, including a workingDown. - Seed one
SchoolwithHasData, and students at runtime with anAnyAsyncguard. Run twice and confirm no duplicates. - Put
Guid.NewGuid()inHasData, then runmigrations addtwice with no model change. Confirm the spurious migration. - Generate an idempotent script and run it twice against the same database.
- Revert to migration 0, then re-apply everything.
Then run the course debugging exercise — fix a migration mismatch. Create the schema by hand from a script, point EF Core at it, and resolve the "object already exists" error by baselining. Separately, have two branches each add a migration, merge them, and resolve the snapshot conflict correctly.
Exercise 4 is the one that matters. It loses real data, silently, and only reading the migration prevents it.
You can now
- Create, apply and revert migrations from the Package Manager Console
- Read a generated migration and spot a data-losing change
- Script migrations for a DBA to run
- Baseline an existing database
- Say what
Update-Database 0does
Review questions
- Why does renaming a property lose data, and how do you prevent it?
- What is
__EFMigrationsHistoryfor, and what breaks if rows are removed? - Why is
Database.MigrateAsync()on startup a poor production practice? - Why does a non-deterministic value in
HasDataproduce a migration every time?
Next: Querying with LINQ