## 10.2 LINQ to Objects ### 10.2.1 Musterdaten Wir werden uns in diesem Abschnitt mit LINQ to Objects beschäftigen und die anderen Implementierungen nicht weiter behandeln. Später in diesem Buch, wenn wir uns mit dem Zugriff auf Datenbanken beschäftigen, werden wir uns noch mit LINQ to SQL beschäftigen. Ehe wir näher auf LINQ eingehen, müssen wir uns eine angemessene Datenquelle beschaffen. Die meisten Beispiele in diesem Kapitel arbeiten mit den Daten, die von einer Klassenbibliothek bereitgestellt werden. Sie finden das Projekt auf der Buch-DVD unter \Beispiele\Kapitel 10\Musterdaten. In der Anwendung sind die vier Klassen Customer, Product, Order und Service sowie die Enumeration Cities definiert. Alle sind sehr einfach gehalten.
    public class Order {
      public int OrderID;
      public int ProductID;
      public int Quantity;
      public bool Shipped;
    }
    public class Customer {
      public string Name;
      public int Alter;
      public Cities City;
      public Order[] Orders;
    }
    public class Product {
      public int ProductID;
      public string ProductName;
      public double Price;
    }
    public enum Cities {
      Aachen,
      Bonn,
      Köln
    }
In der Klasse Service werden drei Arrays definiert, die mehrere Produkte, Kunden und Bestellungen beschreiben. Beachten Sie bitte, dass die einzelnen Bestellungen den Kunden direkt in einem Feld zugeordnet werden. Zudem sind in Service drei Methoden implementiert, die als Datenlieferant entweder die Liste der Kunden, der Bestellungen oder der Produkte zurückliefern. Sämtliche Klassenmitglieder sind static.
    public class Service {
      public static Product[] GetProducts() {return product; }
      public static Customer[] GetCustomers() {return customers; }
      public static Order[] GetOrders() { return orders; }
      public static Product[] product = new Product[]{
        new Product{ ProductID = 1, ProductName = "Käse", Price = 10},
        new Product{ ProductID = 2, ProductName = "Wurst", Price = 5},
        new Product{ ProductID = 3, ProductName = "Obst", Price = 8.56},
        new Product{ ProductID = 4, ProductName = "Gemüse", Price = 4},
        new Product{ ProductID = 5, ProductName = "Fleisch", Price = 17.5},
        new Product{ ProductID = 6, ProductName = "Süßwaren", Price = 3},
        new Product{ ProductID = 7, ProductName = "Bier", Price = 2.8},
        new Product{ ProductID = 8, ProductName = "Pizza", Price = 7}
      };
      public static Order[] orders = new Order[] {
        new Order{ OrderID = 1, ProductID = 4,
                   Quantity = 2, Shipped = true},
        new Order{ OrderID = 2, ProductID = 1,
                   Quantity = 1, Shipped = true},
        new Order{ OrderID = 3, ProductID = 5,
                   Quantity = 4, Shipped = false},
        new Order{ OrderID = 4, ProductID = 4,
                   Quantity = 5, Shipped = true},
        new Order{ OrderID = 5, ProductID = 8,
                   Quantity = 6, Shipped = true},
        new Order{ OrderID = 6, ProductID = 3,
                   Quantity = 3, Shipped = false},
        new Order{ OrderID = 7, ProductID = 7,
                   Quantity = 2, Shipped = true},
        new Order{ OrderID = 8, ProductID = 8,
                   Quantity = 1, Shipped = false},
        new Order{ OrderID = 9, ProductID = 4,
                   Quantity = 1, Shipped = false},
        new Order{ OrderID = 10, ProductID = 1,
                   Quantity = 8, Shipped = true},
        new Order{ OrderID = 11, ProductID = 3,
                   Quantity = 3, Shipped = true},
        new Order{ OrderID = 12, ProductID = 6,
                   Quantity = 6, Shipped = true},
        new Order{ OrderID = 13, ProductID = 1,
                   Quantity = 4, Shipped = false},
        new Order{ OrderID = 14, ProductID = 6,
                   Quantity = 3, Shipped = true},
        new Order{ OrderID = 15, ProductID = 5,
                   Quantity = 7, Shipped = true},
        new Order{ OrderID = 16, ProductID = 1,
                   Quantity = 9, Shipped = true}
      };
    public static Customer[] customers = new Customer[]{
       new Customer{ Name = "Herbert", Alter = 34,
                     City = Cities.Aachen,
                     Orders = new Order[]{orders[3], orders[2],
                                 orders[8], orders[10] } },
       new Customer{ Name = "Willi", Alter = 55,
                     City = Cities.Köln,
                     Orders = new Order[]{orders[6], orders[7],
                                       orders[9] } },
       new Customer{ Name = "Hans", Alter = 25,
                     City = Cities.Bonn,
                     Orders = new Order[]{orders[4], orders[11],
                                       orders[14] } },
       new Customer{ Name = "Freddy", Alter = 38,
                     City = Cities.Bonn,
                     Orders = new Order[]{orders[1], orders[5],
                                       orders[13] } },
       new Customer{ Name = "Theo", Alter = 45,
                     City = Cities.Aachen,
                     Orders = new Order[]{orders[15], orders[12] } }
      };
    }
Sollten Sie selbst in einem eigenen Projekt mit den Daten experimentieren, müssen Sie die Assembly Musterdaten.dll unter Verweise in das Projekt einbinden. Zudem sollten Sie auch den Namespace Musterdaten mit using importieren. ### 10.2.2 Allgemeine Syntax Beginnen wir mit einer einfachen Abfrage, die alle bekannten Kunden aus den Musterdaten der Reihe nach ausgibt. Dabei soll sich die Ausgabe auf die Kunden beschränken, deren Name weniger als sechs Buchstaben hat. In die Ergebnismenge soll der Name des Kunden sowie sein Wohnort aufgenommen werden. Sie können die entsprechende LINQ-Abfrage auf zweierlei Arten definieren. Die erste Variante ist die Abfrage-Syntax (Query Expression Syntax) mit:
    Customer[] customers = Service.GetCustomers();
    var cust = from customer in customers
               where customer.Name.Length < 6
               select new { customer.Name, customer.City };
Wie Sie sehen, kommen dabei die neuen Schlüsselwörter from, where und select von C# zum Einsatz. Die zweite Variante ist die Erweiterungsmethoden-Syntax (Extension Method Syntax). Mit dieser können Sie die Abfrage auch wie folgt formulieren:
    var cust = customers
               .Where(customer => customer.Name.Length < 6)
               .Select(c => new { c.Name, c.City });
