Join Operazioni in LINQ

Un join di due origini dati è un'associazione di oggetti in un'origine dati con oggetti che condividono un attributo comune in un'altra origine dati.

Importante

In questi esempi viene usata un'origine dati System.Collections.Generic.IEnumerable<T>. Le origini dati basate su System.Linq.IQueryProvider usano origini dati System.Linq.IQueryable<T> e alberi delle espressioni. La sintassi consentita per gli alberi delle espressioni presenta limitazioni. Inoltre, ogni origine dati IQueryProvider, ad esempio EF Core può imporre altre restrizioni. Consultare la documentazione relativa all'origine dati.

La creazione di un join è un'operazione importante nelle query che fanno riferimento a origini dati le cui relazioni reciproche non possono essere seguite direttamente. Nella programmazione orientata a oggetti il join potrebbe corrispondere a una correlazione non modellata tra oggetti, ad esempio la direzione inversa di una relazione unidirezionale. Un esempio di relazione unidirezionale è una classe Student con una proprietà di tipo Department che rappresenta la classe principale, ma con la classe Department che non dispone di una proprietà che è una raccolta di oggetti Student. Se si ha un elenco di oggetti Department e si vogliono trovare tutti gli studenti in ogni dipartimento, è possibile usare un'operazione join per individuarli.

I metodi di join disponibili nel framework LINQ sono Join e GroupJoin. Questi metodi eseguono equijoin, ovvero join che associano due origini dati in base all'uguaglianza delle rispettive chiavi. (Ai fini del confronto, Transact-SQL supporta operatori join diversi da equals, ad esempio l'operatore less than). In termini di database relazionale, Join implementa un inner join, un tipo di join in cui vengono restituiti solo gli oggetti che hanno una corrispondenza nell'altro set di dati. Il metodo GroupJoin non ha equivalenti diretti in termini di database relazionale, ma implementa un superset di inner join e left outer join. Un left outer join è un join che restituisce ogni elemento della prima origine dati (a sinistra), anche se non ha elementi correlati nell'altra origine dati.

L'illustrazione seguente mostra una visualizzazione concettuale dei due set e degli elementi dei set che sono inclusi in un inner join o in un left outer join..

Due cerchi sovrapposti che illustrano interno/esterno.

Metodi

Nome metodo Descrizione Sintassi di espressione della query C# Ulteriori informazioni
Join Unisce due sequenze in base a funzioni selector chiave ed estrae coppie di valori join … in … on … equals … Enumerable.Join

Queryable.Join
GroupJoin Unisce due sequenze in base a funzioni selector chiave e raggruppa le corrispondenze risultanti per ogni elemento. join … in … on … equals … into … Enumerable.GroupJoin

Queryable.GroupJoin

Nota

Gli esempi seguenti in questo articolo usano le origini dati comuni per questa area.
Ogni Student ha un livello di classe, un reparto primario e una serie di punteggi. Un Teacher ha anche una proprietà City che identifica il campus in cui l'insegnante tiene le lezioni. Un Department ha un nome e un riferimento a un Teacher che funge da responsabile del reparto.
È possibile trovare il set di dati di esempio nel repository di origine.

public enum GradeLevel
{
    FirstYear = 1,
    SecondYear,
    ThirdYear,
    FourthYear
};

public class Student
{
    public required string FirstName { get; init; }
    public required string LastName { get; init; }
    public required int ID { get; init; }

    public required GradeLevel Year { get; init; }
    public required List<int> Scores { get; init; }

    public required int DepartmentID { get; init; }
}

public class Teacher
{
    public required string First { get; init; }
    public required string Last { get; init; }
    public required int ID { get; init; }
    public required string City { get; init; }
}

public class Department
{
    public required string Name { get; init; }
    public int ID { get; init; }

    public required int TeacherID { get; init; }
}

Nell'esempio seguente viene usata la clausola join … in … on … equals … viene utilizzata per eseguire il join di due sequenze basate su un valore specifico:

var query = from student in students
            join department in departments on student.DepartmentID equals department.ID
            select new { Name = $"{student.FirstName} {student.LastName}", DepartmentName = department.Name };

foreach (var item in query)
{
    Console.WriteLine($"{item.Name} - {item.DepartmentName}");
}

La query precedente può essere espressa usando la sintassi del metodo, come illustrato nel codice seguente:

