C# LINQ Filtering and Projection
Filtering and projection are the two LINQ operations you will use most often. Filtering chooses which items stay in a sequence, while projection changes each kept item into the shape your program needs. Together, Where and Select let you turn raw collections into focused results without writing repetitive loops.
Overview: How Filtering And Projection Work
In LINQ, a sequence is usually represented by IEnumerable<T>, meaning it can produce values of type T one at a time. Where filters a sequence by calling a predicate for each item. A predicate is a function that returns true or false. If the predicate returns true, the item is yielded to the next part of the query; if it returns false, the item is skipped.
Select projects each item into another value. Projection might be simple, such as converting a name to uppercase, or it might create a new object containing selected fields. The output type does not have to match the input type. A List<Product> can become an IEnumerable<string>, an IEnumerable<decimal>, or an enumerable of anonymous objects with named properties.
Most LINQ filtering and projection over in-memory collections uses deferred execution. Calling Where or Select usually creates an iterator object that stores the source sequence and your lambda. The query does not run immediately. It runs when something enumerates it, such as foreach, ToList(), ToArray(), Count(), First(), or string.Join. This is why LINQ can stream data efficiently: for a chain like source.Where(...).Select(...), each item can be tested and transformed before the next item is requested.
Under the hood, these methods are extension methods from System.Linq.Enumerable. The compiler turns your lambda expressions into delegates such as Func<Product, bool> for a filter or Func<Product, string> for a projection. The CLR then executes ordinary method calls, delegate invocations, and enumerator movement. LINQ feels like a query language, but for IEnumerable<T> it is built from normal C# types and methods.
Syntax
int[] source = { 1, 2, 3, 4 };
IEnumerable<int> filtered = source.Where(item => item > 2);
IEnumerable<string> projected = source.Select(item => $"Value {item}");
var combined = source
.Where(item => item > 2)
.Select(item => new { Original = item, Doubled = item * 2 });
| Part | Meaning |
|---|---|
source |
The input sequence, such as an array, list, dictionary entries, or another LINQ query. |
Where |
Keeps only items whose predicate returns true. |
Select |
Converts each input item into one output value. |
item => item > 2 |
A lambda that receives one item and returns a Boolean value. |
item => $"Value {item}" |
A lambda that receives one item and returns the projected value. |
new { ... } |
An anonymous object, useful for temporary report-style results. |
When writing a full program, include the System.Linq namespace. If you use IEnumerable<T> or List<T> explicitly, include System.Collections.Generic too.
Examples
Example 1: Filter Numbers And Project Text
using System;
using System.Linq;
class Program
{
static void Main()
{
int[] scores = { 55, 72, 88, 91, 64, 100 };
var labels = scores
.Where(score => score >= 80)
.Select(score => $"Pass with {score}");
foreach (string label in labels)
{
Console.WriteLine(label);
}
}
}
Output:
Pass with 88
Pass with 91
Pass with 100
Where keeps only scores greater than or equal to 80. Select then turns each remaining integer into a string label. Notice that the input sequence contains integers, but the final query produces strings.
Example 2: Project Objects Into A Report Shape
using System;
using System.Collections.Generic;
using System.Linq;
record Product(string Name, string Category, decimal Price, int Stock);
class Program
{
static void Main()
{
var products = new List<Product>
{
new Product("Notebook", "Office", 4.50m, 20),
new Product("Pen", "Office", 1.25m, 80),
new Product("Mug", "Kitchen", 9.99m, 12),
new Product("Tea", "Kitchen", 6.40m, 0),
new Product("Cable", "Electronics", 12.00m, 7)
};
var available = products
.Where(product => product.Stock > 0)
.Select(product => new
{
product.Name,
product.Category,
InventoryValue = product.Price * product.Stock
});
foreach (var item in available)
{
Console.WriteLine($"{item.Name} ({item.Category}): {item.InventoryValue:F2}");
}
}
}
Output:
Notebook (Office): 90.00
Pen (Office): 100.00
Mug (Kitchen): 119.88
Cable (Electronics): 84.00
This query removes out-of-stock products and projects each remaining product into an anonymous object. Anonymous objects are strongly typed; the compiler knows that item.Name, item.Category, and item.InventoryValue exist inside the same method. They are ideal for temporary summaries, but for public APIs and long-lived results, prefer a named class or record.
Example 3: Use Indexes And Flatten Nested Lists
using System;
using System.Collections.Generic;
using System.Linq;
record Course(string Title, List<string> Students);
class Program
{
static void Main()
{
var courses = new List<Course>
{
new Course("C#", new List<string> { "Ava", "Noah" }),
new Course("SQL", new List<string> { "Mina" }),
new Course("Python", new List<string> { "Omar", "Ivy" })
};
var courseLabels = courses.Select((course, index) => $"{index + 1}. {course.Title}");
Console.WriteLine(string.Join(" | ", courseLabels));
var enrollments = courses
.SelectMany(course => course.Students,
(course, student) => $"{student} studies {course.Title}");
foreach (string enrollment in enrollments)
{
Console.WriteLine(enrollment);
}
}
}
Output:
1. C# | 2. SQL | 3. Python
Ava studies C#
Noah studies C#
Mina studies SQL
Omar studies Python
Ivy studies Python
Select has an overload that provides the zero-based index of each item. This is useful for labels, row numbers, and alternating display rules. SelectMany is projection plus flattening: each course produces several students, and LINQ returns one flat sequence of enrollment strings instead of a sequence of nested student lists.
How It Works Step By Step
- The compiler finds the extension method
Enumerable.WhereorEnumerable.SelectbecauseSystem.Linqis in scope. - The lambda is type-checked against the source item type. In
products.Where(product => product.Stock > 0),productis aProduct. Wherestores the source sequence and predicate delegate in an iterator object.Selectstores its source and selector delegate in another iterator object.- When enumeration starts, the outer iterator asks the previous iterator for the next value. The chain pulls values from the original source only as needed.
- For each source item,
Wherecalls the predicate. Matching items move forward; nonmatching items are discarded. Selectcalls the selector only for items it receives and yields the transformed result.- If you call
ToList(), the query is fully consumed and the results are copied into a new list. Without materialization, repeating enumeration can repeat the filtering and projection work.
Because Where and Select are streaming operators, they do not need to store the entire result before producing the first value. This differs from operators such as OrderBy, which must inspect and buffer the sequence before it can return sorted output.
Common Mistakes
Projecting Before Filtering When The Projection Is Expensive
using System;
using System.Linq;
class Program
{
static string BuildLabel(int number)
{
Console.WriteLine($"Building label for {number}");
return $"Value {number}";
}
static void Main()
{
int[] numbers = { 1, 2, 3, 4 };
var labels = numbers
.Select(number => new { Number = number, Label = BuildLabel(number) })
.Where(item => item.Number % 2 == 0)
.Select(item => item.Label);
foreach (string label in labels)
{
Console.WriteLine(label);
}
}
}
Output:
Building label for 1
Building label for 2
Value 2
Building label for 3
Building label for 4
Value 4
This code is valid, but it builds labels for odd numbers that will be discarded. When possible, filter first and then project.
using System;
using System.Linq;
class Program
{
static string BuildLabel(int number)
{
Console.WriteLine($"Building label for {number}");
return $"Value {number}";
}
static void Main()
{
int[] numbers = { 1, 2, 3, 4 };
var labels = numbers
.Where(number => number % 2 == 0)
.Select(number => BuildLabel(number));
foreach (string label in labels)
{
Console.WriteLine(label);
}
}
}
Output:
Building label for 2
Value 2
Building label for 4
Value 4
Forgetting That Deferred Queries See Later Changes
using System;
using System.Collections.Generic;
using System.Linq;
class Program
{
static void Main()
{
List<string> names = new List<string> { "Ava", "Noah", "Mina" };
var shortNames = names.Where(name => name.Length == 3);
names.Add("Ivy");
Console.WriteLine(string.Join(", ", shortNames));
}
}
Output:
Ava, Ivy
The query was created before Ivy was added, but it was enumerated afterward. If you need a stable snapshot, materialize the query immediately.
using System;
using System.Collections.Generic;
using System.Linq;
class Program
{
static void Main()
{
List<string> names = new List<string> { "Ava", "Noah", "Mina" };
List<string> shortNames = names.Where(name => name.Length == 3).ToList();
names.Add("Ivy");
Console.WriteLine(string.Join(", ", shortNames));
}
}
Output:
Ava
Best Practices
- Use
WherebeforeSelectwhen the filter can reduce the amount of projection work. - Keep predicates and selectors small. Move complex logic into named methods with clear names.
- Project only the fields you actually need, especially for reports and API responses.
- Use anonymous objects for local, temporary shapes; use records or classes for values that cross method boundaries.
- Remember that
WhereandSelectare deferred. UseToList()orToArray()when you need a snapshot. - Avoid side effects inside LINQ lambdas. A query may run later, more than once, or not at all.
- Use
SelectManywhen each input item contains a collection and you want one flat result sequence. - Prefer readable query chains over clever one-liners. If a chain becomes hard to scan, split it into named intermediate variables.
Practice Exercises
- Create a list of temperatures. Use
Whereto keep values above 75 andSelectto format them as strings such asHot: 82. - Create a
Bookrecord with title, author, and page count. Print only books with more than 300 pages, projected into strings likeTitle by Author. - Create a list of departments, where each department has a list of employee names. Use
SelectManyto print one line per employee with the department name.
Summary
Wherefilters a sequence by keeping items whose predicate returnstrue.Selectprojects each input item into a new output value, often with a different type.- Filtering and projection are streaming, deferred LINQ operations over
IEnumerable<T>. - The compiler type-checks lambdas and LINQ calls as ordinary generic method calls.
- Anonymous objects are useful for local report shapes, while named records or classes are better for shared results.
SelectManyprojects nested collections and flattens them into one sequence.- Materialize with
ToList()orToArray()when later source changes should not affect the result.
