C# LINQ ThenBy: Sorting by Multiple Keys
c# linq thenby: Learn how to use C# LINQ ThenBy to sort data by multiple keys, handle descending order, and avoid common pitfalls.
When you need to sort a collection by more than one property, OrderBy alone is not enough. The ThenBy method in C# LINQ adds a secondary sort key that is applied after the primary key, and it can be chained to build a multi-level sort. This article explains how c# linq thenby works, how to combine it with OrderBy, and where its stable sort behavior matters.
OrderBy and ThenBy Basics
The OrderBy method returns an IOrderedEnumerable<T> rather than a plain IEnumerable<T>. That interface exposes ThenBy and ThenByDescending, which let you add subsequent sort keys. The sequence is sorted first by the key passed to OrderBy, then by each key passed to ThenBy in the order they appear.
var sorted = people .OrderBy(p => p.LastName) .ThenBy(p => p.FirstName);
This sorts by last name first, and for people with the same last name, it sorts by first name. The result is a new IOrderedEnumerable<Person>; the original people collection is not modified.
ThenBy can only be called on an IOrderedEnumerable<T>. If you try to call it on a plain IEnumerable<T>, you will get a compile-time error because the method is not defined on that interface. This is a deliberate design that forces you to establish the primary key before adding secondary ones.
Chaining ThenBy for Multiple Sort Keys
There is no practical limit to how many ThenBy calls you can chain. Each call adds another level of sorting, and the order of the calls determines the priority.
var sorted = employees .OrderBy(e => e.Department) .ThenBy(e => e.LastName) .ThenBy(e => e.FirstName);
Here, employees are grouped by department, then within each department sorted by last name, and within identical last names sorted by first name. The sequence of keys is significant: swapping OrderBy and ThenBy would produce a completely different order.
Each ThenBy call returns a new IOrderedEnumerable<T>, so you can continue chaining. The underlying sort is performed lazily when you iterate the sequence, not when you build the query. This means the sort keys are evaluated on each element at that point, and if the source collection changes before iteration, the sort reflects the current state.
Descending Secondary Sorts with ThenByDescending
When a secondary key should be sorted in descending order, use ThenByDescending. It works exactly like ThenBy but reverses the comparison for that key only.
var sorted = products .OrderBy(p => p.Category) .ThenByDescending(p => p.Price);
This sorts products by category in ascending order, and within each category by price from highest to lowest. You can mix ThenBy and ThenByDescending freely. For example, you might sort customers by country ascending, then by total purchases descending.
ThenByDescending is also available on IOrderedEnumerable<T> and follows the same chaining rules as ThenBy.
Using ThenBy in Query Syntax
C# query syntax provides a more compact way to express multi-key sorts. The orderby clause accepts multiple keys separated by commas, and you can apply ascending or descending to each key individually.
var sorted = from e in employees orderby e.Department, e.LastName descending, e.FirstName select e;
This query is compiled into the same method calls as the fluent syntax: OrderBy for the first key, then ThenBy or ThenByDescending for each subsequent key. The descending keyword on LastName maps to ThenByDescending. Query syntax can be more readable when the sort logic is simple, but the fluent syntax gives you finer control when you need to pass comparers or build the sort dynamically.
Stable Sorting and Performance Behavior
LINQ's OrderBy and ThenBy perform a stable sort. A stable sort preserves the original order of elements that compare as equal. This is particularly important when you apply multiple sort keys. For example, if two people have the same last name and first name, their relative order in the input is maintained. This behavior is guaranteed by the LINQ to Objects implementation, and it is what makes chaining ThenBy produce intuitive results.
The time complexity of OrderBy followed by any number of ThenBy calls is O(n log n) for the entire sort, where n is the number of elements. Each additional key adds a comparison step but does not change the asymptotic complexity. The sort is not in-place; it allocates a new sequence. For large collections, this memory overhead is usually acceptable, but if you are sorting very large arrays in a memory-constrained environment, you might consider Array.Sort with a custom comparer, which sorts in place. However, Array.Sort is not stable, so you would lose the stability guarantee.
Because the sort is deferred, the sort keys are evaluated only when the sequence is enumerated. If the key selector functions are expensive, that cost is paid once per element per key at that time. Reusing the sorted result across multiple iterations avoids re-evaluating the keys, so materialize the result with ToList() or ToArray() if you need to iterate it several times.
Common Mistakes and Edge Cases
A frequent mistake is trying to call ThenBy on a variable typed as IEnumerable<T>. The compiler will reject it because ThenBy is not defined on that interface. Always ensure the variable is IOrderedEnumerable<T> or use OrderBy first in the same chain.
Another issue is null values in the sort key. The default comparer for reference types handles nulls by placing them first in ascending order. This is usually acceptable, but if you need a different behavior, you can pass a custom comparer to OrderBy or ThenBy. For example, to treat nulls as the lowest value, you can use Comparer<string>.Create((a, b) => (a == null ? -1 : 0) - (b == null ? -1 : 0)).
String sorting is case-sensitive by default. If you want case-insensitive ordering, use StringComparer.OrdinalIgnoreCase as the comparer. This is a common requirement when sorting names or product codes.
var sorted = names .OrderBy(n => n, StringComparer.OrdinalIgnoreCase) .ThenBy(n => n.Length);
Be aware that ThenBy uses the same comparer as the preceding OrderBy if you do not specify one. If you need a different comparer for the secondary key, you can pass it as the second argument to ThenBy.
When a Custom Comparer Is Better
ThenBy is the clearest way to express a fixed set of sort keys. However, when the sort logic depends on runtime conditions or involves complex comparisons, a custom IComparer<T> might be more maintainable.
For example, if you need to sort by a property that is not directly accessible, or if the sort order changes based on user input, you can write a comparer that encapsulates the logic and pass it to OrderBy.
public class PersonComparer : IComparer<Person> { public int Compare(Person x, Person y) { int result = string.Compare(x.LastName, y.LastName, StringComparison.OrdinalIgnoreCase); if (result != 0) return result; return string.Compare(x.FirstName, y.FirstName, StringComparison.OrdinalIgnoreCase); } } var sorted = people.OrderBy(p => p, new PersonComparer());
This approach centralizes the sort logic and makes it reusable. It also allows you to incorporate additional rules, such as treating certain values as equal or applying culture-specific comparisons. For simple multi-key sorts, ThenBy is more concise and self-documenting. Use a custom comparer when the sort logic is too complex to express with a chain of ThenBy calls, or when the same sorting rule is needed in multiple places.