Grouping and Projecting Data with LINQ (group, by, into) in C#

Organizing Data by Category

Imagine you're building a sales dashboard that needs to show revenue by region, products by category, or customers by signup month. You have a flat list of data but need it organized into logical groups. Manually looping through collections and building dictionaries is tedious and error-prone.

LINQ's group by keywords let you organize collections into categories with a single expression. You specify a key selector—how items should be grouped—and LINQ handles the rest. The into keyword extends this by letting you query the groups themselves, filtering or transforming them before final projection.

You'll learn basic grouping syntax, how to use into for query continuation, and patterns for aggregating group data. By the end, you'll transform raw data into organized summaries without writing manual grouping logic.

Basic group by Syntax

The group clause takes two parts: what to group (the element) and by what key. The result is an IEnumerable<IGrouping<TKey, TElement>>—a sequence of groups where each group has a Key property and contains all elements matching that key. You can iterate groups directly or project them into a more useful shape.

Without LINQ, you'd manually loop through items, check if a key exists in a dictionary, and add items to lists. Group by handles all of this in one expression, making your code declarative and easier to read. The compiler translates it to efficient GroupBy method calls.

Here's basic grouping in action:

BasicGrouping.cs - Simple group by
var products = new List<Product>
{
    new("Laptop", "Electronics", 999),
    new("Mouse", "Electronics", 25),
    new("Desk", "Furniture", 350),
    new("Chair", "Furniture", 200),
    new("Keyboard", "Electronics", 75)
};

// Group by category
var byCategory = from p in products
                 group p by p.Category;

foreach (var group in byCategory)
{
    Console.WriteLine($"{group.Key}:");
    foreach (var product in group)
    {
        Console.WriteLine($"  {product.Name} - ${product.Price}");
    }
}

// Method syntax equivalent
var byCategoryMethod = products.GroupBy(p => p.Category);

record Product(string Name, string Category, decimal Price);

The query groups products by their Category property. Each group is an IGrouping with a Key (the category name) and a sequence of Product objects in that category. You can enumerate the groups and then enumerate items within each group. The method syntax version produces identical results—choose based on readability.

Query Continuation with into

The into keyword creates a range variable for the grouped results, letting you continue querying. After group by into, you can apply where clauses to filter groups, orderby to sort them, or select to project them. This is powerful for filtering aggregates—like showing only categories with more than 3 items or sorting groups by their count.

Without into, your group query ends immediately after grouping. With into, you can chain additional operations on the groups themselves before projecting final results. This eliminates the need for separate filtering steps after grouping.

Using into for continued querying:

GroupContinuation.cs - Using into
var sales = new List<Sale>
{
    new("North", "Q1", 15000),
    new("North", "Q2", 18000),
    new("South", "Q1", 12000),
    new("West", "Q1", 9000),
    new("West", "Q2", 11000),
    new("West", "Q3", 13000)
};

// Group by region, filter groups with 2+ sales, project summary
var regionSummary = from s in sales
                    group s by s.Region into regionGroup
                    where regionGroup.Count() >= 2
                    orderby regionGroup.Key
                    select new
                    {
                        Region = regionGroup.Key,
                        TotalSales = regionGroup.Sum(x => x.Amount),
                        QuarterCount = regionGroup.Count()
                    };

foreach (var summary in regionSummary)
{
    Console.WriteLine($"{summary.Region}: ${summary.TotalSales:N0} " +
        $"across {summary.QuarterCount} quarters");
}

record Sale(string Region, string Quarter, decimal Amount);

After grouping by Region, into creates regionGroup as a variable representing each group. We then filter groups with at least 2 sales, sort by region name, and project a summary with total sales and count. The where clause operates on groups (not individual sales), and orderby sorts the groups. This produces clean, aggregated results in one query.

Projecting Group Results

Often you don't want the raw IGrouping objects—you want summaries or specific shapes. You can project groups inline or use into with a final select. Common projections include aggregates like Count, Sum, Average, or extracting specific fields from the key and elements.

Projecting during grouping creates anonymous types or records with exactly the fields you need. This is cleaner than iterating groups later and manually building result objects. LINQ handles the iteration and transformation in one pass.

Different projection patterns:

GroupProjections.cs - Shaping group results
var orders = new List<Order>
{
    new(1, "Alice", 50),
    new(2, "Bob", 75),
    new(3, "Alice", 30),
    new(4, "Charlie", 100),
    new(5, "Bob", 45)
};

// Direct projection without into
var customerTotals = from o in orders
                     group o.Amount by o.Customer into g
                     select new { Customer = g.Key, Total = g.Sum() };

// Projection with detailed group info
var customerDetails = from o in orders
                      group o by o.Customer into g
                      select new
                      {
                          Customer = g.Key,
                          OrderCount = g.Count(),
                          TotalAmount = g.Sum(x => x.Amount),
                          AvgOrder = g.Average(x => x.Amount),
                          Orders = g.Select(x => x.Id).ToList()
                      };

foreach (var detail in customerDetails)
{
    Console.WriteLine($"{detail.Customer}: {detail.OrderCount} orders, " +
        $"${detail.TotalAmount} total (avg ${detail.AvgOrder:F2})");
}

record Order(int Id, string Customer, decimal Amount);

The first query projects just customer and total, grouping amounts directly. The second creates a detailed summary with count, sum, average, and a list of order IDs. Each IGrouping supports all LINQ operators—Count, Sum, Average, Select, Where, etc. You can build rich summaries without leaving the query expression.

