Filtering and Projection
Where and Select — the two most important LINQ operators, and the mental model that makes every other operator easy
If you only learned two LINQ operators, you'd cover 80% of every data-processing task you'll ever do:
Where— keep the elements that satisfy a predicate.Select— transform each element into something else.
These two are the filter and map primitives that almost every
functional language has. In modern C#, they're extension methods on
IEnumerable<T>. This page walks through them carefully, because
understanding them shapes how you think about everything else.
Mental model
Picture a sequence as data flowing through a pipe. Each operator is a stage in the pipe.
Where is a sieve: things either pass through or don't. Select is
a converter: each thing that comes in becomes something else on the
way out. The shape of the sequence may change (different element
type), but the order is preserved.
Where — filtering
Where takes a predicate (Func<T, bool>) and returns a sequence
containing only the elements for which the predicate is true.
Important: an "empty result" is an empty sequence, not null. You
can iterate it; you just get zero elements.
There is also a variant of Where that gives you the index of
each element:
The two-argument lambda (name, index) => ... opts into the indexed
overload. Useful, but use it sparingly — most code is clearer
without indices.
Select — projecting
Select takes a selector (Func<TSource, TResult>) and produces a
new sequence with the function applied to each element. This is
sometimes called map in other languages.
Note that the result type follows the lambda's return type. Project ints into strings, strings into lengths, records into anonymous objects — anything you want.
Anonymous and tuple projections
A common pattern: project into a small ad-hoc shape mid-pipeline.
Use anonymous types when the shape is consumed inside the same expression. Use records when the shape is returned across a method boundary or used in many places.
Composing Where and Select
The real power shows up when you compose. Every pipeline you'll ever write is some combination of filter, then transform.
Two Where clauses chain naturally — they don't have to be combined
into one giant predicate. Each one expresses a single intent; the
pipeline as a whole reads top to bottom.
Step-by-step walkthrough
Let's trace what happens for a single element flowing through the pipeline above:
For an order with Product = "Gizmo", the first Where rejects it
and neither subsequent operator runs. This is part of what makes
LINQ pipelines efficient — the work for "dropped" elements is
skipped entirely.
Select is not a loop
A common beginner habit is to write code like:
// Don't do this.
people.Select(p => { Console.WriteLine(p.Name); return p; }).ToList();Select is for transforming, not for doing. If you want to
execute a side effect for each element, use a plain foreach. The
LINQ ecosystem leans into the idea that operators are pure and
side-effect-free.
Don't use Select for side effects
Using Select for side effects can produce surprising results because
LINQ is lazy: nothing happens until the sequence is enumerated. Pair
that with a Select whose lambda has side effects, and you'll get
weird "why did it run twice?" bugs.
A multi-file challenge
In PeopleFilter.cs, implement
AdultNamesUpper(IEnumerable<Person> people) which returns the
uppercased Name of every person whose Age >= 18.
Use Where and Select. Do not use foreach.
Program.cs will call your method and print the result one per line.
Given var result = nums.Where(n => n > 10).Select(n => n * 2);, which statement is correct?
The pipeline runs immediately and result is a List<int>.
The Select lambda runs for every element of nums, regardless of the filter.
result is a deferred IEnumerable<int> — neither Where nor Select runs until something enumerates it (e.g. foreach, ToList, Sum).
Where and Select can only be used on List<T> and arrays.