Группировка


Для группировки данных по определенным параметрам применяется оператор group by и метод GroupBy().

Оператор group by
Допустим, у нас есть набор из объектов следующего типа:

record class Person(string Name, string Company);
Данный класс представляет пользователя и имеет два свойства: Name (имя пользователя) и Company (компания, где работает пользователь). Сгруппируем набор пользователей по компании:

Person[] people =
{
new Person("Tom", "Microsoft"), new Person("Sam", "Google"),
new Person("Bob", "JetBrains"), new Person("Mike", "Microsoft"),
new Person("Kate", "JetBrains"), new Person("Alice", "Microsoft"),
};

var companies = from person in people
group person by person.Company;

foreach(var company in companies)
{
Console.WriteLine(company.Key);

foreach(var person in company)
{
Console.WriteLine(person.Name);
}
Console.WriteLine(); // для разделения между группами
}

record class Person(string Name, string Company);
Если в выражении LINQ последним оператором, выполняющим операции над выборкой, является group, то оператор select не применяется.

Оператор group принимает критерий по которому проводится группировка:

group person by person.Company
в данном случае группировка идет по свойству Company. Результатом оператора group является выборка, которая состоит из групп. Каждая группа представляет объект IGrouping<K, V>: параметр K указывает на тип ключа - тип свойства, по которому идет группировка (здесь это тип string). А параметр V представляет тип сгруппированных объектов - в данном случае группируем объекты Person.

Каждая группа имеет ключ, который мы можем получить через свойство Key: g.Key. Здесь это будет название компании.

Все элементы внутри группы можно получить с помощью дополнительной итерации. Элементы группы имеют тот же тип, что и тип объектов, которые передавались оператору group, то есть в данном случае объекты типа Person.

В итоге мы получим следующий вывод:

Microsoft
Tom
Mike
Alice

Google
Sam

JetBrains
Bob
Kate
GroupBy
В качестве альтернативы можно использовать метод расширения GroupBy. Он имеет ряд перегрузок, возьмем самую простую из них:

GroupBy<TSource,TKey> (Func<TSource,TKey> keySelector);
Данная версия получает делегат, которые в качестве параметра принимает каждый элемент коллекции и возвращает критерий группировки.

Перепишем предыдущий пример с помощью метода GroupBy:

Person[] people =
{
new Person("Tom", "Microsoft"), new Person("Sam", "Google"),
new Person("Bob", "JetBrains"), new Person("Mike", "Microsoft"),
new Person("Kate", "JetBrains"), new Person("Alice", "Microsoft"),
};

var companies = people.GroupBy(p => p.Company);

foreach(var company in companies)
{
Console.WriteLine(company.Key);

foreach(var person in company)
{
Console.WriteLine(person.Name);
}
Console.WriteLine(); // для разделения между группами
}

record class Person(string Name, string Company);
Создание нового объекта при группировке
Теперь изменим запрос и создадим из группы новый объект:

Person[] people =
{
new Person("Tom", "Microsoft"), new Person("Sam", "Google"),
new Person("Bob", "JetBrains"), new Person("Mike", "Microsoft"),
new Person("Kate", "JetBrains"), new Person("Alice", "Microsoft"),
};

var companies = from person in people
group person by person.Company into g
select new { Name = g.Key, Count = g.Count() }; ;

foreach(var company in companies)
{
Console.WriteLine($"{company.Name} : {company.Count}");
}

record class Person(string Name, string Company);
Выражение

group person by person.Company into g
определяет переменную g, которая будет содержать группу. С помощью этой переменной мы можем затем создать новый объект анонимного типа (хотя также можно под данную задачу определить новый класс):

select new { Name = g.Key, Count = g.Count() }
Теперь результат запроса LINQ будет представлять набор объектов таких анонимных типов, у которых два свойства Name и Count.

Результат программы:

Microsoft : 3
Google : 1
JetBrains : 2
Аналогичная операция с помощью метода GroupBy():

var companies = people
.GroupBy(p=>p.Company)
.Select(g => new { Name = g.Key, Count = g.Count() });
Вложенные запросы
Также мы можем осуществлять вложенные запросы:

Person[] people =
{
new Person("Tom", "Microsoft"), new Person("Sam", "Google"),
new Person("Bob", "JetBrains"), new Person("Mike", "Microsoft"),
new Person("Kate", "JetBrains"), new Person("Alice", "Microsoft"),
};
var companies = from person in people
group person by person.Company into g
select new
{
Name = g.Key,
Count = g.Count(),
Employees = from p in g select p
};

foreach (var company in companies)
{
Console.WriteLine($"{company.Name} : {company.Count}");
foreach(var employee in company.Employees)
{
Console.WriteLine(employee.Name);
}
Console.WriteLine(); // для разделения компаний
}

record class Person(string Name, string Company);
Здесь свойство Employees каждой группы формируется с помощью дополнительного запроса, который выбирает всех пользователей в этой группе. Консольный вывод программы:

Microsoft : 3
Tom
Mike
Alice

Google : 1
Sam

JetBrains : 2
Bob
Kate
Аналогичный запрос с помощью метода GroupBy:

var companies = people
.GroupBy(p=>p.Company)
.Select(g => new
{
Name = g.Key,
Count = g.Count(),
Employees = g.Select(p=> p)
});