← Open in the full interactive course (progress tracking, search & more)

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.

What Is It?

The Simple Explanation

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.

The Technical Definition

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.

Why Does It Exist?

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.

Big Picture

WHAT Where DOES TO A SEQUENCE
Input: [Mouse($25,), Keyboard($90,), Desk($349,), Lamp($20,), Headphones($200,), Chair($259,)]
▼ .Where(p => p.Stock > 0) ▼
Output: [Mouse($25,), Desk($349,), Lamp($20,), Headphones($200,)]
Only items where the predicate returned true survive — order is preserved, nothing is transformed.

How It Works

Where, STEP BY STEP
1. TAKE THE NEXT ITEM FROM THE SOURCE
2. RUN THE PREDICATE AGAINST IT
3. YIELD IT ONLY IF true
4. REPEAT FOR EVERY REMAINING ITEM

Simple Example

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 Mouse

Code → 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 Type

When 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 thrown

OfType<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.

Index-Aware Filtering

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 Headphones

The 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.

Real-World Example

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.99

Notice 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.

Analogy

A Sieve, Not a Sorter

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.

Under the Hood

Where IS A yield return ITERATOR
CONCEPTUALLY, Where IS IMPLEMENTED SOMETHING LIKE THIS
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(...).

Common Confusion

1. Chained 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."

2. 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.

Common Mistakes

Mistake 1 — Using 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).

Mistake 2 — Forgetting that 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(...).

When Should I Use It?

Use Where when

Consider alternatives when

Mental Model

Where = "keep only the items where this is true"
OfType<T> = "keep only the items that are actually a T"

Remember:
· Chained Where calls are AND, never OR.
· Where never mutates the source and never runs until enumerated.
· Filtering shrinks a sequence; it never reshapes or reorders the surviving items.

Key Takeaway


Check Your Understanding

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?

Show answer

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>?

Show answer

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?

Show answer

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?

Show answer

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.