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. Knowing which one you're in, and when it runs, is the whole senior game.
This is 30 questions across the execution model, operators & correctness, performance & EF translation, and advanced pitfalls. Each has a short answer, a deep dive with C# code, and the senior signal the interviewer is listening for. Every example is from Mattrx — a real multi-tenant platform on .NET 9 / EF Core 9 over Azure SQL, with Campaigns (~4M rows) and CampaignEvents (~1.2B rows).
Block A — the execution model
1. What is deferred execution?
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). Because it's lazy, the query reflects the data at enumeration 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
2. What operators trigger immediate execution?
Anything that must produce a concrete result: ToList/ToArray/ToDictionary, Count/Sum/Min/Max, First/Single/Last, Any/All, and a foreach. Operators returning IEnumerable/IQueryable are deferred; operators returning a value or materialized collection execute now.
3. Which operators stream and which buffer?
Streaming operators (Where, Select, Take, Skip) produce elements one at a time — constant memory. Buffering operators (OrderBy, GroupBy, Reverse, Distinct) must consume the entire source before yielding the first result.
Where/Select: pull 1 -> filter/map -> yield 1 (streaming, O(1) extra memory)
OrderBy: pull ALL -> sort -> then yield (buffering, O(n) memory)
4. What is the multiple-enumeration bug?
Because queries are deferred, enumerating the same query variable twice re-runs it twice — two DB round trips for IQueryable. Fix by materializing once.
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);
5. IQueryable vs IEnumerable — what's the real difference?
IEnumerable<T> runs LINQ in memory using compiled delegates (LINQ to Objects). IQueryable<T> builds an expression tree a provider (EF Core) translates to SQL and runs on the database. Same Where, two totally different engines.
IQueryable<T> --(expression tree)--> EF provider --> SQL --> runs on the DB
IEnumerable<T> --(compiled delegate)--> runs in memory on the app server
6. Query syntax vs method syntax — any real difference?
None in capability — query syntax is compiler sugar that lowers to the same method calls. Query syntax reads better for joins/group by/let; method syntax exposes operators query syntax lacks (Count, Take, First).
7. What role do expression trees play?
For IQueryable, a lambda isn't compiled to a delegate — it's captured as an expression tree, code as data. EF Core walks that tree and translates it to SQL. Untranslatable C# is exactly what breaks the translation.
8. How is deferred execution actually implemented?
With iterators — yield return. An operator like Where returns a state machine that produces the next element only when asked.
// 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
}
Block B — operators & correctness
9. Select vs SelectMany?
Select maps each element to one result (1->1); if that result is a collection you get a nested sequence. SelectMany flattens — mapping each element to a collection and concatenating (1->many).
campaigns.Select(c => c.Events); // IEnumerable<IEnumerable<Event>> (nested)
campaigns.SelectMany(c => c.Events); // IEnumerable<Event> (flattened)
10. First vs FirstOrDefault vs Single?
First returns the first match, throws if none. FirstOrDefault returns default if none. Single expects exactly one and throws on zero or more than one. Single costs more because it verifies uniqueness.
11. How does GroupBy work, and where does it run?
GroupBy partitions by a key. In LINQ-to-Objects it buffers the whole source; in EF Core it translates to SQL GROUP BY — as long as you project aggregates, not the raw groups.
var perDay = await _db.Events
.Where(e => e.TenantId == tenantId)
.GroupBy(e => e.EventDate)
.Select(g => new DailyCount(g.Key, g.Count()))
.ToListAsync(ct);
12. Join vs GroupJoin vs navigation properties?
Join is an inner join. GroupJoin gives each left element its group of right matches (basis for left joins via SelectMany + DefaultIfEmpty). In EF Core, navigation properties express the relationship and EF generates the JOIN.
13. When do you use Aggregate?
Aggregate is the general fold — threading an accumulator through the sequence. Use it for custom reductions; for sums/counts/averages use the specialized operators (clearer, translatable to SQL). Aggregate usually forces client evaluation on IQueryable.
14. How do you dedup — Distinct vs DistinctBy vs GroupBy?
Distinct removes duplicate elements by equality. DistinctBy (.NET 6+) dedups by a key selector. GroupBy(key).Select(g => g.First()) is the older idiom. Distinct on objects depends on Equals/GetHashCode.
15. OrderBy / ThenBy — what should you watch for?
OrderBy then ThenBy for multi-key sorts (a second OrderBy replaces the first). Ordering buffers the whole source, and you must order before Skip/Take for deterministic pages.
16. Skip/Take for pagination — the gotchas?
Skip(n).Take(m) is offset pagination; it must follow an OrderBy, and large offsets get slow (the DB scans skipped rows). For deep pages, prefer keyset/cursor pagination (Where(id > lastSeen).Take(m)).
17. How does equality work for Union/Intersect/Except/Distinct?
The set operators compare with Equals/GetHashCode (or an IEqualityComparer<T>). Classes that don't override equality compare by reference — use records (value equality) or a custom comparer.
Block C — performance & EF translation
18. What LINQ translates to SQL, and what throws?
Standard operators over mapped columns translate; arbitrary C# methods EF can't represent 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.
19. Where does the N+1 problem hide in LINQ?
In a Select or loop that triggers a per-element query. One list query plus N element queries. Fix by projecting the related data in the same query.
// 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);
20. How does projection improve performance?
Select-ing into a DTO makes EF emit a narrower SELECT — only the columns you named, no full entity, no change tracking. For reads, projecting is almost always faster than materializing whole entities.
21. Count() vs Any() — which for existence checks?
Any(). It translates to SQL EXISTS and short-circuits at the first row; Count() > 0 counts every matching row first.
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
22. Does operator order matter?
Yes. Filter (Where) before projecting and ordering so the pipeline works on fewer rows; order before paging. On IQueryable EF composes the whole tree into one SQL statement, but on IEnumerable order controls how many elements each stage processes.
23. What's the danger of calling ToList() too early?
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. Keep the query as IQueryable until the last moment.
24. What is async LINQ, and its limits?
EF Core provides async materializers — ToListAsync, FirstOrDefaultAsync, AnyAsync, CountAsync. But there's no WhereAsync/SelectAsync: only the execution is async. For in-memory async streams, use IAsyncEnumerable/await foreach.
25. How do compiled queries help?
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 — reserve this for measured hot paths.
Block D — advanced, pitfalls & functional
26. What's the closure gotcha with LINQ in a loop?
A lambda captures the variable, not its value. foreach gives a fresh variable per iteration (safe since C# 5), but a for (var i...) capturing i still shares one binding.
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).
27. What do let, into, and multiple from do?
let introduces a computed value inside a query; into continues a query after a group/select (query continuation); multiple from clauses are SelectMany. Query-syntax conveniences that lower to method calls.
28. How do you write a custom LINQ operator?
An extension method on IEnumerable<T> — 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.
29. What LINQ pitfalls do you check for in production?
Multiple enumeration, ToList() too early, Count() > 0 instead of Any(), N+1 from per-element queries, missing OrderBy before paging, un-materialized queries escaping a disposed context, and set operations on classes without value equality.
30. When should you NOT use LINQ?
In hot, tight loops where the allocations (iterators, closures, delegates) measurably hurt — a plain for loop can be markedly faster and allocation-free. Favor LINQ for clarity; drop to loops where a profiler says it matters.
The through-line: LINQ is one syntax over two engines — in-memory delegates and database expression trees. 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.
This post was originally published on PrepStack. The full original has all the code and senior-signal notes.
Top comments (0)