Teilen über


Join-Vorgänge in LINQ

Ein join zweier Datenquellen entspricht der Zuordnung von Objekten einer Datenquelle zu den Objekten einer anderen Datenquelle, die ein Attribut gemeinsam haben.

Wichtig

In diesen Beispielen wird eine System.Collections.Generic.IEnumerable<T>-Datenquelle verwendet. Datenquellen, die auf System.Linq.IQueryProvider basieren, verwenden System.Linq.IQueryable<T>-Datenquellen und Ausdrucksbaumstrukturen. Ausdrucksbaumstrukturen haben Einschränkungen für die zulässige C#-Syntax. Darüber hinaus kann jede IQueryProvider-Datenquelle, z. B. EF Core, weitere Einschränkungen erzwingen. Konsultieren Sie die Dokumentation für Ihre Datenquelle.

Ein Join ist für Abfragen wichtig, die auf Datenquellen ausgerichtet sind, deren Beziehungen zueinander nicht direkt verfolgt werden können. Bei der objektorientierten Programmierung könnte ein Join eine nicht modellierte Korrelation zwischen Objekten bedeuten, z. B. die entgegengesetzte Richtung einer unidirektionalen Beziehung. Ein Beispiel einer unidirektionalen Beziehung ist die Student-Klasse mit einer Eigenschaft vom Typ Department, der die leitende Lehrkraft bezeichnet, während die Department-Klasse keine Eigenschaft besitzt, die einer Auflistung von Student-Objekten entspricht. Wenn Sie eine Liste von Department-Objekten besitzen und alle Lernenden in den einzelnen Fachbereich ermitteln möchten, könnten Sie dazu einen join-Vorgang verwenden.

Die im LINQ-Framework bereitgestellten join-Methoden sind Join und GroupJoin. Diese Methoden führen Gleichheitsverknüpfungen oder Verknüpfungen durch, bei denen zwei Datenquellen auf Basis der Gleichheit ihrer Schlüssel verglichen werden. (Zum Vergleich: Transact-SQL unterstützt auch andere join-Operatoren als equals, z. B. den Operator less than.) In relationalen Datenbank bedeutet dies, dass Join einen inneren join implementiert, d. h. eine Art join, bei dem nur die Objekte zurückgegeben werden, die über eine Entsprechung im anderen Datensatz verfügen. Für die GroupJoin-Methode hat bei relationalen Datenbanken kein direktes Äquivalent. Sie implementieren eine übergeordnete Menge innerer und linker äußerer Joins. Ein linker äußerer join ist ein join, der jedes Element der ersten (linken) Datenquelle zurückgibt, selbst wenn die andere Datenquelle keine zugehörigen Elemente enthält.

Die folgende Abbildung zeigt eine Konzeptansicht zweier Sätze sowie der Elemente innerhalb dieser Sätze, die entweder in einen inneren join oder einen linken äußeren join einbezogen sind.

Zwei überlappende Kreise innen/außen.

Methoden

Methodenname Beschreibung C#-Abfrageausdruckssyntax Weitere Informationen:
Join Verknüpft zwei Sequenzen auf Basis von Schlüsselauswahlfunktionen und extrahiert Wertepaare. join … in … on … equals … Enumerable.Join

Queryable.Join
GroupJoin Verknüpft zwei Sequenzen auf Basis von Schlüsselauswahlfunktionen und gruppiert die sich ergebenden Übereinstimmungen für die einzelnen Elemente. join … in … on … equals … into … Enumerable.GroupJoin

Queryable.GroupJoin

Hinweis

In den folgenden Beispielen in diesem Artikel werden die allgemeinen Datenquellen für diesen Bereich verwendet.
Allen Student sind eine Klassenstufe, ein primärer Fachbereich und mehrere Bewertungen zugeordnet. Teacher verfügen auch über eine City-Eigenschaft, die den Campus identifiziert, auf dem die Lehrkraft unterrichtet. Eine Department hat einen Namen und einen Verweis auf eine Teacher, die den Fachbereich leitet.
Sie finden das Beispieldatensatz im Quell-Repository.

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; }
}

