Articles
C# LINQ Interview Exercises: GroupJoin, Repeated Enumeration, and Query Boundaries
Solve C# LINQ interview exercises on GroupJoin, repeated enumeration, and IQueryable boundaries, with practical examples and provider-aware guidance.
If you are preparing for C# LINQ interview questions, knowing the method names is only the start. Interviewers often want to see whether you can preserve unmatched records in a grouped join, notice when a deferred query runs more than once, and tell whether an operation will execute in memory or be translated by a database provider. Those details affect correctness and performance in ordinary application code.
This guide works through three interview-style exercises with small examples, expected reasoning, and common wrong turns. The goal is not to memorize a LINQ trick; it is to explain what each query returns and where its work happens.
Quick answers: what should you remember?
GroupJoinkeeps each element from the outer sequence and associates it with a sequence of matching inner elements. A customer with no orders still appears with an empty order group.- A query returning
IEnumerable<T>is often deferred. Creating it does not necessarily execute it; enumerating it can run the source and operators again. IQueryable<T>represents an expression for a provider to execute. Enumeration typically asks that provider to run or translate the expression, for example into SQL.- Calling
Count()and thenToList()on the same database query can mean two database executions. Materialize once only when reuse is worth the memory cost. - An operator that works with LINQ-to-Objects is not guaranteed to translate for every database provider. Check the actual provider and generated query instead of guessing.
Exercise 1: how do you keep customers with no orders?
Suppose an interviewer gives you two in-memory collections: customers and orders. Return every customer with the count and total value of their orders. A common requirement is that customers with no order must still appear with zero values.
The important word is “every.” A regular inner join returns only matching pairs, so a customer with no order disappears. GroupJoin instead produces one result per customer and supplies a group containing that customer's matching orders. The group may be empty.
var summary = customers.GroupJoin(orders,
customer => customer.Id,
order => order.CustomerId,
(customer, customerOrders) => new
{
CustomerId = customer.Id,
OrderCount = customerOrders.Count(),
Total = customerOrders.Sum(order => order.Amount)
});
For LINQ-to-Objects, an empty sequence has a count of zero and Sum() over decimal values returns zero. Be deliberate about numeric types and nullable columns in provider-backed queries: SQL aggregates and C# overloads can have different null behavior, so project or coalesce values according to the domain contract.
GroupJoin is sometimes described as a left join, but that shorthand hides its shape. It returns a grouped result, not one flattened row per matching order. That is useful when the next step is an aggregate or when each parent needs its own child collection. If the requested output is one row per order and a nullable order for unmatched customers, flatten the group with SelectMany and DefaultIfEmpty.
var rows = customers.GroupJoin(orders,
customer => customer.Id,
order => order.CustomerId,
(customer, matches) => new { customer, matches })
.SelectMany(x => x.matches.DefaultIfEmpty(),
(x, order) => new
{
CustomerId = x.customer.Id,
OrderId = order == null ? (int?)null : order.Id
});
In an interview, state what one output element represents before choosing the operator. “One row per customer with a child group” points to GroupJoin; “one row per matching pair, retaining unmatched customers” points to a left-join shape. This clarification prevents confusion about duplicates and missing records.
What if a customer has many orders?
Do not repeatedly scan all orders once for every customer. With ordinary LINQ-to-Objects, GroupJoin builds a lookup for the inner sequence and probes it for each outer key. That is generally the right simple approach, with work proportional to building and querying the lookup rather than a nested full scan. If you hand-write customers.Select(c => orders.Where(...)), each customer's inner filter may walk the entire orders sequence.
Also ask about key equality. The default comparer is usually right for integer IDs. For strings, decide whether case, culture, and normalization rules should affect matching; use an explicit IEqualityComparer<TKey> when required. Database collation may not match .NET string comparison, so a query that moves from memory to SQL can change the equality boundary.
Exercise 2: why does this query enumerate twice?
Consider a method that accepts any IEnumerable<Order> and needs to report whether there are orders and then return them:
IEnumerable<Order> FilteredOrders(IEnumerable<Order> source) =>
source.Where(order => order.IsOpen);
var open = FilteredOrders(source);
if (open.Any())
{
foreach (var order in open)
Process(order);
}
The call to FilteredOrders creates a query description. It usually does not cache results. Any() asks the sequence for an element; the later foreach enumerates it again. A list-backed source may simply run the predicate again. A database query may send another command. A file, network stream, or custom iterator may be expensive, stateful, or impossible to enumerate twice.
The interview question is not “is deferred execution bad?” It is “what does this source guarantee, and how many times will this code enumerate it?” Deferral is useful: you can compose filters before work begins, and a query can observe current source values when it executes. Re-enumeration is a separate decision that must fit the source.
If the filtered data is small enough and you need to inspect it more than once, buffer it intentionally:
var open = FilteredOrders(source).ToList();
if (open.Count > 0)
{
foreach (var order in open)
Process(order);
}
Now the filter runs once and later reads reuse the list. The trade-off is memory proportional to the buffered results. If you only need to know whether an item exists before processing, avoid preliminary Any() and process in one pass. Do not eagerly call ToList() everywhere: it can increase memory use, delay the first result, and pull more data than a streaming consumer needs.
What is the trap with Count and ToList?
This is a frequent practical bug: var count = query.Count(); var page = query.Take(20).ToList(); For an IQueryable<T> database source, Count() commonly executes a count query, and ToList() executes a second query for the page. That can be appropriate when a UI genuinely needs both total count and page contents. Treat them as two operations and consider concurrent changes between them.
If the source is an in-memory iterator, Count() can walk the whole sequence and the later ToList() walks it again. Do not “fix” this by loading an unbounded database table into memory just to count it. Choose deliberately: let the database count and fetch the page separately, or materialize a bounded result and derive both values. Data volume, consistency, latency, and memory budget determine the right choice.
Exercise 3: why can a LINQ query work in memory but fail on EF Core?
LINQ query syntax looks consistent across lists and database contexts, but the execution model changes with the source type. With IEnumerable<T>, standard operators use delegates and run in .NET as values are enumerated. With IQueryable<T>, operators build expression trees. The provider decides how much of that expression it can translate to its query language.
Valid C# does not guarantee translatable SQL. A method call that exists only in your application can run after loading objects but may be impossible for a relational provider to express. Translation support also varies between providers and versions. Some GroupJoin result shapes are difficult for a relational database to represent because the result contains a collection for each outer row; aggregate or flattened shapes may translate where a raw grouped collection does not.
When a query fails at runtime, diagnose the boundary in this order:
- Confirm whether the source is
IEnumerable<T>orIQueryable<T>at the unsupported operation. - Find the exact method or projection the provider cannot translate.
- Decide whether it is safe to move that part to the client, and how many rows must be transferred first.
- Keep server-translatable filters and projections before client-side work.
- Inspect generated SQL or query logs and test against the production provider.
AsEnumerable() changes subsequent operators to LINQ-to-Objects; it does not make the query free. Operators before it can still run on the provider, while the later part runs on the client. ToList() also materializes results, but buffers them immediately. The distinction matters when a sequence is large or should be streamed.
var eligible = db.Orders
.Where(order => order.CreatedAt >= cutoff)
.Select(order => new { order.Id, order.CustomerId, order.Amount })
.AsEnumerable()
.Where(order => CustomBusinessRule(order.Amount));
This keeps the date filter and narrow projection on the provider side, then runs the custom method over fewer values in memory. It is suitable only if the transfer volume is acceptable and the rule cannot be expressed in the database. If that rule filters a large dataset, consider a translatable expression or a persisted representation.A worked GroupJoin trace
Make the output shape concrete with three customers: IDs 11, 12, and 13. Suppose orders belong to customers 11 and 13, while customer 12 has none. GroupJoin produces three outer results: customer 11 with its matching order group, customer 12 with an empty group, and customer 13 with its matching group. Aggregating each group returns counts of 1, 0, and 1 without dropping customer 12.
If you flatten with SelectMany and DefaultIfEmpty, the result has one row for each matched order plus one row for customer 12 with a null order. That is why interviewers ask what one output row means: a grouped parent result and a flattened relational result have different row counts and different null handling. State the desired shape before writing either query.
Common LINQ interview mistakes
- Using
Joinwhen unmatched outer items must remain. An inner join drops them; grouped or flattened left-join semantics are needed. - Calling
ToList()as a reflex. Materialization can solve repeated enumeration but can also load too much data and increase latency. - Assuming every sequence is repeatable. An iterator over a stream can be one-shot; an EF query can issue another command on each enumeration.
- Treating
IQueryable<T>as a collection. It is a provider-backed expression until execution, and provider translation defines what is supported. - Switching to client evaluation too early. Calling
AsEnumerable()before a selective filter can move a large workload into the application. - Ignoring output shape. A grouped result and a flattened left join are different contracts.
How should you explain your answer in an interview?
Narrate four things: output shape, source type, expected execution count, and where work runs. For example: “I need one result per customer, including customers with no orders, so I’ll use GroupJoin and aggregate the group. Since this is IQueryable, I’ll verify the provider translates that projection. I’ll avoid enumerating it once for a count and again for output unless two database operations are intentional.”
That connects semantics to runtime cost and makes assumptions visible. If the interviewer changes the source from a list to a database query, you already know which part of your reasoning must be revisited.
Practice checklist
- Test an outer record with no matches, one match, and multiple matches.
- Count how many times a predicate or iterator executes when you call
Any(), then loop. - Compare
IEnumerable<T>andIQueryable<T>behavior for the same-looking query. - Move
AsEnumerable()after a selective provider-side filter and explain why. - Check generated queries for the project's exact EF Core and database versions.
Sources and Further Reading
- Microsoft documentation for
Enumerable.GroupJoin - Introduction to LINQ queries
- How EF Core queries are executed
- EF Core complex query operators and provider translation
- EF Core client versus server evaluation
Research checked September 24, 2026. Translation details are provider- and version-dependent; verify behavior against the project's actual database provider.