var query = students.Join(departments,
    student => student.DepartmentID, department => department.ID,
    (student, department) => new { Name = $"{student.FirstName} {student.LastName}", DepartmentName = department.Name });

foreach (var item in query)
{
    Console.WriteLine($"{item.Name} - {item.DepartmentName}");
}

Nell'esempio seguente viene utilizzata la clausola join … in … on … equals … into … per eseguire il join di due sequenze basate su un valore specifico e vengono raggruppate le corrispondenze risultanti per ogni elemento:

IEnumerable<IEnumerable<Student>> studentGroups = from department in departments
                    join student in students on department.ID equals student.DepartmentID into studentGroup
                    select studentGroup;

foreach (IEnumerable<Student> studentGroup in studentGroups)
{
    Console.WriteLine("Group");
    foreach (Student student in studentGroup)
    {
        Console.WriteLine($"  - {student.FirstName}, {student.LastName}");
    }
}

La query precedente può essere espressa usando la sintassi del metodo, come illustrato nell'esempio seguente:

// Join department and student based on DepartmentId and grouping result
IEnumerable<IEnumerable<Student>> studentGroups = departments.GroupJoin(students,
    department => department.ID, student => student.DepartmentID,
    (department, studentGroup) => studentGroup);

foreach (IEnumerable<Student> studentGroup in studentGroups)
{
    Console.WriteLine("Group");
    foreach (Student student in studentGroup)
    {
        Console.WriteLine($"  - {student.FirstName}, {student.LastName}");
    }
}

Eseguire inner join

In termini di database relazionale, un inner join produce un set di risultati in cui ogni elemento della prima raccolta viene visualizzato una volta per ogni elemento corrispondente nella seconda raccolta. Se un elemento nella prima raccolta non ha corrispondenti, non viene visualizzato nel set di risultati. Il metodo Join, chiamato dalla clausola join in C#, implementa un inner join. Gli esempi seguenti mostrano come eseguire quattro varianti di un inner join:

  • Un inner join semplice che correla gli elementi di due origini dati in base a una chiave semplice.
  • Un inner join che correla gli elementi di due origini dati in base a una chiave composta. Una chiave composta, che è una chiave costituita da più di un valore, consente di correlare gli elementi in base a più di una proprietà.
  • Un multiplo join in cui le operazioni di join successive vengono aggiunte l'una all'altra.
  • Un inner join che viene implementato usando un group join.

Chiave singola join

Nell'esempio seguente vengono ricercati oggetti Teacher con oggetti Deparment con TeacherId corrispondente a Teacher. La clausola select in C# definisce l'aspetto degli oggetti risultanti. Nell'esempio seguente, gli oggetti risultanti sono tipi anonimi costituiti dal nome del reparto e dal nome del docente che dirige il reparto.

var query = from department in departments
            join teacher in teachers on department.TeacherID equals teacher.ID
            select new
            {
                DepartmentName = department.Name,
                TeacherName = $"{teacher.First} {teacher.Last}"
            };

foreach (var departmentAndTeacher in query)
{
    Console.WriteLine($"{departmentAndTeacher.DepartmentName} is managed by {departmentAndTeacher.TeacherName}");
}

Si ottengono gli stessi risultati usando la sintassi del metodo Join:

var query = teachers
    .Join(departments, teacher => teacher.ID, department => department.TeacherID,
        (teacher, department) =>
        new { DepartmentName = department.Name, TeacherName = $"{teacher.First} {teacher.Last}" });

foreach (var departmentAndTeacher in query)
{
    Console.WriteLine($"{departmentAndTeacher.DepartmentName} is managed by {departmentAndTeacher.TeacherName}");
}

Gli insegnanti che non sono capi di reparto non vengono visualizzati nei risultati finali.

Chiave composta join

Anziché correlare gli elementi in base a una sola proprietà, è possibile usare una chiave composta per confrontare gli elementi in base a più proprietà. Specificare la funzione del selettore di chiave per ogni raccolta in modo da restituire un tipo anonimo che include le proprietà da confrontare. Se si applicano etichette alle proprietà, l'etichetta deve essere la stessa in ogni tipo anonimo della chiave. Le proprietà devono inoltre apparire nello stesso ordine.

