.NET EF Core Migrations: Evolve Your Database Safely

Learn .NET EF Core Migrations: Evolve Your Database Safely with practical examples, explanations, best practices, and real-world coding techniques.

Imagine you are building an e-learning platform. Initially, your database contains a Users table. Later, you add courses, enrollments, payments, and learning progress. Eventually, the business requires a new column to store a product SKU, an additional table for course categories, or an index to improve query performance.

Whenever your application data model changes, the database schema may also need to change. Manually updating every database environment can become difficult to manage and reproduce. You need a consistent way to record what changed, review the changes, and apply them in the correct order.

Entity Framework Core (EF Core) migrations provide this capability. They record changes between versions of your EF Core data model and generate instructions for updating the database schema. Migrations can be stored alongside application code, reviewed in source control, tested before deployment, and applied to different environments.

Understanding migrations is essential when maintaining database-backed .NET applications. A migration is more than a command that updates a table: it is a versioned database change that can affect existing records, application compatibility, deployment order, and data integrity.

1. What Are EF Core Migrations?

EF Core migrations are versioned instructions that describe how to move a database schema from one state to another. EF Core compares the current model in your application with the model represented by the previous migration snapshot and generates a migration containing the required changes.

For example, suppose your application originally defines a Product entity with an Id and Name. Later, you add a Sku property. The C# model changes, but the corresponding database table does not automatically gain a new column simply because you edited the class.

A migration can record the operation needed to add that column. Applying the migration updates the database schema so that it matches the intended model.

Migrations can represent many schema changes, including:

  • Creating or dropping tables.
  • Adding, renaming, or removing columns.
  • Changing column types, lengths, or nullability.
  • Creating indexes and unique constraints.
  • Adding or modifying foreign keys and relationships.
  • Executing carefully designed SQL for data transformations.

A migration does not automatically guarantee that a change is safe. For example, dropping a column may permanently remove information, and making a nullable column required may fail if existing rows contain null values. Every generated migration should be reviewed before it is applied to an important database.

2. How Migrations Fit into EF Core

EF Core uses entity classes and a model configuration to understand how your application's objects correspond to database tables, columns, keys, and relationships. Migrations provide a way to evolve the database schema as that model changes.

A typical workflow involves four important parts:

  • Entity model: Your C# classes and EF Core configuration describe the intended database structure.
  • Migration files: Generated code records the operations that move the schema from one version to another.
  • Model snapshot: EF Core maintains a snapshot of the model associated with the latest migration. It uses this information to identify model changes when another migration is created.
  • Migration history: EF Core records applied migrations in the database, normally in a table named __EFMigrationsHistory.

The migration files are normally committed to source control. This allows a development team to review schema changes just as it reviews changes to application code.

To understand how entities and database operations fit into the larger application, explore Data Access in .NET and .NET Application Architectures.

3. Preparing Your Project

Before creating migrations, configure EF Core in your project. You generally need a database provider, the EF Core design-time package, and the EF Core command-line tools.

For a project using SQL Server, install the provider package:

dotnet add package Microsoft.EntityFrameworkCore.SqlServer

Install the design package that supports EF Core tooling operations for your project:

dotnet add package Microsoft.EntityFrameworkCore.Design

Install the EF Core command-line tool if it is not already available:

dotnet tool install --global dotnet-ef

If the tool is already installed, update it when appropriate instead of installing it again. Keep the EF Core provider, design package, and tools compatible with the EF Core version used by your project.

If you use PostgreSQL, SQLite, or another database, install the appropriate EF Core provider instead of the SQL Server provider. Migration operations depend on the configured provider, and some database-specific capabilities or generated SQL can differ.

If you need help understanding package installation, see Managing NuGet Packages in .NET.

4. Creating an Entity and DbContext

Imagine your application needs to store products. Start by defining an entity representing a database record.

public sealed class Product
{
    public int Id { get; set; }

    public string Name { get; set; } = string.Empty;
}

Next, define the database context that exposes the entity through a DbSet.

using Microsoft.EntityFrameworkCore;

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

    public DbSet<Product> Products => Set<Product>();
}

Register the context with the appropriate database provider in your application. For SQL Server, the configuration may look like this:

var builder = WebApplication.CreateBuilder(args);

