C# LINQ Ordering and Grouping

LINQ ordering and grouping let you turn plain sequences into useful reports: sorted lists, ranked results, categories, subtotals, and summaries. They matter because most application data is not useful in the order it arrives, and related items often need to be analyzed together.

In C#, the main operators are OrderBy, OrderByDescending, ThenBy, ThenByDescending, and GroupBy. They work with any IEnumerable<T>, including arrays, lists, query results, and many custom collections.

Overview: How It Works

LINQ ordering creates a new ordered view of a sequence. It does not sort a List<T> in place. When you call OrderBy, LINQ records the source sequence, the key selector, and the comparer. The actual sorting happens later, when you enumerate the query with foreach, ToList, ToArray, First, or a similar terminal operation.

The ordering operators produce an IOrderedEnumerable<T>. This special interface remembers the previous sort keys so that ThenBy can add a secondary key instead of replacing the first one. For example, sorting people by last name and then by first name should keep all Smiths together, then order the Smiths internally. Calling OrderBy twice does not mean the same thing; the second primary order starts a new ordering chain.

LINQ to Objects uses an in-memory sort. During enumeration it reads the source items, computes keys, sorts indexes according to those keys, and yields items in sorted order. The sort is stable, meaning equal keys keep their original relative order unless a later ThenBy key separates them. This is important when data already has a meaningful order such as upload order or priority order.

Grouping creates buckets of items that share the same key. GroupBy returns an IEnumerable<IGrouping<TKey,TElement>>. Each group has a Key property and can itself be enumerated like a sequence. In LINQ to Objects, grouping must read the whole source before it can yield complete groups, because it has to know every item that belongs to each key.

Ordering and grouping are often combined. A typical report groups orders by customer, computes totals inside each group, then orders the groups by total revenue. The key idea is to be clear about what you are sorting: individual items before grouping, elements inside each group, or the groups themselves.

Syntax

var ordered = source.OrderBy(item => item.PrimaryKey)
                    .ThenBy(item => item.SecondaryKey);

var groups = source.GroupBy(item => item.GroupKey);

var report = source.GroupBy(item => item.GroupKey)
                   .Select(group => new
                   {
                       Key = group.Key,
                       Count = group.Count()
                   })
                   .OrderBy(row => row.Key);
Part Meaning
source Any sequence, usually an IEnumerable<T>.
OrderBy Starts a new ascending primary sort.
OrderByDescending Starts a new descending primary sort.
ThenBy Adds an ascending secondary sort to an existing ordered query.
ThenByDescending Adds a descending secondary sort to an existing ordered query.
GroupBy Partitions items by a key and returns groups.
group.Key The shared key for one group.

Examples

Sorting Simple Values

using System;
using System.Linq;

class Program
{
    static void Main()
    {
        int[] scores = { 72, 95, 81, 95, 60 };

        var lowToHigh = scores.OrderBy(score => score);
        var highToLow = scores.OrderByDescending(score => score);

        Console.WriteLine("Ascending: " + string.Join(", ", lowToHigh));
        Console.WriteLine("Descending: " + string.Join(", ", highToLow));
    }
}

Output:

Ascending: 60, 72, 81, 95, 95
Descending: 95, 95, 81, 72, 60

The key selector score => score says that each number is its own sort key. Notice that the original array is not changed; the query produces items in sorted order when string.Join enumerates it.

Sorting Objects with Multiple Keys

using System;
using System.Linq;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        List students = new()
        {
            new Student("Mia", "Chen", 91),
            new Student("Omar", "Lopez", 88),
            new Student("Ava", "Chen", 91),
            new Student("Noah", "Chen", 84),
            new Student("Zoe", "Lopez", 88)
        };

        var sorted = students
            .OrderByDescending(student => student.Score)
            .ThenBy(student => student.LastName)
            .ThenBy(student => student.FirstName);

        foreach (Student student in sorted)
        {
            Console.WriteLine($"{student.Score}: {student.LastName}, {student.FirstName}");
        }
    }
}

record Student(string FirstName, string LastName, int Score);

Output:

91: Chen, Ava
91: Chen, Mia
88: Lopez, Omar
88: Lopez, Zoe
84: Chen, Noah

This query ranks students by score from high to low. When scores tie, it orders by last name, then first name. ThenBy is the correct operator for tie breakers because it extends the existing ordering instead of starting over.

Grouping and Aggregating

