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 projection, filtering, sorting and paging

Shape C# data with Select and SelectMany, filter it with Where, sort it by several keys with OrderBy and ThenBy, and page it with Skip and Take.

  • Intermediate
  • 15 minutes
  • Examples run with .NET 10.0.401
  • By MySmartCoPilot
Free core, Pro depth: 5 of 15 minutes free
Free: Select reshapes each item, and SelectMany flattens lists of lists · Where filters, OrderBy and ThenBy sort, and Skip and Take cut pages. With a pass: 3 in-depth parts and 3 interview answers, about 10 minutes.

What you will learn

  • Transform items with Select and SelectMany into anonymous types, tuples and records
  • Remove repeats and pick extremes by a key with DistinctBy, MinBy and MaxBy
  • Sort by several keys with OrderBy, ThenBy and a comparer, and predict where ties land
  • Split a sequence into pages with Skip, Take and Chunk without repeating or skipping items

Before you start

What you’ll learn · Before you start

What you will learn

  • Transform items with Select and SelectMany into anonymous types, tuples and records
  • Remove repeats and pick extremes by a key with DistinctBy, MinBy and MaxBy
  • Sort by several keys with OrderBy, ThenBy and a comparer, and predict where ties land
  • Split a sequence into pages with Skip, Take and Chunk without repeating or skipping items

Before you start

On this page

A booking screen receives 3 bookings that hold 7 passengers, and must show the adults as one numbered list, by coach and then by name, 4 to a page. Each of those jobs is one LINQ operator or two: projection: Making a new value out of each item of a sequence, such as a passenger's name or a new record, with Select or SelectMany. The items themselves do not change. with Select and SelectMany, filtering with Where, sorting with OrderBy and ThenBy, and paging with Skip and Take.

The runs below do all four on those bookings. You will flatten lists of lists, shape results into types that can leave a method, sort by two keys and cut pages, and then see where each of these operators goes wrong.

Select reshapes each item, and SelectMany flattens lists of lists

LINQ projection makes new values out of the items of a sequence and leaves the items as they were. Select makes exactly one result per item. With bookings that each hold a list of passengers, one result per booking is a list, so SelectMany exists: it hands on the items of every list as one flat sequence. flatten.cs does both:

flatten.cs: Select, SelectMany and Index() on 3 bookings C# · flatten/flatten.cs
// Three bookings hold seven passengers. The bookings are made up for this lesson.
Booking[] bookings =
[
    new("PNR-4521", "B2", [new("Asha", 34), new("Ravi", 36)]),
    new("PNR-4522", "B1", [new("Meera", 61), new("Kabir", 8), new("Isha", 33)]),
    new("PNR-4523", "B2", [new("Dev", 27), new("Zoya", 29)]),
];

// Select makes one result per booking: here each result is a whole list of passengers.
var lists = bookings.Select(b => b.Passengers);
Console.WriteLine($"Select: {lists.Count()} results, lists of {string.Join(", ", lists.Select(l => l.Count))} passengers");

// SelectMany makes one flat sequence of the lists' items, booking after booking.
var everyone = bookings.SelectMany(b => b.Passengers);
Console.WriteLine($"SelectMany: {everyone.Count()} results: {string.Join(", ", everyone.Select(p => p.Name))}");

// A second lambda receives the booking and each of its passengers, so the result can keep both.
var rows = bookings.SelectMany(b => b.Passengers, (b, p) => new { b.Pnr, b.Coach, p.Name, p.Age });

// Index() (.NET 9) pairs every item with its position, counted from 0.
Console.WriteLine("Numbered with Index():");
foreach (var (index, row) in rows.Index())
{
    Console.WriteLine($"  {index + 1}. {row.Name,-6} {row.Age,2}  coach {row.Coach}  {row.Pnr}");
}

// An anonymous type has no name, so it cannot leave the method: project into a record to return the rows.
List<Ticket> tickets = BuildTickets(bookings);
Console.WriteLine($"Tickets: {tickets.Count}, the first is {tickets[0]}");

