Previously, in Part 3, we defused the deadlock special and built a real mental model of async/await. This part covers the other pillar of the technical round: collections and LINQ. These questions do double duty — they show up in the C# round and resurface in the data round, because LINQ is how most .NET code talks to its database.
Four questions cover this round: choosing List, Dictionary, or HashSet by access pattern (indexed sequence, key lookup, membership test), IEnumerable versus IQueryable (compiled delegates in memory versus an expression tree translated to SQL), deferred execution (a query is a recipe that runs every time it's enumerated), and what yield return does (the compiler turns the method into a lazy state machine that streams values on demand).
"When do you pick List, Dictionary, or HashSet?"
Why they ask it: It's the practical baseline. Interviewers want to see that you choose collections by access pattern, not by habit — and that you know the cost model behind each choice.
A strong answer: Choose by the question your code asks most often.
List<T> is an ordered, indexable sequence — great for "give me item 5"
and iteration, but "does it contain X?" means scanning. Dictionary<TKey, TValue>
answers "find the value for this key" in constant time via hashing.
HashSet<T> is a dictionary without values: fast membership tests and
automatic uniqueness.
| Operation | List<T> | Dictionary<K,V> | HashSet<T> |
|---|---|---|---|
| Access by index | O(1) | — | — |
| Lookup by key / contains | O(n) | O(1) | O(1) |
| Add to end | O(1) amortized | O(1) | O(1) |
| Insert in middle | O(n) | — | — |
| Preserves order? | Yes | No (don't rely on it) | No |
Then the calibration that earns senior points: for small collections — a dozen items — none of this matters, and a List is often fastest in practice thanks to memory locality. Big-O describes growth, not small-n reality. "I'd pick the structure that matches the access pattern, and I'd measure before micro-optimizing" is the complete answer.
Try it: Fill a List<int> and a HashSet<int> with a million numbers, then time 10,000 Contains calls against each with Stopwatch. Then rerun with 20 items and notice the difference evaporate. Both halves of that experiment belong in your answer.
Follow-ups to expect:
- "What happens when a List runs out of capacity?" — It allocates a bigger array (roughly doubling) and copies — hence "amortized" O(1) adds.
- "What makes a good dictionary key?" — Stable, immutable, with consistent
EqualsandGetHashCode. - "When would you use a Queue or Stack instead?" — When the access pattern is the point: FIFO pipelines, LIFO undo histories.
Red flag: "I just use List for everything," or reciting the big-O table perfectly and then admitting every membership check in your last project was list.Contains in a loop.
"IEnumerable vs IQueryable — what's the real difference?"
Why they ask it: This is the bridge question between the C# round and the data round, and it has a famous failure mode in Entity Framework that interviewers use as the follow-up. Get this right and you look like someone who has debugged real production queries.
A strong answer: Both represent a query, but they carry it differently.
IEnumerable<T> holds compiled delegates — actual code
that runs in memory over objects you already have. IQueryable<T> holds
an expression tree — a data structure describing your lambdas —
which a provider like EF Core can inspect and translate into SQL, so the filtering happens
in the database. Same LINQ syntax, completely different execution story.
// IQueryable: translated to SQL — the database does the filtering
var adults = db.Users
.Where(u => u.Age >= 18)
.ToList(); // SELECT ... WHERE Age >= 18
// The classic mistake: AsEnumerable/ToList too early
var adults2 = db.Users
.AsEnumerable() // downloads EVERY user first...
.Where(u => u.Age >= 18) // ...then filters in your app's memory
.ToList();
Both return identical results, which is exactly why the bug survives code review — until
the table has a million rows and the second query hauls all of them across the network to
keep three. The rule: keep the query IQueryable until you've expressed every
filter, sort, and projection, and only then materialize.
Try it: In any EF Core project, log the generated SQL (or use query.ToQueryString()) for both versions above. Seeing the WHERE clause present in one and missing in the other is the difference, in black and white.
Follow-ups to expect:
- "Why can't every C# method be translated to SQL?" — The provider must understand the expression; arbitrary method calls have no SQL equivalent and either throw or force client evaluation.
- "What is an expression tree, really?" — Your lambda stored as inspectable data (nodes for calls, members, constants) instead of compiled code.
- "When is switching to in-memory processing legitimate?" — After the database has shrunk the data: complex reshaping of a small, already-filtered result.
Red flag: "IQueryable is just IEnumerable for databases, they basically work the same." That's precisely the mental model that ships the download-the-table bug.
"What is deferred execution, and how has it burned people?"
Why they ask it: Deferred execution is LINQ's most elegant feature and its most reliable source of production incidents. Interviewers ask because the failure stories reveal whether you've actually shipped LINQ-heavy code.
A strong answer: A LINQ query is a recipe, not a meal.
Writing var q = items.Where(...) executes nothing; the query runs each time
something iterates it — a foreach, a Count, a ToList. That laziness is powerful (you can
build queries up conditionally, and only pay for what you enumerate), but it has two
classic teeth. First, the query sees the data as it is at enumeration time, not
as it was when you wrote the query. Second — the big one — multiple
enumeration:
var expensive = orders.Where(o => SlowFraudCheck(o));
var count = expensive.Count(); // runs every SlowFraudCheck
var first = expensive.First(); // runs them all AGAIN
// The fix: materialize once, then reuse the results
var flagged = orders.Where(o => SlowFraudCheck(o)).ToList();
Against a database, that's two round trips; against an expensive predicate, double the
work. ToList() fixes it by executing once and caching the results — and the
nuanced answer names its cost too: you allocate the whole result in memory and freeze it
at that moment. Materialize deliberately, not reflexively.
Try it: Put a Console.WriteLine("checked!") inside a Where predicate over ten items, then call Count() followed by First() on the same query variable. Count the "checked!" lines. Now add ToList() and count again.
Follow-ups to expect:
- "Which LINQ operators execute immediately?" — Anything returning a scalar or concrete collection: Count, Sum, First, ToList, ToArray, ToDictionary.
- "What's the captured-variable trap?" — The query uses the variable's value at enumeration time, so changing it after building the query changes the results.
- "How would you spot multiple enumeration?" — Analyzer warnings, duplicated SQL in EF logs, or a suspicious pair of round trips in a trace.
Red flag: "The query runs when you write the Where line." One sentence, and the interviewer knows every burn in this section is still ahead of the candidate, not behind them.
"What does yield return actually do?"
Why they ask it: It's the "do you know what the compiler does for you" question — the collections cousin of Part 3's async state machine — and it tests whether you understand where deferred execution actually comes from.
A strong answer: A method containing yield return doesn't
run when you call it. Instead, the compiler rewrites it into a state machine
object implementing IEnumerable<T>: calling the method just constructs
that object, and each step of iteration runs the method as far as the next
yield return, hands out that value, and pauses — locals preserved — until the
next step. If that sounds like await's bookmark trick from
Part 3, it should: both are the
compiler turning a pausable method into a resumable object.
The practical superpower is streaming: values are produced one at a time, on demand, so you never need the whole sequence in memory — which is how you can process a huge file line by line, or even define an infinite sequence:
static IEnumerable<int> Naturals()
{
var n = 1;
while (true)
{
yield return n++; // pause here; resume on next MoveNext
}
}
foreach (var n in Naturals().Take(5))
{
Console.WriteLine(n); // 1 2 3 4 5 — the infinite loop never runs away
}
Try it: Add a Console.WriteLine("producing " + n) before the yield in Naturals and run the foreach above. The interleaving — produce one, consume one — is lazy evaluation made visible. Then try calling Naturals().Count() and enjoy explaining why it hangs.
Follow-ups to expect:
- "When does the code before the first yield run?" — On the first MoveNext, not when the method is called — a classic surprise for argument validation.
- "What does yield break do?" — Ends the sequence early, like a return for iterators.
- "When would you avoid an iterator?" — When callers will enumerate repeatedly or need a count/index — materializing is cheaper than re-running the generator.
Red flag: "yield return adds the item to a list and returns the list at the end." That's the eager model — the exact opposite of what iterators do — and it fails the infinite-sequence example instantly.
Frequently asked
- When should you use List, Dictionary, or HashSet in C#?
- Choose by the question your code asks most often. List is an ordered, indexable sequence, great for access by index and iteration, but checking whether it contains an item means scanning. Dictionary answers 'find the value for this key' in constant time via hashing, and HashSet is a dictionary without values: fast membership tests and automatic uniqueness. For a dozen items none of this matters, so measure before micro-optimizing.
- What is the difference between IEnumerable and IQueryable?
- Both represent a query, but IEnumerable holds compiled delegates that run in memory over objects you already have, while IQueryable holds an expression tree describing your lambdas that a provider like EF Core can translate into SQL, so the filtering happens in the database. Calling AsEnumerable or ToList too early downloads every row and filters in your app's memory, so keep the query IQueryable until every filter, sort, and projection is expressed.
- What is deferred execution in LINQ?
- A LINQ query is a recipe, not a meal: writing a Where executes nothing, and the query runs each time something iterates it, such as a foreach, Count, or ToList. That means the query sees the data as it is at enumeration time, and enumerating the same query twice runs it twice, which against a database is two round trips. ToList executes once and caches the results, at the cost of holding the whole result in memory.
- What does yield return do in C#?
- A method containing yield return does not run when you call it. The compiler rewrites it into a state machine object implementing IEnumerable; each iteration step runs the method as far as the next yield return, hands out that value, and pauses with its locals preserved. That streams values one at a time on demand, so you can process a huge file line by line or define an infinite sequence, and code before the first yield runs on the first MoveNext, not at the call.
Where to next
Collections chosen by access pattern, queries kept translatable, recipes materialized on purpose, and iterators understood as compiler magic — that's the whole round. If LINQ itself still feels new, my beginner's guide to LINQ builds the foundation these questions stand on.
With the language rounds behind you, it's time for the round that mirrors the day job: Part 5 — ASP.NET Core Interview Questions, covering middleware, dependency injection, and the web/API interview.