100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
C#

Ordering with OrderBy

Learn how LINQ's OrderBy and OrderByDescending sort sequences, how ThenBy chains secondary sort keys, and how sort stability and custom comparers work.

Core OperatorsBeginner9 min readJul 10, 2026
Analogies

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.

csharp
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 scratch

OrderBy() 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

Was this page helpful?

Topics covered

#LINQStudyNotes#MicrosoftTechnologies#OrderingWithOrderBy#Ordering#OrderBy#Sorting#OrderByDescending#StudyNotes#SkillVeris#ExamPrep