Ordering and Grouping
OrderBy and ThenBy sort by one key then another, and GroupBy hands back groupings that each carry a Key and the elements under it.
IOrderedEnumerable<T> OrderBy<T, TKey>(this IEnumerable<T> source, Func<T, TKey> keySelector)Sorting and grouping are the two operators that rearrange a sequence rather than shrink or reshape it. Both are worth learning carefully, because both have a trap that looks like working code.
Ordering
OrderBy takes a lambda that returns the value to sort on, called the key. It returns an IOrderedEnumerable<T>, not a plain IEnumerable<T>. That matters because ThenBy and ThenByDescending only exist on IOrderedEnumerable<T>. ThenBy is the tie-breaker: it decides the order of elements whose first keys are equal. Calling OrderBy a second time instead re-sorts from scratch and discards the first sort completely.
Each has a descending twin: OrderByDescending and ThenByDescending. When you want to sort by the elements themselves rather than by some key inside them, Order() and OrderDescending(), added in .NET 7, are shorthand for OrderBy(x => x) and OrderByDescending(x => x).
Grouping
GroupBy takes a key lambda and returns IEnumerable<IGrouping<TKey, TElement>>. An IGrouping<TKey, TElement> is two things at once: it has a Key property, and it is itself a sequence of the elements that share that key. So you can call Count(), Sum() or Select() directly on a grouping. Groups come back in the order their keys were first seen, and elements inside a group keep their original order.
In query syntax, orderby a, b compiles to OrderBy(a).ThenBy(b), and group x by key compiles to GroupBy.
Example
Employee[] staff =
[
new Employee("Ada", "Engineering", 3),
new Employee("Bea", "Sales", 5),
new Employee("Cal", "Engineering", 5),
new Employee("Dot", "Sales", 1)
];
// OrderBy sorts by a key. ThenBy breaks ties, and only exists on the result
// of an OrderBy, which is an IOrderedEnumerable<T> rather than a plain
// IEnumerable<T>.
IOrderedEnumerable<Employee> ranked = staff
.OrderByDescending(e => e.Years)
.ThenBy(e => e.Name);
foreach (Employee e in ranked)
{
Console.WriteLine($"{e.Years} {e.Name}");
}
// GroupBy gives one IGrouping per distinct key. A grouping has a Key and is
// itself a sequence of the elements under that key, so Count and Select
// work on it directly.
foreach (IGrouping<string, Employee> grouping in staff.GroupBy(e => e.Department))
{
string names = string.Join("/", grouping.Select(e => e.Name));
Console.WriteLine($"{grouping.Key}: {grouping.Count()} [{names}]");
}
// Output:
// 5 Bea
// 5 Cal
// 3 Ada
// 1 Dot
// Engineering: 2 [Ada/Cal]
// Sales: 2 [Bea/Dot]
record Employee(string Name, string Department, int Years);When to use it
- Sort a results table by score descending, then alphabetically by name so equal scores have a stable order.
- Count how many orders each customer placed, for a dashboard row per customer.
- Bucket log entries by severity and show the newest three in each bucket.
More examples
Order and OrderDescending, next to the longhand
Use Order() for a sequence of numbers, strings or anything else that already knows how to compare itself. Use OrderBy when the thing you sort on lives inside the element. If you are targeting .NET 6 or earlier, Order() does not exist and you must write the identity lambda. Both forms also take an IComparer<T> argument when the default comparison is not the one you want.
int[] scores = [42, 7, 19, 7, 88];
// Sorting by the element itself. These two lines are the same thing:
// Order() was added in .NET 7 as shorthand for OrderBy with a lambda that
// hands the element straight back.
IEnumerable<int> longhand = scores.OrderBy(n => n);
IEnumerable<int> shorthand = scores.Order();
// Descending comes as the same pair.
IEnumerable<int> downLonghand = scores.OrderByDescending(n => n);
IEnumerable<int> downShorthand = scores.OrderDescending();
Console.WriteLine(string.Join(", ", longhand));
Console.WriteLine(string.Join(", ", shorthand));
Console.WriteLine(string.Join(", ", downLonghand));
Console.WriteLine(string.Join(", ", downShorthand));
// Output:
// 7, 7, 19, 42, 88
// 7, 7, 19, 42, 88
// 88, 42, 19, 7, 7
// 88, 42, 19, 7, 7The double-OrderBy bug, and the query-syntax equivalent
This is the sorting bug that reaches production, because the output is sorted and looks plausible. It usually appears when two people edit the same query and each adds an OrderBy. The query-syntax form is immune by construction, which is a fair reason to prefer it for multi-key sorts.
(string Sku, int Qty)[] lines = [("pen", 2), ("mug", 9), ("pad", 2)];
// Wrong. The second OrderBy re-sorts everything by Sku and the Qty sort is
// gone. Only Sku ends up affecting the result.
IEnumerable<(string Sku, int Qty)> wrong = lines
.OrderBy(l => l.Qty)
.OrderBy(l => l.Sku);
// Right. ThenBy sorts by Sku only within runs of equal Qty.
IEnumerable<(string Sku, int Qty)> right = lines
.OrderBy(l => l.Qty)
.ThenBy(l => l.Sku);
// Query syntax: one orderby clause with a comma compiles to OrderBy plus
// ThenBy, which is why it cannot make this mistake.
IEnumerable<(string Sku, int Qty)> same =
from l in lines
orderby l.Qty, l.Sku
select l;
Console.WriteLine(string.Join(" ", wrong.Select(l => l.Sku)));
Console.WriteLine(string.Join(" ", right.Select(l => l.Sku)));
Console.WriteLine(string.Join(" ", same.Select(l => l.Sku)));
// Output:
// mug pad pen
// pad pen mug
// pad pen mugGroupBy that projects the elements as it groups
Grouping and projecting in one step keeps the groupings small, which matters when the source elements are large objects and you only need one field from each. Note that string.Join and Sum are called on the grouping itself, with no .Values or .Items in between: a grouping is a sequence. Also note that GroupBy has to read the entire source before it can hand out the first group, so it cannot stream.
string[] words = ["apple", "avocado", "beet", "cherry", "basil"];
// The two-lambda GroupBy takes a key selector and an element selector, so
// each grouping holds word lengths rather than the words themselves.
// OrderBy on the groupings makes the printed order independent of input.
IOrderedEnumerable<IGrouping<char, int>> byInitial = words
.GroupBy(w => w[0], w => w.Length)
.OrderBy(g => g.Key);
foreach (IGrouping<char, int> grouping in byInitial)
{
Console.WriteLine($"{grouping.Key}: {string.Join("+", grouping)} = {grouping.Sum()}");
}
// Output:
// a: 5+7 = 12
// b: 4+5 = 9
// c: 6 = 6
Discussion