Im folgenden Beispiel wird die join … in … on … equals …-Klausel für einen join zweier Sequenzen auf der Grundlage eines bestimmten Werts verwendet:

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}");
}

Obige Abfrage kann mit Methodensyntax ausgedrückt werden, wie im folgenden Code dargestellt:

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}");
}

Im folgenden Beispiel wird die join … in … on … equals … into …-Klausel für einen join zweier Sequenzen auf der Grundlage eines bestimmten Werts verwendet und um die resultierenden Übereinstimmungen für jedes Element zu gruppieren:

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}");
    }
}

Obige Abfrage kann mit Methodensyntax ausgedrückt werden, wie im folgenden Beispiel dargestellt:

// 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}");
    }
}

Ausführen von inneren Verknüpfungen

Bei relationalen Datenbanken erzeugt ein innerer join einen Ergebnissatz, in dem jedes Element der ersten Aufzählung einmal für jedes übereinstimmende Element in der zweiten Auflistung erscheint. Wenn ein Element in der ersten Auflistung keine übereinstimmenden Elemente besitzt, ist es nicht im Resultset enthalten. Die Join-Methode, die durch die join-Klausel in C# aufgerufen wird, implementiert einen inneren join. Die folgenden Beispiele veranschaulichen das Ausführen von vier Varianten eines inneren join:

  • Ein einfacher innerer join, der Elemente aus zwei Datenquellen anhand eines einfachen Schlüssels verknüpft.
  • Ein innerer join, der Elemente aus zwei Datenquellen anhand eines zusammengesetzten Schlüssels verknüpft. Mit einem zusammengesetzten Schlüssel, der aus mehr als einem Wert besteht, können Sie Elemente anhand mehr als einer Eigenschaft verknüpfen.
  • Ein mehrfacher join in dem aufeinanderfolgende join-Vorgänge aneinander gehängt werden.
  • Ein innerer join, der mithilfe eines Gruppen-join implementiert wird.

join mit einem Schlüssel

Das folgende Beispiel gleicht Teacher-Objekte mit Deparment-Objekten ab, deren TeacherId mit Teacher übereinstimmt. Die select-Klausel in C# definiert, wie die resultierenden Objekte aussehen. Im folgenden Beispiel sind die resultierenden Objekte anonyme Typen, die aus dem Fachbereichsnamen und dem Namen der Lehrkraft bestehen, die den Fachbereich leitet.

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}");
}

Sie erzielen die gleichen Ergebnisse mit der Join Methodensyntax:

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}");
}

Die Lehrkräfte, die keinen Fachbereich leiten, sind nicht in den Endergebnissen enthalten.

join mit zusammengesetztem Schlüssel

Anstatt Elemente anhand nur einer Eigenschaft zu verknüpfen, können Sie einen zusammengesetzten Schlüssel verwenden, um Elemente anhand mehreren Eigenschaften zu vergleichen. Geben Sie die Schlüsselauswahlfunktion für jede Auflistung an, um einen anonymen Typ zurückgegeben, der aus den zu vergleichenden Eigenschaften besteht. Wenn Sie die Eigenschaften beschriften, müssen sie über die gleiche Bezeichnung in jedem anonymen Typ des Schlüssels verfügen. Die Eigenschaften müssen auch in der gleichen Reihenfolge angezeigt werden.

Im folgenden Beispiel wird eine Liste von Teacher-Objekten und eine Liste von Student-Objekten verwendet, um zu bestimmen, welche Lehrkräfte auch Lernende sind. Beide Typen weisen Eigenschaften auf, die Vor- und Nachname jeder Person darstellen. Die Funktion, die die join-Schlüssel aus den Elementen jeder Liste erstellt, gibt einen anonymen Typ zurück, der aus den Eigenschaften besteht. Der join-Vorgang vergleicht diese zusammengesetzten Schlüssel auf Gleichheit und gibt Objektpaare aus jeder Liste zurück, bei denen Vor- und Nachname übereinstimmen.

// 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);

Verwenden Sie die Join Methode wie im folgenden Beispiel gezeigt:

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);
}

Mehrfacher join

