Intermediate 55 min.NET 10

Migrations & relational modelling

Evolve a database deliberately with relationships, indexes, and reviewable migrations.

BY THE END OF THIS TUTORIAL

Here's what you'll be able to do.

  • Distinguish the model, migration history, and live schema
  • Add and review a schema change
  • Understand foreign keys and cascade behavior
Before you begin

Complete EF Core CRUD. Work on a copy of the downloaded project and back up its local database before experimenting.

Understand the idea

Your C# model and your database schema are separate artifacts. Changing a property does not by itself alter an existing table. A migration describes how to move the schema from one known state to another, and the model snapshot records what EF believed the model looked like at the last migration.

Good migration practice is about preserving data as much as changing structure. A generated operation can be technically valid and still be wrong for your intended data transition. Read the migration before applying it.

Choose your environment

Open TaskBoard.csproj from the example download. Install the ASP.NET and web development workload and a Visual Studio release supporting .NET 10. Select the TaskBoard launch profile and press F5. Use Solution Explorer to find the files named below.

Download the complete companion project
STEP 01

Inspect the existing relationship

Each BoardTask belongs to an Identity user through OwnerId. The foreign key prevents a task from referring to a nonexistent user. Cascade delete means deleting the user deletes their tasks in this sample. That is a domain decision: in another application, retention or reassignment might be required.

The index on OwnerId and CreatedUtc helps common ownership and ordering queries. Foreign keys enforce relationships; indexes support access patterns. They solve different problems.

Data/BoardDb.cs
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;
using TaskBoard.Models;

namespace TaskBoard.Data;

public class BoardDb(DbContextOptions<BoardDb> options) : IdentityDbContext<IdentityUser>(options)
{
    public DbSet<BoardTask> Tasks => Set<BoardTask>();
    protected override void OnModelCreating(ModelBuilder builder)
    {
        base.OnModelCreating(builder);
        builder.Entity<BoardTask>().HasIndex(t => new { t.OwnerId, t.CreatedUtc });
        builder.Entity<BoardTask>().HasOne<IdentityUser>().WithMany()
            .HasForeignKey(t => t.OwnerId).OnDelete(DeleteBehavior.Cascade);
    }
}
STEP 02

Add a small schema change

Add an optional DueDate property to BoardTask. Choosing DateTime? allows old records to remain valid without inventing deadlines. Restore the local EF tool, generate a named migration, then inspect its Up and Down methods before applying it.

The sample includes an initial migration, so do not create another initial migration over the same model. Give new migrations names that describe the intended change. Keep the generated migration and snapshot in source control together.

Terminal
dotnet tool restore
dotnet ef migrations add AddTaskDueDate
dotnet ef database update

Before these commands, add public DateTime? DueDate { get; set; } to BoardTask. This exercise changes your local copy; the downloaded baseline intentionally has no due date.

STEP 03

Review for data loss

Adding a nullable column is usually straightforward. Renaming a property deserves more attention: a generated drop-and-add pair can discard values when your intent was to rename a column. Review SQL or migration operations and test against a disposable copy with representative data.

A Down method is not a backup. Reversing a migration cannot restore values that were deleted during an earlier step. Make backups and rehearse recovery for meaningful production data.

Terminal
dotnet ef migrations list
dotnet ef migrations script --output migration.sql

SQLite has migration and scripting limitations compared with server providers. Do not assume a SQL Server deployment script or an idempotent-script workflow will transfer unchanged.

STEP 04

Deploy schema changes separately

TaskBoard applies migrations on Development startup for convenience. Production deployment should coordinate schema changes as a separate, reviewed operation. Multiple app instances should not all race to change the schema.

For larger changes, use an expand-and-contract sequence: add the new nullable field, deploy code that can tolerate both forms, backfill data, then enforce constraints in a later release. This keeps old and new app versions compatible during rollout.

Program.cs excerpt
if (app.Environment.IsDevelopment())
{
    using var scope = app.Services.CreateScope();
    var db = scope.ServiceProvider.GetRequiredService<BoardDb>();
    await db.Database.MigrateAsync();
}
YOUR TURN

Make it your own.

Add a nullable DueDate, generate a migration, and verify an existing task still loads after applying it. Then create a new task and inspect the new column in your SQLite viewer.

See the solution & reasoning

A nullable column permits existing rows to contain NULL. Update the input model and views only after the database change is understood. If you later make DueDate required, first decide how to backfill existing NULL values; a required UI field alone does not repair old rows.

When something doesn't work

“No DbContext was found” can mean the command ran in the wrong project directory. Use --project when needed.

A migration already applied to a shared environment should not be casually edited or removed. Add a forward correction.

EnsureCreated and Migrate are different schema strategies. Do not use EnsureCreated on a database you intend to manage with migrations.

CHECK YOUR UNDERSTANDING

Does changing a C# property automatically update an existing database table?

Does changing a C# property automatically update an existing database table?

ONE STEP FURTHER

That's another skill in your toolkit.

Your progress is saved in this browser. Sign in to save completion to your account.