Filtering with Where
By the end of this lesson
Select the items that satisfy a condition.
Where keeps the items that pass a test and drops the rest. The test is a function that takes one item and returns true or false. A function shaped like that has a name worth knowing: a predicate.
Filtering is the operator you will reach for most, and it is nearly impossible to get wrong. The part of this lesson that matters is the second half, where you pull one item out of a sequence. Four methods do that, they behave differently when nothing matches and when several match, and picking the wrong one is one of the most common sources of runtime exceptions in .NET code.
IEnumerable<Employee> highEarners = employees
.Where(e => e.AnnualSalary > 1_000_000m);
// Asha Mehta, Ravi Iyer, Neha Kulkarni, Tarun Bansal, Meera Nair (5 of 8)
IEnumerable<Employee> supportTeam = employees
.Where(e => e.Department == "Support");
// Imran Sheikh, Divya Rao
// Two conditions, one predicate.
IEnumerable<Employee> seniorEngineers = employees
.Where(e => e.Department == "Engineering" && e.AnnualSalary > 1_500_000m);
// Asha Mehta, Neha Kulkarni
// The same query, written as two filters. Identical result.
IEnumerable<Employee> alsoSeniorEngineers = employees
.Where(e => e.Department == "Engineering")
.Where(e => e.AnnualSalary > 1_500_000m);
// Long-serving staff.
IEnumerable<Employee> joinedBefore2020 = employees
.Where(e => e.JoiningDate < new DateOnly(2020, 1, 1));
// Asha Mehta, Neha Kulkarni, Tarun Bansal, Meera Nair- The lambda receives one Employee and returns a bool. Where calls it once per item and keeps the items that returned true.
- Items come out in the order they went in. Where never reorders anything.
- && inside one predicate and two chained Where calls produce the same items. Chaining reads better when the conditions are unrelated to each other; one predicate reads better when they are two halves of a single idea.
- Nothing in this block has examined a single employee yet. Each line builds a description of a filter, and the work happens when you enumerate the result — in a foreach, or by calling something like ToList or Count. The last lesson in this module is about the consequences of that.
- There is a second overload, Where((item, index) => ...), which also gives you the position. It is occasionally useful and often a sign that the data should have carried a number of its own.
Pulling out one item
Four methods return a single item. The two families differ in what they consider an error, and the OrDefault suffix changes what happens when nothing matches.
| First / FirstOrDefault | Single / SingleOrDefault | |
|---|---|---|
| Several items match | Returns the first one in sequence order | Throws InvalidOperationException |
| Nothing matches | First throws; FirstOrDefault returns the default value | Single throws; SingleOrDefault returns the default value |
| How much is read | Stops at the first match | Keeps reading to prove there is no second match |
| What it says about the data | Any match will do; there may be more | Exactly one should exist; more than one is a bug |
| Typical use | The top row of an ordered sequence, or a best-effort lookup | A lookup by something unique, such as an id or an email address |
// Two people work in Sales, and Tarun comes first in the list.
Employee firstInSales = employees.First(e => e.Department == "Sales");
Console.WriteLine(firstInSales.Name); // Tarun Bansal
// Nobody works in Legal.
Employee? nobody = employees.FirstOrDefault(e => e.Department == "Legal");
Console.WriteLine(nobody is null); // True
// Exactly one person works in Finance, which is what Single asserts.
Employee onlyInFinance = employees.Single(e => e.Department == "Finance");
Console.WriteLine(onlyInFinance.Name); // Sanjay Gupta
// Both of these throw InvalidOperationException:
// employees.First(e => e.Department == "Legal"); no match at all
// employees.Single(e => e.Department == "Sales"); two matches
// A default you choose, instead of null (.NET 6 and later).
Employee fallback = new Employee("Vacant", "Legal", "Unfilled", 0m, new DateOnly(2024, 1, 1));
Employee legalLead = employees.FirstOrDefault(e => e.Department == "Legal", fallback);
Console.WriteLine(legalLead.Name); // Vacant- First and Single both accept a predicate directly, so Where is optional. employees.Where(p).First() and employees.First(p) do the same thing.
- "First" means first in the order of the sequence, which for a List is insertion order. If you want the highest paid or the most recent, order the sequence first — First on an unordered sequence answers a question you did not ask.
- FirstOrDefault returns default(T): null for a class or a record, 0 for an int, and the all-zeros value for a struct such as DateOnly. For a value type that is a real trap, because 0 looks like an answer.
- The Employee? on the FirstOrDefault line is the nullable annotation. With nullable reference types switched on, the compiler warns you when you use nobody without checking it, which is precisely the check people forget.
- Single reads past the match it found. It has to: it cannot report "more than one" without looking. That makes it slightly more work than First and considerably more informative when your assumption about uniqueness is wrong.
Summary
- Where keeps items whose predicate returns true, in their original order, without touching the source
- First returns the first match and throws when there are none; FirstOrDefault returns the default value instead
- Single asserts that exactly one item matches and throws when two do
- FirstOrDefault gives null for reference types and 0 for numbers, so the result needs checking before use
- Order the sequence before asking for the first item, or "first" means whatever order the source happened to be in
Practice
Attempt each one before opening the solution. Getting it wrong first is how the idea sticks.
Try it yourself
Try it yourself
Find the most recent joiner in the Support department and print their name and joining date. If Support has nobody in it, print a message saying so instead.
Then change the department name to "Legal" and run it again. Your code should print the message, not throw.
Show solution
Ordering comes before the single-item call. Without OrderByDescending, FirstOrDefault returns whoever happens to sit earliest in the list, which is Imran only by accident of insertion order.
FirstOrDefault plus a null check is what makes the Legal case a message instead of an exception. First would have been shorter and would have crashed, and it would have crashed in production rather than in testing, because in testing the department always has people in it.
One more option worth knowing: MaxBy(e => e.JoiningDate) returns the item with the highest key directly, without an ordering step. The aggregation lesson covers it.
Employee? newest = employees
.Where(e => e.Department == "Support")
.OrderByDescending(e => e.JoiningDate)
.FirstOrDefault();
if (newest is null)
{
Console.WriteLine("Nobody is recorded in that department.");
}
else
{
Console.WriteLine($"{newest.Name} joined on {newest.JoiningDate}");
}
// Imran Sheikh joined on 14/02/2022 (date format follows your machine's culture)Think about it
Think about it
A payroll screen loads an employee by their email address, using Single. A data import later creates a second record with the same address.
What does the screen do now, and would FirstOrDefault have been an improvement?
Show solution
The screen throws. Single found two matches and refused to choose, so nobody can open that employee until the duplicate is resolved. The failure is loud and points at real corruption in the data.
FirstOrDefault would keep the screen working, and that is not clearly better. It would silently pick whichever record came back first, so pay changes could land on the wrong one of the two records, and the duplicate would stay undiscovered until the figures disagreed.
There is no universally right answer, which is the point. Single says a duplicate is an emergency, First says it is tolerable. Choose based on what a duplicate costs. If it costs money, prefer the loud failure and add a unique constraint in the database so the duplicate cannot be created in the first place.
Knowledge check
Nothing is recorded and there is no score. The explanation appears either way.
Saved in this browser only.