Loading related data¶
Every way EF Core loads a navigation works here, and each one costs a round trip you can see.
Eager loading, one round trip¶
Include is part of the query, so the related rows arrive with the principals:
List<Order> orders = await context.Orders
.Include(o => o.Customer)
.Where(o => o.Freight > 50m)
.ToListAsync();
// orders[0].Customer is populated. No further request.
ThenInclude and filtered includes work the same way:
List<Customer> customers = await context.Customers
.Include(c => c.Orders.Where(o => o.PlacedOn > cutoff))
.ThenInclude(o => o.Lines) // an OrderLine collection on Order
.ToListAsync();
Reach for this by default. One request that returns a graph beats several requests that return the same graph in pieces.
Explicit loading, one round trip when you ask¶
Load a navigation on an entity you already have:
Customer customer = await context.Customers.SingleAsync(c => c.Id == "ALFKI");
await context.Entry(customer).Collection(c => c.Orders).LoadAsync();
await context.Entry(order).Reference(o => o.Customer).LoadAsync();
Each LoadAsync is exactly one request. Use it for a master-detail screen, where the detail is
fetched when a row is selected rather than for every row in the list.
You can filter or aggregate before loading, which sends the query rather than the whole collection:
int count = await context.Entry(customer)
.Collection(c => c.Orders)
.Query()
.CountAsync(o => o.Freight > 50m);
Lazy loading, a round trip per touch¶
Lazy loading works, through EF Core's proxies package as usual:
Order order = await context.Orders.SingleAsync(o => o.Id == 1);
string company = order.Customer!.Company; // fetches the customer now
int lines = order.Lines.Count; // and its lines now
Every touched navigation is a request, so a loop over 100 orders that reads order.Customer makes
100 requests: the classic N+1, and much more expensive over a network than against a local database.
A navigation getter is also synchronous, so a lazy load blocks the calling thread on the round trip.
In a UI application that means loading off the UI thread, or accepting the freeze.
Enable proxies on both halves. The two models have to agree about everything the wire names, and proxies add a model convention.
Not in Blazor WebAssembly
Automatic lazy loading is impossible in a browser: WebAssembly is single-threaded and cannot
block, so the synchronous getter throws after the request has already gone out. Use
LoadAsync. The whole story is on the
Blazor WebAssembly page.
Choosing¶
| You want | Use | Cost |
|---|---|---|
| A list plus its related data | Include |
one request |
| Detail for the row the user just clicked | LoadAsync |
one request, when clicked |
| A count or a filtered subset of a navigation | .Collection(…).Query() |
one request, small answer |
| Convenience in a non-UI, non-browser client | lazy loading | one request per touched navigation |
Only what you need¶
A projection sends less than an Include, because only the selected columns cross the wire:
var summary = await context.Orders
.Where(o => o.PlacedOn > cutoff)
.Select(o => new { o.Id, o.PlacedOn, Company = o.Customer!.Company })
.ToListAsync();
The join happens on the server, and what comes back is three values per row rather than two whole
entities. For a read-only grid this is usually the right shape. Combine it with AsNoTracking.