Eine beliebige Anzahl von join-Vorgängen kann aneinander gehängt werden, um einen mehrfachen join auszuführen. Jede join-Klausel in C# verknüpft eine angegebene Datenquelle mit den Ergebnissen des vorherigen join.

Die erste join-Klausel gleicht Lernende und Fachbereiche ab, bei denen die DepartmentID des Student-Objekt mit der ID eines Department-Objekts übereinstimmt. Sie gibt eine Sequenz von anonymen Typen zurück, die das Student-Objekt und das Department-Objekt enthält.

Die zweite join-Klausel korreliert die anonymen Typen, die vom ersten join mit Teacher-Objekten zurückgegeben werden, basierend auf der ID von Lehrkräften, die der ID der Fachbereichsleitung entsprechen. Sie gibt eine Sequenz anonymer Typen zurück, die die Namen der Lernenden, des Fachbereichs und der Fachbereichsleitung enthalten. Da es sich bei diesem Vorgang um einen inneren join handelt, werden nur die Elemente aus der ersten Datenquelle zurückgegeben, die eine Übereinstimmung in der zweiten Datenquelle haben.

// 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}".""");
}

Das Äquivalent mit mehreren Join-Methoden verwendet den gleichen Ansatz mit dem anonymen Typ:

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}".""");
}

Innerer join mithilfe eines gruppierten join

Das folgende Beispiel zeigt, wie ein innerer join mithilfe eines Gruppen-join implementiert wird. Die Liste von Department-Objekten wird über einen Gruppen-Join mit der Liste von Student-Objekten verknüpft. Die Grundlage bildet ein Abgleich der Department.ID mit der Student.DepartmentID-Eigenschaft. Der Gruppen-join erstellt eine Auflistung von Zwischengruppen, bei der jede Gruppe aus einem Department-Objekt und einer Sequenz von übereinstimmenden Student-Objekten besteht. Die zweite from-Klausel kombiniert (oder vereinfacht) diese Sequenz von Sequenzen in einer längeren Sequenz. Die select-Klausel gibt den Typ der Elemente in der endgültigen Sequenz an. Dieser Typ ist ein anonymer Typ, der aus dem Namen der Lernenden und dem zugehörigen Fachbereichsnamen besteht.

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}");
}

Die gleichen Ergebnisse können mit der GroupJoin Methode wie folgt erzielt werden:

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}");
}

Das Ergebnis entspricht dem Ergebnissatz, der mit der join-Klausel zurückgegeben wird, ohne dass die into-Klausel einen inneren join ausführt. Der folgende Code veranschaulicht diese Abfrage:

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}");
}

Um Verkettungen zu vermeiden, kann die einzelne Join-Methode wie dargestellt verwendet werden:

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}");
}

Ausführen von Gruppenverknüpfungen

Der Gruppen-join ist für das Erstellen hierarchischer Datenstrukturen nützlich. Sie verbindet jedes Element aus der ersten Auflistung mit einem Satz von entsprechenden Elementen aus der zweiten Auflistung.

Hinweis

Jedes Element der ersten Auflistung erscheint im Ergebnissatz eines Gruppen-join, unabhängig davon, ob entsprechende Elemente in der zweiten Auflistung gefunden werden. Sollten keine entsprechenden Elemente gefunden werden, ist die Sequenz der entsprechenden Elemente für das Element leer. Die Ergebnisauswahl hat daher Zugriff auf jedes Element der ersten Auflistung. Dies unterscheidet sich von der Ergebnisauswahl in einem join, bei dem keine Gruppen verknüpft werden. Diese kann nicht auf Elemente aus der ersten Auflistung zugreifen, die keine Übereinstimmung in der zweiten Auflistung haben.

Warnung

Enumerable.GroupJoin hat keine direkte Entsprechung unter den Begriffen herkömmlicher relationaler Datenbanken. Diese Methode implementiert jedoch eine Obermenge innerer Joins und linker äußerer Joins. Beide Vorgänge können im Hinblick auf einen gruppierten join geschrieben werden. Weitere Informationen finden Sie unter Entity Framework Core: GroupJoin.

Im ersten Beispiel in diesem Artikel wird das Ausführen eines Gruppen-join gezeigt. Im zweiten Beispiel wird gezeigt, wie ein Gruppen-join zum Erstellen von XML-Elementen verwendet wird.

