LINQ OrderBy and ThenBy in C#: Sorting Explained
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.
What Is It?
The Simple Explanation
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.
The Technical Definition
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.
Why Does It Exist?
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.
Big Picture
↓ .OrderBy(e => e.Department) — groups by department, order within each group still arbitrary
(Eng, Cara), (Eng, Ana), (Sales, Ben), (Sales, Ana)
↓ .ThenBy(e => e.Name) — breaks ties within each department group
(Eng, Ana), (Eng, Cara), (Sales, Ana), (Sales, Ben)
How It Works
- Every item is compared using only the primary key selector.
- If two items have the same primary key, their relative order from the input is preserved — this is the "stable sort" guarantee, covered further below.
ThenByonly ever compares items that were tied on every key before it — it never overrides an already-decided primary ordering.
Simple Example
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).
Real-World Example
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.
Analogy
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.
Under the Hood
- Items that compare as equal on all sort keys retain their original relative order from the source sequence. This is a documented .NET guarantee, not an implementation accident — you can rely on it.
OrderByreturnsIOrderedEnumerable<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 secondOrderBycall instead would throw away the first ordering entirely, since a second unrelatedOrderBycall doesn't know anything about a prior sort.
- Unlike
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 toOrderByitself is still deferred (nothing runs until enumerated), but once enumeration starts, the whole source is pulled through before the first result comes out.
Common Confusion
1. Chaining two OrderBy calls vs. OrderBy then ThenBy
These 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.
2. 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.
Common Mistakes
Mistake 1 — Chaining a second 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).
Mistake 2 — Assuming 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);.
When Should I Use It?
Use OrderBy / ThenBy when
- You need a new, sorted sequence without disturbing the original.
- You're sorting by one or more properties, including in mixed ascending/descending combinations.
- You need a deterministic, reproducible order — always add a tie-breaking key when ties are possible and order matters.
Consider alternatives when
- You genuinely want to sort a
List<T>in place, with no need for the original order —List<T>.Sort()avoids allocating a new sequence. - You just need the single smallest/largest item, not the whole sequence sorted — use
Min/Max(Aggregation) instead of sorting and taking the first item.
Mental Model
ThenBy = "when tied on everything before me, sort by this key next"
Remember:
· A second
OrderBy re-sorts from scratch; only ThenBy layers on top.· LINQ's sort is stable — ties keep their original relative order unless you add another tie-breaking key.
·
OrderBy never mutates the source; it always returns a new sequence.
Key Takeaway
OrderBy/OrderByDescendingsort by a primary key;ThenBy/ThenByDescendingchain on to break ties with additional keys.- LINQ's sort is guaranteed stable — equal items keep their original relative order.
OrderByreturnsIOrderedEnumerable<T>, which is what makes chainingThenBypossible — a secondOrderBywould discard the first sort entirely.- Neither operator mutates the source — both return a brand-new sequence.
- Add explicit tie-breaking keys whenever a deterministic, reproducible order matters — don't rely on incidental source ordering.
Check Your Understanding
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)?
Show answer
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?
Show answer
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>?
Show answer
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?
Show answer
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.