1. Introducción
Si alguna vez has escrito una consulta LINQ "clásica" con agrupación, por ejemplo:
var cityCounts = students.GroupBy(s => s.City)
.Select(g => new { City = g.Key, Count = g.Count() });
seguro que te diste cuenta de que para un simple recuento por grupos el código queda un poco verboso. Con la llegada de .NET 9 el equipo de Microsoft decidió hacernos la vida más fácil y añadió a LINQ dos métodos muy pedidos:
- CountBy — una forma rápida de contar elementos por clave.
- AggregateBy — un agregador universal por clave (no solo cuenta, también suma, etc).
Por cierto, estos métodos aparecieron gracias a un montón de peticiones de la comunidad, y sus análogos llevan tiempo en muchas librerías LINQ "avanzadas", como MoreLINQ.
2. Método CountBy: recuento conciso por grupos
Descripción
CountBy es literalmente "agrupación seguida de recuento". El método devuelve una colección de pares "clave-cantidad", algo que se necesita un montón en tareas reales de análisis de datos: ciudades top, número de estudiantes por nota, frecuencia de aparición de algo.
Firma (simplificada):
IEnumerable<(TKey Key, int Count)> CountBy<TSource, TKey>(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector)
Uso en la práctica
Ejemplo 1: Número de estudiantes por ciudad
Supón que tenemos esta clase:
class Student
{
public string Name { get; set; }
public int Grade { get; set; }
public string City { get; set; }
}
Y en nuestra app — una lista de estudiantes:
var students = new List<Student>
{
new Student { Name = "Iván", Grade = 5, City = "Neonville" },
new Student { Name = "Anna", Grade = 4, City = "Los Santos" },
new Student { Name = "Egor", Grade = 3, City = "Neonville" },
new Student { Name = "María", Grade = 5, City = "Rosewater" },
new Student { Name = "Oleg", Grade = 4, City = "Neonville" }
};
En .NET 8 y antes había que escribir:
var group = students.GroupBy(s => s.City)
.Select(g => new { City = g.Key, Count = g.Count() });
foreach (var cityInfo in group)
{
Console.WriteLine($"{cityInfo.City}: {cityInfo.Count} estudiantes");
}
En .NET 9 esto lo hace un solo método conciso:
var cityCounts = students.CountBy(s => s.City);
foreach (var (city, count) in cityCounts)
{
Console.WriteLine($"{city}: {count} estudiantes");
}
Sí, lo ves bien — nada de GroupBy manual, ni cálculo de Count() — ¡todo directo y sencillo!
Ejemplo 2: Recuento por notas
Una tarea top para cualquier decano — saber cuántos sobresalientes y cuántos con nota baja hay en el grupo:
var gradeCounts = students.CountBy(s => s.Grade);
foreach (var (grade, count) in gradeCounts)
{
Console.WriteLine($"Nota {grade}: {count} estudiantes");
}
Ejemplo 3: Frecuencia de caracteres en una palabra
CountBy sirve no solo para objetos, sino también para cosas más simples:
string word = "supercalifragilisticexpialidocious";
var charFrequencies = word.CountBy(ch => ch);
foreach (var (letter, count) in charFrequencies)
{
Console.WriteLine($"{letter} : {count}");
}
¿Cómo se ve en memoria? (ilustración)
| Clave | Count (antes de CountBy) | Count (con CountBy) |
|---|---|---|
|
3 | 3 |
|
1 | 1 |
|
1 | 1 |
Este enfoque hace el código más legible y reduce los errores al agrupar.
Errores típicos y matices
No olvides que CountBy devuelve una tupla (Key, Count), no un tipo anónimo. A veces puede parecer menos semántico, pero es súper transparente. Tampoco puedes indicar directamente el tipo de número para Count — siempre devuelve int.
3. Método AggregateBy: agregados universales por grupos
Descripción
Si CountBy es un recuento simple por clave, AggregateBy es una navaja suiza. ¿Quieres sumar ventas por vendedor, la nota máxima por clase, o cualquier cosa que puedas expresar con un acumulador? Todo eso lo hace AggregateBy.
Firma (muy simplificada):
IEnumerable<(TKey Key, TAccumulate Result)> AggregateBy<TSource, TKey, TAccumulate>(
this IEnumerable<TSource> source,
Func<TSource, TKey> keySelector,
TAccumulate seed,
Func<TAccumulate, TSource, TAccumulate> func)
- keySelector — por qué criterio agrupar.
- seed — valor inicial para acumular.
- func — función que describe cómo "acumular" el resultado.
Uso en la práctica
Ejemplo 1: Suma de notas por ciudad
En vez de este LINQ monstruoso:
var sums = students.GroupBy(s => s.City)
.Select(g => new { City = g.Key, Sum = g.Sum(s => s.Grade) });
Escribes:
var sums = students.AggregateBy(
s => s.City, // agrupamos por ciudad
0, // suma inicial
(acc, s) => acc + s.Grade // sumamos la nota
);
foreach (var (city, sum) in sums)
{
Console.WriteLine($"{city}: suma de notas = {sum}");
}
Ejemplo 2: Nota máxima por ciudad
¿Quieres el valor máximo? Solo cambia el acumulador:
var maxByCity = students.AggregateBy(
s => s.City,
int.MinValue, // valor inicial — el mínimo posible
(max, s) => Math.Max(max, s.Grade)
);
foreach (var (city, maxGrade) in maxByCity)
{
Console.WriteLine($"{city}: nota máxima = {maxGrade}");
}
Ejemplo 3: Juntar nombres en una cadena
Actualizamos nuestra lista de estudiantes:
var namesByCity = students.AggregateBy(
s => s.City,
"", // cadena inicial
(acc, s) => string.IsNullOrEmpty(acc) ? s.Name : acc + ", " + s.Name
);
foreach (var (city, names) in namesByCity)
{
Console.WriteLine($"{city}: {names}");
}
Ilustración "por dentro" (esquema)
. {Student, Student, Student, ...}
|
| (agrupamos por city)
v
["Neonville"] ["Rosewater"] ["Los Santos"]
| | |
| (acumulación) |
|___________________________|
|
v
{("Neonville", suma), ("Rosewater", suma), ...}
Comparación con los métodos "antiguos"
Antes, para todas estas tareas había que escribir GroupBy + Select, dentro de los cuales había su propio Sum, Aggregate o alguna otra cosa rara. Ahora AggregateBy esconde ese "dolor" y hace el código corto y expresivo.
Práctica: lo usamos en la app
Nuestro proyecto de clase — diario de estudiantes. Añadimos un informe de nota media por ciudad.
// Calculamos la media con AggregateBy — acumulamos suma y cantidad, luego calculamos la media
var avgByCity = students.AggregateBy(
s => s.City,
(Sum: 0, Count: 0),
(acc, s) => (acc.Sum + s.Grade, acc.Count + 1)
).Select(x => (x.Key, Average: (double)x.Result.Sum / x.Result.Count));
foreach (var (city, avg) in avgByCity)
{
Console.WriteLine($"{city}: nota media = {avg:F2}");
}
Aquí acumulamos suma y cantidad en una tupla, y luego dividimos suma entre cantidad para sacar la media.
Particularidades y trampas
Si tu acumulador es un tipo por referencia, ojo: dentro de AggregateBy se usa la misma referencia para todas las iteraciones del grupo, así que no mutéis el objeto, o puedes afectar sin querer el resultado de todo el grupo.
Y otra cosa: si necesitas no solo agrupar, sino también transformar los resultados — puedes usar .Select directamente. Intenta elegir un seed (valor inicial) semánticamente adecuado, para no tener resultados raros (por ejemplo, no uses 0 para cadenas).
4. Comparación del enfoque antiguo y el nuevo
| Tarea | LINQ antiguo | Nuevo LINQ .NET 9 |
|---|---|---|
| Número de estudiantes por ciudad | |
|
| Suma de notas por ciudad | |
|
| Nota máxima por ciudad | |
|
| Frecuencia de caracteres en una palabra | |
|
Utilidad práctica para el trabajo y proyectos reales
Métodos como CountBy y AggregateBy se vuelven imprescindibles al escribir informes estadísticos, preparar datos para visualización, construir tablas dinámicas y otras tareas típicas. Reducen el código, lo hacen más legible y, lo más importante, más fiable: menos "manualidades" con agrupaciones y agregaciones, menos posibilidades de liarla con la lógica.
En una entrevista para Junior/Middle, conocer los nuevos métodos LINQ muestra que sigues la evolución de la plataforma, y en producción — te permite no reinventar la rueda, sino usar construcciones elegantes y claras que son fáciles de leer y mantener.
GO TO FULL VERSION