Linq: GroupBy, Sum et Count

133

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

Maintenant, je souhaite regrouper la collection en fonction du code produit et renvoyer un objet contenant le nom, le numéro ou les produits pour chaque code et le prix total de 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 grouper par ProductCode, puis je calcule la somme et je compte également le nombre d'enregistrements pour chaque code produit.

Voici 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 effectuée correctement mais le compte est toujours 1.

Données 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 des exemples de données:

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

Le produit 1 doit avoir count = 2!

J'ai essayé de simuler cela dans une application console simple mais là 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)

Product1: ne doit être répertorié qu'une seule fois ... Le code ci-dessus peut être trouvé sur pastebin: http://pastebin.com/cNHTBSie

ThdK
la source

Réponses:

285

Je ne comprends pas d'où vient le premier "résultat avec des exemples de données", mais le problème dans l'application console est que vous utilisez SelectManypour examiner 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()here pour obtenir le nom du produit suppose que chaque produit avec le même code produit a le même nom de produit. Comme indiqué dans les commentaires, vous pouvez regrouper par nom de produit ainsi que par code 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ère également que vous devez changer les Quantityet Pricepropriétés à intet decimaltypes respectivement - Pourquoi utiliser une propriété de chaîne pour les données qui est clairement pas textuelle?

Jon Skeet
la source
Ok, mon application console fonctionne. Merci de m'avoir indiqué d'utiliser First () et d'omettre SelectMany. Le ResultLine est en fait un ViewModel. Le prix sera formaté avec le signe de la devise. C'est pourquoi j'ai besoin que ce soit une chaîne. Mais je pourrais changer la quantité en int .. Je vais voir maintenant si cela peut aussi aider pour mon site Web. Je te ferai savoir.
Jeudi
6
@ThdK: Non, vous devez également le conserver Pricesous forme décimale, puis modifier la façon dont vous le formatez. Gardez la représentation des données propre et ne passez à une vue de présentation qu'au dernier moment possible.
Jon Skeet
4
Pourquoi ne pas regrouper par ProductCode et par nom? Quelque chose comme ça: .GroupBy (l => new {l.ProductCode, l.Name}) et utilisez ProductName = c.Key.Name,
Kirill Bestemyanov
@KirillBestemyanov: Oui, c'est certainement une autre option.
Jon Skeet
Ok, il semble que ma collection était en fait un emballage autour de la vraie collection .. Tirez sur moi .. La bonne chose est que j'ai pratiqué un peu de Linq aujourd'hui :)
ThdK
27

La requête suivante fonctionne. Il utilise chaque groupe pour effectuer la sélection au lieu de SelectMany. SelectManytravaille sur chaque élément de chaque collection. Par exemple, dans votre requête, vous avez un résultat de 2 collections. SelectManyobtient tous les résultats, un total de 3, au lieu de chaque collection. Le code suivant fonctionne sur chaque IGroupingélément de la partie de sélection 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(),
              };
Charles Lambert
la source
2

Parfois, vous devez sélectionner certains champs par FirstOrDefault()ou 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();
Mahdi Jalali
la source
1
pouvez-vous s'il vous plaît expliquer pourquoi j'ai parfois besoin d'utiliser FirstOrDefault() or singleOrDefault () `?
Shanteshwar Inde
@ShanteshwarInde First () et FirstOrDefault () obtient le premier objet d'une série, tandis que Single () et SingleOrDefault () n'en attendent qu'un du résultat. Si Single () et SingleOrDefault () voient qu'il y a plus d'un objet dans un jeu de résultats ou à la suite de l'argument fourni, il lèvera une exception. À l'utilisation, vous utilisez le premier lorsque vous ne voulez que, peut-être qu'un échantillon d'une série et les autres objets ne sont pas importants pour vous, alors que vous utilisez ce dernier si vous n'attendez qu'un seul objet et faites quelque chose s'il y a plus d'un résultat , comme consigner l'erreur.
Kristianne Nerona le