CodeGym /Cursos /C# SELF /Nuevos métodos LINQ: Count...

Nuevos métodos LINQ: CountBy y AggregateBy

C# SELF
Nivel 32 , Lección 2
Disponible

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)
"Neonville"
3 3
"Rosewater"
1 1
"Los Santos"
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
GroupBy + Select + Count
students.CountBy(s => s.City)
Suma de notas por ciudad
GroupBy + Select + Sum
students.AggregateBy(..., 0, ...)
Nota máxima por ciudad
GroupBy + Select + Max
students.AggregateBy(..., ...)
Frecuencia de caracteres en una palabra
GroupBy + Select + Count
word.CountBy(ch => ch)

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.

Comentarios
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION