Chapter 3: Database Migrations (The Professional Way) - IndianTechnoEra
Latest update Android YouTube

Chapter 3: Database Migrations (The Professional Way)

Welcome to Chapter 3! In the previous chapter, we used EnsureCreated() to create our database. While this worked for our first experiment, it's not suitable for real-world applications. In this chapter, we'll learn about Migrations - the professional way to manage database schema changes. Migrations are like Git for your database, allowing you to version control, track, and safely deploy schema changes.

3.1 Why Not EnsureCreated()?

Before diving into migrations, let's understand why EnsureCreated() is problematic for production applications.

Problems with EnsureCreated():

Problem Description Real-World Impact
No Schema Evolution Once the database exists, EnsureCreated does nothing. It cannot modify an existing schema. If you add a new property to your entity, EnsureCreated won't add the column. You'd have to delete and recreate the database, losing all data.
Data Loss The only way to update schema is to delete and recreate the database. In production, you cannot delete customer data just to add a new column.
No Version Control There's no record of what schema changes were applied and when. When you have multiple environments (Dev, Test, Production), you can't track which schema version each environment has.
Team Collaboration Issues Team members might have different database schemas. Developer A adds a column, Developer B pulls the code but doesn't know the schema changed. The application crashes.

The Migrations Solution:

Migrations solve all these problems by:

  • Tracking schema changes in C# code files
  • Allowing incremental updates without data loss
  • Providing a history table in the database
  • Enabling team collaboration through source control
  • Generating SQL scripts for production deployments

3.2 Understanding Migrations

What is a Migration?

A migration is a set of instructions (C# code) that describes how to move from the current database schema to a new version. Each migration contains:

  • Up() method: How to apply the changes (add a table, add a column, etc.)
  • Down() method: How to revert the changes (remove the table, remove the column, etc.)

Migrations Workflow:


┌────────────────┐     ┌────────────────┐     ┌────────────────┐

│                │     │                │     │                │

│  Change your   │────▶│  Create a      │────▶│  Apply to      │

│  Entity Models │     │  Migration     │     │  Database      │

│                │     │                │     │                │

└────────────────┘     └────────────────┘     └────────────────┘

       │                       │                       │

       │                       │                       │

       ▼                       ▼                       ▼

  Add a "Genre"         dotnet ef migrations      dotnet ef database

  property to Book      add AddGenreToBook        update

The Migration History Table:

When you apply migrations, EF Core creates a special table in your database called __EFMigrationsHistory. This table tracks which migrations have been applied:

MigrationId ProductVersion
20231201093000_InitialCreate 8.0.0
20231201104500_AddGenreToBook 8.0.0

EF Core checks this table before running migrations to determine which ones need to be applied.


3.3 Installing the dotnet-ef Tool

To work with migrations, we need the dotnet-ef command-line tool.

Step 1: Install the tool globally


dotnet tool install --global dotnet-ef

If you already have it installed and want to update:


dotnet tool update --global dotnet-ef

Step 2: Verify the installation


dotnet ef

You should see output similar to:


                     _/\__       

               ---==/    \\      

         ___  ___   |.    \|\    

        | __|| __|  |  )   \\\   

        | _| | _|   \_/ |  //|\\  

        |___||_|       /   \\\/\\ 

Entity Framework Core .NET Command-line Tools 8.0.0

Usage: dotnet ef [options] [command]

Alternative: Package Manager Console (Visual Studio)

If you're using Visual Studio, you can also use the Package Manager Console with these commands:


Install-Microsoft.EntityFrameworkCore.Tools

Then use commands like:


Add-Migration InitialCreate

Update-Database


3.4 Preparing Our Project for Migrations

Before creating our first migration, let's clean up our Program.cs file. We need to remove EnsureCreated() because migrations will handle database creation.

Updated Program.cs (Clean Version):


using EFCoreTutorial.Data;

using EFCoreTutorial.Models;

using var db = new AppDbContext();

// REMOVED: db.Database.EnsureCreated();

// Migrations will handle database creation now

// Create some books

var booksToAdd = new[]

{

    new Book

    {

        Title = "The Pragmatic Programmer",

        Author = "David Thomas",

        Price = 49.99m,

        PublishedOn = new DateTime(1999, 10, 20),

        ISBN = "978-0201616224"

    },

    new Book

    {

        Title = "Code Complete",

        Author = "Steve McConnell",

        Price = 54.99m,

        PublishedOn = new DateTime(2004, 6, 9),

        ISBN = "978-0735619678"

    }

};

Console.WriteLine("Adding books...");

db.Books.AddRange(booksToAdd);

int recordsWritten = db.SaveChanges();

Console.WriteLine($"Added {recordsWritten} books.");

// Display all books