Welche der beiden Varianten Sie bevorzugen, bleibt Ihnen überlassen. Die Abfrage-Syntax sieht vielleicht etwas einfacher aus, aber mit etwas Übung gewöhnen Sie sich auch schnell an die Erweiterungsmethoden-Syntax. Grundsätzlich beginnt eine LINQ-Abfrage mit from und nicht wie unter SQL mit select. Der Grund dafür ist, dass zuerst die Datenquelle ausgewählt sein muss, auf der alle nachfolgenden Operationen ausgeführt werden. Dies gestattet es uns darüber hinaus, mit der IntelliSense-Hilfe im Codeeditor zu arbeiten. Mit where wird das Filterkriterium beschrieben, und select legt fest, welche Daten tatsächlich in die Ergebnisliste eingetragen werden. Das Ergebnis wird einer implizit typisierten Variablen zugewiesen, die mit var beschrieben wird. Diese Anweisung könnte auch durch
    IEnumerable<string> cust = from customer in customers ...
ersetzt werden, da eine LINQ-Abfrage Collections vom Typ IEnumerable<T> und IEnumerable bildet. Um aus einem Customer-Objekt für die Ergebnisliste mehrere spezifische Daten zu filtern, übergeben Sie dem Select-Operator einen anonymen Typ, der sich aus den gewünschten Elementen zusammensetzt. In unserem Beispielcode handelt es sich um die Eigenschaften Name und City. Die Ausgabe der Ergebnismenge erfolgt in einer foreach-Schleife. Die Laufvariable wird vom Typ var deklariert.
    foreach (var item in cust)
      Console.WriteLine("Name: {0}, Ort: {1}", item.Name, item.City);
### 10.2.3 Übersicht über die Abfrageoperatoren LINQ stellt Ihnen zahlreiche Abfrageoperatoren zur Verfügung. Alle sind in der Klasse Enumerable im Namespace System.Linq definiert. Die Abfrageoperatoren sind als Erweiterungsmethoden implementiert, die für Typen gelten, die die Schnittstelle IEnumerable<T> implementieren. In Tabelle 10.1 sind alle LINQ-Abfrageoperatoren angegeben. Wir werden im weiteren Verlauf des Kapitels auf die meisten der hier aufgeführten LINQ-Operatoren genauer eingehen.

Tabelle 10.1 Die LINQ-Abfrageoperatoren

Operatortyp | Operator
Aggregatoperatoren | Aggregate, Average, Count, LongCount, Min, Max, Sum
Casting-Operatoren | Cast, OfType, ToArray, ToDictionary, ToList, ToLookup, ToSequence
Elementoperatoren | DefaultIfEmpty, ElementAt, ElementAtOrDefault, First, FirstOrDefault, Last, LastOrDefault, Single, SingleOrDefault
Gleichheitsoperatoren | EqualAll
Sequenzoperatoren | Empty, Range, Repeat
Gruppierungsoperatoren | GroupBy
Join-Operatoren | Join, GroupJoin
Sortieroperatoren | OrderBy, ThenBy, OrderByDescending, ThenByDescending, Reverse
Aufteilungsoperatoren | Skip, SkipWhile, Take, TakeWhile
Quantifizierungsoperatoren | All, Any, Contains
Restriktionsoperatoren | Where
Projektionsoperatoren | Select, SelectMany
Set-Operatoren | Concat, Distinct, Except, Intersect, Union
### 10.2.4 Die »from«-Klausel Ein Abfrageausdruck beginnt mit der from-Klausel. Diese beschreibt, welche Datenquelle abgefragt werden soll, und definiert eine lokale Bereichsvariable, die ein Element in der Datenquelle repräsentiert. Die Datenquelle muss entweder die Schnittstelle IEnumerable<T> oder IEnumerable implementieren. Zu den abfragbaren Datenquellen zählen auch diejenigen, die sich auf IQueryable<T> zurückführen lassen. Datenquelle und Bereichsvariable sind streng typisiert. Wenn Sie mit
    from customer in customers
das Array aller Kunden als Datenquelle angeben, ist die Bereichsvariable vom Typ Customer. Etwas anders ist der Sachverhalt, wenn die Datenquelle beispielsweise vom Typ ArrayList ist. Wie Sie wissen, können in einer ArrayList Objekte unterschiedlichsten Typs verwaltet werden. Um auch solche Datenquellen abfragen zu können, muss die Bereichsvariable explizit typisiert werden, z. B. so:
    ArrayList arr = new ArrayList();
    arr.Add(new Circle());
    arr.Add(new Circle());
    var cust = from Circle kreis in arr
               select kreis;
Manchmal kommt es vor, dass jedes Element einer Datenquelle seinerseits selbst eine Liste untergeordneter Elemente beschreibt. Ein gutes Beispiel dafür ist in unserer Anwendung zu finden, die die Musterdaten bereitstellt.
    public class Customer {
      public string Name;
      public int Alter;
      public Cities City;
      public Order[] Orders;
    }
Jedem Kunden ist ein Array vom Typ Order zugeordnet. Um die Bestellungen abzufragen, muss eine weitere from-Klausel angeführt werden, die auf die Bestellliste des jeweiligen Kunden zugreift. Jede from-Klausel kann separat mit where gefiltert oder mit orderby sortiert werden.
    Customer[] customers = Service.GetCustomers();
    var result = from customer in customers
                 where customer.Name == "Hans"
                 from order in customer.Orders
                 where order.Quantity > 6
                 select order.OrderID;
In diesem Codefragment wird die Liste aller Kunden zuerst nach Hans durchsucht. Die gefundene Dateninformation extrahiert anschließend die Bestellinformationen und beschränkt das Ergebnis auf alle Bestellungen von Hans, die eine Bestellmenge > 6 haben. Dieselbe Abfrage in Erweiterungsmethoden-Syntax sieht so aus:
    var result = customers
                 .Where(c => c.Name == "Hans")
                 .SelectMany(c => c.Orders)
                 .Where(order => order.Quantity > 6)
                 .Select(order => order.OrderID);
Enthält ein gefundenes Element eine Untermenge (hier werden die Bestellungen eines Customer-Objekts durch ein Array beschrieben), benötigen wir den Operator SelectMany. An diesem Beispiel können Sie sehr schön erkennen, dass sich in manchen Fällen Abfrage-Syntax und Erweiterungsmethoden-Syntax doch deutlich unterscheiden. ### 10.2.5 Der Restriktionsoperator »where« Angenommen, Sie möchten alle Kunden auflisten, deren Wohnort Aachen ist. Um eine Folge von Elementen zu filtern, verwenden Sie den Where-Operator.
    Customer[] customers = Service.GetCustomers();
    var result = from cust in customers
                 where cust.City == Cities.Aachen
                 select cust.Name;
    foreach (var item in result)
      Console.WriteLine(item);
