Linq: GroupBy, Sum i Count

137

Mam kolekcję produktów

public class Product {

   public Product() { }

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

Teraz chcę pogrupować kolekcję na podstawie kodu produktu i zwrócić obiekt zawierający nazwę, liczbę lub produkty dla każdego kodu i całkowitą cenę dla każdego produktu.

public class ResultLine{

   public ResultLine() { }

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

Więc używam GroupBy do grupowania według ProductCode, a następnie obliczam sumę i liczę liczbę rekordów dla każdego kodu produktu.

Oto, co mam do tej pory:

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

Z jakiegoś powodu suma jest wykonywana poprawnie, ale liczba zawsze wynosi 1.

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

Wynik z przykładowymi danymi:

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

Produkt 1 powinien mieć liczbę = 2!

Próbowałem to zasymulować w prostej aplikacji konsolowej, ale otrzymałem następujący wynik:

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

Produkt1: powinien być wymieniony tylko raz ... Kod powyższego można znaleźć na pastebin: http://pastebin.com/cNHTBSie

ThdK
źródło

Odpowiedzi:

295

Nie rozumiem, skąd pochodzi pierwszy „wynik z przykładowymi danymi”, ale problem w aplikacji konsoli polega na tym, że używasz SelectManydo przeglądania każdego elementu w każdej grupie .

Myślę, że po prostu chcesz:

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

Użycie First()tutaj do uzyskania nazwy produktu zakłada, że ​​każdy produkt z tym samym kodem produktu ma tę samą nazwę produktu. Jak zauważono w komentarzach, możesz grupować według nazwy produktu, a także kodu produktu, co da te same wyniki, jeśli nazwa jest zawsze taka sama dla dowolnego podanego kodu, ale najwyraźniej generuje lepszy SQL w EF.

Chciałbym również zasugerować, że należy zmienić Quantityi Pricewłaściwości, aby być inti decimaltypy odpowiednio - dlaczego stosowanie właściwość ciąg danych, który nie jest wyraźnie tekstowy?

Jon Skeet
źródło
OK, moja aplikacja konsoli działa. Dzięki za wskazanie mi opcji First () i pominięcie SelectMany. ResultLine jest w rzeczywistości ViewModel. Cena zostanie sformatowana ze znakiem waluty. Dlatego potrzebuję, żeby to był sznurek. Ale mogę zmienić ilość na int. Zobaczę teraz, czy to również pomoże mojej stronie. Dam ci znać.
Czw
6
@ThdK: Nie, powinieneś również zachować Pricejako ułamek dziesiętny, a następnie zmienić sposób jego formatowania. Utrzymuj czystość reprezentacji danych i zmieniaj widok na prezentację tylko w ostatniej możliwej chwili.
Jon Skeet
4
Dlaczego nie pogrupować według kodu produktu i nazwy? Coś takiego: .GroupBy (l => nowy {l.ProductCode, l.Name}) i użyj ProductName = c.Key.Name,
Kirill Bestemyanov
@KirillBestemyanov: Tak, to z pewnością inna opcja.
Jon Skeet
1
Ten post uzyskuje wysokie wyniki podczas wyszukiwania informacji o wynikach zagregowanych za pomocą grupowania według, ale chciałem wyrazić ostrożność podczas używania tego przeciwko EntityFramework. First / FirstOrDefault spowoduje, że EF wygeneruje zagnieżdżone selekcje, które mogą mieć poważne konsekwencje dla wydajności. Sugestia Kirilla dotycząca użycia GroupBy generuje SQL, którego można by się spodziewać.
ShaneH
28

Następujące zapytanie działa. Używa każdej grupy do dokonania wyboru zamiast SelectMany. SelectManydziała na każdym elemencie z każdej kolekcji. Na przykład w zapytaniu masz wynik 2 kolekcji. SelectManypobiera wszystkie wyniki, łącznie 3, zamiast każdego zbioru. Poniższy kod działa na każdym IGroupingz wybranych fragmentów, aby zapewnić prawidłowe działanie operacji agregujących.

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
źródło
2

czasami musisz wybrać niektóre pola FirstOrDefault()lub singleOrDefault()możesz użyć poniższego zapytania:

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
źródło
1
czy możesz wyjaśnić, dlaczego czasami potrzebuję użyć funkcji FirstOrDefault() or singleOrDefault () `?
Shanteshwar Inde
1
@ShanteshwarInde First () i FirstOrDefault () pobiera pierwszy obiekt w serii, podczas gdy Single () i SingleOrDefault () oczekują tylko 1 z wyniku. Jeśli Single () i SingleOrDefault () zauważą, że w zestawie wyników lub w wyniku podanego argumentu jest więcej niż 1 obiekt, zgłosi wyjątek. Używając tego pierwszego, używasz tylko wtedy, gdy chcesz, może próbka serii i inne obiekty nie są dla ciebie ważne, podczas gdy używasz drugiego, jeśli oczekujesz tylko jednego obiektu i robisz coś, jeśli jest więcej niż jeden wynik , jak zarejestruj błąd.
Kristianne Nerona