Back to Blog
C#

C# LINQ GroupBy: Grouping Data Correctly

c# linq groupby: Learn how to use C# LINQ GroupBy to group sequences by keys, handle multiple keys, and understand performance trade-offs with clear examples.

LINQGroupByC# CollectionsData AggregationQuery Syntax
Diagram showing collection elements being grouped into subsets based on a key selector in C# LINQ.

When working with collections in C#, grouping data is a common operation. The c# linq groupby method, part of LINQ, lets you organize elements from a sequence into buckets based on a key. This is useful for aggregating values, building hierarchies, or preparing data for reporting. The method returns an IEnumerable<IGrouping<TKey, TElement>>, where each group has a Key property and is an IEnumerable of the elements that share that key.

The Basic GroupBy Syntax

The simplest form of GroupBy takes a key selector function. For example, given a list of orders, you can group them by CustomerId to see all orders from the same customer:

var orders = new List<Order> { new Order { CustomerId = 1, Amount = 50 }, new Order { CustomerId = 2, Amount = 30 }, new Order { CustomerId = 1, Amount = 20 } }; var grouped = orders.GroupBy(o => o.CustomerId); foreach (var group in grouped) { Console.WriteLine($"Customer {group.Key}: {group.Count()} orders"); }

This code groups orders by CustomerId. The Key in each group is the customer ID, and the group itself enumerates the orders belonging to that customer. The output would be:

Customer 1: 2 orders
Customer 2: 1 order

The key selector can be any expression that returns a value. You can group by a property, a computed value, or even a custom object.

Grouping with Element Selection

Sometimes you do not need the full original elements; you only need certain fields. GroupBy has an overload that accepts an element selector. This can reduce memory and clarify intent:

var groupedAmounts = orders.GroupBy(o => o.CustomerId, o => o.Amount); foreach (var group in groupedAmounts) { Console.WriteLine($"Customer {group.Key}: total {group.Sum()}"); }

Here, each group contains only the Amount values for that customer, not the entire Order objects. This overload is particularly useful when the grouping key is the only property that matters and you want to work with a simpler sequence inside each group.

Grouping by Multiple Keys

A single key is often not enough. For example, grouping sales by both Region and Product requires a composite key. You can either use an anonymous type or a tuple. Both work with GroupBy because they provide structural equality:

var sales = new List<Sale> { new Sale { Region = "North", Product = "Laptop", Quantity = 2 }, new Sale { Region = "South", Product = "Laptop", Quantity = 1 }, new Sale { Region = "North", Product = "Mouse", Quantity = 5 } }; var grouped = sales.GroupBy(s => new { s.Region, s.Product }); foreach (var group in grouped) { Console.WriteLine($"{group.Key.Region} - {group.Key.Product}: {group.Sum(x => x.Quantity)}"); }

The anonymous type new { s.Region, s.Product } acts as the group key. The compiler generates value equality, so two keys with the same region and product compare equal. Tuples work just as well: (s.Region, s.Product). The choice is mostly style; both are clear, though anonymous types hide the type name in method signatures, which can matter in some contexts.

Returning Results vs. Iterating Directly

GroupBy is lazy. It does not execute until you enumerate the result. This has implications if the source sequence is a database query. In LINQ to Objects, the source is in memory anyway, but with Entity Framework, GroupBy translates to a server-side GROUP BY. If you call ToList() or ToArray() on the result, you force immediate execution and materialize all groups into memory.

This behavior matters for performance. If you only need to iterate the groups once, enumerating directly avoids building a separate collection. If you need to access groups multiple times or pass them to another method that expects a list, materializing is necessary.

GroupBy vs. ToLookup

ToLookup is another LINQ method that creates a one-to-many dictionary-like structure. The main difference is that Lookup<TKey, TElement> is immutable and always yields an empty sequence for keys that do not exist, whereas GroupBy produces groups only for keys that actually appear. ToLookup is eager; it builds the lookup immediately. GroupBy is deferred.

MethodExecutionBehavior for missing keyUse case
GroupByDeferredNo group existsStreaming over groups
ToLookupEagerReturns empty sequenceRepeated indexed lookups

