Skip to main content

Command Palette

Search for a command to run...

.NET Code-First: Understanding EF Migrations

Updated
5 min read
.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:

  1. 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
  1. Apply the Migration

To apply the pending changes, run:

dotnet ef database update

EF Core will:

  • Look inside the Migrations folder for any unapplied migrations

  • Execute the corresponding SQL commands against your database

  • Record the migration name in the _EFMigrationsHistory table

This table acts as a ledger—it tracks which migrations have already been applied, so EF won’t reapply them.

Why It Matters

  • The Migrations folder is your source of truth for schema evolution

  • The _EFMigrationsHistory table ensures consistency across environments

  • If 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:

MethodPurposeUse Case
EnsureCreated()Creates the database if it doesn't exist, but skips migrationsGood for quick prototyping or first run
.Migrate()Applies all pending migrations to the databaseRecommended 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 = false

  • Don’t rely on default false for boolean fields unless that’s truly what you want

A missing isActive = true can 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 Migrations folder

  • Drop the _EFMigrationsHistory table from your database

  • Recreate 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.

A

Nice and simple to understand