Linq: GroupBy, Sum и Count

133

У меня есть коллекция товаров

public class Product {

   public Product() { }

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

Теперь я хочу сгруппировать коллекцию на основе кода продукта и вернуть объект, содержащий имя, номер или продукты для каждого кода и общую цену для каждого продукта.

public class ResultLine{

   public ResultLine() { }

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

Поэтому я использую GroupBy для группировки по ProductCode, затем вычисляю сумму, а также подсчитываю количество записей для каждого кода продукта.

Вот что у меня есть на данный момент:

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

По какой-то причине сумма сделана правильно, но счет всегда равен 1.

Данные 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" });

Результат с образцами данных:

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

У продукта 1 должно быть count = 2!

Я попытался смоделировать это в простом консольном приложении, но получил следующий результат:

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

Продукт1: следует указывать только один раз ... Код для вышеуказанного можно найти на pastebin: http://pastebin.com/cNHTBSie

ThdK
источник

Ответы:

286

Я не понимаю, откуда берется первый «результат с образцами данных», но проблема в консольном приложении заключается в том, что вы используете его SelectManyдля просмотра каждого элемента в каждой группе .

Я думаю, ты просто хочешь:

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

Использование First()здесь для получения названия продукта предполагает, что каждый продукт с одинаковым кодом продукта имеет одинаковое название. Как отмечалось в комментариях, вы можете группировать по названию продукта, а также по коду продукта, что даст одинаковые результаты, если имя всегда одинаково для любого данного кода, но, очевидно, генерирует лучший SQL в EF.

Я также полагаю , что вы должны изменить Quantityи Priceсвойство быть intи decimalтипов соответственно - почему использование строки свойство для данных , которые явно не текстуальные?

Джон Скит
источник
Хорошо, мое консольное приложение работает. Спасибо, что указали мне использовать First () и не использовали SelectMany. ResultLine на самом деле является ViewModel. Цена будет отформатирована со знаком валюты. Вот почему мне нужно, чтобы это была строка. Но я могу изменить количество на int .. Я посмотрю, может ли это также помочь для моего сайта. Я дам Вам знать.
ThdK
6
@ThdK: Нет, вы также должны сохранить Priceв десятичном формате, а затем изменить его форматирование. Следите за чистотой представления данных и переходите к представлению презентации только в последний момент.
Джон Скит
4
Почему бы не сгруппировать по ProductCode и Name? Примерно так: .GroupBy (l => new {l.ProductCode, l.Name}) и используйте ProductName = c.Key.Name,
Кирилл Бестемьянов
@KirillBestemyanov: Да, это еще один вариант, конечно.
Джон Скит
1
Этот пост дает высокие результаты при поиске информации об агрегированных результатах с использованием group by, но я хотел бы предостеречь при его использовании против EntityFramework. First / FirstOrDefault приведет к тому, что EF будет создавать вложенные выборки, которые могут иметь серьезные последствия для производительности. Предложение Кирилла об использовании GroupBy генерирует ожидаемый SQL.
ShaneH
27

Следующий запрос работает. Он использует каждую группу для выбора вместо SelectMany. SelectManyработает над каждым элементом из каждой коллекции. Например, в вашем запросе у вас есть результат 2 коллекций. SelectManyполучает все результаты, всего 3, вместо каждой коллекции. Следующий код работает с каждым IGroupingв выбранной части, чтобы ваши агрегированные операции работали правильно.

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

иногда вам нужно выбрать некоторые поля FirstOrDefault()или singleOrDefault()вы можете использовать следующий запрос:

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
не могли бы вы объяснить, почему иногда мне нужно использовать FirstOrDefault() or singleOrDefault () `?
Shanteshwar Inde
@ShanteshwarInde First () и FirstOrDefault () получает первый объект в серии, тогда как Single () и SingleOrDefault () ожидают только 1 от результата. Если Single () и SingleOrDefault () обнаруживают, что в результирующем наборе или в результате предоставленного аргумента присутствует более 1 объекта, он выдаст исключение. При использовании вы используете первый только тогда, когда хотите, возможно, образец серии и другие объекты не важны для вас, тогда как вы используете последний, если вы ожидаете только один объект и делаете что-то, если есть более одного результата , вроде зарегистрируйте ошибку.
Кристиан Нерона,