var connectionString =
    builder.Configuration.GetConnectionString("DefaultConnection")
    ?? throw new InvalidOperationException(
        "Database connection string is not configured.");

builder.Services.AddDbContext<AppDbContext>(options =>
    options.UseSqlServer(connectionString));

var app = builder.Build();

app.Run();

The example assumes that the application has a suitable connection string and uses the SQL Server provider. Store environment-specific connection details in configuration and use secure secret management for production credentials.

When EF Core tooling creates a migration, it needs to discover the context and construct the model at design time. Depending on your project structure, it may use the application's service provider or require a design-time context factory. A context that cannot be created by the tooling must be configured appropriately before migration commands can succeed.

5. Creating Your First Migration

Once your entity model and context are configured, create a migration using the EF Core CLI. Run the command from the directory containing the relevant project file:

dotnet ef migrations add InitialCreate

The name InitialCreate is a descriptive identifier for the migration. You could choose another meaningful name, such as AddProducts or CreateCourseTables, depending on the changes being recorded.

EF Core normally generates files in the project's Migrations directory. These files typically include a migration class, a designer file containing model metadata, and an updated model snapshot.

If the project contains multiple contexts or separates the data-access project from the startup project, you may need to specify both projects explicitly:

dotnet ef migrations add InitialCreate --context AppDbContext --project MyApp.Data --startup-project MyApp.Api

Here, --project identifies the project where the context and migration files belong, while --startup-project identifies the project used by the tooling to obtain configuration and application services. Replace these example names with your actual project names.

6. Reviewing the Generated Migration

Creating a migration generates code that describes the changes EF Core has identified. For example, an initial migration might create a Products table.

using Microsoft.EntityFrameworkCore.Migrations;

public partial class InitialCreate : Migration
{
    protected override void Up(MigrationBuilder migrationBuilder)
    {
        migrationBuilder.CreateTable(
            name: "Products",
            columns: table => new
            {
                Id = table.Column<int>(
                    nullable: false)
                    .Annotation(
                        "SqlServer:Identity",
                        "1, 1"),

                Name = table.Column<string>(
                    maxLength: 200,
                    nullable: false)
            },
            constraints: table =>
            {
                table.PrimaryKey(
                    "PK_Products",
                    x => x.Id);
            });
    }

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

This is an illustrative SQL Server migration, and the exact generated code depends on the model, provider, and EF Core version. For this example, the model must configure the Name property with a maximum length of 200 characters for the migration to produce that column definition.

The Up() method describes how to apply the change. The Down() method describes the reverse operation for scenarios where a migration is rolled back. Not all data transformations can be reversed reliably, so the presence of a Down method does not guarantee that rolling back a migration will restore lost data.

Always review generated migrations before applying them. Look for unexpected table drops, column removals, nullable-to-required changes, data type conversions, and changes that may lock large tables or require significant processing time.

7. Applying Migrations to a Database

Creating a migration does not automatically apply it to the database. After reviewing the migration, use the database-update command to apply pending migrations to the configured database.

dotnet ef database update

EF Core checks its migration history, identifies migrations that have not yet been applied, and executes their database operations in sequence. The database's migration-history table records which migrations have been applied successfully.

You can also target a particular migration by name:

dotnet ef database update AddProductSku

When the target is a migration that comes after the current database state, EF Core applies the required intervening migrations. Targeting an earlier migration can attempt to roll back later migrations, potentially executing destructive operations. Do not use this as an unreviewed production rollback strategy.

Be careful about which database your connection string points to before running an update. A command executed from your development machine can modify a shared or production database if the supplied configuration points there.

8. Updating the Model with New Migrations

As development continues, the data model will change. Suppose your Product entity now needs a SKU and an optional description:

public sealed class Product
{
    public int Id { get; set; }

    public string Name { get; set; } = string.Empty;

    public string Sku { get; set; } = string.Empty;

