C# LINQ — IEnumerable, Where, Select, GroupBy, OrderBy, and Aggregation
In this tutorial, you will learn about C# LINQ. We cover key concepts, practical examples, and best practices to help you master this topic.
C# LINQ (Language Integrated Query) brings SQL-like query capabilities directly into C# code, enabling declarative data manipulation over any IEnumerable
What You'll Learn
You will master LINQ in C#: query syntax vs method syntax, filtering with Where, transforming with Select, sorting with OrderBy/ThenBy, grouping with GroupBy, aggregating with Sum/Average/Count, and understanding deferred execution in .NET.
Why It Matters
LINQ is one of the most transformative features in C#. It replaces verbose loops with declarative queries, reduces bugs, and makes code more readable. Any data source that implements IEnumerable
Real-World Use
EF Core queries translate LINQ to SQL for database access. REST APIs use LINQ to filter, sort, and project data before returning responses. Data processing pipelines chain LINQ operations for ETL workflows. Reporting systems aggregate data with LINQ. Background services filter and batch Process items using LINQ.
Learning Path
graph LR
A["20: Exception Handling"] --> B["21: LINQ"]
B --> C["22: LINQ Advanced"]
C --> D["23: Delegates & Events"]
D --> E["24: Lambdas"]
style A fill:#4a90d9,stroke:#2c5f8a,color:#fff
style B fill:#4a90d9,stroke:#2c5f8a,color:#fff
style C fill:#4a90d9,stroke:#2c5f8a,color:#fff
style D fill:#4a90d9,stroke:#2c5f8a,color:#fff
style E fill:#4a90d9,stroke:#2c5f8a,color:#fff
Data Setup
var products = new List<Product>
{
new() { Id = 1, Name = "Laptop", Category = "Electronics", Price = 999.99m, Stock = 15 },
new() { Id = 2, Name = "Mouse", Category = "Electronics", Price = 29.99m, Stock = 100 },
new() { Id = 3, Name = "Desk", Category = "Furniture", Price = 299.99m, Stock = 10 },
new() { Id = 4, Name = "Chair", Category = "Furniture", Price = 199.99m, Stock = 25 },
new() { Id = 5, Name = "Monitor", Category = "Electronics", Price = 399.99m, Stock = 8 },
new() { Id = 6, Name = "Notebook", Category = "Stationery", Price = 4.99m, Stock = 500 },
new() { Id = 7, Name = "Pen Set", Category = "Stationery", Price = 12.99m, Stock = 200 },
};
class Product
{
public int Id { get; set; }
public string Name { get; set; }
public string Category { get; set; }
public decimal Price { get; set; }
public int Stock { get; set; }
}
Method Syntax vs Query Syntax
// Method syntax (fluent)
var cheapProducts = products
.Where(p => p.Price < 100)
.OrderBy(p => p.Price)
.Select(p => p.Name);
// Query syntax (SQL-like)
var cheapQuery = from p in products
where p.Price < 100
orderby p.Price
select p.Name;
// Both produce the same result
Console.WriteLine("Products under $100:");
foreach (var name in cheapProducts)
Console.WriteLine($" {name}");
Where (Filtering)
// Multiple conditions
var filtered = products
.Where(p => p.Category == "Electronics" && p.Stock > 10);
// Filter by index
var everyOther = products.Where((p, index) => index % 2 == 0);
// OfType<T> filters by type
var mixed = new List<object> { 1, "hello", 2, "world", 3 };
var strings = mixed.OfType<string>(); // "hello", "world"
Select (Projection)
// Simple projection
var names = products.Select(p => p.Name);
// Anonymous type projection
var summaries = products.Select(p => new
{
p.Name,
p.Price,
p.Category
});
// Indexed projection
var numbered = products.Select((p, i) => $"{i + 1}. {p.Name}");
// SelectMany (flattening)
int[][] numbers = { new[] { 1, 2 }, new[] { 3, 4 }, new[] { 5, 6 } };
var all = numbers.SelectMany(n => n); // 1, 2, 3, 4, 5, 6
OrderBy / ThenBy (Sorting)
var sorted = products
.OrderBy(p => p.Category) // Primary sort
.ThenByDescending(p => p.Price) // Secondary sort
.ThenBy(p => p.Name); // Tertiary sort
Console.WriteLine("Products sorted by category, then price descending:");
foreach (var p in sorted)
Console.WriteLine($" {p.Category}: {p.Name} (${p.Price})");
GroupBy
var grouped = products.GroupBy(p => p.Category);
foreach (var group in grouped)
{
Console.WriteLine($"\n{group.Key} ({group.Count()} items):");
Console.WriteLine($" Total stock: {group.Sum(p => p.Stock)}");
Console.WriteLine($" Avg price: {group.Average(p => p.Price):C}");
foreach (var product in group)
Console.WriteLine($" {product.Name} - ${product.Price}");
}
Aggregation
Console.WriteLine($"Total products: {products.Count()}");
Console.WriteLine($"Total stock: {products.Sum(p => p.Stock)}");
Console.WriteLine($"Average price: {products.Average(p => p.Price):C}");
Console.WriteLine($"Max price: {products.Max(p => p.Price):C}");
Console.WriteLine($"Min price: {products.Min(p => p.Price):C}");
// Aggregate with custom logic
var allNames = products.Aggregate("", (acc, p) => acc == "" ? p.Name : acc + ", " + p.Name);
Console.WriteLine($"All names: {allNames}");
Expected output:
Total products: 7
Total stock: 858
Average price: $278.28
Max price: $999.99
Min price: $4.99
All names: Laptop, Mouse, Desk, Chair, Monitor, Notebook, Pen Set
Deferred Execution
var query = products.Where(p =>
{
Console.WriteLine($" Checking: {p.Name}");
return p.Price > 100;
});
Console.WriteLine("Query defined. No execution yet.");
Console.WriteLine("Materializing with ToList():");
var result = query.ToList(); // Execution happens here
Expected output:
Query defined. No execution yet.
Materializing with ToList():
Checking: Laptop
Checking: Mouse
Checking: Desk
Checking: Chair
Checking: Monitor
Checking: Notebook
Checking: Pen Set
Immdediate Execution
// These methods execute immediately:
var list = products.Where(p => p.Price > 100).ToList();
var array = products.Select(p => p.Name).ToArray();
var dict = products.ToDictionary(p => p.Id);
var lookup = products.ToLookup(p => p.Category);
var hashSet = products.Where(p => p.Stock > 10).ToHashSet();
// Singleton results also execute immediately
var first = products.First(p => p.Name == "Laptop");
var last = products.Last();
var single = products.Single(p => p.Id == 3);
var count = products.Count();
var any = products.Any(p => p.Stock == 0);
var all = products.All(p => p.Price > 0);
Common LINQ Methods Reference
| Method | Purpose | Example |
|---|---|---|
| Where | Filter | products.Where(p => p.Price > 100) |
| Select | Transform | products.Select(p => p.Name) |
| OrderBy | Sort ascending | products.OrderBy(p => p.Price) |
| OrderByDescending | Sort descending | products.OrderByDescending(p => p.Price) |
| ThenBy | Secondary sort | products.OrderBy(p => p.Category).ThenBy(p => p.Name) |
| GroupBy | Group | products.GroupBy(p => p.Category) |
| First | First element | products.First(p => p.Name == "Laptop") |
| FirstOrDefault | First or null | products.FirstOrDefault(p => p.Name == "Tablet") |
| Single | Exactly one | products.Single(p => p.Id == 3) |
| Any | Check existence | products.Any(p => p.Stock == 0) |
| All | Check all | products.All(p => p.Price > 0) |
| Count | Count elements | products.Count(p => p.Category == "Electronics") |
| Sum | Sum values | products.Sum(p => p.Stock) |
| Average | Average values | products.Average(p => p.Price) |
| Min/Max | Extremum | products.Min(p => p.Price) |
| Distinct | Unique elements | products.Select(p => p.Category).Distinct() |
| Take | First N | products.Take(3) |
| Skip | Skip N | products.Skip(3) |
| TakeWhile | While condition | products.TakeWhile(p => p.Price > 50) |
Common Mistakes
Mistake 1: Multiple Enumeration
// BAD: enumerates twice
var query = products.Where(p => p.Price > 100);
var count = query.Count();
var first = query.First();
// GOOD: materialize once
var materialized = products.Where(p => p.Price > 100).ToList();
var count = materialized.Count;
var first = materialized.First();
Mistake 2: Confusing First and FirstOrDefault
First throws if no match. FirstOrDefault returns default (null for ref types, 0 for value types). Use First when you know the element exists.
Mistake 3: Using Count() > 0 Instead of Any()
Any() returns true as soon as it finds a match. Count() > 0 iterates the entire collection. For IEnumerableAny() is more efficient.
Mistake 4: Order of Operations Matters
Where(p => ...).OrderBy(p => ...).Take(10) is more efficient than OrderBy(p => ...).Where(p => ...).Take(10) because ordering first on all elements is wasteful.
Mistake 5: Not Using Select for Performance
products.Select(p => new { p.Name, p.Price }) only transfers needed fields. Without Select, all columns/properties are transferred.
Mistake 6: Modifying Source While Iterating a LINQ Query
Changing the source collection while iterating a LINQ query throws InvalidOperationException. Materialize the query first.
Practice Questions
- What is the difference between method syntax and query syntax in LINQ?
- How does deferred execution affect when a query runs?
- When would you use
FirstOrDefaultinstead ofFirst? - Write a LINQ query that groups products by category and finds the most expensive product in each category.
- What is the difference between
SelectandSelectMany?
Challenge
Given a list of orders, use LINQ to find the top 3 customers by total order value. Each order has a CustomerName, OrderDate, and List of OrderItems (each with ProductName, Quantity, UnitPrice). Return customer name and total spent.
FAQ
Mini Project
Create a product analysis dashboard:
// Calculate various statistics
var stats = new
{
TotalProducts = products.Count,
Categories = products.Select(p => p.Category).Distinct().Count(),
TotalValue = products.Sum(p => p.Price * p.Stock),
AveragePrice = products.Average(p => p.Price),
// Top 3 by stock value
TopProducts = products
.OrderByDescending(p => p.Price * p.Stock)
.Take(3)
.Select(p => $"{p.Name} (${p.Price * p.Stock:N0})"),
// Category summary
CategorySummary = products
.GroupBy(p => p.Category)
.Select(g => new
{
Category = g.Key,
ProductCount = g.Count(),
TotalStock = g.Sum(p => p.Stock),
AvgPrice = g.Average(p => p.Price)
}),
// Products needing restock (stock < 10)
LowStock = products
.Where(p => p.Stock < 10)
.OrderBy(p => p.Stock)
.Select(p => $"{p.Name}: {p.Stock} units"),
// Price tiers
PriceTiers = products
.GroupBy(p => p.Price switch
{
< 50 => "Budget",
< 200 => "Mid-range",
< 500 => "Premium",
_ => "Luxury"
})
.Select(g => $"{g.Key}: {g.Count()} products (avg ${g.Average(p => p.Price):N2})")
};
Console.WriteLine("Product Dashboard");
Console.WriteLine("=================");
Console.WriteLine($"Total: {stats.TotalProducts} products");
Console.WriteLine($"Categories: {stats.Categories}");
Console.WriteLine($"Inventory value: {stats.TotalValue:C}");
Console.WriteLine($"Average price: {stats.AveragePrice:C}");
Console.WriteLine("\nTop 3 Products by Value:");
foreach (var p in stats.TopProducts)
Console.WriteLine($" {p}");
Console.WriteLine("\nCategory Summary:");
foreach (var c in stats.CategorySummary)
Console.WriteLine($" {c.Category}: {c.ProductCount} items, {c.TotalStock} stock, avg {c.AvgPrice:C}");
Console.WriteLine("\nLow Stock Products:");
foreach (var p in stats.LowStock)
Console.WriteLine($" {p}");
Console.WriteLine("\nPrice Tiers:");
foreach (var t in stats.PriceTiers)
Console.WriteLine($" {t}");
Expected output:
Product Dashboard
=================
Total: 7 products
Categories: 3
Inventory value: $21,343.65
Average price: $278.28
Top 3 Products by Value:
Laptop ($14,999.85)
Monitor ($3,199.92)
Chair ($4,999.75)
Category Summary:
Electronics: 3 items, 123 stock, avg $476.66
Furniture: 2 items, 35 stock, avg $249.99
Stationery: 2 items, 700 stock, avg $8.99
Low Stock Products:
Monitor: 8 units
Price Tiers:
Budget: 3 products (avg $15.99)
Mid-range: 2 products (avg $249.99)
Premium: 1 products (avg $399.99)
Luxury: 1 products (avg $999.99)
What's Next
You have mastered basic LINQ operations. The next lesson covers advanced LINQ: Join, GroupJoin, Zip, Aggregation, and query syntax in depth.
Built by the developers of DodaTech
Doda Browser, DodaZIP & Durga Antivirus Pro