简体   繁体   中英

Grouping Deeply Nested Objects Using LINQ

I have the following classes:

public class Item
{
    public int Id { get; set; }
    public string Sku { get; set; }
    public List<Price> Prices { get; set; }
}

public class Price
{
    public int Id { get; set; }
    public double Cost { get; set; }
    public PriceList List { get; set; }
}

public class PriceList
{
    public int Id { get; set; }
    public string FileName { get; set; }
    public DateTime ImportDateTime { get; set; }
}

Essentially, it's for a list of items [ Item ] in which each item has a list of prices [ List<Price> Prices ] (one-to-many), and each price has one price list [ PriceList List ] (one-to-one).

What I need is a list of all price lists.

It seems like what I need is a group to a "grandchild", basically, grouping by the PriceList 's Id (which is under Price , which in turn is under Item in the list).

So, as an example, if I have five price lists, it should return five rows.

I achieved it doing the following long way:

List<PriceList> priceLists = new List<PriceList>();

foreach (Item item in items)
{
    foreach (Price price in item.Prices)
    {
        PriceList list = price.List;

        if (!priceLists.Any(x => x.Id == list.Id))
        {
            priceLists.Add(list);
        }
    }
}

How can it be achieved using LINQ?


UPDATE:

Here's a basic sample set:

PriceList priceList1 = new PriceList { Id = 1, FileName = "Price List 1.csv" };
PriceList priceList2 = new PriceList { Id = 2, FileName = "Price List 2.csv" };

Price price1 = new Price { Id = 1, Cost = 2.65, List = priceList1 };
Price price2 = new Price { Id = 2, Cost = 14.23, List = priceList2 };
Price price3 = new Price { Id = 3, Cost = 29.01, List = priceList1 };
Price price4 = new Price { Id = 4, Cost = 1, List = priceList2 };
Price price5 = new Price { Id = 5, Cost = 56.12, List = priceList1 };

Item item1 = new Item { Id = 1, Sku = "item1", Prices = new List<Price> { price1, price2 } };
Item item2 = new Item { Id = 2, Sku = "item2", Prices = new List<Price> { price3, price4 } };
Item item3 = new Item { Id = 3, Sku = "item3", Prices = new List<Price> { price5 } };

List<Item> itemsList = new List<Item> { item1, item2, item3 };

Let's start from the bottom upwards:

  1. I have a list of Item ( itemsList ), which contains three Item s.
  2. Each Item has a list of Price .
  3. Each Price has one PriceList .

To clarify the reasoning behind all this: The user imports a spreadsheet of prices (price list) they get periodically from their supplier, prices fluctuate and each price list has different prices for the same items. Hence the reason of having an item with many prices and each price has its price list which was used to upload it (there's more information included in the PriceList class which I omitted to keep it simple).

I hope I'm clear.

This seems to be what you want, using an extension to do Distinct by a lambda expression:

var ans = itemsList.SelectMany(item => item.Prices.Select(price => price.List)).DistinctBy(price => price.Id);

The extension is as follows:

public static IEnumerable<T> DistinctBy<T, TKey>(this IEnumerable<T> src, Func<T, TKey> keyFun) {
    var seenKeys = new HashSet<TKey>();
    foreach (T e in src)
        if (seenKeys.Add(keyFun(e)))
            yield return e;
}

If you changed your approach to use a HashSet<Int> to track the seen ids you would probably be a tiny bit more efficient than LINQ.

It looks like you can combine a SelectMany (to get all the Prices lists) with a Select (to get all the associated PriceList properties):

List<PriceList> priceLists = items.SelectMany(i => i.Prices).Select(p => p.List).ToList();

Edit

The above will return all the PriceLists , but since many Items could reference the same PriceList , then you need a way to filter on the PriceList.Id field.

One way to do that would be to override the Equals and GetHashCode property of the PriceList object. If you truly consider two PriceList objects to be equal if they have the same Id , then you could do the following:

public class PriceList
{
    public int Id { get; set; }
    public string FileName { get; set; }
    public DateTime ImportDateTime { get; set; }

    public override bool Equals(object obj)
    {
        var other = obj as PriceList;
        return Id == other?.Id;
    }

    public override int GetHashCode()
    {
        return Id;
    }
}

This allows you to use the Linq method, Distinct on your prices (it will call the Equals method to distinguish between PriceLists in the results:

List<PriceList> priceLists = items
    .SelectMany(i => i.Prices)
    .Select(i => i.List)
    .Distinct()
    .ToList();

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM