.NET Code-First: Understanding EF Migrations

Entity Framework Core’s Code-First strategy empowers developers to design their database schema directly from C# models. It’s clean, expressive, and ideal for agile development. But to use it effectively, you need to understand how migrations work, how to seed data, and how EF tracks schema changes across environments.
What is Code-First?
In Code-First, you define your data models in C#:
public class Expense
{
public int Id { get; set; }
public string Type { get; set; }
public decimal Amount { get; set; }
public DateTime Date { get; set; }
}
EF Core then generates the database schema based on these models—no manual SQL required.
EF Migrations: Creating and Applying
Migrations are snapshots of your model changes, any new table, or changes to existing table. You create and apply them like this:
dotnet ef migrations add AddExpenseTable
dotnet ef database update
We will look at these commands in detail, just make sure you're targeting the correct environment and database—especially during development.
🧱Looking into Migrations Folder and _EFMigrationsHistory Table
Entity Framework Core uses two key components to manage schema changes: the Migrations folder in your project and the _EFMigrationsHistory table in your database.
Let’s look at these step by step:
Scaffold a Migration
When you run:
dotnet ef migrations add ExpenseTable
EF Core generates a new C# file inside the Migrations folder. This file contains the schema changes based on your updated model. At this point, the migration is pending—it hasn’t been applied to the database yet.
In case you run the migrations add command by mistake and you think the DB design is not complete to be applied yet, you can undo the migrations add command and delete the last entry in Migrations folder:
dotnet ef migrations remove
Apply the Migration
To apply the pending changes, run:
dotnet ef database update
EF Core will:
Look inside the
Migrationsfolder for any unapplied migrationsExecute the corresponding SQL commands against your database
Record the migration name in the
_EFMigrationsHistorytable
This table acts as a ledger—it tracks which migrations have already been applied, so EF won’t reapply them.
Why It Matters
The
Migrationsfolder is your source of truth for schema evolutionThe
_EFMigrationsHistorytable ensures consistency across environmentsIf you delete a migration file but don’t clean the history table, EF may get confused
If you reset the folder, you should also drop the history table to start fresh
⚠️Common Pitfall: Wrong Database Target
If your app is pointing to some online DB during development, your migration will apply there—even if you intended to update your local DB. This can lead to:
Schema drift between environments
Unintended production changes
Confusing deployment behavior
Solution: Be Explicit
Double check the target database, confirm the connection string. Use the --connection flag or verify your appsettings.json before running updates:
dotnet ef database update --connection "Server=localhost;Database=MyLocalDb;..."
Another way to apply migration after dotnet ef migrations add ExpenseTable is to call .Migrate() either at startup Program.cs or manually through some endpoint.
context.Database.Migrate();
🔍EnsureCreated() vs .Migrate()
These two methods are often misunderstood:
| Method | Purpose | Use Case |
EnsureCreated() | Creates the database if it doesn't exist, but skips migrations | Good for quick prototyping or first run |
.Migrate() | Applies all pending migrations to the database | Recommended for production as it applies incremental changes |
.EnsureCreated() will create the database if app is running first time or you changed the database server like setting up a new environment or deploying first time, but it will not keep any track of these changes.
.EnsureCreated() does not create and use _EFMigrationsHistory table.
Best Practice
Call .Migrate() during app startup in production:
using (var scope = app.Services.CreateScope())
{
var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
db.Database.Migrate();
}
🌱Seeding Data: When and How?
There are scenarios when your applications needs some bootstrap data to be already available in the database before first run, e.g. Admin login credentials, default roles or settings etc. It can be inserted in the database during migration, it is called Seeding.
EF Core supports data seeding via OnModelCreating:
modelBuilder.Entity<ExpenseType>().HasData(
new Expense { Id = 1, Type = "Travel", Date = DateTime.Today }
);
This data will be inserted during dotnet ef database update or .Migrate().
⚠️Seeding Gotchas: Identity Columns and Flags
EF Core’s seeding logic doesn’t run constructors or default value logic. So:
Always set identity columns like
Id,Guid, etc.Explicitly set flags like
isActive = true,isDeleted = falseDon’t rely on default
falsefor boolean fields unless that’s truly what you want
A missing
isActive = truecan silently disable a record—even though it looks fine in the DB.
Cleaning Up Migrations: Starting Fresh
Sometimes you need a clean slate—especially during early development or after major refactoring. Here’s how to reset your migration history. You can:
Delete all files in the
MigrationsfolderDrop the
_EFMigrationsHistorytable from your databaseRecreate and apply initial migration:
dotnet ef migrations add InitialCreate
dotnet ef database update
⚠️ Only do this in development or staging environments. Never reset migrations in production unless you're rebuilding the entire DB.
Migration Checklist:
Here is the checklist for quick reference, to keep your migrations clean and predictable:
✅ Log your connection string during startup
✅ Use .Migrate() in production, not .EnsureCreated()
✅ Keep migrations in source control
✅ Seed essential data with explicit values e.g. isActive:true. Avoid relying on default false for booleans
✅ Remove unused migrations with dotnet ef migrations remove
✅ Confirm schema changes post-deploy with a /health endpoint
✅ Use environment-specific configs (appsettings.Development.json, etc.)
✅ Drop _EFMigrationsHistory only when starting fresh, Reset only in dev/staging—never in production
Final Thoughts
EF Core’s Code-First approach is powerful—but it demands clarity and discipline. By understanding migrations, seeding, and deployment flow, you’ll build resilient apps that scale with confidence.
Whether you're deploying to self-managed infrastructure or cloud or syncing across environments, a clean schema is the foundation of a smooth experience.
📘Thanks for reading!
This post is part of Tech It Easy—my blog where I share real-world solutions, deployment strategies, and developer insights from the trenches. If this helped you navigate EF Core migrations with more confidence, consider sharing it or dropping a comment. Let’s make tech easier, together.