Mit dem Select-Operator geben Sie das Element an, das in die Ergebnisliste aufgenommen werden soll. In diesem Fall ist das der Name jeder entsprechend durch den Where-Operator gefundenen Person. Die Ergebnisliste wird in der foreach-Schleife durchlaufen und an der Konsole ausgegeben. Sie werden Herbert und Theo in der Ergebnisliste finden. Sie können die Abfrage-Syntax auch durch die Erweiterungsmethoden-Syntax ersetzen. Geben Sie dabei direkt das Array an, das durchlaufen werden soll. An der Codierung der Konsolenausgabe ändert sich nichts.
    var result = customers
                 .Where( cust => cust.City == Cities.Aachen)
                 .Select(cust => cust.Name);
Mehrere Filterkriterien zu berücksichtigen ist nicht weiter schwierig. Sie müssen nur den where-Operator ergänzen und benutzen zur Formulierung des Filters die C#-spezifischen Operatoren. Im nächsten Codefragment werden alle noch nicht ausgelieferten Bestellungen gesucht, deren Bestellmenge größer 3 ist.
    Order[] orders = Service.GetOrders();
    var result = from order in orders
                 where order.Quantity > 3 && order.Shipped == false
                 select order.OrderID;
oder:
    var result = orders
                 .Where(order => order.Quantity > 3 &&
                        order.Shipped == false)
                 .Select(ord => ord.OrderID);
