Your country

Tools that support it use your country for local currency, number formats, units and paper size. Your choice is saved only in this browser.

Type a name or a two-letter code. Use the up and down arrow keys to move through the countries, Enter to choose one and Escape to close.

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.

  • Beginner
  • 15 minutes
  • Examples run with .NET 10.0.401
  • By MySmartCoPilot
Free lesson, Pro answers: 15 minutes
Free: the whole lesson. With a pass: the model answers to its 2 interview questions.

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:

first.cs: the same question with a loop and with LINQ C# · first/first.cs
// 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:

Interactive · stage by stage The 8 trains of first.cs, through Where, OrderBy and Select Step through the query. Each stage takes the sequence the stage above it hands on and hands on a new one.
The 8 trains of first.cs, through Where, OrderBy and Select, step 5 of 5. Showing trains, Where(t => t.Seats > 0) (6 of 8 kept), OrderBy(t => t.Departs) (06:00 first, 19:45 last), Select(t => a line) (6 lines of text), foreach. In focus: foreach.trains8 trains in a ListWhere(t => t.Seats > 0)6 of 8 keptOrderBy(t => t.Departs)06:00 first, 19:45 lastSelect(t => a line)6 lines of textforeachprints each line

The drawing shows the whole scene; its 5 steps are listed below.

Every step as text
  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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

  1. Question 1 of 2 In first.cs, 2 of the 8 trains have 0 seats and the LINQ half printed 6 lines. If train 20103 sold its last 3 seats, how many lines would the same query print?

    Type a number.

    A hint

    Where keeps only the trains whose lambda, t.Seats > 0, returns true.

    Show the answer: Question 1 of 2

    Answer: 5 lines

    With 20103 at 0 seats, three trains fail the test t.Seats > 0, so Where hands on 5 trains. OrderBy sorts those 5 and Select makes one line from each: 5 lines.

  2. Question 2 of 2 Select reads 6 trains. How many results does it hand on?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: 6: exactly one result for each item it reads

    Select is a projection: one result per item, in the same order. Filtering is Where's job, and sorting is OrderBy's.

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:

syntax.cs: one query in method syntax and in query syntax C# · syntax/syntax.cs
// 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.

Run SQL on CSV & Excel Files Ask a CSV file the same kind of question in SQL, with WHERE and ORDER BY, and compare the clauses with LINQ's.

Checkpoint

Check: Query syntax and the methods it becomes

  1. Question 1 of 2 Which method calls does the clause orderby t.To descending, t.Number become?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: OrderByDescending(t => t.To).ThenBy(t => t.Number)

    syntax.cs showed the same rule: orderby t.From, t.Seats descending became OrderBy, then ThenByDescending, and SequenceEqual found the two queries' lines identical.

  2. Question 2 of 2 How do you count the trains to NDLS with query syntax?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Put the query expression in brackets and call Count() on it

    Count, First, Distinct, Take and Sum are always method calls. Query syntax and method syntax mix freely, because the query expression is itself a sequence the method can be called on.

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:

pick.cs: First, Single and their OrDefault forms, then Any, All and Count C# · pick/pick.cs
// 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

  1. Question 1 of 2 Two trains leave MAS. What does trains.Single(t => t.From == "MAS") do?

    Choose one answer.

    Show the answer: Question 1 of 2

    Answer: It throws InvalidOperationException, because more than one train matches

    Single throws both for no match and for two or more. That makes it a check: use it where a second match means the data is wrong.

  2. Question 2 of 2 none is an empty list of trains. What does none.All(t => t.Seats > 1000) return?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: True

    For an empty sequence Any is false and All is true. Check Any() first when "all of none" should not count as a yes.

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:

deferred.cs: when a query runs, and what ToList keeps C# · deferred/deferred.cs
// 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:

Interactive · message by message Who runs when, as a foreach reads the logged query Step through the foreach of deferred.cs. The loop is on the left, Select in the middle and Where, which reads the list, on the right.
Who runs when, as a foreach reads the logged query, step 7 of 7. Messages so far: foreach to Select: next item?; Select to Where: next item?; Where to Select: train 20102; Select to foreach: 20102; Where to Select: train 20103; Select to foreach: no more items.foreachSelectt => t.NumberWhereread 20104: endnext item?next item?train 2010220102train 20103no more items

The drawing shows the whole scene; its 7 steps are listed below.

Every step as text
  1. 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.
  2. The loop asks. The foreach asks Select for its first item. Select has none of its own, so it asks Where.
  3. 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.
  4. 20102 goes up. Where hands 20102 to Select, which prints "Select gets 20102" and turns the train into its number.
  5. The loop gets 20102. Select hands 20102 to the loop, which prints "foreach got 20102". 20103 and 20104 have not been read yet.
  6. 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.
  7. 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

  1. Question 1 of 2 withSeats = trains.Where(t => t.Seats > 0) counted 6 trains. Then a train with 30 seats and a train with 0 seats are added to trains. What does withSeats.Count() return now?

    Type a number.

    A hint

    The query reads the list again when Count runs.

    Show the answer: Question 1 of 2

    Answer: 7 trains

    Count runs the query on the list as it is now: the 6 trains with seats, plus the new one with 30 seats. The new train with 0 seats fails the test, so the answer is 7.

  2. Question 2 of 2 var logged = four.Where(t => { Console.WriteLine("Where reads " + t.Number); return t.Seats < 50; }); What has this line printed when it has run?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: Nothing: the lambda runs only when something reads logged

    A query variable holds the steps. They run when the query is enumerated, one train at a time for Where.

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:

rainfall.cs: eight checks of five LINQ queries C# · practice/rainfall.cs
// 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.

  1. Question 1 of 12 Which operator keeps the items for which its lambda returns true?

    Choose one answer.

    Show the answer: Question 1 of 12

    Answer: Where

    Where filters, OrderBy and ThenBy sort, Select reshapes. Each returns a new sequence for the next step to read.

  2. Question 2 of 12 You want the trains sorted by their station, and the trains of one station by departure. Which query is it?

    Choose one answer.

    Show the answer: Question 2 of 12

    Answer: trains.OrderBy(t => t.From).ThenBy(t => t.Departs)

    The first sort key goes in OrderBy, and each key after it in a ThenBy, in order of importance.

  3. Question 3 of 12 A colleague writes trains.Select(t => t.Number).Where(n => n.Seats > 0) and the build fails. Why?

    Choose one answer.

    Show the answer: Question 3 of 12

    Answer: After Select the items are numbers, so the Where lambda receives an int, which has no Seats

    Write the steps in the order the data needs them: filter and sort while you still have the trains, and shape them into numbers or text last, as first.cs did.

  4. Question 4 of 12 What does the C# compiler do with a query written in query syntax?

    Choose one answer.

    Show the answer: Question 4 of 12

    Answer: It turns it into calls of the standard query operators, such as Where and Select

    Microsoft's guide to writing LINQ queries says the compiler translates query syntax into method calls, and that the two are semantically identical.

  5. Question 5 of 12 In syntax.cs, what does let left = t.Seats == 0 ? "full" : $"{t.Seats} left" do?

    Choose one answer.

    Show the answer: Question 5 of 12

    Answer: It works out a value once per train and lets the clauses after it use that value

    let introduces a new range variable, computed for each item. The compiler carries it next to t through the rest of the query.

  6. Question 6 of 12 Which query expression gives the same lines as this method chain?

    Read the code, then choose one answer.

    var numbers = trains
        .Where(t => t.To == "NDLS")
        .OrderByDescending(t => t.Seats)
        .Select(t => t.Number);
    Show the answer: Question 6 of 12

    Answer: from t in trains where t.To == "NDLS" orderby t.Seats descending select t.Number

    Read the chain from top to bottom and write one clause per method: from names the source, then where, orderby and select in the same order.

  7. Question 7 of 12 What does First do when no item matches its lambda?

    Choose one answer.

    Show the answer: Question 7 of 12

    Answer: It throws InvalidOperationException

    First throws when there is nothing to return. Use FirstOrDefault when no match is a normal case, and handle its default.

  8. Question 8 of 12 seats is the sequence of the 8 trains' seat counts, and no train has more than 100. What does seats.FirstOrDefault(s => s > 100) return?

    Type a number.

    A hint

    The OrDefault forms return the default of the element type when nothing matches.

    Show the answer: Question 8 of 12

    Answer: 0

    The default of int is 0, so pick.cs printed 0, the same value it printed for a real full train with FirstOrDefault(s => s == 0). FirstOrDefault(s => s > 100, -1) returns -1 instead.

  9. Question 9 of 12 Every booking reference must appear once in today's list; a second one means an upstream bug that must not go unnoticed. Which call finds the booking for a reference?

    Choose one answer.

    Show the answer: Question 9 of 12

    Answer: bookings.Single(b => b.Ref == reference)

    Choose by what a wrong count means. When a second match is a bug, Single makes the bug fail loudly where it is found.

  10. Question 10 of 12 When does a query such as trains.Where(t => t.Seats > 0) run?

    Choose one answer.

    Show the answer: Question 10 of 12

    Answer: Each time something reads it: a foreach, Count, ToList or another operator that runs it

    A query returning a sequence runs when it is enumerated, and again on every new enumeration. ToList and ToArray run it once and keep the result.

  11. Question 11 of 12 kept = withSeats.ToList() holds 7 trains. Then two trains with seats are added to trains. How many trains does kept hold?

    Type a number.

    A hint

    ToList ran the query once, when it was called.

    Show the answer: Question 11 of 12

    Answer: 7 trains

    kept is a list of the 7 trains the query found when ToList ran. Adding to trains changes what the query would find next time, not the list that was already made, as deferred.cs showed with 8 against 7.

  12. Question 12 of 12 In part 4 of deferred.cs, all four "Where reads" lines printed before the first "foreach got". Why, when part 3 interleaved them?

    Choose one answer.

    Show the answer: Question 12 of 12

    Answer: OrderBy must read every item before it can say which comes first

    Where and Select stream: each item goes through before the next is read. OrderBy and GroupBy are nonstreaming: they read the whole source when the first item is asked for.

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.

  1. 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?

    Model answer, Pro. Model answers open with any pass; prices are in the notice on this page. First step and follow-ups

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

    Model answer, Pro. First step and follow-ups

Key takeaways

  • Where keeps the items its lambda accepts, OrderBy and ThenBy sort by keys, and Select turns each item into a new value; each returns a new sequence and leaves the source as it was.
  • Query syntax, from from to select, compiles to calls of the same methods; operators without a clause, such as Count and First, are method calls on the query in brackets.
  • First throws when nothing matches, Single also throws for a second match, and the OrDefault forms 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, ToArray and ToDictionary run 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

Related tools

Report a problem with this lesson

Quick answers and tool search

Type to search tools or to get a quick answer, for example 18% of 2500. Use the up and down arrow keys to move through the results, Enter to choose, and Escape to close.