Console.WriteLine("\nAll books in database:");

foreach (var book in db.Books.OrderBy(b => b.Title))

{

    Console.WriteLine($"- {book.Title} by {book.Author} (${book.Price})");

}

Note: If you run this now, it will crash because the database doesn't exist yet. We'll fix that by creating and applying our first migration.


3.5 Creating Your First Migration

Now we'll create our first migration, which will set up the initial database schema based on our Book entity.

Step 1: Navigate to your project directory


cd EFCoreTutorial

Step 2: Create the migration


dotnet ef migrations add InitialCreate

What happens when you run this command?

  1. EF Core compares your current model (entities and DbContext) against an empty model (snapshot).
  2. It detects that no tables exist, so it creates a migration to create all tables.
  3. It generates C# files in a new Migrations folder.

Expected Output:


Build started...

Build succeeded.

Done. To undo this action, use 'ef migrations remove'

What got created?

Look in your project folder. You'll see a new Migrations folder with files:


Migrations/

├── 20231201123000_InitialCreate.cs         # Main migration file

├── 20231201123000_InitialCreate.Designer.cs # Metadata (don't edit)

└── AppDbContextModelSnapshot.cs             # Snapshot of current model

The timestamp (20231201123000) will be different based on your current date and time.

Examining the Migration File:

Open the main migration file (the one without .Designer.cs). You'll see:


using Microsoft.EntityFrameworkCore.Migrations;

#nullable disable

namespace EFCoreTutorial.Migrations

{

    /// <inheritdoc />

    public partial class InitialCreate : Migration

    {

        /// <inheritdoc />

        protected override void Up(MigrationBuilder migrationBuilder)

        {

            migrationBuilder.CreateTable(

                name: "Books",

                columns: table => new

                {

                    Id = 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: true),

                    Price = table.Column<decimal>(type: "decimal(18,2)", nullable: false),

                    PublishedOn = table.Column<DateTime>(type: "datetime2", nullable: true),

                    ISBN = table.Column<string>(type: "nvarchar(max)", nullable: true)

                },

                constraints: table =>

                {

                    table.PrimaryKey("PK_Books", x => x.Id);

                });

        }

        /// <inheritdoc />

        protected override void Down(MigrationBuilder migrationBuilder)

        {

            migrationBuilder.DropTable(

                name: "Books");

        }

    }

}

Understanding the Migration Code:

Part Description
Up() method Contains the operations to apply the migration (create table, add columns, etc.). This is run when you update the database.
Down() method Contains the operations to revert the migration (drop table, remove columns, etc.). This is used if you need to roll back.
CreateTable() Creates the Books table with all columns based on our Book entity.
.Annotation("SqlServer:Identity", "1, 1") Makes Id an auto-incrementing identity column.
PrimaryKey() Sets Id as the primary key.

3.6 Applying the Migration to the Database

Now that we have a migration, let's apply it to create the actual database.

Step 1: Run the update command


dotnet ef database update

Expected Output:


Build started...

Build succeeded.

info: Microsoft.EntityFrameworkCore.Migrations[20402]

      Applying migration '20231201123000_InitialCreate'.

done.

