C# LINQ
LINQ is two different tools sharing one syntax: in-memory sequence pipelines (LINQ-to-Objects) and expression trees translated to SQL (EF-style providers). Most LINQ bugs come from forgetting which one you are holding.
Method
- Respect deferred execution. A query describes work; iteration
performs it. Consequences: enumerating twice runs twice (and hits
the database twice); captured variables are read at enumeration
time, not definition time; an
IEnumerablereturned up the stack may execute long after its data source changed. Materialize deliberately withToList()/ToArray()exactly once at the boundary where results are needed (see caching-strategy's cache-boundary instinct, applied in-process). - Keep provider queries translatable. Inside an
IQueryable, only what the provider can turn into SQL belongs: no local method calls, no custom property logic. Filter, project (Selectinto a DTO with just the needed columns), sort, and page in the database; switch to objects explicitly (AsEnumerable()) only when server-side work is done. The classic failure is a filter that silently runs client-side after fetching the table (see n-plus-one-queries, pagination-performance). - Choose operators for intent and cost.
AnyoverCount() > 0;FirstOrDefaultwith a predicate overWhere(...).FirstOrDefault();ToDictionary/ToLookupto replace nestedWherescans (turning O(n*m) joins into O(n+m): see algorithmic-optimization);GroupByandJoinknowingly, as they buffer. Know the buffering operators (OrderBy,GroupBy,Reverse) versus streaming ones (Where,Select,Take) when pipelines process large sequences. - Drop to loops without guilt when they win. Early exit with
side effects, index arithmetic, multiple accumulators in one
pass, hot paths where allocation matters (each lambda and
iterator allocates; profiles decide: see benchmark-design), and
any pipeline a teammate had to read twice. A clear
foreachbeats a clever aggregate; LINQ is for making intent more readable, not for code golf (see cognitive-load). - Compose queries, not strings. Build conditional filters by
chaining (
if (x != null) query = query.Where(...)): the provider composes one SQL statement, and the pattern replaces dynamic SQL assembly (see sql-injection-defense). Extract reusable filters asIQueryableextension methods so the same predicate is not re-invented per call site. - Verify the generated SQL for the hot queries. Log or intercept the SQL for the top endpoints (EF's ToQueryString, query logging): check the WHEREs landed server-side, the columns projected are the ones needed, and the plan behaves (see query-plan-reading, orm-tradeoffs). LINQ that reads well and queries badly is found here, not in review.
Boundaries
- Async enumeration (
ToListAsync,await foreach) belongs to the provider/stream boundary (see dotnet-async); mixing sync materialization into async request paths blocks threads. - PLINQ parallelizes CPU-bound in-memory work only, with the same shared-state cautions as any parallelism; it is not for I/O and not a default (see python-concurrency triage).
- Query syntax vs method syntax is style except for joins and let-clauses where query syntax reads better; pick per-team and stay consistent (see style-guides).