EF Core Basics and Relationships
How classes become tables, and how tables point at each other
Guess first
Pick an answer. You will see if you were right at the end of the lesson.
What does DeleteBehavior.Cascade on the Order to OrderItem relationship do?
Real-world analogy
What is it?
Entity Framework Core (EF Core) is an object-relational mapper. You write classes, and it maps them to tables and turns LINQ into SQL. A DbContext holds your DbSet properties and tracks the changes you make. Relationships come from foreign keys: EF Core reads your properties by convention, and you correct it with the Fluent API in OnModelCreating. Migrations record each schema change as code, so every environment can reach the same shape.
Real-world relevance
On a real project the first migration creates the tables, and each later feature adds a small migration that goes through code review like any other change. The painful surprises are usually relationships: a foreign key property with a name EF Core does not recognise, or a cascade delete that removes order history when someone deletes a customer. Both are easy to catch by reading the generated migration before you apply it.
Key points
- DbContext is a unit of work — It remembers the changes you make and writes them together when you call SaveChanges. Treat one context as one short piece of work.
- DbSet is a table — Each DbSet<Order> property is the door to one table. You query it with LINQ, and EF Core turns the query into SQL.
- Foreign key plus navigation — The foreign key property (CustomerId) is the column. The navigation property (Customer) is the object you walk to in code.
- Three relationship shapes — One-to-many puts the key on the many side. One-to-one puts it on the dependent side, with a unique index. Many-to-many uses a join table.
- Skip navigations hide the join — Give Post a List<Tag> and Tag a List<Post>. EF Core creates the join table for you.
- Conventions first, Fluent API second — EF Core guesses from names: Id is the key, CustomerId is the foreign key. Use OnModelCreating when the guess is wrong or you need more control.
- Migrations are versioned schema steps — dotnet ef migrations add Name writes a change file. dotnet ef database update applies it.
- Do not edit applied migrations — Other databases already ran that file, so changing it makes them disagree. Add a new migration instead.
- Cascade delete can surprise you — A required relationship deletes its children by default when the parent goes. Pick Restrict where losing child rows would hurt.
Quick check
Answer from memory — scrolling up first makes it stick less.
You applied a migration to the shared test database and then spot a typo in it. What is the safe fix?
Which line describes a many-to-many relationship in EF Core?
Code example
public class Customer
{
public int Id { get; set; }
public string Name { get; set; } = "";
public List<Order> Orders { get; set; } = new();
}
public class Order
{
public int Id { get; set; }
public int CustomerId { get; set; }
public Customer Customer { get; set; } = null!;
public List<OrderItem> Items { get; set; } = new();
}
public class OrderItem
{
public int Id { get; set; }
public int OrderId { get; set; }
public string Sku { get; set; } = "";
}
public class ShopContext : DbContext
{
public ShopContext(DbContextOptions<ShopContext> options) : base(options) { }
public DbSet<Customer> Customers => Set<Customer>();
public DbSet<Order> Orders => Set<Order>();
protected override void OnModelCreating(ModelBuilder b)
{
b.Entity<Order>().HasOne(o => o.Customer).WithMany(c => c.Orders)
.HasForeignKey(o => o.CustomerId).OnDelete(DeleteBehavior.Restrict);
b.Entity<Order>().HasMany(o => o.Items).WithOne()
.HasForeignKey(i => i.OrderId).OnDelete(DeleteBehavior.Cascade);
}
}Line-by-line walkthrough
- 1. Customer holds a list of Orders, and Order holds a list of OrderItems. That is the one-to-many chain.
- 2. Order has both CustomerId (the column) and Customer (the object you walk to). OrderItem only has the column.
- 3. ShopContext lists two DbSets. OrderItem has no DbSet, but EF Core still finds it through the Items navigation.
- 4. OnModelCreating spells out the relationships the conventions could only guess at.
- 5. Customer to Order uses Restrict, so the database refuses to delete a customer who still has orders.
- 6. Order to OrderItem uses Cascade, so deleting an order removes its own items with it.
Spot the bug
public class Customer { public int Id { get; set; } }
public class Order
{
public int Id { get; set; }
public int OwnerId { get; set; }
public Customer Customer { get; set; } = null!;
}
// The Orders table now has OwnerId and CustomerId.
// Saved orders show a CustomerId, and OwnerId is always 0.Need a hint?
Show answer
Explain like I'm 5
Fun fact
Hands-on challenge
More resources
- Relationships in EF Core (Microsoft Learn)
- Migrations overview (Microsoft Learn)
- DbContext lifetime, configuration and initialization (Microsoft Learn)
Lock it in