static List<Ticket> BuildTickets(IEnumerable<Booking> bookings) =>
    bookings
        .SelectMany(b => b.Passengers, (b, p) => (b.Pnr, p.Name))
        .Select((pair, index) => new Ticket(index + 1, pair.Name, pair.Pnr))
        .ToList();

record Passenger(string Name, int Age);
record Booking(string Pnr, string Coach, List<Passenger> Passengers);
record Ticket(int No, string Name, string Pnr);

Output

Select: 3 results, lists of 2, 3, 2 passengers
SelectMany: 7 results: Asha, Ravi, Meera, Kabir, Isha, Dev, Zoya
Numbered with Index():
  1. Asha   34  coach B2  PNR-4521
  2. Ravi   36  coach B2  PNR-4521
  3. Meera  61  coach B1  PNR-4522
  4. Kabir   8  coach B1  PNR-4522
  5. Isha   33  coach B1  PNR-4522
  6. Dev    27  coach B2  PNR-4523
  7. Zoya   29  coach B2  PNR-4523
Tickets: 7, the first is Ticket { No = 1, Name = Asha, Pnr = PNR-4521 }

Recorded with .NET SDK 10.0.401 (runtime 10.0.12) on macOS 26 arm64. To run it yourself: dotnet run flatten.cs

Select handed on 3 results, lists of 2, 3 and 2 passengers; SelectMany handed on the 7 passengers. Step through how it walks the bookings:

Interactive · step by step SelectMany turns 3 bookings into 7 passengers Step through SelectMany. The bookings are on the left; the flat sequence it hands on grows on the right.
SelectMany turns 3 bookings into 7 passengers, step 5 of 5. Showing PNR-4521, coach B2, PNR-4522, coach B1, PNR-4523, coach B2, Asha, 34 (index 0), Ravi, 36 (index 1), Meera, 61 (index 2), Kabir, 8 (index 3), Isha, 33 (index 4), Dev, 27 (index 5), Zoya, 29 (index 6). In focus: Asha, 34, Ravi, 36, Meera, 61, Kabir, 8, Isha, 33, Dev, 27, Zoya, 29.PNR-4521, coach B2PNR-4522, coach B1PNR-4523, coach B2Asha, 34index 0Ravi, 36index 1Meera, 61index 2Kabir, 8index 3Isha, 33index 4Dev, 27index 5Zoya, 29index 6

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

Every step as text
  1. Three bookings. The source has 3 items, and each holds a list. Select(b => b.Passengers) would hand on those 3 lists as they are, which is why the first line of the run says lists of 2, 3 and 2.
  2. The first booking. SelectMany calls its lambda for the first booking, gets its list and hands on the passengers in it one at a time: Asha, then Ravi. They are items 0 and 1 of the new sequence.
  3. The second booking. Then the second booking: Meera, Kabir and Isha become items 2, 3 and 4. The lists are read in the order of the bookings, and each list in its own order.
  4. The third booking. The third booking adds Dev and Zoya as items 5 and 6. SelectMany has read every list once.
  5. One flat sequence. The result is one sequence of 7 passengers, not a sequence of lists. Index() pairs each one with its position, 0 to 6, and the run printed them numbered from 1.

The second lambda of SelectMany receives the booking and each of its passengers, so a row can keep the PNR and the coach beside the name. Index(), new in .NET 9, pairs every item with its position from 0; Select((item, i) => …) does the same inside a projection.

A LINQ projection usually builds a new shape on the spot. new { b.Pnr, p.Name } is an anonymous type: A class without a name that the compiler makes for an expression such as new { City, Total }. Its properties are read-only, Equals compares their values, and it can be used only inside the method that creates it. More in “Tuples, anonymous types and deconstruction”: quick, with value equality, but without a name that a method could return. A tuple works for small pairs. To pass rows out of a method, project into a record, as BuildTickets did.

Key idea

Select makes exactly one result per item and SelectMany one flat sequence out of the lists it gets; choose the result’s shape by where it goes: an anonymous type inside the method, a record out of it.