What happened?

  1. EF Core checked the __EFMigrationsHistory table (which didn't exist yet).
  2. It determined that no migrations had been applied.
  3. It executed the Up() method of InitialCreate, which created the Books table.
  4. It added a record to __EFMigrationsHistory to track that this migration was applied.

Verify in SQL Server:

Open SSMS and connect to (localdb)\mssqllocaldb. You should now see:

  • Database: EFCoreBookStore
  • Tables:
    • dbo.Books (our data table)
    • dbo.__EFMigrationsHistory (migration tracking table)

Query the __EFMigrationsHistory table:


SELECT * FROM __EFMigrationsHistory

Result:

MigrationId ProductVersion
20231201123000_InitialCreate 8.0.0

3.7 Running the Application Now

Now that the database is created via migrations, let's run our application:


dotnet run

Expected Output:


Adding books...

info: Microsoft.EntityFrameworkCore.Database.Command[20101]

      Executed DbCommand (0ms) [Parameters=[...], CommandType='Text', CommandTimeout='30']

      SET NOCOUNT ON;

      INSERT INTO [Books] ([Author], [ISBN], [Price], [PublishedOn], [Title])

      VALUES (...);

      SELECT [Id] FROM [Books] WHERE @@ROWCOUNT = 1 AND [Id] = scope_identity();

info: Microsoft.EntityFrameworkCore.Database.Command[20101]

      Executed DbCommand (0ms) [Parameters=[...], CommandType='Text', CommandTimeout='30']

      SET NOCOUNT ON;

      INSERT INTO [Books] ([Author], [ISBN], [Price], [PublishedOn], [Title])

      VALUES (...);

      SELECT [Id] FROM [Books] WHERE @@ROWCOUNT = 1 AND [Id] = scope_identity();

Added 2 books.

All books in database:

- Code Complete by Steve McConnell ($54.99)

- The Pragmatic Programmer by David Thomas ($49.99)

It works! The database exists, and we can add and query data.


3.8 Making Model Changes - Adding a Second Migration

Now let's see the real power of migrations. We'll modify our Book entity by adding a new property and creating a migration to update the database without losing data.

Step 1: Modify the Book entity

Open Models/Book.cs and add a Genre property:


using System.ComponentModel.DataAnnotations;

namespace EFCoreTutorial.Models;

public class Book

{

    public int Id { get; set; }

    

    [Required]

    [MaxLength(200)]

    public string Title { get; set; }

    

    public string? Author { get; set; }

    

    [DataType(DataType.Currency)]

    public decimal Price { get; set; }

    

    public DateTime? PublishedOn { get; set; }

    

    public string? ISBN { get; set; }

    

    // NEW PROPERTY: Genre

    [MaxLength(50)]

    public string? Genre { get; set; }

}

Step 2: Create a new migration


dotnet ef migrations add AddGenreToBook

Step 3: Examine the new migration

Open the newly created migration file (e.g., 20231201134500_AddGenreToBook.cs):


using Microsoft.EntityFrameworkCore.Migrations;

#nullable disable

namespace EFCoreTutorial.Migrations

{

    /// <inheritdoc />

    public partial class AddGenreToBook : Migration

    {

        /// <inheritdoc />

        protected override void Up(MigrationBuilder migrationBuilder)

        {

            migrationBuilder.AddColumn<string>(

                name: "Genre",

                table: "Books",

                type: "nvarchar(50)",

                maxLength: 50,

                nullable: true);

        }

        /// <inheritdoc />

        protected override void Down(MigrationBuilder migrationBuilder)

        {

            migrationBuilder.DropColumn(

                name: "Genre",

                table: "Books");

        }

    }

}

Notice: The migration only contains the changes - adding a Genre column. It doesn't recreate the whole table.

Step 4: Apply the migration


dotnet ef database update

Step 5: Verify the change

In SSMS, check the Books table structure. You'll see the new Genre column has been added, and your existing data is still there!


SELECT * FROM Books

The existing rows will have NULL in the Genre column (since they were added before we had this property).


3.9 Understanding the Migration History

Let's look at how EF Core tracks which migrations have been applied.

Check the current migration status:


dotnet ef migrations list

Output:


20231201123000_InitialCreate

20231201134500_AddGenreToBook

This shows all migrations, with the most recent at the bottom.

View the migrations in the database:


SELECT * FROM __EFMigrationsHistory ORDER BY MigrationId

Result:

MigrationId ProductVersion
20231201123000_InitialCreate 8.0.0
20231201134500_AddGenreToBook 8.0.0

When you run database update, EF Core:

  1. Checks this table to see which migrations are already applied
  2. Applies any migrations that are in your project but not in this table
  3. Adds a record for each newly applied migration

3.10 Reverting a Migration (Rollback)

Sometimes you need to undo a migration. Maybe you made a mistake or need to roll back to a previous version.

Option 1: Revert to a specific migration

To revert back to the InitialCreate migration (which will remove the Genre column):


dotnet ef database update InitialCreate

This runs the Down() method of all migrations after InitialCreate (in this case, just AddGenreToBook), effectively removing the Genre column.

Option 2: Revert all migrations (remove database)

To completely remove the database:


dotnet ef database update 0

This runs the Down() method of all migrations, removing all tables. The database will still exist but will be empty.

Option 3: Remove the last migration (if not applied)

If you created a migration but haven't applied it yet, you can remove it:


dotnet ef migrations remove

This deletes the last migration files. Useful if you made a mistake in the migration code.


3.11 Generating SQL Scripts for Production

In production environments, you typically don't run database update directly. Instead, you generate a SQL script and have a DBA review and run it.

Generate a SQL script for all migrations:


dotnet ef migrations script -o migrate.sql

This creates a migrate.sql file containing all the SQL commands to bring any database from empty to the latest migration.

Generate a script from one migration to another:


dotnet ef migrations script InitialCreate AddGenreToBook -o from_initial_to_genre.sql

This creates a script that updates from InitialCreate to AddGenreToBook.

Examine the generated SQL:

Open migrate.sql. You'll see something like:


-- First migration: InitialCreate

CREATE TABLE [Books] (

    [Id] int NOT NULL IDENTITY,

    [Title] nvarchar(200) NOT NULL,

    [Author] nvarchar(max) NULL,

    [Price] decimal(18,2) NOT NULL,

    [PublishedOn] datetime2 NULL,

    [ISBN] nvarchar(max) NULL,

    CONSTRAINT [PK_Books] PRIMARY KEY ([Id])

);

-- Insert into migration history

INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion])

