Skip to main content
ANVISoftware Solutions
Lesson 46 of 62Intermediate15 min

Sorting

By the end of this lesson

Order results by one or several keys.

OrderBy takes a lambda that returns the value to sort on — the key — and gives back the same items in a new order. The source list is not rearranged.

Sorting on one key holds no surprises. Sorting on two is where a specific mistake appears in almost every codebase: writing a second OrderBy when you meant ThenBy. The result is not a compiler error and it does not look wrong on screen, so it survives review.

One key, then two keys
C#
IEnumerable<Employee> bySalary = employees
    .OrderByDescending(e => e.AnnualSalary);
// Meera Nair 2700000, Neha Kulkarni 2400000, Asha Mehta 1800000, Ravi Iyer 1200000,
// Tarun Bansal 1100000, Divya Rao 960000, Sanjay Gupta 950000, Imran Sheikh 720000

IEnumerable<Employee> byDepartmentThenPay = employees
    .OrderBy(e => e.Department)
    .ThenByDescending(e => e.AnnualSalary);

foreach (Employee e in byDepartmentThenPay)
{
    Console.WriteLine($"{e.Department}: {e.Name} {e.AnnualSalary}");
}
// Engineering: Neha Kulkarni 2400000
// Engineering: Asha Mehta 1800000
// Engineering: Ravi Iyer 1200000
// Finance: Sanjay Gupta 950000
// Sales: Meera Nair 2700000
// Sales: Tarun Bansal 1100000
// Support: Divya Rao 960000
// Support: Imran Sheikh 720000
  • OrderBy sorts ascending: smallest number, earliest date, A before Z. OrderByDescending reverses that. There is no boolean flag to pass; they are two methods.
  • OrderBy returns IOrderedEnumerable<T> rather than plain IEnumerable<T>. That is the whole trick behind ThenBy: ThenBy only exists on an already-ordered sequence, so the compiler will not let you write it first.
  • ThenByDescending applies only where the first key ties. Within Engineering the three salaries decide the order; across departments the department name decides it.
  • Sorting eight items costs nothing. Sorting a large sequence means reading all of it before the first result can come out, because the last item read might belong at the front. That is a real cost and the sorting lesson is the right place to say so.
The mistake and the fix, on the same data
C#
// WRONG — reads as "by department, then by salary". It is not.
IEnumerable<Employee> wrong = employees
    .OrderBy(e => e.Department)
    .OrderBy(e => e.AnnualSalary);
// Imran Sheikh (Support) 720000
// Sanjay Gupta (Finance) 950000
// Divya Rao (Support) 960000
// Tarun Bansal (Sales) 1100000
// Ravi Iyer (Engineering) 1200000
// ... departments interleaved, salary ascending

// RIGHT — one ordering, with a secondary key.
IEnumerable<Employee> right = employees
    .OrderBy(e => e.Department)
    .ThenBy(e => e.AnnualSalary);
// Engineering: 1200000, 1800000, 2400000
// Finance: 950000
// Sales: 1100000, 2700000
// Support: 720000, 960000
  • The wrong version is not broken code; it is code that answers a different question. That is why it is hard to spot: the output is sorted, neatly, by something.
  • In the wrong version the department ordering is not entirely gone in theory — it would break ties between equal salaries. With eight distinct salaries there are no ties, so it contributes nothing at all.
  • Read a chain from the top: the last ordering call that is not a ThenBy decides the primary key. If that sentence is hard to apply to your query, the query has too many ordering calls.

The two chains, compared directly:

 OrderBy(a).OrderBy(b)OrderBy(a).ThenBy(b)
Primary keyba
Role of the other keya breaks ties in bb breaks ties in a
How it reads in EnglishSort by a. Now sort by b.Sort by a, breaking ties with b.
Number of ordering passesTwoOne, with a composite comparison
Usually what the author meantNoYes

Summary

  • OrderBy and OrderByDescending sort by a key and return a new sequence, leaving the source untouched
  • ThenBy and ThenByDescending add secondary keys and exist only on an already-ordered sequence
  • A second OrderBy makes its own key primary and demotes the earlier key to a tie-break
  • Ordering in memory is stable; ordering in a database is not, so paging needs a unique tie-break
  • Text ordering follows the current culture unless you pass a comparer such as StringComparer.Ordinal

Practice

Attempt each one before opening the solution. Getting it wrong first is how the idea sticks.

Try it yourself

Try it yourself

List everyone grouped visually by department, A to Z, and within each department oldest joiner first. Print department, name and joining date on one line each.

Write it with a single OrderBy call and nothing else that sorts.

Show solution

OrderBy for the department, ThenBy for the date. The constraint in the prompt — one OrderBy — is the whole exercise: it forces the secondary key to be expressed as a ThenBy rather than as a second sort.

Note that no grouping is involved. The rows appear gathered by department because they are sorted by it. When all you need is an ordered report, sorting is simpler than grouping and does not buffer the sequence into group objects.

C#
IEnumerable<Employee> report = employees
    .OrderBy(e => e.Department)
    .ThenBy(e => e.JoiningDate);

foreach (Employee e in report)
{
    Console.WriteLine($"{e.Department} | {e.Name} | {e.JoiningDate}");
}
// Engineering | Neha Kulkarni | 2017-01-09
// Engineering | Asha Mehta    | 2019-04-01
// Engineering | Ravi Iyer     | 2021-07-12
// Finance     | Sanjay Gupta  | 2023-09-04
// Sales       | Meera Nair    | 2016-03-21
// Sales       | Tarun Bansal  | 2018-06-18
// Support     | Divya Rao     | 2020-11-02
// Support     | Imran Sheikh  | 2022-02-14
// (dates written in year-month-day form here for readability; DateOnly prints
//  using your machine's culture unless you pass a format)

Think about it

Think about it

A screen shows employees ordered by department only, ten to a page, using Skip and Take against a database.

Engineering has forty people. A user reports that somebody appeared on both page two and page three, and somebody else never appeared at all. Nothing in the code changed between the two requests. What happened?

Show solution

Forty rows share the same ordering key, so their relative order is not determined by the query. The database is free to return them in whatever order its plan produces, and that order can differ between the request for page two and the request for page three.

Skip and Take work on positions in a sequence. If the sequence is not in a fixed order, positions mean nothing across two separate queries: position 25 in one run and position 25 in another are not guaranteed to be the same row.

The fix is to make the ordering total — order by department, then by something unique, such as an employee id. Not a cosmetic change: without a unique tie-break, paged results are not reliable, and the bug only shows up when a page boundary lands inside a group of ties, which is why it reaches production.

Knowledge check

Nothing is recorded and there is no score. The explanation appears either way.

employees.OrderBy(e => e.Department).OrderBy(e => e.AnnualSalary) is enumerated. How are the results ordered?

Saved in this browser only.