Checkpoint

Check: Select, SelectMany and the shape of a result

  1. Question 1 of 2 A fourth booking with 4 passengers is added to the 3 bookings of flatten.cs. How many items does bookings.SelectMany(b => b.Passengers) hand on now?

    Type a number.

    A hint

    SelectMany hands on every passenger of every booking.

    Show the answer: Question 1 of 2

    Answer: 11 passengers

    The three bookings hold 2 + 3 + 2 = 7 passengers, and the new one adds 4: 11. Select(b => b.Passengers) would still hand on one list per booking, 4 items.

  2. Question 2 of 2 A method must return the passenger rows that a SelectMany builds. What should the rows be?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: A record such as Ticket(int No, string Name, string Pnr)

    Anonymous types are for shapes used inside one method. Project into a record, as BuildTickets did, when the results travel.

Where filters, OrderBy and ThenBy sort, and Skip and Take cut pages

Filtering keeps the items whose test is true, sorting orders them by a key selector: A lambda that takes an item and returns the value an operator works with, such as b => b.Coach for OrderBy or DistinctBy, or o => o.State for GroupBy., and paging cuts one page out of the sorted rows. pages.cs does the three to the passengers:

pages.cs: filtering, sorting by two keys and paging C# · pages/pages.cs
// Filtering, sorting and paging the passengers of the three bookings.
Booking[] bookings =
[
    new("PNR-4521", "B2", [new("Asha", 34), new("Ravi", 36)]),
    new("PNR-4522", "B1", [new("Meera", 61), new("Kabir", 8), new("Isha", 33)]),
    new("PNR-4523", "B2", [new("Dev", 27), new("Zoya", 29)]),
];
var rows = bookings.SelectMany(b => b.Passengers, (b, p) => (b.Coach, p.Name, p.Age));

// Where keeps the items whose test is true: the adults.
var adults = rows.Where(r => r.Age >= 18);
Console.WriteLine($"Adults: {adults.Count()} of {rows.Count()}");

// OrderBy sorts by a key; ThenBy breaks ties with a second key: by coach, then by name within a coach.
var ordered = adults.OrderBy(r => r.Coach).ThenBy(r => r.Name);
Console.WriteLine($"By coach, then name: {string.Join(", ", ordered.Select(r => $"{r.Coach} {r.Name}"))}");

// Skip and Take cut one page out of the ordered rows.
const int size = 4;
for (int page = 1; page <= 3; page++)
{
    var onPage = ordered.Skip((page - 1) * size).Take(size).Select(r => r.Name).ToList();
    Console.WriteLine($"Page {page} (Skip({(page - 1) * size}).Take({size})): {(onPage.Count == 0 ? "empty" : string.Join(", ", onPage))}");
}

// Chunk cuts the whole sequence into arrays of the same size in one pass; the last one may be shorter.
foreach (var chunk in ordered.Chunk(size))
{
    Console.WriteLine($"Chunk of {chunk.Length}: {string.Join(", ", chunk.Select(r => r.Name))}");
}

record Passenger(string Name, int Age);
record Booking(string Pnr, string Coach, List<Passenger> Passengers);

Output

Adults: 6 of 7
By coach, then name: B1 Isha, B1 Meera, B2 Asha, B2 Dev, B2 Ravi, B2 Zoya
Page 1 (Skip(0).Take(4)): Isha, Meera, Asha, Dev
Page 2 (Skip(4).Take(4)): Ravi, Zoya
Page 3 (Skip(8).Take(4)): empty
Chunk of 4: Isha, Meera, Asha, Dev
Chunk of 2: Ravi, Zoya

Recorded with .NET SDK 10.0.401 (runtime 10.0.12) on macOS 26 arm64. To run it yourself: dotnet run pages.cs

Filtering with Where left the 6 adults. Sorting with OrderBy put coach B1 before B2, and ThenBy sorted by name inside each coach. Paging with Skip((page - 1) * size).Take(size) cut page 1 and page 2, and page 3 came back empty, not as an error. Chunk(4) cut the same two pages in one pass, as arrays.