Gruppen-join

Das folgende Beispiel führt einen Gruppen-join von Objekten des Typs Department und Student aus, der auf der Department.ID basiert und mit der Student.DepartmentID-Eigenschaft übereinstimmt. Bei einem join, bei dem keine Gruppen verknüpft werden, wird für jede Übereinstimmung ein Elementpaar erzeugt. Im Gegensatz dazu erzeugt ein Gruppen-join nur ein resultierendes Objekt für jedes Element der ersten Auflistung, was in diesem Beispiel ein Department-Objekt ist. Die entsprechenden Elemente aus der zweiten Auflistung, die in diesem Beispiel Student-Objekte sind, werden in einer Auflistung gruppiert. Die Ergebnisauswahlfunktion erstellt schließlich einen anonymen Typ für jede Übereinstimmung, die aus Department.Name und einer Auflistung von Student-Objekten besteht.

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}");
    }
}

Im obigen Beispiel enthält die Variable query die Abfrage, mit der eine Liste erstellt wird, in der jedes Element ein anonymer Typ ist, der den Namen des Fachbereichs und eine Auflistung der Lernenden in diesem Fachbereich enthält.

Die entsprechende Abfrage mit Methodensyntax ist im folgenden Code dargestellt:

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}");
    }
}

Gruppen-join zum Erstellen von XML-Code

Gruppenverknüpfungen lassen sich ideal für das Erstellen von XML mithilfe von LINQ to XML nutzen. Das folgende Beispiel ähnelt dem vorherigen, nur dass die Ergebnisauswahlfunktion anstatt eines anonymen Typs XML-Elemente erstellt, die die verknüpften Objekte darstellen.

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);

Die entsprechende Abfrage mit Methodensyntax ist im folgenden Code dargestellt:

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);

Ausführen von Left Outer Joins

Ein linker äußerer join ist ein join, der jedes Element der ersten Auflistung zurückgibt, unabhängig davon, ob es entsprechende Elemente in der zweiten Auflistung gibt. Sie können LINQ verwenden, um einen linken äußeren join auszuführen, indem Sie die DefaultIfEmpty-Methode für die Ergebnisse eines Gruppen-join aufrufen.

Im folgenden Beispiel wird veranschaulicht, wie die DefaultIfEmpty-Methode für die Ergebnisse eines Gruppen-join verwendet wird, um einen linken äußeren join auszuführen.

Der erste Schritt zum Erstellen eines linken äußeren join von zwei Auflistungen besteht darin, einen inneren join mit einem Gruppen-join auszuführen. (Siehe Ausführen innerer Verknüpfungen für eine Erläuterung dieses Vorgangs.) In diesem Beispiel wird ein innerer Join der Liste der Department-Objekte mit der Liste der Student-Objekte basierend auf der ID eines Department-Objekt durchgeführt, das mit der DepartmentID der Lernenden übereinstimmt.

Der zweite Schritt besteht darin, jedes Element der ersten (linken) Liste in den Ergebnissatz einzuschließen, selbst wenn das Element in der rechten Auflistung keine Übereinstimmungen hat. Dies wird durch den Aufruf von DefaultIfEmpty für jede Sequenz von übereinstimmenden Elementen aus dem Gruppen-join erreicht. In diesem Beispiel wird DefaultIfEmpty für jede Sequenz von übereinstimmenden Student-Objekten aufgerufen. Diese Methode gibt eine Auflistung zurück, die einen einzelnen Standardwert enthält, wenn die Sequenz von übereinstimmenden Student-Objekten für jedes Department-Objekt leer ist, womit die Methode sicherstellt, dass jedes Department-Objekt in der Ergebnisauflistung dargestellt wird.

Hinweis

Der Standardwert für einen Verweistyp ist null; deshalb wird im Beispiel nach einem NULL-Verweis gesucht, bevor auf jedes Element jeder Student-Auflistung zugegriffen wird.

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}");
}

Die entsprechende Abfrage mit Methodensyntax ist im folgenden Code dargestellt:

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}");
}

Siehe auch