Lazy, Eager and Explicit Loading
ReadingRoom's members page loads members, then shows each member's loans. On your laptop with three members it is instant. In production with fifty it is slow, and the log shows fifty-one queries. The cause is how related data was loaded.
The rule
Related data is never free: include it, load it, or pay per access.
Member has a navigation property to its loans:
public class Member
{
public int Id { get; set; }
public string Name { get; set; } = "";
public List<Loan> Loans { get; set; } = [];
}Loading db.Members does not fill Loans. You choose one of three strategies:
Eager — ask for it up front with Include:
var members = await db.Members
.Include(m => m.Loans.Where(l => l.ReturnedAt == null))
.ToListAsync();Explicit — load it later, on purpose, for one entity:
var member = await db.Members.FindAsync(5);
await db.Entry(member!).Collection(m => m.Loans).LoadAsync();Lazy — load it automatically the first time the property is read. It is off by default; you opt in with the Microsoft.EntityFrameworkCore.Proxies package, UseLazyLoadingProxies(), and virtual navigation properties.
Lazy loading is convenient and dangerous. A loop over 50 members that reads member.Loans fires one query for the members and then one per member: the N+1 problem. Each query is quick; together they are slow.
For multiple collections, Include can produce huge joined results; .AsSplitQuery() fetches each collection in its own query instead. And when a page only needs a few columns, a Select projection often beats loading whole entities.
The shortcut
Before a query, ask "will I use the related data for every row, some rows, or by surprise?" Every row → eager (Include). Some rows → explicit. "By surprise" is lazy — and usually a bug waiting to happen.
Count the queries
One for members, then one per member for loans: 1 + 50 = 51.
The trap: seeing an empty Loans list and concluding the member has no loans. Without Include or a load, EF simply never fetched them.
Drills
Answer fast — trust the rule.
Which method eagerly loads a member's loans in the same query?
Listing 20 members with lazy loading, the page reads each member's Loans. How many queries run?
What does lazy loading require in EF Core?
A librarian expands one member out of a list to see their loans. Which strategy fits best?
Remember
- Related data is never free: include it, load it, or pay per access.
- Every row →
Include; some rows → explicitLoadAsync; lazy invites N+1. - An empty navigation often means "not loaded", not "none".
