LINQ & collections
When does a LINQ query actually run
A LINQ query is a description of work, not a result. Where, Select, OrderBy and friends return a query object and run nothing. The predicate fires only when you enumerate: a foreach, ToList, ToArray, Count, First, or anything that pulls elements out.
var q = orders.Where(o => o.Total > 100); // nothing has filtered yet
foreach (var o in q) { ... } // NOW the predicate runs, once per element
Wrong "Where filters the list right there and stores the matches." It stores a recipe. Nothing is filtered until something asks for the first element.
The multiple-enumeration trap
Because a query holds a recipe and not a cached result, enumerating it twice runs the whole pipeline twice.
IEnumerable<Order> big = db.Orders.Where(o => o.Total > 100);
var count = big.Count(); // round-trip #1
foreach (var o in big) Render(o); // round-trip #2, re-runs everything
Against a database this is two SQL queries. Against an in-memory list it just wastes CPU, but if the source is non-repeatable (a network stream, a random generator, a query whose data changed between reads) the two passes can return different elements. The fix is to materialize once:
var list = db.Orders.Where(o => o.Total > 100).ToList(); // one round-trip
var count = list.Count; // now just reads the list
foreach (var o in list) Render(o);
ToList and ToArray are the materialization points: they force the query to run and snapshot the results into a real collection you can reuse and index.
IEnumerable vs IQueryable, the short version
Both describe a deferred query. The difference is where it runs.
IEnumerable<T>carries compiled delegates (Func<T, bool>). Its operators run in your process, in memory. This is LINQ to Objects.IQueryable<T>carries expression trees (Expression<Func<T, bool>>). A provider like EF Core walks the tree and translates it to SQL, so the filter runs at the database.
The practical rule: keep the type IQueryable while you still want work pushed to the database, and only call AsEnumerable or ToList once you want the rest to run in memory. Return IEnumerable from a repository and you have quietly forced every later Where to filter after the whole table is loaded.
The captured-variable bug
A query captures the variable, not its value at definition time. Change the variable before you enumerate and the query sees the new value:
int min = 2;
var q = numbers.Where(n => n > min);
min = 4;
q.ToList(); // filters on n > 4, not n > 2
If you meant to freeze min at 2, either capture a local copy or call ToList at the point of definition.
How yield return becomes a state machine
An iterator block is why LINQ to Objects is lazy. The compiler rewrites a method containing yield return into a class implementing IEnumerator<T>: a numeric _state field, a _current field, and every local you use hoisted to a field so it survives across pauses. MoveNext is a switch on _state. Each yield return x sets _current = x, records where to resume, and returns true; the next MoveNext jumps back to that point. The method body never runs until the first MoveNext, which is exactly what foreach calls.
IEnumerable<int> Take2(IEnumerable<int> src) {
foreach (var x in src) { yield return x; } // pauses after each element
}
Two consequences interviewers probe. Argument validation in an iterator method does not run until enumeration, so a guard clause fires late unless you split the method (a public wrapper that validates, a private iterator that yields). And you cannot yield from inside a catch, from a lock, or from a method with ref/out parameters, because the rewrite cannot preserve that control flow across a pause.
What breaks when IQueryable cannot translate
IQueryable.Where takes an Expression<Func<T,bool>>, an inspectable tree. EF Core walks it and emits SQL. Put an opaque C# method call in the predicate and the tree contains a node the provider cannot map:
q.Where(u => IsEligible(u)).ToList(); // IsEligible has no SQL equivalent
Wrong "EF just loads the rows and runs my method in memory." Before EF Core 3.0 it did (silent client evaluation, the source of quiet full-table scans). Since 3.0 an untranslatable predicate throws InvalidOperationException at runtime. You choose the boundary explicitly: AsEnumerable() (or ToList) before the Where pulls rows first and runs the method client-side. Keep the type IQueryable and translatable to push filtering to the database; that is what separates a query that returns 10 rows from one that returns 10 million.
Dictionary and HashSet internals
Both are open-addressing-free: a prime-sized bucket array pointing into an entries array, collisions chained through a next index. Capacity is a prime (not a power of two), so the bucket index is a modulo, and resizing grows to the next prime above double the count, rehashing every entry.
The contract that makes them work: equal keys must return equal GetHashCode. The reverse is not required. The trap that costs a bug report is the mutable key. Insert an object, then mutate a field that feeds its hash, and it now hashes to a different bucket than where it lives; Contains returns false for a key that is provably in the set. Keys should be immutable, or at least stable in their hashed fields.
Default struct equality reflects over fields and boxes, which is slow; override Equals/GetHashCode, or use a record for value equality for free. List<T> is a backing array that starts at capacity 4 on first add and doubles on overflow, so Add is amortized O(1) while each grow copies everything. Preallocate with new List<T>(count) when you know the size to skip the resize chain.
beta
The interviewer part is in the works.
The diagnostic, personal maps, and AI mock interviews are being finished right now. The notes stay free either way. Leave an email and you'll get the first-cohort invite, plus a month of Pro when it opens.