L'esempio seguente usa un elenco di oggetti Teacher e un elenco di oggetti Student per determinare quali docenti sono anche studenti. Entrambi questi tipi hanno proprietà che rappresentano il nome e la famiglia di ogni persona. Le funzioni che creano le chiavi di join dagli elementi di ogni elenco restituiscono un tipo anonimo costituito dalle proprietà. L'operazione di join confronta le chiavi composte per verificarne l'uguaglianza e restituisce coppie di oggetti da ogni elenco in cui sia il nome che il cognome corrispondono.

// Join the two data sources based on a composite key consisting of first and last name,
// to determine which employees are also students.
IEnumerable<string> query =
    from teacher in teachers
    join student in students on new
    {
        FirstName = teacher.First,
        LastName = teacher.Last
    } equals new
    {
        student.FirstName,
        student.LastName
    }
    select teacher.First + " " + teacher.Last;

string result = "The following people are both teachers and students:\r\n";
foreach (string name in query)
{
    result += $"{name}\r\n";
}
Console.Write(result);

È possibile usare il metodo Join, come indicato nell'esempio seguente:

IEnumerable<string> query = teachers
    .Join(students,
        teacher => new { FirstName = teacher.First, LastName = teacher.Last },
        student => new { student.FirstName, student.LastName },
        (teacher, student) => $"{teacher.First} {teacher.Last}"
 );

Console.WriteLine("The following people are both teachers and students:");
foreach (string name in query)
{
    Console.WriteLine(name);
}

join multiplo

È possibile collegare tra loro qualsiasi numero di operazioni di join per eseguire un join multiplo. Ogni clausola join in C# consente di correlare un'origine dati specificata con i risultati del join precedente.

La prima clausola join corrisponde a studenti e reparti in base alla corrispondenza che un oggetto Student ha per DepartmentID con un oggetto Department per ID. Restituisce una sequenza di tipi anonimi che contengono l'oggetto Student e Department.

La seconda clausola join correla i tipi anonimi restituiti dalla prima operazione di join con gli oggetti Teacher in base all'ID dell'insegnante che corrisponde all'ID del responsabile del dipartimento. Restituisce una sequenza di tipi anonimi che contengono il nome dello studente, il nome del reparto e il nome del responsabile del reparto. Poiché questa operazione è un inner join, vengono restituiti solo gli oggetti della prima origine dati per cui esiste una corrispondenza nella seconda origine dati.

// The first join matches Department.ID and Student.DepartmentID from the list of students and
// departments, based on a common ID. The second join matches teachers who lead departments
// with the students studying in that department.
var query = from student in students
    join department in departments on student.DepartmentID equals department.ID
    join teacher in teachers on department.TeacherID equals teacher.ID
    select new {
        StudentName = $"{student.FirstName} {student.LastName}",
        DepartmentName = department.Name,
        TeacherName = $"{teacher.First} {teacher.Last}"
    };

foreach (var obj in query)
{
    Console.WriteLine($"""The student "{obj.StudentName}" studies in the department run by "{obj.TeacherName}".""");
}

L'equivalente che usa più metodi Join usa lo stesso approccio con il tipo anonimo:

var query = students
    .Join(departments, student => student.DepartmentID, department => department.ID,
        (student, department) => new { student, department })
    .Join(teachers, commonDepartment => commonDepartment.department.TeacherID, teacher => teacher.ID,
        (commonDepartment, teacher) => new
        {
            StudentName = $"{commonDepartment.student.FirstName} {commonDepartment.student.LastName}",
            DepartmentName = commonDepartment.department.Name,
            TeacherName = $"{teacher.First} {teacher.Last}"
        });

foreach (var obj in query)
{
    Console.WriteLine($"""The student "{obj.StudentName}" studies in the department run by "{obj.TeacherName}".""");
}

Inner join tramite join raggruppati

Nell'esempio seguente viene illustrato come implementare un inner join usando un group join. L'elenco di oggetti Department viene collegato con un group join all'elenco di oggetti Student in base all'oggetto Department.ID corrispondente alla proprietà Student.DepartmentID. Il group join crea una raccolta di gruppi intermedi, dove ogni gruppo è costituito da un oggetto Department e una sequenza di oggetti Student corrispondenti. La seconda clausola from combina (o rende flat) questa sequenza di sequenze in una sequenza più lunga. La clausola select specifica il tipo di elementi nella sequenza finale. Tale tipo è anonimo ed è costituito dal nome dello studente e dal nome del reparto corrispondente.

var query1 =
    from department in departments
    join student in students on department.ID equals student.DepartmentID into gj
    from subStudent in gj
    select new
    {
        DepartmentName = department.Name,
        StudentName = $"{subStudent.FirstName} {subStudent.LastName}"
    };
