Ef Core Mastery

By the end of this lesson you'll be able to model the three core relationship types — one-to-many, many-to-many, and one-to-one — plus owned entities, choose the right loading strategy with Include , and evolve your schema safely with migrations. These are the skills that turn a toy app into a real database-backed system.

Learn Python, JavaScript, Java and more with free interactive lessons, real projects and AI-powered help. Beginner-friendly.

Part of the free C# course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.

Think of your database as a library . An author has many books — that's one-to-many . A book wears many tags ("Fiction", "Classic") and a tag labels many books — that's many-to-many , and you need an index card (a join table) listing which book has which tag. An author has exactly one membership card — that's one-to-one . An author's address isn't a thing the library catalogues on its own; it's just extra lines printed on the author's record — that's an owned entity . And migrations are the library's renovation log: every time you add a shelf or move a room, you write down the change so you can rebuild — or undo — the layout later.

A navigation property is just a property that points to the related entity (or list of them). A foreign key (FK) is the plain column — like AuthorId — that physically links the rows.

1. One-to-Many — the Workhorse

One-to-many is by far the most common relationship: one Author writes many Book s. You model it with two navigation properties — a collection ( List Book Books ) on the "one" side, and a reference plus a foreign key ( Author and int AuthorId ) on the "many" side. EF can often infer the relationship from those names, but spelling it out with the Fluent API lets you set the delete behaviour. Read this worked example first.

Now you try — but in plain, runnable C#. The program below models the same one-to-many shape (an Author with a List Book ) as in-memory objects, so it executes here. Fill in the three ___ blanks, then run it.

2. Many-to-Many — Skip Navigations

In a many-to-many, both sides carry a collection: a Book has many Tag s and a Tag labels many Book s. Since EF Core 5 you no longer write a join class by hand — you put a List on each side and EF generates the hidden join table for you (this is called a skip navigation because you "skip over" the join table when you query). Physically there are still three tables; you just never have to mention the middle one.

3. One-to-One & Owned Entities

A one-to-one ties two entities so each maps to at most one of the other — an Author and their Profile . You configure it with HasOne/WithOne , and the foreign key lives on the dependent side ( Profile.AuthorId ) and is made unique . An owned entity is different: it has no identity of its own and isn't a row in its own table — it's a value object (like an Address or a money amount) whose columns are folded into the owner's table with OwnsOne . Reach for owned types when a thing only makes sense as part of its parent.

4. Loading Related Data — Eager, Lazy & Explicit

By default EF loads an entity without its relationships — db.Authors.ToList() gives you authors with empty Books lists. You then choose how related data arrives. Eager loading with Include pulls it in the same query (one round-trip). Explicit loading fetches it later, by hand, only when you decide you need it. Lazy loading fetches it automatically the first time you touch a navigation — convenient, but the classic source of accidental extra queries. Use ThenInclude to reach a second level (an author's books' tags).

Eager ( Include ) is the default choice when you know you'll need the related data — it's one round-trip and predictable. The catch: chaining several Include s onto collection navigations multiplies rows (a "cartesian explosion"). If you load an author's 10 books and 5 tags, a single joined query returns 50 rows of duplicated author data. AsSplitQuery() fixes this by issuing one query per collection instead.

Explicit loading shines when related data is only sometimes needed — you load the parent cheaply, then call .Load() on the rare path that needs the children.

Lazy loading is the most convenient and the most dangerous: a navigation touched inside a loop fires one query per iteration (the N+1 problem). It's off by default for a reason — prefer Include unless you have a specific reason not to.

Time to query related data yourself. Once an Include has loaded everything, you work with it using ordinary LINQ. The exercise below uses in-memory lists in place of the loaded tables, so it runs — join books to authors and group to count. Fill in the two ___ blanks.

5. Migrations — Versioning Your Schema

Your entity classes are the source of truth for your schema, but the database doesn't update itself. Migrations bridge that gap: each one is a generated class with an Up() (apply the change) and a Down() (reverse it), like a commit you can roll forward or back. You run Add-Migration (or dotnet ef migrations add ) after changing your model, then Update-Database to apply it. EF tracks which migrations have run in a __EFMigrationsHistory table, so it only ever applies the new ones.

Here's a small but real relational graph — an Order with many OrderItem s, each pointing at a Product by id. It's modelled exactly like EF entities (one-to-many + a foreign-key lookup) but as plain in-memory objects, so it runs. You join each item to its product and total the order — the same query you'd run after an Include .

With real EF you'd write db.Orders.Include(o => o.Items).ThenInclude(i => i.Product) and the join would happen in SQL — the LINQ over the loaded objects would be identical.

Q: Do I always need a foreign-key property like AuthorId ?

