Where, Select and SelectMany
Where keeps some elements, Select changes each one, and SelectMany flattens a sequence of sequences into one flat sequence.
source.Where(x => predicate).Select(x => projection).SelectMany(x => innerSequence)Three operators cover most real queries. Where decides which elements survive. Select decides what each surviving element becomes. SelectMany handles the case where each element holds a collection of its own and you want all of those collections joined into one.
Where and Select
Where takes a predicate: a lambda that returns bool. Elements for which it returns true are passed along, in their original order. The element type does not change, so filtering a sequence of orders gives you a sequence of orders.
Select takes a selector: a lambda that returns anything. It is one in, one out, so the count never changes but the element type usually does. Select(b => b.Title) turns a sequence of books into a sequence of strings. That operation is called a projection.
SelectMany
If each book has a string[] Tags, then Select(b => b.Tags) gives you a sequence of arrays, and reading it needs two nested loops. SelectMany(b => b.Tags) gives you a single flat sequence of strings instead. Two nested foreach loops over a Select result is almost always a missed SelectMany.
Shorthands
A lambda body can be a single expression, b => b.Title, or a block, b => { return b.Title; }. They mean the same thing. In query syntax, a second from clause compiles to a SelectMany call, so from b in books from t in b.Tags select t and books.SelectMany(b => b.Tags) are the same query.
Example
Book[] library =
[
new Book("Dune", 1965, ["scifi", "classic"]),
new Book("Emma", 1815, ["classic"]),
new Book("Neuromancer", 1984, ["scifi", "cyberpunk"])
];
// Where keeps the elements that pass the test. Book in, Book out.
IEnumerable<Book> modern = library.Where(b => b.Year > 1900);
// Select turns each element into something else: one in, one out. Book in,
// string out, so the sequence changes type but not length.
IEnumerable<string> titles = modern.Select(b => b.Title.ToUpperInvariant());
// SelectMany flattens. Each book gives a string[]; SelectMany joins them
// all into one sequence of strings. Select here would give a sequence of
// arrays, needing two nested loops to read.
IEnumerable<string> allTags = library.SelectMany(b => b.Tags);
Console.WriteLine(string.Join(", ", titles));
Console.WriteLine(string.Join(", ", allTags));
// Distinct drops repeats; Order sorts, so the printed line is predictable.
Console.WriteLine(string.Join(", ", allTags.Distinct().Order()));
// Output:
// DUNE, NEUROMANCER
// scifi, classic, classic, scifi, cyberpunk
// classic, cyberpunk, scifi
record Book(string Title, int Year, string[] Tags);When to use it
- Turn a list of orders, each holding its own line items, into one flat list of line items for a CSV export.
- Keep only the records whose status is active, then project them down to the two fields a JSON response needs.
- Collect every tag used across a collection of posts, with repeats removed.
More examples
The index overloads of Select and Where
Reach for these when the position matters: numbering a printed list, keeping every other row, or tagging an item with where it came from. Query syntax has no equivalent clause, so a query that needs the index has to be written in method syntax. Note that the index is the position in the sequence the operator received, not in the original source, so an index taken after a Where counts the survivors.
string[] steps = ["mix", "bake", "cool", "ice"];
// Select has a second overload whose lambda takes two arguments: the
// element and its zero-based position in the source sequence.
IEnumerable<string> numbered = steps.Select((step, index) => $"{index + 1}. {step}");
// Where has the matching overload. This keeps the even-numbered positions.
IEnumerable<string> evenPositions = steps.Where((step, index) => index % 2 == 0);
foreach (string line in numbered)
{
Console.WriteLine(line);
}
Console.WriteLine(string.Join(", ", evenPositions));
// Output:
// 1. mix
// 2. bake
// 3. cool
// 4. ice
// mix, coolSelectMany keeping the outer element in scope
The one-argument SelectMany loses track of which outer element produced each inner one. The three-argument form fixes that, which is what you need to turn a parent-and-children shape into flat report rows. This is the single most common reason to prefer query syntax: two from clauses say what is happening more plainly than two lambdas.
PurchaseOrder[] orders =
[
new PurchaseOrder("A-1", ["pen", "pad"]),
new PurchaseOrder("A-2", ["mug"])
];
// The three-argument SelectMany takes a second lambda that receives both
// the outer element and one inner element, so the result can mention both.
IEnumerable<string> flat = orders.SelectMany(
o => o.Lines,
(o, line) => $"{o.Id}:{line}");
// Query syntax: a second from clause compiles to exactly that call. This is
// the shorthand for the method chain above, and it reads better.
IEnumerable<string> flatQuery =
from o in orders
from line in o.Lines
select $"{o.Id}:{line}";
Console.WriteLine(string.Join(" ", flat));
Console.WriteLine(string.Join(" ", flatQuery));
// Output:
// A-1:pen A-1:pad A-2:mug
// A-1:pen A-1:pad A-2:mug
record PurchaseOrder(string Id, string[] Lines);
Discussion