.NET Academy

LINQ: grouping, joining and flattening

Summarize data with GroupBy, combine sequences with Join and SelectMany, and meet query syntax.

13 min read

You will learn

  • Group elements and aggregate each group
  • Build dictionaries with ToDictionary
  • Combine two sequences with Join
  • Flatten nested collections with SelectMany and read query syntax

Beyond filtering and sorting

Real reports ask questions like how much did each customer spend?, which products appear in which orders?, what are all the tags across all posts? LINQ has operators for summarizing, combining and flattening data — the same ideas as SQL's GROUP BY and JOIN, which you'll meet in Backend Fundamentals.

Sample data:

record Order(int Id, string Customer, string Product, int Quantity, decimal UnitPrice);
record Customer(string Name, string City);
 
var orders = new List<Order>
{
    new(1, "Ada", "Pen", 10, 1.5m),
    new(2, "Bob", "Laptop", 1, 999m),
    new(3, "Ada", "Notebook", 3, 4m),
    new(4, "Cy",  "Pen", 2, 1.5m),
};
var customers = new List<Customer> { new("Ada", "London"), new("Bob", "Paris"), new("Cy", "Paris") };

GroupBy: summarize per key

GroupBy splits a sequence into groups sharing a key. Each group has a .Key and is itself a sequence:

var spending = orders
    .GroupBy(o => o.Customer)
    .Select(g => new
    {
        Customer = g.Key,
        Orders = g.Count(),
        Total = g.Sum(o => o.Quantity * o.UnitPrice),
    })
    .OrderByDescending(x => x.Total);
 
foreach (var s in spending)
    Console.WriteLine($"{s.Customer}: {s.Orders} orders, {s.Total:F2}");
// Bob: 1 orders, 999.00
// Ada: 2 orders, 27.00
// Cy: 1 orders, 3.00

ToDictionary and ToLookup

Turn a query into a fast lookup:

Dictionary<string, decimal> totals = orders
    .GroupBy(o => o.Customer)
    .ToDictionary(g => g.Key, g => g.Sum(o => o.Quantity * o.UnitPrice));
 
Console.WriteLine(totals["Ada"]);   // 27.0
 
var byProduct = orders.ToLookup(o => o.Product);   // like a dictionary of groups
foreach (var o in byProduct["Pen"]) Console.WriteLine(o.Customer);   // Ada, Cy

ToDictionary throws on duplicate keys; ToLookup allows many values per key.

Join: combine two sequences

Match elements from two sources by a shared key — like SQL's inner join:

var ordersWithCity = orders.Join(
    customers,
    order => order.Customer,          // key from the first sequence
    customer => customer.Name,        // key from the second
    (order, customer) => $"#{order.Id} {order.Product} → {customer.City}");
// #1 Pen → London, #2 Laptop → Paris, ...

SelectMany: flatten

When each element contains a collection, Select gives you a sequence of collections. SelectMany flattens them into one sequence:

var posts = new[]
{
    new { Title = "LINQ tips", Tags = new[] { "csharp", "linq" } },
    new { Title = "Async basics", Tags = new[] { "csharp", "async" } },
};
 
var allTags = posts.SelectMany(p => p.Tags).Distinct();   // csharp, linq, async

More useful operators

var pairs = names.Zip(scores, (n, s) => $"{n}: {s}");     // combine two sequences pairwise
var batches = items.Chunk(100);                            // split into arrays of 100
var total = numbers.Aggregate((acc, n) => acc * n);        // general fold — here, a product
bool same = list1.SequenceEqual(list2);
var both = a.Intersect(b); var either = a.Union(b); var onlyA = a.Except(b);

Query syntax

LINQ also has a SQL-like query syntax. The compiler translates it into the same method calls:

var bigSpenders =
    from o in orders
    where o.Quantity * o.UnitPrice > 10
    orderby o.Customer
    select new { o.Customer, o.Product };
 
var joined =
    from o in orders
    join c in customers on o.Customer equals c.Name
    group o by c.City into cityGroup
    select new { City = cityGroup.Key, Orders = cityGroup.Count() };

Most C# code uses method syntax, but query syntax can be easier to read for joins and multi-step groupings. Every query-syntax query can be written with methods; not every method (like Count, First or Distinct) has a query keyword.

Tip

In Track 10, the exact same LINQ queries run against a database: Entity Framework Core translates Where, GroupBy and Join into SQL.

Try it

Program.cs
Loading editor…

C# runtime not loaded

Key takeaways

  • GroupBy creates groups with a Key; aggregate each group with Count, Sum, etc.
  • ToDictionary / ToLookup turn results into fast lookups.
  • Join matches two sequences by key; SelectMany flattens nested collections.
  • Query syntax (from … where … select) compiles to the same method calls.

Exercise

Using the orders from Orders(): TotalByCustomer maps each customer to the total they spent (quantity × unit price). TopCustomer returns the customer who spent the most. ProductsBoughtBy returns the distinct products a customer bought, sorted alphabetically.

Program.cs
Loading editor…

C# runtime not loaded

Hint 1

TotalByCustomer: orders.GroupBy(o => o.Customer).ToDictionary(g => g.Key, g => g.Sum(o => o.Quantity * o.UnitPrice))

Hint 2

TopCustomer: reuse TotalByCustomer and take MaxBy(pair => pair.Value).Key.

Hint 3

ProductsBoughtBy: Where → Select(o => o.Product) → Distinct() → Order() → ToList().

Show solution
foreach (var (customer, total) in TotalByCustomer(Orders()))
    Console.WriteLine($"{customer}: {total}");

Dictionary<string, decimal> TotalByCustomer(List<Order> orders) =>
    orders
        .GroupBy(o => o.Customer)
        .ToDictionary(g => g.Key, g => g.Sum(o => o.Quantity * o.UnitPrice));

string TopCustomer(List<Order> orders) =>
    TotalByCustomer(orders).MaxBy(pair => pair.Value).Key;

List<string> ProductsBoughtBy(List<Order> orders, string customer) =>
    orders
        .Where(o => o.Customer == customer)
        .Select(o => o.Product)
        .Distinct()
        .Order()
        .ToList();

List<Order> Orders() =>
[
    new(1, "Ada", "Pen", 10, 1.5m),
    new(2, "Bob", "Laptop", 1, 999m),
    new(3, "Ada", "Notebook", 3, 4m),
    new(4, "Cy", "Pen", 2, 1.5m),
    new(5, "Ada", "Pen", 5, 1.5m),
    new(6, "Bob", "Mouse", 2, 20m),
];

record Order(int Id, string Customer, string Product, int Quantity, decimal UnitPrice);

Quiz

4 questions · 3 correct to pass
  1. 1.What does each element of `orders.GroupBy(o => o.Customer)` contain?

  2. 2.When do you need SelectMany instead of Select?

  3. 3.What does `ToDictionary(p => p.Id)` do if two elements have the same Id?

  4. 4.Which query-syntax line is equivalent to `people.Where(p => p.Age > 30).Select(p => p.Name)`?

To complete this lesson

  • ○ Solve the exercise
  • ○ Pass the quiz (3/4 correct)