Migrations & relational modelling
Evolve a database deliberately with relationships, indexes, and reviewable migrations.
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
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.
Install the .NET 10 SDK and C# Dev Kit. Open the TaskBoard folder in VS Code. Run dotnet restore, then dotnet run in the integrated terminal. Browse http://localhost:5300. Open files with Ctrl+P (Cmd+P on macOS).
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.
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);
}
}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.
dotnet tool restore
dotnet ef migrations add AddTaskDueDate
dotnet ef database updateBefore these commands, add public DateTime? DueDate { get; set; } to BoardTask. This exercise changes your local copy; the downloaded baseline intentionally has no due date.
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.
dotnet ef migrations list
dotnet ef migrations script --output migration.sqlSQLite 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.
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.
if (app.Environment.IsDevelopment())
{
using var scope = app.Services.CreateScope();
var db = scope.ServiceProvider.GetRequiredService<BoardDb>();
await db.Database.MigrateAsync();
}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.
Does changing a C# property automatically update an existing database table?
That's another skill in your toolkit.
Your progress is saved in this browser. Sign in to save completion to your account.