Call GroupBy with a key selector: orders.GroupBy(o => o.Customer). You get one group per distinct key, and each group has a Key and is itself a sequence of the matching items. To group by several columns, make the key an anonymous type: sales.GroupBy(s => new { s.Region, s.Year }).
GroupBy is LINQ's version of SQL's GROUP BY, with one difference that trips people up: it doesn't collapse each group into a single row. It returns a sequence of IGrouping<TKey, TElement> objects, and every group still holds all of its items, so you decide afterwards whether to count them, sum them, list them or turn the groups into a dictionary. This page covers the basic call, the overloads that pick what goes into each group, grouping by more than one column, query syntax, per-group aggregates with a HAVING-style filter, the .NET 9 CountBy and AggregateBy shortcuts, and when to use ToLookup instead. Each example runs on this page: hit Run, then edit the code and run it again.
1GroupBy with a key selectorRecommended
Pass GroupBy a lambda that returns the key for each item. The result is an IEnumerable<IGrouping<string, Order>>: loop over it to get the groups, read group.Key for the key, and loop over the group itself (or call LINQ methods on it) for its items. The key can be any expression, not just a property, so grouping by a computed bucket is the same call.
Output
Prints alice: count=3, total=42.48 followed by her three orders (books 12.50, games 19.99, music 9.99), then bob: count=2, total=74.99 and carol: count=1, total=8.00, then small: 12.50, 8.00, 9.99 and large: 59.99, 19.99, 15.00. The order is part of the contract: GroupBy yields the groups in the order their keys first appear in the source, and the items inside each group in source order. It does not sort, so add OrderBy(g => g.Key) when you want the groups sorted. In code you usually write var instead of spelling out IGrouping<string, Order>.
2Choosing what goes in each group
The most-asked version of this question has a list of people and cars and wants each PersonId with the list of its cars. GroupBy has overloads for that. A second lambda, the element selector, picks what each group holds, so the groups contain car names instead of whole Person objects. A third, the result selector, receives each key and its items and builds the output object in the same call. Plain GroupBy followed by Select does the same job and is what most code uses.
Output
The element selector prints 1: Ferrari, BMW, 2: Audi, Volvo and 3: Toyota. The result selector gives person 1 owns 2: [Ferrari, BMW], person 2 owns 2: [Audi, Volvo] and person 3 owns 1: [Toyota], and the Select version matches it (Select version: 3 owners, first owns 2). When you need to look the groups up by key later, finish with ToDictionary: carsById[2] = [Audi, Volvo]. The ToList() inside the projection gives each group a real List<string> with Count and indexing; the outer query is still deferred until you enumerate it or call ToList() on it, as the Select version does.
3Group by multiple columns
To group by two or more properties, return a key that combines them. The classic answer is an anonymous type, new { s.Region, s.Year }: the compiler gives anonymous types value equality, so two keys with the same values land in the same group. A tuple, (s.Region, s.Product), works the same way on C# 7 and later, and a record is the choice when the key needs a name, for example because a method returns the groups. What doesn't work is an ordinary class.
Output
The anonymous key gives north 2025: 195 units, south 2025: 140 units and north 2026: 150 units, and the key itself prints as { Region = north, Year = 2025 }. The tuple key prints as (north, widget), and because the names are inferred from the properties (C# 7.1+) you can still write g.Key.Region: the first line is (north, widget): rows=3, g.Key.Region=north. The record key also finds 3 groups and prints as RegionYear { Region = north, Year = 2025 }. The class key prints class key: 6 groups, one per row: a class compares by reference unless it overrides Equals and GetHashCode, so no two keys ever match. Use an anonymous type or tuple inside a method and a record when the key type has to be named.
4Query syntax: group … by … into
LINQ's query syntax has a group clause. group s by s.House ends the query and yields the groups; add into g to keep going, then orderby and select over the groups, much like SQL. The form group s.Name by … is the element selector from section 2: it puts only the names in each group. Multiple columns work the same way as in method syntax, with an anonymous type after by.
Output
The first query prints Blue count=2 best=81, Green count=1 best=64 and Red count=3 best=95, sorted by house because of the orderby. The two-column query prints one line per house and year, from Blue year 1: Ben to Red year 2: Ana, Fay. The plain query without into keeps first-appearance order: Red:3 | Blue:2 | Green:1. The compiler turns both syntaxes into the same GroupBy calls, so pick whichever reads better; query syntax tends to win once there is a let, a join or several clauses.
5Count, Sum and HAVING per group
The SQL-style report, one row per key with totals, is GroupBy followed by a Select that calls Count(), Sum(), Average(), Max() or MaxBy() on each group. SQL's HAVING is simply a Where placed after the GroupBy, so it filters groups instead of rows. When all you want is a count or a running total per key, .NET 9 added CountBy and AggregateBy, which compute it directly without building the groups.
Output
The report prints games count=3 sum=103.98 avg=34.66 biggest=bob, then books count=3 sum=35.50 avg=11.83 biggest=bob and music count=1 sum=9.99 avg=9.99 biggest=alice. The HAVING filter leaves more than one order: alice, bob. CountBy prints CountBy books: 3, CountBy games: 3 and CountBy music: 1, and AggregateBy sums each customer, from AggregateBy alice: 42.48 to AggregateBy dana: 24.00. Both return KeyValuePair items, which deconstruct into (key, value) in a foreach. MaxBy needs .NET 6; on older frameworks use g.OrderByDescending(o => o.Total).First(). The same Where-after-GroupBy pattern finds duplicate values in a list; to remove them instead, see remove duplicates from a list.
6ToLookup, and why GroupBy runs late
GroupBy uses deferred execution: it only stores the query, and it does the grouping each time you enumerate it, against whatever the source holds at that moment. ToLookup takes the same arguments but groups straight away and keeps the result as an ILookup<TKey, TElement>, a read-only key-to-many collection you can index like a dictionary. Both accept an IEqualityComparer, here StringComparer.OrdinalIgnoreCase so file extensions group regardless of case.
Output
readme.txt was added after both calls, and only the deferred query sees it: GroupBy .txt: notes.txt, todo.TXT, readme.txt against ToLookup .txt: notes.txt, todo.TXT. The comparer merged the spellings, and each key is the first one seen: keys: .txt .JPG .csv .png, with photo.JPG, cat.jpg in the .JPG group. Indexing a lookup with a missing key returns an empty sequence (lookup[".pdf"] count: 0, Contains: False), while the dictionary throws KeyNotFoundException: The given key '.pdf' was not present in the dictionary. Use ToLookup when you will look groups up by key several times, and ToDictionary when each key should map to a mutable List<T>.
7Which should you use?
| Method | Gives you | Runs | Best for |
|---|---|---|---|
| GroupBy(x => x.Key) | Groups with all items | Deferred | The default, any .NET version |
| GroupBy(x => new { x.A, x.B }) | Groups by several columns | Deferred | Multi-column keys inside a method |
| GroupBy(…).Select(g => new { … }) | One row per key | Deferred | Reports: Count, Sum, Average per key |
| group x by k into g | Same as GroupBy | Deferred | Longer queries with orderby, let, join |
| CountBy / AggregateBy | One value per key | Deferred | Counts and totals only, .NET 9+ |
| ToLookup(x => x.Key) | Indexable groups | Immediately | Repeated lookups by key |
| GroupBy(…).ToDictionary(…) | Key to List<T> | Immediately | Groups you will modify later |
Frequently asked questions
How do I group by multiple columns in LINQ?
Return a key that holds all the columns. In method syntax write list.GroupBy(x => new { x.Region, x.Year }); in query syntax write group x by new { x.Region, x.Year } into g. Read the parts back with g.Key.Region and g.Key.Year. A tuple key, GroupBy(x => (x.Region, x.Year)), works the same way on C# 7 and later, and a record works when the key type needs a name.
Why does GroupBy put every item in its own group?
Because the key is an ordinary class, which compares by reference: every new MyKey(...) is a different object, so no two keys match. With a class key, four rows gave 4 groups instead of 3. Use an anonymous type, a tuple or a record (all three have value equality), override Equals and GetHashCode on the class, or pass an IEqualityComparer<MyKey> as the second argument to GroupBy, which brought the same rows down to 3 groups.
Does GroupBy keep the original order?
Yes. The documentation guarantees that groups come out in the order their keys first appear in the source, and that the items inside each group keep their source order. GroupBy never sorts, so add .OrderBy(g => g.Key) if you want the groups in key order. The order of CountBy and AggregateBy results is not documented, so sort those when it matters.
What is the difference between GroupBy and ToLookup?
They group the same way, but GroupBy is deferred and ToLookup is immediate. GroupBy re-runs the grouping every time you enumerate it: enumerating a GroupBy over 6 numbers twice called the key selector 12 times, against 6 after ToList(). ToLookup groups once and returns an ILookup you can index with lookup[key], which gives an empty sequence for a missing key instead of throwing.
How do I find duplicates in a list with GroupBy?
Group by the value and keep the groups with more than one item: list.GroupBy(x => x).Where(g => g.Count() > 1).Select(g => g.Key). Select $"{g.Key} x{g.Count()}" instead to see how often each appears; for ["[email protected]", "[email protected]", "[email protected]", "[email protected]", "[email protected]", "[email protected]"] that prints [email protected] x3, [email protected] x2. To find duplicates by one property, group by that property instead.
How do I group numbers into ranges, like ages by decade?
Make the key the start of the range. For decades, integer division does it: ages.GroupBy(a => a / 10 * 10) puts 23 and 29 under 20, and 31, 37 and 38 under 30. For uneven ranges, return a label from a switch expression, for example a switch { < 18 => "minor", < 65 => "adult", _ => "senior" }.
How do I convert a GroupBy result to a Dictionary?
Call ToDictionary on the groups: list.GroupBy(x => x.Key).ToDictionary(g => g.Key, g => g.ToList()) gives a Dictionary<TKey, List<T>>. Use g.Select(x => x.Name).ToList() as the value to keep only one property, or g.Count() for a count per key. If you only need to read the groups, list.ToLookup(x => x.Key) does the same in one call.