No — EF can create a shadow FK behind the scenes from the navigation alone. But adding the explicit AuthorId property is usually better: you can set and query it directly without loading the whole related entity, and it makes the relationship obvious in your code.

Q: When should I use a many-to-many vs a join entity?

If the link itself carries no data, use the automatic many-to-many (a list on each side). If the link needs its own fields — say a BookTag with an AddedDate — model it as a real entity with two one-to-many relationships instead.

Q: Eager, lazy or explicit — which should I default to?

Default to eager ( Include ). It makes every database round-trip explicit in your code, which is exactly what you want in a web app. Reach for explicit loading when related data is only occasionally needed, and avoid lazy loading unless you fully understand the N+1 risk.

Q: What's the difference between EnsureCreated() and migrations?

EnsureCreated() builds the whole schema once from the current model and can't evolve it — fine for quick demos and tests. Migrations track each change over time and can upgrade an existing database without losing data. Use migrations for anything real, and never mix the two.

Q: My query got slow after adding an Include — why?

You probably hit a cartesian explosion: including multiple collections joins them all into one result, duplicating parent rows. Add .AsSplitQuery() so EF runs a separate query per collection, or only Include what you actually use.

No blanks this time — just a brief and a starter with the data set up for you. Model and query a small relational graph in memory: a list of OrderItem s (each with a ProductId and Quantity ) joined to Product s. Total the units ordered and find the most expensive line. Run it and check your output against the expected lines in the comments.

Practice quiz

In a one-to-many relationship, where does the foreign key live?

  • On the 'one' side
  • In a separate join table
  • On the 'many' (dependent) side
  • Nowhere — it's inferred only

Answer: On the 'many' (dependent) side. The 'many' side carries the foreign key (e.g. Book.AuthorId) plus a reference navigation back to the 'one' side.

How do you model a one-to-many in your entity classes?

  • A collection navigation on the 'one' side, a reference + FK on the 'many' side
  • A List on both sides
  • An owned entity
  • A unique FK on the principal

Answer: A collection navigation on the 'one' side, a reference + FK on the 'many' side. The 'one' side has a collection (List<Book> Books); the 'many' side has a reference (Author) and the FK (AuthorId).

Since EF Core 5, how is a many-to-many relationship configured?

  • You must hand-write a join entity class
  • With OwnsOne
  • It isn't supported
  • A List on each side; EF generates the hidden join table

Answer: A List on each side; EF generates the hidden join table. You put a collection on each side and EF generates the join table for you — a 'skip navigation', so you never mention the middle table.

In a one-to-one relationship, the foreign key is placed where and made what?

  • On the principal, made nullable
  • On the dependent, made unique
  • On both sides
  • In the owned type

Answer: On the dependent, made unique. The FK lives on the dependent side (e.g. Profile.AuthorId) and is made unique, so each principal maps to at most one dependent.

How is an owned entity (configured with OwnsOne) stored?

  • As extra columns folded into the owner's table
  • In its own separate table
  • As a many-to-many join
  • It isn't persisted

Answer: As extra columns folded into the owner's table. An owned entity has no identity of its own; its columns are folded into the owner's table (e.g. HomeAddress_Street on Authors).

What does eager loading with Include() do?

  • Loads the relation in a separate later query
  • Loads it automatically on first access
  • Loads the related data in the same query (a JOIN)
  • Skips the relation entirely

Answer: Loads the related data in the same query (a JOIN). Include pulls the related data in the same query as one round-trip, making the database access explicit.

What is ThenInclude used for?

  • Filtering rows
  • Reaching a second level of related data (e.g. an author's books' tags)
  • Splitting a query
  • Deleting related rows

Answer: Reaching a second level of related data (e.g. an author's books' tags). ThenInclude chains onto an Include to load a deeper level — for example each book's Tags after including an author's Books.

What problem does AsSplitQuery() solve?

  • The N+1 problem
  • Missing foreign keys
  • Migration drift
  • Cartesian explosion from including multiple collections

Answer: Cartesian explosion from including multiple collections. Including several collections multiplies rows (cartesian explosion); AsSplitQuery issues one query per collection instead.

Which loading strategy is the classic source of accidental N+1 queries?

  • Eager loading
  • Lazy loading
  • Explicit loading
  • Split query

Answer: Lazy loading. Lazy loading fires a query the first time you touch a navigation, so a navigation read inside a loop becomes one query per iteration.

What do EF Core migrations let you do?

  • Run queries faster
  • Cache query results
  • Version and evolve your database schema over time with Up/Down
  • Replace the DbContext

Answer: Version and evolve your database schema over time with Up/Down. Each migration has an Up() to apply a schema change and a Down() to reverse it; Add-Migration then Update-Database evolves the database safely.