Console.WriteLine("Inner join using GroupJoin():");
foreach (var v in query1)
{
    Console.WriteLine($"{v.DepartmentName} - {v.StudentName}");
}

È possibile ottenere gli stessi risultati usando il metodo GroupJoin, come indicato di seguito:

var queryMethod1 = departments
    .GroupJoin(students, department => department.ID, student => student.DepartmentID,
        (department, gj) => new { department, gj })
    .SelectMany(departmentAndStudent => departmentAndStudent.gj,
        (departmentAndStudent, subStudent) => new
        {
            DepartmentName = departmentAndStudent.department.Name,
            StudentName = $"{subStudent.FirstName} {subStudent.LastName}"
        });

Console.WriteLine("Inner join using GroupJoin():");
foreach (var v in queryMethod1)
{
    Console.WriteLine($"{v.DepartmentName} - {v.StudentName}");
}

Il risultato è equivalente al set di risultati che si ottiene usando la clausola join senza la clausola into per eseguire un inner join. Il codice seguente illustra questa query equivalente:

var query2 = from department in departments
    join student in students on department.ID equals student.DepartmentID
    select new
    {
        DepartmentName = department.Name,
        StudentName = $"{student.FirstName} {student.LastName}"
    };

Console.WriteLine("The equivalent operation using Join():");
foreach (var v in query2)
{
    Console.WriteLine($"{v.DepartmentName} - {v.StudentName}");
}

Per evitare il concatenamento, è possibile usare il singolo metodo Join come illustrato di seguito:

var queryMethod2 = departments.Join(students, departments => departments.ID, student => student.DepartmentID,
    (department, student) => new
    {
        DepartmentName = department.Name,
        StudentName = $"{student.FirstName} {student.LastName}"
    });

Console.WriteLine("The equivalent operation using Join():");
foreach (var v in queryMethod2)
{
    Console.WriteLine($"{v.DepartmentName} - {v.StudentName}");
}

Eseguire join raggruppati

Il join di gruppo è utile per produrre strutture di dati gerarchiche. Abbina ogni elemento della prima raccolta con un set di elementi correlati della seconda raccolta.

Nota

Ogni elemento della prima raccolta viene visualizzato nel set di risultati di un join di gruppo indipendentemente dal fatto che gli elementi correlati vengano trovati nella seconda raccolta. Nel caso in cui non venga trovato alcun elemento correlato, la sequenza di elementi correlati per l'elemento è vuota. Il selettore del risultato ha pertanto accesso a ogni elemento della prima raccolta. È diverso dal selettore del risultato in un join non di gruppo, che non può accedere a elementi della prima raccolta che non hanno corrispondenza nella seconda raccolta.

Avviso

Enumerable.GroupJoin non ha un equivalente diretto nei termini tradizionali del database relazionale. Tuttavia, questo metodo implementa un superset di inner join e left outer join. Entrambe queste operazioni possono essere scritte in termini di join raggruppati. Per altre informazioni, vedere Entity Framework Core, GroupJoin.

Il primo esempio in questo articolo illustra come eseguire un join di gruppo. Il secondo esempio descrive come usare un join di gruppo per creare elementi XML.

join di gruppo

L'esempio seguente esegue un join di gruppo di oggetti di tipo Department e Student basato su Department.ID corrispondente alla proprietà Student.DepartmentID. Diversamente da un join non di gruppo che produce una coppia di elementi per ogni corrispondenza, il join di gruppo produce un solo oggetto risultante per ogni elemento della prima raccolta, che in questo esempio è un oggetto Department. Gli elementi corrispondenti della seconda raccolta, che in questo esempio sono oggetti Student vengono raggruppati in una raccolta. La funzione del selettore del risultato crea infine un tipo anonimo per ogni corrispondenza costituita da Department.Name e una raccolta di oggetti Student.

var query = from department in departments
    join student in students on department.ID equals student.DepartmentID into studentGroup
    select new
    {
        DepartmentName = department.Name,
        Students = studentGroup
    };

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

    // Output each of the students in that department.
    foreach (Student? student in v.Students)
    {
        Console.WriteLine($"  {student.FirstName} {student.LastName}");
    }
}

Nell'esempio precedente, la variabile query contiene la query che crea un elenco in cui ogni elemento è un tipo anonimo che contiene il nome del reparto e una raccolta di studenti che studiano in tale reparto.

