Osy#betaRun it on Osyrin: osyrin.com

Reference / Query

IGrouping<K, T> and ILookup<K, T>

list.GroupBy(x => key[, x => element]) → IGrouping<K, T> per group · g.Key · foreach (var x in g) · g.First() · list.ToLookup(x => key[, x => element]) → ILookup<K, T> · lookup[key] · lookup.Count · lookup.Contains(key)

A `GroupBy` that is not reduced to one row per group answers C#'s `IGrouping<K, T>` — each group is a sequence of its elements that also carries its `Key`, so you can hold a group, iterate it, take its first element, filter groups by their size, or turn them into a dictionary. `ToLookup` holds the same groups as an `ILookup<K, T>`, indexed by key, where a key it does not hold answers an empty sequence. Over rows read from the data store the rows are read first, then grouped exactly as C#'s LINQ groups them.

stable5 examples compiled by CIlinqquerygroupingcollections

Summary#

Group a sequence and keep the groups — each one is a sequence of its elements with a Key, exactly as in C#:

class Visit { public string Room = ""; public string Who = ""; public int Minutes; }

string Summary(List<Visit> visits) {
  var parts = new List<string>();
  foreach (var g in visits.GroupBy(v => v.Room)) {
    parts.Add(g.Key + ":" + g.Count() + ":" + g.First().Who);
  }
  return string.Join(",", parts);
}

Groups come out in the order their keys first appear, and each group's elements stay in source order.

Signature#

IEnumerable<IGrouping<K, T>> xs.GroupBy(x => key)                  // groups of the elements
IEnumerable<IGrouping<K, E>> xs.GroupBy(x => key, x => element)    // groups of a projection of them
K           g.Key                                                  // the key every element of the group shares
T           g.First() / g.Last() / g.FirstOrDefault(x => p)        // one element of the group
ILookup<K, T>  xs.ToLookup(x => key[, x => element])               // the same groups, held by key
IEnumerable<T> lookup[key]                                         // that key's elements — EMPTY when absent
int         lookup.Count                                           // how many keys
bool        lookup.Contains(key)                                   // does it hold that key?

Description#

What a group is#

A group is a list of its elements that carries one more thing: the Key they share. So every list verb works on it — Count(), Sum(x => …), OrderBy, First(), foreach — and g.Key answers the key. A variable can hold one: IGrouping<string, Visit> g is a type you can write.

That makes the ordinary C# shapes work, with C#'s results:

List<string> FirstPerRoom(List<Visit> visits) =>
  visits.GroupBy(v => v.Room).Select(g => g.First().Who).ToList();

int RoomsVisitedTwice(List<Visit> visits) =>
  visits.GroupBy(v => v.Room).Where(g => g.Count() > 1).Count();

Dictionary<string, int> MinutesPerRoom(List<Visit> visits) =>
  visits.GroupBy(v => v.Room).ToDictionary(g => g.Key, g => g.Sum(v => v.Minutes));

Over rows from the data store#

A grouping that reduces each group to one row of key and aggregates — GroupBy(k).Select(g => new T { … g.Key, g.Count(), g.Sum(…) }) — is a real GROUP BY in the database: see GroupBy (and HAVING). Anything else you do with a group needs the group's ROWS — its first element, the group itself, a foreach over it — so the rows the query selects are read and then grouped, as C#'s LINQ would group them. A Where before the GroupBy still filters in the database, so only those rows travel:

entity Climb {
  [Required, MaxLength(40)] string Wall;
  [Required, MaxLength(40)] string Name;
  int Grade;
}

List<string> HardestPerWall() =>
  Climb.Where(c => c.Grade > 0)
       .GroupBy(c => c.Wall)
       .Select(g => g.Key + "=" + g.OrderByDescending(c => c.Grade).First().Name)
       .ToList();

One element per group reads one or two rows a group#

When the projection uses the group only through g.Key and one pick — g.First(), g.Last(), g.FirstOrDefault(), g.LastOrDefault(), optionally after g.OrderBy(…)/ThenBy(…) — the database returns only the rows that decide each group: its first row in source order, which places the group, and the row the pick chooses. HardestPerWall above reads two rows a wall however many climbs each wall holds. The answer is the one C# gives over every row — ties go to the earlier row, as a stable sort leaves them — and an OrderBy before the GroupBy sets that source order:

List<string> EasiestPerWall() =>
  Climb.OrderByDescending(c => c.Grade)
       .GroupBy(c => c.Wall)
       .Select(g => g.Key + ":" + g.Last().Name)
       .ToList();

While the current unit of work holds an unsaved change to those rows, every row is read and grouped instead, so the answer includes what it has not saved yet.

ToLookup — the groups, held by key#

ToLookup makes the same groups and holds them by key. Unlike a Dictionary, indexing it with a key it does not hold is not an error: it answers an empty sequence. Enumerating it yields its groups, in first-appearance order.

string Visitors(List<Visit> visits, string room) {
  ILookup<string, string> byRoom = visits.ToLookup(v => v.Room, v => v.Who);
  if (!byRoom.Contains(room)) return "nobody (" + byRoom.Count + " rooms visited)";
  return string.Join(", ", byRoom[room]);
}

A custom key comparer is not supported; normalise the key instead (v => v.Room.ToUpperInvariant()).

See also#

Related

GroupBy (and HAVING)

Group rows by a key and reduce each group to one row — `g.Key`, `g.Count()`, `g.Count(o => p)`…

LINQ over a local list

Query a local `List<T>`, `HashSet<T>` or `T[]` — of your own `class` values OR of plain scalars like `string[]` and…

Skip / Take (paging)

Page a query with Skip(n) (OFFSET) and Take(m) (LIMIT). The count can be a compile-time constant OR a runtime integer —…

Sequence fields on a class

A class can hold many values in one field — `string[]`, `int[]`, `List<Tag>`, `HashSet<string>`, `Dictionary<string…