C# & .NET (C# 14, .NET 10 LTS) Module 14 – LINQ
LINQ: your first queries
Filter, sort and reshape C# collections with LINQ: Where, OrderBy and Select, the same query in query syntax, First and Single, and when a query runs.
What you will learn
- Filter, sort and reshape a list with Where, OrderBy, ThenBy and Select
- Write the same query in method syntax and in query syntax
- Choose First, FirstOrDefault, Single, Any, All or Count by what each returns or throws
- Predict when a query runs, and keep its result with ToList, ToArray or ToDictionary
Before you start
What you’ll learn · Before you start
What you will learn
- Filter, sort and reshape a list with Where, OrderBy, ThenBy and Select
- Write the same query in method syntax and in query syntax
- Choose First, FirstOrDefault, Single, Any, All or Count by what each returns or throws
- Predict when a query runs, and keep its result with ToList, ToArray or ToDictionary
Before you start
On this page
Which of 8 trains can still take a booking, in order of departure? A foreach loop answers it with a list to collect
into, an if, a sort and a second loop. LINQ: Language Integrated Query: the C# query syntax and the methods of System.Linq that filter, sort, group, join and reshape any sequence that implements IEnumerable<T>, and, through a provider such as a database library, IQueryable<T>. answers it with one expression of three steps,
Where, OrderBy and Select, and the first run below shows both giving the same six trains.
You will write queries in both of C#’s spellings, pick the operator that returns a single answer without being surprised by what it throws, and see exactly when a query runs: not on the line where you write it, but every time something reads it.
Where, OrderBy and Select describe the result, and LINQ writes the loop
first.cs holds a made-up timetable of 8 trains and asks the same question twice, once with a loop and once with
LINQ:
// The same question answered twice: which trains still have seats, in order of departure?
// The timetable is made up for this lesson: 8 trains with a number, two stations, a departure time and free seats.
List<Train> trains =
[
new(20101, "SBC", "MAS", new(6, 0), 85),
new(20102, "NDLS", "MMCT", new(16, 50), 0),
new(20103, "MAS", "SBC", new(7, 50), 3),
new(20104, "HWH", "NDLS", new(15, 20), 61),
new(20105, "SBC", "NDLS", new(20, 0), 0),
new(20106, "MMCT", "NDLS", new(17, 0), 40),
new(20107, "NDLS", "HWH", new(6, 15), 12),
new(20108, "MAS", "HWH", new(19, 45), 22),
];
// With a loop: collect the trains with seats, sort what was collected, then print each one.
Console.WriteLine("With a loop:");
var withSeats = new List<Train>();
foreach (var t in trains)
{
if (t.Seats > 0)
{
withSeats.Add(t);
}
}
withSeats.Sort((a, b) => a.Departs.CompareTo(b.Departs));
foreach (var t in withSeats)
{
Console.WriteLine($" {t.Number} at {t.Departs:HH:mm}, {t.Seats} seats");
}
// With LINQ: the same three steps, each one a method that takes a lambda and returns a new sequence.
Console.WriteLine("With LINQ:");
var query = trains
.Where(t => t.Seats > 0)
.OrderBy(t => t.Departs)
.Select(t => $"{t.Number} at {t.Departs:HH:mm}, {t.Seats} seats");
foreach (var line in query)
{
Console.WriteLine($" {line}");
}
// The query made new sequences; the list it read is as it was.
Console.WriteLine($"trains still holds {trains.Count} trains, and {trains[0].Number} is still first");
record Train(int Number, string From, string To, TimeOnly Departs, int Seats); Output
With a loop: 20101 at 06:00, 85 seats 20107 at 06:15, 12 seats 20103 at 07:50, 3 seats 20104 at 15:20, 61 seats 20106 at 17:00, 40 seats 20108 at 19:45, 22 seats With LINQ: 20101 at 06:00, 85 seats 20107 at 06:15, 12 seats 20103 at 07:50, 3 seats 20104 at 15:20, 61 seats 20106 at 17:00, 40 seats 20108 at 19:45, 22 seats trains still holds 8 trains, and 20101 is still first
Recorded with .NET SDK 10.0.401 (runtime 10.0.12) on macOS 26 arm64. To run it yourself: dotnet run first.cs
Both halves printed the same six lines. The LINQ half says what the result is, and each step is a method that takes a
lambda expression: An unnamed method written where a delegate is expected, such as x => x * 2: parameters, the => operator and a body that is one expression or a block of statements. More in “Lambdas and closures”: Where(t => t.Seats > 0) keeps the trains whose lambda returns true,
OrderBy(t => t.Departs) sorts them by the key its lambda returns, and Select turns each train into the text its
lambda builds. Each method returns a new sequence, and the next method reads that one. Step through what each stage
hands on:
The drawing shows the whole scene; its 5 steps are listed below.
Every step as text
- The source. The list holds 8 trains in the order they were added: 20101 first, 20108 last. The query will read it, and nothing in the query writes to it.
- Where filters. Where calls its lambda once for each train and keeps the trains for which it returns true. 20102 and 20105 have 0 seats, so 6 of the 8 trains go on.
- OrderBy sorts. OrderBy reads the key its lambda returns, the departure time, and hands the 6 trains on from the earliest: 20101 at 06:00, then 20107 at 06:15, then 20103 at 07:50.
- Select reshapes. Select turns each train into the text its lambda builds, such as "20101 at 06:00, 85 seats". It hands on exactly one result per train: still 6.
- The loop reads it. The foreach prints the 6 lines, the same lines the loop version printed. The list still holds its 8 trains in their first order, as the last line of the run says.
These methods are the standard query operator: One of the LINQ methods of the Enumerable class, such as Where, Select, OrderBy, GroupBy, Join or Count. They are extension methods on IEnumerable<T>, so any array, list or iterator can call them., the methods of the
Enumerable class, from Where and Select to GroupBy, Join and Count. They are extension method: A static method of a static class whose first parameter is marked this, such as WordCount(this string text). Callers write text.WordCount(), and the compiler turns the call into TextExtensions.WordCount(text). More in “Extension methods and C# 14 extension members” on
IEnumerable<T> in the System.Linq namespace, so an array, a List<T>, a dictionary or one of your own iterators
can call them. Console projects and file-based apps import System.Linq without a using line, because it is one of
the .NET SDK’s implicit usings: Global using directives that the SDK adds to a project, so its files can use types of namespaces such as System, System.IO and System.Linq without writing using lines. More in “Your first C# program as a file-based app”.
ThenBy adds a second sort key under the first: OrderBy(t => t.From).ThenBy(t => t.Departs) sorts by station, and
by departure among the trains of one station. The next section uses it.
Calling Where and expecting the list to change
trains.Where(t => t.Seats > 0); on a line of its own changes nothing: it returns a new sequence, and the line
throws it away, as the last line of the run shows by counting 8 trains. Keep the result,
var withSeats = trains.Where(t => t.Seats > 0).ToList();, or change the list itself with a method of the list,
trains.RemoveAll(t => t.Seats == 0);.
Checkpoint
Check: Where, OrderBy, ThenBy and Select
Query syntax is the same query, with from first and select last
C# has a second way to write a query: query syntax: The form of a LINQ query that starts with from and ends with select or group, such as from t in trains where t.Seats > 0 select t.Number. The compiler turns it into calls of the standard query operators., clauses that read like SQL. The
compiler translates it into calls of the same methods, so the choice between the two spellings is about reading, not
about what runs. syntax.cs writes one query both ways, in method syntax: The form of a LINQ query written as a chain of method calls that take lambdas, such as trains.Where(t => t.Seats > 0).Select(t => t.Number). and in query
syntax, and compares the results:
// One query written twice: once with methods, once with query syntax. The compiler turns the second form into
// calls of the same methods, so both give the same trains in the same order.
List<Train> trains =
[
new(20101, "SBC", "MAS", new(6, 0), 85),
new(20102, "NDLS", "MMCT", new(16, 50), 0),
new(20103, "MAS", "SBC", new(7, 50), 3),
new(20104, "HWH", "NDLS", new(15, 20), 61),
new(20105, "SBC", "NDLS", new(20, 0), 0),
new(20106, "MMCT", "NDLS", new(17, 0), 40),
new(20107, "NDLS", "HWH", new(6, 15), 12),
new(20108, "MAS", "HWH", new(19, 45), 22),
];
// Method syntax: trains with seats, by station, the most seats first within a station.
var methods = trains
.Where(t => t.Seats > 0)
.OrderBy(t => t.From)
.ThenByDescending(t => t.Seats)
.Select(t => $"{t.From,-4} {t.Number} {t.Seats,2} seats");
// Query syntax: from first, select last; "orderby a, b descending" is OrderBy, then ThenByDescending.
var clauses =
from t in trains
where t.Seats > 0
orderby t.From, t.Seats descending
select $"{t.From,-4} {t.Number} {t.Seats,2} seats";
Console.WriteLine("Method syntax:");
foreach (var line in methods)
{
Console.WriteLine($" {line}");
}
Console.WriteLine("Query syntax:");
foreach (var line in clauses)
{
Console.WriteLine($" {line}");
}
Console.WriteLine($"Same lines, same order: {methods.SequenceEqual(clauses)}");
// let names a value worked out once per train, so the where and the select can both use it.
var nearlyFull =
from t in trains
let left = t.Seats == 0 ? "full" : $"{t.Seats} left"
where t.Seats < 15
orderby t.Number
select $"{t.Number}: {left}";
Console.WriteLine($"Under 15 seats: {string.Join(", ", nearlyFull)}");
// Count has no clause of its own: a query expression in brackets, then the method.
int toDelhi = (from t in trains where t.To == "NDLS" select t).Count();
Console.WriteLine($"Trains to NDLS: {toDelhi}");
record Train(int Number, string From, string To, TimeOnly Departs, int Seats); Output
Method syntax: HWH 20104 61 seats MAS 20108 22 seats MAS 20103 3 seats MMCT 20106 40 seats NDLS 20107 12 seats SBC 20101 85 seats Query syntax: HWH 20104 61 seats MAS 20108 22 seats MAS 20103 3 seats MMCT 20106 40 seats NDLS 20107 12 seats SBC 20101 85 seats Same lines, same order: True Under 15 seats: 20102: full, 20103: 3 left, 20105: full, 20107: 12 left Trains to NDLS: 3
Recorded with .NET SDK 10.0.401 (runtime 10.0.12) on macOS 26 arm64. To run it yourself: dotnet run syntax.cs
SequenceEqual confirmed what the six lines show: the same trains in the same order. orderby t.From, t.Seats descending became OrderBy, then ThenByDescending, which is why 20108 with 22 seats comes before 20103 with 3 at
the station MAS. Each clause maps to a method:
| Clause | The method it becomes | In syntax.cs |
|---|---|---|
from t in trains |
none: it names the source and the range variable t |
every query |
where t.Seats > 0 |
Where(t => t.Seats > 0) |
the first query |
orderby t.From, t.Seats descending |
OrderBy, then ThenByDescending |
the first query |
select … |
Select(t => …) |
every query |
let left = … |
a Select that carries left beside t |
nearlyFull |
group … by …, join … on … equals … |
GroupBy, Join (later lessons of this module) |
none |
let names a value worked out once per train, so the where and the select after it can both use it. A query
must start with from and end with select or group, which Microsoft’s introduction points out is the reverse of
the order of the clauses in SQL. Many operators have no clause at all: Count, First, Distinct, Take and
Sum are always method calls. Put the query expression in brackets and call the method on it, as the line that
counted 3 trains to NDLS did.
Checkpoint
Check: Query syntax and the methods it becomes
First, Single, Any and Count return one answer, and fail in different ways
Some operators return a single value instead of a sequence. pick.cs tries four of them on three stations: one with
no train, one with exactly one and one with two:
// Operators that return one answer instead of a sequence, tried on a station with no train, one train and two.
List<Train> trains =
[
new(20101, "SBC", "MAS", new(6, 0), 85),
new(20102, "NDLS", "MMCT", new(16, 50), 0),
new(20103, "MAS", "SBC", new(7, 50), 3),
new(20104, "HWH", "NDLS", new(15, 20), 61),
new(20105, "SBC", "NDLS", new(20, 0), 0),
new(20106, "MMCT", "NDLS", new(17, 0), 40),
new(20107, "NDLS", "HWH", new(6, 15), 12),
new(20108, "MAS", "HWH", new(19, 45), 22),
];
string[] stations = ["PUNE", "HWH", "SBC"]; // trains from them: none, one, two
foreach (var station in stations)
{
Console.WriteLine($"Trains from {station}:");
Show("First", () => trains.First(t => t.From == station));
Show("FirstOrDefault", () => trains.FirstOrDefault(t => t.From == station));
Show("Single", () => trains.Single(t => t.From == station));
Show("SingleOrDefault", () => trains.SingleOrDefault(t => t.From == station));
}
// A default value is a value: 0 seats from FirstOrDefault looks exactly like a full train.
var seats = trains.Select(t => t.Seats);
Console.WriteLine($"FirstOrDefault(s => s > 100): {seats.FirstOrDefault(s => s > 100)}");
Console.WriteLine($"FirstOrDefault(s => s == 0): {seats.FirstOrDefault(s => s == 0)}");
Console.WriteLine($"FirstOrDefault(s => s > 100, -1): {seats.FirstOrDefault(s => s > 100, -1)}");
// Questions with a yes, a no or a number for an answer.
Console.WriteLine($"Any(t => t.Seats == 0): {trains.Any(t => t.Seats == 0)}");
Console.WriteLine($"All(t => t.Seats < 100): {trains.All(t => t.Seats < 100)}");
Console.WriteLine($"Count(t => t.To == \"NDLS\"): {trains.Count(t => t.To == "NDLS")}");
List<Train> none = [];
Console.WriteLine($"On an empty list: Any() {none.Any()}, All(t => t.Seats > 1000) {none.All(t => t.Seats > 1000)}, Count() {none.Count()}");
static void Show(string name, Func<Train?> pick)
{
try
{
Console.WriteLine($" {name,-16} {pick()?.Number.ToString() ?? "null"}");
}
catch (InvalidOperationException e)
{
Console.WriteLine($" {name,-16} {e.GetType().Name}: {e.Message}");
}
}
record Train(int Number, string From, string To, TimeOnly Departs, int Seats); Output
Trains from PUNE: First InvalidOperationException: Sequence contains no matching element FirstOrDefault null Single InvalidOperationException: Sequence contains no matching element SingleOrDefault null Trains from HWH: First 20104 FirstOrDefault 20104 Single 20104 SingleOrDefault 20104 Trains from SBC: First 20101 FirstOrDefault 20101 Single InvalidOperationException: Sequence contains more than one matching element SingleOrDefault InvalidOperationException: Sequence contains more than one matching element FirstOrDefault(s => s > 100): 0 FirstOrDefault(s => s == 0): 0 FirstOrDefault(s => s > 100, -1): -1 Any(t => t.Seats == 0): True All(t => t.Seats < 100): True Count(t => t.To == "NDLS"): 3 On an empty list: Any() False, All(t => t.Seats > 1000) True, Count() 0
Recorded with .NET SDK 10.0.401 (runtime 10.0.12) on macOS 26 arm64. To run it yourself: dotnet run pick.cs
The run fills this table, and each cell is a line it printed:
| Operator | No match (PUNE) | One match (HWH) | Two matches (SBC) |
|---|---|---|---|
First |
throws InvalidOperationException |
20104 | 20101, the first |
FirstOrDefault |
null, the default |
20104 | 20101, the first |
Single |
throws InvalidOperationException |
20104 | throws: more than one |
SingleOrDefault |
null, the default |
20104 | throws: more than one |
First returns the first match and fails only when there is none. Single is a check as well as a search: it
throws unless exactly one item matches, so use it when a second match would mean the data is wrong, such as two
trains with one number. The OrDefault forms return default instead of throwing when nothing matches: null for a
record or a string, and 0 for an int.
That 0 is the trap of the next lines. No train has more than 100 seats, so FirstOrDefault(s => s > 100) returned
0, the same 0 that FirstOrDefault(s => s == 0) returned for a real full train. .NET 6 added overloads with the
value to return when nothing matches, and FirstOrDefault(s => s > 100, -1) gave -1, which no train can have.
Using the result of FirstOrDefault without checking it
trains.FirstOrDefault(t => t.From == "PUNE").Number throws NullReferenceException, because there is no train
and the result is null; with nullable reference types on, the compiler warns about the line first. Check the
result, if (trains.FirstOrDefault(t => t.From == "PUNE") is { } train), or use First when a missing train is
a bug that should stop the program.
Any answers whether at least one item matches, and stops at the first one it finds. All answers whether every
item matches; on an empty list it returned True, because no item fails the test. Count reads the whole sequence
and returns how many matched.
Key idea
Use First when any match will do, Single when there must be exactly one, and an OrDefault form only when no
match is a normal case that the next line handles.
Checkpoint
Check: First, Single, Any, All and Count
A query runs each time it is read, and ToList keeps one result
A query variable holds the steps of the query, not its results. Count(), a foreach or ToList() runs the steps
on the source as it is at that moment, which is called deferred execution: Running work only when its results are read. Calling an iterator method, or building a LINQ query, runs none of the work; it happens as a loop reads the sequence, and again for every new loop over it. More in “Iterators: IEnumerable, yield and lazy sequences”.
deferred.cs adds trains to the list after the query is written and watches the lambdas run:
// When does a query run? Each time something reads it, and only as far as the reader asks.
List<Train> trains =
[
new(20101, "SBC", "MAS", new(6, 0), 85),
new(20102, "NDLS", "MMCT", new(16, 50), 0),
new(20103, "MAS", "SBC", new(7, 50), 3),
new(20104, "HWH", "NDLS", new(15, 20), 61),
new(20105, "SBC", "NDLS", new(20, 0), 0),
new(20106, "MMCT", "NDLS", new(17, 0), 40),
new(20107, "NDLS", "HWH", new(6, 15), 12),
new(20108, "MAS", "HWH", new(19, 45), 22),
];
// 1. A query variable holds the recipe, not the trains, so it sees trains added after it was written.
var withSeats = trains.Where(t => t.Seats > 0);
Console.WriteLine($"Count() of the query: {withSeats.Count()}");
trains.Add(new(20109, "SBC", "HWH", new(9, 30), 30));
Console.WriteLine($"After adding 20109, Count() of the same query: {withSeats.Count()}");
// 2. ToList runs the query once and keeps what it found; the query itself keeps reading the list.
var kept = withSeats.ToList();
trains.Add(new(20110, "HWH", "MAS", new(22, 10), 5));
Console.WriteLine($"After adding 20110: the query finds {withSeats.Count()}, the list kept by ToList holds {kept.Count}");
// 3. Each train goes through the whole query before the next one is read: Where, then Select, then the loop.
List<Train> four = [trains[0], trains[1], trains[2], trains[3]];
var logged = four
.Where(t => { Console.WriteLine($" Where reads {t.Number}"); return t.Seats < 50; })
.Select(t => { Console.WriteLine($" Select gets {t.Number}"); return t.Number; });
Console.WriteLine("logged is written, and nothing has run yet. Now the foreach:");
foreach (int number in logged)
{
Console.WriteLine($" foreach got {number}");
}
// 4. OrderBy has to see every train before it can hand out the first one.
var sorted = four
.Where(t => { Console.WriteLine($" Where reads {t.Number}"); return t.Seats < 50; })
.OrderBy(t => t.Seats)
.Select(t => t.Number);
Console.WriteLine("With OrderBy in the middle:");
foreach (int number in sorted)
{
Console.WriteLine($" foreach got {number}");
}
// 5. ToDictionary also runs at once, and stops at the first key it has seen before.
var byNumber = trains.ToDictionary(t => t.Number);
Console.WriteLine($"ToDictionary by number: {byNumber.Count} trains, 20104 leaves {byNumber[20104].From}");
try
{
_ = trains.ToDictionary(t => t.From);
}
catch (ArgumentException e)
{
Console.WriteLine($"ToDictionary by station: {e.GetType().Name}: {e.Message}");
}
record Train(int Number, string From, string To, TimeOnly Departs, int Seats); Output
Count() of the query: 6 After adding 20109, Count() of the same query: 7 After adding 20110: the query finds 8, the list kept by ToList holds 7 logged is written, and nothing has run yet. Now the foreach: Where reads 20101 Where reads 20102 Select gets 20102 foreach got 20102 Where reads 20103 Select gets 20103 foreach got 20103 Where reads 20104 With OrderBy in the middle: Where reads 20101 Where reads 20102 Where reads 20103 Where reads 20104 foreach got 20102 foreach got 20103 ToDictionary by number: 10 trains, 20104 leaves HWH ToDictionary by station: ArgumentException: An item with the same key has already been added. Key: SBC
Recorded with .NET SDK 10.0.401 (runtime 10.0.12) on macOS 26 arm64. To run it yourself: dotnet run deferred.cs
The same query counted 6 trains, then 7 once 20109 had been added: it read the list again. ToList() ran it once and
kept the 7 trains it found, so after 20110 was added the query found 8 and the kept list still held 7. Microsoft’s
introduction to LINQ calls this the difference between deferred and immediate execution: operators that return a
single value, such as Count and First, and ToList, ToArray and ToDictionary run the query at once; almost
all the operators that return a sequence wait until it is read.
Part 3 of the run shows the order of the work. Step through the first loop and watch which lambda runs:
The drawing shows the whole scene; its 7 steps are listed below.
Every step as text
- Nothing has run. The query is written and stored in logged, yet no line of Where or Select has printed. The foreach is the first thing that asks it for an item.
- The loop asks. The foreach asks Select for its first item. Select has none of its own, so it asks Where.
- Select asks Where. Where reads trains from the list until its lambda returns true. It reads 20101, which has 85 seats, and goes on to 20102, which has 0.
- 20102 goes up. Where hands 20102 to Select, which prints "Select gets 20102" and turns the train into its number.
- The loop gets 20102. Select hands 20102 to the loop, which prints "foreach got 20102". 20103 and 20104 have not been read yet.
- The next item. The loop asks again, and the same chain runs for one more train: Where reads 20103, which has 3 seats, and it travels up to the loop.
- The end. On the third request Where reads 20104, which has 61 seats, and finds no train after it, so the loop ends. Each train that passed went the whole way to the loop before the next was read.
Each train that passed the test went through Where, then Select, then the loop, before the next train was read.
Where and Select are streaming operator: A LINQ operator that hands on each result as soon as it has read enough of its source, such as Where or Select. A non-streaming operator, such as OrderBy or GroupBy, reads the whole source before it hands on the first result.: they hand on each result as soon as
they have it. Part 4 put OrderBy in the middle, and all four “Where reads” lines came first, because a sort must see
every train before it can hand on the one with the fewest seats. GroupBy and the other operators that sort or group
behave the same way.
Coming from Java
Java’s streams are lazy too, but the Stream documentation says a stream should be operated on only once, and a
reused stream may throw IllegalStateException. A LINQ query can be read any number of times, and every read runs
it again, from the start, on the source as it is then.
Keep a result when you need the same answer twice or the source may change: ToList() or ToArray() for a list or
an array, ToDictionary to look items up by a key. The last lines of the run show ToDictionary finding train 20104
by its number, and stopping with ArgumentException when two trains share a station, because a dictionary holds one
item per key.
Key idea
A query is a recipe that runs each time something reads it, on the source as it is then; ToList, ToArray and
ToDictionary run it once and keep what it found.
Checkpoint
Check: When a query runs, and keeping a result
Practise: questions about a rainfall table
rainfall.cs holds synthetic monthly rainfall for three cities, one reading missing, and asks five questions about
them, each a method for you to write with the operators of this lesson. Until you write their bodies, all 8 checks
fail:
// Practice: write the five methods at the bottom with LINQ so that every check passes, then run this file again.
// The readings are synthetic: monthly rainfall in millimetres for three cities. null is a missing reading.
Reading[] readings =
[
new("Pune", "Jun", 140), new("Pune", "Jul", 190), new("Pune", "Aug", 120),
new("Pune", "Sep", 115), new("Pune", "Oct", 70), new("Pune", "Nov", 15),
new("Chennai", "Jun", 50), new("Chennai", "Jul", 85), new("Chennai", "Aug", 120),
new("Chennai", "Sep", 115), new("Chennai", "Oct", 270), new("Chennai", "Nov", 360),
new("Guwahati", "Jun", 310), new("Guwahati", "Jul", null), new("Guwahati", "Aug", 260),
new("Guwahati", "Sep", 180), new("Guwahati", "Oct", 90), new("Guwahati", "Nov", 12),
];
var checks = new List<(string Name, Func<string> Run, string Expected)>
{
("the wettest reading", () => Wettest(readings), "Chennai Nov 360"),
("dry months in Pune, under 20 mm", () => string.Join(", ", DryMonths(readings, "Pune", 20)), "Nov"),
("dry months in Guwahati, under 100 mm", () => string.Join(", ", DryMonths(readings, "Guwahati", 100)), "Oct, Nov"),
("any reading above 350 mm", () => AnyAbove(readings, 350).ToString(), "True"),
("any reading above 400 mm", () => AnyAbove(readings, 400).ToString(), "False"),
("missing readings", () => Missing(readings).ToString(), "1"),
("first month above 100 mm in Chennai", () => FirstAbove(readings, "Chennai", 100), "Aug"),
("first month above 200 mm in Pune", () => FirstAbove(readings, "Pune", 200), "none"),
};
int passed = 0;
foreach (var (name, run, expected) in checks)
{
string verdict;
try
{
string got = run();
verdict = got == expected ? "ok" : $"FAIL, got \"{got}\"";
}
catch (Exception e)
{
verdict = $"FAIL, {e.GetType().Name}";
}
if (verdict == "ok")
{
passed++;
}
Console.WriteLine($"{name}: {verdict}");
}
Console.WriteLine($"{passed} of {checks.Count} checks pass");
return passed == checks.Count ? 0 : 1;
// "City Month mm" of the reading with the most rain.
static string Wettest(IEnumerable<Reading> readings) =>
throw new NotImplementedException("Sort the readings by Mm, highest first, and take the first one.");
// The months of one city with less rain than below, in the order of the readings; a missing reading is not dry.
static string[] DryMonths(IEnumerable<Reading> readings, string city, int below) =>
throw new NotImplementedException("Keep the readings of the city under below, then select their months.");
// Whether any reading is above mm.
static bool AnyAbove(IEnumerable<Reading> readings, int mm) =>
throw new NotImplementedException("One operator answers this with true or false.");
// How many readings are missing.
static int Missing(IEnumerable<Reading> readings) =>
throw new NotImplementedException("Count the readings whose Mm is null.");
// The first month of a city above mm, or "none".
static string FirstAbove(IEnumerable<Reading> readings, string city, int mm) =>
throw new NotImplementedException("Filter, select the month, then FirstOrDefault with a default value.");
record Reading(string City, string Month, int? Mm); Output (exit status 1)
the wettest reading: FAIL, NotImplementedException dry months in Pune, under 20 mm: FAIL, NotImplementedException dry months in Guwahati, under 100 mm: FAIL, NotImplementedException any reading above 350 mm: FAIL, NotImplementedException any reading above 400 mm: FAIL, NotImplementedException missing readings: FAIL, NotImplementedException first month above 100 mm in Chennai: FAIL, NotImplementedException first month above 200 mm in Pune: FAIL, NotImplementedException 0 of 8 checks pass
Recorded with .NET SDK 10.0.401 (runtime 10.0.12) on macOS 26 arm64. To run it yourself: dotnet run rainfall.cs
Answer the five questions
Copy rainfall.cs and replace each throw with a query: OrderByDescending and First for the wettest reading,
Where and Select for the dry months, Any, Count, and FirstOrDefault with a default value for the last one.
A missing reading is null, and r.Mm < below is false for it, so it never counts as dry. Run
dotnet run rainfall.cs until it prints 8 of 8 checks pass.
Then try the questions below. They begin with whichever idea your answers so far show you know least, and each choice explains itself, right or wrong:
Practice that adapts to you
Practise this lesson
12 questions about 4 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.
Interview questions
Fresher · What is the difference between First, FirstOrDefault and Single in LINQ?
All three return one item instead of a sequence, and they differ in what they do when the number of matches is not
one. First returns the first match and throws InvalidOperationException when there is none. FirstOrDefault
returns the first match, or the type’s default when there is none: null for a reference type, 0 for an int,
which you must then tell apart from a real 0; .NET 6 added an overload that returns a value you choose instead. Single
throws both when there is no match and when there is more than one, so it also checks the data; SingleOrDefault
allows no match but still throws for two. Use First when any match will do and Single when a second match means
the data is wrong.
Model answers open with any pass: ₹79 for 1 week ($2.99), one payment that never renews.
-
Fresher to mid
A checkout looks up the customer with customers.Single(c => c.Email == email). Since a bulk import, it throws "Sequence contains more than one matching element" for a few users. What does the error tell you, and what do you change, in the code and in the data?
-
Mid to senior
A service builds var overdue = invoices.Where(i => i.Due < today) once at start-up, where today is a local, and reads overdue on every request. New invoices show up in it, yet after midnight the invoices due the day before are still missing. Explain both behaviours and fix the code.
Key takeaways
Wherekeeps the items its lambda accepts,OrderByandThenBysort by keys, andSelectturns each item into a new value; each returns a new sequence and leaves the source as it was.- Query syntax, from
fromtoselect, compiles to calls of the same methods; operators without a clause, such asCountandFirst, are method calls on the query in brackets. Firstthrows when nothing matches,Singlealso throws for a second match, and theOrDefaultforms return a default that you must handle, or the value you pass since .NET 6.- A query runs each time it is read, on the source as it is then, one item at a time through streaming operators;
ToList,ToArrayandToDictionaryrun it once and keep the result.
Terms in this lesson
- LINQ
- Language Integrated Query: the C# query syntax and the methods of System.Linq that filter, sort, group, join and reshape any sequence that implements IEnumerable<T>, and, through a provider such as a database library, IQueryable<T>.
- method syntax
- The form of a LINQ query written as a chain of method calls that take lambdas, such as trains.Where(t => t.Seats > 0).Select(t => t.Number).
- query syntax
- The form of a LINQ query that starts with from and ends with select or group, such as from t in trains where t.Seats > 0 select t.Number. The compiler turns it into calls of the standard query operators.
- standard query operator
- One of the LINQ methods of the Enumerable class, such as Where, Select, OrderBy, GroupBy, Join or Count. They are extension methods on IEnumerable<T>, so any array, list or iterator can call them.
- streaming operator
- A LINQ operator that hands on each result as soon as it has read enough of its source, such as Where or Select. A non-streaming operator, such as OrderBy or GroupBy, reads the whole source before it hands on the first result.
References
- Language Integrated Query (LINQ) (Microsoft Learn)
- Introduction to LINQ queries (Microsoft Learn)
- Write LINQ queries (query syntax and method syntax) (Microsoft Learn)
- Query expression basics (Microsoft Learn)
- Standard query operators overview (Microsoft Learn)
- Enumerable class (Microsoft Learn)
- Enumerable.FirstOrDefault method (Microsoft Learn)
- Enumerable.OrderBy method (Microsoft Learn)
- .NET project SDK overview: implicit using directives (Microsoft Learn)
- Interface Stream<T> (Java SE 25) (Oracle)
Related tools
Report a problem with this lesson
Kept only in this browser. Your Learn progress