#### Die Überladungen des Where-Operators Wenn Sie sich die .NET-Dokumentation des Where-Operators ansehen, finden Sie die beiden folgenden Signaturen:
    public static IEnumerable<T> Where<T>(
           this IEnumerable<T> source,
           Func<T, bool> predicate
    public static IEnumerable<T> Where<T>(
           this IEnumerable<T> source,
           Func<T, int, bool> predicate
Die erste wird für Abfragen verwendet, wie wir sie weiter oben eingesetzt haben. Die IEnumerable<T>-Collection wird dabei komplett gemäß den Filterkriterien durchsucht. Mit der zweiten Signatur können Sie den Bereich der Ergebnisliste einschränken, und zwar anhand des nullbasierten Index, der als Integer angegeben wird. Nehmen wir an, Sie interessieren sich für alle Bestellungen, deren Bestellmenge > 3 ist. Allerdings möchten Sie, dass die Ergebnisliste sich auf Indizes in der Datenquelle beschränkt, die < 10 sind. Es werden demnach nur die Indizes 0 bis einschließlich 9 in der Datenquelle orders berücksichtigt.
    var result = orders
                 .Where((order, index) => order.Quantity > 3 &&
                                          index < 10)
                 .Select(ord => ord.OrderID);
Hier müssen Sie die Schreibweise der Erweiterungsmethoden-Syntax einsetzen, um der überladenen Erweiterungsmethode Where die erforderlichen Argumente übergeben zu können. Das Ergebnis wird mit den Bestellungen gebildet, die die IDs 3, 4, 5 und 10 haben. #### Wie funktioniert der »Where«-Operator? Betrachten wir noch einmal die folgende Anweisung:
    var result = customers
                 .Where( cust => cust.City == Cities.Aachen)
Where ist eine Erweiterungsmethode der Schnittstelle IEnumerable<T> und gilt auch für das Array vom Typ Customer. Der Ausdruck
    cust => cust.City == Cities.Aachen
ist ein Lambda-Ausdruck, im eigentlichen Sinne also das Delegate auf eine anonyme Methode. In der Definition des Where-Operators wird dieses Delegate durch das Delegate
    Func<T, bool> predicate
beschrieben (siehe die Definition von Where oben). Der generische Typparameter T wird durch den Datentyp der Elemente in der zugrunde liegenden Collection beschrieben, die bekanntlich die Schnittstelle IEnumenerable<T> implementiert. In unserem Beispiel handelt es sich um Customer-Objekte. Daher können wir bei korrekter Codierung innerhalb des Lambda-Ausdrucks auch auf die IntelliSense-Liste zurückgreifen. Der zweite Parameter teilt uns mit, von welchem Datentyp der Rückgabewert des Lambda-Ausdrucks ist. Hier wird ein boolescher Typ vorgegeben, denn über true weiß LINQ, dass auf das untersuchte Element das Suchkriterium zutrifft; bei einer Rückgabe von false trifft es nicht zu. Das Zusammenspiel zwischen den neuen Lambda-Ausdrücken und Erweiterungsmethoden im Kontext generischer Typen und Delegates ist hier sehr gut zu erkennen. In ähnlicher Weise funktionieren auch viele andere Operatoren. Ich werde daher im Folgenden nicht jedes Mal erneut das komplexe Zusammenspiel der verschiedenen Sprachkomponenten erörtern. ### 10.2.6 Projektionsoperatoren #### Der »Select«-Operator Der Select-Operator macht die Ergebnisse der Abfrage über ein Objekt verfügbar, das die Schnittstelle IEnumerable<T> implementiert, zum Beispiel:
    var result = from order in orders
                 select order.OrderID;
oder alternativ:
    var result = orders.Select(order => order.OrderID);
Die Rückgabe ist in beiden Fällen eine Liste mit den Bestellnummern der in der Collection vertretenen Bestellungen. Liefert der Select-Operator eine Liste mit neu strukturierten Datenzeilen, müssen Sie einen anonymen Typ als Ergebnismenge definieren:
    var result = from customer in customers
                 select new { customer.Name, customer.City };
#### Der Operator »SelectMany« SelectMany kommt dann sinnvoll zum Einsatz, wenn es sich bei den einzelnen Elementen in einer Elementliste um Arrays handelt, deren Einzelelemente von Interesse sind. In der Anwendung Musterdaten trifft das auf alle Objekte vom Typ Customer zu, weil die Bestellungen in einem Array verwaltet werden.
    var result = from cust in customers
                 where cust.Name == "Hans"
                    from order in cust.Orders
                    where order.Quantity > 6
                    select order.OrderID;
In Abschnitt 10.2.4, »Die ›from‹-Klausel«, hatten wir bereits dieses Beispiel, sodass ich an dieser Stelle auf weitere Ausführungen verzichte. ### 10.2.7 Sortieroperatoren Sortieroperatoren ermöglichen eine Sortierung von Elementen in Ausgabefolgen mit einer angegebenen Sortierrichtung. Mit dem Operator OrderBy können Sie auf- und absteigend sortieren, mit OrderByDescending nur absteigend. Nachfolgend sehen Sie ein Beispiel für eine aufsteigende Sortierung. Dabei werden die Bestellmengen aller Bestellungen der Reihe nach in die Ergebnisliste geschrieben.
    Order[] orders = Service.GetOrders();
    var result = from order in orders
                 orderby order.Quantity
                 select new { order.OrderID, order.Quantity };
    foreach (var item in result)
      Console.WriteLine("ID: {0,-3}{1}", item.OrderID, item.Quantity);
Sehen wir uns diese LINQ-Abfrage noch in der Erweiterungsmethoden-Syntax an:
    var result = orders
                 .OrderBy(order => order.Quantity)
                 .Select(order => new { order.OrderID, order.Quantity });
Durch die Ergänzung von descending lässt sich auch eine absteigende Sortierung erzwingen:
    ...
    orderby order.Quantity descending
    ...
Das folgende Codefragment zeigt, wie Sie mit dem Operator OrderByDescending zum gleichen Ergebnis kommen:
    var result = orders
                 .OrderByDescending(order => order.Quantity)
                 .Select(order => new { order.OrderID, order.Quantity });
Wenn Sie mehrere Sortierkriterien festlegen wollen, helfen Ihnen die beiden Operatoren ThenBy beziehungsweise ThenByDescending weiter. Deren Einsatz setzt aber die vorhergehende Verwendung von OrderBy oder OrderByDescending voraus. Nehmen wir an, die erste Sortierung soll die Bestellmenge berücksichtigen und die zweite, ob die Bestellung bereits ausgeliefert ist. Die Anweisung dazu lautet:
    Order[] orders = Service.GetOrders();
    var result = orders
                 .OrderBy(order => order.Quantity)
                 .ThenBy(order => order.Shipped)
                 .Select(order =>  new {order.OrderID, order.Quantity,
                                        order.Shipped });
    foreach (var item in result)
      Console.WriteLine("ProductID: {0,-3}Menge:{1,-4} Geliefert:{2}",
                 item.OrderID, item.Quantity, item.Shipped);
Manchmal kann es vorkommen, dass Sie die gesamte Ergebnisliste in umgekehrter Reihenfolge benötigen. Hier kommt der Operator Reverse zum Einsatz, der am Ende auf die Ergebnisliste angewendet wird:
    var result = orders
                 .Select(order => new {order.ProductID, order.Quantity })
                 .Reverse();
Wie Sie wissen, werden einige Abfrageoperatoren als Schlüsselwörter von C# angeboten und gestatten die sogenannte Abfrage-Syntax. Reverse und ThenBy zählen nicht dazu. Möchten Sie die von einer Abfrage-Syntax gelieferte Ergebnismenge umkehren, können Sie sich eines kleinen Tricks bedienen. Sie schließen die Abfrage-Syntax in runde Klammern ein und können darauf den Punktoperator mit folgendem Reverse angeben:
    var result = (from order in orders
                  select new {order.ProductID, order.Quantity })
                 .Reverse();
### 10.2.8 Gruppieren mit »GroupBy« Manchmal ist es notwendig, Ergebnisse anhand spezifischer Kriterien zu gruppieren. Dazu dient der Operator GroupBy. Machen wir uns das zuerst an einem Beispiel deutlich. Ausgangspunkt sei wieder das Array mit Customer-Objekten. Jetzt sollen die Kunden (Customer-Objekte) nach deren Wohnsitz (Cities) gruppiert werden.
    Customer[] customers = Service.GetCustomers();
    var result = customers
                 .GroupBy(cust => cust.City);
    foreach (IGrouping<Cities, Customer> temp in result) {
      Console.WriteLine(new string('=', 40));
      Console.WriteLine("Stadt: {0}", temp.Key);
      Console.WriteLine(new string('-', 40));
      foreach (var item in temp)
        Console.WriteLine("       {0}", item.Name);
    }
Die Ausgabe an der Konsole sehen Sie in Abbildung 10.1. Hier klicken, um das Bild zu vergrößern Abbildung 10.1 Die Ausgabe des Beispiels »GroupBy« Der Operator GroupBy ist vielfach überladen. Sehen wir uns eine der Überladungen an:
    public static IEnumerable<IGrouping<K,T>> GroupBy<T,K>(
       this IEnumerable<T> source, Func<T,K> keyselector);
Alle Überladungen geben dabei den Typ IEnumerable<IGrouping<K,T>> zurück. Die Schnittstelle IGrouping<K,T> ist eine spezialisierte Form von IEnumerable<T>. Sie definiert die schreibgeschützte Eigenschaft key, die den Wert der zu bildenden Gruppe abruft.
    public interface IGrouping<K,T> : IEnumerable<T> {
       K key { get; }
    }
Im Beispiel oben werden mittels key die Städte aus dem generischen Typ K (also Cities) abgefragt. Betrachten wir nun die äußere Schleife:
    foreach (IGrouping<Cities, Customer> temp in result)
Sie müssen der Schnittstelle IGrouping im ersten Typparameter in unserem Beispiel Cities zuweisen, den Datentyp des Elements, nach dem gruppiert werden soll. Der zweite Typparameter beschreibt den Typ des zu gruppierenden Elements. Die äußere Schleife beschreibt die einzelnen Gruppen und gibt als Resultat alle Elemente zurück, die zu der entsprechenden Gruppe gehören. In unserem Beispielcode wird diese Untergruppe mit der Variablen item beschrieben. In der inneren Schleife werden anschließend alle Elemente von temp erfasst und die gewünschten Informationen ausgegeben. Der GroupBy-Operator kann auch in der Schreibweise der Abfragesyntax dargestellt werden:
    var result = from customer in customers
                 group customer by customer.City
### 10.2.9 Verknüpfungen mit »Join« Mit dem Join-Operator definieren Sie Beziehungen zwischen mehreren Auflistungen, ähnlich wie Sie in SQL mit dem gleichnamigen JOIN-Statement Tabellen miteinander in Beziehung setzen. In unseren Musterdaten liegen insgesamt 16 Bestellungen vor. Es soll nun für jede Bestellung die Bestellnummer des bestellten Artikels, die Bestellmenge und der Einzelpreis des Artikels ausgegeben werden. Die Listen der Produkte und Bestellungen spielen in diesem Fall eine entscheidende Rolle.
    Order[] orders = Service.GetOrders();
    Product[] products = Service.GetProducts();
    var liste = orders
                .Join(products,
                      ord => ord.ProductID,
                      prod => prod.ProductID,
                      (a, b) => new {a.OrderID,
                                        a.ProductID,
                                        b.Price,
                                        a.Quantity});
    foreach(var m in liste)
      Console.WriteLine("Order: {0,-3} Product: {1} Menge: {2} Preis: {3}",
       m.OrderID, m.ProductID, m.Quantity, m.Price);
Der Join-Operator ist überladen. In diesem Beispiel haben wir den folgenden benutzt:
    public static IEnumerable<V> Join<T, U, V, K>(
         this Enumerable<T> outer,
         IEnumerable<U> inner,
         Func<T, K> outerKeySelector,
         Func<U, K> innerKeySelector,
         Func<T, U, V> resultSelector);
Join wird als Erweiterungsmethode der Liste definiert, auf die Join aufgerufen wird. In unserem Beispiel ist es die durch orders beschriebene Liste aller Bestellungen. Die innere Liste wird durch das erste Argument beschrieben und ist in unserem Beispielcode die Liste aller Produkte products. Als zweites Argument erwartet Join im Parameter outerKeySelector das Schlüsselfeld der äußeren Liste (hier: orders), das mit dem im dritten Argument angegebenen Schlüsselfeld der inneren Liste in Beziehung gesetzt wird. Im vierten Argument wird die Ergebnisliste festgelegt. Dazu werden zwei Parameter übergeben: Der erste projiziert ein Element der äußeren Liste, der zweite ein Element der inneren Liste in das Ergebnis der Join-Abfrage. Beachten Sie, dass in der Definition von Join der generische Typ T die äußere Liste beschreibt und der Typ U die innere. Die Schlüssel (in unserem Beispiel werden dazu die Felder genommen), die die ProductID beschreiben, verstecken sich hinter dem generischen Typ K, die Ergebnisliste hinter V. Sie können eine Join-Abfrage auch in Abfragesyntax notieren:
    var liste = from ord in orders
                join prod in products
                     on ord.ProductID equals prod.ProductID
                select new { ord.OrderID, ord.ProductID,
                             prod.Price, ord.Quantity };
Die Ergebnisliste sehen Sie in Abbildung 10.2. Sie sollten darauf achten, dass Sie beim Vergleich links von equals den Schlüssel der äußeren Liste angeben, rechts davon den Schlüssel der inneren Liste. Wenn Sie beide vertauschen, erhalten Sie einen Compilerfehler. Hier klicken, um das Bild zu vergrößern Abbildung 10.2 Resultat der »Join«-Abfrage #### Der Operator »GroupJoin« Join führt Daten aus der linken und rechten Liste genau dann zusammen, wenn die angegebenen Kriterien alle erfüllt sind. Ist eines oder sind mehrere der Kriterien nicht erfüllt, befindet sich kein Datensatz in der Ergebnismenge. Damit ist der Join-Operator mit dem INNER JOIN-Statement einer SQL-Abfrage vergleichbar. Suchen Sie ein Äquivalent zu einem LEFT OUTER JOIN oder RIGHT OUTER JOIN, hilft Ihnen der GroupJoin-Operator weiter. Nehmen wir an, Sie möchten wissen, welche Bestellungen für die einzelnen Produkte vorliegen. Sie können die LINQ-Abfrage dann wie folgt definieren:
    Product[] products = Service.GetProducts();
    Customer[] customers = Service.GetCustomers();
    var liste = products
                .GroupJoin(customers.SelectMany(cust => cust.Orders),
                 prod => prod.ProductID,
                 ord => ord.ProductID,
                       (a, b) => new { a.ProductID, Orders = b });
    foreach (var t in liste) {
      Console.WriteLine("ProductID: {0}", t.ProductID, t.Orders);
      foreach (var order in t.Orders)
         Console.WriteLine("   OrderID: {0}", order.OrderID);
    }
GroupJoin arbeitet sehr ähnlich wie der Join-Operator. Der Unterschied zwischen den beiden Operatoren besteht darin, was in die Ergebnismenge aufgenommen wird. Mit Join sind es nur Daten, deren Schlüssel sowohl in der outer-Liste als auch der inner-Liste vertreten sind. Findet Join in der inner-Liste kein passendes Element, wird das outer-Element nicht in die Ergebnisliste aufgenommen. Ganz anders ist das Verhalten von GroupJoin. Dieser Operator nimmt auch dann ein Element aus der outer-Liste in die Ergebnisliste auf, wenn keine entsprechenden Daten in inner vorhanden sind. Sie können das sehr schön in Abbildung 10.3 sehen, denn der Artikel mit der ProductID: 2 ist in keiner Bestellung zu finden. Hier klicken, um das Bild zu vergrößern Abbildung 10.3 Ergebnisliste der LINQ-Abfrage mit dem »GroupJoin«-Operator Sie können den GroupJoin-Operator auch in einem Abfrageausdruck beschreiben. Er wird mit join... into... definiert.
    Product[] products = Service.GetProducts();
    Customer[] customers = Service.GetCustomers();
    var liste = from cust in customers
                from ord in cust.Orders
                select ord;
    var expr = from prod in products
               join custord in liste
               on prod.ProductID equals custord.ProductID into allOrders
               select new { prod.ProductID, Orders = allOrders};
### 10.2.10 Die Set-Operatoren-Familie #### Der Operator »Distinct« Vielleicht kennen Sie die Wirkungsweise von DISTINCT bereits von SQL. In LINQ hat der Distinct-Operator die gleiche Aufgabe: Er garantiert, dass in der Ergebnismenge ein Element nicht doppelt auftritt.
    string[] cities = new string[]{
       "Aachen", "Köln", "Bonn", "Aachen", "Bonn", "Frankfurt"};
    var liste = (from p in cities select p).Distinct();
    foreach (string city in liste)
       Console.WriteLine(city);
Im Array cities kommen die beiden Städte Aachen in Bonn je zweimal vor. Der auf die Ergebnismenge angewendete Distinct-Operator erkennt dies und sorgt dafür, dass jede Stadt nur einmal angezeigt wird. #### Der Operator »Union« Der Union-Operator verbindet zwei Listen miteinander. Dabei werden doppelte Vorkommen ignoriert.
    string[] cities = new string[]{
             "Aachen", "Bonn", "Aachen", "Frankfurt"};
    string[] namen = new string[]{
             "Peter", "Willi", "Hans"};
    var listeCities = from c in cities
                      select c;
    var listeNamen  = from n in namen
                      select n;
    var listeComplete = listeCities.Union(listeNamen);
    foreach (var p in listeComplete)
      Console.WriteLine(p);
In der Ergebnisliste werden der Reihe nach Aachen, Köln, Bonn, Frankfurt, Peter, Willi und Hans erscheinen. #### Der Operator »Intersect« Der Intersect-Operator bildet eine Ergebnisliste aus zwei anderen Listen. In der Ergebnisliste sind aber nur die Elemente enthalten, die in beiden Listen gleichermaßen enthalten sind. Intersect bildet demnach eine Schnittmenge ab.
    string[] cities1 = new string[]{
            "Aachen", "Köln", "Bonn", "Aachen", "Frankfurt"};
    string[] cities2 = new string[]{
            "Düsseldorf", "Bonn", "Bremen", "Köln"};
    var listeCities1 = from c in cities1
                       select c;
    var listeCities2  = from n in cities2
                        select n;
    var listeComplete = listeCities1.Intersect(listeCities2);
    foreach (var p in listeComplete)
      Console.WriteLine(p);
Das Ergebnis wird durch die Städte Köln und Bonn gebildet. #### Der Operator »Except« Während Intersect die Gemeinsamkeiten aufspürt, sucht der Operator Except nach allen Elementen, durch die sich die Listen voneinander unterscheiden. Dabei sind nur die Elemente in der Ergebnisliste enthalten, die in der ersten Liste angegeben sind und in der zweiten Liste fehlen. Wenn Sie in dem Codefragment anstelle von Intersect den Operator Except verwenden, enthält die Ergebnisliste die Orte Aachen und Frankfurt. ### 10.2.11 Die Familie der Aggregatoperatoren LINQ stellt mit Count, LongCount, Sum, Min, Max, Average und Aggregate eine Reihe von Aggregatoperatoren zur Verfügung, um Berechnungen an Quelldaten durchzuführen. #### Die Operatoren »Count« und »LongCount« Sehr einfach einzusetzen sind die beiden Operatoren Count und LongCount. Beide unterscheiden sich dahingehend, dass Count einen int als Typ zurückgibt und LongCount einen long. Um Count zu testen, wollen wir zuerst wissen, wie viele Bestellungen insgesamt eingegangen sind:
    Order[] orders = Service.GetOrders();
    var anzahl = (from x in orders
                  select x).Count();
    Console.WriteLine("Anzahl der Bestellungen gesamt = {0}", anzahl);
Alternativ können Sie auch Folgendes formulieren:
    var anzahl = orders.Count();
Das Ergebnis lautet 16. Vielleicht interessiert uns auch, wie viele Bestellungen jeder einzelne Kunde aufgegeben hat. Wir müssen dann den folgenden Code schreiben:
    Customer[] customers = Service.GetCustomers();
    var orderCounts = from c in customers
                      select new { c.Name, OrderCount = c.Orders.Count() };
    foreach (var k in orderCounts)
      Console.WriteLine("{0} - {1}", k.Name, k.OrderCount);
#### Der Operator »Sum« Sum ist grundsätzlich zunächst einmal sehr einfach einzusetzen. Der Operator liefert eine Summe als Ergebnis der LINQ-Abfrage. Im folgenden Codefragment wird die Summe aller Integer-Werte ermittelt, die das Array bilden. Das Ergebnis lautet 114.
    int[] arr = new int[] { 1, 3, 7, 4, 99 };
    var sumInt = arr.Sum();
    Console.WriteLine("Integer-Summe = {0}", sumInt);
Das folgende Beispiel ist nicht mehr so einfach. Hier soll der Gesamtbestellwert über alle Produkte für jeden Kunden ermittelt werden.
    var allOrders =
       from cust in customers
       from ord in cust.Orders
       join prod in products on ord.ProductID equals prod.ProductID
       select new { cust.Name, ord.ProductID,
                    OrderAmount = ord.Quantity * prod.Price };
    var summe =
       from cust in customers
       join ord in allOrders
       on cust.Name equals ord.Name into custWithOrd
       select new { cust.Name, TotalSumme = custWithOrd.Sum(s => s.OrderAmount) };
    foreach(var s in summe)
      Console.WriteLine("Name: {0,-7} Bestellsumme: {1}",
                        s.Name, s.TotalSumme);
Analysieren wir den Code schrittweise, und überlegen wir, was das Resultat des folgenden Abfrageteilausdrucks ist.
    var allOrders =
       from cust in customers
       from ord in cust.Orders
       join prod in products on ord.ProductID equals prod.ProductID
       select new { cust.Name, ord.ProductID,
                    OrderAmount = ord.Quantity * prod.Price };
Zuerst ist es notwendig, die Bestellungen aus jedem Customer-Objekt zu filtern. Danach wird ein Join gebildet, der die ProductIDs aus den einzelnen Bestellungen eines Kunden mit der ProductID aus der Liste der Artikel verbindet. Das Ergebnis ist eine Art Tabelle mit Spalten für den Bestellernamen, die ProductID und die Gesamtsumme für diesen Artikel, die anhand der Bestellmenge gebildet wurde (siehe Abbildung 10.4). Nun gilt es noch, die Ergebnisliste nach den Kunden zu gruppieren und dann die Gesamtsumme aller Bestellungen zu bilden:
    var summe =
       from cust in customers
       join ord in allOrders
       on cust.Name equals ord.Name into custWithOrd
       select new { cust.Name,
            TotalSumme = custWithOrd.Sum(s => s.OrderAmount) };
Hier klicken, um das Bild zu vergrößern Abbildung 10.4 Bestellwert als Zwischenergebnis Wir sollten uns daran erinnern, dass der Join-Operator mit diesen Fähigkeiten ausgestattet ist. Es müssen zuerst die beiden Listen customers und allOrders zusammengeführt werden. Sie können sich das so vorstellen, dass die Gruppierung mit Join zur Folge hat, dass für jeden Customer eine eigene »Tabelle« erzeugt wird, in der alle seine Bestellungen beschrieben sind. Die Variable s steht hier für ein Gruppenelement, letztendlich also für eine Bestellung. Die Gruppierung nach Customer-Objekten gestattet es uns nun, mit dem Operator Sum den Inhalt der Spalte OrderAmount zu summieren. Das Resultat der kompletten LINQ-Abfrage sehen Sie in Abbildung 10.5. Hier klicken, um das Bild zu vergrößern Abbildung 10.5 Ergebnis der Abfrage der Gesamtbestellsumme #### Die Operatoren »Min«, »Max« und »Average« Die Aggregatoperatoren Min und Max ermitteln den minimalen bzw. maximalen Wert in einer Datenliste, und Average liefert das arithmetische Mittel. Grundsätzlich ist der Einsatz der Operatoren sehr einfach, wie das folgende Codefragment exemplarisch an Max zeigt:
    var max = (from p in products
               select p.Price).Max();
Das funktioniert aber auch nur, solange numerische Werte als Datenquelle vorliegen. Sie brauchen den Code nur wie folgt leicht zu ändern, um festzustellen, dass nun eine ArgumentException ausgelöst wird.
    var max = (from p in products
               select new { p.Price }).Max();
Die Meldung zu der Exception besagt, dass mindestens ein Typ die IComparable-Schnittstelle implementieren muss. In der ersten funktionsfähigen Version des Codes stand in der Ergebnisliste ein numerischer Wert, der der Forderung entspricht. Im zweiten, einen Fehler verursachenden Codefragment hingegen wird ein anonymer Typ beschrieben, der mit der geforderten Schnittstelle überhaupt nicht dienen kann. Diese Lösung der Problematik ist nicht schwer. Die Operatoren sind alle so überladen, dass ihnen einen Wert-Selektor übergeben werden kann. Mit anderen Worten: Geben Sie das gewünschte Element aus der Liste der Elemente, die den anonymen Typ bilden, als zu bewertenden Ausdruck an.
    var max = (from p in products
               select new { p.Price })
               .Max( x => x.Price);
### 10.2.12 Generierungsoperatoren #### Der Operator »Range« Dieser Operator liefert ausgehend von einem Startwert eine Gruppe von Integer-Werten, die aus einem spezifizierten Wertebereich ausgewählt werden. Die Definition des Operators lautet wie folgt:
    public static IEnumerable<int> Range(int start, int count);
Bei genauer Betrachtung ist dieser Operator mit einer for-Schleife vergleichbar. Sie übergeben dem ersten Parameter den Startwert und teilen mit dem zweiten Parameter mit, wie oft eine bestimmte Operation ausgeführt werden soll. Im folgenden Codefragment werden alle Produkte gesucht, deren Preis größer 5 ist. Das ist der definierte Startwert. Unsere Liste wird dabei nur einmal durchlaufen.
    Product[] products = Service.GetProducts();
    var numbers = Enumerable.Range(5, 1)
                  .SelectMany(x => (from prod in products
                  where prod.Price > x
                  select new { prod.ProductName }));
    foreach (var res in numbers) {
      Console.WriteLine(res);
    }
Keine Frage, wir könnten mit einer einfachen where-Bedingung zum gleichen Resultat kommen. Der Range-Operator ist auch viel besser dazu geeignet, mathematische Operationen zu codieren. Dies demonstriert der folgende Code:
    var nums = Enumerable.Range(1, 10).Select(x => 2 * x);
    foreach (var num in nums)
      Console.WriteLine(num);
#### Der Operator »Repeat« Der Repeat-Operator arbeitet ähnlich wie der zuvor besprochene Range-Operator. Repeat gibt eine Gruppe zurück, in der dasselbe Element mehrfach enthalten ist. Die Anzahl der Wiederholungen ist dabei festgelegt. Auch zu diesem Operator wollen wir uns zunächst die Definition ansehen.
    public static IEnumerable<T> Repeat<T>(T element, int count);
Dem ersten Parameter übergeben Sie das Element, das wiederholt werden soll. Dem zweiten Parameter teilen Sie die Anzahl der Wiederholungen mit. Mit
    Product[] products = Service.GetProducts();
    var prods = Enumerable.Repeat((from p in products
                                   select p.ProductName), 3)
                                   .SelectMany(x => x);
    foreach (var p in prods)
      Console.WriteLine(p);
werden beispielsweise alle Produktnamen dreimal ausgegeben. ### 10.2.13 Quantifizierungsoperatoren Beabsichtigen Sie, die Existenz von Elementen in einer Liste anhand von Bedingungen oder definierten Regeln zu überprüfen, helfen die Quantifizierungsoperatoren Ihnen weiter. #### Der Operator »Any« Any ist ein Operator, der ein Prädikat auswertet und einen booleschen Wert zurückliefert. Nehmen wir an, Sie möchten wissen, ob der Kunde Willi auch das Produkt mit der ID = 6 bestellt hat. Any hilft, das festzustellen.
    bool result = (from cust in customers
                   from ord in cust.Orders
                   where cust.Name == "Willi"
                   select new { ord.ProductID })
                   .Any(ord => ord.ProductID == 6);
    if (result)
      Console.WriteLine("ProductID 6 ist enthalten");
    else
      Console.WriteLine("ProductID 6 ist nicht enthalten");
Die Elemente werden so lange ausgewertet, bis der Operator auf ein Element stößt, das die Bedingung erfüllt. #### Der Operator »All« Während Any schon true liefert, wenn für ein Element die Bedingung erfüllt ist, liefert der Operator All nur dann true, wenn alle untersuchten Elemente der Bedingung entsprechen. Möchten Sie beispielsweise feststellen, ob alle Preise der Einzelprodukte > 3 sind, genügt die folgende LINQ-Abfrage:
    bool result = (from prod in products
                   select prod).All(p => p.Price > 3);
### 10.2.14 Aufteilungsoperatoren Mit where und select filtern Sie eine Datenquelle nach vorgegebenen Kriterien. Das Ergebnis ist anschließend eine neue Menge von Daten, die den Kriterien entspricht. Möchten Sie nur eine Teilmenge der Datenquelle betrachten, ohne Filterkriterien einzusetzen, eignen sich die Aufteilungsoperatoren. #### Der Operator »Take« Sie könnten zum Beispiel daran interessiert sein, nur die ersten drei Produkte aus der Liste aller Produkte auszugeben. Mit dem Take-Operator ist das sehr einfach zu realisieren:
    Product[] prods = Service.GetProducts();
    var result = prods.Take(3);
    foreach (var prod in result)
      Console.WriteLine(prod.ProductName);
Wir greifen in unserem Beispiel auf eine Datenquelle zu, die uns der Aufruf der Methode GetProducts liefert. Natürlich kann die zu untersuchende Datenquelle zuvor durch einen anderen LINQ-Ausdruck gebildet werden:
    Product[] prods = Service.GetProducts();
    var result = (from prod in prods
                  where prod.Price > 3
                  select new { prod.ProductName, prod.Price })
                   .Take(3);
    foreach (var prod in result)
      Console.WriteLine("{0,-7}{1}", prod.ProductName, prod.Price);
#### Der Operator »TakeWhile« Der Operator Take basiert auf einem Integer als Zähler. Sehr ähnlich arbeitet auch TakeWhile. Der Unterschied zum zuvor behandelten Operator ist, dass Sie ein Prädikat angeben können, das als Kriterium der Filterung angesehen wird. TakeWhile durchläuft die Datenquelle und gibt das gefundene Element zurück, wenn das Ergebnis der Prüfung true ist. Beendet wird der Durchlauf unter zwei Umständen: Das Ende der Datenquelle ist erreicht. Das Ergebnis einer Untersuchung lautet false. Wir wollen uns das an einem Beispiel ansehen. Auch dabei wird als Quelle auf die Liste der Produkte zurückgegriffen. Das Prädikat sagt aus, dass diejenigen Produkte in der Ergebnisliste erfasst werden sollen, deren Preis höher als 3 ist:
    Product[] prods = Service.GetProducts();
    var result = (from prod in prods
                  select new { prod.ProductName, prod.Price })
                  .TakeWhile(n => n.Price > 3);
    foreach (var prod in result)
      Console.WriteLine("{0,-7}{1}", prod.ProductName, prod.Price);
Es werden die folgenden Produkte angezeigt: Käse Wurst Obst Gemüse Fleisch Beachten Sie, dass in der Ergebnisliste das Produkt Pizza nicht enthalten ist, da die Schleife beendet wird, ehe Pizza einer Untersuchung unterzogen werden kann. #### Die Operatoren »Skip« und »SkipWhile« Take und TakeWhile werden um Skip und SkipWhile ergänzt. Skip überspringt eine bestimmte Anzahl von Elementen in einer Datenquelle. Der verbleibende Rest bildet die resultierende Ergebnismenge. Um zum Beispiel die ersten beiden in der Liste enthaltenen Produkte aus der Ergebnisliste auszuschließen, codieren Sie die folgenden Anweisungen:
    Product[] prods = Service.GetProducts();
    var result = (from prod in prods
                  select new { prod.ProductName, prod.Price })
                  .Skip(2);
SkipWhile erwartet ein Prädikat. Die Elemente werden damit verglichen. Dabei werden die Elemente so lange übersprungen, wie das Ergebnis der Überprüfung true liefert. Sobald eine Überprüfung false ist, werden das betreffende Element und alle Nachfolgeelemente in die Ergebnisliste aufgenommen. Das Prädikat im folgenden Codefragment sucht in der Liste aller Produkte nach dem ersten Produkt, für das die Bedingung nicht gilt, dass der Preis > 3 ist. Dieses und alle darauf folgenden Elemente werden in die Ergebnisliste geschrieben.
    Product[] prods = Service.GetProducts();
    var result = (from prod in prods
                  select new { prod.ProductName, prod.Price })
                  .SkipWhile(x => x.Price > 3 );
Ausgegeben werden folgende Produkte: Süßwaren Bier Pizza ### 10.2.15 Elementoperatoren Bisher lieferten uns alle Operatoren immer eine Ergebnismenge zurück. Oft möchten Sie aber aus einer Liste ein bestimmtes einzelnes Element herausfinden. Hierbei unterstützen uns die Operatoren, denen wir uns nun widmen. #### Der Operator »First« Dieser Operator sucht das erste Element in einer Datenquelle. Wegen der Überladung kann es sich um das positional erste Element handeln oder um das erste Element einer mit einem Prädikat gebildeten Ergebnisliste. Das folgende Beispiel zeigt, wie einfach der Einsatz von First ist. Als Ergebnis wird auf der Konsole Käse ausgegeben.
    Product[] prods = Service.GetProducts();
    var result = (from prod in prods
                  select new { prod.ProductName })
                  .First();
    Console.WriteLine("{0}", result.ProductName);
Vielleicht möchten Sie aber eine Liste aller Produkte haben, deren Preis kleiner 10 ist, und aus dieser Liste nur das erste Listenelement herausfiltern.
    Product[] prods = Service.GetProducts();
    var result = (from prod in prods
                  select new { prod.ProductName, prod.Price })
                  .First(item => item.Price < 10);
    Console.WriteLine("{0}", result.ProductName);
Hier lautet das Produkt Wurst. #### Der Operator »FirstOrDefault« Versuchen Sie einmal, das letzte Codefragment mit dem Prädikat
    item => item.Price < 1
auszuführen. Sie werden eine Fehlermeldung erhalten, weil kein Produkt in der Datenquelle enthalten ist, das der genannten Bedingung entspricht. In solchen Fällen empfiehlt es sich, anstelle des Operators First den Operator FirstOrDefault zu benutzen. Für den Fall, dass kein Element gefunden wird, liefert der Operator default(T) zurück. Handelt es sich um einen Referenztyp, ist das null. FirstOrDefault liegt ebenfalls in zwei Überladungen vor. Sie können neben der parameterlosen Variante auch die parametrisierte Überladung benutzen, der Sie das gewünschte Prädikat übergeben.
    Product[] prods = Service.GetProducts();
    var result = (from prod in prods
                  select new { prod.ProductName, prod.Price })
                  .FirstOrDefault(item => item.Price < 1);
    if (result == null)
      Console.WriteLine("Kein Element entspricht der Bedingung.");
    else
      Console.WriteLine("{0}", result.ProductName);
#### Die Operatoren »Last« und »LastOrDefault« Sicherlich können Sie sich denken, dass die beiden Operatoren Last und LastOrDefault Ergänzungen der beiden im Abschnitt zuvor behandelten Operatoren sind. Beide operieren auf die gleiche Weise wie First und FirstOrDefault, nur dass das letzte Element der Liste das Ergebnis bildet.
    Product[] prods = Service.GetProducts();
    var result = (from prod in prods
                  select new { prod.ProductName, prod.Price })
                  .LastOrDefault(item => item.Price < 5);
    if (result == null)
      Console.WriteLine("Kein Element entspricht der Bedingung.");
    else
      Console.WriteLine("{0}", result.ProductName);
#### Die Operatoren »Single« und »SingleOrDefault« Alle bislang vorgestellten Elementoperatoren lieferten eine Ergebnismenge, aus der ein Element herausgelöst wurde: Entweder lieferten sie das erste oder das letzte Element. Mit Single bzw. SingleOrDefault können Sie nach einem bestimmten, eindeutigen Element Ausschau halten. »Eindeutig« bedeutet in diesem Zusammenhang, dass es kein Zwischenergebnis gibt, aus dem anschließend ein Element das Ergebnis bildet. In der Musterdaten-Anwendung ist beispielsweise das Feld ProductID eindeutig, vergleichbar mit der Primärschlüsselspalte einer Datenbanktabelle. Mit Single und SingleOrDefault können Sie ein eindeutiges Element finden. Werden mehrere gefunden, wird eine InvalidOperationException ausgelöst. Auch für dieses Pärchen gilt: Besteht die Möglichkeit, dass kein Element gefunden wird, sollten Sie den Operator SingleOrDefault einsetzen, der – wie bei den anderen Operatoren auch – default(T) als Rückgabewert liefert und keine Ausnahme auslöst wie Single in diesem Fall. Sie können beide Operatoren parameterlos aufrufen oder ein Prädikat angeben.
    Product[] prods = Service.GetProducts();
    var result = (from prod in prods
                  select new { prod.ProductID, prod.ProductName })
                 .Single( p => p.ProductID == 2);
    if (result == null)
      Console.WriteLine("Kein Element entspricht der Bedingung.");
    else
      Console.WriteLine("{0}", result.ProductName);
#### Die Operatoren »ElementAt« und »ElementOrDefault« Möchten Sie ein bestimmtes Element aus einer Liste anhand seiner Position extrahieren, sollten Sie entweder die Methode ElementAt oder die Methode ElementAtOrDefault verwenden.
    Product[] prods = Service.GetProducts();
    var result = (from prod in prods
                  select new { prod.ProductID, prod.ProductName )
                  .ElementAtOrDefault(3);
    if (result == null)
       Console.WriteLine("Kein Element entspricht der Bedingung.");
    else
      Console.WriteLine("{0}", result.ProductName);
Beide Methoden erwarten die Angabe des Index in der Liste. Da Listen nullbasiert sind, wird bei der Angabe von 3 das vierte Element extrahiert. ElementAtOrDefault liefert wieder den Standardwert, falls der Index negativ oder größer als die Elementanzahl ist. #### Der Operator »DefaultIfEmpty« Standardmäßig liefert dieser Operator eine Liste von Elementen ab. Sollte die Liste jedoch leer sein, führt dieser Operator nicht sofort zu einer Exception. Stattdessen ist der Rückgabewert dann entweder default(T) oder – falls Sie die überladene Fassung von DefaultIfEmpty eingesetzt haben – ein spezifischer Wert.
    List<string> liste = new List<string>();
    liste.Add("Peter");
    liste.Add("Uwe");
    foreach (string tempStr in liste.DefaultIfEmpty("leer")) {
      Console.WriteLine(tempStr);
    }