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.
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.
// 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 key | b | a |
| Role of the other key | a breaks ties in b | b breaks ties in a |
| How it reads in English | Sort by a. Now sort by b. | Sort by a, breaking ties with b. |
| Number of ordering passes | Two | One, with a composite comparison |
| Usually what the author meant | No | Yes |
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.
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.
Saved in this browser only.