Linq: GroupBy, Summe und Anzahl

132

Ich habe eine Sammlung von Produkten

public class Product {

   public Product() { }

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

Jetzt möchte ich die Sammlung basierend auf dem Produktcode gruppieren und ein Objekt zurückgeben, das den Namen, die Nummer oder die Produkte für jeden Code und den Gesamtpreis für jedes Produkt enthält.

public class ResultLine{

   public ResultLine() { }

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

Also benutze ich ein GroupBy, um nach ProductCode zu gruppieren, dann berechne ich die Summe und zähle auch die Anzahl der Datensätze für jeden Produktcode.

Das habe ich bisher:

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

Aus irgendeinem Grund wird die Summe korrekt gemacht, aber die Zählung ist immer 1.

Sampe Daten:

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

Ergebnis mit Beispieldaten:

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

Produkt 1 sollte count = 2 haben!

Ich habe versucht, dies in einer einfachen Konsolenanwendung zu simulieren, aber dort habe ich das folgende Ergebnis erhalten:

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

Produkt1: sollte nur einmal aufgeführt werden ... Der Code für die oben genannten Informationen finden Sie auf pastebin: http://pastebin.com/cNHTBSie

ThdK
quelle

Antworten:

284

Ich verstehe nicht, wo das erste „Ergebnis mit Beispieldaten“ herkommt, aber das Problem in der Konsole App ist , dass Sie verwenden SelectMany, betrachten in jeder Gruppe jedes Element .

Ich denke du willst nur:

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

Bei der Verwendung von First()hier zum Abrufen des Produktnamens wird davon ausgegangen, dass jedes Produkt mit demselben Produktcode denselben Produktnamen hat. Wie in den Kommentaren erwähnt, können Sie sowohl nach Produktnamen als auch nach Produktcode gruppieren. Dies führt zu denselben Ergebnissen, wenn der Name für einen bestimmten Code immer gleich ist, aber anscheinend in SQL besseres SQL generiert.

Ich würde auch vorschlagen , dass Sie die ändern sollten Quantityund PriceEigenschaften sein intund decimalTypen jeweils - Warum eine String - Eigenschaft für Daten , die eindeutig nicht textliche ist?

Jon Skeet
quelle
Ok, meine Konsolen-App funktioniert. Vielen Dank, dass Sie mich darauf hingewiesen haben, First () zu verwenden und SelectMany wegzulassen. Die ResultLine ist eigentlich ein ViewModel. Der Preis wird mit einem Währungszeichen formatiert. Deshalb muss es eine Saite sein. Aber ich könnte die Menge in int ändern. Ich werde jetzt sehen, ob dies auch für meine Website hilfreich sein kann. Ich lasse es dich wissen.
ThdK
6
@ThdK: Nein, Sie sollten Priceauch eine Dezimalzahl beibehalten und dann die Formatierung ändern. Halten Sie die Datendarstellung sauber und wechseln Sie erst im letztmöglichen Moment zu einer Präsentationsansicht.
Jon Skeet
4
Warum nicht nach Produktcode und Name gruppieren? So etwas in der Art: .GroupBy (l => new {l.ProductCode, l.Name}) und verwenden Sie ProductName = c.Key.Name,
Kirill
@ KirillBestemyanov: Ja, das ist sicherlich eine andere Option.
Jon Skeet
Ok, es scheint, dass meine Sammlung tatsächlich eine Hülle um die reale Sammlung war. Erschieß mich. Gut, dass ich heute Linq geübt habe :)
ThdK
27

Die folgende Abfrage funktioniert. Es verwendet jede Gruppe, um die Auswahl anstelle von durchzuführen SelectMany. SelectManyarbeitet an jedem Element aus jeder Sammlung. In Ihrer Abfrage haben Sie beispielsweise ein Ergebnis von 2 Sammlungen. SelectManyErhält alle Ergebnisse, insgesamt 3, anstelle jeder Sammlung. Der folgende Code funktioniert jeweils IGroupingim ausgewählten Bereich, damit Ihre Aggregatvorgänge ordnungsgemäß funktionieren.

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
quelle
2

Manchmal müssen Sie einige Felder auswählen, FirstOrDefault()oder singleOrDefault()Sie können die folgende Abfrage verwenden:

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
quelle
1
FirstOrDefault() or Kannst du bitte erklären, warum ich manchmal singleOrDefault () `verwenden muss?
Shanteshwar Inde
@ShanteshwarInde First () und FirstOrDefault () erhalten das erste Objekt in einer Reihe, während Single () und SingleOrDefault () nur 1 vom Ergebnis erwarten. Wenn Single () und SingleOrDefault () feststellen, dass eine Ergebnismenge oder das angegebene Argument mehr als ein Objekt enthält, wird eine Ausnahme ausgelöst. Bei der Verwendung verwenden Sie das erstere, wenn Sie nur möchten, möglicherweise ist ein Beispiel einer Serie und der anderen Objekte für Sie nicht wichtig, während Sie das letztere verwenden, wenn Sie nur ein Objekt erwarten, und etwas tun, wenn es mehr als ein Ergebnis gibt , wie den Fehler protokollieren.
Kristianne Nerona