← Back to C# series
πŸ“š
Collections & LINQ
Collections Β· Prerequisite: generics

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.

C#.NET 8collectionsLINQ
Duration
⏱ ~1-1.5 hours
Level
πŸ“Š Intermediate
Prerequisite
🎯 Generics
OUTCOME
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`

csharp
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

csharp
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

OperatorDescription
`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.

csharp
// 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`.

csharp
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

csharp
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**

text
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

csharp
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**

text
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

csharp
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**

text
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

csharp
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**

text
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

csharp
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**

text
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

xml
<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

csharp
#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

xml
<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

csharp
#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

xml
<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

csharp
#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

csharp
#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

xml
<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

csharp
#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

xml
<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

  1. `Where` after `Select` vs `Select` after `Where` β€” the lambda's parameter type depends on the order.
  2. Forgetting deferred execution and iterating multiple times re-evaluates every time β†’ wasted work. Materialize with `.ToList()` if reusing.
  3. Confusing the `Count` method vs the `Count` property: `IEnumerable<T>` has the method; `List<T>` has the property.
  4. Calling `First()` / `Single()` on an empty sequence β†’ exception. Use `FirstOrDefault()` for safety.
  5. 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.

csharp
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.

  1. Overall average score (one decimal place)
  2. The top scorer's name and score
  3. **Average score per class**

Expected output

text
Overall average: 81.3
Top scorer: Seoyeon (95 pts)
=== Average by class ===
Class A: 87.0
Class B: 75.7

Hints

  • `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.

csharp
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:

  1. Total revenue (sum of `quantity * unitPrice`)
  2. **Revenue per product** β€” sorted by revenue descending

Expected output

text
Total revenue: 18,900 won
=== Revenue by product ===
Apple: 7,500 won
Banana: 6,400 won
Tangerine: 5,000 won

Hints

  • `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

xml
<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

csharp
#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

xml
<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

csharp
#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

bash
cd src/WhereSelect
dotnet run

cd ../OrderByGroup
dotnet run

cd ../Aggregation
dotnet run

cd ../QuerySyntax
dotnet run

cd ../DeferredExec
dotnet run

Next 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.

Example code / lecture materials

All lecture materials and example code are openly available on GitHub.

View on GitHub β†—