    public string? Description { get; set; }
}

After changing the model, create a new migration:

dotnet ef migrations add AddProductSkuAndDescription

Review the generated operations. Adding a required column to a table that already contains records needs particular care, because existing rows must satisfy the new constraint. Depending on the database and EF Core configuration, a generated operation might use a default value, or it may need a staged data migration.

For a populated database, a safer approach may be to add a nullable column first, populate existing rows with appropriate values, and only then enforce a required constraint if the business rules demand it. The correct sequence depends on the data and the requirements of the application.

Once the migration has been reviewed, test it against a representative database and apply it using the appropriate development or deployment workflow.

9. Useful EF Core Migration Commands

The EF Core CLI provides commands for inspecting and managing migration history. The exact options can vary by EF Core version, but these common commands are useful in everyday development.

Command Purpose
dotnet ef migrations add AddProducts Creates a migration from model changes.
dotnet ef migrations list Lists available migrations and, when a database can be accessed, their application status.
dotnet ef database update Applies pending migrations to the configured database.
dotnet ef migrations script Generates SQL for migration operations that can be reviewed before execution.
dotnet ef migrations remove Removes the most recent migration from the project when it is safe to do so.

The migrations remove command is intended for correcting the latest migration in the development workflow when that migration has not been applied to a database. If it has already been applied to a shared or production database, do not simply remove the migration files. Create a new migration to represent the desired change instead.

10. Generating SQL Scripts for Deployment

Directly applying migrations from a developer's machine is convenient during local development. Production deployments often need a more controlled process so that database changes can be reviewed, approved, scheduled, and executed with the correct permissions.

EF Core can generate a SQL script from your migrations:

dotnet ef migrations script

To create a script intended to bring a database from its current migration state to the latest migration, you can generate an idempotent script:

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

An idempotent migration script checks migration history and includes logic to apply migrations that have not already been applied. This makes it useful when deployment environments may be at different migration levels.

Idempotency does not make arbitrary SQL operations harmless, and it does not remove the need to review or test the script. Review permissions, execution time, locking behavior, data transformations, and potential failure recovery before applying it to production.

EF Core also supports migration bundles in supported versions and configurations. A bundle packages migration operations into an executable deployment artifact, which can be useful when the deployment process should apply migrations without requiring the full source project.

11. Handling Data Changes and Destructive Migrations

Schema changes and data changes are related but different. Adding a column changes the schema, whereas populating that column for existing records transforms data. A well-designed migration may need to perform both operations.

Consider renaming a column from FullName to DisplayName. If EF Core interprets the change as removing one column and creating another, existing data may be lost. Review rename operations carefully and use an explicit rename operation when that is the intended change.

Similarly, dropping a table, shrinking a column, changing numeric precision, or altering a required relationship can have consequences beyond a simple schema update. Test these operations with realistic data before deployment.

For large databases, consider whether the schema change requires a long table lock, expensive data rewrite, or additional database capacity. Large changes may need to be split across multiple deployments so that the application and database remain compatible during the transition.

Avoid treating rollback as the only recovery plan. A migration that drops data may not be reversible, and an application rollback may not be compatible with a newly changed schema. Before making significant changes, prepare a tested backup and recovery plan appropriate for the database and business requirements.

12. Migrations in Production Environments

Applying migrations to a production database requires additional discipline because the database may contain critical information and serve live users.

A controlled deployment workflow commonly follows these steps:

  1. Create the migration and commit it to source control.
  2. Review the generated migration and any custom SQL.
  3. Test the migration against a representative database, including realistic record volumes where appropriate.
  4. Generate a deployment script or bundle and review the output.
  5. Take or verify an appropriate backup and confirm the recovery procedure.
  6. Apply the migration using a controlled deployment process and appropriate database permissions.
  7. Verify the schema, application health, and critical business operations after deployment.

Avoid blindly applying migrations from every application's startup routine in production. When multiple instances start at the same time, startup-time schema changes can create coordination and permission problems. Separate deployment steps generally make approvals, auditing, and recovery easier to manage.

If you must support zero-downtime deployments, use an expand-and-contract approach when appropriate. First introduce backward-compatible schema changes, deploy application versions that can work with the expanded schema, migrate or backfill data, and only remove obsolete structures after the application no longer depends on them.

13. Common EF Core Migration Problems

Unable to create a DbContext

The tooling may be unable to obtain required configuration or construct the context. Check the startup project, design-time dependencies, provider registration, and constructor requirements. A design-time context factory may help when normal startup configuration is unsuitable for tooling.

No changes were detected

EF Core may determine that the model has not changed relative to its snapshot. Confirm that your entity configuration belongs to the intended context and that the expected property or relationship change is actually part of the model.

The database already has tables

Adding an initial migration to an existing database requires a plan for aligning its schema and migration history. Do not blindly apply a migration that tries to recreate tables that already exist. Inspect the database, compare the models, and choose a documented baseline strategy.

Migration fails on existing data

A constraint or type change may be incompatible with existing records. Inspect the failed operation, correct or transform the data as needed, and test the full migration against a representative database before retrying.

Different environments have different migration histories

Inspect the migration-history table and ensure that the deployment process applies the expected migrations. Do not manually edit migration history without a carefully verified recovery or baseline plan.

14. Best Practices for EF Core Migrations

