Kapitel 2 Grundlagen der LINQ-Syntax In diesem Kapitel: LINQ-Abfragen Abfrageschlüsselwörter Verzögerte Auswertung von Abfragen und Auflösung von Erweiterungsmethoden Abschließende Gedanken zu LINQ-Abfragen Zusammenfassung 24 29 41 45 48 23 Paolo Pialorsi, Marco Russo: Datenbankprogrammierung mit Microsoft LINQ. Microsoft Press 2008 (ISBN 978-3-86645-428-6) 24 Kapitel 2: Grundlagen der LINQ-Syntax Sprachintegrierte Abfrage (Language Integrated Query, LINQ) ermöglicht Entwicklern, Sequenzen von Elementen (Objekte, Entitäten, Datenbankdatensätze, XML-Knoten usw.) innerhalb ihrer Softwareprojekte mit einer gemeinsamen Syntax und einer einheitlichen Programmiersprache unabhängig von der Herkunft der behandelten Elemente abzufragen und zu verwalten. Das Schlüsselfeature von LINQ ist die Integration in weit verbreitete Programmiersprachen. Diese Integration wird ermöglicht durch die Verwendung einer für alle Arten von Inhalten gemeinsamen Syntax. Wie Kapitel 1 erläutert hat, stellt LINQ eine grundlegende Infrastruktur für viele unterschiedliche Implementierungen von Abfragemodulen bereit. Dazu gehören LINQ to Objects, LINQ to SQL, LINQ to DataSet, LINQ to Entities, LINQ to XML usw. Alle diese Abfrageerweiterungen basieren auf spezialisierten Erweiterungsmethoden und nutzen für die Abfrageausdruckssyntax einen gemeinsamen Satz von Schlüsselwörtern, die in diesem Kapitel behandelt werden. Bevor wir uns die einzelnen Schlüsselwörter im Detail ansehen, gehen wir verschiedene Aspekte einer einzelnen LINQ-Abfrage durch und führen in die grundlegenden Elemente der LINQ-Syntax ein. LINQ-Abfragen LINQ basiert auf einem Satz von Abfrageoperatoren, die als Erweiterungsmethoden definiert sind und mit jedem Objekt arbeiten, das die Schnittstelle IEnumerable<T> oder IQueryable<T> implementiert. HINWEIS Die Anhänge B und C enthalten weitere Details zu Erweiterungsmethoden. Durch dieses Konzept wird LINQ zu einem universellen Abfrageframework, weil viele Auflistungen oder Typen IEnumerable<T> bzw. IQueryable<T> implementieren und jeder Entwickler seine eigene Implementierung definieren kann. Außerdem ist diese Abfrageinfrastruktur äußerst erweiterbar, wie Sie noch in Kapitel 12 sehen werden. Mit der Architektur können Entwickler das Verhalten einer Methode basierend auf dem Typ der abzufragenden Daten spezialisieren. Zum Beispiel besitzen sowohl LINQ to SQL als auch LINQ to XML spezialisierte LINQ-Operatoren, um relationale Daten bzw. XML-Knoten zu verarbeiten. Abfragesyntax Die Einführung in die Abfragesyntax beginnt mit einem einfachen Beispiel. Nehmen Sie an, Sie müssen ein Array von Objekten eines Typs Developer mit LINQ to Objects abfragen und die Namen der Entwickler extrahieren, die C# als Hauptprogrammiersprache verwenden. Listing 2.1 zeigt, wie der Code aussehen könnte. using System; using System.Linq; using System.Collections.Generic; public class Developer { public string Name; public string Language; public int Age; } Paolo Pialorsi, Marco Russo: Datenbankprogrammierung mit Microsoft LINQ. Microsoft Press 2008 (ISBN 978-3-86645-428-6) 25 LINQ-Abfragen class App { static void Main() { Developer[] developers = new Developer[] { new Developer {Name = "Paolo", Language = "C#"}, new Developer {Name = "Marco", Language = "C#"}, new Developer {Name = "Frank", Language = "VB.NET"}}; var developersUsingCSharp = from d in developers where d.Language == "C#" select d.Name; foreach (var item in developersUsingCSharp) { Console.WriteLine(item); } } } Listing 2.1 Ein einfacher Abfrageausdruck in C# 3.0 Als Ergebnis liefert dieser Code die Namen Paolo und Marco. In Visual Basic 2008 lässt sich die gleiche Abfrage (und für den gleichen Typ Developer) mit einer Syntax ausdrücken, wie sie Listing 2.2 zeigt. Imports System Imports System.Linq Imports System.Collections.Generic Public Class Developer Public Name As String Public Language As String Public Age As Integer End Class Module App Sub Main() Dim developers As New Developer New Developer New Developer New Developer() { _ With {.Name = "Paolo", .Language = "C#"}, _ With {.Name = "Marco", .Language = "C#"}, _ With {.Name = "Frank", .Language = "VB.NET"}} Dim developersUsingCSharp = _ From d In developers _ Where d.Language = "C#" _ Select d.Name For Each item in developersUsingCSharp Console.WriteLine(item) Next End Sub End Module Listing 2.2 Ein einfacher Abfrageausdruck in Visual Basic 2008 Paolo Pialorsi, Marco Russo: Datenbankprogrammierung mit Microsoft LINQ. Microsoft Press 2008 (ISBN 978-3-86645-428-6) 26 Kapitel 2: Grundlagen der LINQ-Syntax Die Syntax der Abfragen (in den Listings 2.1 und 2.2 fett gedruckt) bezeichnet man als Abfrageausdruck. In manchen LINQ-Implementierungen spricht man bei einer speicherresidenten Darstellung dieser Abfragen von einer Ausdrucksbaumstruktur. Ein Abfrageausdruck operiert auf einer oder mehreren Informationsquellen, indem einer oder mehrere Abfrageoperatoren entweder aus der Gruppe der Standardabfrageoperatoren oder aus der Gruppe der domänenspezifischen Operatoren angewandt werden. Im Allgemeinen liefert die Auswertung eines Abfrageausdrucks eine Folge von Werten. Ein Abfrageausdruck wird nur dann ausgewertet, wenn sein Inhalt aufgezählt wird. Kapitel 11 gibt weitere Einzelheiten zu Abfrageausdrücken und Ausdrucksbaumstrukturen an. HINWEIS Im Sinne einer einfachen Darstellung zeigen wir in den folgenden Beispielen nur die C# 3.0-Syntax. Die Visual Basic 2008-Version dieses Beispiels ist aber der C# 3.0-Version sehr ähnlich. Diese Abfragen lesen sich fast wie SQL-Anweisungen, auch wenn ihr Format etwas abweicht. Der hier definierte Beispielausdruck besteht aus einem Auswahlbefehl select d.Name der auf eine Gruppe von Elementen angewandt wird from d in developers wobei als Ziel der from-Klausel jede Instanz einer Klasse infrage kommt, die die Schnittstelle IEnumerable<T> implementiert. Die Auswahl wendet eine bestimmte Filterbedingung an: where d.Language == "C#" Die Sprachcompiler übersetzen diese Klauseln in Aufrufe von Erweiterungsmethoden, die sequenziell auf das Ziel der Abfrage angewandt werden. Die in der Assembly System.Core.dll untergebrachte Kernbibliothek von LINQ definiert einen Satz von Erweiterungsmethoden, die nach Ziel und Zweck gruppiert sind. Zum Beispiel enthält die Assembly eine Klasse Enumerable, die zum Namespace System.Linq gehört. Sie definiert Erweiterungsmethoden, die sich auf Instanzen von Typen anwenden lassen, die die Schnittstelle IEnumerable<T> implementieren. Die in der Beispielabfrage definierte Filterbedingung (where) wird in einen Aufruf der WhereErweiterungsmethode der Klasse Enumerable übersetzt. Die beiden Überladungen dieser Methode übernehmen einen Delegaten für eine predicate-Funktion, die die zu überprüfende Filterbedingung beschreibt, während die Ergebnisdaten partitioniert werden. In diesem Fall ist die filternde predicate-Funktion ein generischer Delegat. Er übernimmt ein Element vom Typ »T«, d. h. vom selben Typ wie die Instanzen, die in der zu filternden Aufzählung gespeichert sind. Der Delegat liefert ein boolesches Ergebnis, das die Mitgliedschaft des Elements in der gefilterten Ergebnismenge angibt: public static IEnumerable<T> Where<T>( this IEnumerable<T> source, Func<T, bool> predicate); Wie aus der Methodensignatur hervorgeht, können Sie diese Methode für jeden Typ aufrufen, der IEnumerable<T> implementiert, folglich auch für das developers-Array des Beispiels: Paolo Pialorsi, Marco Russo: Datenbankprogrammierung mit Microsoft LINQ. Microsoft Press 2008 (ISBN 978-3-86645-428-6) LINQ-Abfragen 27 var filteredDevelopers = developers.Where(delegate (Developer d) { return (d.Language == "C#"); }); Hier stellt das an die Where-Methode übergebene predicate-Argument einen anonymen Delegaten zu einer Funktion dar, die für jedes Element des Typs Developer aufgerufen wird, das aus der Quelldatenmenge (developers) stammt. Der Aufruf der Where-Methode liefert eine Teilmenge von Elementen: alle diejenigen Elemente, die der predicate-Bedingung genügen. In C# 3.0 und Visual Basic 2008 kann ein anonymer Delegat in einfacherer Weise – mit einem LambdaAusdruck – definiert werden. Der Code für das Filterbeispiel lässt sich dann wie folgt kompakter formulieren: var filteredDevelopers = developers.Where(d => d.Language == "C#"); WICHTIG In den Anhängen B und C finden Sie weitere Einzelheiten zur Syntax von Erweiterungsmethoden, LambdaAusdrücken, anonymen Delegaten usw. Die select-Anweisung ist ebenfalls eine Erweiterungsmethode (namens Select), die von der Klasse Enumerable bereitgestellt wird. Die Signatur der Methode Select sieht so aus: public static IEnumerable<TResult> Select<TSource, TResult>( this IEnumerable<TSource> source, Func<TSource, TResult> selector); Das Argument selector ist eine Projektion, die eine Aufzählung von Objekten des Typs TResult zurückgibt. Die Aufzählung wird von einem Satz von Quellobjekten des Typs TSource abgerufen. Genau wie im obigen Beispiel lässt sich diese Methode mithilfe eines Lambda-Ausdrucks auf die gesamte developers-Auflistung anwenden. Es ist auch möglich, die Methode auf der von der Programmiersprache gefilterten Auflistung (namens filteredDevelopers) aufzurufen, weil es sich dabei immer noch um einen Typ handelt, der IEnumerable<T> implementiert: var csharpDevelopersNames = filteredDevelopers.Select(d => d.Name); Aufbauend auf der eben beschriebenen Sequenz von Anweisungen lässt sich die Beispielabfrage ohne Verwendung der Abfrageausdruckssyntax neu formulieren: IEnumerable<string> developersUsingCSharp = developers .Where(d => d.Language == "C#") .Select(d => d.Name); Die Methoden Where und Select übernehmen Lambda-Ausdrücke als Argumente. Diese Lambda-Ausdrücke werden in Prädikate und Projektionen übersetzt, die auf einem Satz von generischen Delegattypen basieren, die in der Assembly System.Core.dll des Namespaces System definiert sind. Die komplette Familie der generischen Delegattypen sieht folgendermaßen aus: Paolo Pialorsi, Marco Russo: Datenbankprogrammierung mit Microsoft LINQ. Microsoft Press 2008 (ISBN 978-3-86645-428-6) 28 Kapitel 2: Grundlagen der LINQ-Syntax public delegate TResult Func< TResult >(); public delegate TResult Func< T, TResult >( T arg ); public delegate TResult Func< T1, T2, TResult > (T1 arg1, T2 arg2 ); public delegate TResult Func< T1, T2, T3, TResult > ( T1 arg1, T2 arg2, T3 arg3 ); public delegate TResult Func< T1, T2, T3, T4, TResult > (T1 arg1, T2 arg2, T3 arg3, T4 arg4 ); Viele Erweiterungsmethoden der Klasse Enumerable übernehmen diese Delegaten als Argumente und die Beispiele in diesem Kapitel greifen ebenfalls darauf zurück. Die endgültige Version der anfänglichen Abfrage könnte wie in Listing 2.3 aussehen. Func<Developer, bool> filteringPredicate = d => d.Language == "C#"; Func<Developer, string> selectionPredicate = d => d.Name; IEnumerable<string> developersUsingCSharp = developers .Where(filteringPredicate) .Select(selectionPredicate); Listing 2.3 Der erste in grundlegende Elemente übersetzte Abfrageausdruck Genau wie der Visual Basic 2008-Compiler übersetzt der C# 3.0-Compiler die LINQ-Abfrageausdrücke (Listing 2.1 und Listing 2.2) in eine Anweisung ähnlich der in Listing 2.3. Wenn Sie sich einmal mit der Abfrageausdruckssyntax (Listing 2.1 und Listing 2.2) vertraut gemacht haben, ist es einfacher und leichter, die Syntax zu schreiben und zu verwalten, selbst wenn sie optional ist und Sie immer auf die äquivalente – wortreichere – Version (Listing 2.3) zurückgreifen können. Trotzdem ist es manchmal notwendig, eine Erweiterungsmethode direkt aufzurufen, weil die Abfrageausdruckssyntax nicht alle möglichen Erweiterungsmethoden abdeckt. WICHTIG Kapitel 3 geht ausführlicher auf alle Erweiterungsmethoden ein, die in der Klasse Enumerable des Namespaces System.Linq definiert sind. Vollständige Abfragesyntax Im vorherigen Abschnitt wurde eine einfache Abfrage über eine Liste von Objekten beschrieben. Die Abfragesyntax ist allerdings umfangreicher und stärker gegliedert als in diesem Beispiel gezeigt. Dabei gibt es für die Programmiersprachen viele verschiedene Schlüsselwörter, die den meisten Abfrageszenarios genügen. Jede Abfrage beginnt mit einer from-Klausel und endet entweder mit einer select-Klausel oder einer group-Klausel. Im Unterschied zur SQL-Syntax beginnt die Abfrage nicht mit einer select-Anweisung, sondern mit einer from-Klausel. Neben anderen technischen Gründen hängt das vor allem damit zusammen, Microsoft IntelliSense für den restlichen Teil der Abfrage bereitzustellen, um das Formulieren von Bedingungs-, Auswahl- und anderen Abfrageausdrucksklauseln zu erleichtern. Eine select-Klausel projiziert das Ergebnis eines Ausdrucks in ein aufzählbares Objekt. Eine group-Klausel projiziert das Ergebnis eines Ausdrucks basierend auf einer Gruppierungsbedingung in einen Satz von Gruppen, wobei jede Gruppe ein aufzählbares Objekt darstellt. Der folgende Code zeigt einen Prototyp für die vollständige Abfrageausdruckssyntax: Paolo Pialorsi, Marco Russo: Datenbankprogrammierung mit Microsoft LINQ. Microsoft Press 2008 (ISBN 978-3-86645-428-6) Abfrageschlüsselwörter 29 query-expression ::= from-clause query-body query-body ::= join-clause* (from-clause join-clause* | let-clause | where-clause)* orderby-clause? (select-clause | groupby-clause) query-continuation? from-clause ::= from itemName in srcExpr select-clause ::= select selExpr groupby-clause ::= group selExpr by keyExpr Auf die erste from-Klausel können null oder mehr from-, let- oder where-Klauseln folgen. Eine let-Klausel weist dem Ergebnis eines Ausdrucks einen Namen zu. Diese Klausel ist nützlich, wenn Sie denselben Ausdruck mehrmals innerhalb einer Abfrage referenzieren müssen. let-clause ::= let itemName = selExpr Wie bereits erwähnt, definiert eine where-Klausel einen Filter, der angewandt wird, um spezifische Elemente in die Ergebnisse einzuschließen. where-clause ::= where predExpr Jede from-Klausel generiert eine lokale Bereichsvariable. Sie entspricht den einzelnen Elementen in der Quellsequenz, auf die Abfrageoperatoren (wie zum Beispiel die Erweiterungsmethoden von System.Linq. Enumerable) angewandt werden. Auf eine from-Klausel können beliebig viele join-Klauseln folgen. Vor der abschließenden select- oder groupKlausel können Sie mit einer orderby-Klausel angeben, wie die Ergebnisse sortiert werden sollen: join-clause ::= join itemName in srcExpr on keyExpr equals keyExpr (into itemName)? orderby-clause ::= orderby (keyExpr (ascending | descending)?)* query-continuation ::= into itemName query-body Das ganze Buch hindurch finden Sie Beispiele für Abfrageausdrücke. Nutzen Sie diesen Abschnitt als Referenz, wenn Sie bestimmte Elemente Ihrer Syntax überprüfen möchten. Abfrageschlüsselwörter In den folgenden Abschnitten werden die verschiedenen Abfrageschlüsselwörter behandelt, die in der Abfrageausdruckssyntax verfügbar sind. Paolo Pialorsi, Marco Russo: Datenbankprogrammierung mit Microsoft LINQ. Microsoft Press 2008 (ISBN 978-3-86645-428-6) 30 Kapitel 2: Grundlagen der LINQ-Syntax Die Klausel from Das erste Schlüsselwort ist die from-Klausel. Sie definiert die Datenquelle einer Abfrage oder Unterabfrage und eine Bereichsvariable, die jedes einzelne aus der Datenquelle abzufragende Element definiert. Als Datenquelle kommt jede Instanz eines Typs infrage, der die Schnittstellen IEnumerable, IEnumerable<T> oder IQueryable<T> (die IEnumerable<T> implementiert) implementiert. Das folgende Codefragment zeigt eine Beispielanweisung in C# 3.0, die diese Klausel verwendet. from rangeVariable in dataSource Der Sprachcompiler leitet den Typ der Bereichsvariablen aus dem Typ der Datenquelle ab. Ist zum Beispiel die Datenquelle vom Typ IEnumerable<Developer>, erhält die Bereichsvariable den Typ Developer. In den Fällen, in denen Sie keine stark typisierte Datenquelle verwenden – beispielsweise eine ArrayList von Objekten des Typs Developer, die IEnumerable implementiert –, sollten Sie den Typ der Bereichsvariablen explizit angeben. Listing 2.4 zeigt ein Beispiel für eine derartige Abfrage mit einer expliziten Deklaration des Typs Developer für die Bereichsvariable d. ArrayList developers = new ArrayList(); developers.Add(new Developer { Name = "Paolo", Language = "C#" }); developers.Add(new Developer { Name = "Marco", Language = "C#" }); developers.Add(new Developer { Name = "Frank", Language = "VB.NET" }); var developersUsingCSharp = from Developer d in developers where d.Language == "C#" select d.Name; foreach (string item in developersUsingCSharp) { Console.WriteLine(item); } Listing 2.4 Ein Abfrageausdruck für eine nicht generische Datenquelle mit Typdeklaration für die Bereichsvariable Im obigen Beispiel ist die Typumwandlung obligatorisch. Andernfalls lässt sich die Abfrage nicht kompilieren, weil der Compiler nicht in der Lage ist, den Typ der Bereichsvariablen automatisch abzuleiten. Dabei geht die Fähigkeit verloren, den Language- und Name-Memberzugriff in derselben Abfrage aufzulösen. Abfragen können mit mehreren from-Klauseln Verknüpfungen zwischen mehreren Datenquellen definieren. In C# 3.0 verlangt jede Datenquelle die Deklaration einer from-Klausel, wie es in Listing 2.5 zu sehen ist, das Kunden mit ihren Bestellungen verknüpft. Beachten Sie bitte, dass die Beziehung zwischen Customer und Order durch die Anwesenheit eines Orders-Arrays vom Typ Order in jeder Instanz von Customer physisch definiert ist. WICHTIG Wenn Sie mehrere from-Klauseln verwenden, wird die Verknüpfungsbedingung durch die Struktur der Daten bestimmt und unterscheidet sich vom Konzept einer Verknüpfung in einer relationalen Datenbank. (Hierfür müssen Sie die joinKlausel in einem Abfrageausdruck verwenden, was später in diesem Kapitel erläutert wird.) Paolo Pialorsi, Marco Russo: Datenbankprogrammierung mit Microsoft LINQ. Microsoft Press 2008 (ISBN 978-3-86645-428-6) 31 Abfrageschlüsselwörter public class Customer { public String Name { get; set; } public String City { get; set; } public Order[] Orders { get; set; } } public class Order { public Int32 IdOrder { get; set; } public Decimal EuroAmount { get; set; } public String Description { get; set; } } // ... Code weggelassen ... static void queryWithJoin() { Customer[] customers = new Customer[] { new Customer { Name = "Paolo", City = "Brescia", Orders = new Order[] { new Order { IdOrder = 1, EuroAmount = 100, new Order { IdOrder = 2, EuroAmount = 150, new Order { IdOrder = 3, EuroAmount = 230, }}, new Customer { Name = "Marco", City = "Torino", Orders = new Order[] { new Order { IdOrder = 4, EuroAmount = 320, new Order { IdOrder = 5, EuroAmount = 170, }}}; Description = "Order 1" }, Description = "Order 2" }, Description = "Order 3" }, Description = "Order 4" }, Description = "Order 5" }, var ordersQuery = from c in customers from o in c.Orders select new { c.Name, o.IdOrder, o.EuroAmount }; foreach (var item in ordersQuery) { Console.WriteLine(item); } } Listing 2.5 Ein C# 3.0-Abfrageausdruck mit einer Verknüpfung zwischen einer Reihe von Datenquellen In Visual Basic 2008 kann eine einzelne from-Klausel mehrere Datenquellen, die jeweils durch Komma getrennt werden, definieren (siehe Listing 2.6). Dim customers As Customer() = { _ New Customer With {.Name = "Paolo", .City = "Brescia", _ .Orders = New Order() { _ New Order With {.IdOrder = 1, .EuroAmount = 100, New Order With {.IdOrder = 2, .EuroAmount = 150, New Order With {.IdOrder = 3, .EuroAmount = 230, }}, _ New Customer With {.Name = "Marco", .City = "Torino", _ .Orders = New Order() { _ New Order With {.IdOrder = 4, .EuroAmount = 320, New Order With {.IdOrder = 5, .EuroAmount = 170, }}} .Description = "Order 1"}, _ .Description = "Order 2"}, _ .Description = "Order 3"} _ .Description = "Order 4"}, _ .Description = "Order 5"} _ Paolo Pialorsi, Marco Russo: Datenbankprogrammierung mit Microsoft LINQ. Microsoft Press 2008 (ISBN 978-3-86645-428-6) 32 Kapitel 2: Grundlagen der LINQ-Syntax Dim ordersQuery = _ From c In customers, _ o In c.Orders _ Select c.Name, o.IdOrder, o.EuroAmount For Each item In ordersQuery Console.WriteLine(item) Next Listing 2.6 Ein Visual Basic 2008-Abfrageausdruck mit einer Verknüpfung zwischen einer Reihe von Datenquellen Auf Verknüpfungen geht dieses Kapitel später ausführlicher ein. Die Klausel where Wie bereits erwähnt, spezifiziert die where-Klausel eine Filterbedingung, die auf die Datenquelle angewendet wird. Das Prädikat wendet eine boolesche Bedingung auf jedes Element in der Datenquelle an, sodass nur diejenigen Elemente herausgezogen werden, bei denen die Auswertung der Bedingung true ergibt. In ein und derselben Abfrage können mehrere where-Klauseln oder eine where-Klausel mit mehreren Prädikaten erscheinen, die mit den logischen Operatoren (&&, || und ! in C# 3.0 bzw. And, Or, AndAlso, OrElse, Is und IsNot in Visual Basic 2008) kombiniert werden. Als Prädikat kommt in Visual Basic 2008 jeder Ausdruck infrage, der sich zu einem booleschen Wert auswerten lässt. Somit können Sie auch einen numerischen Ausdruck verwenden, der als true gilt, wenn er ungleich null ist. Die Abfrage in Listing 2.7 verwendet eine where-Klausel, um alle Bestellungen mit einem EuroAmount größer als 200 Euro herauszuziehen. var ordersQuery = from c in customers from o in c.Orders where o.EuroAmount > 200 select new { c.Name, o.IdOrder, o.EuroAmount }; Listing 2.7 Ein C# 3.0-Abfrageausdruck mit einer where-Klausel Listing 2.8 zeigt die entsprechende Abfragesyntax in Visual Basic 2008. Dim ordersQuery = _ From c In customers, _ o In c.Orders _ Where o.EuroAmount > 200 _ Select c.Name, o.IdOrder, o.EuroAmount Listing 2.8 Ein Visual Basic 2008-Abfrageausdruck mit einer where-Klausel Paolo Pialorsi, Marco Russo: Datenbankprogrammierung mit Microsoft LINQ. Microsoft Press 2008 (ISBN 978-3-86645-428-6) Abfrageschlüsselwörter 33 Die Klausel select Die select-Klausel spezifiziert die Gestalt der Abfrageausgabe. Sie basiert auf einer Projektion, die bestimmt, was aus dem Ergebnis der Auswertung aller vorangehenden Klauseln und Ausdrücke auszuwählen ist. In Visual Basic 2008 ist die Select-Klausel nicht obligatorisch. Ist sie nicht angegeben, gibt die Abfrage einen Typ zurück, der auf der für den aktuellen Gültigkeitsbereich gekennzeichneten Bereichsvariablen basiert. In den Listings 2.7 und 2.8 wurde die select-Klausel verwendet, um anonyme Typen zu projizieren, die aus Eigenschaften oder Membern der Bereichsvariablen im Gültigkeitsbereich bestehen. Ein Vergleich der C# 3.0-Syntax (Listing 2.7) und der Visual Basic 2008-Syntax (Listing 2.8) zeigt, dass die Visual Basic 2008Syntax im select-Muster mehr einer SQL-Anweisung ähnelt, während die C# 3.0-Version eher die Syntax der Programmiersprache erkennen lässt. In der Tat müssen Sie in C# 3.0 explizit Ihre Absicht deklarieren, um eine neue Instanz eines anonymen Typs zu erstellen, während die Sprachsyntax in Visual Basic 2008 schlanker ist und die inneren Abläufe verbirgt. Die Klauseln group und into Mit der group-Klausel können Sie ein Ergebnis projizieren, das nach einem Schlüssel gruppiert wurde. Die Klausel lässt sich als Alternative zur from-Klausel einsetzen und erlaubt es, einzelne oder mehrere Schlüsselwerte zu verwenden. Listing 2.9 zeigt ein Beispiel für eine Abfrage, die Entwickler nach ihrer Programmiersprache gruppiert. Developer[] developers = new Developer[] { new Developer { Name = "Paolo", Language = "C#" }, new Developer { Name = "Marco", Language = "C#" }, new Developer { Name = "Frank", Language = "VB.NET" }, }; var developersGroupedByLanguage = from d in developers group d by d.Language; foreach (var group in developersGroupedByLanguage) { Console.WriteLine("Language: {0}", group.Key); foreach (var item in group) { Console.WriteLine("\t{0}", item.Name); } } Listing 2.9 Ein C# 3.0-Abfrageausdruck, um Entwickler nach der Programmiersprache zu gruppieren Das Codefragment nach Listing 2.9 liefert die folgende Ausgabe: Language: C# Paolo Marco Language: VB.NET Frank Paolo Pialorsi, Marco Russo: Datenbankprogrammierung mit Microsoft LINQ. Microsoft Press 2008 (ISBN 978-3-86645-428-6) 34 Kapitel 2: Grundlagen der LINQ-Syntax Wie das Codebeispiel zeigt, ist das Ergebnis der Abfrage eine Aufzählung von Gruppen, die durch einen Schlüssel identifiziert werden und aus inneren Elementen bestehen. Praktisch können Sie jede Gruppe im Ergebnis der Abfrage aufzählen, ihre Key-Eigenschaft auf der Konsole ausgeben und die Elemente in jeder Gruppe durchsuchen, um ihre Werte herauszuziehen. Wie bereits weiter oben erwähnt, können Sie Elemente nach einem Mehrfachschlüsselwert, der anonyme Typen verwendet, gruppieren. Listing 2.10 zeigt ein Beispiel, das die Entwickler nach Sprache und Alter gruppiert. Developer[] developers = new Developer { Name new Developer { Name new Developer { Name }; new Developer[] { = "Paolo", Language = "C#", Age = 32 }, = "Marco", Language = "C#", Age = 37}, = "Frank", Language = "VB.NET", Age = 48 }, var developersGroupedByLanguage = from d in developers group d by new { d.Language, AgeCluster = (d.Age / 10) * 10 }; foreach (var group in developersGroupedByLanguage) { Console.WriteLine("Language: {0}", group.Key); foreach (var item in group) { Console.WriteLine("\t{0}", item.Name); } } Listing 2.10 Ein C# 3.0-Abfrageausdruck, um Entwickler nach Programmiersprache und Alter zu gruppieren Dieses Mal sieht die Ausgabe des Codefragments gemäß Listing 2.10 wie folgt aus: Language: { Language = C#, AgeCluster = 30 } Paolo Marco Language: { Language = VB.NET, AgeCluster = 40 } Frank In diesem Beispiel ist der Schlüssel (Key) für jede Gruppe ein anonymer Typ, der durch zwei Eigenschaften definiert wird: Language und AgeCluster. Visual Basic 2008 unterstützt auch die Gruppierung der Ergebnisse mithilfe der Group By-Klausel. Das Beispiel in Listing 2.11 zeigt eine Abfrage, die der in Listing 2.9 äquivalent ist. Dim developers As New Developer New Developer New Developer Developer() With {.Name With {.Name With {.Name = = = = { _ "Paolo", .Language = "C#", .Age = 32}, _ "Marco", .Language = "C#", .Age = 37}, _ "Frank", .Language = "VB.NET", .Age = 48}} Dim developersGroupedByLanguage = _ From d In developers _ Group d By d.Language Into Group _ Select Language, Group For Each group In developersGroupedByLanguage Console.WriteLine("Language: {0}", group.Language) Paolo Pialorsi, Marco Russo: Datenbankprogrammierung mit Microsoft LINQ. Microsoft Press 2008 (ISBN 978-3-86645-428-6) Abfrageschlüsselwörter 35 For Each item In group.Group Console.WriteLine(" {0}", item.Name) Next Next Listing 2.11 Ein Visual Basic 2008-Abfrageausdruck, um Entwickler nach der Programmiersprache zu gruppieren Die Visual Basic 2008-Syntax ist etwas komplexer als die entsprechende C# 3.0-Syntax. In Visual Basic 2008 müssen Sie die Gruppierung mithilfe der Into-Klausel projizieren, um ein neues Group-Objekt der Elemente zu erstellen, und dann explizit das Auswahlmuster deklarieren. Allerdings lässt sich das Ergebnis der Gruppierung einfacher aufzählen, weil der Key-Wert seinen Namen (Language) beibehält. C# 3.0 bietet ebenfalls eine into-Klausel, die in Verbindung mit dem Schlüsselwort group nützlich, wenn auch nicht obligatorisch ist. Das Schlüsselwort into können Sie verwenden, um die Ergebnisse einer select-, group- oder join-Anweisung in einer temporären Variablen zu speichern. Eine derartige Konstruktion bietet sich an, wenn Sie zusätzliche Abfragen über den Ergebnissen ausführen müssen. Aufgrund dieses Verhaltens bezeichnet man das Schlüsselwort auch als Fortsetzungsklausel. Listing 2.12 zeigt ein Beispiel für einen C# 3.0-Abfrageausdruck, der die into-Klausel verwendet. var developersGroupedByLanguage = from d in developers group d by d.Language into developersGrouped select new { Language = developersGrouped.Key, DevelopersCount = developersGrouped.Count() }; foreach (var group in developersGroupedByLanguage) { Console.WriteLine ("Language {0} contains {1} developers", group.Language, group.DevelopersCount); } Listing 2.12 Ein C# 3.0-Abfrageausdruck, der die into-Klausel verwendet Die Klausel orderby Wie der Name vermuten lässt, dient die orderby-Klausel dazu, das Ergebnis einer Abfrage in auf- oder absteigender Reihenfolge zu sortieren. Die Sortierung lässt sich mit einem oder mehreren Schlüsseln durchführen, die verschiedene Sortierrichtungen kombinieren. Listing 2.13 zeigt ein Beispiel für eine Abfrage, um die von Kunden aufgegebenen Bestellungen herauszuziehen und nach EuroAmount zu sortieren. var ordersSortedByEuroAmount = from c in customers from o in c.Orders orderby o.EuroAmount select new { c.Name, o.IdOrder, o.EuroAmount }; Listing 2.13 Ein C# 3.0-Abfrageausdruck mit einer orderby-Klausel Paolo Pialorsi, Marco Russo: Datenbankprogrammierung mit Microsoft LINQ. Microsoft Press 2008 (ISBN 978-3-86645-428-6) 36 Kapitel 2: Grundlagen der LINQ-Syntax Die Beispielabfrage in Listing 2.14 wählt Bestellungen sortiert nach Kundenname (Name) und EuroAmount in absteigender Reihenfolge aus. var ordersSortedByCustomerAndEuroAmount = from c in customers from o in c.Orders orderby c.Name, o.EuroAmount descending select new { c.Name, o.IdOrder, o.EuroAmount }; Listing 2.14 Ein C# 3.0-Abfrageausdruck, der eine orderby-Klausel mit mehreren Sortierbedingungen verwendet Listing 2.15 zeigt die entsprechende Abfrage als Visual Basic 2008-Version. Dim ordersSortedByCustomerAndEuroAmount = _ From c In customers, _ o In c.Orders _ Order By c.Name, o.EuroAmount Descending _ Select c.Name, o.IdOrder, o.EuroAmount Listing 2.15 Ein Visual Basic 2008-Abfrageausdruck, der eine orderby-Klausel mit mehreren Sortierbedingungen verwendet Hier weisen beide Sprachen eine sehr ähnliche Syntax auf. Die Klausel join Mit dem Schlüsselwort join können Sie unterschiedliche Datenquellen verbinden, und zwar auf der Basis eines Members, der sich auf Gleichheit prüfen lässt. Die Klausel funktioniert ähnlich wie eine SQLGleichheitsverknüpfung. Es ist allerdings nicht möglich, die zu verknüpfenden Elemente mit Vergleichsoperatoren der Art größer als, kleiner als oder ungleich zu vergleichen. Die Gleichheitstests definieren Sie mit einem speziellen Schlüsselwort equals, das ein anderes Verhalten als der Operator == zeigt, weil die Position der Operanden eine Rolle spielt. Bei equals verwendet der linke Schlüssel die äußere Quellsequenz und der rechte Schlüssel die innere Quelle. Der Gültigkeitsbereich der äußeren Quelle beschränkt sich auf die linke Seite von equals und die innere Quellsequenz ist nur auf der rechten Seite gültig. In Pseudocode sieht dieses Konzept wie folgt aus: join-clause ::= join innerItem in innerSequence on outerKey equals innerKey Mithilfe der join-Klausel können Sie innere Verknüpfungen, Gruppenverknüpfungen und linke äußere Verknüpfungen definieren. Eine innere Verknüpfung gibt eine lineare Ergebniszuordnung der äußeren Datenquellenelemente zur entsprechenden inneren Datenquelle zurück. Dabei werden die äußeren Datenquellenelemente, die keine Entsprechung bei den inneren Datenquellenelementen haben, übersprungen. Listing 2.16 gibt eine einfache Abfrage mit einer inneren Verknüpfung zwischen Artikelkategorien und dazu gehörenden Produkten an. public class Category { public Int32 IdCategory { get; set; } public String Name { get; set; } } Paolo Pialorsi, Marco Russo: Datenbankprogrammierung mit Microsoft LINQ. Microsoft Press 2008 (ISBN 978-3-86645-428-6) 37 Abfrageschlüsselwörter public class Product { public String IdProduct { get; set; } public Int32 IdCategory { get; set; } public String Description { get; set; } } // ... Code weggelassen ... Category[] categories = new Category[] { new Category { IdCategory = 1, Name = "Pasta"}, new Category { IdCategory = 2, Name = "Beverages"}, new Category { IdCategory = 3, Name = "Other food"}, }; Product[] products = new Product[] { new Product { IdProduct = "PASTA01", IdCategory new Product { IdProduct = "PASTA02", IdCategory new Product { IdProduct = "PASTA03", IdCategory new Product { IdProduct = "BEV01", IdCategory = new Product { IdProduct = "BEV02", IdCategory = }; = 1, Description = 1, Description = 1, Description 2, Description = 2, Description = = "Tortellini" }, = "Spaghetti" }, = "Fusilli" }, "Water" }, "Orange Juice" }, var categoriesAndProducts = from c in categories join p in products on c.IdCategory equals p.IdCategory select new { c.IdCategory, CategoryName = c.Name, Product = p.Description }; foreach (var item in categoriesAndProducts) { Console.WriteLine(item); } Listing 2.16 Ein C# 3.0-Abfrageausdruck mit einer inneren Verknüpfung Diese Ausgabe dieses Codefragments sieht etwa wie folgt aus. Beachten Sie, dass die Kategorie Other food fehlt, weil sie keine Artikel enthält. { { { { { IdCategory IdCategory IdCategory IdCategory IdCategory = = = = = 1, 1, 1, 2, 2, CategoryName CategoryName CategoryName CategoryName CategoryName = = = = = Pasta, Product = Tortellini } Pasta, Product = Spaghetti } Pasta, Product = Fusilli } Beverages, Product = Water } Beverages, Product = Orange Juice } Eine Gruppenverknüpfung liefert eine hierarchische Ergebnismenge, bei der die inneren Sequenzelemente mit ihren korrespondierenden äußeren Sequenzelementen gruppiert werden. Fehlen für ein Element der äußeren Sequenz korrespondierende innere Sequenzelemente, wird das äußere Element mit einem leeren Array verknüpft. Eine Gruppenverknüpfung besitzt aufgrund ihres hierarchischen Ergebnisses keine Entsprechung in der SQL-Syntax. Listing 2.17 zeigt ein Beispiel für eine derartige Abfrage. (In Kapitel 3 lernen Sie eine erweiterte Form dieses Abfragetyps kennen.) Paolo Pialorsi, Marco Russo: Datenbankprogrammierung mit Microsoft LINQ. Microsoft Press 2008 (ISBN 978-3-86645-428-6) 38 Kapitel 2: Grundlagen der LINQ-Syntax var categoriesAndProducts = from c in categories join p in products on c.IdCategory equals p.IdCategory into productsByCategory select new { c.IdCategory, CategoryName = c.Name, Products = productsByCategory }; foreach (var category in categoriesAndProducts) { Console.WriteLine("{0} - {1}", category.IdCategory, category.CategoryName); foreach (var product in category.Products) { Console.WriteLine("\t{0}", product.Description); } } Listing 2.17 Ein C# 3.0-Abfrageausdruck mit einer Gruppenverknüpfung Dieses Mal ist die Kategorie Other food in der Ausgabe vorhanden, auch wenn sie leer ist: 1 – Pasta Tortellini Spaghetti Fusilli 2 – Beverages Water Orange Juice 3 - Other food In Visual Basic 2008 steht das spezielle Schlüsselwort Group Join zur Verfügung, um Gruppenverbindungen in Abfrageausdrücken zu definieren. Eine linke äußere Verknüpfung liefert eine lineare Ergebnismenge, die alle äußeren Quellelemente umfasst, selbst wenn das entsprechende innere Quellelement fehlt. Um dieses Ergebnis zu erzeugen, brauchen Sie die Erweiterungsmethode DefaultIfEmpty, die einen Standardwert zurückgibt, falls ein Datenquellenwert leer ist. Auf diese und viele andere Erweiterungsmethoden geht Kapitel 3 näher ein. Listing 2.18 zeigt ein Beispiel für diese Syntax. var categoriesAndProducts = from c in categories join p in products on c.IdCategory equals p.IdCategory into productsByCategory from pc in productsByCategory.DefaultIfEmpty( new Product { IdProduct = String.Empty, Description = String.Empty, IdCategory = 0}) select new { c.IdCategory, CategoryName = c.Name, Product = pc.Description }; Paolo Pialorsi, Marco Russo: Datenbankprogrammierung mit Microsoft LINQ. Microsoft Press 2008 (ISBN 978-3-86645-428-6) 39 Abfrageschlüsselwörter foreach (var item in categoriesAndProducts) { Console.WriteLine(item); } Listing 2.18 Ein C# 3.0-Abfrageausdruck mit einer linken äußeren Verknüpfung Dieses Beispiel erzeugt die folgende Ausgabe auf der Konsole: { { { { { { IdCategory IdCategory IdCategory IdCategory IdCategory IdCategory = = = = = = 1, 1, 1, 2, 2, 3, CategoryName CategoryName CategoryName CategoryName CategoryName CategoryName = = = = = = Pasta, Product = Tortellini } Pasta, Product = Spaghetti } Pasta, Product = Fusilli } Beverages, Product = Water } Beverages, Product = Orange Juice } Other food, Product = } Die Kategorie Other food ist mit einem leeren Artikel vertreten. Diesen Wert stellt die Erweiterungsmethode DefaultIfEmpty bereit. Schließlich sei zur join-Klausel erwähnt, dass Sie Elemente mithilfe von zusammengesetzten Schlüsseln vergleichen können. Verwenden Sie einfach anonyme Typen, wie es für das Schlüsselwort group gezeigt wurde. Wenn zum Beispiel ein zusammengesetzter Schlüssel in Category aus IdCategory und Year besteht, können Sie die folgende Anweisung schreiben, wobei in der equals-Bedingung ein anonymer Typ verwendet wird. from c in categories join p in products on new { c.IdCategory, c.Year } equals new { p.IdCategory, p.Year } into productsByCategory Wie Sie in diesem Kapitel bereits gesehen haben, können Sie die Ergebnisse von Verknüpfungen auch mit verschachtelten from-Klauseln erhalten. Dieser Ansatz bietet sich an, wenn Sie Abfragen mit NichtGleichheitsverknüpfungen (Non-Equijoins) definieren müssen. Die Syntax von Visual Basic 2008 ähnelt der von C# 3.0, bietet aber auch einige Shortcuts, um Verknüpfungen schneller zu definieren. Es lassen sich implizite Verknüpfungsanweisungen definieren, indem mehrere In-Klauseln in der From-Anweisung verwendet und Gleichheitsbedingungen mit einer Where-Klausel definiert werden. Ein Beispiel für diese Syntax ist in Listing 2.19 zu sehen. Dim categoriesAndProducts = _ From c In categories, p In products _ Where c.IdCategory = p.IdCategory _ Select c.IdCategory, CategoryName = c.Name, Product = p.Description For Each item In categoriesAndProducts Console.WriteLine(item) Next Listing 2.19 Eine implizite Verknüpfungsanweisung in Visual Basic 2008 Paolo Pialorsi, Marco Russo: Datenbankprogrammierung mit Microsoft LINQ. Microsoft Press 2008 (ISBN 978-3-86645-428-6) 40 Kapitel 2: Grundlagen der LINQ-Syntax Listing 2.20 zeigt die gleiche Abfrage, die hier aber mit der normalen expliziten join-Syntax formuliert ist. Dim categoriesAndProducts = _ From c In categories Join p In products _ On p.IdCategory Equals c.IdCategory _ Select c.IdCategory, CategoryName = c.Name, Product = p.Description Listing 2.20 Eine explizite Verknüpfungsanweisung in Visual Basic 2008 Beachten Sie, dass in Visual Basic 2008 die Reihenfolge der Elemente im Gleichheitstest keine Rolle spielt, weil der Compiler sie in eigener Regie anordnet. Dadurch ist die Abfragesyntax etwas »entspannter« als sie vom herkömmlichen relationalen SQL bekannt ist. Die Klausel let Mit der let-Klausel ist es möglich, das Ergebnis eines Unterausdrucks in einer Variablen zu speichern, die sich dann an anderer Stelle in der Abfrage verwenden lässt. Diese Klausel ist nützlich, wenn Sie den gleichen Ausdruck mehrmals in derselben Abfrage verwenden müssen und ihn nicht jedes Mal neu definieren möchten. Mit der let-Klausel definieren Sie für diesen Ausdruck eine Bereichsvariable und verweisen darauf innerhalb der Abfrage. Nachdem Sie die in der let-Klausel definierte Bereichsvariable zugewiesen haben, können Sie sie nicht mehr ändern. Wenn jedoch die Bereichsvariable einen abfragbaren Typ speichert, kann sie abgefragt werden. Das Beispiel in Listing 2.21 wendet diese Klausel an, um dieselben Artikelkategorien mit der Anzahl ihrer Artikel auszuwählen und nach der Anzahl selbst zu sortieren. var categoriesByProductsNumberQuery = from c in categories join p in products on c.IdCategory equals p.IdCategory into productsByCategory let ProductsCount = productsByCategory.Count() orderby ProductsCount select new { c.IdCategory, ProductsCount}; foreach (var item in categoriesByProductsNumberQuery) { Console.WriteLine(item); } Listing 2.21 Beispiel in C# 3.0 für die let-Klausel Die Ausgabe des obigen Codefragments sieht wie folgt aus: { IdCategory = 3, ProductsCount = 0 } { IdCategory = 2, ProductsCount = 2 } { IdCategory = 1, ProductsCount = 3 } Visual Basic 2008 verwendet eine ähnliche Syntax wie C# 3.0 und erlaubt es ebenfalls, mehrere durch Komma getrennte Aliasnamen innerhalb derselben let-Klausel zu definieren. Paolo Pialorsi, Marco Russo: Datenbankprogrammierung mit Microsoft LINQ. Microsoft Press 2008 (ISBN 978-3-86645-428-6) Verzögerte Auswertung von Abfragen und Auflösung von Erweiterungsmethoden 41 Zusätzliche Schlüsselwörter in Visual Basic 2008 Visual Basic 2008 umfasst zusätzliche Schlüsselwörter für Abfrageausdrücke, die in C# 3.0 nur über Erweiterungsmethoden verfügbar sind. Die folgende Liste beschreibt diese Schlüsselwörter: Aggregate wendet eine Aggregatfunktion auf eine Datenquelle an. Mit diesem Schlüsselwort können Sie statt mit einer From-Klausel eine neue Abfrage beginnen. Distinct beseitigt doppelte Werte in Abfrageergebnissen. Skip überspringt die ersten n Elemente eines Abfrageergebnisses. Skip While überspringt von einem Abfrageergebnis die ersten Elemente, die einem angegebenen Prädikat entsprechen. Take nimmt die ersten n Elemente eines Abfrageergebnisses. Take While nimmt von einem Abfrageergebnis die ersten Elemente, die einem angegebenen Prädikat entsprechen. Die Schlüsselwörter Skip und Take bzw. Skip While und Take While können zusammen verwendet werden, um die Abfrageergebnisse zu paginieren. Kapitel 3 zeigt einige Beispiele zu diesem Thema. Mehr zur Abfragesyntax Mittlerweile kennen Sie alle Abfrageschlüsselwörter, die über die Programmiersprachen verfügbar sind. Denken Sie dabei daran, dass jeder Abfrageausdruck vom Sprachcompiler in einen Aufruf der entsprechenden Erweiterungsmethoden konvertiert wird. Wenn Sie eine Datenquelle mithilfe von LINQ abfragen möchten und für eine bestimmte Operation in einem Abfrageausdruck kein Schlüsselwort vorhanden ist, können Sie native oder benutzerdefinierte Erweiterungsmethoden in Verbindung mit der Abfrageausdruckssyntax direkt aufrufen. Falls Sie ausschließlich Erweiterungsmethoden verwenden (wie es in Listing 2.3 zu sehen ist), spricht man von Methodensyntax. Wird die Abfragesyntax in Verbindung mit Erweiterungsmethoden eingesetzt (wie in Listing 2.17 gezeigt), bezeichnet man das Ergebnis als gemischte Abfragesyntax. Verzögerte Auswertung von Abfragen und Auflösung von Erweiterungsmethoden In diesem Abschnitt geht es um zwei Verhaltensweisen eines Abfrageausdrucks: verzögerte Abfrageauswertung und Auflösung von Erweiterungsmethoden. Beide Konzepte sind für alle LINQ-Implementierungen wichtig. Verzögerte Abfrageauswertung Ein Abfrageausdruck wird nicht ausgewertet, wenn er definiert wird, sondern erst, wenn er verwendet wird. Sehen Sie sich dazu das Beispiel in Listing 2.22 an. Paolo Pialorsi, Marco Russo: Datenbankprogrammierung mit Microsoft LINQ. Microsoft Press 2008 (ISBN 978-3-86645-428-6) 42 Kapitel 2: Grundlagen der LINQ-Syntax List<Developer> developers new Developer { Name = new Developer { Name = new Developer { Name = }); = new List<Developer>(new Developer[] { "Paolo", Language = "C#", Age = 32 }, "Marco", Language = "C#", Age = 37}, "Frank", Language = "VB.NET", Age = 48 }, var query = from d in developers where d.Language == "C#" select new { d.Name, d.Age }; Console.WriteLine("There are {0} C# developers.", query.Count()); Listing 2.22 Beispiel für eine LINQ-Abfrage über einer Gruppe von Entwicklern Dieser Code deklariert eine sehr einfache Abfrage, die lediglich zwei Elemente enthält. Das können Sie am Code ablesen, der die Liste der Entwickler deklariert, oder anhand der Konsolenausgabe des Codes, der die Erweiterungsmethode Count aufruft: There are 2 C# developers. Nehmen Sie nun an, dass Sie den Inhalt der Quellsequenz ändern möchten, indem Sie eine neue DeveloperInstanz hinzufügen – nachdem die Variable query definiert worden ist (siehe Listing 2.23). developers.Add(new Developer { Name = "Roberto", Language = "C#", Age = 35 }); Console.WriteLine("There are {0} C# developers.", query.Count()); Listing 2.23 Beispielcode, um die Gruppe der abzufragenden Entwickler zu ändern Wenn Sie die Variable query erneut aufzählen oder einfach ihre Elementanzahl überprüfen, wie es in Listing 2.23 geschieht, nachdem ein neuer Entwickler hinzugefügt wurde, erhalten Sie als Ergebnis drei Elemente. Der neue Entwickler ist jetzt im Ergebnis enthalten, obwohl er erst nach der Definition von query hinzugefügt wurde. Dieses Verhalten ist darin begründet, dass ein Abfrageausdruck vom logischen Standpunkt her eine Art von »Abfrageplan« beschreibt. Ausgeführt wird er erst, wenn er verwendet wird, und er wird immer wieder ausgeführt, wenn Sie ihn aufrufen. Manche LINQ-Implementierungen – wie zum Beispiel LINQ to Objects – implementieren dieses Verhalten über Delegaten. Andere Implementierungen – wie zum Beispiel LINQ to SQL – verwenden Ausdrucksbaumstrukturen, die sich auf die Schnittstelle IQueryable<T> stützen. Diese so genannte verzögerte Abfrageauswertung ist ein fundamentales Konzept in LINQ, und zwar unabhängig von der verwendeten LINQ-Implementierung. Verzögerte Abfrageauswertung ist nützlich, weil Sie Abfragen einmalig definieren und mehrmals anwenden können: Wenn sich die Quellsequenz geändert hat, wird das Ergebnis immer mit dem neuesten Inhalt aktualisiert. Betrachten Sie aber eine Situation, in der Sie einen Snapshot des Ergebnisses zu einem bestimmten Sicherungspunkt erstellen möchten, um es mehrmals zu verwenden und die erneute Ausführung aus Performancegründen zu vermeiden, oder um unabhängig von Änderungen an der Quellsequenz zu sein. In diesem Fall müssen Sie eine Kopie des Ergebnisses erstellen. Dazu können Sie einen Satz so genann- Paolo Pialorsi, Marco Russo: Datenbankprogrammierung mit Microsoft LINQ. Microsoft Press 2008 (ISBN 978-3-86645-428-6) Verzögerte Auswertung von Abfragen und Auflösung von Erweiterungsmethoden 43 ter Konvertierungsoperatoren (wie zum Beispiel ToArray, ToList, ToDictionary und ToLookup) verwenden, die genau für diesen Zweck ausgelegt sind. Kapitel 3 beschäftigt sich ausführlich mit den Konvertierungsoperatoren. Auflösung von Erweiterungsmethoden Die Auflösung von Erweiterungsmethoden gehört zu den wichtigsten Konzepten, die Sie verstehen müssen, um LINQ zu beherrschen. Sehen Sie sich den Code in Listing 2.24 an. Er definiert eine benutzerdefinierte Liste vom Typ Developer (namens Developers) und eine Klasse DevelopersExtension mit einer Erweiterungsmethode Where, die speziell für Instanzen des Typs Developers konzipiert ist. public sealed class Developers : List<Developer> { public Developers(IEnumerable<Developer> items) : base(items) { } } public static class DevelopersExtension { public static IEnumerable<Developer> Where( this Developers source, Func<Developer, bool> predicate) { Console.WriteLine("Invoked Where extension method for Developers"); return (source.AsEnumerable().Where(predicate)); } public static IEnumerable<Developer> Where( this Developers source, Func<Developer, int, bool> predicate) { Console.WriteLine("Invoked Where extension method for Developers"); return (source.AsEnumerable().Where(predicate)); } } Listing 2.24 Beispielcode, um die Gruppe der abzufragenden Entwickler zu modifizieren Die einzige spezielle Aufgabe in den benutzerdefinierten Where-Erweiterungsmethoden besteht darin, mit einer Ausgabe auf der Konsole anzuzeigen, dass die Methoden aufgerufen wurden. Danach wird die Anforderung an die Where-Erweiterungsmethoden übergeben, die für jede Standardinstanz des Typs IEnumerable<T> definiert sind, wobei die Quelle mit einer Methode AsEnumerable (auf die Kapitel 3 eingeht) konvertiert wird. Wenn Sie das schon bekannte developers-Array verwenden, ist das Verhalten der Abfrage in Listing 2.25 recht interessant. Developers developers = new Developers(new Developer[] { new Developer { Name = "Paolo", Language = "C#", Age = 32 }, new Developer { Name = "Marco", Language = "C#", Age = 37}, new Developer { Name = "Frank", Language = "VB.NET", Age = 48 }, }); Paolo Pialorsi, Marco Russo: Datenbankprogrammierung mit Microsoft LINQ. Microsoft Press 2008 (ISBN 978-3-86645-428-6) 44 Kapitel 2: Grundlagen der LINQ-Syntax var query = from d in developers where d.Language == "C#" select d; Console.WriteLine("There are {0} C# developers.", query.Count()); Listing 2.25 Ein Abfrageausdruck über einer benutzerdefinierten Liste vom Typ Developers Der Compiler konvertiert den Abfrageausdruck in den folgenden Code, wie Sie es bereits weiter vorn in diesem Kapitel gesehen haben: Var expert = developers .Where (d => d.Language == "C#") .Select(d => d); Durch die Anwesenheit der Klasse DevelopersExtension handelt es sich bei der Erweiterungsmethode Where um diejenige, die von DevelopersExtension definiert wird, und nicht um die universelle Version, die in System.Linq.Enumerable definiert ist. (Um als Containerklasse für Erweiterungsmethoden zu gelten, muss die Klasse DevelopersExtension als static deklariert und im aktuellen – oder in einem per using-Direktiven in den aktiven Namespace eingebundenen – Namespace definiert sein.) Der vom Compiler generierte Code, der die Erweiterungsmethoden auflöst, sieht wie folgt aus: var expr = Enumerable.Select( DevelopersExtension.Where( developers, d => d.Language == "C#"), d=> d ); Letztlich werden immer die statischen Methoden einer statischen Klasse aufgerufen, doch die erforderliche Syntax ist mit Erweiterungsmethoden kompakter und intuitiver als die Verwendung der weitschweifigeren expliziten statischen Methodenaufrufe. Jetzt erfahren Sie die wirkliche Leistung von LINQ. Mithilfe von Erweiterungsmethoden konnten Sie benutzerdefinierte Verhaltensweisen für spezifische Typen definieren. In den folgenden Kapiteln werden LINQ to SQL, LINQ to XML und andere Implementierungen von LINQ erläutert. Dabei handelt es sich dank der von den Compilern realisierten Auflösung von Erweiterungsmethoden lediglich um spezifische Implementierungen von Abfrageoperatoren. An diesem Punkt sieht alles prima aus. Stellen Sie sich aber nun vor, dass Sie die benutzerdefinierte Liste vom Typ Developers mit der standardmäßigen und nicht mit der spezialisierten Where-Erweiterungsmethode abfragen müssen. Die benutzerdefinierte Liste sollten Sie in eine verallgemeinerte Liste konvertieren, um die vom Compiler vorgenommene Auflösung der Erweiterungsmethode umzuleiten. Auch dieses Szenario kann von den Konvertierungsoperatoren – die in Kapitel 3 behandelt werden – profitieren. Paolo Pialorsi, Marco Russo: Datenbankprogrammierung mit Microsoft LINQ. Microsoft Press 2008 (ISBN 978-3-86645-428-6) Abschließende Gedanken zu LINQ-Abfragen 45 Abschließende Gedanken zu LINQ-Abfragen In diesem Abschnitt geht es um einige weitere Details, die das Degenerieren von Abfrageausdrücken und die Ausnahmebehandlung betreffen. Abfrageausdrücke degenerieren Manchmal muss man die Elemente einer Datenquelle durchlaufen, ohne dass Filtern, Sortieren, Gruppieren oder benutzerdefiniertes Projizieren erforderlich ist. Sehen Sie sich zum Beispiel die in Listing 2.26 angegebene Abfrage an. Developer[] developers = new Developer[] { ... }; var query = from d in developers select d; foreach (var developer in query) { Console.WriteLine(developer.Name); } Listing 2.26 Ein degenerierter Abfrageausdruck über einer Liste des Typs Developers In diesem Beispiel durchläuft der Code einfach die Datenquelle, sodass sich die Frage stellt, weshalb die Datenquelle nicht direkt verwendet wird, wie es in Listing 2.27 geschieht. Developer[] developers = new Developer[] { ... }; foreach (var developer in developers) { Console.WriteLine(developer.Name); } Listing 2.27 Iteration über einer Liste des Typs Developers Augenscheinlich sind die Ergebnisse von Listing 2.26 und Listing 2.27 gleich. Allerdings stellt der Abfrageausdruck in Listing 2.26 sicher, dass die benutzerdefinierte Methode aufgerufen wird, wenn eine spezifische Select-Erweiterungsmethode für die Datenquelle existiert, und das Ergebnis mit dem einer Übersetzung des Abfrageausdrucks in seine entsprechende Methodensyntax übereinstimmt. Eine Abfrage, die einfach ein Ergebnis liefert, das der ursprünglichen Datenquelle gleicht (und somit trivial oder nutzlos scheint), wird als degenerierter Abfrageausdruck bezeichnet. Wenn Sie andererseits die Datenquelle direkt durchlaufen (wie in Listing 2.27), umgehen Sie den Aufruf irgendeiner benutzerdefinierten Select-Erweiterungsmethode und das korrekte Verhalten ist nicht garantiert, sofern Sie nicht ausdrücklich die Datenquelle ohne Verwendung von LINQ durchlaufen möchten. Paolo Pialorsi, Marco Russo: Datenbankprogrammierung mit Microsoft LINQ. Microsoft Press 2008 (ISBN 978-3-86645-428-6) 46 Kapitel 2: Grundlagen der LINQ-Syntax Ausnahmebehandlung Abfrageausdrücke können innerhalb ihrer Definition auf externe Methoden verweisen. Die Methoden müssen nicht immer fehlerfrei ausgeführt werden. Die in Listing 2.28 definierte Abfrage ruft die Methode DoSomething über jedem Element der Datenquelle auf. static Boolean DoSomething(Developer dev) { if (dev.Age > 40) throw new ArgumentOutOfRangeException("dev"); return (dev.Language == "C#"); } static void Main() { Developer[] developers = new Developer[] { ... new Developer { Name = "Frank", Language = "VB.NET", Age = 48 }, }; var query = from d in developers let SomethingResult = DoSomething(d) select new { d.Name, SomethingResult }; foreach (var item in query) { Console.WriteLine(item); } } Listing 2.28 Ein C# 3.0-Abfrageausdruck, der auf einer externen Methode basiert, die eine fiktive Ausnahme auslöst Die Methode DoSomething löst eine fiktive Ausnahme aus für jeden Entwickler, der älter als 40 Jahre ist. Der Aufruf dieser Methode erfolgt innerhalb der Abfrage. Erreicht die Schleife während der Abfrageausführung das Element mit dem Entwickler Frank, der 48 Jahre alt ist, löst die benutzerdefinierte Methode eine Ausnahme aus. Überlegen Sie genau, ob Sie benutzerdefinierte Methoden in Abfragedefinitionen aufrufen möchten, weil dies eine gefährliche Angelegenheit ist. Davon können Sie sich beim Ausführen dieses Beispielcodes selbst überzeugen. Wenn Sie aber externe Methoden aufrufen wollen, ist es am besten, die Auflistung des Abfrageergebnisses in einen try...catch-Block einzuhüllen. Wie Sie eben im Abschnitt »Verzögerte Abfrageauswertung« gesehen haben, wird ein Abfrageausdruck jedes Mal ausgewertet, wenn er aufgezählt wird, und nicht, wenn er definiert wird. Dementsprechend zeigt Listing 2.29, wie Sie den Code von Listing 2.28 in der richtigen Weise schreiben. Developer[] developers = new Developer[] { ... new Developer { Name = "Frank", Language = "VB.NET", Age = 48 }, }; Paolo Pialorsi, Marco Russo: Datenbankprogrammierung mit Microsoft LINQ. Microsoft Press 2008 (ISBN 978-3-86645-428-6) Abschließende Gedanken zu LINQ-Abfragen 47 var query = from d in developers let SomethingResult = DoSomething(d) select new { d.Name, SomethingResult }; try { foreach (var item in query) { Console.WriteLine(item); } } catch (ArgumentOutOfRangeException e) { Console.WriteLine(e.Message); } Listing 2.29 Ein C# 3.0-Abfrageausdruck mit Ausnahmebehandlung Im Allgemeinen ist es nutzlos, die Definition eines Abfrageausdrucks in einen try...catch-Block einzuschließen. Darüber hinaus sollten Sie aus dem gleichen Grund vermeiden, die Ergebnisse der Methoden oder Konstruktoren direkt als Datenquellen für einen Abfrageausdruck zu verwenden. Weisen Sie stattdessen ihre Ergebnisse Instanzvariablen zu und hüllen Sie die Variablenzuweisung in einen try...catch-Block ein, wie es in Listing 2.30 geschehen ist. static void queryWithExceptionHandledInDataSourceDefinition() { Developer[] developers = null; try { developers = createDevelopersDataSource(); } catch (InvalidOperationException e) { // Annehmen, dass die Methode createDevelopersDataSource // im Fehlerfall eine InvalidOperationException auslöst // Ausnahme behandeln ... Console.WriteLine(e.Message); } if (developers != null) { var query = from d in developers let SomethingResult = DoSomething(d) select new { d.Name, SomethingResult }; try { foreach (var item in query) { Console.WriteLine(item); } } catch (ArgumentOutOfRangeException e) { Console.WriteLine(e.Message); } } } Paolo Pialorsi, Marco Russo: Datenbankprogrammierung mit Microsoft LINQ. Microsoft Press 2008 (ISBN 978-3-86645-428-6) 48 Kapitel 2: Grundlagen der LINQ-Syntax private static Developer[] createDevelopersDataSource() { // Fiktive InvalidOperationException ausgelöst throw new InvalidOperationException(); } Listing 2.30 Ein C# 3.0-Abfrageausdruck mit Ausnahmebehandlung in der Deklaration lokaler Variablen Zusammenfassung In diesem Kapitel wurden die Prinzipien der Abfrageausdrücke und ihre unterschiedlichen Syntaxvarianten (Abfragesyntax, Methodensyntax und gemischte Syntax) sowie alle wichtigen Schlüsselwörter, die in C# 3.0 und Visual Basic 2008 verfügbar sind, behandelt. Dabei haben Sie zwei wichtige LINQ-Features kennen gelernt: verzögerte Auswertung von Abfragen und Auflösung von Erweiterungsmethoden. Außerdem hat dieses Kapitel Beispiele von degenerierten Abfrageausdrücken gezeigt und erläutert, wie Sie Ausnahmen bei der Auflistung von Abfrageausdrücken behandeln. Im nächsten Kapitel geht es dann detailliert um LINQ to Objects. Paolo Pialorsi, Marco Russo: Datenbankprogrammierung mit Microsoft LINQ. Microsoft Press 2008 (ISBN 978-3-86645-428-6) Einführung In diesem Abschnitt: Über dieses Buch Systemanforderungen Die Companion-Website Support für dieses Buch XX XXII XXII XXII XIX Paolo Pialorsi, Marco Russo: Datenbankprogrammierung mit Microsoft LINQ. Microsoft Press 2008 (ISBN 978-3-86645-428-6) XX Einführung Dieses Buch behandelt die sprachintegrierte Abfrage (Language Integrated Query, LINQ) umfassend und tiefgehend. Es soll Ihnen in erster Linie ein geschlossenes Bild vermitteln, was LINQ ist und was Sie mit LINQ tun bzw. nicht tun können. Das Buch richtet sich an .NET-Entwickler, die in Microsoft .NET 2.0 sattelfest sind und sich die Frage stellen, ob sie ihr Fachwissen auf Microsoft .NET 3.5 aktualisieren sollten. Um mit LINQ zu arbeiten, müssen Sie auf Ihrem Entwicklungscomputer Microsoft .NET Framework 3.5 und Microsoft Visual Studio 2008 installieren. Das Buch wurde auf Basis der RTM (Released to Market)-Edition von LINQ und Microsoft .NET 3.5 geschrieben. Allerdings befinden sich bestimmte Komplexe wie zum Beispiel LINQ to Entities, das ADO.NET Entity Framework und Parallel LINQ immer noch im Betastadium. Gegenüber dem derzeitigen Release könnten beim endgültigen Release einige Features dieser Technologien geändert, entfernt oder hinzugefügt werden. Auf der Website http://www.programminglinq.com/ verwalten wir eine Änderungsliste, einen Revisionsverlauf, Korrekturen und ein Blog zu LINQ im Allgemeinen und zu diesem Buch im Besonderen. Außerdem finden Sie auf der Webseite http://www.programminglinq.com/booklinks.aspx alle im Buch genannten URLs (sortiert nach der Seitennummer), sodass Sie diese URLs nicht manuell kopieren müssen. Über dieses Buch Dieses Buch ist in fünf Teile mit insgesamt 18 Kapiteln und 3 Anhängen gegliedert. Als Einsteiger in C# 3.0 und/oder Visual Basic 2008 sollten Sie zunächst Anhang B bzw. Anhang C lesen. Diese Anhänge beschäftigen sich mit den neuen Features, die diese Sprachen einführen, um vollständige Unterstützung für LINQ zu bieten. Sind Sie bereits mit diesen neuen Sprachversionen vertraut, können Sie diese Anhänge als Referenz nutzen, falls Sie bei der Syntax in Bezug auf LINQ unsicher sind. In unseren Beispielen verwenden wir hauptsächlich C#, doch sind fast alle vorgestellten LINQ-Features auch in Visual Basic 2008 verfügbar. Wenn es zweckmäßig ist, verwenden wir Visual Basic 2008, weil es einige Features zu bieten hat, die in C# 3.0 nicht zur Verfügung stehen. Der erste Teil des Buchs »Grundlagen von LINQ« führt LINQ ein, erläutert die Syntax und liefert alle erforderlichen Informationen, damit Sie LINQ mit speicherinternen Objekten verwenden können. Mit LINQ to Objects sollten Sie sich als Erstes vertraut machen, denn viele Features davon begegnen Ihnen in den anderen LINQ-Implementierungen, die in diesem Buch beschrieben werden. Die ersten drei Kapitel des ersten Teils sollten Sie auf jeden Fall lesen. Der zweite Teil dieses Buchs, »LINQ und relationale Daten«, ist allen LINQ-Implementierungen gewidmet, die Zugriff auf relationale Datenspeicher bieten. Die LINQ to SQL-Implementierung gliedert sich in drei Kapitel. Kapitel 4 »LINQ to SQL: Daten abfragen« erläutert die Grundlagen, wie Sie relationale Daten den LINQ-Entitäten zuordnen und LINQ-Abfragen erstellen, die in SQL-Abfragen transformiert werden. In Kapitel 5 »LINQ to SQL: Daten verwalten« erfahren Sie, wie Sie mit den Entitäten von LINQ to SQL Änderungen an Daten behandeln, die aus einer Datenbank extrahiert wurden. Kapitel 6 »Tools für LINQ to SQL« ist ein Führer zu den Tools, die Sie dabei unterstützen, Datenmodelle für LINQ to SQL zu definieren. Wenn Sie LINQ to SQL in Ihren Anwendungen einsetzen möchten, lesen Sie am besten alle entsprechenden Kapitel. Paolo Pialorsi, Marco Russo: Datenbankprogrammierung mit Microsoft LINQ. Microsoft Press 2008 (ISBN 978-3-86645-428-6) Über dieses Buch XXI Kapitel 7 »LINQ to DataSet« beschäftigt sich mit der Implementierung von LINQ, die sich auf ADO.NETDataSets richtet. Wenn Sie in einer Anwendung DataSets verwenden, erfahren Sie in diesem Kapitel, wie Sie LINQ integrieren, oder zumindest, wie Sie stufenweise von DataSets zu einem Domänenmodell übergehen, das mit LINQ to SQL oder LINQ to Entitites behandelt wird. Kapitel 8 »LINQ to Entities« beschreibt die LINQ-Implementierung, die den Zugriff auf das ADO.NET Entity Framework einhüllt. Dieses Kapitel sollten Sie nach dem LINQ to SQL-Kapitel lesen, weil auf die Konzepte, die bei den beiden Implementierungen ähnlich sind, später wieder Bezug genommen wird. In diesem Kapitel gehen wir davon aus, dass Sie über das ADO.NET Entity Framework Bescheid wissen. Falls Sie nicht über genügend Erfahrung verfügen, können Sie sich zuerst mit dem entsprechenden Anhang befassen. Der dritte Teil »LINQ und XML« besteht aus zwei LINQ to XML-Kapiteln: Kapitel 9 »LINQ to XML: Infoset verwalten« und Kapitel 10 »LINQ to XML: Knoten abfragen«. Lesen Sie diese Kapitel am besten zuerst, bevor Sie Code entwickeln, der in irgendeiner Form Daten in XML liest oder manipuliert. Die komplexesten Themen des Buchs sind im vierten Teil »LINQ erweitert« versammelt. In Kapitel 11 »Ausdrucksbaumstrukturen intern« lernen Sie, wie Sie eine Ausdrucksbaumstruktur verarbeiten, erstellen und lesen. Kapitel 12 »LINQ erweitern« erläutert, wie Sie LINQ erweitern, indem Sie eigene Datenstrukturen erstellen, einen vorhandenen Dienst einhüllen und schließlich einen benutzerdefinierten LINQAnbieter erstellen. Eine LINQ-Schnittstelle zum Parallel Framework für .NET wird in Kapitel 13 »LINQ parallel« beschrieben. Schließlich bietet Kapitel 14 »Andere LINQ-Implementierungen« einen Überblick über die wichtigsten LINQ-Komponenten, die von Drittanbietern zur Verfügung stehen. Die einzelnen Kapitel in diesem Teil können Sie unabhängig von den anderen lesen. Lediglich Kapitel 12 verweist auf ein anderes Kapitel in diesem Abschnitt, und zwar auf Kapitel 11. Der fünfte Teil »LINQ im Einsatz« ist der Verwendung von LINQ in unterschiedlichen Szenarios einer verteilten Anwendung gewidmet. Kapitel 15 »LINQ in einer Multitier-Lösung« dürfte für jeden Leser interessant sein, weil es sich um ein sehr architekturbezogenes Kapitel handelt, das Ihnen dabei hilft, die richtigen Entwurfsentscheidungen für Ihre Anwendungen zu treffen. Die Kapitel 16, 17 und 18 präsentieren relevante Informationen über die Verwendung von LINQ mit vorhandenen Bibliotheken wie zum Beispiel ASP.NET, Windows Presentation Foundation, Silverlight und Windows Communication Foundation. Wir empfehlen, dass Sie Kapitel 15 lesen, bevor Sie sich mit den Details der spezifischen Bibliotheken befassen. Von den Kapiteln 16, 17 und 18 können Sie auch ein oder mehrere Kapitel überspringen, wenn Sie die jeweilige Technologie nicht verwenden. Zusätzliche Inhalte online finden Wenn neue oder aktualisierte Zusatzinformationen zu Ihrem Buch verfügbar werden, stellen wir sie auf der Microsoft Press Online Developer Tools-Website online. Dabei wird es sich unter anderem um aktualisierte Buchinhalte, Artikel, Links zu begleitenden Inhalten, Errata und Beispielkapitel handeln. Die Website ist unter http://www.microsoft.com/learning/books/online/developer zu finden und wird regelmäßig aktualisiert. Paolo Pialorsi, Marco Russo: Datenbankprogrammierung mit Microsoft LINQ. Microsoft Press 2008 (ISBN 978-3-86645-428-6) XXII Einführung Systemanforderungen Damit Sie mit LINQ arbeiten und den von uns bereitgestellten Beispielcode verwalten können, sind folgende Anforderungen zu erfüllen: Unterstützte Betriebssysteme: Microsoft Windows Server 2003, Windows Server 2008, Windows Vista, Windows XP mit Service Pack 2 Microsoft Visual Studio 2008 Die Companion-Website Zu diesem Buch gehört eine Companion-Website, von der Sie den im Buch verwendeten Code herunterladen können. Der Code ist nach Themen organisiert. Die Adresse der Companion-Website lautet: http://www.microsoft.com/mspress/companion/9780735624009. Alternativ können Sie die Beispieldateien auch unter http://www.microsoft-press.de/support.asp herunterladen. Geben Sie dazu die Nummer 428 in das untere Eingabefeld auf dieser Webseite ein und klicken Sie auf Suchen. Support für dieses Buch Es wurden große Anstrengungen unternommen, um die Fehlerfreiheit für das Buch und die Beispieldateien zu gewährleisten. Sofern doch einmal Korrekturen oder zusätzliche Hinweise zu diesem Buch oder den zugehörigen Beispieldateien notwendig waren, so finden Sie diese im World Wide Web unter http://www.microsoft-press.de/support.asp Falls Sie Kommentare, Fragen oder Anregungen zu diesem Buch haben, senden Sie sie bitte an folgende Microsoft Press-Adresse: [email protected] Beachten Sie bitte, dass über diese Mailadresse kein Softwareservice angeboten wird. Für Supportinformationen bezüglich der Softwareprodukte besuchen Sie bitte die Microsoft-Website: http://www.microsoft.com/germany/support. Paolo Pialorsi, Marco Russo: Datenbankprogrammierung mit Microsoft LINQ. Microsoft Press 2008 (ISBN 978-3-86645-428-6)