OrderBy doesn't sort in place — it hands you a new sequence, arranged the way you asked.
"Sort these by price" sounds simple, until you need "sort by department, then by name within each department" — a two-level sort a single comparison can't express cleanly. LINQ's sorting operators handle both the simple case and the layered case with the same small, composable vocabulary.
This lesson covers OrderBy/OrderByDescending for the primary sort key, ThenBy/ThenByDescending for tie-breaking secondary keys, and the guarantee of stable sorting that makes all of this predictable.
OrderBy arranges a sequence in ascending order according to a key you choose from each item — cheapest first, earliest first, alphabetically first. OrderByDescending does the reverse. ThenBy/ThenByDescending chain onto either of those to break ties with a secondary (or third, or fourth) key.
public static IOrderedEnumerable<TSource> OrderBy<TSource, TKey>(
this IEnumerable<TSource> source,
Func<TSource, TKey> keySelector)OrderBy doesn't take a full comparison — just a key selector, a function that extracts the value to sort by from each item. LINQ uses that key's default comparer (Comparer<TKey>.Default) to do the actual comparing, which is why it works out of the box for numbers, strings, dates, and any type implementing IComparable<T>. Notice the return type: IOrderedEnumerable<TSource>, not plain IEnumerable<TSource> — that more specific type is exactly what makes ThenBy available to chain afterward.
A hand-written sort means writing your own comparer, and multi-key sorting compounds the pain — a manual comparison function with nested tie-breaking logic gets messy fast:
// Manual multi-key sort
employees.Sort((a, b) =>
{
int deptCompare = string.Compare(a.Department, b.Department);
if (deptCompare != 0) return deptCompare;
return string.Compare(a.Name, b.Name);
});// OrderBy + ThenBy
var sorted = employees
.OrderBy(e => e.Department)
.ThenBy(e => e.Name);The LINQ version reads as a direct translation of the business requirement — "by department, then by name" — with no manual tie-breaking logic to get wrong. It also doesn't mutate the original collection, unlike List<T>.Sort(), which sorts in place and destroys the original order.
ThenBy only ever compares items that were tied on every key before it — it never overrides an already-decided primary ordering.public record Employee(int Id, string Name, string Department, decimal Salary);
List<Employee> employees =
[
new(1, "Cara", "Engineering", 95000m),
new(2, "Ben", "Sales", 72000m),
new(3, "Ana", "Engineering", 88000m),
new(4, "Dev", "Sales", 80000m),
new(5, "Ella", "Engineering", 88000m),
];
// Single key, ascending
var byName = employees.OrderBy(e => e.Name);
// Single key, descending
var bySalaryDesc = employees.OrderByDescending(e => e.Salary);
// Multi-key: department first, then salary descending within each department
var report = employees
.OrderBy(e => e.Department)
.ThenByDescending(e => e.Salary);
foreach (var e in report)
Console.WriteLine($"{e.Department}: {e.Name} — ${e.Salary}");
// Engineering: Cara — $95000
// Engineering: Ana — $88000 (tied with Ella on salary, Ana came first in source)
// Engineering: Ella — $88000
// Sales: Dev — $80000
// Sales: Ben — $72000Code → Meaning → Result: Notice Ana appears before Ella despite an identical $88000 salary — that's the stable sort guarantee: since ThenByDescending found them tied, it left them in their original relative order from the source list (Ana came before Ella in employees).
An admin dashboard shows a leaderboard of top customers by total spend, and within a tie, alphabetically by name for a deterministic, presentable order:
public record CustomerSpend(string Name, decimal TotalSpent);
List<CustomerSpend> spending =
[
new("Cara Lopez", 4200m),
new("Ana Cole", 4200m),
new("Ben Diaz", 3100m),
];
var leaderboard = spending
.OrderByDescending(c => c.TotalSpent)
.ThenBy(c => c.Name);
int rank = 1;
foreach (var c in leaderboard)
Console.WriteLine($"#{rank++} {c.Name} — ${c.TotalSpent}");
// #1 Ana Cole — $4200
// #2 Cara Lopez — $4200
// #3 Ben Diaz — $3100Without the ThenBy(c => c.Name) tie-breaker, Ana and Cara's relative order for that tied $4200 would depend entirely on their original order in spending — fine for one run, but fragile and confusing if the underlying data source (say, a database query without an explicit order) ever returns rows in a different sequence. Explicit tie-breaking keys make sort output deterministic and reproducible.
Think of OrderBy as sorting files into drawers by department — all "Engineering" files together, all "Sales" files together, each drawer in department order. ThenBy is then organizing the folders within each drawer alphabetically. You never mix files between drawers while doing the within-drawer sort — that's exactly why ThenBy only ever breaks ties from the level above it, never overriding it.
OrderBy returns IOrderedEnumerable<T> — a type that remembers "I'm already sorted by this key, apply any further key as a tie-break, not a fresh sort." A plain re-sort with a second OrderBy call instead would throw away the first ordering entirely, since a second unrelated OrderBy call doesn't know anything about a prior sort.Where/Select, which can yield an item the moment it's ready, sorting fundamentally requires seeing every item before it can produce the first one — you can't know what's smallest until you've looked at everything. The call to OrderBy itself is still deferred (nothing runs until enumerated), but once enumeration starts, the whole source is pulled through before the first result comes out.OrderBy calls vs. OrderBy then ThenByThese look similar but behave very differently: list.OrderBy(a).OrderBy(b) sorts by a, then completely re-sorts by b — the second call discards the first ordering entirely, since each OrderBy call independently sorts the whole sequence. list.OrderBy(a).ThenBy(b) sorts by a primarily, using b only to break ties. If you want a true multi-key sort, always use ThenBy/ThenByDescending after the first OrderBy, never a second OrderBy.
OrderBy vs List<T>.Sort()List<T>.Sort() (from Foundations) sorts the list in place, mutating the original, and returns void. OrderBy never touches the source — it returns a brand-new sequence, leaving the original list's order untouched. Also worth knowing: List<T>.Sort() is not guaranteed stable, while LINQ's OrderBy is.
OrderBy instead of ThenBy employees.OrderBy(e => e.Department).OrderBy(e => e.Name) sorts by name only — the department ordering is completely discarded by the second call. employees.OrderBy(e => e.Department).ThenBy(e => e.Name).
OrderBy mutates the source Calling employees.OrderBy(e => e.Name); without capturing the result and expecting employees itself to now be sorted. OrderBy always returns a new sequence: var sorted = employees.OrderBy(e => e.Name);.
List<T> in place, with no need for the original order — List<T>.Sort() avoids allocating a new sequence.Min/Max (Aggregation) instead of sorting and taking the first item.OrderBy re-sorts from scratch; only ThenBy layers on top.OrderBy never mutates the source; it always returns a new sequence.
OrderBy/OrderByDescending sort by a primary key; ThenBy/ThenByDescending chain on to break ties with additional keys.OrderBy returns IOrderedEnumerable<T>, which is what makes chaining ThenBy possible — a second OrderBy would discard the first sort entirely.You've learned how to sort sequences by one or more keys. Let's check your understanding.
1. What is the difference between list.OrderBy(a).OrderBy(b) and list.OrderBy(a).ThenBy(b)?
Correct: B
Why B is correct: As covered in Common Confusion, a second OrderBy is a fresh, independent sort — it has no memory of the previous one. Only ThenBy layers a tie-break on top of an existing order.
Why A is incorrect: This is exactly the mistake the lesson warns about — they behave very differently.
Why C is incorrect: ThenBy works with any key type that has a natural or default comparer, not just numbers.
Why D is incorrect: Both chains compile fine — the issue is behavior, not syntax.
Reinforcement: Always use ThenBy, never a second OrderBy, when you want a true multi-key sort.
2. Two employees have the identical salary. After sorting with employees.OrderByDescending(e => e.Salary) (no ThenBy), what determines their relative order in the result?
Correct: B
Why B is correct: As explained in Under the Hood, LINQ's sort is a documented, guaranteed stable sort — items comparing equal on the given key(s) retain their original relative order from the source.
Why A is incorrect: Stability is a documented .NET guarantee, not undefined behavior — this is exactly the point of "stable."
Why C is incorrect: LINQ never adds an implicit fallback sort key — if you want name as a tie-breaker, you must add .ThenBy(e => e.Name) explicitly.
Why D is incorrect: No such automatic behavior exists — Id plays no role unless you explicitly sort by it.
Reinforcement: Stability means "ties preserve source order" — not "ties are broken by some other implicit rule."
3. Why does OrderBy return IOrderedEnumerable<T> instead of plain IEnumerable<T>?
Correct: B
Why B is correct: This is exactly what makes chained multi-key sorting work correctly — the more specific return type is what exposes the ThenBy/ThenByDescending methods and gives them the context of "already sorted by this."
Why A is incorrect: The distinct type serves a real, functional purpose, as explained.
Why C is incorrect: IOrderedEnumerable<T> has nothing to do with indexing — like IEnumerable<T>, it only supports sequential enumeration.
Why D is incorrect: Any IEnumerable<T>, ordered or not, can generally be enumerated multiple times if the underlying source allows it — that's unrelated to why this specific return type exists.
Reinforcement: The specific return type is what makes ThenBy type-safe and meaningful — it's a deliberate design choice, not incidental.
4. You need a customer leaderboard sorted by total spend (highest first), with alphabetical order by name as a deterministic tie-breaker. Which LINQ expression is correct?
Correct: B
Why B is correct: OrderByDescending establishes the primary sort (highest spend first); ThenBy correctly layers alphabetical order as the tie-breaker without disturbing the primary sort.
Why A is incorrect: The second OrderBy discards the spend-based sort entirely, resulting in a purely alphabetical list.
Why C is incorrect: This is invalid — ThenByDescending can only be called on an IOrderedEnumerable<T> (the result of a prior OrderBy/OrderByDescending), not as the very first operator in a chain; it wouldn't compile.
Why D is incorrect: Like option A, the second OrderByDescending here discards the name-based sort — the final result would be sorted purely by spend, with no meaningful tie-break at all.
Reinforcement: Establish the primary key with OrderBy/OrderByDescending, then layer every additional key with ThenBy/ThenByDescending — never a second OrderBy.
You can now sort by one or more keys with confidence. Next up: grouping related items together with GroupBy.
dotnetmadeeasy.com — Learn C# and .NET, the right way.