Linq: GroupBy, Sum and Count


133

Ho una collezione di prodotti

public class Product {

   public Product() { }

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

Ora voglio raggruppare la raccolta in base al codice prodotto e restituire un oggetto contenente il nome, il numero o i prodotti per ciascun codice e il prezzo totale per ciascun prodotto.

public class ResultLine{

   public ResultLine() { }

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

Quindi utilizzo GroupBy per raggruppare per ProductCode, quindi calcolo la somma e conto anche il numero di record per ciascun codice prodotto.

Questo è quello che ho finora:

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

Per qualche motivo, la somma viene eseguita correttamente ma il conteggio è sempre 1.

Dati di campionamento:

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

Risultato con dati di esempio:

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

Il prodotto 1 dovrebbe avere count = 2!

Ho provato a simulare questo in una semplice applicazione console ma lì ho ottenuto il seguente risultato:

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

Product1: dovrebbe essere elencato una sola volta ... Il codice per quanto sopra è disponibile su pastebin: http://pastebin.com/cNHTBSie

Risposte:


285

Non capisco da dove provenga il primo "risultato con dati di esempio", ma il problema nell'app della console è che stai usando SelectManyper esaminare ogni elemento di ciascun gruppo .

Penso che tu voglia solo:

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'uso di First()qui per ottenere il nome del prodotto presuppone che ogni prodotto con lo stesso codice prodotto abbia lo stesso nome prodotto. Come notato nei commenti, è possibile raggruppare per nome prodotto e codice prodotto, il che darà gli stessi risultati se il nome è sempre lo stesso per un determinato codice, ma apparentemente genera un SQL migliore in EF.

Suggerirei anche di modificare rispettivamente le proprietà Quantitye e i tipi: perché usare una proprietà stringa per i dati che non è chiaramente testuale?Priceintdecimal


Ok, la mia app per console funziona. Grazie per avermi segnalato di usare First () e di escludere SelectMany. ResultLine è in realtà un ViewModel. Il prezzo verrà formattato con il segno di valuta. Ecco perché ne ho bisogno per essere una stringa. Ma potrei cambiare la quantità in int .. Vedrò ora se questo può aiutare anche per il mio sito web. Ti farò sapere.
Giovedì

6
@ThdK: No, dovresti mantenere Priceanche un numero decimale, quindi cambiare il modo in cui lo formatti. Mantieni pulita la rappresentazione dei dati e passa a una vista di presentazione solo all'ultimo momento possibile.
Jon Skeet,

4
Perché non raggruppare per codice prodotto e nome? Qualcosa del genere: .GroupBy (l => new {l.ProductCode, l.Name}) e usa ProductName = c.Key.Name,
Kirill Bestemyanov,

@KirillBestemyanov: Sì, questa è un'altra opzione, certamente.
Jon Skeet,

Ok, sembra che la mia collezione fosse in realtà un involucro attorno alla vera collezione ..
Sparami .. Per fortuna

27

La seguente query funziona. Usa ogni gruppo per fare la selezione invece di SelectMany. SelectManyfunziona su ciascun elemento di ogni raccolta. Ad esempio, nella tua query hai un risultato di 2 raccolte. SelectManyottiene tutti i risultati, per un totale di 3, invece di ogni raccolta. Il seguente codice funziona su ciascuno IGroupingnella parte selezionata per far funzionare correttamente le operazioni di aggregazione.

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

2

a volte è necessario selezionare alcuni campi FirstOrDefault()oppure singleOrDefault()è possibile utilizzare la query seguente:

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

1
puoi spiegare perché a volte ho bisogno di usare FirstOrDefault() or singleOrDefault () `?
Shanteshwar Inde

@ShanteshwarInde First () e FirstOrDefault () ottiene il primo oggetto di una serie, mentre Single () e SingleOrDefault () prevede solo 1 dal risultato. Se Single () e SingleOrDefault () rileva che ci sono più di 1 oggetto in un set di risultati o come risultato dell'argomento fornito, genererà un'eccezione. All'utilizzo, usi il primo quando vuoi, forse un campione di una serie e gli altri oggetti non sono importanti per te, mentre usi il secondo se ti aspetti solo un oggetto e fai qualcosa se ci sono più di un risultato , come registrare l'errore.
Kristianne Nerona,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.