El agrupamiento de un conjunto de datos mediante LINQ, ya sea mediante la extensión de IEnumerable o IQueryable, tiene una serie de características las cuales me gustaría comentar aquí.
Para explicar un poco las posibilidades del agrupamiento mediante la extensión GroupBy voy a exponer un ejemplo con LINQ to Objects y para ello, primero, definiremos una clase sencilla que represente la entidad Cliente.
public class Cliente
{
public int IdProvincia { get; set; }
public int Tipo { get; set; }
public string Nombre { get; set; }
public decimal VolumenNegocio { get; set; }
public override string ToString()
{
return string.Format("\t\t-> Cliente: {0} - Provincia:{1} - Tipo: {2} - Vol.:{3}e\n",
Nombre, IdProvincia, Tipo,VolumenNegocio);
}
}
Como se puede observar no es más que una clase con 4 propiedades implícitas y el método ToString() sobrescrito para poder mostrar por la consola la información de un objeto iterado del tipo Cliente.
Vamos a generar una lista de clientes mediante una lista genérica de la siguiente forma:
//inicializamos lista de clientes
var list = new List<Cliente>
{
new Cliente {IdProvincia = 1, Nombre = "Cliente1", Tipo = 10, VolumenNegocio = 100.00m},
new Cliente {IdProvincia = 2, Nombre = "Cliente2", Tipo = 20, VolumenNegocio = 20.00m},
new Cliente {IdProvincia = 3, Nombre = "Cliente3", Tipo = 20, VolumenNegocio = 230.00m},
new Cliente {IdProvincia = 3, Nombre = "Cliente4", Tipo = 20, VolumenNegocio = 500.00m},
new Cliente {IdProvincia = 1, Nombre = "Cliente5", Tipo = 30, VolumenNegocio = 10.00m},
new Cliente {IdProvincia = 2, Nombre = "Cliente6", Tipo = 10, VolumenNegocio = 750.00m},
new Cliente {IdProvincia = 1, Nombre = "Cliente7", Tipo = 20, VolumenNegocio = 340.00m},
new Cliente {IdProvincia = 1, Nombre = "Cliente8", Tipo = 20, VolumenNegocio = 170.00m}
};
La intención es agrupar esta lista de clientes por la provincia, a la cual pertenecen, a través de la propiedad
IdProvincia del tipo
int. Para ello, en primer lugar, echaremos un vistazo a la extensión
GroupBy que incorpora la clase
System.Linq.Enumerable. Dicha extensión presenta las siguientes sobrecargas:
public static IEnumerable<IGrouping<TKey, TSource>> GroupBy<TSource, TKey>
(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector);
public static IEnumerable<TResult> GroupBy<TSource, TKey, TResult>
(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector,
Func<TKey, IEnumerable<TSource>, TResult> resultSelector);
public static IEnumerable<IGrouping<TKey, TElement>> GroupBy<TSource, TKey, TElement>
(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector,
Func<TSource, TElement> elementSelector);
public static IEnumerable<IGrouping<TKey, TSource>> GroupBy<TSource, TKey>
(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector,
IEqualityComparer<TKey> comparer);
public static IEnumerable<TResult> GroupBy<TSource, TKey, TResult>
(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector,
Func<TKey, IEnumerable<TSource>, TResult> resultSelector,
IEqualityComparer<TKey> comparer);
public static IEnumerable<TResult> GroupBy<TSource, TKey, TElement, TResult>
(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector,
Func<TSource, TElement> elementSelector,
Func<TKey, IEnumerable<TElement>, TResult> resultSelector);
public static IEnumerable<IGrouping<TKey, TElement>> GroupBy<TSource, TKey, TElement>
(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector,
Func<TSource, TElement> elementSelector,
IEqualityComparer<TKey> comparer);
public static IEnumerable<TResult> GroupBy<TSource, TKey, TElement, TResult>
(this IEnumerable<TSource> source, Func<TSource, TKey> keySelector,
Func<TSource, TElement> elementSelector,
Func<TKey, IEnumerable<TElement>, TResult> resultSelector,
IEqualityComparer<TKey> comparer);
En todas ellas existen un denominador común, el parámetro keySelector del tipo Func<TSource, TElement> que determina el valor de tipo TKey por el cual se agrupara la lista. Por otro lado GroupBy puede retornar un objeto del tipo IEnumerable<TSource> si se especifica el parámetro resultSelector (tipo Func<TSource, TKey>) o bien un objeto del tipo IEnumerable<IGroupingKey<TKey, TElement>> para todos los demás. Vamos centrarnos en éste último por ahora.
Manejando resultados IEnumerable<IGroupingKey<TKey, TElement>>
Si el agrupamiento se realiza sobre una lista de elementos iterativos o “IEnumerables” es obvio que el resultado, otra lista de subelementos agrupados por una clave del tipo TKey, sea otra lista de elementos iterativos de ahí que el tipo resultante sea IEnumerable<T>. La singularidad esta en el tipo genérico en la iteración del valor retornado el cual es del tipo IGroupingKey<TKey,TElement> y vamos a ver el porqué. Lo que LINQ hará ante un GroupBy es realizar un recorrido completo por la lista y seleccionará todos los elementos del tipo TElement de una misma clave (TKey). Por consiguiente el resultado será una lista del agrupamiento de elementos definidos por la interfaz IGroupingKey<TKey,TElement>, en la que almacenará la clave y el conjunto de elemento agrupados por el valor de dicha clave, respetivamente en cada tipo genérico. Veamos un ejemplo.
En nuestra lista de clientes vamos a agrupar los elementos por el identificador de provincia definido por Cliente.IdProvincia. Para ello definiremos una variable del tipo IEnumerable<IGroupingKey<int,Cliente> pues el tipo de datos de agrupamiento es del tipo int (propiedad IdProvincia) y el elemento a agrupar es del tipo Cliente, por tanto:
IEnumerable<IGrouping<int, Cliente>> groupByProv =
from cliente in list group cliente by cliente.IdProvincia;
O lo que es lo mismo:
var groupByProv = list.GroupBy(cliente => cliente.IdProvincia);
Ahora la variable groupByProv posee una lista de todas las clave/agrupaciones realizadas. De hecho podemos comprobarlo de la siguiente forma:
foreach (var cProv in groupByProv)//cProv es del tipo IEnumerable<IGrouping<int, Cliente>>
{
Console.WriteLine(
string.Format("Provincia {0}, {1} clientes", cProv.Key, cProv.Count()));
}
Cuyo resultado por pantalla seria:
La variable implícita cProv posee tanto la clave utilizada (IdProvinicia) como la iteración de los clientes agrupados por dicha propiedad, de forma que podría ser recorrida de igual forma.
foreach (var cProv in groupByProv)//cProv es del tipo IEnumerable<IGrouping<int, Cliente>>
{
Console.WriteLine(
string.Format("Provincia {0}, {1} clientes", cProv.Key, cProv.Count()));
foreach(var cliente in cProv)
Console.WriteLine(cliente.ToString());
}
Y es resultado:
Sin embargo podríamos rizar aún más el rizo y agrupar, de nuevo, cada una de las agrupaciones por el tipo de cliente. De esta forma tendríamos una primera agrupación según la IdProvincia del cliente y por cada uno, una agrupación por tipo de cliente de la siguiente forma:
1: foreach (var cProv in groupByProv)//cProv es del tipo IEnumerable<IGrouping<int, Cliente>>
2: {
3: Console.WriteLine(string.Format("Provincia {0} ", cProv.Key));
4:
5: IEnumerable<IGrouping<int, Cliente>> groupByTipo = cProv.GroupBy(cliente => cliente.Tipo);
6:
7: foreach (var cTipo in groupByTipo)
8: {
9: Console.WriteLine(string.Format("\tTipo {0} \n", cTipo.Key));
10:
11: foreach (var cliente in cTipo)
12: Console.WriteLine(cliente.ToString());
13: }
14: }
Lo que hemos hecho en el ejemplo anterior es aprovechar la iteración del primer agrupamiento para volver a generar un extensión GroupBy pero en esta ocasión con la propiedad Cliente.Tipo (int) como TKey. El resultado:
Manejando resultados IEnumerable<TSource>
Como comenté anteriormente, GroupBy puede devolver una iteración de tipo IEnumerable<TSource> indicando en la sobrecarga el parámetro resultSelector. Para ver este ejemplo, en base a la lista de cliente generada anteriormente, vamos a mostrar, por ejemplo, para cada una de las provincias (agrupamiento) el número total de clientes, el valor máximo, mínimo y medio de la propiedad Cliente.VolumenNegocio (decimal) de modo que nos ofrecerá información, por cada provincia, de los valores máximo, mínimo y medio del volumen de negocio de los clientes.
Para ello utilizaremos la sobrecarga:
1: System.Linq.Enumerable.GroupBy<TSource, TKey, TResult>(
2: this System.Collections.Generic.IEnumerable<TSource>,
3: System.Func<TSource,TKey>,
4: System.Func<TKey,System.Collections.Generic.IEnumerable<TSource>,TResult>)
La implementación será:
1: var groupByInfo = list.GroupBy(
2: cliente => cliente.IdProvincia, //seguimos con el agrupamiento por provinicia
3: (provincia, clientes) => new //retornamos un tipo anónimo
4: {
5: //con info sobre el agrupamiento
6: Key = provincia,
7: Count = clientes.Count(),
8: Min = clientes.Min(c => c.VolumenNegocio),
9: Max = clientes.Max(c => c.VolumenNegocio),
10: Avg = clientes.Average(c => c.VolumenNegocio)
11: });
El primer lugar, el tipo de la iteración de retorno es del tipo anónimo y es por ello que nos basamos en una variable implícitamente tipada (var). El primer parámetro (línea 2) es del tipo keySelector que vimos anteriormente y como dije es común en todas las sobrecargas pues es el valor clave de agrupamiento. Seguidamente el segundo parámetro (líneas 3-11), del tipo Func<int,IEnumerable<Cliente>,anonymous type> es donde se proyectará el resultado del agrupamiento.
Iteramos y mostramos los resultados.
1: foreach (var res in groupByInfo)
2: Console.WriteLine(
3: string.Format("Provinicia:{0}\n\tCount:{1}\n\tMin Volumen:{2}\n\tMax Volumen:{3}\n\tAvg:{4}"
4: , res.Key, res.Count, res.Min, res.Max, res.Avg));
En total 3 iteraciones con sus respectivas “estadísticas”:
El código completo:
namespace LINQGroupingBy
{
using System;
using System.Collections.Generic;
using System.Linq;
class Program
{
static void Main(string[] args)
{
//Ejemplo para LINQ to Objects
//inicializamos lista de clientes
var list = new List<Cliente>
{
new Cliente {IdProvincia = 1, Nombre = "Cliente1", Tipo = 10, VolumenNegocio = 100.00m},
new Cliente {IdProvincia = 2, Nombre = "Cliente2", Tipo = 20, VolumenNegocio = 20.00m},
new Cliente {IdProvincia = 3, Nombre = "Cliente3", Tipo = 20, VolumenNegocio = 230.00m},
new Cliente {IdProvincia = 3, Nombre = "Cliente4", Tipo = 20, VolumenNegocio = 500.00m},
new Cliente {IdProvincia = 1, Nombre = "Cliente5", Tipo = 30, VolumenNegocio = 10.00m},
new Cliente {IdProvincia = 2, Nombre = "Cliente6", Tipo = 10, VolumenNegocio = 750.00m},
new Cliente {IdProvincia = 1, Nombre = "Cliente7", Tipo = 20, VolumenNegocio = 340.00m},
new Cliente {IdProvincia = 1, Nombre = "Cliente8", Tipo = 20, VolumenNegocio = 170.00m}
};
//obtenemos todos los clientes agrupados por provincias
var groupByProv = list.GroupBy(cliente => cliente.IdProvincia);
//tambien se puede expresar como:
//
//IEnumerable<IGrouping<int, Cliente>> groupByProv =
// from cliente in list group cliente by cliente.IdProvincia;
foreach (var cProv in groupByProv)//cProv es del tipo IEnumerable<IGrouping<int, Cliente>>
{
Console.WriteLine(
string.Format("Provincia {0}, {1} clientes", cProv.Key, cProv.Count()));
foreach(var cliente in cProv)
Console.WriteLine(cliente.ToString());
}
Console.ReadKey();
foreach (var cProv in groupByProv)//cProv es del tipo IEnumerable<IGrouping<int, Cliente>>
{
Console.WriteLine(string.Format("Provincia {0} ", cProv.Key));
IEnumerable<IGrouping<int, Cliente>> groupByTipo = cProv.GroupBy(cliente => cliente.Tipo);
foreach (var cTipo in groupByTipo)
{
Console.WriteLine(string.Format("\tTipo {0} \n", cTipo.Key));
foreach (var cliente in cTipo)
Console.WriteLine(cliente.ToString());
}
}
Console.ReadKey();
Console.Clear();
//volvemos a agrupar
var groupByInfo = list.GroupBy(
cliente => cliente.IdProvincia, //seguimos con el agrupamiento por provinicia
(provincia, clientes) => new //retornamos un tipo anónimo
{
//con info sobre el agrupamiento
Key = provincia,
Count = clientes.Count(),
Min = clientes.Min(c => c.VolumenNegocio),
Max = clientes.Max(c => c.VolumenNegocio),
Avg = clientes.Average(c => c.VolumenNegocio)
});
//mostramos los resultados
foreach (var res in groupByInfo)
Console.WriteLine(
string.Format("Provinicia:{0}\n\tCount:{1}\n\tMin Volumen:{2}\n\tMax Volumen:{3}\n\tAvg:{4}"
, res.Key, res.Count, res.Min, res.Max, res.Avg));
Console.ReadKey();
}
public class Cliente
{
public int IdProvincia { get; set; }
public int Tipo { get; set; }
public string Nombre { get; set; }
public decimal VolumenNegocio { get; set; }
public override string ToString()
{
return string.Format("\t\t-> Cliente: {0} - Provincia:{1} - Tipo: {2} - Vol.:{3}e\n",
Nombre, IdProvincia, Tipo, VolumenNegocio);
}
}
}
}