Avatar ·

Группировка по нескольким столбцам с помощью LINQ в C#

Привет всем! Я новичок в C# и LINQ, и столкнулся с проблемой при группировке данных. У меня есть список заказов (List), где каждый заказ содержит поля: CustomerId, ProductId, Quantity и Price. Мне нужно сгруппировать заказы по двум полям одновременно — по CustomerId и ProductId — чтобы получить общее количество и сумму для каждого клиента и товара. Вот мой класс: ```csharp public class Order { public int CustomerId { get; set; } public int ProductId { get; set; } public int Quantity { get; set; } public decimal Price { get; set; } } ``` Я пробовал использовать GroupBy с одним полем, например: ```csharp var grouped = orders.GroupBy(o => o.CustomerId); ``` Это работает, но мне нужно сгруппировать сразу по двум полям. Я читал, что можно использовать анонимный тип, но не понимаю, как потом обращаться к ключу и как правильно суммировать Quantity и Price в каждой группе. Подскажите, как правильно написать LINQ-запрос для группировки по CustomerId и ProductId с последующим подсчётом суммы Quantity и суммы Price для каждой группы? Буду благодарен за примеры кода! <<>>

Решение: группировка с помощью анонимного типа

Для группировки по нескольким полям в LINQ используется анонимный тип в качестве ключа. Это позволяет объединить несколько полей в один составной ключ. После группировки вы можете обращаться к отдельным полям ключа через свойства анонимного типа.

var result = orders
    .GroupBy(o => new { o.CustomerId, o.ProductId })
    .Select(g => new
    {
        CustomerId = g.Key.CustomerId,
        ProductId = g.Key.ProductId,
        TotalQuantity = g.Sum(o => o.Quantity),
        TotalPrice = g.Sum(o => o.Price)
    })
    .ToList();

Объяснение:

  • GroupBy(o => new { o.CustomerId, o.ProductId }) — создаёт группы с составным ключом. Каждая группа содержит все заказы с одинаковым сочетанием CustomerId и ProductId.
  • g.Key.CustomerId и g.Key.ProductId — обращение к отдельным полям ключа.
  • g.Sum(o => o.Quantity) — суммирует Quantity для всех элементов в группе.
  • g.Sum(o => o.Price) — суммирует Price для всех элементов в группе.

Полный пример с тестовыми данными

using System;
using System.Collections.Generic;
using System.Linq;

public class Order
{
    public int CustomerId { get; set; }
    public int ProductId { get; set; }
    public int Quantity { get; set; }
    public decimal Price { get; set; }
}

public class Program
{
    public static void Main()
    {
        var orders = new List<Order>
        {
            new Order { CustomerId = 1, ProductId = 101, Quantity = 2, Price = 10.5m },
            new Order { CustomerId = 1, ProductId = 101, Quantity = 3, Price = 10.5m },
            new Order { CustomerId = 1, ProductId = 102, Quantity = 1, Price = 25.0m },
            new Order { CustomerId = 2, ProductId = 101, Quantity = 5, Price = 10.5m },
            new Order { CustomerId = 2, ProductId = 103, Quantity = 1, Price = 50.0m }
        };

        var result = orders
            .GroupBy(o => new { o.CustomerId, o.ProductId })
            .Select(g => new
            {
                CustomerId = g.Key.CustomerId,
                ProductId = g.Key.ProductId,
                TotalQuantity = g.Sum(o => o.Quantity),
                TotalPrice = g.Sum(o => o.Price)
            })
            .ToList();

        foreach (var item in result)
        {
            Console.WriteLine($"Customer {item.CustomerId}, Product {item.ProductId}: " +
                              $"Qty={item.TotalQuantity}, Total={item.TotalPrice:C}");
        }
    }
}

Результат:

Customer 1, Product 101: Qty=5, Total=$52.50
Customer 1, Product 102: Qty=1, Total=$25.00
Customer 2, Product 101: Qty=5, Total=$52.50
Customer 2, Product 103: Qty=1, Total=$50.00

Важные замечания

  • Анонимный тип автоматически реализует Equals и GetHashCode, поэтому сравнение ключей работает корректно.
  • Порядок полей в анонимном типе важен: new { o.CustomerId, o.ProductId } и new { o.ProductId, o.CustomerId } — это разные ключи.
  • Если нужно сохранить результат для дальнейшего использования, можно создать именованный класс вместо анонимного типа.
<<>>

Альтернативный подход: использование кортежей (C# 7.0+)

Начиная с C# 7.0, можно использовать кортежи для группировки. Это даёт более читаемый синтаксис и упрощает обращение к ключам без анонимных типов:

var result = orders
    .GroupBy(o => (o.CustomerId, o.ProductId))
    .Select(g => new
    {
        CustomerId = g.Key.CustomerId,
        ProductId = g.Key.ProductId,
        TotalQuantity = g.Sum(o => o.Quantity),
        TotalPrice = g.Sum(o => o.Price)
    })
    .ToList();

Преимущества кортежей:

  • Более лаконичный синтаксис: (o.CustomerId, o.ProductId) вместо new { o.CustomerId, o.ProductId }.
  • Элементы кортежа имеют имена по умолчанию (Item1, Item2), но вы можете задать свои имена: (int CustId, int ProdId).
  • Кортежи — значимые типы, что может дать небольшой прирост производительности.

Недостаток: Если вам нужно передать результат в другой метод, анонимные типы проще преобразовать в именованные классы, чем кортежи (хотя кортежи тоже можно использовать как возвращаемые типы).

Оба подхода (анонимные типы и кортежи) корректно решают задачу. Выбор зависит от версии C# и личных предпочтений.