Search code examples
c#linqlinq-group

c# Linq grouped by with composite key of int, list<int>


I have a product object that has a certain location and allowable shipping methods for that product. What I'm trying to do is Group the products by location AND by the allowable ship methods.

For example the following example data would product two groupings one with IDLocation = 1 and ShipMethods of 1,2 with a count of 2 and the other would be IDLocation = 1 and ShipMethods of 1,2 with a count of 3.

public class CartProduct
    {
        public int IDLocation { get; set; }
        public List<int> ShipMethods { get; set; }

        public List<CartProduct> GetExampleData()
        {
            return new List<CartProduct>() { new CartProduct() { IDLocation = 1, ShipMethods = new List<int>(){ 1, 2 } },
                new CartProduct() { IDLocation = 1, ShipMethods = new List<int>(){ 1, 2 } },
                new CartProduct() { IDLocation = 1, ShipMethods = new List<int>(){ 3, 4 } },
                new CartProduct() { IDLocation = 1, ShipMethods = new List<int>(){ 3, 4 } },
                new CartProduct() { IDLocation = 1, ShipMethods = new List<int>(){ 3, 4 } }
            };
        }
    }

I would like to see a grouping of IDLocation first, then if the ship methods are the same group those together as well.

I've tried several version of group by and select many with no luck.

List<CartProduct> CPList = new CartProduct().GetExampleData();
var GroupItems  = CPList.GroupBy(x => x.IDLocation) // then by ShipMethods??

Solution

  • The comparer argument in GroupBy allows you to define equality for purposes of the object grouping. The comparer is a separate class that compares two like objects and returns true if they are equal. The class, which needs to implement IComparer<CartItem>, can be implemented like this:

    class CartGroupComparer : IEqualityComparer<CartProduct>
    {
        public bool Equals(CartProduct x, CartProduct y)
        {
            return x.IDLocation == y.IDLocation
                 && x.ShipMethods.OrderBy(x=>x)
                       .SequenceEqual(y.ShipMethods.OrderBy(x=>x));
        }
    
        public int GetHashCode(CartProduct obj)
        {
            return obj.IDLocation.GetHashCode() 
                    ^ obj.ShipMethods.Sum().GetHashCode();
        }
    }
    

    (Note: for simplicity, this assumes that ShipMethods will never be null.)

    The Equals method tests two items for equality; if equal, they will be added added to the same group. The GetHashCode method must return an equal value for equal items, and a simple implementation is above.

    You can use this comparer directly in your GroupBy clause:

    new CartProduct().GetExampleData()
             .GroupBy(a => a, new CartGroupComparer());