182 votes

Linq : GroupBy, Sum et Count

J'ai une collection de produits

public class Product {

   public Product() { }

   public string ProductCode {get; set;}
   public decimal Price {get; set; }
   public string Name {get; set;}
}

Je veux maintenant regrouper la collection en fonction du code produit et renvoyer un objet contenant le nom, le nombre de produits pour chaque code et le prix total pour chaque produit.

public class ResultLine{

   public ResultLine() { }

   public string ProductName {get; set;}
   public string Price {get; set; }
   public string Quantity {get; set;}
}

J'utilise donc un GroupBy pour regrouper par ProductCode, puis je calcule la somme et compte également le nombre d'enregistrements pour chaque code produit.

C'est ce que j'ai jusqu'à présent :

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>();

Pour une raison quelconque, la somme est faite correctement mais le compte est toujours 1.

Les données de Sampe :

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" });

Résultat avec données échantillons :

Product1: count 1   - Price:13 (2x6.5)
Product2: count 1   - Price:12 (1x12)

Le produit 1 devrait avoir le compte = 2 !

J'ai essayé de simuler cela dans une simple application console mais j'ai obtenu le résultat suivant :

Product1: count 2   - Price:13 (2x6.5)
Product1: count 2   - Price:13 (2x6.5)
Product2: count 1   - Price:12 (1x12)

Produit1 : ne doit être listé qu'une seule fois... Le code de ce qui précède peut être trouvé sur pastebin : http://pastebin.com/cNHTBSie

372voto

Jon Skeet Points 692016

Je ne comprends pas d'où vient le premier "résultat avec les données de l'échantillon", mais le problème dans l'application console est que vous utilisez SelectMany à regarder chaque élément de chaque groupe .

Je pense que tu veux juste :

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();

L'utilisation de First() ici pour obtenir le nom du produit suppose que chaque produit ayant le même code produit a le même nom de produit. Comme indiqué dans les commentaires, vous pourriez regrouper par nom de produit ainsi que par code de produit, ce qui donnera les mêmes résultats si le nom est toujours le même pour un code donné, mais génère apparemment un meilleur SQL dans EF.

Je suggérerais également que vous modifiiez l'option Quantity y Price les propriétés à être int y decimal Pourquoi utiliser une propriété de type chaîne de caractères pour des données qui ne sont manifestement pas textuelles ?

41voto

Charles Lambert Points 3051

La requête suivante fonctionne. Elle utilise chaque groupe pour effectuer la sélection au lieu de SelectMany . SelectMany travaille sur chaque élément de chaque collection. Par exemple, dans votre requête, vous avez un résultat de 2 collections. SelectMany obtient tous les résultats, un total de 3, au lieu de chaque collection. Le code suivant fonctionne sur chaque IGrouping dans la partie sélectionnée pour que vos opérations d'agrégation fonctionnent correctement.

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(),
              };

7voto

Mahdi Jalali Points 107

Vous devez parfois sélectionner certains champs par FirstOrDefault() o singleOrDefault() vous pouvez utiliser la requête ci-dessous :

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();

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X