LINQ basics: filter, project, sort
Query collections declaratively with Where, Select, OrderBy and LINQ's aggregate methods.
13 min read
You will learn
- Filter with Where and transform with Select
- Sort with OrderBy, ThenBy and their Descending versions
- Use First, FirstOrDefault, Any, All and Count
- Compute Sum, Average, Min and Max
Say what you want, not how to loop
Here's a familiar task — the names of adult users, sorted — written with loops:
var result = new List<string>();
foreach (var user in users)
{
if (user.Age >= 18)
result.Add(user.Name);
}
result.Sort();And with LINQ (Language-Integrated Query):
var result = users
.Where(u => u.Age >= 18)
.Select(u => u.Name)
.Order()
.ToList();The LINQ version reads like a description of the result. Each method takes a lambda — everything you learned about delegates pays off here. LINQ works on anything that implements IEnumerable<T>: arrays, lists, dictionaries, sets, strings, and (in Track 10) database tables.
For the examples below:
record Product(string Name, string Category, decimal Price, int Stock);
var products = new List<Product>
{
new("Laptop", "Electronics", 999m, 3),
new("Mouse", "Electronics", 19.99m, 10),
new("Notebook", "Office", 4.5m, 100),
new("Pen", "Office", 1.2m, 0),
};Where: filter
var cheap = products.Where(p => p.Price < 20); // Mouse, Notebook, Pen
var inStockOffice = products.Where(p => p.Category == "Office" && p.Stock > 0);Select: transform each element
var names = products.Select(p => p.Name); // "Laptop", "Mouse", ...
var labels = products.Select(p => $"{p.Name} ({p.Price:F2})");
var withTax = products.Select(p => new { p.Name, Total = p.Price * 1.2m }); // anonymous typeSorting
products.OrderBy(p => p.Price); // cheapest first
products.OrderByDescending(p => p.Stock); // most stock first
products.OrderBy(p => p.Category).ThenBy(p => p.Name); // tie-breaker with ThenBy
new[] { 3, 1, 2 }.Order(); // sort simple values by themselvesPicking single elements
| Method | Returns | When nothing matches |
|---|---|---|
First(pred) | the first match | throws |
FirstOrDefault(pred) | the first match | default (null for classes) |
Single(pred) | the only match | throws (also throws if several match) |
Last(pred), ElementAt(i) | … | throws |
MaxBy(sel) / MinBy(sel) | element with the largest/smallest key | null for empty sequences of classes |
var laptop = products.First(p => p.Name == "Laptop");
var garden = products.FirstOrDefault(p => p.Category == "Garden"); // null
var priciest = products.MaxBy(p => p.Price); // LaptopAsking questions
bool anySoldOut = products.Any(p => p.Stock == 0); // true
bool allCheap = products.All(p => p.Price < 1000); // true
int officeCount = products.Count(p => p.Category == "Office"); // 2
bool hasPen = products.Select(p => p.Name).Contains("Pen");Any() with no arguments checks for emptiness — clearer and often faster than Count() > 0.
Aggregates
decimal total = products.Sum(p => p.Price * p.Stock);
decimal average = products.Average(p => p.Price);
decimal cheapest = products.Min(p => p.Price);
int mostStock = products.Max(p => p.Stock);Paging, distinct and conversion
products.Skip(10).Take(10); // page 2 with 10 per page
products.Select(p => p.Category).Distinct(); // "Electronics", "Office"
products.Where(...).ToList(); // materialize into a List<T>
products.ToArray();
products.ToDictionary(p => p.Name); // key selector → Dictionary<string, Product>Chaining
Each method returns a new sequence, so you chain them into a pipeline — formatted one call per line:
var report = products
.Where(p => p.Stock > 0)
.OrderByDescending(p => p.Price * p.Stock)
.Take(3)
.Select(p => $"{p.Name}: {p.Price * p.Stock:F2}");LINQ never modifies the source
Where, Select, OrderBy… all return new sequences. The original list is untouched — unlike List.Sort() or RemoveAll(), which change the list in place.
Try it
C# runtime not loaded
Key takeaways
- LINQ queries any
IEnumerable<T>with chained methods that take lambdas. Wherefilters,Selecttransforms,OrderBy/ThenBysort.First/Singlethrow on no match; theOrDefaultversions returndefault.Any,All,Count,Sum,Average,Min,Maxanswer questions about a sequence.
Exercise
Using LINQ, implement CheapNames (names of products priced at or under maxPrice, sorted alphabetically), InventoryValue (sum of price × stock), MostExpensiveIn (the priciest product in a category, or null) and AllInStock (true when every product has stock > 0). Don't change Catalog — the checks use it.
C# runtime not loaded
Hint 1
CheapNames: products.Where(p => p.Price <= maxPrice).Select(p => p.Name).Order().ToList()
Hint 2
InventoryValue: products.Sum(p => p.Price * p.Stock)
Hint 3
MostExpensiveIn: filter by category, then .OrderByDescending(p => p.Price).FirstOrDefault() — or MaxBy(p => p.Price).
Show solution
Console.WriteLine(string.Join(", ", CheapNames(Catalog(), 20)));
List<string> CheapNames(List<Product> products, decimal maxPrice) =>
products
.Where(p => p.Price <= maxPrice)
.Select(p => p.Name)
.Order()
.ToList();
decimal InventoryValue(List<Product> products) =>
products.Sum(p => p.Price * p.Stock);
Product? MostExpensiveIn(List<Product> products, string category) =>
products
.Where(p => p.Category == category)
.OrderByDescending(p => p.Price)
.FirstOrDefault();
bool AllInStock(List<Product> products) =>
products.All(p => p.Stock > 0);
List<Product> Catalog() =>
[
new("Laptop", "Electronics", 999m, 3),
new("Mouse", "Electronics", 19.99m, 10),
new("Notebook", "Office", 4.5m, 100),
new("Pen", "Office", 1.2m, 0),
new("Monitor", "Electronics", 199m, 0),
new("Desk", "Office", 250m, 2),
];
record Product(string Name, string Category, decimal Price, int Stock);Quiz
4 questions · 3 correct to pass1.What does `numbers.Where(n => n > 10)` return?
2.What does `people.Select(p => p.Name)` produce?
3.What's the difference between First() and FirstOrDefault()?
4.How do you sort people by last name, then by first name?
To complete this lesson
- ○ Solve the exercise
- ○ Pass the quiz (3/4 correct)