Linq: GroupBy, suma y recuento

Resuelto ThdK asked hace 11 años • 3 respuestas

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

ThdK avatar May 13 '13 19:05 ThdK
Aceptado

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 SelectManypara 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 Quantityy Pricea tipos inty decimalrespectivamente: ¿por qué utilizar una propiedad de cadena para datos que claramente no son textuales?

Jon Skeet avatar May 13 '2013 13:05 Jon Skeet

La siguiente consulta funciona. Utiliza cada grupo para realizar la selección en lugar de SelectMany. SelectManytrabaja en cada elemento de cada colección. Por ejemplo, en tu consulta tienes como resultado 2 colecciones. SelectManyobtiene todos los resultados, un total de 3, en lugar de cada colección. El siguiente código funciona en cada uno IGroupingde 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(),
              };
Charles Lambert avatar May 13 '2013 13:05 Charles Lambert

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();
Mahdi Jalali avatar May 14 '2019 06:05 Mahdi Jalali