Nested and Multi-Level Grouping

Sometimes you need grouping within groups—like products by category and then by price range, or sales by year and then by quarter. You can nest group clauses or use composite keys. For simple two-level grouping, use an anonymous type as the key.

Nested grouping creates a hierarchy of IGrouping objects. You can iterate the outer groups, then inner groups, or project them into nested structures. This is powerful for creating hierarchical reports or drill-down data.

Multi-level grouping examples:

NestedGroups.cs - Hierarchical grouping
var transactions = new List<Transaction>
{
    new(2024, "Q1", "North", 5000),
    new(2024, "Q1", "South", 3000),
    new(2024, "Q2", "North", 6000),
    new(2024, "Q2", "South", 4000),
    new(2025, "Q1", "North", 5500)
};

// Composite key grouping
var byYearQuarter = from t in transactions
                    group t by new { t.Year, t.Quarter } into g
                    select new
                    {
                        Period = $"{g.Key.Year} {g.Key.Quarter}",
                        Total = g.Sum(x => x.Amount)
                    };

// Nested grouping with projection
var hierarchical = from t in transactions
                   group t by t.Year into yearGroup
                   select new
                   {
                       Year = yearGroup.Key,
                       Quarters = from q in yearGroup
                                  group q by q.Quarter into qGroup
                                  select new
                                  {
                                      Quarter = qGroup.Key,
                                      Regions = from r in qGroup
                                                group r by r.Region into rGroup
                                                select new
                                                {
                                                    Region = rGroup.Key,
                                                    Total = rGroup.Sum(x => x.Amount)
                                                }
                                  }
                   };

record Transaction(int Year, string Quarter, string Region, decimal Amount);

The first query groups by a composite key (both year and quarter), producing flat groups. The second creates a three-level hierarchy: year → quarter → region. Each level uses group...into with a nested query. This builds a tree structure perfect for hierarchical reports or JSON serialization.

Choosing Your Grouping Approach

Query syntax vs method syntax: Use query syntax for complex queries with multiple clauses. It reads naturally for group-filter-project workflows. Use method syntax (GroupBy) for simple grouping or when chaining with other method calls. Both produce identical compiled code.

Projection timing: Project groups immediately if you know the final shape. Use into when you need to filter or sort groups before projection. Immediate projection is cleaner when you don't need group filtering.

Memory considerations: Grouping loads all data into memory to organize it. For large datasets with database sources, ensure grouping happens in SQL (via IQueryable) rather than in-memory. Check the generated SQL to verify group by translates to the database.

Alternative: ToDictionary vs GroupBy: If you need fast key lookup after grouping, consider ToDictionary or ToLookup. Lookup preserves multi-valued keys like GroupBy but returns a dictionary-like structure. This is faster for repeated key-based access than iterating groups.

Try It Yourself

Build a console app that groups sample data and produces summary reports using group, by, and into.

Steps

  1. Init: dotnet new console -n GroupByDemo
  2. Open: cd GroupByDemo
  3. Replace Program.cs with the code shown below
  4. Execute: dotnet run
Demo.cs
var employees = new List<Employee>
{
    new("Alice", "Engineering", 85000),
    new("Bob", "Engineering", 92000),
    new("Carol", "Sales", 75000),
    new("Dave", "Sales", 68000),
    new("Eve", "Engineering", 95000),
    new("Frank", "Marketing", 72000)
};

// Group by department with summary
var deptSummary = from e in employees
                  group e by e.Department into dept
                  where dept.Count() >= 2
                  orderby dept.Key
                  select new
                  {
                      Department = dept.Key,
                      EmployeeCount = dept.Count(),
                      TotalSalary = dept.Sum(x => x.Salary),
                      AvgSalary = dept.Average(x => x.Salary)
                  };

Console.WriteLine("=== Department Summary (2+ employees) ===\n");

foreach (var dept in deptSummary)
{
    Console.WriteLine($"{dept.Department}:");
    Console.WriteLine($"  Employees: {dept.EmployeeCount}");
    Console.WriteLine($"  Total Salary: ${dept.TotalSalary:N0}");
    Console.WriteLine($"  Avg Salary: ${dept.AvgSalary:N0}\n");
}

record Employee(string Name, string Department, decimal Salary);
GroupByDemo.csproj
<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net8.0</TargetFramework>
    <Nullable>enable</Nullable>
    <ImplicitUsings>enable</ImplicitUsings>
  </PropertyGroup>
</Project>

What you'll see

=== Department Summary (2+ employees) ===

Engineering:
  Employees: 3
  Total Salary: $272,000
  Avg Salary: $90,667

Sales:
  Employees: 2
  Total Salary: $143,000
  Avg Salary: $71,500

Try modifying the where clause to show all departments, or change orderby to sort by average salary descending.

Reader Questions

When should I use group by instead of method syntax GroupBy?

Use query syntax when grouping is part of a larger query with multiple clauses. It's often more readable for complex queries. Use method syntax for simple grouping or when chaining operations. Both compile to the same code—choose based on clarity for your specific scenario.

What does into do after group by?

The into keyword creates a query continuation, letting you filter or transform groups further. After group by into, you can write additional clauses like where, orderby, or select on the grouped results. Without into, the query ends at the grouping.

How do I count items in each group efficiently?

Use select new { Key = g.Key, Count = g.Count() } after grouping. Each IGrouping supports standard LINQ operators. For large datasets, ensure your source is IQueryable (like Entity Framework) so Count translates to SQL rather than loading all data into memory first.

Back to Articles