C# (.NET 8 LTS) track

Lesson 14 · Part 3: Collections & LINQ

LINQ

Query collections declaratively with LINQ, from filtering and projection to grouping and aggregation.

Part 3: Collections & LINQ6 min readFree lesson

LINQ (Language Integrated Query) is a powerful feature of .NET that allows you to query data sources such as collections, databases, and XML declaratively, like SQL. It is used naturally in C# and supports two syntaxes: method chain or query syntax.

Learning Objectives

  • Use standard LINQ operators such as Where, Select, OrderBy, GroupBy, and Join.
  • Know the aggregate operators Sum, Average, Max, Aggregate
  • Understand the difference between method chain and query expression.
  • Know the meaning of deferred execution

Key concepts

1) Where and Select

int[] nums = [1, 2, 3, 4, 5, 6];
var result = nums.Where(n => n > 3).Select(n => n * n);
//Results: 16, 25, 36
  • Where is filtered, Select is converted.
  • Lambda (=>) is a one-line function.

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");
}
  • In descending order: OrderByDescending / ThenByDescending.
  • The result of GroupBy has elements in the group together with the sequence IGrouping<TKey, TElement> — Key.

3) Aggregation operators

operator Description
Sum, Average, Max, Min, Count General statistics
Aggregate(seed, (acc, x) => ...) Direct definition of cumulative expression
Any, All Elements that satisfy the condition are Are-there-any/all-of-them

4) Method chains vs query expressions

The same action can be used in two ways.

//method chain
var a = nums.Where(n => n > 3).Select(n => n * n);

//Query expression (SQL-like)
var b = from n in nums
        where n > 3
        select n * n;
  • In simple cases, the method chain is short and clear.
  • Query expressions such as join, let, and complex group ... by ... into are often easy to read.

5) Deferred execution

LINQ expressions are not executed immediately. It is only evaluated when iterating over foreach or when a termination operator such as ToList/ToArray is called.

var q = list.Where(n => n > 0);
list.Add(99);
foreach (var n in q) { /*99 is also included*/ }

Force evaluation to .ToList() to "snapshot" the original in place.

See as example

Example 1 — WhereSelect: Filtering and converting

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);
}

Execution result

25
36
49
64

Note: Where only passes the condition true, and Select converts each element to a different form. If you change the order, the results may change.

Example 2 — OrderByGroup: Sorting and grouping

Person[] people =
[
    new("exponent", "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()}name: {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;
}

Execution result

Busan - Minho (30)
Busan - Yoon Jae (28)
Seoul - Jisoo (25)
Seoul - Seoyeon (22)
---
[Seoul] 2 people: Jisoo, Seoyeon
[Busan] 2 people: Minho, Yoonjae

Memo: ThenBy is the secondary standard applied when the primary sorting standard is the same. IGrouping<K,T> has both Key properties and element sequences.

Example 3 — Aggregation: Aggregation operator

int[] nums = [10, 20, 30, 40, 50];

Console.WriteLine($"Sum: {nums.Sum()}");
Console.WriteLine($"Average: {nums.Average()}");
Console.WriteLine($"Maximum value: {nums.Max()}");
Console.WriteLine($"Minimum: {nums.Min()}");
Console.WriteLine($"Count: {nums.Count()}");

//Aggregate: Direct accumulation expression definition (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 numbers? {allBigger}, even existence? {anyEven}");

Execution result

Total: 150
Average: 30
Maximum: 50
Minimum: 10
Count: 5
Product: 12000000
All positive numbers? True, even numbers exist? True

Note: Aggregate(seed, func) cumulatively applies func to all elements starting from seed. Sum/Average etc. can be seen as special cases of Aggregate.

Example 4 — QuerySyntax: Method chain vs query expression

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);

//Query expression with the same meaning
var b = from n in nums
        where n % 2 == 0
        select n * n;

Console.WriteLine($"Method chain: [{string.Join(", ", a)}]");
Console.WriteLine($"Query expression: [{string.Join(", ", b)}]");

Execution result

Method chain: [4, 16, 36, 64, 100]
Query expression: [4, 16, 36, 64, 100]

Note: When compiled, both grammars result in the same code. You can choose according to your taste and readability. Even within the query expression, join, group ... by ... into, etc. can be freely combined.

Example 5 — DeferredExec: Delayed Execution Demonstration

List<int> nums = [1, 2, 3];

//Nothing to do at this point — just remember “what to do”
var query = nums.Where(n => n > 1);

nums.Add(99);   //Modifying the original after query definition

Console.Write("Lazy execution results:");
foreach (int n in query)
{
    Console.Write($"{n} ");
}
Console.WriteLine();

//Create a snapshot with ToList for immediate evaluation
var snapshot = nums.Where(n => n > 1).ToList();
nums.Add(100);
Console.WriteLine($"Snapshot result: [{string.Join(", ", snapshot)}]");

Execution result

Lazy execution result: 2 3 99
Snapshot result: [2, 3, 99]

Note: The reason the first result includes 99 is because at the moment foreach is actually executed, it looks at nums again. You can force the evaluation time with ToList()/ToArray().

Common Mistakes

  1. Where followed by Select, Select followed by Where — Lambda parameter types vary depending on the order.
  2. Forgetting delayed execution, run foreach several times and re-evaluate each time → performance loss. If you want to rewrite the results, use .ToList().
  3. Confusion between Count method and Count property: IEnumerable<T> is a method, List<T> is a property (Count).
  4. Calling First() / Single() on an empty sequence → Exception. To use safely, use FirstOrDefault().
  5. Trying to index GroupBy results like Dictionary — needs to be converted to ToDictionary(g => g.Key).

