EF Core Database Migration Guide — Add, Update, and Rollback

In this article we going to learn how to do database migrations in Entity Framework Core — how to add a migration, apply it to the database, and roll back when something goes wrong.

If you've worked with EF Core for more than a week, you've already hit the migration workflow. You add a new property to your model, run the app, and suddenly it crashes because the database doesn't have that column yet. That's where migrations come in — they keep your database schema in sync with your C# models automatically.

The concept is simple. Every time you change your model, you create a migration. The migration is a C# file that describes exactly what changed — add this column, remove that table, change this index. When you apply the migration, EF Core runs those changes against the database. Your schema stays in sync with your code without you writing any SQL by hand.

But there's more to it than just running a couple of commands. You need to know how to handle rollbacks when a migration breaks something, how to manage migrations in a team where multiple developers are adding migrations at the same time, and how to apply migrations safely in a production environment. Let me walk through all of it.

This tutorial shows how to:

  • Set up EF Core with a DbContext and your first model
  • Create your first migration using the CLI
  • Apply the migration to update the database
  • Add more migrations as your models change
  • Roll back to a previous migration when something goes wrong
  • Remove a migration that hasn't been applied yet
  • Handle migrations in production safely
  • Avoid common mistakes like merge conflicts in migrations

Why EF Core Migrations

Without migrations, keeping your database schema in sync with your code is a manual process — write an ALTER TABLE script, run it against dev, then staging, then production, hope nobody forgets a step. In a team with multiple developers it gets messy fast.

EF Core migrations give you a version control system for your database schema. Every change is tracked in a C# file with a timestamp. You can see exactly what changed and when. You can apply changes, roll them back, and script them for deployment. It's the same idea as git for your code but for your database schema.


Step 1 : Set Up EF Core in Your .NET Project

If you don't have EF Core set up yet, start here. Install the packages :

dotnet add package Microsoft.EntityFrameworkCore
dotnet add package Microsoft.EntityFrameworkCore.SqlServer
dotnet add package Microsoft.EntityFrameworkCore.Tools

Create your model classes. Let's use a simple example — a blog with posts :

public class Blog
{
    public int BlogId { get; set; }
    public string Title { get; set; } = string.Empty;
    public string Author { get; set; } = string.Empty;
    public DateTime CreatedAt { get; set; }
}

public class Post
{
    public int PostId { get; set; }
    public string Title { get; set; } = string.Empty;
    public string Content { get; set; } = string.Empty;
    public bool IsPublished { get; set; }
    public DateTime PublishedAt { get; set; }

    // Foreign key
    public int BlogId { get; set; }
    public Blog Blog { get; set; } = null!;
}

Create your DbContext :

using Microsoft.EntityFrameworkCore;

public class AppDbContext : DbContext
{
    public AppDbContext(DbContextOptions<AppDbContext> options) : base(options) { }

    public DbSet<Blog> Blogs { get; set; }
    public DbSet<Post> Posts { get; set; }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        modelBuilder.Entity<Blog>()
            .Property(b => b.Title)
            .IsRequired()
            .HasMaxLength(200);

        modelBuilder.Entity<Post>()
            .Property(p => p.Title)
            .IsRequired()
            .HasMaxLength(300);
    }
}

Register it in Program.cs :