using System;
using System.Linq;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        List orders = new()
        {
            new Order("Books", "Notebook", 7.50m),
            new Order("Games", "Chess Set", 22.00m),
            new Order("Books", "C# Guide", 31.00m),
            new Order("Games", "Card Deck", 5.00m),
            new Order("Office", "Desk Lamp", 18.25m)
        };

        var report = orders
            .GroupBy(order => order.Category)
            .Select(group => new
            {
                Category = group.Key,
                Count = group.Count(),
                Total = group.Sum(order => order.Price)
            })
            .OrderByDescending(row => row.Total);

        foreach (var row in report)
        {
            Console.WriteLine($"{row.Category}: {row.Count} orders, ${row.Total:F2}");
        }
    }
}

record Order(string Category, string Item, decimal Price);

Output:

Books: 2 orders, $38.50
Games: 2 orders, $27.00
Office: 1 orders, $18.25

GroupBy creates one group per category. The Select projection turns each group into a small summary object, and the final OrderByDescending sorts those summaries by total revenue.

How It Works Step by Step

For an ordering query, the CLR does not immediately rearrange memory. The extension method returns an iterator object that stores the source and delegates. When enumeration begins, LINQ pulls every item from the source into internal buffers, invokes the key selector for each item, sorts based on the keys and comparer, then yields the original items in the computed order. Because the entire input must be sorted, OrderBy cannot stream one item at a time the way Where can.

For grouping, LINQ builds a lookup table from keys to lists of elements. It reads each source item, computes its key, compares that key using the default equality comparer unless you provide one, and appends the item to the matching group. Only after the source has been consumed can the final groups be enumerated reliably.

These details affect performance. Sorting is generally O(n log n). Grouping is usually close to O(n), but it requires memory for the groups and depends on good hash behavior for keys. If your source is a database query through Entity Framework, similar method names may be translated to SQL instead of using LINQ to Objects, so the database engine performs the ordering or grouping.

Common Mistakes

Using OrderBy Twice for Tie Breakers

var sorted = students
    .OrderBy(student => student.LastName)
    .OrderBy(student => student.FirstName);

The second OrderBy starts a new primary sort and discards the previous ordering as the main key. Use ThenBy when the second key should only break ties.

var sorted = students
    .OrderBy(student => student.LastName)
    .ThenBy(student => student.FirstName);

Expecting OrderBy to Modify the Original List

using System;
using System.Linq;
using System.Collections.Generic;

class Program
{
    static void Main()
    {
        List numbers = new() { 3, 1, 2 };

        numbers.OrderBy(number => number);
        Console.WriteLine("Original list: " + string.Join(", ", numbers));

        List sorted = numbers.OrderBy(number => number).ToList();
        Console.WriteLine("Sorted copy: " + string.Join(", ", sorted));
    }
}

Output:

Original list: 3, 1, 2
Sorted copy: 1, 2, 3

OrderBy returns a query. If you want a new list, call ToList. If you specifically want to mutate an existing List<T>, use List<T>.Sort instead of LINQ.

Forgetting Case Sensitivity in Group Keys

String grouping uses the default equality comparer unless told otherwise. That means "Books" and "books" are different keys. When user-entered text should be treated as the same category, normalize the key or pass an appropriate comparer.

Best Practices

  • Use ThenBy and ThenByDescending for secondary, tertiary, and later sort keys.
  • Materialize with ToList or ToArray when you need to store the sorted result or enumerate it many times.
  • Sort groups after GroupBy when you want to order categories; sort elements inside each group when you want ordered items within categories.
  • Choose keys that are simple and stable. Avoid key selectors with side effects because deferred execution may call them later than expected.
  • Use StringComparer.OrdinalIgnoreCase or a normalized key for case-insensitive grouping or sorting.
  • Remember that ordering and grouping buffer data in memory for LINQ to Objects. Be careful with very large sequences.

Practice Exercises

  1. Create a list of products with name, category, and price. Print all products ordered by category ascending and price descending.
  2. Group a list of employees by department and print each department name with its employee count.
  3. Given a list of sales, group by salesperson, compute the total, and print salespeople ordered from highest total to lowest.

Summary

  • OrderBy and OrderByDescending start a new primary sort.
  • ThenBy and ThenByDescending add tie breakers to an existing ordered query.
  • GroupBy returns groups with a Key and the matching elements.
  • LINQ ordering and grouping use deferred execution but must buffer source data when enumerated.
  • Use projections and aggregates such as Count and Sum to turn groups into useful reports.