1. Introduction
Filtering means picking out only those elements from a collection that match a certain condition. Imagine: your database has 10,000 products, but your manager needs "just" 12 that fit some new tricky criteria. Writing a manual loop and a bunch of checks every time is no fun, and the code gets unreadable. LINQ takes care of all that hassle for you.
In real apps, filtering is the most common data operation: we show users only the records they care about, "clean up" collections from unwanted items, make selections for reports, search, or sending emails. At interviews, LINQ filtering questions pop up all the time. Understanding this topic is a must for any beginner .NET developer.
2. Getting to Know the Where Method
What It Does and How It Works
The Where method is a LINQ extension method that takes a function (or lambda expression) defining the filter condition. It returns a new sequence (without changing the original!), containing only those elements for which the condition returns true.
Main signature:
public static IEnumerable<TSource> Where<TSource>(
this IEnumerable<TSource> source,
Func<TSource, bool> predicate)
Don't freak out about generics and scary words. The main idea: Where takes your original collection and a condition you set. In practice, it's simple:
- source — the collection you're filtering (like List<Product>)
- predicate — the function/condition (like p => p.Price > 100)
Simple Starter Task
Say we have a list of products, and we want to pick only those with a price over 100. Back in the day, you'd write a foreach loop and manually add the right items to a new list. LINQ lets you do it in one line.
3. Filtering
Our Product Class and Starting Collection
In the last module, we wrote a class for products like this:
class Product
{
public string Name { get; set; }
public double Price { get; set; }
// For pretty printing:
public override string ToString()
{
return $"{Name} (price: {Price})";
}
}
Let's say we have a list of products:
var products = new List<Product>
{
new Product { Name = "Bread", Price = 30 },
new Product { Name = "Milk", Price = 87 },
new Product { Name = "Cheese", Price = 250 },
new Product { Name = "Chocolate", Price = 130 }
};
Filtering Products Over 100 with Where
var expensiveProducts = products.Where(p => p.Price > 100);
foreach (var product in expensiveProducts)
{
Console.WriteLine(product);
}
What's happening under the hood:
- Where loops through all items in products,
- For each one, it calls the function (in our case: p => p.Price > 100),
- If the function returns true, the product goes into the new collection.
What you see on screen:
Cheese (price: 250)
Chocolate (price: 130)
Notice: the original products list didn't change! LINQ doesn't mess with your data.
4. Lazy Filtering (Deferred Execution)
Here's a cool and important thing: the result of Where only gets calculated when you actually access the elements. For example, as soon as you start a foreach loop, LINQ "walks" through the original collection and filters items on the fly. If you don't use the result, nothing happens. This is called deferred execution (deferred execution).
This gives us a bunch of perks:
- You can build really long method chains (like filter and sort), and only when you actually ask for the result does it all run.
- Saves memory: no extra intermediate collections are created.
- If you need to, you can cancel or "break" after finding enough items.
Visual Diagram
Original collection —► Where (condition) —► Only the needed elements are iterated
[ x ] [ x ] [ + ] [ + ] —► p.Price > 100 —► [ + ] [ + ]
(x — element doesn't match, + — matches)
5. Syntax Variants
Classic Extension Method Syntax (Method Syntax)
This is the style we've been using:
var result = products.Where(p => p.Price > 100);
Alternative: Query Syntax
LINQ also supports a syntax that looks like SQL queries:
var result = from p in products
where p.Price > 100
select p;
The result is the same!
Which syntax you use is up to you, but method syntax (dot-methods) is more common, especially in real projects and when building long chains.
6. Complex Filter Conditions
Multiple Conditions
You can use logical operators (&&, ||, !):
// Find all expensive products except "Chocolate"
var filtered = products.Where(
p => p.Price > 100 && p.Name != "Chocolate"
);
Filtering by String (Substring, Case, Contains)
// Products whose name contains the letter "l"
var lProducts = products.Where(p => p.Name.Contains("l"));
Heads up: Contains is case-sensitive! If you want "case-insensitive", you can do this:
var lProducts = products.Where(p => p.Name
.ToLower().Contains("l")); // now both "Milk" and "Chocolate" will be in the result
Filtering Across Multiple Collections
Say you have two lists — products and payments. You can filter products that show up in the payments list — but for that, it's better to use Any and Join methods, which we'll talk about later. Just know: LINQ can filter in more advanced ways too!
7. Handy Tips
Nested Filtering and Method Chains
You can chain multiple filter steps by calling Where several times in a row:
var filtered = products
.Where(p => p.Price > 100)
.Where(p => p.Name.StartsWith("Ch"));
But it's better to combine conditions in one Where using logical operators — it's more efficient and the code is simpler.
Built-in Comparators and Custom Functions
Sometimes the standard operators aren't enough. For example, you need to filter strings case-insensitively and with culture awareness. In that case, it's handy to use comparison methods:
var rusProducts = products.Where(
p => p.Name.StartsWith("ch", StringComparison.OrdinalIgnoreCase)
);
8. Filtering and User Input
Let's try a simple command-line filter in practice! For example, ask the user for a minimum price, then show all matching products.
Console.Write("Minimum product price? ");
var minPriceStr = Console.ReadLine();
if (double.TryParse(minPriceStr, out double minPrice))
{
var filtered = products.Where(p => p.Price >= minPrice);
foreach (var product in filtered)
{
Console.WriteLine(product);
}
}
else
{
Console.WriteLine("Error: not a number.");
}
Now our "mini-app" almost looks like a real store!
9. Common Mistakes and Pitfalls with Where
In programming, just like in life, there are gotchas. Here's what to watch out for.
Forgot to Call ToList() or ToArray()
The result of Where is not a list or even an array! It's an IEnumerable<Product> object. Most of the time it works fine in a foreach loop, but if you need an actual collection (like if you want indexed access), don't forget to call .ToList() or .ToArray():
var filteredList = products.Where(p => p.Price > 100).ToList();
If you skip this, it's easy to get an error like "Collection was modified during enumeration" — especially if you try to change the original collection while looping.
Changing the Original Collection
Since LINQ filtering is deferred, if you remove items from the original list in the middle of looping, you might get an exception. This is especially true in multi-threaded scenarios or if the collection changes during filtering.
Null Values and Predicates
If your collection has null and you access a field in your predicate without checking, you'll get a NullReferenceException. For example:
var filtered = products.Where(p => p.Name.StartsWith("A"));
If any item in products is null, the code will crash.
Tip: always add a null check if there's a risk of such elements:
var filtered = products.Where(p => p != null && p.Name.StartsWith("A"));
GO TO FULL VERSION