The rest of the lesson refines each of the four jobs with more operators. Here is what each of them does:

Operator What it does
Distinct, DistinctBy drop repeated items, or items with a repeated key, keeping the first
MinBy, MaxBy return the item with the smallest or largest key, not the key
ExceptBy, IntersectBy, UnionBy compare two sequences by one key
Order, OrderDescending sort values by themselves, without a key lambda
a comparer such as StringComparer.Ordinal decides how two keys compare
TakeLast, TakeWhile, SkipWhile, Take(5..10) take from the end, while a test holds, or by a range
keyset paging asks for the items after the last key already shown, instead of for page n
Shuffle returns the items in a random order

Checkpoint

Check: Where, OrderBy, ThenBy, Skip and Take

  1. Question 1 of 2 Pages hold 4 rows. How many rows does Skip skip for page 3?

    Type a number.

    A hint

    Page n skips the rows of the n - 1 pages before it.

    Show the answer: Question 1 of 2

    Answer: 8 rows

    Skip((3 - 1) * 4) skips 8 rows, the rows of pages 1 and 2, and Take(4) takes the next four. In pages.cs page 3 came back empty because only 6 adults exist.

  2. Question 2 of 2 In pages.cs, what does ThenBy(r => r.Name) do after OrderBy(r => r.Coach)?

    Choose one answer.

    Show the answer: Question 2 of 2

    Answer: It sorts by name among the rows that have the same coach

    OrderBy sets the first key and every ThenBy breaks the ties of the keys before it.

Repeats and extremes by a key: Distinct, DistinctBy, MaxBy and the set operators Pro: opens with a Pro, Premium or Ultimate pass

Removing repeats looks like one job and is three: what counts as the same item, which copy you keep, and whether another list takes part. The run below answers each with a station log.

In this part:

  • Distinct compares whole items, so a class and a record behave differently
  • DistinctBy keeps the first item of each key
  • MaxBy returns the item, Max only the key
  • ExceptBy, IntersectBy and UnionBy compare by one key

