Question

How to Group by Multiple Columns in LINQ (C#)

csharp.netlinqgroup-by

Question

How can I group data by multiple columns in LINQ, similar to SQL GROUP BY Column1, Column2?

For example, given a SQL query like this:

SELECT MaterialID, ProductID, SUM(Quantity)
FROM @Transactions
GROUP BY MaterialID, ProductID

and a target structure like this:

QuantityBreakdown
(
    MaterialID int,
    ProductID int,
    Quantity float
)

how can I write the equivalent logic in LINQ so that records are grouped by both MaterialID and ProductID, and the Quantity values are summed for each group?

Short Answer

By the end of this page, you will understand how to group records by more than one field in LINQ using C#. You will learn how anonymous objects are used as composite group keys, how to calculate aggregates like Sum, and how this pattern compares to SQL GROUP BY queries.

Concept

In LINQ, grouping by multiple columns means creating a composite key. Instead of grouping by a single value, you group by an object that contains several values.

In SQL, this is straightforward:

GROUP BY MaterialID, ProductID

In LINQ, the equivalent idea is usually written like this:

group item by new { item.MaterialID, item.ProductID }

or with method syntax:

.GroupBy(x => new { x.MaterialID, x.ProductID })

This matters because real data is often grouped by combinations of fields, not just one field. For example:

  • sales by store and date
  • logs by user and status
  • transactions by material and product
  • orders by customer and month

After grouping, you often calculate aggregate values such as:

  • Sum
  • Count
  • Average
  • Min
  • Max

In your case, the goal is to group transactions by MaterialID and ProductID, then sum Quantity for each group.

A key difference from SQL is that LINQ returns objects, not rows directly. So after grouping, you usually project the result into a new object using select or Select(...).

Mental Model

Think of grouping like sorting receipts into labeled folders.

  • If you group by one column, each folder gets one label, such as MaterialID = 10.
  • If you group by multiple columns, each folder gets a label made of several parts, such as:
    • MaterialID = 10
    • ProductID = 200

Every record with the same combination goes into the same folder.

Once the records are in each folder, you can total them up. In this case, you add all Quantity values inside each folder.

So the composite key is just the folder label made from multiple values.

Take Quiz

Syntax and Examples

Query syntax

var result =
    from t in transactions
    group t by new { t.MaterialID, t.ProductID } into g
    select new
    {
        g.Key.MaterialID,
        g.Key.ProductID,
        Quantity = g.Sum(x => x.Quantity)
    };

Method syntax

var result = transactions
    .GroupBy(t => new { t.MaterialID, t.ProductID })
    .Select(g => new
    {
        g.Key.MaterialID,
        g.Key.ProductID,
        Quantity = g.Sum(x => x.Quantity)
    });

Example with classes

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

public class Transaction
{
    public int MaterialID { get; set; }
    public int ProductID { get; set; }
    public double Quantity { get; set; }
}

public class QuantityBreakdown
{
    public int MaterialID { get; set; }
    public int ProductID { get; set; }
    public double Quantity { get; set; }
}

var transactions = new List<Transaction>
{
    new Transaction { MaterialID = 1, ProductID = 10, Quantity = 5 },
    new Transaction { MaterialID = 1, ProductID = 10, Quantity = 3 },
    new Transaction { MaterialID = 1, ProductID = 20, Quantity = 2 },
    new Transaction { MaterialID = 2, ProductID = 10, Quantity = 7 }
};

var breakdown = transactions
    .GroupBy(t => new { t.MaterialID, t.ProductID })
    .Select(g => new QuantityBreakdown
    {
        MaterialID = g.Key.MaterialID,
        ProductID = g.Key.ProductID,
        Quantity = g.Sum(x => x.Quantity)
    })
    .ToList();

Result

This produces grouped totals like:

  • (1, 10) => 8
  • (1, 20) => 2
  • (2, 10) => 7

Why new { ... } works

The anonymous object created by new { t.MaterialID, t.ProductID } becomes the grouping key. LINQ compares keys by their property values, so records with the same MaterialID and ProductID end up in the same group.

Step by Step Execution

Consider this example:

var transactions = new List<Transaction>
{
    new Transaction { MaterialID = 1, ProductID = 10, Quantity = 5 },
    new Transaction { MaterialID = 1, ProductID = 10, Quantity = 3 },
    new Transaction { MaterialID = 1, ProductID = 20, Quantity = 2 }
};

var result = transactions
    .GroupBy(t => new { t.MaterialID, t.ProductID })
    .Select(g => new
    {
        g.Key.MaterialID,
        g.Key.ProductID,
        Total = g.Sum(x => x.Quantity)
    });

Step-by-step

1. Start with the input records

The list contains:

  • (1, 10, 5)
  • (1, 10, 3)
  • (1, 20, 2)

2. Apply GroupBy

.GroupBy(t => new { t.MaterialID, t.ProductID })

This creates groups based on these keys:

  • Key { MaterialID = 1, ProductID = 10 }
  • Key { MaterialID = 1, ProductID = 20 }

So the groups become:

  • Group (1, 10) contains quantities 5 and 3
  • Group (1, 20) contains quantity 2

3. Apply Select

.Select(g => new
{
    g.Key.MaterialID,
    g.Key.ProductID,
    Total = g.Sum(x => x.Quantity)
})

For each group:

  • read the key values from g.Key
  • sum the Quantity values inside that group

4. Final output

The result becomes:

{ MaterialID = 1, ProductID = 10, Total = 8 }
{ MaterialID = 1, ProductID = 20, Total = 2 }

This is the LINQ equivalent of SQL GROUP BY MaterialID, ProductID with SUM(Quantity).

Real World Use Cases

Grouping by multiple columns appears often in real applications.

Inventory systems

Group stock movements by:

  • MaterialID
  • ProductID

Then calculate total quantity moved.

Sales reporting

Group sales by:

  • StoreId
  • Date

Then sum revenue for each store per day.

API analytics

Group requests by:

  • UserId
  • StatusCode

Then count how many successful or failed requests each user made.

Order processing

Group order items by:

  • OrderId
  • ProductId

Then total quantities for each product in each order.

Log analysis scripts

Group logs by:

  • ServiceName
  • Severity

Then count how many warnings or errors each service produced.

Take Quiz

Real Codebase Usage

In real C# projects, developers often use multi-column grouping in these patterns:

Aggregation for DTOs

A common pattern is grouping raw records and projecting them into a DTO or view model.

var summary = transactions
    .GroupBy(t => new { t.MaterialID, t.ProductID })
    .Select(g => new QuantityBreakdown
    {
        MaterialID = g.Key.MaterialID,
        ProductID = g.Key.ProductID,
        Quantity = g.Sum(x => x.Quantity)
    });

Filtering before grouping

Developers often reduce the dataset first.

var summary = transactions
    .Where(t => t.Quantity > 0)
    .GroupBy(t => new { t.MaterialID, t.ProductID })
    .Select(g => new QuantityBreakdown
    {
        MaterialID = g.Key.MaterialID,
        ProductID = g.Key.ProductID,
        Quantity = g.Sum(x => x.Quantity)
    });

Guarding against bad input

If a collection might be null, code often checks before grouping.

if (transactions == null)
{
    return new List<QuantityBreakdown>();
}

Entity Framework queries

When used with EF or LINQ to Entities, this pattern is often translated into SQL by the provider. That makes it useful for database reporting without writing raw SQL.

Materializing at the end

A common practice is to keep the query composable and call ToList() only when needed.

var query = transactions
    .GroupBy(t => new { t.MaterialID, t.ProductID })
    .Select(g => new QuantityBreakdown
    {
        MaterialID = g.Key.MaterialID,
        ProductID = g.Key.ProductID,
        Quantity = g.Sum(x => x.Quantity)
    });

var list = query.ToList();

Common Mistakes

1. Grouping by only one field by accident

Broken example:

var result = transactions
    .GroupBy(t => t.MaterialID)
    .Select(g => new
    {
        MaterialID = g.Key,
        Quantity = g.Sum(x => x.Quantity)
    });

This ignores ProductID, so different products for the same material get merged together.

Use this instead:

.GroupBy(t => new { t.MaterialID, t.ProductID })

2. Forgetting to project the grouped result

Broken example:

var groups = transactions.GroupBy(t => new { t.MaterialID, t.ProductID });

This gives you groups, but not the final shape you usually want.

Add Select(...) to create a useful result:

var result = transactions
    .GroupBy(t => new { t.MaterialID, t.ProductID })
    .Select(g => new
    {
        g.Key.MaterialID,
        g.Key.ProductID,
        Quantity = g.Sum(x => x.Quantity)
    });

3. Trying to access grouped properties directly

Broken example:

.Select(g => new
{
    g.MaterialID,
    g.ProductID
})

A group does not expose those properties directly. They are inside g.Key.

Correct version:

.Select(g => new
{
    g.Key.MaterialID,
    g.Key.ProductID
})

4. Using the wrong numeric type

If your model uses float, double, or decimal, be consistent. For money, prefer decimal. For quantities, choose the type that matches your domain.

5. Expecting SQL text syntax in LINQ

LINQ is not SQL written in C#. The concepts are similar, but the syntax is object-based and strongly typed.

6. Calling ToList() too early

Less ideal:

var result = transactions.ToList()
    .GroupBy(t => new { t.MaterialID, t.ProductID });

If transactions is an IQueryable, calling ToList() too early may fetch more data than needed before grouping. Prefer composing the query first.

Comparisons

ConceptLINQ styleBest use case
Group by one column.GroupBy(x => x.MaterialID)When only one field defines the group
Group by multiple columns.GroupBy(x => new { x.MaterialID, x.ProductID })When a combination of fields defines the group
SQL GROUP BYGROUP BY MaterialID, ProductIDDatabase-side grouping in SQL
LINQ query syntaxgroup x by ... into g select ...Readable for SQL-like thinking
LINQ method syntax.GroupBy(...).Select(...)Common in fluent C# code

Query syntax vs method syntax

var querySyntax =
    from t in transactions
    group t by new { t.MaterialID, t.ProductID } into g
    select new
    {
        g.Key.MaterialID,
        g.Key.ProductID,
        Quantity = g.Sum(x => x.Quantity)
    };
var methodSyntax = transactions
    .GroupBy(t => new { t.MaterialID, t.ProductID })
    .Select(g => new
    {
        g.Key.MaterialID,
        g.Key.ProductID,
        Quantity = g.Sum(x => x.Quantity)
    });

Both are valid. Choose the one your team finds easier to read.

Anonymous type vs named type

OptionExampleWhen to use
Anonymous typenew { t.MaterialID, t.ProductID }Quick grouping keys inside a query
Named typeCustom class or recordReusable key structure across methods

For most LINQ grouping, anonymous types are the simplest choice.

Take Quiz

Cheat Sheet

// Group by multiple columns
var result = items
    .GroupBy(x => new { x.Col1, x.Col2 })
    .Select(g => new
    {
        g.Key.Col1,
        g.Key.Col2,
        Total = g.Sum(x => x.Amount)
    });

Key rules

  • Use new { ... } to group by multiple fields.
  • Access grouped key values with g.Key.PropertyName.
  • Use Select(...) after GroupBy(...) to shape the result.
  • Use aggregate methods like:
    • Sum
    • Count
    • Average
    • Min
    • Max
  • Query syntax and method syntax are equivalent in capability.

SQL to LINQ mapping

SQLLINQ
GROUP BY A, B.GroupBy(x => new { x.A, x.B })
SUM(Value)g.Sum(x => x.Value)
SELECT A, B, SUM(Value).Select(g => new { g.Key.A, g.Key.B, Total = g.Sum(x => x.Value) })

Common pattern

var breakdown = transactions
    .GroupBy(t => new { t.MaterialID, t.ProductID })
    .Select(g => new QuantityBreakdown
    {
        MaterialID = g.Key.MaterialID,
        ProductID = g.Key.ProductID,
        Quantity = g.Sum(x => x.Quantity)
    })
    .ToList();

FAQ

How do I group by two columns in LINQ?

Use an anonymous object as the key:

.GroupBy(x => new { x.Column1, x.Column2 })

Is LINQ GroupBy equivalent to SQL GROUP BY?

Yes, conceptually. Both group rows by key values. In LINQ, you then project the grouped result into objects.

How do I sum values after grouping in LINQ?

Use Sum inside the projection:

.Select(g => new { Total = g.Sum(x => x.Quantity) })

Can I group by more than two columns?

Yes. Add more properties to the key:

.GroupBy(x => new { x.A, x.B, x.C })

Should I use query syntax or method syntax for grouping?

Either is fine. Method syntax is very common in C# projects, while query syntax can feel more familiar if you know SQL.

Why do I need g.Key after GroupBy?

Because each group stores its grouping values inside a key object. That key contains the fields you grouped by.

Does this work with Entity Framework?

Usually yes, as long as the LINQ provider can translate the query to SQL. This pattern is commonly supported.

Take Quiz

Mini Project

Description

Build a small inventory summary tool that groups transaction records by MaterialID and ProductID, then calculates the total quantity for each pair. This mirrors a common reporting task in inventory, warehouse, and purchasing systems.

Goal

Create a LINQ query that produces a grouped quantity breakdown from a list of transactions.

Requirements

  1. Create a Transaction class with MaterialID, ProductID, and Quantity properties.
  2. Create a QuantityBreakdown class for the grouped output.
  3. Add at least five sample transactions, including repeated MaterialID and ProductID combinations.
  4. Group the transactions by both MaterialID and ProductID.
  5. Sum the Quantity values for each group and print the results.
Take Quiz

Keep learning

Related questions

View more