La query equivalente tramite la sintassi del metodo è illustrata nel codice seguente:

var query = departments.GroupJoin(students, department => department.ID, student => student.DepartmentID,
    (department, Students) => new { DepartmentName = department.Name, Students });

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

    // Output each of the students in that department.
    foreach (Student? student in v.Students)
    {
        Console.WriteLine($"  {student.FirstName} {student.LastName}");
    }
}

join di gruppo per creare XML

I join di gruppo sono ideali per la creazione di XML tramite LINQ to XML. L'esempio seguente è simile a quello precedente tranne per il fatto che, invece di creare tipi anonimi, la funzione del selettore del risultato crea elementi XML che rappresentano gli oggetti uniti in join.

XElement departmentsAndStudents = new("DepartmentEnrollment",
    from department in departments
    join student in students on department.ID equals student.DepartmentID into studentGroup
    select new XElement("Department",
        new XAttribute("Name", department.Name),
        from student in studentGroup
        select new XElement("Student",
            new XAttribute("FirstName", student.FirstName),
            new XAttribute("LastName", student.LastName)
        )
    )
);

Console.WriteLine(departmentsAndStudents);

La query equivalente tramite la sintassi del metodo è illustrata nel codice seguente:

XElement departmentsAndStudents = new("DepartmentEnrollment",
    departments.GroupJoin(students, department => department.ID, student => student.DepartmentID,
        (department, Students) => new XElement("Department",
            new XAttribute("Name", department.Name),
            from student in Students
            select new XElement("Student",
                new XAttribute("FirstName", student.FirstName),
                new XAttribute("LastName", student.LastName)
            )
        )
    )
);

Console.WriteLine(departmentsAndStudents);

Eseguire left outer join

Un left outer join è un join in cui viene restituito ogni elemento della prima raccolta, anche se non ha elementi correlati nella seconda raccolta. È possibile usare LINQ per eseguire un left outer join chiamando il metodo DefaultIfEmpty nei risultati di un join di gruppo.

L'esempio seguente illustra come usare il metodo DefaultIfEmpty nei risultati di un join di gruppo per eseguire un left outer join.

Il primo passaggio nella produzione di un left outer join di due raccolte consiste nell'esecuzione di un inner join usando un join di gruppo. Per una descrizione di questo processo, vedere Eseguire inner join. In questo esempio, l'elenco di oggetti Department viene unito all'elenco di oggetti Student in base all'ID di un oggetto Department che corrisponde al DepartmentID dello studente.

Il secondo passaggio consiste nell'includere ogni elemento della prima raccolta di sinistra nel set di risultati anche se l'elemento non ha corrispondenze nella raccolta di destra. Questa operazione viene eseguita chiamando DefaultIfEmpty in ogni sequenza di elementi corrispondenti dal join di gruppo. In questo esempio DefaultIfEmpty viene chiamato in ogni sequenza di oggetti Student corrispondenti. Il metodo restituisce una raccolta che contiene un solo valore predefinito se la sequenza di oggetti Student corrispondenti è vuota per qualsiasi oggetto Department, assicurando in questo modo che ogni oggetto Department sia rappresentato nella raccolta di risultati.

Nota

Il valore predefinito per un tipo di riferimento è null. Di conseguenza, l'esempio cerca un riferimento Null prima di accedere a ogni elemento di ogni raccolta Student.

var query =
    from student in students
    join department in departments on student.DepartmentID equals department.ID into gj
    from subgroup in gj.DefaultIfEmpty()
    select new
    {
        student.FirstName,
        student.LastName,
        Department = subgroup?.Name ?? string.Empty
    };

foreach (var v in query)
{
    Console.WriteLine($"{v.FirstName:-15} {v.LastName:-15}: {v.Department}");
}

La query equivalente tramite la sintassi del metodo è illustrata nel codice seguente:

var query = students.GroupJoin(departments, student => student.DepartmentID, department => department.ID,
    (student, departmentList) => new { student, subgroup = departmentList.AsQueryable() })
    .SelectMany(joinedSet => joinedSet.subgroup.DefaultIfEmpty(), (student, department) => new
    {
        student.student.FirstName,
        student.student.LastName,
        Department = department.Name
    });

foreach (var v in query)
{
    Console.WriteLine($"{v.FirstName:-15} {v.LastName:-15}: {v.Department}");
}

Vedi anche