EF Core Migrations
Working with data models requires updating them regularly to reflect changes in application functionality. The database schema must stay in sync with the application while adding or removing features. Entity Framework Core (EF Core) provides the Migrations feature that lets you update the database schema without losing existing data.
This article explores EF Core Migrations, their key features, and usage.
Migrations in EF Core
Changes in a database model must be reflected in the database tables to keep them synchronized and ensure the application functions correctly. Entity Framework Core (EF Core) provides the Migrations tool to automatically track and apply such changes.
EF Core compares the current model with the previous one, identifies the differences, and generates migration files. These migrations can then be applied to the database. EF Core also maintains a history table that tracks which migrations have been applied, making it easy to manage schema changes over time. Therefore, EF Core Migrations eliminate the need to manually update database schemas, reducing the risk of errors and saving time.
Migrations are mainly used in the Code-First approach, where the database schema is created and maintained based on C# model classes. In contrast, the Database-First approach that relies on an existing database schema is not well suited for EF Core Migrations.
Why do I need migrations?
EF Core Migrations offers an efficient way to manage database changes, especially in team environments where multiple developers may modify the same schema. Key benefits include:
- Schema evolution: As the application grows, the database schema must adapt. EF Core Migrations helps track and apply these changes over time.
- Versioned management: Migrations treat database changes as versioned updates, making it easier to manage and revert modifications when needed.
- Automation: EF Core automates the generation and application of schema changes, reducing the need for manual SQL coding.
- Consistency: Migrations ensure that all database instances remain in sync, minimizing the risk of discrepancies and errors.
- Team collaboration: Each developer can work with a local database that reflects the latest schema updates, improving coordination and reducing conflicts.
Now, let's explore how to create and configure EF Core migrations in a practical scenario. We've set up a test project for this walkthrough that uses PostgreSQL as the data source. To connect to the database, we'll use dotConnect for PostgreSQL, a robust data provider with full EF Core support that enables direct access to PostgreSQL databases during application development.
Install dotConnect for PostgreSQL
After creating a test project in Visual Studio, we can install dotConnect for PostgreSQL with EF Core support using the Package Manager Console of Visual Studio or .NET CLI. Run the following command in your preferred utility:
Package Manager Console:
Install-Package Devart.Data.PostgreSql.EFCore
.NET CLI:
dotnet add package Devart.Data.PostgreSql.EFCore
When successfully installed, this data provider enables us to connect directly to our PostgreSQL database without additional client libraries or the need to master new technologies.
DbContext and DbSet
Before we proceed to the next step of adding a migration, let us explore the two essential concepts – DbContext and DbSet. These concepts are necessary for us, as we have to configure DbContext in our project.
- DbContext manages the database connections, tracks changes to your entities, and executes queries. It also eliminates the need to write complex SQL queries manually, as it generates them automatically.
- DbSet represents a collection of entities in the database (a virtual database table). We need to define it for each entity type (e.g., Customer, Order, Product) to query and manipulate data for that entity.
This tutorial works with two tables, Customers and Orders. Let us define the Customers and Orders entities.
public class Customer
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public string Email { get; set; }
}
public class Order
{
public int OrderId { get; set; }
public int CustomerId { get; set; }
public int ProductId { get; set; }
public int Date { get; set; }
public Customer Customer { get; set; }
}
We also need a DbContext implementation. Here we define the ecommercedbModel class:
using Microsoft.EntityFrameworkCore;
public class ecommercedbModel : DbContext
{
public DbSet<Customer> Customers { get; set; }
public DbSet<Order> Orders { get; set; }
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UsePostgreSql("Host=localhost;Database=ecommercedb;Username=postgres;Password=mypassword;");
}
}
The next step is creating an EF Core migration.
Add or create an EF Core migration
After setting up the DbContext and registering our entities using DbSet or making changes to them, we can add a migration using the Add-Migration command.
Package Manager Console:
add-migration MigrationName
CLI:
dotnet ef migrations add MigrationName
Make sure to provide the actual migration name in the command. It is recommended that migrations be named like Git commit messages for better organization.
When you create a migration, EF Core generates a new Migrations folder in your project. This folder contains two files: one that defines the migration itself and another called the snapshot file.
The snapshot file captures the current state of the database schema and serves as a reference for EF Core to determine whether new migrations are required. EF Core compares the current model with the one from the previous migration (if available) and generates a migration file containing Up and Down methods.
The file name includes a timestamp followed by the migration name you provided.
- The Up method defines the C# code needed to apply changes to the database schema based on updates made to the model since the last migration.
- The Down method contains the logic to undo those changes, returning the database to its previous state.
EF Core also creates or updates a ModelSnapshot file, which represents the latest state of the model. This snapshot helps EF Core detect future changes. The class name matches the migration name you specified.
Store migrations in a custom directory
Some scenarios may require storing migration files in custom folders, not the default Migrations folder. To achieve this, you need to specify the path in the command for creating that migration.
Package Manager Console:
Add-Migration MigrationName -OutputDir "Custom/Folder/Path"
CLI:
dotnet ef migrations add MigrationName --output-dir Custom/Folder/Path
In this command, MigrationName is the name of your migration, while the --output-dir (-OutputDir in PMC) parameter specifies the path to the folder to store the migration files.
Customize the migration code
Sometimes, you may need to customize the generated migration code before applying it. You should edit the Up or Down method in the corresponding migration file to do this. Let us consider some common use cases that require editing the migration code.
Rename a column using EF Core migrations
For example, you want to rename the Email column in the Customer table to EmailAddress. By default, EF Core might generate code that drops the existing column and creates a new one with the new name, and this approach can lead to data loss.
To avoid this, modify the migrationBuilder code manually. Instead of allowing EF Core to drop and recreate the column, explicitly rename it using the RenameColumn method:
migrationBuilder.RenameColumn(
name: "OldColumnName",
table: "YourTableName",
newName: "NewColumnName"
);
Open the generated migration file in the Migrations folder. To rename without losing data, replace the generated code in the Up() method with this:
migrationBuilder.RenameColumn( name: "Email", table: "Customers", newName: "EmailAddress");
And in the Down() method, reverse the rename:
migrationBuilder.RenameColumn( name: "EmailAddress", table: "Customers", newName: "Email");
In this approach, RenameColumn() instructs EF Core to rename the column at the database level without dropping and recreating it, while the Down() method helps roll back the rename if needed.
Run the migration update command to apply changes to your database:
Update-Database
Another option is executing the raw SQL query in the command line tool:
migrationBuilder.Sql("UPDATE customers SET CreatedAt = NOW()");
Also, it is possible to rename a column by modifying the entire model. This method is optional but can be useful in some scenarios:
Rename the property in your model class:
public class Customer
{
// Old:
// public string Email { get; set; }
// New:
public string EmailAddress { get; set; }
// Other properties...
}
Then, generate a new migration that will contain the rename operation:
Add-Migration RenameEmailToEmailAddress
Combine two columns into a single column
In some cases, you may need to perform operations that EF Core cannot generate automatically, such as merging two columns into one. For example, you might want to combine the FirstName and LastName columns into a single FullName column.
EF Core generates code that drops the FirstName and LastName fields by default and creates a new FullName field. However, this approach results in data loss for both original columns.
To avoid this, you can write raw SQL in your migration file and manually control the sequence of actions. Since migrations are applied sequentially, each building on the previous one, your custom code should:
- 1. Add a new nullable FullName column.
- 2. Populate it using existing FirstName and LastName data.
- 3. Drop the original FirstName and LastName columns.
Open the newly generated migration class in the Migrations folder. Replace or add code in the Up() method:
protected override void Up(MigrationBuilder migrationBuilder) {
// Add FullName column first
migrationBuilder.AddColumn<string>(
name: "FullName",
table: "Customers",
nullable: true);
// Run SQL to populate FullName from FirstName and LastName (columns must exist here)
migrationBuilder.Sql("UPDATE \"Customers\" SET \"FullName\" = \"FirstName\" || ' ' || \"LastName\";");
// Now drop the old columns
migrationBuilder.DropColumn(name: "FirstName", table: "Customers");
migrationBuilder.DropColumn(name: "LastName", table: "Customers");
}
In the Down() method, reverse the changes, as in the previous scenario:
protected override void Down(MigrationBuilder migrationBuilder) {
// Recreate FirstName and LastName columns
migrationBuilder.AddColumn<string>(
name: "FirstName",
table: "Customers",
nullable: true);
migrationBuilder.AddColumn<string>(
name: "LastName",
table: "Customers",
nullable: true);
// Optional: split FullName back into FirstName and LastName
// (Add SQL here if needed)
// Drop FullName column
migrationBuilder.DropColumn(
name: "FullName",
table: "Customers");
}
Run the below command in PMC to update your database schema:
Update-Database
The migrationBuilder.Sql() runs a raw SQL command concatenating FirstName and LastName with a space. It adds FullName, filling it and safely removing FirstName and LastName. This ensures data integrity during the process of restructuring your database schema.
Another approach suggests editing the model class where you can add a new property and remove the old ones:
public class Customer
{
public int Id { get; set; }
// Remove FirstName and LastName
public string FullName { get; set; }
public string Email { get; set; }
// ... other properties
}
After that, in the Package Manager Console or CLI terminal, create a new migration that will perform the database schema changes:
Add-Migration CombineNameColumnsIntoFullName
Detect unapplied model changes before migration
EF Core can check whether any model changes have occurred since the last migration without committing to a new migration.
Execute the following command in CLI to see if the code is synchronized with the current migration:
dotnet ef migrations add CheckChanges
EF Core will create a new migration file in the specified output folder (TempMigrations) if there are changes. You can inspect this file to see the expected changes.
You can also modify the command to disable the building of the migration:
dotnet ef migrations add CheckChanges --no-build --output-dir TempMigrations
Make sure to delete the TempMigrations folder afterward to avoid cluttering your project.
Revert/Roll back an EF Core migration
You can roll back your database to a specific migration by specifying the target migration in the update command. The target migration marks the point to which the database should be restored, and all changes introduced by later migrations will be undone.
Package Manager Console:
update-database TargetMigration
CLI:
dotnet ef database update TargetMigration
This command removes the entries for any migrations applied after the target migration from the EFMigrationsHistory table in the database. However, it does not delete those migrations from the Migrations folder or modify the ModelSnapshot file.
Remove an EF Core migration
If you created a migration by mistake or no longer need it, you can delete it using the following command:
Package Manager Console:
remove-migration
CLI:
dotnet ef migrations remove
This command deletes the most recent migration, including its generated class file, and reverts the ModelSnapshot file to the state of the previous migration. If there are no pending migrations, it will throw an exception. If the migration has already been applied to the database, you must roll it back before removing it.
Suppose you must remove an older migration (not the latest one). In that case, you must first delete all newer migrations, update the model accordingly, and then generate a new migration that reflects the correct changes.
Create SQL scripts from migrations
You may need to deploy one or multiple migrations against another database. The simplest option is executing a dedicated SQL script against the target database. You can generate that script from the migration using the below command:
Package Manager Console:
Script-Migration -From InitialCreate -To RenameEmailToEmailAddress -Output migration.sql
CLI:
dotnet ef migrations script -From InitialCreate -To RenameEmailToEmailAddress -Output migration.sql
By default, this script includes all migrations, but you can specify the necessary range to include using the -to and -from options.
Complete list of EF Core migration commands
Below are the EF Core migration commands available for the CLI and the Package Manager Console, depending on your preferences.
Package Manager Console
| Command | Description |
| add-migration [name] | Creates a new migration with the specific migration name |
| remove-migration | Removes the latest migration |
| update-database | Updates the database to the latest migration |
| update-database [name] | Updates the database to a specific migration name point |
| get-migrations | Lists all available migrations |
| script-migration | Generates an SQL script for all migrations |
| drop-database | Drops the database |
Command Line
| Command | Description |
| dotnet ef migrations add [name] | Creates a new migration with the specific migration name |
| dotnet ef migrations remove | Removes the latest migration |
| dotnet ef database update | Updates the database to the latest migration |
| dotnet ef database update [name] | Updates the database to a specific migration name point |
| dotnet ef migrations list | Lists all available migrations |
| dotnet ef migrations script | Generates a SQL script for all migrations |
| dotnet ef migrations has-pending-model-changes | Checks if there are any model changes since the last migration |
| dotnet ef database drop | Drops the database |
Multi-stage database migrations
One of the most common scenarios for using migrations is handling multiple database environments (such as development and production). You need to test and stage the schema changes properly before reaching production. Multi-stage migrations help organize and control this process. Let us review this process step by step.
Use separate connection strings for each environment
Configure your DbContext to use different databases depending on environment or configuration as shown below:
public class AppDbContext : DbContext
{
public DbSet<Customer> Customers { get; set; }
public DbSet<Order> Orders { get; set; }
private readonly string _connectionString;
public AppDbContext(string connectionString)
{
_connectionString = connectionString;
}
protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
{
optionsBuilder.UsePostgreSql(_connectionString);
}
}
For example, we use the following connection strings for the development and production environments:
Development:
"Host=localhost;Database=CommerceDB_dev;User Id=postgres;Password=yourpassword;"
Production:
"Host=prodserver;Database=CommerceDB;User Id=produser;Password=prodpassword;"
Maintain separate migration branches or naming
To manage migrations accurately across environments, you have two options:
1. Use different migration folders or namespaces for dev and production:
- - Migrations/Dev for development
- - Migrations/Prod for production
2. Keep migrations in one folder but branch migration names to indicate the environment.
- - 20250520_Dev_AddNewTable
- - 20250525_Prod_AddNewTable
This helps us avoid accidentally applying a development-only migration to production.
Create development migrations first
Start with migrations that target the development database:
dotnet ef migrations add AddNewFeature_Dev -o Migrations/Dev dotnet ef database update --connection "Host=localhost;Database=CommerceDB_dev;User Id=postgres;Password=yourpassword;"
Test your changes thoroughly in CommerceDB_dev.
Create production migration after testing
Once the development migration is stable, you can re-create the migration for production with the same or similar schema changes:
dotnet ef migrations add AddNewFeature_Prod -o Migrations/Prod dotnet ef database update --connection "Host=prodserver;Database=CommerceDB;User Id=produser;Password=prodpassword;"
Alternatively, if your migrations are identical, you can merge the dev migrations into production after testing and apply them to the production database.
Apply migrations programmatically per environment
At the app startup or deployment, apply migrations using the correct connection string:
var connectionString = Environment.GetEnvironmentVariable("CONNECTION_STRING");
using var context = new AppDbContext(connectionString);
context.Database.Migrate();
Configure your deployment pipeline or environment variables accordingly.
Use migration bundles or scripts for production
For more controlled production deployment, generate a migration bundle or SQL script after testing:
dotnet ef migrations bundle --output prod_migrations.exe
Or
dotnet ef migrations script -o prod_migration.sql
Deploy and execute the bundle or script on production outside the application.
Best practices for multi-stage migrations
- Back up production databases before applying migrations
- Test all migrations on dev/staging before production
- Keep migration history clean and linear per environment
- Use feature branches in source control for schema changes
- Automate migrations as part of CI/CD pipelines with environment-specific configs