14. LINQ
LINQ (Language Integrated Query) is C#'s powerful weapon for querying collections declaratively, SQL-style. Cover Where, Select, OrderBy, GroupBy, Aggregate, and deferred execution in one lecture.
What you'll learn
- 1Use standard LINQ operators like `Where`, `Select`, `OrderBy`, `GroupBy`, `Join`
- 2Know aggregation operators `Sum`, `Average`, `Max`, `Aggregate`
- 3Understand the difference between method-chain and query syntax
- 4Understand **deferred execution**
Overview
LINQ (Language Integrated Query) is a powerful .NET feature that lets you query data sources (collections, databases, XML) **declaratively, SQL-style**. It's used naturally inside C# and supports two syntaxes: method chains or query syntax.
Core Concepts
1) `Where` and `Select`
int[] nums = [1, 2, 3, 4, 5, 6];
var result = nums.Where(n => n > 3).Select(n => n * n);
// Result: 16, 25, 36- `Where` **filters**; `Select` **transforms**.
- Lambdas (`=>`) are tiny inline functions.
2) Sorting and grouping
var sorted = people.OrderBy(p => p.Age).ThenBy(p => p.Name);
var byCity = people.GroupBy(p => p.City);
foreach (var group in byCity)
{
Console.WriteLine($"{group.Key}: {group.Count()} people");
}- Descending: `OrderByDescending` / `ThenByDescending`.
- `GroupBy` returns a sequence of `IGrouping<TKey, TElement>` β has both the `Key` and the elements in the group.
3) Aggregation operators
| Operator | Description |
|---|---|
| `Sum`, `Average`, `Max`, `Min`, `Count` | Common statistics |
| `Aggregate(seed, (acc, x) => ...)` | Custom accumulator |
| `Any`, `All` | Does any/all element match the predicate? |
4) Method chain vs query syntax
The same operation can be written two ways.
// Method chain
var a = nums.Where(n => n > 3).Select(n => n * n);
// Query syntax (SQL-like)
var b = from n in nums
where n > 3
select n * n;- For simple cases the method chain is shorter and clearer.
- For complex `join`, `let`, `group ... by ... into` the query syntax often reads better.
5) Deferred execution
LINQ expressions are **not executed immediately**. They're evaluated only when you iterate (e.g. `foreach`) or call a terminal operator like `ToList`/`ToArray`.
var q = list.Where(n => n > 0);
list.Add(99);
foreach (var n in q) { /* 99 is included */ }To "snapshot" the source right now, force eval with `.ToList()`.
Examples
Example 1 β `WhereSelect`: filter and transform
int[] nums = [1, 2, 3, 4, 5, 6, 7, 8];
var squaresOfBig = nums.Where(n => n > 4).Select(n => n * n);
foreach (int x in squaresOfBig)
{
Console.WriteLine(x);
}**Output**
25
36
49
64**Note:** `Where` only lets matching items through; `Select` reshapes each one. Swapping the order can change the result.
Example 2 β `OrderByGroup`: sort and group
Person[] people =
[
new("Jisoo", "Seoul", 25),
new("Minho", "Busan", 30),
new("Seoyeon", "Seoul", 22),
new("Yoonjae", "Busan", 28),
];
var sorted = people.OrderBy(p => p.City).ThenByDescending(p => p.Age);
foreach (Person p in sorted)
{
Console.WriteLine($"{p.City} - {p.Name} ({p.Age})");
}
Console.WriteLine("---");
var groups = people.GroupBy(p => p.City);
foreach (var g in groups)
{
Console.WriteLine($"[{g.Key}] {g.Count()} people: {string.Join(", ", g.Select(p => p.Name))}");
}
public class Person(string name, string city, int age)
{
public string Name { get; } = name;
public string City { get; } = city;
public int Age { get; } = age;
}**Output**
Busan - Minho (30)
Busan - Yoonjae (28)
Seoul - Jisoo (25)
Seoul - Seoyeon (22)
---
[Seoul] 2 people: Jisoo, Seoyeon
[Busan] 2 people: Minho, Yoonjae**Note:** `ThenBy` is the secondary key applied when the primary tied. `IGrouping<K,T>` exposes both the `Key` and the element sequence.
Example 3 β `Aggregation`: aggregation operators
int[] nums = [10, 20, 30, 40, 50];
Console.WriteLine($"Sum: {nums.Sum()}");
Console.WriteLine($"Average: {nums.Average()}");
Console.WriteLine($"Max: {nums.Max()}");
Console.WriteLine($"Min: {nums.Min()}");
Console.WriteLine($"Count: {nums.Count()}");
// Aggregate: custom accumulator (here, product)
int product = nums.Aggregate(1, (acc, n) => acc * n);
Console.WriteLine($"Product: {product}");
bool allBigger = nums.All(n => n > 0);
bool anyEven = nums.Any(n => n % 2 == 0);
Console.WriteLine($"All positive? {allBigger}, any even? {anyEven}");**Output**
Sum: 150
Average: 30
Max: 50
Min: 10
Count: 5
Product: 12000000
All positive? True, any even? True**Note:** `Aggregate(seed, func)` starts from `seed` and applies `func` cumulatively across the elements. `Sum`/`Average` etc. are essentially special cases of `Aggregate`.
Example 4 β `QuerySyntax`: method chain vs query syntax
int[] nums = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// Method chain
var a = nums.Where(n => n % 2 == 0).Select(n => n * n);
// Equivalent query syntax
var b = from n in nums
where n % 2 == 0
select n * n;
Console.WriteLine($"Method chain: [{string.Join(", ", a)}]");
Console.WriteLine($"Query syntax: [{string.Join(", ", b)}]");**Output**
Method chain: [4, 16, 36, 64, 100]
Query syntax: [4, 16, 36, 64, 100]**Note:** Both compile to the same code. Pick whichever reads best. Inside query syntax you can freely combine `join`, `group ... by ... into`, etc.
Example 5 β `DeferredExec`: deferred execution demo
List<int> nums = [1, 2, 3];
// Not executed here β only remembers "what to do"
var query = nums.Where(n => n > 1);
nums.Add(99); // mutate the source after defining the query
Console.Write("Deferred result: ");
foreach (int n in query)
{
Console.Write($"{n} ");
}
Console.WriteLine();
// To evaluate immediately, snapshot via ToList
var snapshot = nums.Where(n => n > 1).ToList();
nums.Add(100);
Console.WriteLine($"Snapshot result: [{string.Join(", ", snapshot)}]");**Output**
Deferred result: 2 3 99
Snapshot result: [2, 3, 99]**Note:** The first result includes `99` because `foreach` re-reads `nums` at the moment it runs. `ToList()`/`ToArray()` force evaluation at a specific point.
Full example code (src/)
src/Aggregation/Aggregation.csproj
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<RootNamespace>CodingNow.Lecture.Coll14</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
src/Aggregation/Program.cs
#nullable enable
int[] nums = [10, 20, 30, 40, 50];
Console.WriteLine($"Sum: {nums.Sum()}");
Console.WriteLine($"Average: {nums.Average()}");
Console.WriteLine($"Max: {nums.Max()}");
Console.WriteLine($"Min: {nums.Min()}");
Console.WriteLine($"Count: {nums.Count()}");
// Aggregate: custom accumulator (here, product)
int product = nums.Aggregate(1, (acc, n) => acc * n);
Console.WriteLine($"Product: {product}");
bool allBigger = nums.All(n => n > 0);
bool anyEven = nums.Any(n => n % 2 == 0);
Console.WriteLine($"All positive? {allBigger}, any even? {anyEven}");
src/DeferredExec/DeferredExec.csproj
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<RootNamespace>CodingNow.Lecture.Coll14</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
src/DeferredExec/Program.cs
#nullable enable
List<int> nums = [1, 2, 3];
// Not executed here β just remembers "what to do"
var query = nums.Where(n => n > 1);
nums.Add(99); // mutate source after defining the query
Console.Write("Deferred result: ");
foreach (int n in query)
{
Console.Write($"{n} ");
}
Console.WriteLine();
// To evaluate immediately, snapshot with ToList
var snapshot = nums.Where(n => n > 1).ToList();
nums.Add(100);
Console.WriteLine($"Snapshot result: [{string.Join(", ", snapshot)}]");
src/OrderByGroup/OrderByGroup.csproj
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<RootNamespace>CodingNow.Lecture.Coll14</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
src/OrderByGroup/Program.cs
#nullable enable
Person[] people =
[
new("Jisoo", "Seoul", 25),
new("Minho", "Busan", 30),
new("Seoyeon", "Seoul", 22),
new("Yoonjae", "Busan", 28),
];
// City ascending; within same city, age descending
var sorted = people.OrderBy(p => p.City).ThenByDescending(p => p.Age);
foreach (Person p in sorted)
{
Console.WriteLine($"{p.City} - {p.Name} ({p.Age})");
}
Console.WriteLine("---");
// Group by city
var groups = people.GroupBy(p => p.City);
foreach (var g in groups)
{
Console.WriteLine($"[{g.Key}] {g.Count()} people: {string.Join(", ", g.Select(p => p.Name))}");
}
public class Person(string name, string city, int age)
{
public string Name { get; } = name;
public string City { get; } = city;
public int Age { get; } = age;
}
src/QuerySyntax/Program.cs
#nullable enable
int[] nums = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// Method chain
var a = nums.Where(n => n % 2 == 0).Select(n => n * n);
// Equivalent query syntax (SQL-like)
var b = from n in nums
where n % 2 == 0
select n * n;
Console.WriteLine($"Method chain: [{string.Join(", ", a)}]");
Console.WriteLine($"Query syntax: [{string.Join(", ", b)}]");
src/QuerySyntax/QuerySyntax.csproj
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<RootNamespace>CodingNow.Lecture.Coll14</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
src/WhereSelect/Program.cs
#nullable enable
int[] nums = [1, 2, 3, 4, 5, 6, 7, 8];
// Where: only let matching elements through; Select: transform each
var squaresOfBig = nums.Where(n => n > 4).Select(n => n * n);
foreach (int x in squaresOfBig)
{
Console.WriteLine(x);
}
src/WhereSelect/WhereSelect.csproj
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<RootNamespace>CodingNow.Lecture.Coll14</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
Common Mistakes
- `Where` after `Select` vs `Select` after `Where` β the lambda's parameter type depends on the order.
- Forgetting deferred execution and iterating multiple times re-evaluates every time β wasted work. Materialize with `.ToList()` if reusing.
- Confusing the `Count` method vs the `Count` property: `IEnumerable<T>` has the method; `List<T>` has the property.
- Calling `First()` / `Single()` on an empty sequence β exception. Use `FirstOrDefault()` for safety.
- Trying to index a `GroupBy` result like a `Dictionary` β convert via `ToDictionary(g => g.Key)`.
Summary
- LINQ is the standard SQL-style query tool over collections.
- `Where`/`Select`/`OrderBy`/`GroupBy`/`Aggregate` are the workhorses.
- Method chain and query syntax express the same thing differently.
- Deferred execution is efficient, but use `.ToList()` to pin the evaluation point if needed.
Practice
**Practice - 14. LINQ**
Problem 1 β Student score analysis
- Project folder: `Homework01/`
- Key concepts: `Average`, `OrderByDescending`, `First`, `GroupBy`
Requirements
Use this student data.
Student[] students =
[
new("Jisoo", "Class A", 88),
new("Minho", "Class B", 72),
new("Seoyeon", "Class A", 95),
new("Yoonjae", "Class B", 65),
new("Haneul", "Class A", 78),
new("Doyoon", "Class B", 90),
];Use only LINQ to print all of the following.
- Overall average score (one decimal place)
- The top scorer's name and score
- **Average score per class**
Expected output
Overall average: 81.3
Top scorer: Seoyeon (95 pts)
=== Average by class ===
Class A: 87.0
Class B: 75.7Hints
- `students.Average(s => s.Score)`
- `students.OrderByDescending(s => s.Score).First()`
- `students.GroupBy(s => s.Class)` β call `Average` per group
---
Problem 2 β Order revenue stats
- Project folder: `Homework02/`
- Key concepts: `Sum`, `GroupBy`, `Select`, anonymous type or tuple
Requirements
Use this order data.
Order[] orders =
[
new("Apple", 3, 1500),
new("Banana", 5, 800),
new("Apple", 2, 1500),
new("Tangerine", 10, 500),
new("Banana", 3, 800),
];Use LINQ to print:
- Total revenue (sum of `quantity * unitPrice`)
- **Revenue per product** β sorted by revenue descending
Expected output
Total revenue: 18,900 won
=== Revenue by product ===
Apple: 7,500 won
Banana: 6,400 won
Tangerine: 5,000 wonHints
- `orders.Sum(o => o.Quantity * o.UnitPrice)` for total revenue.
- `GroupBy(o => o.Product)` β `Select(g => new { Name = g.Key, Total = g.Sum(o => o.Quantity * o.UnitPrice) })` β `OrderByDescending`.
- Thousand-separator format: `$"{value:N0} won"`.
Check your answer
Try it yourself, then compare against the [`answer/`](./answer/) folder.
Answer (answer/)
homework/answer/Homework01/Homework01.csproj
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<RootNamespace>CodingNow.Lecture.Coll14</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
homework/answer/Homework01/Program.cs
#nullable enable
Student[] students =
[
new("Jisoo", "Class A", 88),
new("Minho", "Class B", 72),
new("Seoyeon", "Class A", 95),
new("Yoonjae", "Class B", 65),
new("Haneul", "Class A", 78),
new("Doyoon", "Class B", 90),
];
double overallAvg = students.Average(s => s.Score);
Console.WriteLine($"Overall average: {overallAvg:F1}");
Student top = students.OrderByDescending(s => s.Score).First();
Console.WriteLine($"Top scorer: {top.Name} ({top.Score} pts)");
Console.WriteLine("=== Average by class ===");
var byClass = students
.GroupBy(s => s.Class)
.OrderBy(g => g.Key);
foreach (var g in byClass)
{
Console.WriteLine($"{g.Key}: {g.Average(s => s.Score):F1}");
}
public class Student(string name, string @class, int score)
{
public string Name { get; } = name;
public string Class { get; } = @class;
public int Score { get; } = score;
}
homework/answer/Homework02/Homework02.csproj
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<RootNamespace>CodingNow.Lecture.Coll14</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>
homework/answer/Homework02/Program.cs
#nullable enable
Order[] orders =
[
new("Apple", 3, 1500),
new("Banana", 5, 800),
new("Apple", 2, 1500),
new("Tangerine", 10, 500),
new("Banana", 3, 800),
];
int total = orders.Sum(o => o.Quantity * o.UnitPrice);
Console.WriteLine($"Total revenue: {total:N0} won");
Console.WriteLine("=== Revenue by product ===");
var byProduct = orders
.GroupBy(o => o.Product)
.Select(g => new { Name = g.Key, Total = g.Sum(o => o.Quantity * o.UnitPrice) })
.OrderByDescending(x => x.Total);
foreach (var p in byProduct)
{
Console.WriteLine($"{p.Name}: {p.Total:N0} won");
}
public class Order(string product, int quantity, int unitPrice)
{
public string Product { get; } = product;
public int Quantity { get; } = quantity;
public int UnitPrice { get; } = unitPrice;
}
Try It Yourself
cd src/WhereSelect
dotnet run
cd ../OrderByGroup
dotnet run
cd ../Aggregation
dotnet run
cd ../QuerySyntax
dotnet run
cd ../DeferredExec
dotnet runNext Lecture
[15_Exception_Handling](../../04_%EC%98%88%EC%99%B8_%EC%9E%85%EC%B6%9C%EB%A0%A5/15_%EC%98%88%EC%99%B8%EC%B2%98%EB%A6%AC/) β Handle runtime errors safely.
All lecture materials and example code are openly available on GitHub.
View on GitHub β