builder.Services.AddDbContext<AppDbContext>(options =>
    options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")));

And add the connection string in appsettings.json :

{
  "ConnectionStrings": {
    "DefaultConnection": "Server=(localdb)\\mssqllocaldb;Database=BlogDb;Trusted_Connection=True;"
  }
}

Step 2 : Create Your First Migration

Now that the models and DbContext are ready, create the first migration. In the terminal, navigate to your project folder and run :

dotnet ef migrations add InitialCreate

This generates a Migrations folder in your project with three files :

  • 20241215120000_InitialCreate.cs — the migration file with Up() and Down() methods
  • 20241215120000_InitialCreate.Designer.cs — snapshot metadata used internally by EF Core
  • AppDbContextModelSnapshot.cs — a snapshot of your current model state

Open the migration file and have a look at it :

public partial class InitialCreate : Migration
{
    protected override void Up(MigrationBuilder migrationBuilder)
    {
        migrationBuilder.CreateTable(
            name: "Blogs",
            columns: table => new
            {
                BlogId = table.Column<int>(type: "int", nullable: false)
                    .Annotation("SqlServer:Identity", "1, 1"),
                Title = table.Column<string>(type: "nvarchar(200)", maxLength: 200, nullable: false),
                Author = table.Column<string>(type: "nvarchar(max)", nullable: false),
                CreatedAt = table.Column<DateTime>(type: "datetime2", nullable: false)
            },
            constraints: table =>
            {
                table.PrimaryKey("PK_Blogs", x => x.BlogId);
            });

        migrationBuilder.CreateTable(
            name: "Posts",
            columns: table => new
            {
                PostId = table.Column<int>(type: "int", nullable: false)
                    .Annotation("SqlServer:Identity", "1, 1"),
                Title = table.Column<string>(type: "nvarchar(300)", maxLength: 300, nullable: false),
                Content = table.Column<string>(type: "nvarchar(max)", nullable: false),
                IsPublished = table.Column<bool>(type: "bit", nullable: false),
                PublishedAt = table.Column<DateTime>(type: "datetime2", nullable: false),
                BlogId = table.Column<int>(type: "int", nullable: false)
            },
            constraints: table =>
            {
                table.PrimaryKey("PK_Posts", x => x.PostId);
                table.ForeignKey(
                    name: "FK_Posts_Blogs_BlogId",
                    column: x => x.BlogId,
                    principalTable: "Blogs",
                    principalColumn: "BlogId",
                    onDelete: ReferentialAction.Cascade);
            });
    }

    protected override void Down(MigrationBuilder migrationBuilder)
    {
        migrationBuilder.DropTable(name: "Posts");
        migrationBuilder.DropTable(name: "Blogs");
    }
}

The Up() method runs when you apply the migration — creates tables, adds columns, creates indexes. The Down() method runs when you roll back — drops those same tables. EF Core generates both automatically from your model changes.

Never manually edit migration files unless you have a very specific reason. EF Core tracks the model snapshot separately and editing a migration file without matching snapshot can cause issues.


Step 3 : Apply the Migration to the Database

Now apply the migration to actually create the tables in your database :

dotnet ef database update

EF Core runs the Up() method from all pending migrations in order. For the first run it creates the Blogs and Posts tables plus a __EFMigrationsHistory table. That history table is how EF Core tracks which migrations have already been applied — it records the migration name and the EF Core version.

Run this command and check your database — you should see the Blogs, Posts, and __EFMigrationsHistory tables.


Step 4 : Add More Migrations as Models Change

A few weeks later the requirements change. You need to add a Tags property to Blog and a ViewCount to Post.

Update your models :

public class Blog
{
    public int BlogId { get; set; }
    public string Title { get; set; } = string.Empty;
    public string Author { get; set; } = string.Empty;
    public DateTime CreatedAt { get; set; }
    public string Tags { get; set; } = string.Empty;  // New
}

public class Post
{
    public int PostId { get; set; }
    public string Title { get; set; } = string.Empty;
    public string Content { get; set; } = string.Empty;
    public bool IsPublished { get; set; }
    public DateTime PublishedAt { get; set; }
    public int ViewCount { get; set; }  // New
    public int BlogId { get; set; }
    public Blog Blog { get; set; } = null!;
}

Create a new migration :

dotnet ef migrations add AddTagsAndViewCount

Apply it :

dotnet ef database update

EF Core compares the current model snapshot against the database's applied migrations and generates only the new changes — AddColumn for Tags and AddColumn for ViewCount. It doesn't recreate tables it already created.

Each migration builds on the previous one. That's why the migration name matters — use meaningful names like AddUserProfileTable, AddIndexOnEmail, RenameProductToItem. Future you (and your teammates) will thank you when looking at migration history.


Step 5 : Roll Back to a Previous Migration

Something went wrong. The new migration caused an issue in production or you realize the change was wrong. Roll back to the previous migration.

To roll back to a specific migration, pass its name to database update :

dotnet ef database update InitialCreate

This runs the Down() method of AddTagsAndViewCount migration — removes the Tags column from Blogs and ViewCount column from Posts. Your database goes back to the state after InitialCreate.

To roll back ALL migrations and get an empty database :

dotnet ef database update 0

The 0 means go back before any migration was applied. This runs Down() for every migration in reverse order and drops all tables. Use this carefully — especially not in production.

After rolling back, the migrations still exist as files in your project. EF Core just hasn't applied them to the database. You can apply them again anytime with database update.


Step 6 : Remove a Migration That Hasn't Been Applied

You created a migration but haven't applied it to the database yet. You realize it's wrong and want to redo it.

dotnet ef migrations remove

This deletes the latest migration file and updates the model snapshot to the previous state. Only works if the migration hasn't been applied to the database yet. If it has, you need to roll back first using database update to the previous migration, then remove.

Never delete migration files manually. Always use migrations remove. Manually deleting leaves the model snapshot out of sync with the migration files and causes all sorts of confusing errors.


Step 7 : List All Migrations and Their Status

Want to see all migrations and which ones have been applied to the database?

dotnet ef migrations list

Output looks something like this :

20241201093000_InitialCreate (Applied)
20241215120000_AddTagsAndViewCount (Applied)
20241220085000_AddSlugToPost (Pending)

Pending means the migration file exists but hasn't been applied to the database yet. This is useful for checking what needs to run before a deployment.


Step 8 : Apply Migrations Programmatically at App Startup

For development environments and simple apps, you can apply migrations automatically when the app starts instead of running CLI commands manually.

In Program.cs :

var app = builder.Build();

// Apply pending migrations on startup
using (var scope = app.Services.CreateScope())
{
    var db = scope.ServiceProvider.GetRequiredService<AppDbContext>();
    db.Database.Migrate();
}

Database.Migrate() applies all pending migrations when the app starts. If there are no pending migrations, it does nothing.

This is convenient for development but think carefully before using it in production. If a migration fails halfway through, you end up in a broken state and the app can't start. For production, it's safer to apply migrations as a separate deployment step with proper monitoring and the ability to rollback.


Step 9 : Generate SQL Script for Production Deployments

In production you probably don't want to run EF Core commands directly against the database. Instead, generate a SQL script, review it, test it in staging, then run it in production.

Generate a script for all migrations :

dotnet ef migrations script --output migrations.sql

This generates a .sql file with all the SQL statements that EF Core would run. You can review it, have a DBA check it, test it in staging, and then apply it in production using SSMS or your deployment pipeline.

Generate a script for migrations between two specific points :

dotnet ef migrations script InitialCreate AddTagsAndViewCount --output delta.sql

This generates only the SQL for migrations between InitialCreate and AddTagsAndViewCount. Useful for incremental deployments.

Add the --idempotent flag to generate a script that checks migration history before running each step — safe to run multiple times :

dotnet ef migrations script --idempotent --output migrations.sql

The idempotent script wraps each migration in an IF NOT EXISTS check against __EFMigrationsHistory. So if you run it twice, the already-applied migrations are skipped safely.


Common Issues and Fixes

"No migrations configuration type was found"

Make sure you have Microsoft.EntityFrameworkCore.Tools installed. Also check that you're running the command from the correct project folder — the one that contains the DbContext.

"Unable to create an object of type AppDbContext"

EF Core needs to instantiate your DbContext at design time to read the model. If your DbContext requires a connection string from configuration, EF Core can't always get it during a CLI command. Add a design-time factory :

public class AppDbContextFactory : IDesignTimeDbContextFactory<AppDbContext>
{
    public AppDbContext CreateDbContext(string[] args)
    {
        var optionsBuilder = new DbContextOptionsBuilder<AppDbContext>();
        optionsBuilder.UseSqlServer("Server=(localdb)\\mssqllocaldb;Database=BlogDb;Trusted_Connection=True;");

        return new AppDbContext(optionsBuilder.Build());
    }
}

Migration conflicts in team projects

Two developers add migrations at the same time. When they merge, EF Core gets confused because two migrations think they follow the same previous migration.

Fix — one developer deletes their migration, pulls the other developer's migration, then recreates their own migration on top. The merged snapshot will be consistent.

To minimize this, coordinate with your team. Keep migrations small and focused. Merge main into your branch before creating a migration to reduce conflicts.

"There is already an object named X in the database"

The migration is trying to create a table that already exists. This usually happens when someone created the table manually or a migration was partially applied. Check __EFMigrationsHistory to see what's been applied, and compare with the actual database state.


Summary

You learned how to use EF Core migrations to manage database schema changes. You covered :

  • Setting up EF Core with a DbContext, models, and connection string
  • Creating the first migration with dotnet ef migrations add InitialCreate
  • Applying migrations with dotnet ef database update
  • Adding subsequent migrations as models change over time
  • Rolling back to a specific migration with dotnet ef database update MigrationName
  • Rolling back everything with dotnet ef database update 0
  • Removing an unapplied migration with dotnet ef migrations remove
  • Listing all migrations and their applied status with dotnet ef migrations list
  • Applying migrations programmatically at app startup with Database.Migrate()
  • Generating SQL scripts for production deployments with --idempotent flag
  • Fixing common issues like design-time factory errors and merge conflicts

Migrations are one of those things where the basic commands are simple but knowing what to do when something goes wrong is what really matters. Roll back carefully, never delete migration files manually, and always generate a SQL script before running migrations in production.

I hope you like this article...

Happy coding! 🚀

Post a Comment

0 Comments