If you find yourself calling group multiple times on the same GroupBy result, you are re-enumerating the source each time. In such cases, ToLookup can be more efficient because it builds the structure once.

Controlling Group Ordering

By default, the order of groups is the order of first appearance of each key in the source sequence. Within a group, elements retain their relative order from the source. This is usually sufficient, but you might want to sort groups by key or sort elements inside each group.

var grouped = orders.GroupBy(o => o.CustomerId) .OrderBy(g => g.Key); var sortedInside = orders.GroupBy(o => o.CustomerId) .Select(g => new { Key = g.Key, Items = g.OrderBy(o => o.Amount).ToList() });

The first example orders the groups by customer ID. The second orders the elements within each group by amount. Note that ordering groups after grouping does not change the order of elements within each group; you must apply sorting separately if needed.

Performance and Memory Considerations

Grouping is not free. For a sequence of N elements, GroupBy constructs a hash table internally to assign elements to groups. The time complexity is typically O(N), assuming the key hashes well. Memory usage grows with the number of distinct keys, not the total number of elements, because each element is stored in its group's sequence. However, if the source itself is a large in-memory list, the deferred nature still means the original list is held by the iterator.

When grouping in LINQ to Objects, the key selector is called once per element. If the key selector is expensive—such as parsing a string—consider storing the parsed value beforehand. For database-backed LINQ, the grouping happens on the server, so you should avoid pulling entire tables into memory just to group. Using the proper queryable API, like GroupBy in Entity Framework, translates to SQL and can benefit from database indexes.

One common mistake is using GroupBy when you actually need a dictionary. If you want a unique key per element, ToDictionary is more suitable. GroupBy is meant for cases where multiple elements share a key.

Handling Edge Cases in Grouping

Empty sequences: GroupBy on an empty sequence returns an empty sequence; no groups appear. Null keys: if your key selector returns null, it is treated as a valid key, and all elements with null keys go into one group. This is fine as long as your grouping logic can handle a null key. Be aware that a dictionary-based lookup, like ToLookup, also allows null keys.

Another edge case occurs when the key selector throws an exception. For example, if you access a property that has a null reference in the source, the exception is thrown during enumeration, not when calling GroupBy. The deferred execution means you must try/catch around the loop that consumes the groups.

Practical Example: Grouping Log Entries by Date

Consider a list of log entries. You want to group them by date (ignoring time) and count errors per day:

var logs = new List<LogEntry> { // sample data }; var errorsPerDay = logs .Where(log => log.Level == "Error") .GroupBy(log => log.Timestamp.Date) .Select(g => new { Date = g.Key, Count = g.Count() }) .OrderByDescending(x => x.Date);

Here grouping by log.Timestamp.Date reduces the key to just the date. The Where filters before grouping, reducing the number of elements the grouping has to process. This pattern—filter, group, project—is common in data analysis.

Choosing GroupBy Over Other Approaches

If your goal is to quickly compute aggregates per key, GroupBy combined with Sum, Count, or Average is direct. The alternative is a manual loop with a Dictionary<TKey, Aggregate>, which gives you more imperative control but is more verbose and error-prone. For example, grouping into a dictionary of lists manually requires checking whether the key exists:

var lookup = new Dictionary<int, List<Order>>(); foreach (var order in orders) { if (!lookup.TryGetValue(order.CustomerId, out var list)) { list = new List<Order>(); lookup.Add(order.CustomerId, list); } list.Add(order); }

GroupBy hides this bookkeeping, making the code shorter and less likely to introduce bugs. However, if you need to modify the groups (add/remove items) after creation, a Dictionary may be more flexible because GroupBy groups are read-only.

For most grouping and aggregation tasks, GroupBy is the idiomatic choice. It keeps the intent visible and leverages LINQ's composability with other operators. Understanding its execution model, overloads, and alternatives ensures that you pick the right tool for the specific data-processing scenario.

c# linq groupby: Practical Usage and Code Examples | RYUSLOG DEV