Perform grouped joins

The group join is useful for producing hierarchical data structures. It pairs each element from the first collection with a set of correlated elements from the second collection.

For example, a class or a relational database table named Student might contain two fields: Id and Name. A second class or relational database table named Course might contain two fields: StudentId and CourseTitle. A group join of these two data sources, based on matching Student.Id and Course.StudentId, would group each Student with a collection of Course objects (which might be empty).

Note

Each element of the first collection appears in the result set of a group join regardless of whether correlated elements are found in the second collection. In the case where no correlated elements are found, the sequence of correlated elements for that element is empty. The result selector therefore has access to every element of the first collection. This differs from the result selector in a non-group join, which cannot access elements from the first collection that have no match in the second collection.

Warning

Enumerable.GroupJoin has no direct equivalent in traditional relational database terms. However, this method does implement a superset of inner joins and left outer joins. Both of these operations can be written in terms of a grouped join. For more information, see Join Operations and Entity Framework Core, GroupJoin.

The first example in this article shows you how to perform a group join. The second example shows you how to use a group join to create XML elements.

Note

The examples in this topic use the Person and Pet data classes from Perform inner joins.

Example - Group join

The following example performs a group join of objects of type Person and Pet based on the Person matching the Pet.Owner property. Unlike a non-group join, which would produce a pair of elements for each match, the group join produces only one resulting object for each element of the first collection, which in this example is a Person object. The corresponding elements from the second collection, which in this example are Pet objects, are grouped into a collection. Finally, the result selector function creates an anonymous type for each match that consists of Person.FirstName and a collection of Pet objects.

Person magnus = new(FirstName: "Magnus", LastName: "Hedlund");
Person terry = new("Terry", "Adams");
Person charlotte = new("Charlotte", "Weiss");
Person arlene = new("Arlene", "Huff");

List<Person> people = new() { magnus, terry, charlotte, arlene };

List<Pet> pets = new()
{
    new(Name: "Barley", Owner: terry),
    new("Boots", terry),
    new("Whiskers", charlotte),
    new("Blue Moon", terry),
    new("Daisy", magnus),
};

// Create a list where each element is an anonymous type
// that contains the person's first name and a collection of
// pets that are owned by them.
var query =
    from person in people
    join pet in pets on person equals pet.Owner into gj
    select new
    {
        OwnerName = person.FirstName,
        Pets = gj
    };

foreach (var v in query)
{
    // Output the owner's name.
    Console.WriteLine($"{v.OwnerName}:");

    // Output each of the owner's pet's names.
    foreach (var pet in v.Pets)
    {
        Console.WriteLine($"  {pet.Name}");
    }
}

/* Output:
     Magnus:
       Daisy
     Terry:
       Barley
       Boots
       Blue Moon
     Charlotte:
       Whiskers
     Arlene:
 */

Example - Group join to create XML

Group joins are ideal for creating XML by using LINQ to XML. The following example is similar to the previous example except that instead of creating anonymous types, the result selector function creates XML elements that represent the joined objects.

// using System.Xml.Linq;

Person magnus = new(FirstName: "Magnus", LastName: "Hedlund");
Person terry = new("Terry", "Adams");
Person charlotte = new("Charlotte", "Weiss");
Person arlene = new("Arlene", "Huff");

List<Person> people = new() { magnus, terry, charlotte, arlene };

List<Pet> pets = new()
{
    new(Name: "Barley", Owner: terry),
    new("Boots", terry),
    new("Whiskers", charlotte),
    new("Blue Moon", terry),
    new("Daisy", magnus),
};

XElement ownersAndPets = new("PetOwners",
    from person in people
    join pet in pets on person equals pet.Owner into gj
    select new XElement("Person",
        new XAttribute("FirstName", person.FirstName),
        new XAttribute("LastName", person.LastName),
        from subpet in gj
        select new XElement("Pet", subpet.Name)
    )
);

Console.WriteLine(ownersAndPets);

/* Output:
     <PetOwners>
       <Person FirstName="Magnus" LastName="Hedlund">
         <Pet>Daisy</Pet>
       </Person>
       <Person FirstName="Terry" LastName="Adams">
         <Pet>Barley</Pet>
         <Pet>Boots</Pet>
         <Pet>Blue Moon</Pet>
       </Person>
       <Person FirstName="Charlotte" LastName="Weiss">
         <Pet>Whiskers</Pet>
       </Person>
       <Person FirstName="Arlene" LastName="Huff" />
     </PetOwners>
*/

See also