Learning outcome
By the end of this lesson, you should be able to look at an IEnumerable<Order> and decide whether you want a projected sequence of child collections or one flattened sequence of child elements. In interview terms, the key question is: do I want “one result per outer item” or “one result per inner item across all outer items”? That is the difference between Select and SelectMany.
Intuition
Imagine a set of customer orders. Each order contains zero or more order lines. If you use Select, you keep the order structure and transform each order into something else, such as a summary DTO or a list of line descriptions. If you use SelectMany, you remove one level of nesting and produce one sequence that contains all matching lines from all orders.
That means the result shapes are different:
SelectturnsOrder -> SomethingSelectManyturnsOrder -> Many Thingsand then flattens them into a single sequence
A practical mental model is:
Selectanswers: “What is each order mapped to?”SelectManyanswers: “What are all the lines from all orders?”
This matters when you want totals, search across all lines, or join line-level data to another sequence. It also matters when an order has no lines. With Select, that empty list stays attached to the order. With SelectMany, that order contributes nothing to the flattened output.
Deep dive
Select applies a projection once per source element. If the projection returns a collection, the result is a sequence of collections. That is not flattening; it is nesting one layer deeper.
SelectMany also applies a projection, but the projected values must be sequences, and LINQ concatenates those sequences into one result sequence. This is the flattening step. A classic interview trap is assuming SelectMany removes duplicates. It does not. It only changes shape. If the same SKU appears in multiple lines, both lines remain unless you explicitly use Distinct, grouping, or a set-based operation.
Projecting each order to its Lines collection with Select gives you a sequence of sequences. Using SelectMany to flatten those same collections gives you one flat sequence.
Empty inner sequences are important. Suppose one order has no lines. With Select, that order still appears in the outer result, paired with an empty inner collection. With SelectMany, there is simply no element contributed by that order. This difference is often the deciding factor in production code.
Another subtle point is that SelectMany can preserve access to the outer element if you need it. In real code, you often use the overload that supplies both the outer item and each inner item, which makes it easy to build a flattened record containing order metadata and line data.
Failure modes
Common mistakes include:
- Using
Selectwhen you intended a flat list, then later wondering why you have nested collections - Using
SelectManywhen you still need the order boundary, such as when building grouped summaries - Assuming flattening removes duplicates; it does not
- Forgetting that orders with no lines disappear from a flattened line sequence
- Calling
SelectManytoo early and losing the parent order context needed for later logic
A good interview answer usually names the shape of the output before naming the operator.
Interview drill
Try answering these out loud:
- If I need a list of
OrderSummaryobjects, which operator do I use? Why? - If I need every line SKU across all orders in one sequence, which operator do I use? Why?
- What happens to an order with no lines under each operator?
- Does
SelectManydeduplicate lines? - How would you keep the order ID while flattening order lines?
A strong answer mentions projection vs flattening, output shape, and empty inner collections.
Revision checklist
Selectkeeps one output per source itemSelectManyflattens nested sequences into one output sequenceSelectis ideal for summaries and nested structuresSelectManyis ideal for one flat stream of inner items- Empty inner sequences remain nested with
Selectand disappear from the flattened output withSelectMany - Flattening does not imply deduplication
Production code
The following example uses a small order domain to show both operators side by side. Notice how the projected result preserves the order boundary, while the flattened result exposes each line as a separate item. The example also includes an order with no lines so you can see the behavior clearly.
Code walkthrough
The projected query uses Select and returns one anonymous object per order. Each object contains the order identity plus a nested list of line summaries. That is the correct choice when the caller still cares about which lines belong to which order.
The flattened query uses SelectMany and returns one anonymous object per line. Each flattened item includes the parent order ID and customer so that the parent context is not lost. This is a common pattern when you need a report or a search result across all line items.
The Linus order has no lines. In the Select result, it still appears with an empty list. In the SelectMany result, it contributes no rows at all. That is not an error; it is the expected effect of flattening an empty inner sequence.
When interviewers ask about Select versus SelectMany, start by describing the shape you want. If you can clearly state “nested collection” versus “flat collection,” the choice usually follows naturally.
Output: Order projection versus flattening
SELECT result shape:
Order 1001 for Ada has 2 line(s).
[SKU-1 x2, SKU-2 x1]
Order 1002 for Grace has 2 line(s).
[SKU-2 x4, SKU-3 x1]
Order 1003 for Linus has 0 line(s).
[]
SELECTMANY result shape:
Order 1001 / Ada / SKU-1 / 2 / 25.00
Order 1001 / Ada / SKU-2 / 1 / 5.00
Order 1002 / Grace / SKU-2 / 4 / 20.00
Order 1002 / Grace / SKU-3 / 1 / 20.00
Total flattened lines: 4
Orders with no lines still appear in SELECT: 1
Executable code examples
Order projection versus flattening
Program.cs
using System;
using System.Collections.Generic;
using System.Linq;
var orders = new List<Order>
{
new(1001, "Ada")
{
Lines =
[
new("SKU-1", 2, 12.50m),
new("SKU-2", 1, 5.00m)
]
},
new(1002, "Grace")
{
Lines =
[
new("SKU-2", 4, 5.00m),
new("SKU-3", 1, 20.00m)
]
},
new(1003, "Linus")
{
Lines = []
}
};
var projected = orders.Select(order => new
{
order.Id,
order.Customer,
LineSummaries = order.Lines.Select(line => $"{line.Sku} x{line.Quantity}").ToList()
});
var flattened = orders.SelectMany(order => order.Lines.Select(line => new
{
OrderId = order.Id,
order.Customer,
line.Sku,
line.Quantity,
line.UnitPrice,
LineTotal = line.Quantity * line.UnitPrice
}));
Console.WriteLine("SELECT result shape:");
foreach (var item in projected)
{
Console.WriteLine($"Order {item.Id} for {item.Customer} has {item.LineSummaries.Count} line(s).");
Console.WriteLine($" [{string.Join(", ", item.LineSummaries)}]");
}
Console.WriteLine();
Console.WriteLine("SELECTMANY result shape:");
foreach (var item in flattened)
{
Console.WriteLine($"Order {item.OrderId} / {item.Customer} / {item.Sku} / {item.Quantity} / {item.LineTotal:0.00}");
}
Console.WriteLine();
Console.WriteLine($"Total flattened lines: {flattened.Count()}");
Console.WriteLine($"Orders with no lines still appear in SELECT: {projected.Count(item => item.LineSummaries.Count == 0)}");
public sealed record Order(int Id, string Customer)
{
public List<OrderLine> Lines { get; init; } = [];
}
public sealed record OrderLine(string Sku, int Quantity, decimal UnitPrice);