LINQ Interview Questions for Senior Developers — Deferred Execution, IQueryable vs IEnumerable, Performance & Pitfalls (Deep Answers)
30 senior LINQ interview questions with deep C# answers: deferred execution, IQueryable vs IEnumerable, EF translation, performance, and pitfalls.
- Author
- Randhir Jassal
- Published
- Reading time
- 18 min read
- Views
- 5 views
Junior LINQ interviews ask "what does
Wheredo." Senior interviews ask why your query hit the database three times to render one number, why.Where(...)after.ToList()quietly loads a billion rows into memory, and why the same LINQ runs on SQL Server in one place and in C# in another. LINQ looks like a tidy query language and is really two very different engines (in-memory delegates vs database expression trees) behind one syntax — and knowing which one you're in, and when it runs, is the whole senior game. This guide is 30 questions across the execution model, operators & correctness, performance & EF translation, and advanced pitfalls.
Each answer is a short answer you could say in one breath, a deep dive into the mechanism (with C# code), and the senior signal the interviewer is listening for. Every example is from Mattrx — a real multi-tenant marketing-analytics platform on .NET 9 / EF Core 9 over Azure SQL, with Campaigns (~4M rows) and CampaignEvents (~1.2B rows), every query scoped by TenantId. No Foo and Bar.
How to use this guide
Read the deep dive even when the operator is obvious — seniority is in when the query runs, where it runs, and what SQL it becomes. The blocks build on each other. If you can say when each snippet executes and on which engine, you're ready.
Block A — the execution model
1. What is deferred execution?
Short answer: A LINQ query defines what to compute but doesn't run until you enumerate it — the work is deferred to the moment you iterate (or call a materializing operator). Building the query is free; iterating is where the cost lands. This is LINQ's single most important concept.
Deep dive: Because it's lazy, the query reflects the data at enumeration time, not definition time.
var active = campaigns.Where(c => c.IsActive); // defines the query — nothing runs yet
campaigns.Add(newlyActiveCampaign); // added BEFORE we enumerate...
foreach (var c in active) { /* ... */ } // query RUNS here — newCampaign is included
Senior signal: You describe LINQ as lazy-by-default, know enumeration triggers the work, and can predict that late-added data shows up.
2. What operators trigger immediate execution?
Short answer: Anything that must produce a concrete result: ToList/ToArray/ToDictionary, Count/Sum/Min/Max, First/Single/Last, Any/All, and a foreach. Everything else (Where, Select, OrderBy, Take…) is deferred and just builds the pipeline.
Deep dive: The rule of thumb: operators returning IEnumerable/IQueryable are deferred; operators returning a value or a collection execute now.
Senior signal: You split operators into "returns a query (deferred)" vs "returns a value/materialized collection (immediate)" without memorizing a list.
3. Which operators stream and which buffer?
Short answer: Streaming operators (Where, Select, Take, Skip) produce elements one at a time as you pull — constant memory. Buffering operators (OrderBy, GroupBy, Reverse, Distinct) must consume the entire source before yielding the first result. Buffering breaks streaming and holds everything in memory.
Deep dive: That's why OrderBy on a huge in-memory sequence can blow up memory: it materializes all of it to sort.
Where/Select: pull 1 -> filter/map -> yield 1 (streaming, O(1) extra memory)
OrderBy: pull ALL -> sort -> then yield (buffering, O(n) memory)
Senior signal: You know sorting/grouping/dedup must buffer the whole source, and you place them thoughtfully in the pipeline (and prefer to push them to SQL).
4. What is the multiple-enumeration bug?
Short answer: Because queries are deferred, enumerating the same query variable twice re-runs it twice — two DB round trips for IQueryable, two full passes for IEnumerable. The fix is to materialize once (ToList) and reuse the result.
Deep dive:
var q = _db.Campaigns.Where(c => c.TenantId == tenantId);
var count = q.Count(); // DB round trip #1
var first = q.First(); // DB round trip #2
var page = q.Take(20).ToList(); // DB round trip #3 — three queries for "one" query!
// FIX: materialize once, then work with the list in memory.
var list = await q.ToListAsync(ct);
Senior signal: You spot re-enumeration of a deferred query and materialize once — and you know resharper/analyzer warnings about "possible multiple enumeration" are real.
5. IQueryable vs IEnumerable — what's the real difference?
Short answer: IEnumerable<T> runs LINQ in memory using compiled delegates (LINQ to Objects). IQueryable<T> builds an expression tree that a provider (EF Core) translates to another language — SQL — and runs on the database. Same Where, two totally different engines.
Deep dive: The moment a query goes from IQueryable to IEnumerable (an AsEnumerable(), or a method EF can't translate), everything after it runs in C# on rows already pulled over the wire.
IQueryable<T> --(expression tree)--> EF provider --> SQL --> runs on the DB
IEnumerable<T> --(compiled delegate)--> runs in memory on the app server
Senior signal: You track the IQueryable/IEnumerable boundary as "expression tree -> SQL" vs "delegate -> memory," and know exactly where a query leaves the database.
6. Query syntax vs method syntax — any real difference?
Short answer: None in capability — query syntax (from c in … where … select) is compiler sugar that lowers to the same method calls (Where, Select…). Query syntax reads better for joins/group by/let; method syntax exposes operators query syntax lacks (Count, Take, First). Most teams mix them.
Deep dive: from c in xs where c.Active select c.Name compiles to xs.Where(c => c.Active).Select(c => c.Name) — identical IL.
Senior signal: You know they compile to the same thing and choose per readability, reaching for query syntax on complex joins/groupings.
7. What role do expression trees play?
Short answer: For IQueryable, a lambda isn't compiled to a delegate — it's captured as an expression tree, a data structure describing the code. EF Core walks that tree and translates it into SQL. That's why LINQ-to-Entities can run on the database and LINQ-to-Objects (delegates) cannot.
Deep dive: A method EF can't represent in the tree (an arbitrary C# helper) can't be translated -> it either throws or forces client evaluation.
Senior signal: You explain that expression trees are code as data that EF compiles to SQL, and that untranslatable C# is exactly what breaks the translation.
8. How is deferred execution actually implemented?
Short answer: With iterators — yield return. An operator like Where returns a state machine that produces the next element only when asked, calling into its source lazily. LINQ-to-Objects is essentially a chain of yield-based iterators.
Deep dive: You can write your own deferred operator with yield return; it composes into the same pull-based pipeline.
// A custom streaming operator — deferred, one element at a time.
public static IEnumerable<T> WhereActive<T>(this IEnumerable<T> src, Func<T,bool> ok)
{
foreach (var x in src) if (ok(x)) yield return x; // runs only as the caller pulls
}
Senior signal: You connect deferred execution to yield/iterators and can hand-roll a lazy operator.
Block B — operators & correctness
9. Select vs SelectMany?
Short answer: Select maps each element to one result (1->1); if that result is itself a collection you get a nested IEnumerable<IEnumerable<T>>. SelectMany flattens — mapping each element to a collection and concatenating them into one sequence (1->many).
Deep dive:
campaigns.Select(c => c.Events); // IEnumerable<IEnumerable<Event>> (nested)
campaigns.SelectMany(c => c.Events); // IEnumerable<Event> (flattened)
Senior signal: You reach for SelectMany to flatten one-to-many (and know it's what multiple from clauses compile to).
10. First vs FirstOrDefault vs Single?
Short answer: First returns the first match, throws if none. FirstOrDefault returns default (null/0) if none. Single expects exactly one and throws if there are zero or more than one. Use Single to assert uniqueness, First when order-first is fine, and OrDefault when absence is valid.
Deep dive: Single runs an extra check (it must look for a second match), so First is cheaper when you only need one and uniqueness isn't the point.
Senior signal: You pick the operator by intent (assert-one vs take-first vs tolerate-none) and know Single costs more because it verifies uniqueness.
11. How does GroupBy work, and where does it run?
Short answer: GroupBy partitions a sequence by a key. In LINQ-to-Objects it buffers the whole source in memory to build groups; in EF Core it translates to SQL GROUP BY and runs on the database — as long as you project aggregates (not the raw groups).
Deep dive:
// Translates to SQL GROUP BY — aggregation happens on the DB, not in C#.
var perDay = await _db.Events
.Where(e => e.TenantId == tenantId)
.GroupBy(e => e.EventDate)
.Select(g => new DailyCount(g.Key, g.Count()))
.ToListAsync(ct);
Senior signal: You know GroupBy buffers in memory but translates to SQL GROUP BY in EF when you project aggregates — and that returning the raw groups can force client evaluation.
12. Join vs GroupJoin vs navigation properties?
Short answer: Join is an inner join (matching pairs). GroupJoin gives each left element its group of right matches (the basis for left joins via SelectMany + DefaultIfEmpty). In EF Core you rarely write either — navigation properties (c.Events) express the relationship and EF generates the JOIN.
Deep dive: Explicit Join is for keys EF doesn't model as a relationship; otherwise navigations are clearer and translate the same.
Senior signal: You prefer navigation properties in EF, and know GroupJoin + DefaultIfEmpty is how you express a left outer join in LINQ.
13. When do you use Aggregate?
Short answer: Aggregate is the general fold — it threads an accumulator through the sequence with your function. Use it for custom reductions the built-ins don't cover; for sums/counts/averages use Sum/Count/Average (clearer, and translatable to SQL).
Deep dive: Aggregate usually doesn't translate to SQL, so on IQueryable it tends to force client evaluation — keep it for in-memory reductions.
Senior signal: You use Aggregate only for custom in-memory folds and reach for the specialized aggregates on the database side.
14. How do you dedup — Distinct vs DistinctBy vs GroupBy?
Short answer: Distinct removes duplicate elements (by equality). DistinctBy (.NET 6+) dedups by a key selector without a custom comparer. GroupBy(key).Select(g => g.First()) is the older way to "distinct by a field." For reference types, Distinct needs proper equality (records or an IEqualityComparer).
Deep dive: Mattrx uses DistinctBy(e => e.UserId) to get unique users from an event stream in one pass.
Senior signal: You reach for DistinctBy over the GroupBy().Select(First) idiom and know Distinct on objects depends on Equals/GetHashCode.
15. OrderBy / ThenBy — what should you watch for?
Short answer: OrderBy then ThenBy for multi-key sorts (a second OrderBy replaces the first, doesn't refine it). LINQ-to-Objects OrderBy is a stable sort; ordering buffers the whole source. And ordering interacts with paging — you must order before Skip/Take for deterministic pages.
Deep dive: A common bug: Skip/Take without an OrderBy gives non-deterministic pages (DB order isn't guaranteed).
Senior signal: You use ThenBy (not a second OrderBy) for secondary keys and always order before paging.
16. Skip/Take for pagination — the gotchas?
Short answer: Skip(n).Take(m) is offset pagination; it must follow an OrderBy to be deterministic, and large offsets get slow (the DB still scans the skipped rows). For deep pages, prefer keyset/cursor pagination (Where(id > lastSeen).Take(m)).
Deep dive: Mattrx uses keyset pagination on CampaignEvents because Skip(1_000_000) would make SQL Server count through a million rows first.
Senior signal: You know offset pagination degrades on deep pages and switch to keyset/cursor for large sets.
17. How does equality work for Union/Intersect/Except/Distinct?
Short answer: The set operators compare elements with Equals/GetHashCode (or an IEqualityComparer<T> you pass). For classes that don't override equality, they compare by reference, which almost never does what you want — use records (value equality) or a custom comparer.
Deep dive: Two "equal" DTOs that are different instances won't dedup unless equality is value-based.
Senior signal: You make set operations correct by using records/IEqualityComparer, and you know reference equality is the silent default that breaks them.
Block C — performance & EF translation
18. What LINQ translates to SQL, and what throws?
Short answer: Standard operators over mapped columns translate; calling arbitrary C# methods EF can't represent (a custom helper, some string/date ops) can't be translated. EF Core 3.0+ throws rather than silently evaluating on the client — so an untranslatable query fails loudly instead of loading everything.
Deep dive: Move untranslatable logic into the final projection (which can run client-side) or rewrite it to translatable operators.
Senior signal: You know EF Core 3+ throws on untranslatable predicates and treat that as protection, not an obstacle. (More in the EF Core guide.)
19. Where does the N+1 problem hide in LINQ?
Short answer: In a Select (or loop) that triggers a per-element query — projecting a navigation without including/projecting it, or querying inside a foreach. One list query plus N element queries. Fix by projecting the related data in the same query.
Deep dive:
// N+1: the loop issues one COUNT query per campaign.
foreach (var c in await _db.Campaigns.ToListAsync(ct))
c.EventCount = await _db.Events.CountAsync(e => e.CampaignId == c.Id, ct);
// FIX: one query that projects the aggregate.
var list = await _db.Campaigns
.Select(c => new CampaignSummary(c.Id, c.Name, c.Events.Count()))
.ToListAsync(ct);
Senior signal: You catch per-element queries and collapse them into one projected query, verifying with the generated SQL.
20. How does projection improve performance?
Short answer: Select-ing into a DTO makes EF emit a narrower SELECT — only the columns you named, no full entity, no change tracking. Less data over the wire, less memory. For reads, projecting is almost always faster than materializing whole entities.
Deep dive: Mattrx's list endpoints project 3–4 columns instead of loading full Campaign rows with every column.
Senior signal: You project to DTOs for reads by default and connect it to a smaller SQL SELECT list.
21. Count() vs Any() — which for existence checks?
Short answer: Any(). To check "does at least one exist," Any translates to SQL EXISTS and short-circuits at the first row; Count() > 0 counts every matching row first. Same answer, very different cost on a large table.
Deep dive:
if (_db.Events.Count(e => e.CampaignId == id) > 0) { /* ... */ } // counts ALL matches
if (await _db.Events.AnyAsync(e => e.CampaignId == id, ct)) { /* ... */ } // EXISTS — stops at first
Senior signal: You use Any for existence, Count only when you need the number, and know the SQL difference (EXISTS vs COUNT).
22. Does operator order matter?
Short answer: Yes. Filter (Where) before projecting and ordering so the pipeline (and SQL) works on fewer rows; order before paging. On IQueryable, EF composes the whole tree into one SQL statement so the DB optimizes it — but on IEnumerable, order literally controls how many elements each stage processes.
Deep dive: Where then OrderBy sorts fewer rows than OrderBy then Where in memory; push filters as early as possible.
Senior signal: You filter early and order before paging, and you know in-memory pipelines are order-sensitive for performance while EF folds it into optimized SQL.
23. What's the danger of calling ToList() too early?
Short answer: It materializes the query at that point — so any Where/Select after it runs in memory on all the rows you just pulled, not in SQL. ToList() in the middle of a query is how "fast" code quietly loads a million rows and filters them in C#.
Deep dive: Keep the query as IQueryable until the last moment; materialize once, at the end.
Senior signal: You defer ToList() to the end of the query and flag mid-query materialization as a common perf regression.
24. What is async LINQ, and its limits?
Short answer: EF Core provides async materializers — ToListAsync, FirstOrDefaultAsync, AnyAsync, CountAsync — so the DB round trip doesn't block a thread. But there's no WhereAsync/SelectAsync: the operators build the query synchronously; only the execution is async. For in-memory async streams, use IAsyncEnumerable/await foreach.
Deep dive: Mattrx always uses the …Async materializers on the request path so the thread is freed during the DB call.
Senior signal: You know only the materializers are async (not the operators), and reach for IAsyncEnumerable for streaming async data.
25. How do compiled queries help?
Short answer: EF.CompileAsyncQuery caches the LINQ->SQL translation so a very hot query skips re-building the expression tree each call. EF already caches query plans, so this is a micro-optimization for the highest-frequency queries, not a default.
Deep dive: Mattrx compiles its two hottest dashboard queries; below a few thousand calls/sec it's not worth the ceremony.
Senior signal: You reserve compiled queries for measured hot paths and know EF already caches plans by default.
Block D — advanced, pitfalls & functional
26. What's the closure gotcha with LINQ in a loop?
Short answer: A lambda captures the variable, not its value — so building queries/delegates over a loop variable can all capture the same (final) variable. Modern foreach gives a fresh variable per iteration (safe since C# 5), but a for (var i…) capturing i still shares one binding.
Deep dive:
var predicates = new List<Func<int,bool>>();
for (var i = 0; i < thresholds.Count; i++)
predicates.Add(x => x > thresholds[i]); // BUG: all capture the same i (= Count)
// FIX: copy into a local, or use foreach (fresh binding per iteration).
Senior signal: You know lambdas capture variables (not values), that foreach is safe but indexed for isn't, and you copy the loop var into a local.
27. What do let, into, and multiple from do?
Short answer: let introduces a computed value inside a query (avoiding recomputation); into continues a query after a group/select (query continuation); multiple from clauses are SelectMany — the flattening cross-product/one-to-many. They're query-syntax conveniences that lower to method calls.
Deep dive: from c in campaigns from e in c.Events select … compiles to campaigns.SelectMany(c => c.Events, (c, e) => …).
Senior signal: You map let/into/multiple-from to their method-syntax equivalents and know multiple from = SelectMany.
28. How do you write a custom LINQ operator?
Short answer: An extension method on IEnumerable<T> (in-memory) — usually with yield return for deferred behavior. For IQueryable<T>, a custom operator must build/compose an expression tree to stay translatable, which is much harder — most custom operators are in-memory.
Deep dive: Mattrx has small in-memory helpers (Batch, WhereNotNull) as IEnumerable extension methods; anything that must hit SQL stays as composed standard operators.
Senior signal: You write in-memory operators as yield-based extensions and know an IQueryable operator needs expression-tree composition to translate.
29. What LINQ pitfalls do you check for in production?
Short answer: Multiple enumeration, ToList() too early (in-memory filtering), Count() > 0 instead of Any(), N+1 from per-element queries, missing OrderBy before paging, un-materialized queries escaping a using (disposed context), and set operations on classes without value equality. Most LINQ bugs are one of these.
Deep dive: The subtle one: returning an IEnumerable/IQueryable from a method whose DbContext is disposed by the time the caller enumerates — it throws on enumeration, far from the cause.
Senior signal: You have a concrete checklist and know a deferred query outliving its context is a classic "works in dev, throws in prod" trap.
30. When should you NOT use LINQ?
Short answer: In hot, tight loops where the allocations (iterators, closures, delegates) and indirection measurably hurt — a plain for loop can be markedly faster and allocation-free. Also when LINQ obscures rather than clarifies (a clever nested Aggregate no one can read). Favor LINQ for clarity; drop to loops where a profiler says it matters.
Deep dive: Mattrx keeps LINQ for 99% of code but uses plain loops in the hottest ingestion path where per-element allocations added up across 180M events.
Senior signal: You default to LINQ for readability but know its allocation cost in hot paths and can justify dropping to a loop with a measurement.
Bonus — what makes an answer "senior" in LINQ?
Short answer: A junior knows the operators; a senior knows when the query runs, where it runs, and what it becomes. "Why is this slow?" gets "add .ToList()" from a junior and "you already have a ToList() mid-query forcing in-memory filtering — remove it so the Where translates to SQL" from a senior.
Deep dive: The recurring senior lens across all 30: LINQ hides deferral, the memory-vs-database boundary, and the SQL it generates. Predict execution timing, keep queries as IQueryable until the end, project to narrow the SQL, and read the generated query.
Senior signal: You reason about deferral + the IQueryable/IEnumerable boundary reflexively, and you can always answer "when does this run and on which engine?"
The model to carry forward
LINQ is one syntax over two engines: in-memory delegates and database expression trees. Everything follows — deferral means the query runs on enumeration (so materialize once), the IQueryable/IEnumerable boundary decides SQL-vs-memory, projection narrows the SQL, Any beats Count > 0, and a mid-query ToList() silently drags work back to the app server. Answer each question with when it runs, where it runs, and what SQL it becomes, and you'll sound like someone who has profiled real LINQ against a billion rows — which is the signal.
Further reading
- EF Core Interview Questions for Senior Developers — Change Tracking, Query Performance, Concurrency & Migrations (Deep Answers)
- C# Interview Questions for Senior Developers — Async, Memory, LINQ & Concurrency (Deep Answers)
- SQL Server Interview Questions for Senior Developers — Execution Plans, Indexing & Locking (Deep Answers)
Prepping for a senior .NET interview and want these drilled live, or a mock focused on LINQ execution and EF translation? I'm always happy to help — reach me at randhir.jassal@gmail.com.
Get the next issue
A short, curated email with the newest posts and questions.