1 runnable example (C#) · about 4 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Sorting by several keys: ties, comparers and the OrderBy-twice bug, reproduced Pro: opens with a Pro, Premium or Ultimate pass

A sort by several keys fails in quiet ways: rows come out in an order that looks almost right. The run below reproduces the three common failures on seven berths.

In this part:

  • A second OrderBy replaces the first, and ThenBy fixes it
  • Ties keep their order, because OrderBy is a stable sort
  • Comparers decide what comes first: case, and codes such as B10

1 runnable example (C#) · about 3 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Paging that neither repeats nor skips: Skip, Take, Chunk and keyset pages Pro: opens with a Pro, Premium or Ultimate pass

Paging bugs show up as a row seen twice, a row never seen, or page 0 quietly showing page 1. The run below produces each of them on purpose and fixes them.

In this part:

  • Page n skips the pages before it, and page 0 is page 1
  • Ranges, TakeLast, TakeWhile and Chunk
  • Offset pages repeat or skip items when the list changes
  • Testing Shuffle by what must stay true

1 runnable example (C#) · about 3 minutes

Opening this part with your pass…

This part opens with any MySmartCoPilot pass.

  • Pro pass · 1 week₹79$2.99
  • 30 days₹179$5.99
  • 1 year₹1,699$45

One payment; a pass never renews. The price shown is the total you pay: no fees or charges are added at checkout.

It also opens every other in-depth part in Learn and every Pro tool. Sales are final, except where the law gives you a right to withdraw, for example in the EU and the UK (refund policy).

Practise: a quiz leaderboard

leaderboard.cs holds nine quiz attempts by six learners and checks three methods for you to write: each learner’s best attempt, the ranked rows, and one page of them. Until you write their bodies, all 7 checks fail:

leaderboard.cs: seven checks of Best, Ranked and Page C# · practice/leaderboard.cs
// Practice: write Best, Ranked and Page at the bottom so that every check passes, then run this file again.
// Nine quiz attempts by six learners; a learner may try more than once. The attempts are made up for this lesson.
Attempt[] attempts =
[
    new("Asha", 72), new("Ravi", 88), new("Asha", 91), new("meera", 88), new("Dev", 64),
    new("Ravi", 79), new("Zoya", 91), new("Kabir", 55), new("Asha", 85),
];

var checks = new List<(string Name, Func<string> Run, string Expected)>
{
    ("one row per learner", () => Best(attempts).Count().ToString(), "6"),
    ("Asha's best score", () => Best(attempts).Single(a => a.Learner == "Asha").Score.ToString(), "91"),
    ("Ravi's best score", () => Best(attempts).Single(a => a.Learner == "Ravi").Score.ToString(), "88"),
    ("page 1 of 3", () => string.Join(" | ", Page(Ranked(attempts), 1, 3)), "1. Asha 91 | 2. Zoya 91 | 3. meera 88"),
    ("page 2 of 4", () => string.Join(" | ", Page(Ranked(attempts), 2, 4)), "5. Dev 64 | 6. Kabir 55"),
    ("a page after the last one", () => string.Join(" | ", Page(Ranked(attempts), 3, 4)), ""),
    ("page 0", () => string.Join(" | ", Page(Ranked(attempts), 0, 4)), "ArgumentOutOfRangeException"),
};

int passed = 0;
foreach (var (name, run, expected) in checks)
{
    string got;
    try
    {
        got = run();
    }
    catch (Exception e)
    {
        got = e.GetType().Name;
    }
    string verdict = got == expected ? "ok" : $"FAIL, got \"{got}\"";
    if (verdict == "ok")
    {
        passed++;
    }
    Console.WriteLine($"{name}: {verdict}");
}
Console.WriteLine($"{passed} of {checks.Count} checks pass");
return passed == checks.Count ? 0 : 1;

// Each learner's best attempt, one attempt per learner.
static IEnumerable<Attempt> Best(IEnumerable<Attempt> attempts) =>
    throw new NotImplementedException("Put the highest scores first, then keep the first attempt of each learner.");

// "rank. Learner score" for every learner's best attempt: the highest score first, equal scores by name,
// ignoring upper and lower case, and ranks counted from 1.
static IEnumerable<string> Ranked(IEnumerable<Attempt> attempts) =>
    throw new NotImplementedException("Sort Best by score, then by name with a comparer, and number the rows with Select.");

// Page page (counted from 1) of size rows; a page before the first throws ArgumentOutOfRangeException.
static IEnumerable<string> Page(IEnumerable<string> rows, int page, int size) =>
    throw new NotImplementedException("Check page, then Skip the rows of the pages before it and Take one page.");

record Attempt(string Learner, int Score);

Output (exit status 1)

one row per learner: FAIL, got "NotImplementedException"
Asha's best score: FAIL, got "NotImplementedException"
Ravi's best score: FAIL, got "NotImplementedException"
page 1 of 3: FAIL, got "NotImplementedException"
page 2 of 4: FAIL, got "NotImplementedException"
a page after the last one: FAIL, got "NotImplementedException"
page 0: FAIL, got "NotImplementedException"
0 of 7 checks pass

Recorded with .NET SDK 10.0.401 (runtime 10.0.12) on macOS 26 arm64. To run it yourself: dotnet run leaderboard.cs

Rank the learners

Copy leaderboard.cs. In Best, sort by score, highest first, and keep the first attempt of each learner with DistinctBy. In Ranked, sort by score and then by name with StringComparer.OrdinalIgnoreCase, and number the rows with Select((a, i) => …). In Page, throw for a page below 1, then Skip and Take. Run dotnet run leaderboard.cs until it prints 7 of 7 checks pass.

The questions below stay with the first two sections, projection, filtering, sorting and paging, and your weakest idea comes up first:

Practice that adapts to you

Practise this lesson

6 questions about 5 ideas of this lesson. Each answer is explained at once, and what you get right is kept in this browser.

  1. Question 1 of 6 bookings.Select(b => b.Passengers) on 3 bookings returns what?

    Choose one answer.

    Show the answer: Question 1 of 6

    Answer: 3 items, each a list of passengers

    flatten.cs printed "Select: 3 results, lists of 2, 3, 2 passengers", then SelectMany's 7 passengers.

  2. Question 2 of 6 You flatten bookings into passengers but also need each passenger's PNR. Which call keeps both?

    Choose one answer.

    Show the answer: Question 2 of 6

    Answer: bookings.SelectMany(b => b.Passengers, (b, p) => new { b.Pnr, p.Name })

    The overload SelectMany(collectionSelector, resultSelector) is how a flattened row keeps data from the item it came from.

  3. Question 3 of 6 Rows must be numbered 1, 2, 3 in the order of the query, and the numbers must stay with the rows after a later Where. What do you write?

    Choose one answer.

    Show the answer: Question 3 of 6

    Answer: Number them with Select((row, i) => (No: i + 1, row)) or Index() before the Where

    Index() (.NET 9) and the Select overload with an index both number items in the order they are read; number the rows at the point whose order you mean.

  4. Question 4 of 6 Which operator keeps the 6 adults out of the 7 passengers in pages.cs?

    Choose one answer.

    Show the answer: Question 4 of 6

    Answer: Where(r => r.Age >= 18)

    Filtering is Where; reshaping is Select; Take cuts by position.

  5. Question 5 of 6 With 6 rows and pages of 4, what does page 3, Skip(8).Take(4), give?

    Choose one answer.

    Show the answer: Question 5 of 6

    Answer: An empty sequence

    A page after the last one is empty. Show "no more results" for it instead of treating it as an error.

  6. Question 6 of 6 A results page must list the cheapest fares first and, for equal fares, the earliest departure. Which chain is it?

    Choose one answer.

    Show the answer: Question 6 of 6

    Answer: fares.OrderBy(f => f.Price).ThenBy(f => f.Departs)

    Write the keys in the order a reader ranks them: the first in OrderBy, each later one in a ThenBy.

Interview questions

Fresher · What is the difference between Select and SelectMany?

Both run a lambda on every item. Select keeps one result per item, so when the lambda returns a list, you get a sequence of lists. SelectMany expects the lambda to return a sequence and hands on the items of all those sequences as one flat sequence, in order: three bookings with 2, 3 and 2 passengers become 7 passengers. An overload of SelectMany also passes the outer item to a second lambda, so each result can combine the booking and the passenger. Use Select to reshape and SelectMany to flatten.

  1. Fresher to mid

    A sales report sorts orders with .OrderBy(o => o.Region).OrderBy(o => o.Date). Users say the regions are mixed together, although within one date the regions look sorted. Explain exactly what the query does, fix it, and say how a test would have caught it.

    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

    An export merges two CSV files into a List<Customer> and calls Distinct() on it, but the duplicate customers stay. Customer is a class. Explain why, and compare three ways to remove the duplicates.

    Model answer, Pro. First step and follow-ups

  3. Mid to senior

    A feed loads 20 posts a page with posts.OrderByDescending(p => p.CreatedAt).Skip(page * 20).Take(20). In busy hours users see the same post twice while scrolling, and now and then a post never appears. Explain both and redesign the paging.

    Model answer, Pro. First step and follow-ups

Key takeaways

  • Select makes one result per item and SelectMany flattens the lists it gets into one sequence; project into a record when the result leaves the method.
  • Where filters, OrderBy sorts and ThenBy adds keys under the first one, and Skip with Take cuts a page, with an empty page after the last one.
  • The refining operators, from DistinctBy and MaxBy to Order, comparers and Chunk, each answer one of those four jobs more exactly.

Terms in this lesson

key selector
A lambda that takes an item and returns the value an operator works with, such as b => b.Coach for OrderBy or DistinctBy, or o => o.State for GroupBy.
projection
Making a new value out of each item of a sequence, such as a passenger's name or a new record, with Select or SelectMany. The items themselves do not change.

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.