Search code examples
linqlistlinq-to-objectsobservablecollectiongeneric-collections

Linq Concating a list of tags with count from an inner collection of an outer collection


I'm trying to write a single Linq Query that can concat inner collections of tags from an outer collection of parent products and return a collection of Tags with a consolidated count ordered decending by count.

Here's a working solution that I currently have and am looking for help to convert to a single Linq Query.

Module Module1

Class Tag
    Property Name As String
    Property Count As Integer
End Class

Class Product
    Property Name As String
    Property tags As List(Of Tag)
End Class

Sub Main()

    Dim Products As New List(Of Product) From {
            {New Product With {.Name = "P1",
                               .tags = New List(Of Tag) From {{New Tag With {.Name = "T1"}},
                                                              {New Tag With {.Name = "T3"}},
                                                              {New Tag With {.Name = "T5"}}
                                                             }
                              }
            },
            {New Product With {.Name = "P2",
                               .tags = New List(Of Tag) From {{New Tag With {.Name = "T2"}},
                                                              {New Tag With {.Name = "T4"}},
                                                              {New Tag With {.Name = "T6"}}
                                                             }
                              }
            },
            {New Product With {.Name = "P3",
                               .tags = New List(Of Tag) From {{New Tag With {.Name = "T2"}},
                                                              {New Tag With {.Name = "T3"}},
                                                              {New Tag With {.Name = "T4"}}
                                                             }
                              }
            },
            {New Product With {.Name = "P4",
                               .tags = New List(Of Tag) From {{New Tag With {.Name = "T4"}},
                                                              {New Tag With {.Name = "T5"}},
                                                              {New Tag With {.Name = "T6"}},
                                                              {New Tag With {.Name = "T7"}},
                                                              {New Tag With {.Name = "T8"}}
                                                             }
                              }
            }
        }


    Dim ReportingTags As New List(Of Tag)

    '-- Start : Needs to be converted to pure Linq (if possible)------------------------------------------

    Dim InterimTags As New List(Of String)

    For Each p As Product In Products
        Dim TmpTags As New List(Of String)
        InterimTags.AddRange(TmpTags.Concat(From t In p.tags Select t.Name))
    Next

    ReportingTags.AddRange((From t In InterimTags
                            Group By name = t Into Count = Count()
                            Select New Tag With {.Name = name,
                                                 .Count = Count}).OrderByDescending(Function(t) t.Count))

    '-- End ----------------------------------------------------------------------------------------------

    For Each t As Tag In ReportingTags
        Console.WriteLine("Tag: {0} - Count: {1}", t.Name, t.Count)
    Next

    Console.ReadLine()

End Sub

End Module

I'll be taking the output and converting it to a observablecollection(of TagModel) - so I'm trying to eliminate double/triple handling of the data.

Thanks

Graeme


Solution

  • I'm a C# guy, but Linq is Linq, so I figured I'd take a stab. I was able to match your results by putting the following in your red section:

    Dim outStuff = From t In Products.SelectMany(Function(p) p.tags)
                   Group By tagName = t.Name
                   Into g = Group, Count()
                   Order By Count Descending
    

    And then I used

    For Each t In outStuff
        Console.WriteLine("Tag: {0} - Count: {1}", t.tagName, t.Count)
    Next
    

    to do the output.

    I'm SURE this isn't the most graceful or elegant VB, and you can probably refine the Linq to be a little prettier, but it should be a good start for you.

    The sample code in your question helped a lot to get this done.

    For a quick explanation, if you're not familiar with SelectMany or Group By, they do a couple of different and extremely useful things.

    SelectMany works like Select, except that when applied to a collection property, the contents of all of the collection properties are flattened out and returned as a single collection of the appropriate type, instead of being returned as a collection of collections. In this case, it gets all the tags from all the products, and puts them into one collection to work with.

    Group By does sort of the opposite, and takes a big flat group, and turns it into a collection of collections, grouped according the criteria designated (t.Name in this case).