Linq: GroupBy, suma y recuento
tengo una coleccion de productos
public class Product {
public Product() { }
public string ProductCode {get; set;}
public decimal Price {get; set; }
public string Name {get; set;}
}
Ahora quiero agrupar la colección según el código de producto y devolver un objeto que contenga el nombre, el número de productos para cada código y el precio total de cada producto.
public class ResultLine{
public ResultLine() { }
public string ProductName {get; set;}
public string Price {get; set; }
public string Quantity {get; set;}
}
Entonces uso GroupBy para agrupar por ProductCode, luego calculo la suma y también cuento la cantidad de registros para cada código de producto.
Esto es lo que tengo hasta ahora:
List<Product> Lines = LoadProducts();
List<ResultLine> result = Lines
.GroupBy(l => l.ProductCode)
.SelectMany(cl => cl.Select(
csLine => new ResultLine
{
ProductName =csLine.Name,
Quantity = cl.Count().ToString(),
Price = cl.Sum(c => c.Price).ToString(),
})).ToList<ResultLine>();
Por alguna razón, la suma se hace correctamente pero el conteo siempre es 1.
Datos de muestra:
List<CartLine> Lines = new List<CartLine>();
Lines.Add(new CartLine() { ProductCode = "p1", Price = 6.5M, Name = "Product1" });
Lines.Add(new CartLine() { ProductCode = "p1", Price = 6.5M, Name = "Product1" });
Lines.Add(new CartLine() { ProductCode = "p2", Price = 12M, Name = "Product2" });
Resultado con datos de muestra:
Product1: count 1 - Price:13 (2x6.5)
Product2: count 1 - Price:12 (1x12)
¡El producto 1 debería tener un recuento = 2!
Intenté simular esto en una aplicación de consola simple pero obtuve el siguiente resultado:
Product1: count 2 - Price:13 (2x6.5)
Product1: count 2 - Price:13 (2x6.5)
Product2: count 1 - Price:12 (1x12)
Producto1: solo debe aparecer en la lista una vez... El código para lo anterior se puede encontrar en Pastebin: http://pastebin.com/cNHTBSie
No entiendo de dónde viene el primer "resultado con datos de muestra", pero el problema en la aplicación de consola es que estás usando SelectMany
para mirar cada elemento de cada grupo. .
Creo que solo quieres:
List<ResultLine> result = Lines
.GroupBy(l => l.ProductCode)
.Select(cl => new ResultLine
{
ProductName = cl.First().Name,
Quantity = cl.Count().ToString(),
Price = cl.Sum(c => c.Price).ToString(),
}).ToList();
El uso deFirst()
aquí para obtener el nombre del producto supone que todos los productos con el mismo código de producto tienen el mismo nombre de producto. Como se indicó en los comentarios, puede agrupar por nombre de producto y por código de producto, lo que dará los mismos resultados si el nombre es siempre el mismo para cualquier código determinado, pero aparentemente genera un mejor SQL en EF.
También sugeriría que cambie las propiedades Quantity
y Price
a tipos int
y decimal
respectivamente: ¿por qué utilizar una propiedad de cadena para datos que claramente no son textuales?
La siguiente consulta funciona. Utiliza cada grupo para realizar la selección en lugar de SelectMany
. SelectMany
trabaja en cada elemento de cada colección. Por ejemplo, en tu consulta tienes como resultado 2 colecciones. SelectMany
obtiene todos los resultados, un total de 3, en lugar de cada colección. El siguiente código funciona en cada uno IGrouping
de los de la parte seleccionada para que sus operaciones agregadas funcionen correctamente.
var results = from line in Lines
group line by line.ProductCode into g
select new ResultLine {
ProductName = g.First().Name,
Price = g.Sum(pc => pc.Price).ToString(),
Quantity = g.Count().ToString(),
};
A veces es necesario seleccionar algunos campos FirstOrDefault()
o singleOrDefault()
puede utilizar la siguiente consulta:
List<ResultLine> result = Lines
.GroupBy(l => l.ProductCode)
.Select(cl => new Models.ResultLine
{
ProductName = cl.select(x=>x.Name).FirstOrDefault(),
Quantity = cl.Count().ToString(),
Price = cl.Sum(c => c.Price).ToString(),
}).ToList();