LyraLearn AI Learning Platform
Exams
← Module 5 Β· Entity Framework in Practice
🎧 Listen

Querying Well

Most EF performance problems are not EF problems β€” they are queries that fetch too much, too often, or too late. Four habits fix nearly all of them.

IQueryable is a recipe, not a result

A LINQ query against a DbSet builds an expression tree; nothing hits the database until you materialize it (ToList, First, Count, or iterating). That's deferred execution, and it's a superpower: you can compose filters conditionally and EF translates the final shape into one SQL statement:

var q = _db.Permits.AsQueryable();
if (status != null) q = q.Where(p => p.Status == status);
var page = q.OrderBy(p => p.Number).Skip(50).Take(25).ToList();

The moment you call .ToList() (or slip into IEnumerable), everything after it runs in memory on rows already fetched. The classic mistake β€” _db.Permits.ToList().Where(...) β€” downloads the whole table to filter it in C#. Compose first, materialize last.

Project early with Select

If a screen needs five columns, don't load fifty. A projection β€” Select into a view model before materializing β€” becomes a SQL SELECT of exactly those columns, including values reached through navigations (p.Applicant.Name becomes a join, not a second query). Projection is also the cleanest fix for lazy-loading and serialization issues: view models have no proxies, no cycles, no surprises.

Loading related data: Include vs lazy vs N+1

Access a navigation property that wasn't loaded and lazy loading (when enabled) silently issues another query. Do that inside a loop over 200 permits and you've run 201 queries β€” the N+1 problem, the most common EF performance bug in existence. Your options:

Note that lazy loading is off by default in EF Core β€” it only happens if someone opted in with UseLazyLoadingProxies(), so check your context configuration before assuming. Either way, when a page is slow, count the queries first β€” a profiler or EF's logging (next lessons) makes N+1 jump out instantly.

AsNoTracking for read-only work

By default the context tracks every entity it loads so it can detect changes. For read-only screens β€” grids, reports, lookups β€” that bookkeeping is pure overhead. Add .AsNoTracking() to skip it: less memory, faster materialization, and no accidental saves of things you never meant to modify. (Projections to view models are effectively untracked already.) A simple team rule works well: queries that feed a screen are no-tracking or projections; only code that intends to call SaveChanges loads tracked entities.

🧠 Quiz yourself on this lesson →

Ask the AI Tutor

Grounded in the course lessons β€” it cites its sources and says when it doesn't know.