Where is the workhorse of LINQ — keep only the items that answer "yes" to a question you ask about each one.
Almost every query starts the same way: "give me only the ones that..." Only the in-stock products. Only the orders from last month. Only the employees in Engineering. That single, recurring need — narrowing a collection down to items matching a condition — is what LINQ's filtering operators exist for, and Where is the one you'll reach for constantly.
This lesson covers Where, combining multiple conditions, OfType<T> for filtering by runtime type, and index-aware filtering.
Where walks through a sequence and keeps only the items for which a condition you supply comes back true. It's the LINQ equivalent of an if statement inside a loop — except you write the condition once, and Where handles the looping.
Where is an extension method on IEnumerable<T> with this shape:
public static IEnumerable<TSource> Where<TSource>(
this IEnumerable<TSource> source,
Func<TSource, bool> predicate)It takes a Func<TSource, bool> — a delegate that receives one item and returns true or false — exactly the kind of predicate you built by hand in Intermediate Part III. Where returns a new IEnumerable<TSource> containing only the items where the predicate returned true. There's also an overload that passes the item's index as a second parameter, covered later in this lesson.
Before LINQ, filtering meant an accumulator list and a manual if:
// Manual loop
var inStock = new List<Product>();
foreach (var p in products)
{
if (p.Stock > 0)
inStock.Add(p);
}Where replaces that pattern entirely — no accumulator, no manual Add, just the condition itself:
// Where
var inStock = products.Where(p => p.Stock > 0);The value compounds once you start chaining: filtering is almost always the first step in a larger query — narrow the data down before sorting, projecting, or grouping it, which is exactly what the rest of this module builds on.
true survive — order is preserved, nothing is transformed.true or false.true, the item is produced to whoever is consuming the sequence. If false, it's skipped entirely.public record Product(int Id, string Name, string Category, decimal Price, int Stock);
List<Product> products =
[
new(1, "Wireless Mouse", "Electronics", 24.99m, 120),
new(2, "Mechanical Keyboard", "Electronics", 89.99m, 0),
new(3, "Standing Desk", "Furniture", 349.00m, 15),
new(4, "Desk Lamp", "Furniture", 19.50m, 60),
new(5, "Noise-Cancelling Headphones", "Electronics", 199.99m, 8),
new(6, "Office Chair", "Furniture", 259.00m, 0),
];
// Single condition
var inStock = products.Where(p => p.Stock > 0);
// Multiple conditions — just use && inside the lambda
var affordableElectronics = products.Where(
p => p.Category == "Electronics" && p.Price < 100);
// Multiple conditions — or chain two Where calls (equivalent, sometimes clearer)
var affordableElectronicsChained = products
.Where(p => p.Category == "Electronics")
.Where(p => p.Price < 100);
foreach (var p in affordableElectronics)
Console.WriteLine(p.Name);
// Wireless MouseCode → Meaning → Result: A single lambda with && and two chained Where calls produce identical results — LINQ evaluates them item by item either way. Chaining can read more clearly when each condition represents a distinct, nameable business rule.
OfType<T> — Filtering by Runtime TypeWhen a collection holds mixed types — common with object collections, heterogeneous results, or a base-type list holding several derived types — OfType<T> filters down to only the items that are (or can be safely cast to) a specific type, silently skipping ones that aren't:
object[] mixedData = [42, "hello", 3.14, "world", 100, null];
IEnumerable<string> onlyStrings = mixedData.OfType<string>();
foreach (var s in onlyStrings)
Console.WriteLine(s);
// hello
// world
IEnumerable<int> onlyInts = mixedData.OfType<int>();
// 42, 100 — note: null is skipped, not thrownOfType<T> is effectively Where(x => x is T).Cast<T>() in one step — it filters and narrows the type at the same time, which is why it belongs in this lesson rather than the projection lesson.
Where has an overload whose predicate takes the item's zero-based index as a second parameter — useful when the position itself matters, not just the value:
// Keep only products at even positions in the source list
var everyOther = products.Where((p, index) => index % 2 == 0);
foreach (var p in everyOther)
Console.WriteLine(p.Name);
// Wireless Mouse, Standing Desk, Noise-Cancelling HeadphonesThe index reflects each item's position in the sequence as it arrives at Where — if you've already chained an earlier Where or OrderBy, the index counts positions in that already-transformed sequence, not the original source.
A product search feature needs to filter a catalog by several optional criteria at once — category, price range, and in-stock status — the kind of layered filter every e-commerce site's search bar implements:
List<Product> SearchProducts(
List<Product> catalog, string? category, decimal? maxPrice, bool inStockOnly)
{
var query = catalog.Where(p =>
(category == null || p.Category == category) &&
(maxPrice == null || p.Price <= maxPrice) &&
(!inStockOnly || p.Stock > 0));
return query.ToList();
}
var results = SearchProducts(products, category: "Electronics", maxPrice: 100m, inStockOnly: true);
foreach (var p in results)
Console.WriteLine($"{p.Name} — ${p.Price}");
// Wireless Mouse — $24.99Notice how each optional filter is expressed as "skip this check if the criterion wasn't provided" — a single Where call cleanly handles an arbitrary combination of optional search filters, which is exactly the shape real search endpoints need.
Think of Where as a kitchen sieve, not a sorting machine. You pour ingredients through it, and it lets through only the pieces small enough to pass — nothing is rearranged, nothing is transformed, some pieces simply don't make it through. That's exactly what Where does: the surviving items keep their original shape and relative order; some items just don't make it into the output.
public static IEnumerable<T> Where<T>(
this IEnumerable<T> source, Func<T, bool> predicate)
{
foreach (var item in source)
{
if (predicate(item))
yield return item;
}
}
Because it's built with yield return, calling .Where(...) does not immediately loop through anything — it hands back an object that will do the looping only once you enumerate it (with foreach, ToList(), and so on). This is deferred execution, and it applies to almost every LINQ operator — its own full lesson comes later in this module, but keep it in mind: nothing has actually run yet just because you wrote .Where(...).
Where calls vs one Where with && — both are "AND," not "OR"Two chained .Where(a).Where(b) calls behave like a && b — an item must survive both filters. If you actually want "either condition," you need || inside one predicate: Where(p => a || b). Chaining two separate Where calls can never express "or."
OfType<T> vs Cast<T>OfType<T> silently skips items that aren't of type T. Cast<T> (covered in Projection) attempts to cast every item and throws an InvalidCastException the moment one doesn't fit. Reach for OfType<T> when a mismatch is expected and should be filtered out; reach for Cast<T> when a mismatch is a bug you want to know about immediately.
Where when you only need one item products.Where(p => p.Id == 3).First() filters the entire sequence just to grab one match. Use First(predicate) or FirstOrDefault(predicate) directly — they stop at the first match instead of filtering everything (a topic revisited in Aggregation).
Where doesn't mutate the original collection Assuming products.Where(p => p.Stock > 0); (without assigning the result) somehow filtered products itself. Where always returns a brand-new sequence; the original collection is untouched. Capture the result: var filtered = products.Where(...).
OfType<T>.First/FirstOrDefault/Single with a predicate instead.Any(predicate), not Where(...).Any().Where calls are AND, never OR.Where never mutates the source and never runs until enumerated.Where keeps only the items matching a predicate, preserving their original shape and order.&&/|| inside one predicate, or chain multiple Where calls for AND logic.OfType<T> filters by runtime type, silently skipping non-matching items (unlike Cast<T>, which throws).Where lets a predicate use each item's position.Where is built with yield return and doesn't run until enumerated — a preview of deferred execution, covered fully later in this module.You've learned how to filter sequences with Where and OfType<T>. Let's check your understanding.
1. What does products.Where(p => p.Stock > 0).Where(p => p.Price < 50) select?
Correct: B
Why B is correct: Chained Where calls each further narrow the sequence — an item must survive both filters to appear in the final result, which is logical AND.
Why A is incorrect: Chaining Where calls can never express OR — that requires || inside a single predicate.
Why C is incorrect: This describes negated conditions, which is not what the code does at all.
Why D is incorrect: Where can be chained any number of times — each call returns a new IEnumerable<T> that the next call can filter further.
Reinforcement: Chained Where calls always combine as AND — memorize this, since it's a very common source of subtle bugs when a developer actually wanted OR.
2. What is the key difference between OfType<T> and Cast<T>?
Correct: B
Why B is correct: As covered in Common Confusion, OfType<T> filters out non-matching items; Cast<T> assumes every item already is (or safely converts to) T and throws InvalidCastException otherwise.
Why A is incorrect: Their error-handling behavior is meaningfully different, which is exactly why choosing the right one matters.
Why C is incorrect: This reverses the actual behavior of the two methods.
Why D is incorrect: Both work on any IEnumerable source, not just arrays.
Reinforcement: Use OfType<T> when mismatches are expected and should be quietly excluded; use Cast<T> when a mismatch represents a bug you want surfaced immediately.
3. Given var q = products.Where(p => p.Stock > 0); followed by no further code — has the filtering actually happened at that point?
Correct: B
Why B is correct: As shown in Under the Hood, Where is implemented as a yield return iterator — calling it just builds an object describing the work; nothing runs until something enumerates the result.
Why A is incorrect: This is exactly the misconception deferred execution corrects — the call itself does no looping.
Why C is incorrect: Deferred execution applies to Where regardless of the concrete source type.
Why D is incorrect: This is entirely a runtime behavior, not something the compiler resolves ahead of time.
Reinforcement: This is your first taste of deferred execution — a concept with its own full lesson coming up later in this module.
4. A list contains object values of mixed types: integers, strings, and nulls. Which method keeps only the strings, safely ignoring everything else including nulls?
Correct: C
Why C is correct: OfType<string>() is designed exactly for this: it keeps only the items that actually are string, quietly skipping ints, nulls, and anything else.
Why A is incorrect: Cast<string>() would throw an InvalidCastException the moment it reached the first non-string item (like an int).
Why B is incorrect: This only removes nulls — the integers would still be included, which isn't what's being asked for.
Why D is incorrect: This would produce a sequence of the same length as the original, with null in place of every non-string item — not a filtered-down sequence of just the strings.
Reinforcement: OfType<T> is the correct, idiomatic tool whenever you need to filter and narrow by runtime type in one step.
You now know how to filter sequences with confidence. Next up: reshaping the items that survive, with Select and SelectMany.
dotnetmadeeasy.com — Learn C# and .NET, the right way.