VALUES (N'20231201123000_InitialCreate', N'8.0.0');

-- Second migration: AddGenreToBook

ALTER TABLE [Books] ADD [Genre] nvarchar(50) NULL;

INSERT INTO [__EFMigrationsHistory] ([MigrationId], [ProductVersion])

VALUES (N'20231201134500_AddGenreToBook', N'8.0.0');

This script is safe to run on any environment - it checks the migration history table and only applies new migrations.


3.12 Best Practices with Migrations

DO:

  • Commit migrations to source control (Git, SVN, etc.). They are part of your codebase.
  • Create one migration per logical change. Don't bundle unrelated schema changes.
  • Name migrations descriptively (e.g., AddGenreToBook, not Update1).
  • Review migration code before applying, especially for complex changes.
  • Test migrations on a staging environment before production.
  • Generate SQL scripts for production deployments.

DON'T:

  • Don't edit migration files after they're committed and shared with the team. Create a new migration instead.
  • Don't use EnsureCreated() with migrations - it bypasses the migration system.
  • Don't delete migration files that have been applied to production.
  • Don't manually modify the database without creating corresponding migrations.

3.13 Common Migration Commands Reference

Command Description Example
dotnet ef migrations add [Name] Create a new migration based on model changes dotnet ef migrations add AddGenreToBook
dotnet ef database update Apply all pending migrations dotnet ef database update
dotnet ef database update [Migration] Update to a specific migration dotnet ef database update InitialCreate
dotnet ef migrations list List all migrations dotnet ef migrations list
dotnet ef migrations remove Remove the last migration (if not applied) dotnet ef migrations remove
dotnet ef migrations script Generate a SQL script dotnet ef migrations script -o script.sql
dotnet ef database drop Drop the database dotnet ef database drop --force

3.14 Troubleshooting Migration Issues

Issue 1: "The migration 'X' was not found"

Problem: You're trying to update to a migration that doesn't exist.

Solution: Check the migration name with dotnet ef migrations list. Use the exact name (including timestamp if needed).

Issue 2: "Unable to create an object of type 'AppDbContext'"

Problem: EF Core can't create your DbContext.

Solution: Ensure you have a parameterless constructor or a design-time factory. For now, add this to your AppDbContext:


public AppDbContext()

{

}

public AppDbContext(DbContextOptions<AppDbContext> options) : base(options)

{

}

Issue 3: Migration fails with foreign key conflicts

Problem: You're trying to add a foreign key but existing data violates it.

Solution: Either clean the data first, or make the column nullable, or provide default values in the migration.

Issue 4: Multiple migrations with same name

Problem: Team members created migrations with the same name.

Solution: The last person to merge should rename their migration or resolve conflicts. Better to coordinate and use timestamp-based names (which EF Core does automatically).


3.15 Chapter Summary

Concept Key Takeaway
Why Migrations? EnsureCreated() doesn't handle schema evolution. Migrations provide version control for your database.
Migration Files Each migration has Up() (apply) and Down() (revert) methods. They're C# code you can review and modify.
__EFMigrationsHistory Database table that tracks which migrations have been applied.
dotnet ef migrations add Creates a new migration based on model changes.
dotnet ef database update Applies pending migrations to the database.
Migration Scripts Generate SQL scripts for production deployments with dotnet ef migrations script.
Rollback Use database update [MigrationName] to revert to a specific migration.

What's Next?

Congratulations! You've mastered the professional way to manage database schema changes. You now have a solid foundation for building real-world applications with EF Core.

In Chapter 4: Defining the Model (Fluent API vs. Data Annotations), we will:

  • Deep dive into model configuration options
  • Compare Data Annotations (attributes) vs. Fluent API (code configuration)
  • Learn about EF Core conventions and how to override them
  • Configure tables, columns, indexes, and relationships
  • Organize configurations using IEntityTypeConfiguration<T>
  • See practical examples of both approaches

This will give you the tools to precisely control how your C# classes map to database tables.


إرسال تعليق

Feel free to ask your query...
Cookie Consent
We serve cookies on this site to analyze traffic, remember your preferences, and optimize your experience.
Oops!
It seems there is something wrong with your internet connection. Please connect to the internet and start browsing again.
AdBlock Detected!
We have detected that you are using adblocking plugin in your browser.
The revenue we earn by the advertisements is used to manage this website, we request you to whitelist our website in your adblocking plugin.
Site is Blocked
Sorry! This site is not available in your country.