Cleanup

  • LINQ is a standard tool for querying collections in SQL style.
  • Where/Select/OrderBy/GroupBy/Aggregate are most frequently used.
  • Method chain and query expression are the same function with different expressions.
  • It is efficient thanks to lazy execution, but to prevent unexpected results, you can specify the evaluation time with .ToList().

Try it yourself

cd src/WhereSelect
dotnet run

cd ../OrderByGroup
dotnet run

cd ../Aggregation
dotnet run

cd ../QuerySyntax
dotnet run

cd ../DeferredExec
dotnet run

Example files

The runnable files used in this lesson. Copy them into your own project and experiment.

src/Aggregation/Aggregation.csprojText
<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.csC#
#nullable enable

int[] nums = [10, 20, 30, 40, 50];

Console.WriteLine($"Sum: {nums.Sum()}");
Console.WriteLine($"Average: {nums.Average()}");
Console.WriteLine($"Maximum value: {nums.Max()}");
Console.WriteLine($"Minimum: {nums.Min()}");
Console.WriteLine($"Count: {nums.Count()}");

//Aggregate: Define a direct accumulation function (here multiplication)
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 numbers? {allBigger}, even existence? {anyEven}");
src/DeferredExec/DeferredExec.csprojText
<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.csC#
#nullable enable

List<int> nums = [1, 2, 3];

//Nothing to do at this point — just remember “what to do”
var query = nums.Where(n => n > 1);

nums.Add(99);   //Change the source after defining the query

Console.Write("Lazy execution results:");
foreach (int n in query)
{
    Console.Write($"{n} ");
}
Console.WriteLine();

//Snapshot with ToList for immediate evaluation
var snapshot = nums.Where(n => n > 1).ToList();
nums.Add(100);
Console.WriteLine($"Snapshot result: [{string.Join(", ", snapshot)}]");
src/OrderByGroup/OrderByGroup.csprojText
<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.csC#
#nullable enable

Person[] people =
[
    new("exponent", "Seoul", 25),
    new("minho", "Busan", 30),
    new("Seoyeon", "Seoul", 22),
    new("Yoonjae", "Busan", 28),
];

//In ascending order by city, in the same city by descending age.
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()}name: {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.csC#
#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);

//Query expression with the same meaning (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 expression: [{string.Join(", ", b)}]");
src/QuerySyntax/QuerySyntax.csprojText
<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.csC#
#nullable enable

int[] nums = [1, 2, 3, 4, 5, 6, 7, 8];

//Where: Pass only those that satisfy the conditions, Select: Convert each
var squaresOfBig = nums.Where(n => n > 4).Select(n => n * n);

foreach (int x in squaresOfBig)
{
    Console.WriteLine(x);
}
src/WhereSelect/WhereSelect.csprojText
<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>

Assignment

Problem 1 — Student Performance Analysis

  • Reference solution: shown below
  • Core concepts: Average, OrderByDescending, First, GroupBy

Requirements

Use the following student data:

Student[] students =
[
    new("exponent", "Class A", 88),
    new("minho", "Class B", 72),
    new("Seoyeon", "Class A", 95),
    new("Yoonjae", "Class B", 65),
    new("sky", "Class A", 78),
    new("Doyun", "Class B", 90),
];

Print all of the following using only LINQ:

  1. Overall average score (1 decimal place)
  2. Name and score of the highest scoring student
  3. Average score by class (Class)

Expected output

Overall average: 81.3
Highest score: Seoyeon (95 points)
=== Average per class ===
Class A: 87.0
Class B: 75.7

Hint

  • students.Average(s => s.Score)
  • students.OrderByDescending(s => s.Score).First()
  • students.GroupBy(s => s.Class) → Call Average in each group

Problem 2 — Order Sales Statistics

  • Reference solution: shown below
  • Key concepts: Sum, GroupBy, Select, anonymous types or tuples

Requirements

Use the following order data:

Order[] orders =
[
    new("apple", 3, 1500),
    new("banana", 5, 800),
    new("apple", 2, 1500),
    new("Tangerine", 10, 500),
    new("banana", 3, 800),
];

Output the following using LINQ.

  1. Total sales (sum of Quantity * Unit Price)
  2. Sum of sales by product — Sorted in descending order of sales

Expected output

Total sales: 18,900 won
=== Sales by product ===
Apple: 7,500 won
Banana: 6,400 won
Tangerine: 5,000 won

Hint

  • Total sales as orders.Sum(o => o.Quantity * o.UnitPrice).
  • GroupBy(o => o.Product) → Select(g => new { Name = g.Key, Total = g.Sum(o => o.Quantity * o.UnitPrice) }) → OrderByDescending.
  • Thousands classification: $"{value:N0} won".

Check the correct answer

After solving it yourself, compare it with the reference solution below.

Show the reference solution

Try the assignment yourself first — comparing your attempt with a working solution is where most of the learning happens.

Homework01/Homework01.csprojText
<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>
Homework01/Program.csC#
#nullable enable

Student[] students =
[
    new("exponent", "Class A", 88),
    new("minho", "Class B", 72),
    new("Seoyeon", "Class A", 95),
    new("Yoonjae", "Class B", 65),
    new("sky", "Class A", 78),
    new("Doyun", "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($"Highest score: {top.Name} ({top.Score} points)");

Console.WriteLine("=== Average per 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;
}
Homework02/Homework02.csprojText
<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>
Homework02/Program.csC#
#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 sales: {total:N0} won");

Console.WriteLine("=== Sales 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;
}

Learn this lesson with a mentor

Work through the C# (.NET 8 LTS) track on your own, or join a guided cohort at our Pitampura campus and online.

Call NowEnroll Now