Write Readable, Composable Queries
The best LINQ code reads like a sentence: orders.Where(o => o.IsPaid).OrderByDescending(o => o.Date).Select(o => o.ToSummary()) tells a reviewer exactly what happens, in order, without needing to trace loop variables or mutable accumulators. Resist the urge to cram everything into one unreadable one-liner with deeply nested lambdas — breaking a complex query into named intermediate variables (var paidOrders = ...; var summaries = paidOrders.Select(...);) costs nothing at runtime (LINQ is lazily evaluated either way) and makes the intent legible to the next person who has to modify it, including future you.
Cricket analogy: It's like a coach explaining a game plan step by step — 'first assess the pitch, then set the field, then choose the bowler' — rather than mumbling all three decisions in one breath; naming intermediate LINQ variables is the coding equivalent of laying out the plan in clear, separate steps.
Avoid Common Performance Pitfalls
The single most common LINQ performance mistake is multiple enumeration: passing an unmaterialized IEnumerable<T> query around and letting different parts of the code enumerate it independently, which re-runs the entire query (including any expensive database call or file I/O) every single time. If a query result is going to be used more than once — checked with Any(), then iterated, then counted — call ToList() once immediately after building the query and work with the concrete list from then on; the memory cost of holding the list is almost always cheaper than re-running the source query repeatedly. A second common mistake is calling Count() > 0 or !list.Any() the wrong way around out of habit rather than reasoning about short-circuiting, and a third is filtering after ToList() (items.ToList().Where(...)) instead of before, which materializes far more data than necessary before discarding most of it.
Cricket analogy: It's like a broadcaster re-requesting the full ball-by-ball feed from the stadium three separate times — once to check if a wicket fell, once to show the replay, once to update the scorecard — instead of pulling the feed once and reusing it for all three (ToList once, work from the list).
// BAD: multiple enumeration re-runs the query (and the DB call) three times.
var expensiveQuery = _db.Orders.Where(o => o.Status == OrderStatus.Pending);
if (expensiveQuery.Any())
{
var count = expensiveQuery.Count();
foreach (var order in expensiveQuery) { /* ... */ }
}
// GOOD: materialize once, reuse the concrete list.
var pendingOrders = await _db.Orders
.Where(o => o.Status == OrderStatus.Pending)
.ToListAsync();
if (pendingOrders.Count > 0)
{
foreach (var order in pendingOrders) { /* ... */ }
}
Filtering after materializing — items.ToList().Where(x => x.IsActive) — pulls every row into memory before discarding most of it. Always place Where (and any other narrowing operator) before ToList()/ToListAsync() so filtering happens as early as possible, ideally server-side against an IQueryable<T>.
IQueryable-Specific Best Practices
When a query is backed by EF Core or another IQueryable<T> provider, the overriding rule is to keep the query translatable: stick to operators and expressions the provider actually supports, because calling an arbitrary C# method (a custom static helper, a regex match, a non-mapped property getter) inside a Where clause on IQueryable<T> either throws at runtime or — worse, in older EF versions — silently falls back to client-side evaluation, quietly pulling the whole table into memory. Use AsNoTracking() for read-only queries so EF Core skips the overhead of change-tracking snapshots it will never need, and prefer projecting to a DTO with Select before materializing rather than fetching full entities and mapping afterward, since the projection lets EF Core generate a narrower SQL SELECT.
Cricket analogy: It's like a translator at a press conference who can only translate standard cricket terminology into the local language — asking them to translate an obscure regional slang term (an untranslatable method call) either fails outright or forces them to fetch a native speaker (client-side evaluation) instead of translating live, the way EF Core can't translate arbitrary C# methods to SQL.
For read-only queries in EF Core, chain .AsNoTracking() (or use a DbContext configured with QueryTrackingBehavior.NoTracking) — it avoids the memory and CPU cost of building change-tracking snapshots for entities you'll never call SaveChanges on.
Testing and Maintainability
LINQ-to-Objects logic (pure in-memory transformations, unlike EF Core's IQueryable<T> translation layer) is straightforward to unit test: feed a small in-memory List<T> of representative objects, including edge cases like an empty list or a list with exactly one matching element, and assert on the projected/filtered result. To keep queries maintainable, extract commonly repeated predicates into named extension methods — orders.Where(o => o.IsOverdue()) reads better and is tested once, rather than the same o.DueDate < DateTime.UtcNow && !o.IsPaid condition being copy-pasted (and potentially drifting out of sync) across a dozen call sites.
Cricket analogy: It's like testing a run-rate calculator against a handful of known scorecards, including an edge case like a rain-shortened match with zero overs bowled, before trusting it on live data — and giving a repeated condition like 'chasing team, last over, needs under 10' its own named function ('IsLastOverThriller') instead of re-writing the boolean logic at every commentary box.
- Prefer readable, composable chains with named intermediate variables over dense one-liners with nested lambdas.
- Materialize a query once with ToList()/ToListAsync() when its result will be used more than once, to avoid multiple enumeration.
- Filter before materializing, not after — items.Where(...).ToList(), never items.ToList().Where(...).
- Keep IQueryable<T> queries translatable; calling unsupported methods inside a Where clause can throw or silently trigger client-side evaluation.
- Use AsNoTracking() for read-only EF Core queries to skip unnecessary change-tracking overhead.
- Extract repeated predicates into named extension methods so the condition is defined and tested once.
- Unit test LINQ-to-Objects logic against small in-memory lists that include edge cases like empty or single-element sequences.
Practice what you learned
1. What is the recommended fix for a LINQ query that is enumerated multiple times in different parts of a method?
2. Why should filtering happen before ToList() rather than after, in a chain like items.Where(x => x.IsActive).ToList()?
3. What can happen if you call an unsupported custom C# method inside a Where clause on an EF Core IQueryable<T>?
4. What is the benefit of extracting a repeated LINQ predicate into a named extension method like IsOverdue()?
Was this page helpful?
You May Also Like
LINQ in Real Projects
How LINQ is actually used day-to-day in production .NET codebases, from EF Core queries to DTO projection and reporting.
Common LINQ Patterns
The recurring LINQ idioms — filtering, flattening, grouping, and joining — that show up in almost every C# codebase.
LINQ Interview Questions
The conceptual questions, coding challenges, and gotchas that come up most often when LINQ is tested in C# interviews.
Related Reading
Related Study Notes in Microsoft Technologies
Browse all study notesWindows 10 / UWP Development Study Notes
.NET · 30 topics
Microsoft TechnologiesWindows Batch Scripting Study Notes
Batch · 30 topics
Microsoft TechnologiesMFC (Microsoft Foundation Classes) Study Notes
C++ · 30 topics
Microsoft TechnologiesSilverlight Study Notes
.NET · 30 topics
Microsoft TechnologiesXAML Study Notes
.NET · 30 topics
Microsoft TechnologiesWPF (Windows Presentation Foundation) Study Notes
.NET · 30 topics