Sorting with OrderBy and OrderByDescending
OrderBy() sorts a sequence in ascending order according to a key selector, returning an IOrderedEnumerable<T> — a specialized interface that preserves the sorting metadata needed for further chained sorts. OrderByDescending() does the same in reverse order, and both use the default comparer for the key's type (numeric comparison for numbers, ordinal or culture-aware comparison for strings, depending on the overload) unless you pass an explicit IComparer<TKey>.
Cricket analogy: Sorting the ICC batting rankings from highest rating to lowest is exactly what OrderByDescending(player => player.Rating) does, using the numeric rating as the default sort key.
Secondary Sort Keys with ThenBy
When two elements share the same primary sort key, ThenBy() (or ThenByDescending()) adds a secondary sort key without disturbing the primary order — it can only be called on an IOrderedEnumerable<T>, which is exactly what OrderBy() returns, chaining as players.OrderBy(p => p.Team).ThenByDescending(p => p.Score). Calling OrderBy() a second time instead of ThenBy() is a common mistake: OrderBy() re-sorts from scratch by the new key, discarding the previous primary sort entirely rather than layering it as a tiebreaker.
Cricket analogy: Ranking batters first by team, then within each team by strike rate, uses a primary key plus a tiebreaker, exactly like OrderBy(p => p.Team).ThenByDescending(p => p.StrikeRate).
Custom Comparers
Every OrderBy()/OrderByDescending()/ThenBy() method has an overload accepting an IComparer<TKey>, letting you override the default comparison logic — useful for case-insensitive string sorts (OrderBy(s => s.Name, StringComparer.OrdinalIgnoreCase)), culture-specific sorts, or custom business rules like sorting an enum by a defined priority order rather than its declared numeric value. Passing a custom IComparer<TKey> does not change the key selector; it only changes how two key values are compared once extracted.
Cricket analogy: Sorting player names ignoring case differences so 'de Kock' and 'De Kock' aren't treated as different entries mirrors OrderBy(p => p.Name, StringComparer.OrdinalIgnoreCase), a custom comparer overriding default string comparison.
Stability of OrderBy
LINQ's sorting operators are guaranteed to be stable — elements with equal keys retain their original relative order from the source sequence, which is a documented behavioral guarantee of OrderBy(), OrderByDescending(), ThenBy(), and ThenByDescending() across both LINQ-to-Objects and (in practice) most IQueryable providers translating to SQL's ORDER BY. This matters when the sort key alone doesn't fully distinguish elements — a stable sort means you can rely on insertion order as an implicit final tiebreaker without adding an explicit ThenBy().
Cricket analogy: When two players finish a tournament with identical batting averages, a stable ranking keeps them listed in the order they originally appeared in the squad list, just as OrderBy() preserves original relative order for tied keys.
var players = new List<Player>
{
new Player { Name = "Root", Team = "England", Score = 87 },
new Player { Name = "Kohli", Team = "India", Score = 112 },
new Player { Name = "Smith", Team = "Australia", Score = 112 },
new Player { Name = "Stokes", Team = "England", Score = 45 },
};
// Simple ascending sort
var byName = players.OrderBy(p => p.Name);
// Descending sort by numeric key
var byScoreDesc = players.OrderByDescending(p => p.Score);
// Primary key + secondary tiebreaker
var byTeamThenScore = players.OrderBy(p => p.Team)
.ThenByDescending(p => p.Score);
// Custom comparer for case-insensitive string sort
var byNameIgnoreCase = players.OrderBy(p => p.Name, StringComparer.OrdinalIgnoreCase);
// Common mistake: this discards the team ordering entirely instead of layering it
var wrongTiebreak = players.OrderBy(p => p.Team)
.OrderByDescending(p => p.Score); // re-sorts from scratchOrderBy() and OrderByDescending() return IOrderedEnumerable<T>, not plain IEnumerable<T>. This distinct return type is what allows ThenBy() to exist as an extension method specifically on IOrderedEnumerable<T> — it's the compiler's way of ensuring ThenBy() can only follow an existing sort, never stand alone.
Calling OrderBy() a second time on an already-sorted IOrderedEnumerable<T> does not add a secondary sort — it discards the previous ordering and re-sorts the sequence from scratch using only the new key. If you need a secondary sort key, you must use ThenBy() or ThenByDescending(), not a second OrderBy() call.
- OrderBy() sorts ascending and OrderByDescending() sorts descending, both returning IOrderedEnumerable<T>.
- ThenBy() and ThenByDescending() add secondary sort keys and can only be chained after an existing order.
- Calling OrderBy() twice re-sorts from scratch, discarding the prior order — use ThenBy() for tiebreakers instead.
- Custom IComparer<TKey> overloads let you override default comparison logic, such as case-insensitive or business-defined priority sorting.
- LINQ's sort operators are stable — elements with equal keys retain their original relative order.
- IOrderedEnumerable<T> is the specialized return type that enables ThenBy() chaining.
- Sort stability lets you rely on insertion order as an implicit final tiebreaker without an explicit ThenBy().
Practice what you learned
1. What is the return type of players.OrderBy(p => p.Score)?
2. What happens if you call OrderBy() a second time on an already-sorted sequence instead of using ThenBy()?
3. What guarantee do LINQ's OrderBy()/ThenBy() operators provide regarding elements with equal keys?
4. What does passing a custom IComparer<TKey> to OrderBy() change?
5. Which method must you call after OrderBy() to add a secondary sort key without disturbing the primary sort?
Was this page helpful?
You May Also Like
Filtering with Where
Learn how LINQ's Where operator filters sequences using predicates, how deferred execution affects when the filter actually runs, and how to combine multiple conditions safely.
Grouping with GroupBy
Learn how LINQ's GroupBy operator clusters elements by a key into IGrouping sequences, including result selectors, composite keys, and how it differs from SQL's GROUP BY.
Projection with Select
Learn how LINQ's Select operator transforms each element of a sequence into a new shape, including anonymous types, the indexed overload, and how it differs from SelectMany.
Related Reading
Related Study Notes in Microsoft Technologies
Browse all study notesWindows 10 / UWP Development Study Notes
.NET · 30 topics
Microsoft TechnologiesWindows Batch Scripting Study Notes
Batch · 30 topics
Microsoft TechnologiesMFC (Microsoft Foundation Classes) Study Notes
C++ · 30 topics
Microsoft TechnologiesSilverlight Study Notes
.NET · 30 topics
Microsoft TechnologiesXAML Study Notes
.NET · 30 topics
Microsoft TechnologiesWPF (Windows Presentation Foundation) Study Notes
.NET · 30 topics