  • Create small, descriptive migrations that represent meaningful schema changes.
  • Review generated migration code rather than accepting every generated operation without inspection.
  • Keep migration files and model snapshots in source control.
  • Test migrations against databases that resemble the environments where they will run.
  • Pay particular attention to required columns, data type changes, destructive operations, and custom SQL.
  • Use deployment scripts or migration bundles when a controlled production workflow is appropriate.
  • Do not remove or rewrite migrations that have already been applied to shared environments as a substitute for a new migration.
  • Keep EF Core tooling, design packages, and database providers compatible.
  • Plan backups, recovery, permissions, and execution timing for important production changes.
  • Use a backward-compatible migration strategy when application versions must coexist during deployment.

Conclusion

EF Core migrations provide a structured way to version and apply database schema changes alongside your .NET application. By understanding migration files, the model snapshot, the database migration history, and the tooling commands, you can make schema evolution more predictable and reviewable.

The most important principle is to treat migrations as code that can affect valuable data. Generate them deliberately, inspect the operations, test them against representative data, and use a controlled deployment process for production databases.

Continue learning with Data Access in .NET to understand how EF Core interacts with your database, AppSettings and Configuration in .NET to manage connection settings, Managing NuGet Packages to understand EF Core dependencies, and .NET Application Architecture to see where persistence belongs in a maintainable application.

Code Examples

# Install the SQL Server provider
dotnet add package Microsoft.EntityFrameworkCore.SqlServer

# Add the EF Core design-time package
dotnet add package Microsoft.EntityFrameworkCore.Design

# Install EF Core CLI tools if needed
dotnet tool install --global dotnet-ef

# Create the initial migration
dotnet ef migrations add InitialCreate

# List available migrations
dotnet ef migrations list

# Apply pending migrations to the configured database
dotnet ef database update

# Generate a SQL script for review
dotnet ef migrations script

# Generate an idempotent SQL script
dotnet ef migrations script --idempotent -o migrations.sql

# Specify the context when a project has multiple contexts
dotnet ef migrations add AddProductSku --context AppDbContext
    
// Example entity before a migration.
public sealed class Product
{
    public int Id { get; set; }

    public string Name { get; set; } = string.Empty;
}

// Later, change the model to add a SKU.
public sealed class Product
{
    public int Id { get; set; }

    public string Name { get; set; } = string.Empty;

    public string Sku { get; set; } = string.Empty;
}

// After changing the model, create a migration:
// dotnet ef migrations add AddProductSku
//
// Review the generated migration before applying it:
// dotnet ef database update
    
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Migrations;

public partial class AddProductSku : Migration
{
    protected override void Up(MigrationBuilder migrationBuilder)
    {
        migrationBuilder.AddColumn<string>(
            name: "Sku",
            table: "Products",
            type: "nvarchar(100)",
            maxLength: 100,
            nullable: true);
    }

    protected override void Down(MigrationBuilder migrationBuilder)
    {
        migrationBuilder.DropColumn(
            name: "Sku",
            table: "Products");
    }
}

// This is an illustrative SQL Server migration.
// A nullable column can be introduced before existing rows
// are populated and a required constraint is applied later.
    

Pro Tip

Treat each migration as a versioned database change. Review its Up and Down operations, test it against realistic data, and inspect generated SQL before production deployment. Never assume a destructive migration can be safely reversed just because a Down method exists.

🎨 IgKnight Docs

Master HTML, CSS, JS, Python, .NET and more with easy-to-follow lessons and practical code examples.

© 2025 IgKnight Learning Platform. All rights reserved.