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.
"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.
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.