linq language integrated query en c

632
Référence Réseaux et télécom Programmation Génie logiciel Sécurité Système d’exploitation LINQ Language Integrated Query en C# 2008 Joseph C. Rattz

Upload: souf13

Post on 01-Dec-2015

252 views

Category:

Documents


37 download

TRANSCRIPT

Page 1: LINQ Language Integrated Query en C

Référ

ence

Réseauxet télécom

Programmation

Génie logiciel

Sécurité

Système d’exploitation

LINQLanguage Integrated Query en C# 2008

Joseph C. Rattz

Page 2: LINQ Language Integrated Query en C

LINQLanguage Integrated Queryen C# 2008Joseph C. Rattz, Jr.

Traduction : Michel Martin, MVP

Relecture technique : Mitsuru Furuta, Microsoft France

Pierrick Gourlain, MVP Client Application

Matthieu Mezil, MVP C#

Linq FM Prél Page I Mercredi, 18. février 2009 8:15 08

Page 3: LINQ Language Integrated Query en C

Pearson Education France a apporté le plus grand soin à la réalisation de ce livre afin de vous four-nir une information complète et fiable. Cependant, Pearson Education France n’assume de respon-sabilités, ni pour son utilisation, ni pour les contrefaçons de brevets ou atteintes aux droits de tiercespersonnes qui pourraient résulter de cette utilisation.

Les exemples ou les programmes présents dans cet ouvrage sont fournis pour illustrer les descriptionsthéoriques. Ils ne sont en aucun cas destinés à une utilisation commerciale ou professionnelle.

Pearson Education France ne pourra en aucun cas être tenu pour responsable des préjudicesou dommages de quelque nature que ce soit pouvant résulter de l’utilisation de ces exemples ouprogrammes.

Tous les noms de produits ou marques cités dans ce livre sont des marques déposées par leurspropriétaires respectifs.

Aucune représentation ou reproduction, même partielle, autre que celles prévues à l’article L. 122-5 2˚ et 3˚ a) du code de lapropriété intellectuelle ne peut être faite sans l’autorisation expresse de Pearson Education France ou, le cas échéant, sansle respect des modalités prévues à l’article L. 122-10 dudit code.

No part of this book may be reproduced or transmitted in any form or by any means, electronic or mechanical, includingphotocopying, recording or by any information storage retrieval system, without permission from Pearson Education, Inc.

Publié par Pearson Education France47 bis, rue des Vinaigriers75010 PARISTél. : 01 72 74 90 00www.pearson.fr

Mise en pages : TyPAO

Copyright © 2009 Pearson Education FranceTous droits réservés

Titre original : Pro LINQ Language Integrated Query in C# 2008

Traduit de l’américain par Michel Martin

Relecture technique : Mitsuru Furuta, Pierrick Gourlain, Matthieu Mezil

ISBN original : 978-1-59059-789-9Copyright © 2007 by Joseph C. Rattz, Jr.All rights reserved

Édition originale publiée par Apress2855 Telegraph Avenue, Suite 600,Berkeley, CA 94705www.apress.com

Linq.book Page II Mercredi, 18. février 2009 7:58 07

ISBN : 978-2-7440-4106-8

Page 4: LINQ Language Integrated Query en C

Table des matières

À propos de l’auteur.......................................................................................................... XI

Traducteur et relecteurs techniques ................................................................................ XIII

Partie I

LINQ et C# 2008

1 Hello LINQ.................................................................................................................... 3Un changement de paradigme .................................................................................... 3

Interrogation XML .......................................................................................... 4Interrogation d’une base de données SQL Server ........................................... 5

Introduction ................................................................................................................ 6LINQ et l’interrogation des données ............................................................... 7Composants ..................................................................................................... 7Comment travailler avec LINQ ....................................................................... 9

LINQ ne se limite pas aux requêtes ............................................................................ 9Quelques conseils avant de commencer ..................................................................... 12

Utilisez le mot-clé var si vous n’êtes pas à l’aise ........................................... 12Utilisez les opérateurs Cast ou OfType pour les collections héritées ............. 14Préférez l’opérateur OfType à l’opérateur Cast .............................................. 15Les requêtes aussi peuvent être boguées ......................................................... 15Sachez tirer parti des requêtes différées .......................................................... 16Utiliser le log du DataContext ....................................................................... 17Utilisez le forum LINQ ................................................................................... 18

Résumé ....................................................................................................................... 18

2 Améliorations de C# 3.0 pour LINQ .......................................................................... 19Les nouveautés du langage C# 3.0 ............................................................................. 19

Les expressions lambda ................................................................................... 20Arbres d’expressions ....................................................................................... 25Le mot-clé var, l’initialisation d’objets et les types anonymes ...................... 26Méthodes d’extension ..................................................................................... 31Méthodes partielles ......................................................................................... 37Expressions de requête .................................................................................... 39

Résumé ....................................................................................................................... 49

Linq.book Page III Mercredi, 18. février 2009 7:58 07

Page 5: LINQ Language Integrated Query en C

IV Table des matières

Partie II

LINQ to Objects

3 Introduction à LINQ to Objects.................................................................................. 53

Vue d’ensemble de LINQ to Objects .......................................................................... 53

IEnumerable<T>, séquences et opérateurs de requête standard ................................ 54

IEnumerable<T>, yield et requêtes différées ......................................................... 55

Délégués Func ................................................................................................. 58

Les opérateurs de requête standard ............................................................................. 59

Résumé ............................................................................................................ 61

4 Les opérateurs différés................................................................................................. 63

Espaces de noms référencés ....................................................................................... 63

Assemblies référencés ................................................................................................ 64

Classes communes ...................................................................................................... 64

Les opérateurs différés, par groupes fonctionnels ...................................................... 65

Restriction ....................................................................................................... 65

Projection ........................................................................................................ 67

Partage ............................................................................................................ 76

Concaténation .................................................................................................. 83

Tri .................................................................................................................... 85

Opérateurs de jointure ..................................................................................... 100

Opérateurs de regroupement ........................................................................... 104

Opérateurs d’initialisation ............................................................................... 110

Opérateurs de conversion ................................................................................ 115

Opérateurs dédiés aux éléments ...................................................................... 122

Opérateurs de génération ................................................................................. 126

Résumé ....................................................................................................................... 129

5 Les opérateurs non différés ......................................................................................... 131

Espaces de noms référencés ....................................................................................... 131

Classes communes ...................................................................................................... 131

Les opérateurs non différés, par groupes fonctionnels ............................................... 134

Opérateurs de conversion ................................................................................ 134

Opérateurs d’égalité ........................................................................................ 145

Opérateurs agissant au niveau des éléments ................................................... 148

Quantificateurs ................................................................................................ 160

Fonctions de comptage .................................................................................... 165

Résumé ....................................................................................................................... 178

Linq.book Page IV Mercredi, 18. février 2009 7:58 07

Page 6: LINQ Language Integrated Query en C

Table des matières V

Partie III

LINQ to XML

6 Introduction à LINQ to XML ..................................................................................... 183

Introduction ................................................................................................................ 185Se passer de l’API W3C DOM XML ......................................................................... 185Résumé ....................................................................................................................... 187

7 L’API LINQ to XML.................................................................................................... 189

Espaces de noms référencés ....................................................................................... 189Améliorations de l’API ............................................................................................... 190

La construction fonctionnelle simplifie la création d’arbres XML ................. 190L’élément, point central d’un objet XML ....................................................... 192Noms, espaces de noms et préfixes ................................................................. 194Extraction de valeurs de nœuds ....................................................................... 196

Le modèle d’objet LINQ to XML .............................................................................. 199Exécution différée des requêtes, suppression de nœuds et bogue d’Halloween ......... 200Création XML ............................................................................................................ 202

Création d’éléments avec XElement ............................................................... 202Création d’attributs avec XAttribute ............................................................ 205Création de commentaires avec XComment ..................................................... 206Création de conteneurs avec XContainer ....................................................... 207Création de déclarations avec XDeclaration ................................................. 207Création de types de documents avec XDocumentType .................................. 208Création de documents avec XDocument ......................................................... 209Création de noms avec XName ......................................................................... 210Création d’espaces de noms avec XNamespace ............................................... 211Création de nœuds avec XNode ........................................................................ 211Création d’instructions de traitement avec XProcessingInstruction ......... 211Création d’éléments streaming avec XStreamingElement .......................... 213Création de textes avec XText ......................................................................... 215Définition d’un objet CData avec XCData ....................................................... 215

Sauvegarde de fichiers XML ...................................................................................... 216Sauvegardes avec XDocument.Save() ........................................................... 216Sauvegarde avec XElement.Save ................................................................... 217

Lecture de fichiers XML ............................................................................................ 218Lecture avec XDocument.Load() ................................................................... 218Lecture avec XElement.Load() ..................................................................... 219Extraction avec XDocument.Parse() ou XElement.Parse() ....................... 220

Déplacements XML .................................................................................................... 221Propriétés de déplacement ............................................................................... 222Méthodes de déplacement ............................................................................... 225

Linq.book Page V Mercredi, 18. février 2009 7:58 07

Page 7: LINQ Language Integrated Query en C

VI Table des matières

Modification de données XML ................................................................................... 238Ajout de nœuds ............................................................................................... 238Suppression de nœuds ..................................................................................... 242Mise à jour de nœuds ...................................................................................... 245XElement.SetElementValue() sur des objets enfants de XElement ............ 248

Attributs XML ............................................................................................................ 250Création d’un attribut ...................................................................................... 250Déplacements dans un attribut ........................................................................ 250Modification d’attributs ................................................................................... 253

Annotations XML ....................................................................................................... 258Ajout d’annotations avec XObject.AddAnnotation() .................................. 258Accès aux annotations avec XObject.Annotation() ou XObject.Annotations() .......................................................................... 258Suppression d’annotations avec XObject.RemoveAnnotations() .............. 258Exemples d’annotations .................................................................................. 259

Événements XML ....................................................................................................... 262XObject.Changing ....................................................................................... 262XObject.Changed ........................................................................................ 262Quelques exemples d’événements .................................................................. 263Le bogue d’Halloween .................................................................................... 267

Résumé ....................................................................................................................... 267

8 Les opérateurs LINQ to XML..................................................................................... 269Introduction aux opérateurs LINQ to XML ............................................................... 270Opérateur Ancestors ................................................................................................. 270

Prototypes ........................................................................................................ 270Exemples ......................................................................................................... 271

Opérateur AncestorsAndSelf ................................................................................... 274Prototypes ........................................................................................................ 274Exemples ......................................................................................................... 275

Opérateur Attributes ............................................................................................... 277Prototypes ........................................................................................................ 277Exemples ......................................................................................................... 277

Opérateur DescendantNodes ..................................................................................... 279Prototype ......................................................................................................... 279Exemple ........................................................................................................... 279

Opérateur DescendantNodesAndSelf ....................................................................... 280Prototype ......................................................................................................... 280Exemple ........................................................................................................... 281

Opérateur Descendants ............................................................................................. 282Prototypes ........................................................................................................ 282Exemples ......................................................................................................... 282

Opérateur DescendantsAndSelf ............................................................................... 284Prototypes ........................................................................................................ 284Exemples ......................................................................................................... 284

Linq.book Page VI Mercredi, 18. février 2009 7:58 07

Page 8: LINQ Language Integrated Query en C

Table des matières VII

Opérateur Elements ................................................................................................... 287Prototypes ........................................................................................................ 287Exemples ......................................................................................................... 287

Opérateur InDocumentOrder ..................................................................................... 289Prototype ......................................................................................................... 289Exemple ........................................................................................................... 289

Opérateur Nodes ......................................................................................................... 290Prototype ......................................................................................................... 290Exemple ........................................................................................................... 291

Opérateur Remove ....................................................................................................... 292Prototypes ........................................................................................................ 292Exemples ......................................................................................................... 292

Résumé ....................................................................................................................... 294

9 Les autres possibilités de XML ................................................................................... 295Espaces de noms référencés ....................................................................................... 295Requêtes ..................................................................................................................... 296

La description du chemin n’est pas une obligation ......................................... 296Une requête complexe ..................................................................................... 298

Transformations .......................................................................................................... 303Transformations avec XSLT ............................................................................ 304Transformations avec la construction fonctionnelle ........................................ 306Astuces ............................................................................................................ 308

Validation .................................................................................................................... 314Les méthodes d’extension ............................................................................... 314Prototypes ........................................................................................................ 314Obtention d’un schéma XML .......................................................................... 315Exemples ......................................................................................................... 317

XPath .......................................................................................................................... 328Prototypes ........................................................................................................ 328

Résumé ....................................................................................................................... 329

Partie IV

LINQ to DataSet

10 LINQ to DataSet ......................................................................................................... 333Référence des assemblies ........................................................................................... 334Espaces de noms référencés ....................................................................................... 334Code commun utilisé dans les exemples .................................................................... 334Opérateurs dédiés aux DataRow .................................................................................. 336

Opérateur Distinct ........................................................................................ 336Opérateur Except ............................................................................................ 340Opérateur Intersect ...................................................................................... 342

Linq.book Page VII Mercredi, 18. février 2009 7:58 07

Page 9: LINQ Language Integrated Query en C

VIII Table des matières

Opérateur Union .............................................................................................. 344Opérateur SequencialEqual .......................................................................... 346

Opérateurs dédiés aux champs ................................................................................... 347Opérateur Field<T> ........................................................................................ 351Opérateur SetField<T> .................................................................................. 356

Opérateurs dédiés aux DataTable .............................................................................. 359Opérateur AsEnumerable ................................................................................ 359Opérateur CopyToDataTable<DataRow> ........................................................ 360

Résumé ....................................................................................................................... 365

11 Possibilités complémentaires des DataSet................................................................ 367Espaces de noms référencés ....................................................................................... 367DataSets typés ........................................................................................................... 367Un exemple plus proche de la réalité .......................................................................... 369Résumé ....................................................................................................................... 372

Partie V

LINQ to SQL

12 Introduction à LINQ to SQL..................................................................................... 377Introduction à LINQ to SQL ...................................................................................... 378

La classe DataContext ................................................................................... 380Classes d’entités .............................................................................................. 381Associations .................................................................................................... 382Détection de conflit d’accès concurrentiel ...................................................... 383Résolution de conflit d’accès concurrentiel .................................................... 383

Prérequis pour exécuter les exemples ......................................................................... 383Obtenir la version appropriée de la base de données Northwind .................... 384Génération des classes d’entité de la base de données Northwind ................. 384Génération du fichier de mappage XML de la base de données Northwind ... 385

Utilisation de l’API LINQ to SQL ............................................................................. 386IQueryable<T> ......................................................................................................... 386Quelques méthodes communes .................................................................................. 386

La méthode GetStringFromDb() ................................................................... 387La méthode ExecuteStatementInDb() ......................................................... 388

Résumé ....................................................................................................................... 388

13 Astuces et outils pour LINQ to SQL......................................................................... 391Introduction aux astuces et aux outils pour LINQ to SQL ......................................... 391Astuces ....................................................................................................................... 392

La propriété DataContext.Log ...................................................................... 392La méthode GetChangeSet() ......................................................................... 393Utilisation de classes partielles ou de fichiers de mappage ............................. 393Utilisation de méthodes partielles ................................................................... 394

Linq.book Page VIII Mercredi, 18. février 2009 7:58 07

Page 10: LINQ Language Integrated Query en C

Table des matières IX

Outils .......................................................................................................................... 394SQLMetal ........................................................................................................ 394Le Concepteur Objet/Relationnel .................................................................... 401

Utiliser SQLMetal et le Concepteur O/R ................................................................... 414Résumé ....................................................................................................................... 415

14 Opérations standard sur les bases de données......................................................... 417Prérequis pour exécuter les exemples ......................................................................... 417

Méthodes communes ....................................................................................... 418Utilisation de l’API LINQ to SQL .................................................................. 418

Opérations standard de bases de données ................................................................... 418Insertions ......................................................................................................... 418Requêtes .......................................................................................................... 423Mises à jour ..................................................................................................... 446Suppressions .................................................................................................... 450

Surcharger les méthodes de mise à jour des bases de données .................................. 453Surcharge de la méthode Insert .................................................................... 453Surcharge de la méthode Update .................................................................... 454Surcharge de la méthode Delete .................................................................... 454Exemple ........................................................................................................... 454Surcharge dans le Concepteur Objet/Relationnel ............................................ 457Considérations ................................................................................................. 457

Traduction SQL .......................................................................................................... 457Résumé ....................................................................................................................... 459

15 Les classes d’entité LINQ to SQL ............................................................................. 461Prérequis pour exécuter les exemples ......................................................................... 461Les classes d’entité ..................................................................................................... 461

Création de classes d’entité ............................................................................. 462Schéma de fichier de mappage externe XML ................................................. 493Projection dans des classes d’entité/des classes de non-entité ........................ 494Dans une projection, préférez l’initialisation d’objet à la construction paramétrée ............................................................. 496

Extension des classes d’entité avec des méthodes partielles ...................................... 499Les classes API importantes de System.Data.Linq ................................................. 501

EntitySet<T> ................................................................................................. 502EntityRef<T> ................................................................................................. 502Table<T> ......................................................................................................... 504IExecuteResult ............................................................................................. 505ISingleResult<T> ........................................................................................ 506IMultipleResults ......................................................................................... 506

Résumé ....................................................................................................................... 508

16 La classe DataContext................................................................................................ 509Prérequis pour exécuter les exemples ......................................................................... 509

Linq.book Page IX Mercredi, 18. février 2009 7:58 07

Page 11: LINQ Language Integrated Query en C

X Table des matières

Méthodes communes ....................................................................................... 509

Utilisation de l’API LINQ to SQL .................................................................. 509

La classe [Your]DataContext .................................................................................. 510

La classe DataContext .............................................................................................. 510

Principaux objectifs ......................................................................................... 513

Datacontext() et [Your]DataContext() .................................................... 520

SubmitChanges() ........................................................................................... 532

DatabaseExists() ......................................................................................... 539

CreateDatabase() ......................................................................................... 540

DeleteDatabase() ........................................................................................ 541

CreateMethodCallQuery() ........................................................................... 542

ExecuteQuery() ............................................................................................ 543

Translate() ................................................................................................... 546

ExecuteCommand() ......................................................................................... 547

ExecuteMethodCall() ................................................................................... 549

GetCommand() ................................................................................................. 557

GetChangeSet() ............................................................................................. 558

GetTable() ..................................................................................................... 560

Refresh() ....................................................................................................... 562

Résumé ....................................................................................................................... 568

17 Les conflits d’accès concurrentiels ............................................................................ 571

Prérequis pour exécuter les exemples ......................................................................... 571

Méthodes communes ....................................................................................... 571

Utilisation de l’API LINQ to SQL .................................................................. 571

Conflits d’accès concurrentiels ................................................................................... 571

Contrôle d’accès concurrentiel optimiste ........................................................ 572

Contrôle d’accès concurrentiel pessimiste ...................................................... 585

Une approche alternative pour les middle-tier et les serveurs ......................... 588

Résumé ....................................................................................................................... 591

18 Informations complémentaires sur SQL .................................................................. 593

Prérequis pour exécuter les exemples ......................................................................... 593

Utilisation de l’API LINQ to SQL .................................................................. 593

Utilisation de l’API LINQ to XML ................................................................. 593

Les vues d’une base de données ................................................................................. 593

Héritage des classes d’entité ....................................................................................... 595

Transactions ................................................................................................................ 601

Résumé ....................................................................................................................... 603

Index ................................................................................................................................... 607

Linq.book Page X Mercredi, 18. février 2009 7:58 07

Page 12: LINQ Language Integrated Query en C

À propos de l’auteur

Joseph C. Rattz Jr a commencé sa carrière de développeur en 1990, lorsqu’un ami lui ademandé de l’aide pour développer l’éditeur de texte "ANSI Master" sur un ordinateurCommodore Amiga. Un jeu de pendu (The Gallows) lui a rapidement fait suite. Aprèsces premiers programmes écrits en Basic compilé, Joe s’est tourné vers le langage C, àdes fins de vitesse et de puissance. Il a alors développé des applications pour les maga-zines JumpDisk (périodique avec CD consacré aux ordinateurs Amiga) et Amiga World.Comme il développait dans une petite ville et sur une plate-forme isolée, Joe a appristoutes les "mauvaises" façons d’écrire du code. C’est en tentant de faire évoluer sesapplications qu’il a pris conscience de l’importance de la maintenabilité du code.

Deux ans plus tard, Joe a intégré la société Policy Management Systems en tant queprogrammeur pour développer une application client/serveur dans le domaine del’assurance pour OS/2 et Presentation Manager. D’année en année, il a ajouté le C++,Unix, Java, ASP, ASP.NET, C#, HTML, DHTML et XML à sa palette de langages alorsqu’il travaillait pour SCT, DocuCorp, IBM et le comité d’Atlanta pour les jeux Olympi-ques, CheckFree, NCR, EDS, Delta Technology, Radiant Systems et la société GenuineParts. Joe apprécie particulièrement le développement d’interfaces utilisateurs et deprogrammes exécutés côté serveur. Sa phase favorite de développement est le débo-gage.

Joe travaille actuellement pour la société Genuine Parts Company (maison mère deNAPA), dans le département Automotive Parts Group Information System, où il déve-loppe le site web Storefront. Ce site gère les stocks de NAPA et fournit un accès à leurscomptes et données à travers un réseau d’ordinateurs AS/400.

Vous pouvez le contacter sur le site www.linqdev.com.

Linq.book Page XI Mercredi, 18. février 2009 7:58 07

Page 13: LINQ Language Integrated Query en C

Linq.book Page XII Mercredi, 18. février 2009 7:58 07

Page 14: LINQ Language Integrated Query en C

Traducteur et relecteurs techniques

À propos du traducteur

Michel Martin est un passionné des technologies Microsoft. Nommé MVP par Micro-soft depuis 2003, il anime des ateliers de formation, réalise des CD-ROM d’autoforma-tion vidéo et a écrit plus de 250 ouvrages techniques, parmi lesquels Développez desgadgets pour Windows Vista et Windows Live (Pearson, 2007) et le Programmeur VisualBasic 2008 (Pearson, 2008). Il a récemment créé le réseau social eFriends Network,accessible à l’adresse http://www.efriendsnetwork.com.

À propos des relecteurs techniques

Mitsuru Furuta est responsable technique en charge des relations développeurs chezMicrosoft France. Il blogue sur http://blogs.msdn.com/mitsufu.

Pierrick Gourlain est architecte logiciel. Nommé MVP par Microsoft depuis 2007, ilest passionné de nouvelles technologies, plus particulièrement de LINQ, WPF, WCF,WF et des langages dynamiques. Il collabore à plusieurs projets open-source hébergéssur codeplex (http://www.codeplex.com).

Matthieu Mezil est consultant formateur, nommé MVP C# par Microsoft depuis avril2008. Passionné par .NET, il s’est spécialisé sur l’Entity Framework. Il blogue surhttp://blogs.codes-sources.com/matthieu (fr) et http://msmvps.com/blogs/matthieu(en).

Linq.book Page XIII Mercredi, 18. février 2009 7:58 07

Page 15: LINQ Language Integrated Query en C

Linq.book Page XIV Mercredi, 18. février 2009 7:58 07

Page 16: LINQ Language Integrated Query en C

I

LINQ et C# 2008

Linq.book Page 1 Mercredi, 18. février 2009 7:58 07

Page 17: LINQ Language Integrated Query en C

Linq.book Page 2 Mercredi, 18. février 2009 7:58 07

Page 18: LINQ Language Integrated Query en C

1

Hello LINQ

Listing 1.1 : Hello Linq.

using System;using System.Linq;

string[] greetings = {"hello world", "hello LINQ", "hello Pearson"};

var items = from s in greetings where s.EndsWith("LINQ") select s;

foreach (var item in items) Console.WriteLine(item);

INFO

Le code du Listing 1.1 a été inséré dans un projet basé sur le modèle "ApplicationConsole", de Visual Studio 2008. Si cette directive n’est pas déjà présente dans le squelettede l’application, ajoutez une instruction using System.Linq pour référencer cet espacede noms.

L’exécution de ce code avec le raccourci clavier Ctrl+F5 affiche le message suivantdans la console :

Un changement de paradigme

Avez-vous remarqué un changement par rapport à votre style de programmation ? Entant que développeur .NET, vous n’êtes certainement pas passé à côté. À travers cetexemple trivial, une requête SQL (Structured Query Language) a été exécutée sur un

Hello LINQ

Linq.book Page 3 Mercredi, 18. février 2009 7:58 07

Page 19: LINQ Language Integrated Query en C

4 LINQ et C# 2008 Partie I

tableau de Strings1. Intéressez-vous à la clause where. Vous ne rêvez pas, j’ai bienutilisé la méthode EndsWidth sur un objet String. Vous vous demandez certainementquel est le type de cette variable. C# fait-il toujours des vérifications statiques destypes ? Oui, à la compilation ! Cette prouesse est rendue possible par LINQ (LanguageINtegrated Query).

Interrogation XML

Après avoir examiné le code du Listing 1.1, ce deuxième exemple va commencer àvous faire entrevoir le potentiel mis entre les mains du développeur .NET par LINQ. Enutilisant l’API LINQ to XML, le Listing 1.2 montre avec quelle facilité il est possibled’interagir et d’interroger des données XML (eXtensible Markup Language). Remar-quez en particulier comment les données XML sont manipulées à travers l’objet books.

Listing 1.2 : Requête XML basée sur LINQ to XML.

using System;using System.Linq;using System.Xml.Linq;

XElement books = XElement.Parse( @"<books> <book> <title>Pro LINQ: Language Integrated Query en C# 2008</title> <author>Joe Rattz</author> </book> <book> <title>Pro WF: Windows Workflow en .NET 3.0</title> <author>Bruce Bukovics</author> </book> <book> <title>Pro C# 2005 et la plateforme.NET 2.0, Troisième édition</title> <author>Andrew Troelsen</author> </book></books>");

var titles = from book in books.Elements("book") where (string) book.Element("author") == "Joe Rattz" select book.Element("title");

foreach(var title in titles) Console.WriteLine(title.Value);

INFO

Si l’assembly System.Xml.Linq.dll n’apparaît pas dans les références du projet, ajoutez-la. Remarquez également la référence à l’espace de noms System.Xml.Linq.

1. L’ordre d’interrogation est inversé par rapport à une requête SQL traditionnelle. Par ailleurs, uneinstruction "s in" a été ajoutée pour fournir une référence à l’ensemble des éléments source. Ici, letableau de chaînes "hello world", "hello LINQ" et "hello Pearson".

Linq.book Page 4 Mercredi, 18. février 2009 7:58 07

Page 20: LINQ Language Integrated Query en C

Chapitre 1 Hello LINQ 5

Appuyez sur Ctrl+F5 pour exécuter ce code. Voici le résultat affiché dans la console.

Avez-vous remarqué comment les données XML ont été découpées dans un objet detype XElement sans qu’il ait été nécessaire de définir un objet XmlDocument ? Les exten-sions de l’API XML sont un des avantages de LINQ to XML. Au lieu d’être centré surles objets XmlDocument, comme le préconise le W3C Document Object Model (DOM),LINQ to XML permet au développeur d’interagir à tous les niveaux du document enutilisant la classe XElement.

INFO

Outre ses possibilités d’interrogation, LINQ to XML fournit également une interface detravail XML plus puissante et plus facile à utiliser.

Notez également que la même syntaxe SQL est utilisée pour interroger les donnéesXML, comme s’il s’agissait d’une base de données.

Interrogation d’une base de données SQL Server

Ce nouvel exemple montre comment utiliser LINQ to SQL pour interroger des tablesdans des bases de données. Le Listing 1.3 interroge la base de données exemple MicrosoftNorthwind.

Listing 1.3 : Une simple interrogation de base de données basée sur une requête LINQ to SQL.

using System;using System.Linq;using System.Data.Linq;

using nwind;

Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind");

var custs = from c in db.Customers where c.City == "Rio de Janeiro" select c;

foreach (var cust in custs) Console.WriteLine("{0}", cust.CompanyName);

INFO

Ce code fait référence à l’assembly System.Data.Linq.dll. Si cette assembly n’est passpécifiée dans les premières lignes du listing, ajoutez-la. Notez qu’il est également fait réfé-rence à l’espace de noms System.Data.Linq.

Pro LINQ: Language Integrated Query en C# 2008

Linq.book Page 5 Mercredi, 18. février 2009 7:58 07

Page 21: LINQ Language Integrated Query en C

6 LINQ et C# 2008 Partie I

Pour que cet exemple fonctionne, il est nécessaire de faire appel à l’utilitaire en ligne decommande SQLMetal ou au concepteur d’objets relationnels, afin de générer des clas-ses d’entités qui pointent vers la base de données Northwind. Reportez-vous au Chapi-tre 12 pour en savoir plus sur l’utilisation de SQLMetal.

Les classes d’entités de cet exemple faisant partie de l’espace de noms nwind, la clauseusing nwind; a été utilisée en début de listing pour y faire référence.

INFO

Il se peut que vous deviez changer la chaîne de connexion passée au constructeur Northwinddans ce listing. Reportez-vous aux sections relatives à DataContext() et [Your]DataContext()du Chapitre 16 pour prendre connaissance des différents modes de connexion possibles.

Appuyez sur Ctrl+F5 pour exécuter ce code. Le résultat ci-après devrait s’afficher dansla console :

Cet exemple utilise la table Customers de la base de données Northwind. Il se contentede sélectionner les clients qui résident à Rio de Janeiro. À première vue, il n’y a rien denouveau ou de différent dans ce code. Vous remarquerez pourtant que la requête estintégrée dans le code. Les fonctionnalités de l’éditeur sont donc également accessiblesau niveau de la requête ; en particulier la vérification de la syntaxe et l’Intellisense.L’écriture "à l’aveuglette" des requêtes et la détection des erreurs à l’exécution fontdonc bel et bien partie du passé !

Vous voulez baser une clause where sur un champ de la table Customers, mais vousn’arrivez pas à vous rappeler le nom des champs ? Intellisense affichera les noms deschamps et vous n’aurez plus qu’à choisir dans la liste. Dans l’exemple précédent, ilsuffit de taper c. pour qu’Intellisense liste tous les champs de la table Customers.

Vous verrez au Chapitre 2 que les requêtes LINQ peuvent utiliser deux syntaxes : lasyntaxe "à point" object.method(), traditionnelle dans le langage C#, et une nouvellesyntaxe propre à LINQ. Les requêtes présentées jusqu’ici utilisent cette nouvellesyntaxe mais, bien entendu, vous pouvez continuer à utiliser la syntaxe traditionnelle.

Introduction

La plate-forme .NET et les langages qui l’accompagnent (C# et VB) sont aujourd’huiéprouvés. Cependant, il reste un point douloureux pour les développeurs : l’accès auxsources de données. La manipulation de bases de données et de code XML se révèlegénéralement lourde et parfois problématique.

Hanari CarnesQue DelíciaRicardo Adocicados

Linq.book Page 6 Mercredi, 18. février 2009 7:58 07

Page 22: LINQ Language Integrated Query en C

Chapitre 1 Hello LINQ 7

Les problèmes rencontrés dans la manipulation des bases de données sont multiples.Pour commencer, le langage n’est pas en mesure d’interagir avec les données au niveaunatif. Cela signifie que, fréquemment, les erreurs de syntaxe ne sont pas détectéesjusqu’à l’exécution. De même, les champs incorrectement référencés ne sont pas détec-tés. De telles erreurs peuvent être désastreuses, en particulier si elles se produisentpendant l’exécution d’une routine de gestion d’erreurs. Rien n’est plus frustrant qu’unmécanisme de gestion d’erreurs mis en échec à cause d’une erreur syntaxique qui n’ajamais été détectée !

Un autre problème peut provenir d’une différence entre les types des données stockésdans une base de données ou dans des éléments XML, par exemple, et les types gérés par lelangage de programmation. Les données date et heure sont en particulier concernées.

L’extraction, l’itération et la manipulation de données XML risquent également d’êtretrès fastidieuses. Souvent, alors qu’un simple fragment XML doit être manipulé, il estnécessaire de créer un XmlDocument pour se conformer à l’API W3C DOM XML.

Au lieu d’ajouter de nouvelles classes et méthodes pour pallier ces déficiences, les ingé-nieurs de Microsoft ont décidé d’aller plus loin en modifiant la syntaxe des requêtesd’interrogation. C’est ainsi que LINQ a vu le jour. Cette technologie, directementaccessible dans les langages de programmation, permet d’interroger tous types dedonnées, des tableaux mémoire aux collections en passant par les bases de données, lesdocuments XML et bien d’autres ensembles de données.

LINQ et l’interrogation des données

LINQ est essentiellement un langage d’interrogation. Il peut retourner un ensembled’objets, un objet unique ou un sous-ensemble de champs appartenant à un objet ou àun ensemble d’objets. Cet ensemble d’objets est appelé une "séquence". La plupart desséquences LINQ sont de type IEnumerable<T>, où T est le type des objets stockés dansla séquence. Par exemple, une séquence d’entiers est stockée dans une variable de typeIEnumerable<int>. Comme vous le verrez dans la suite du livre, la plupart des méthodesLINQ retournent un IEnumerable<T>.

Dans les exemples étudiés jusqu’ici, toutes les requêtes ont retourné un IEnumerable<T>ou un type hérité. Le mot-clé "var" a parfois été utilisé par souci de simplification. Vousverrez au Chapitre 2 qu’il s’agit d’un raccourci d’écriture.

Composants

La puissance et l’universalité de LINQ devraient le faire adopter dans de nombreuxdomaines. En fait, tous les types de données stockés sont de bons candidats aux requê-tes LINQ. Ceci concerne les bases de données, Active Directory, le Registre deWindows, le système de fichiers, les feuilles de calcul Excel, etc.

Linq.book Page 7 Mercredi, 18. février 2009 7:58 07

Page 23: LINQ Language Integrated Query en C

8 LINQ et C# 2008 Partie I

Microsoft a défini plusieurs domaines de prédilection pour LINQ. Il ne fait aucun douteque cette liste sera complétée par la suite.

LINQ to ObjectsLINQ to Objects est le nom donné à l’API IEnumerable<T> pour les opérateurs derequête standard. Vous l’utiliserez par exemple pour requêter des tableaux et des collec-tions de données en mémoire. Les opérateurs de requête standard LINQ to Objects sontles méthodes statiques de la classe System.Linq.Enumerable.

LINQ to XMLLINQ to XML est le nom de l’API dédiée au travail sur les données XML (cette inter-face était précédemment appelée XLINQ). LINQ to XML ne se contente pas de définirdes librairies XML afin d’assurer la compatibilité avec LINQ. Il apporte également unesolution à plusieurs déficiences du standard XML DOM et facilite le travail avec lesdonnées XML. À titre d’exemple, il n’est désormais plus nécessaire de créer unXmlDocument pour traiter une portion réduite de XML. Qui s’en plaindra ? Pourpouvoir travailler avec LINQ to XML, vous devez faire référence à l’assemblySystem.Xml.Linq.dll dans votre projet :

LINQ to DataSetLINQ to DataSet est le nom de l’API permettant de travailler avec des DataSets. Denombreux développeurs utilisent ces types d’objets. Sans qu’aucune réécriture de codene soit nécessaire, ils pourront désormais tirer avantage de la puissance de LINQ pourinterroger leurs DataSets.

LINQ to SQLLINQ to SQL est le nom de l’API IQueryable<T>, qui permet d’appliquer des requêtesLINQ aux bases de données Microsoft SQL Server (cette interface était précédemmentconnue sous le nom DLinq). Pour pouvoir utiliser LINQ to SQL, vous devez faire réfé-rence à l’assembly System.Data.Linq.dll :

LINQ to EntitiesLINQ to Entities est une API alternative utilisée pour interfacer des bases de données.Elle découple le modèle objet entity de la base de données elle-même en ajoutant unmappage logique entre les deux. Ce découplage procure une puissance et une flexibilitéaccrues. Étant donné que LINQ to Entities ne fait pas partie du framework LINQ, nousne nous y intéresserons pas dans cet ouvrage. Cependant, si LINQ to SQL ne vous

using System.Xml.Linq;

using System.Data.Linq;

Linq.book Page 8 Mercredi, 18. février 2009 7:58 07

Page 24: LINQ Language Integrated Query en C

Chapitre 1 Hello LINQ 9

semble pas assez flexible, vous devriez vous intéresser à LINQ to Entities ; en particu-lier si vous avez besoin d’une plus grande souplesse entre les entités et la base dedonnées, si vous manipulez des données provenant de plusieurs tables ou si vous voulezpersonnaliser la modélisation des entités.

Comment travailler avec LINQ

Il n’existe aucun produit LINQ à acheter ou à installer : c’est juste le nom qui a étédonné à l’outil d’interrogation de C# 3.0 et au Framework .NET 3.5, apparu dans VisualStudio 2008.

Pour obtenir des informations à jour sur LINQ et Visual Studio 2008, connectez-vous sur les pages www.linqdev.com et http://apress.com/book/bookDisplay.html?bID=10241.

LINQ ne se limite pas aux requêtes

LINQ étant l’abréviation de Language INtegrated Query (langage d’interrogation inté-gré), vous pourriez penser qu’il se limite à l’interrogation de données. Comme vous leverrez dans la suite du livre, son domaine d’action va beaucoup plus loin...

Vous est-il déjà arrivé de devoir remanier les données renvoyées par une méthode avantde pouvoir les passer en argument à une autre méthode ? Supposons par exemple quevous appeliez la méthode A. Cette méthode retourne un tableau de string contenant desvaleurs numériques stockées en tant que chaînes de caractères. Vous devez alors appelerune méthode B qui demande un tableau d’entiers en entrée. Puis mettre en place uneboucle pour convertir un à un les éléments du tableau. Quelle plaie ! LINQ apporte uneréponse élégante à ce problème.

Supposons que nous ayons un tableau de string reçu d’une méthode A, comme indiquédans le Listing 1.4.

Listing 1.4 : Une requête XML basée sur LINQ to XML.

string[] numbers = { "0042", "010", "9", "27" };

Dans cet exemple, le tableau de string a été déclaré de façon statique. Avant d’appeler laméthode B, il est nécessaire de convertir ce tableau de chaînes en un tableau d’entiers :

int[] nums = numbers.Select(s => Int32.Parse(s)).ToArray();

Cette conversion pourrait-elle être plus simple ?

Voici le code à utiliser pour afficher le tableau d’entiers nums :

foreach(int num in nums) Console.WriteLine(num);

Linq.book Page 9 Mercredi, 18. février 2009 7:58 07

Page 25: LINQ Language Integrated Query en C

10 LINQ et C# 2008 Partie I

Et voici l’affichage résultant dans la console :

Peut-être pensez-vous que cette conversion s’est contentée de supprimer les zérosdevant les nombres. Pour nous en assurer, nous allons trier les données numériques. Sitel est le cas, 9 sera affiché en dernier et 10, en premier. Le Listing 1.5 effectue laconversion et le tri des données.

Listing 1.5 : Conversion d’un tableau de chaînes en entiers et tri croissant.

string[] numbers = { "0042", "010", "9", "27" };

int[] nums = numbers.Select(s => Int32.Parse(s)).OrderBy(s => s).ToArray();

foreach(int num in nums) Console.WriteLine(num);

Voici le résultat :

Cela fonctionne, mais il faut bien avouer que cet exemple est simpliste. Nous allonsmaintenant nous intéresser à des données plus complexes.

Supposons que nous disposions de la classe Employee et qu’une de ses méthodesretourne le nom des employés. Supposons également que nous disposions d’une classeContact et qu’une de ses méthodes liste les contacts d’un des employés. Supposonsenfin que vous souhaitiez obtenir la liste des contacts de chacun des employés.

La tâche semble assez simple. Cependant, la méthode qui retourne le nom des employésfournit un ArrayList d’objets Employee, et la méthode qui liste les contacts nécessiteun tableau de type Contact. Voici le code des classes Employee et Contact :

namespace LINQDev.HR{ public class Employee { public int id; public string firstName; public string lastName; public static ArrayList GetEmployees() { // Le "vrai" code ferait certainement une requête // sur une base de données à ce point précis ArrayList al = new ArrayList();

4210927

9102742

Linq.book Page 10 Mercredi, 18. février 2009 7:58 07

Page 26: LINQ Language Integrated Query en C

Chapitre 1 Hello LINQ 11

// Ajout des données dans le tableau ArrayList al al.Add(new Employee { id = 1, firstName = "Joe", lastName = "Rattz"} ); al.Add(new Employee { id = 2, firstName = "William", lastName = "Gates"} ); al.Add(new Employee { id = 3, firstName = "Anders", lastName = "Hejlsberg"} ); return(al); } }}namespace LINQDev.Common{ public class Contact { public int Id; public string Name; public static void PublishContacts(Contact[] contacts) { // Cette méthode se contente d’afficher les contacts dans la console foreach(Contact c in contacts) Console.WriteLine("Contact Id: {0} Contact: {1}", c.Id, c.Name); } }}

Comme vous pouvez le voir, la classe Employee et la méthode GetEmployee sont dansl’espace de noms LINQDev.HR, et la méthode GetEmployees retourne un ArrayList.Quant à la méthode PublishContacts, elle se trouve dans l’espace de nomsLINQDev.Common et demande un tableau d’objets Contact en entrée.

Avant l’arrivée de LINQ, vous auriez dû passer en revue les ArrayList retournés par laméthode GetEmployees et créer un nouveau tableau de type Contact afin d’assurer lacompatibilité avec la méthode PublishContacts. Comme le montre le Listing 1.6,LINQ facilite grandement les choses.

Listing 1.6 : Appel des méthodes GetEmployees et PublishContacts.

ArrayList alEmployees = LINQDev.HR.Employee.GetEmployees();

LINQDev.Common.Contact[] contacts = alEmployees .Cast<LINQDev.HR.Employee>() .Select(e => new LINQDev.Common.Contact { Id = e.id, Name = string.Format("{0} {1}", e.firstName, e.lastName) }) .ToArray<LINQDev.Common.Contact>();

LINQDev.Common.Contact.PublishContacts(contacts);

Pour convertir le tableau ArrayList d’objets Employee en un tableau d’objets Contact,nous l’avons transformé en une séquence IEnumerable<Employee> en utilisant l’opéra-teur de requête standard Cast. Cette transformation est nécessaire car une collectionhéritée ArrayList est renvoyée par GetEmployees. Syntaxiquement parlant, ce sont lesobjets de la classe System.Object et non ceux de la classe Employee qui sont stockésdans l’ArrayList. Le casting vers des objets Employee est donc nécessaire. Si laméthode GetEmployees avait renvoyé une collection générique List, cette étapen’aurait pas été nécessaire. Malheureusement, ce type de collection n’était pas disponiblelors de l’écriture de ce code hérité.

Linq.book Page 11 Mercredi, 18. février 2009 7:58 07

Page 27: LINQ Language Integrated Query en C

12 LINQ et C# 2008 Partie I

Le casting terminé, l’opérateur Select est appliqué sur la séquence d’objets Employee.Dans l’expression lambda (le code passé comme argument de la méthode Select), unobjet Contact est instancié et initialisé en utilisant les valeurs retournées par les objetsEmployee (vous en saurez plus en consultant la section réservées aux méthodes anonymesau Chapitre 2).

Pour terminer, la séquence d’objets Contact est convertie en un tableau d’objetsContact en utilisant l’opérateur ToArray. Ceci afin d’assurer la compatibilité avec laméthode PublishContacts. Voici le résultat affiché dans la console :

J’espère que vous êtes maintenant convaincu que LINQ ne se limite pas à l’interroga-tion de données. En parcourant les autres chapitres de ce livre, essayez de trouver denouveaux champs d’application de LINQ.

Quelques conseils avant de commencer

Pendant l’écriture de cet ouvrage, j’ai parfois été troublé, embrouillé, voire bloqué alorsque j’expérimentais LINQ. Pour vous éviter de tomber dans les mêmes pièges, je vais vousdonner quelques conseils. Tous les concepts propres à LINQ n’ayant pas encore été intro-duits, il serait logique que ces conseils figurent à la fin de l’ouvrage. Rassurez-vous : je nevais pas vous imposer la lecture complète de l’ouvrage ! Mais ne vous formalisez pas sivous ne comprenez pas entièrement ce qui va être dit dans les pages suivantes…

Utilisez le mot-clé var si vous n’êtes pas à l’aise

Il n’est pas nécessaire d’utiliser le mot-clé var lorsque vous affectez une séquence declasses anonymes à une variable, mais cela peut vous aider à passer l’étape de la compi-lation, en particulier si vous ne savez pas exactement quel type de données vous êtes entrain de manipuler. Bien entendu, il est préférable de connaître le type des données Tdes IEnumerable<T> mais, parfois, en particulier lorsque vous commencez en program-mation LINQ, cela peut se révéler difficile. Si le code ne veut pas se compiler à caused’une incompatibilité dans un type de données, pensez à transformer ce type en utilisantle mot-clé var.

Supposons que vous ayez le code suivant :

// Ce code produit une erreur à la compilationNorthwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind");

IEnumerable<?> orders = db.Customers .Where(c => c.Country == "USA" && c.Region == "WA") .SelectMany(c => c.Orders);

Contact Id: 1 Contact: Joe RattzContact Id: 2 Contact: William GatesContact Id: 3 Contact: Anders Hejlsberg

Linq.book Page 12 Mercredi, 18. février 2009 7:58 07

Page 28: LINQ Language Integrated Query en C

Chapitre 1 Hello LINQ 13

Il se peut que vous ne sachiez pas exactement quel est le type des données de laséquence d’IEnumerable. Une astuce bien pratique consiste à affecter le résultat dela requête à une variable dont le type est spécifié automatiquement grâce au mot-clévar, puis à obtenir son type grâce à la méthode GetType (voir Listing 1.7).

Listing 1.7 : Un exemple de code qui utilise le mot-clé var.

Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind");

var orders = db.Customers .Where(c => c.Country == "USA" && c.Region == "WA") .SelectMany(c => c.Orders);

Console.WriteLine(orders.GetType());

Dans cet exemple, le type de la variable orders est spécifié par l’intermédiaire du mot-clé var. Voici le type affiché dans la console :

Dans tout le charabia retourné par le compilateur, nwind.Order est certainement lapartie la plus importante, puisqu’elle indique le type de la séquence.

Si l’expression affichée dans la console vous intrigue, exécutez l’exemple dans le débogueuret examinez la variable orders dans la fenêtre Espion Express. Son type est le suivant :

System.Linq.IQueryable<nwind.Order>{System.Data.Linq.DataQuery<nwind.Order>}

La séquence est donc de type nwind.Order. Il s’agit en fait d’un IQuerya-

ble<nwind.Order>, mais vous pouvez l’affecter à un IEnumerable<nwind.Order>,puisque IQueryable<T> hérite de IEnumerable<T>.

Vous pouvez donc réécrire le code précédent et passer en revue les résultats en utilisantles instructions du Listing 1.8.

Listing 1.8 : Le même code que dans le Listing 1.7, sauf au niveau des codes explicites.

Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind");

IEnumerable<Order> orders = db.Customers .Where(c => c.Country == "USA" && c.Region == "WA") .SelectMany(c => c.Orders);

foreach(Order item in orders) Console.WriteLine("{0} - {1} - {2}", item.OrderDate, item.OrderID, item.ShipName);

INFO

Pour que ce code fonctionne, vous devez spécifier une directive using pour les espaces denoms System.Collections.Generic et System.Linq (ce deuxième espace de noms estobligatoire dès que vous utilisez des instructions en rapport avec LINQ).

System.Data.Linq.DataQuery`1[nwind.Order]

Linq.book Page 13 Mercredi, 18. février 2009 7:58 07

Page 29: LINQ Language Integrated Query en C

14 LINQ et C# 2008 Partie I

Ce code produit le résultat suivant :

Utilisez les opérateurs Cast ou OfType pour les collections héritées

La grande majorité des opérateurs de requête LINQ ne peut être utilisée que sur descollections qui implémentent l’interface IEnumerable<T>. Aucune des collections héri-tées de C# (celles présentes dans l’espace de noms System.Collection) n’implémentecette interface. Mais, alors, comment utiliser LINQ avec des collections héritées ?

Deux opérateurs de requête standard sont là pour convertir des collections héritées enséquences IEnumerable<T> : Cast et OfType (voir Listing 1.9).

Listing 1.9 : Conversion d’une collection héritée en un IEnumerable<T> avec l’opérateur Cast.

// Création d’une collection héritéeArrayList arrayList = new ArrayList();// L’initialisation de collections ne fonctionne pas// avec les collections héritéesarrayList.Add("Adams");arrayList.Add("Arthur");arrayList.Add("Buchanan");IEnumerable<string> names = arrayList.Cast<string>().Where(n => n.Length < 7);foreach(string name in names) Console.WriteLine(name);

Le Listing 1.10 représente le même exemple, en utilisant cette fois-ci l’opérateurOfType.

Listing 1.10 : Utilisation de l’opérateur OfType.

// Création d’une collection héritéeArrayList arrayList = new ArrayList();// L’initialisation de collections ne fonctionne pas// avec les collections héritéesarrayList.Add("Adams");arrayList.Add("Arthur");arrayList.Add("Buchanan");IEnumerable<string> names = arrayList.OfType<string>().Where(n => n.Length < 7);foreach(string name in names) Console.WriteLine(name);

Ces deux exemples produisent le même résultat :

Ces deux opérateurs sont quelque peu différents : Cast essaye de convertir tous leséléments de la collection dans le type spécifié. Une exception est générée si un des

3/21/1997 12:00:00 AM - 10482 - Lazy K Kountry Store5/22/1997 12:00:00 AM - 10545 - Lazy K Kountry Store…4/17/1998 12:00:00 AM - 11032 - White Clover Markets5/1/1998 12:00:00 AM - 11066 - White Clover Markets

AdamsArthur

Linq.book Page 14 Mercredi, 18. février 2009 7:58 07

Page 30: LINQ Language Integrated Query en C

Chapitre 1 Hello LINQ 15

éléments ne peut pas être converti. Au contraire, OfType ne convertit que les élémentsqui peuvent l’être.

Préférez l’opérateur OfType à l’opérateur Cast

Les génériques ont été implémentés dans C# pour permettre une vérification de typestatique (c’est-à-dire pendant la compilation) sur les collections. Avant l’apparition desgénériques, il n’y avait aucun moyen de s’assurer que les éléments d’une collectionhéritée (un ArrayList ou un Hashtable, par exemple) étaient tous de même type etavaient le type requis. Rien par exemple n’empêchait l’insertion d’un objet Textboxdans un ArrayList supposé ne contenir que des objets Label.

Avec l’apparition des génériques dans C# 2.0, les développeurs peuvent désormaiss’assurer qu’une collection ne contient que des éléments dont le type est spécifié. Bienque les opérateurs OfType et Cast soient utilisables sur une collection héritée, Castnécessite que tous les objets de la collection aient le type attendu. Pour éviter de générerdes exceptions en cas d’incompatibilité de type, préférez-lui l’opérateur OfType. Parson intermédiaire, seuls les objets du type spécifié seront stockés dans la séquenceIEnumerable<T>, et aucune exception ne sera générée. Le cas échéant, les objets dont letype n’est pas celui attendu ne seront pas convertis.

Les requêtes aussi peuvent être boguées

Au Chapitre 3, vous verrez que les requêtes LINQ sont souvent différées. Elles ne sontdonc pas exécutées dès leur invocation. Considérez par exemple le code suivant, extraitdu Listing 1.1 :

var items = from s in greetings where s.EndsWith("LINQ") select s;

foreach (var item in items) Console.WriteLine(item);

Contrairement à ce que vous pourriez penser, la requête n’est pas exécutée à l’initialisa-tion de la variable items. Elle ne sera exécutée que lorsqu’une ligne de code aurabesoin de son résultat ; typiquement lors de l’énumération du résultat de la requête. Ici,le résultat de la requête n’est pas calculé jusqu’à ce que l’instruction foreach soitexécutée.

On oublie souvent que l’exécution d’une requête est différée jusqu’à l’énumération desa séquence. Une requête mal formulée pourrait ainsi produire une erreur bien deslignes plus loin, lorsque sa séquence est énumérée, et le programmeur pourrait avoir dumal à penser que la requête en est l’origine.

Examinons le code du Listing 1.11.

Linq.book Page 15 Mercredi, 18. février 2009 7:58 07

Page 31: LINQ Language Integrated Query en C

16 LINQ et C# 2008 Partie I

Listing 1.11 : Cette requête contient une erreur intentionnelle qui n’est levée qu’à l’énumération.

string[] strings = { "un", "deux", null, "trois" };

Console.WriteLine("Avant l’appel à Where()");IEnumerable<string> ieStrings = strings.Where(s => s.Length == 3);Console.WriteLine("Après l’appel à Where()");

foreach(string s in ieStrings){ Console.WriteLine("Traitement " + s);}

Le troisième élément du tableau a pour valeur null. L’expression null.Length vaproduire une exception lors de l’énumération de la séquence ieStrings, et en parti-culier de son troisième élément. Pourtant, la ligne à l’origine de l’erreur est allègrementpassée… Voici le résultat obtenu à l’exécution de ce code :

L’opérateur Where n’a pas produit d’exception. L’exception a seulement été levéelorsque l’on a essayé de lire le troisième élément de la séquence. Imaginez que laséquence ieStrings soit passée à une fonction qui énumère la séquence dans uneliste déroulante ou un contrôle équivalent. Penseriez-vous que l’exception provient dela requête LINQ ? Il y a de grandes chances pour que vous cherchiez l’erreur dans lecode de la fonction…

Sachez tirer parti des requêtes différées

Au Chapitre 3, vous en apprendrez bien plus sur les requêtes différées. Cependant, jevoudrais dès à présent insister sur le fait que, si une requête différée retourne un IEnu-merable<T>, cet objet peut être énuméré autant de fois que nécessaire sans pour autantdevoir rappeler la requête.

La plupart des codes de cet ouvrage appellent une requête et stockent l’IEnumera-ble<T> retourné dans une variable. Une instruction foreach est alors appliquée sur laséquence IEnumerable<T> à des fins démonstratives. Si ce code est exécuté à plusieursreprises, il n’est pas nécessaire de rappeler la requête à chaque exécution. Il serait plusjudicieux d’écrire une méthode d’initialisation et d’y placer toutes les requêtes néces-saires. Cette méthode serait appelée une fois. Vous pourriez alors énumérer la séquencede votre choix pour obtenir la dernière version des résultats.

Avant l’appel à Where()Après l’appel à Where()Traitement unTraitement deux

Unhandled Exception: System.NullReferenceException: Object reference not set to aninstance of an object.…

Linq.book Page 16 Mercredi, 18. février 2009 7:58 07

Page 32: LINQ Language Integrated Query en C

Chapitre 1 Hello LINQ 17

Utiliser le log du DataContext

Lorsque vous travaillerez avec LINQ to SQL, vous devrez garder à l’esprit que la classerelative à la base de données, générée par SQLMetal, hérite de System.Data.Linq.Data-Context. Cette classe dispose donc de quelques fonctionnalités préinstallées. Entreautres de l’objet TextWriter Log.

Si vous avez déjà expérimenté une rupture de code liée aux données, vous serez ravid’apprendre qu’il est possible d’utiliser l’objet Log du DataContext pour observer lesdonnées résultant de la requête, tout comme vous le feriez dans SQL Server EnterpriseManager ou Query Analyzer (voir l’exemple du Listing 1.12).

Listing 1.12 : Un exemple d’utilisation du log du DataContext.

Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind");

db.Log = Console.Out;

IQueryable<Order> orders = from c in db.Customers from o in c.Orders where c.Country == "USA" && c.Region == "WA" select o;

foreach(Order item in orders) Console.WriteLine("{0} - {1} - {2}", item.OrderDate, item.OrderID, item.ShipName);

Ce code produit la sortie suivante dans la console :

SELECT [t1].[OrderID], [t1].[CustomerID], [t1].[EmployeeID], [t1].[OrderDate],[t1].[RequiredDate], [t1].[ShippedDate], [t1].[ShipVia], [t1].[Freight],[t1].[ShipName], [t1].[ShipAddress], [t1].[ShipCity], [t1].[ShipRegion],[t1].[ShipPostalCode], [t1].[ShipCountry]FROM [dbo].[Customers] AS [t0], [dbo].[Orders] AS [t1]WHERE ([t0].[Country] = @p0) AND ([t0].[Region] = @p1) AND ([t1].[CustomerID] =[t0].[CustomerID])-- @p0: Input String (Size = 3; Prec = 0; Scale = 0) [USA]-- @p1: Input String (Size = 2; Prec = 0; Scale = 0) [WA]-- Context: SqlProvider(Sql2005) Model: AttributedMetaModel Build: 3.5.20706.13/21/1997 12:00:00 AM - 10482 - Lazy K Kountry Store5/22/1997 12:00:00 AM - 10545 - Lazy K Kountry Store6/19/1997 12:00:00 AM - 10574 - Trail’s Head Gourmet Provisioners6/23/1997 12:00:00 AM - 10577 - Trail’s Head Gourmet Provisioners1/8/1998 12:00:00 AM - 10822 - Trail’s Head Gourmet Provisioners7/31/1996 12:00:00 AM - 10269 - White Clover Markets11/1/1996 12:00:00 AM - 10344 - White Clover Markets3/10/1997 12:00:00 AM - 10469 - White Clover Markets3/24/1997 12:00:00 AM - 10483 - White Clover Markets4/11/1997 12:00:00 AM - 10504 - White Clover Markets7/11/1997 12:00:00 AM - 10596 - White Clover Markets10/6/1997 12:00:00 AM - 10693 - White Clover Markets10/8/1997 12:00:00 AM - 10696 - White Clover Markets10/30/1997 12:00:00 AM - 10723 - White Clover Markets11/13/1997 12:00:00 AM - 10740 - White Clover Markets1/30/1998 12:00:00 AM - 10861 - White Clover Markets2/24/1998 12:00:00 AM - 10904 - White Clover Markets4/17/1998 12:00:00 AM - 11032 - White Clover Markets5/1/1998 12:00:00 AM - 11066 - White Clover Markets

Linq.book Page 17 Mercredi, 18. février 2009 7:58 07

Page 33: LINQ Language Integrated Query en C

18 LINQ et C# 2008 Partie I

Utilisez le forum LINQ

Il y a fort à parier que, tôt ou tard, vous vous retrouverez dans une situation bloquanteen expérimentant LINQ. N’hésitez pas à faire appel au forum dédié à LINQ surMSDN.com, en vous connectant à l’adresse www.linqdev.com. Ce forum est suivi parles développeurs Microsoft. Vous y trouverez de nombreuses ressources très intéressantes.

Résumé

Je sens que vous êtes impatient de passer au chapitre suivant. Je voudrais cependantvous rappeler quelques petites choses avant que vous ne tourniez les pages.

LINQ va changer la façon dont les développeurs .NET interrogent leurs données. Leséditeurs de logiciels vont certainement ajouter un sticker "Compatible LINQ" sur leursproduits, tout comme ils le font actuellement avec XML.

Gardez bien en mémoire que LINQ n’est pas juste une nouvelle librairie que vous ajou-tez à vos projets. Il s’agit d’une tout autre approche pour interroger vos données,consistant en plusieurs composants qui dépendent de la source de données à interroger.Alors que nous écrivons ces lignes, vous pouvez utiliser LINQ pour interroger descollections de données en mémoire avec LINQ to Objects, des fichiers XML avecLINQ to SQL, des DataSets avec LINQ to DataSets et des bases de données SQLServer avec LINQ to SQL.

Rappelez-vous également que LINQ n’est pas simplement un langage de requête. Dansun de mes projets, j’ai utilisé LINQ avec succès non seulement pour interroger dessources de données, mais également pour modifier le format des données afin de lesprésenter dans une fenêtre WinForm.

Enfin, j’espère que vous tiendrez compte des astuces que j’ai mentionnées à la fin de cechapitre. Si vous ne comprenez pas entièrement certaines d’entre elles, ce n’est pas unproblème. Vous en saisirez toutes les subtilités au fur et à mesure de votre progressiondans le livre. Stockez-les dans un coin de votre tête : elles vous feront gagner du temps.

Après vous être intéressé aux exemples et conseils de ce chapitre, vous êtes peut-êtreperplexe devant la syntaxe de LINQ. Ne vous en faites pas, au prochain chapitre vousallez découvrir en détail toutes les modifications apportées au langage C# 3.0 parMicrosoft et comprendrez plus facilement le code.

Linq.book Page 18 Mercredi, 18. février 2009 7:58 07

Page 34: LINQ Language Integrated Query en C

2

Améliorations de C# 3.0pour LINQ

Le chapitre précédent vous a initié au monde merveilleux de LINQ. J’y ai donné quel-ques exemples pour attiser votre appétit et des astuces qui pourront vous paraître quel-que peu prématurées. Certaines syntaxes vous laissent peut-être perplexe, car le coderevêt un aspect entièrement nouveau. C# a en effet dû être remanié pour supporter lesfonctionnalités avancées de LINQ. Dans ce chapitre, vous allez découvrir les facettesles plus innovantes de C# 3.0.

Les nouveautés du langage C# 3.0

Pour que LINQ s’intègre parfaitement dans C#, des améliorations significatives ont dûêtre apportées au langage. Toutes les améliorations déterminantes ont été dictées par lesupport de LINQ. Bien que chacune d’entre elles soit intéressante en tant que telle, c’estl’ensemble qui fait de C# 3.0 un langage si puissant.

Pour bien comprendre la syntaxe de LINQ, vous devez au préalable vous intéresser àcertaines nouvelles fonctionnalités de C# 3.0. Ce chapitre va passer en revue lesnouveautés suivantes :

m les expressions lambda ;

m les arbres d’expressions ;

m le mot-clé var, l’initialisation des objets et des collections et les types anonymes ;

m les méthodes d’extension ;

m les méthodes partielles ;

m les expressions de requête.

Linq.book Page 19 Mercredi, 18. février 2009 7:58 07

Page 35: LINQ Language Integrated Query en C

20 LINQ et C# 2008 Partie I

Les assemblies et espaces de noms nécessaires à la bonne exécution des exemples de cechapitre ne seront pas mentionnés s’ils ont déjà été utilisés au Chapitre 1. En revanche,les nouveaux assemblies et espaces de noms seront signalés lors de leur première utili-sation.

Les expressions lambda

Bien qu’inventées en 1936 par le mathématicien américain Alonzo Church et utiliséesdans des langages aussi anciens que LISP, les expressions lambda sont une nouveautédu langage C# 3.0. Leur but premier vise à simplifier la syntaxe des algorithmes.

Avant de nous intéresser aux expressions lambda, nous allons nous attarder quelquesinstants sur la possibilité de passer un algorithme dans un argument d’une méthode.

Utilisation de méthodes nomméesAvant la sortie de C# 2.0, lorsqu’une méthode/une variable avait besoin d’un délégué,le développeur devait créer une méthode nommée et passer ce nom à chaque utilisationdu délégué.

Supposons que deux développeurs travaillent sur un même projet. Le développeurnuméro 1 crée un code réutilisable et le développeur numéro 2 utilise ce code pourcréer une application. Supposons que le développeur 1 définisse une méthode généri-que permettant de filtrer des tableaux d’entiers, en permettant de spécifier l’algorithmede tri à utiliser. Dans un premier temps, il crée un délégué qui reçoit un entier etretourne la valeur true si la valeur passée peut être incluse dans le tableau.

Ainsi, il créé une classe utilitaire et ajoute le délégué et la méthode de filtre. Voici lecode utilisé :

public class Common{ public delegate bool IntFilter(int i); public static int[] FilterArrayOfInts(int[] ints, IntFilter filter) { ArrayList aList = new ArrayList(); foreach (int i in ints) { if (filter(i)) { aList.Add(i); } } return ((int[])aList.ToArray(typeof(int))); }}

Le développeur numéro 1 a placé le délégué et la méthode FilterArrayOfInt() dansune DLL (Dynamic Link Library) afin de les rendre accessibles dans plusieurs applications.

Linq.book Page 20 Mercredi, 18. février 2009 7:58 07

Page 36: LINQ Language Integrated Query en C

Chapitre 2 Améliorations de C# 3.0 pour LINQ 21

La méthode FilterArrayOfInt() du listing précédent admet deux paramètres enentrée : le tableau à trier et un délégué qui fait référence à la méthode de tri à utiliser. Letableau d’entiers trié est renvoyé par la méthode.

Supposons maintenant que le développeur numéro 2 veuille limiter le tri aux entiersimpairs. Voici la méthode de tri utilisée :

public class Application{ public static bool IsOdd(int i) { return ((i & 1) == 1); }}

En se basant sur le code de la méthode FilterArrayOfInts, la méthode IsOdd seraappelée pour tous les entiers du tableau qui lui seront passés. Ce filtre ne retournera lavaleur true que dans le cas où l’entier passé est impair. Le Listing 2.1 donne un exempled’utilisation de la méthode FilterArrayOfInts.

Listing 2.1 : Appel de la méthode commune FilterArrayOfInts.

using System.Collections;

int[] nums = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };

int[] oddNums = Common.FilterArrayOfInts(nums, Application.IsOdd);

foreach (int i in oddNums) Console.WriteLine(i);

Voici le résultat :

Comme vous pouvez le remarquer, pour passer le délégué dans le second paramètre dela méthode FilterArrayOfInts, il suffit d’indiquer son nom. En définissant un autrefiltre, le résultat peut être tout autre. Il est ainsi possible de définir un filtre pour lesnombres pairs, pour les nombres premiers ou pour un tout autre critère. Les déléguéssont intéressants chaque fois que le code doit être utilisé à plusieurs reprises.

Utiliser des méthodes anonymesCet exemple fonctionne à la perfection, mais à la longue il peut être fastidieux d’écriretous les filtres et autres délégués dont vous avez besoin : la plupart de ces méthodes serontappelées une seule fois et il peut être frustrant de créer autant de méthodes que de trisnécessaires. Depuis C# 2.0, les développeurs peuvent faire appel aux méthodes anony-mes, afin de passer du code comme argument et ainsi d’éviter l’utilisation de délégués.

1357

Linq.book Page 21 Mercredi, 18. février 2009 7:58 07

Page 37: LINQ Language Integrated Query en C

22 LINQ et C# 2008 Partie I

Dans cet exemple, plutôt que créer la méthode IsOdd, le code de filtrage est passé dansl’argument (voir Listing 2.2).

Listing 2.2 : Appel du filtre par l’intermédiaire d’une méthode anonyme.

int[] nums = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };

int[] oddNums = Common.FilterArrayOfInts(nums, delegate(int i) { return ((i & 1) == 1); });

foreach (int i in oddNums) Console.WriteLine(i);

Comme vous le voyez, il n’est plus nécessaire de définir une méthode de filtrage. Cettetechnique est particulièrement intéressante si le code qui remplace le délégué a peu dechances d’être utilisé à plusieurs reprises. Le résultat est bien entendu identique à celuide l’exemple précédent :

Les méthodes anonymes ont un inconvénient : elles sont verbeuses et difficiles à lire. Ilserait vraiment agréable de pouvoir écrire le code de la méthode d’une manière plusconcise !

Utiliser les expressions lambdaEn C#, les expressions lambda consistent en une liste de paramètres séparés entre euxpar des virgules1, suivis de l’opérateur lambda (=>) puis d’une expression ou d’unedéclaration.

(param1, param2, …paramN) => expr

Si l’expression/la déclaration est plus complexe, vous pouvez utiliser un bloc délimitépar les caractères { et } :

(param1, param2, …paramN) =>{ statement1; statement2; ... statementN; return(lambda_expression_return_type);}

Dans cet exemple, le type de données renvoyé par l’instruction return doit correspondreau code de retour spécifié par le délégué. Voici un exemple d’expression lambda :

x => x

1357

1. Si les paramètres sont au nombre de deux (ou plus), ils doivent être délimités par des parenthèses.

Linq.book Page 22 Mercredi, 18. février 2009 7:58 07

Page 38: LINQ Language Integrated Query en C

Chapitre 2 Améliorations de C# 3.0 pour LINQ 23

Cette expression lambda pourrait se lire "x conduit à x" ou encore "entrée x sortie x".Cela signifie que la variable d’entrée x est également renvoyée par l’expression lambda.Étant donné que la fonction ne compte qu’un seul paramètre en entrée, il n’est pasnécessaire de l’entourer de parenthèses. Il est important d’avoir à l’esprit que le déléguédétermine le type de l’entrée x ainsi que le type qui doit être retourné. Par exemple, si ledélégué définit une chaîne en entrée et retourne un booléen, l’expression x => x ne peutpas être utilisée. Dans ce cas, la partie à droite de l’opérateur lambda doit retourner unbooléen. Par exemple :

x => x.Length > 0

Cette expression lambda pourrait se lire "x conduit à x.Length > 0" ou encore "entrée x,sortie x.Length > 0". Étant donné que la partie à droite de l’opérateur lambda est équi-valente à un booléen, le délégué doit indiquer que la méthode renvoie un booléen, sansquoi une erreur se produira à la compilation.

L’expression lambda ci-après tente de retourner la longueur de l’argument fourni enentrée. Le délégué doit donc spécifier que la valeur retournée est de type entier (int).

s => s.Length

Si plusieurs paramètres sont passés en entrée de l’expression lambda, séparez-les pardes virgules et entourez-les par des parenthèses, comme dans l’expression suivante :

(x, y) => x == y

Les expressions lambda complexes peuvent être spécifiées à l’intérieur d’un bloc,comme dans :

(x, y) =>{ if (x > y) return (x); else return (y);}

ATTENTIONATTENTION

Gardez à l’esprit que le délégué doit indiquer le type des paramètres en entrée et del’élément renvoyé. Dans tous les cas, assurez-vous que ces éléments sont en accord avec lestypes définis dans le délégué.

Pour vous rafraîchir la mémoire, voici la déclaration delegate définie par le programmeurnuméro 1 :

delegate bool IntFilter(int i);

L’application développée par le programmeur numéro 2 devra accepter un paramètre detype int et retourner une valeur de type bool. Cela peut se déduire de la méthode appeléeet du but du filtre, mais dans tous les cas rappelez-vous que c’est le délégué qui dicteles types en entrée et en sortie.

Linq.book Page 23 Mercredi, 18. février 2009 7:58 07

Page 39: LINQ Language Integrated Query en C

24 LINQ et C# 2008 Partie I

En utilisant une expression lambda, l’exemple précédent se transforme en leListing 2.3.

Listing 2.3 : Appel du filtre avec une expression lambda.

int[] nums = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };

int[] oddNums = Common.FilterArrayOfInts(nums, i => ((i & 1) == 1));

foreach (int i in oddNums) Console.WriteLine(i);

Ce code est vraiment concis. S’il vous semble quelque peu déroutant, une fois que vousy serez habitué vous verrez à quel point il est réutilisable et facile à maintenir. Bienentendu, les résultats sont les mêmes que dans les exemples précédents :

Pour récapituler, voici quelques instructions concernant les trois approches dont nousvenons de parler :

int[] oddNums = // Approche méthode nommée Common.FilterArrayOfInts(nums, Application.IsOdd);

int[] oddNums = // Approche méthode anonyme Common.FilterArrayOfInts(nums, delegate(int i){return((i & 1) == 1);});

int[] oddNums = // Approche expression lambda Common.FilterArrayOfInts(nums, i => ((i & 1) == 1));

La première version semble plus courte que les autres, mais vous devez garder à l’espritqu’elle est associée à une méthode nommée dans laquelle est défini le traitement àeffectuer. Cette alternative sera certainement le meilleur choix si la méthode doit êtreréutilisée et/ou si l’algorithme mis en œuvre est complexe et/ou doit être confié à desspécialistes.

ASTUCE

Les algorithmes complexes et/ou réutilisés sont mieux gérés par des méthodes nommées. Ilssont alors accessibles à tout développeur, même s’il ne saisit pas toutes les nuances du codemis en œuvre.

C’est au développeur de choisir quelle méthode est la plus appropriée dans son casprécis : une méthode nommée, une méthode anonyme ou une expression lambda.

Les expressions lambda peuvent être passées comme argument des requêtes LINQ.Étant donné que ces requêtes ont toutes les chances d’utiliser des arguments à usageunique ou en tout cas peu réutilisés, l’alternative des opérateurs lambda offre une

1357

Linq.book Page 24 Mercredi, 18. février 2009 7:58 07

Page 40: LINQ Language Integrated Query en C

Chapitre 2 Améliorations de C# 3.0 pour LINQ 25

grande flexibilité et n’oblige pas le programmeur à écrire une méthode nommée pourchaque requête.

Arbres d’expressions

Les arbres d’expressions permettent de représenter sous la forme d’arbres les expres-sions lambda utilisées dans des requêtes. Ils autorisent l’évaluation simultanée de tousles opérateurs impliqués dans une requête. Ils semblent donc parfaitement adaptés à lamanipulation de sources de données telles que celles embarquées dans une base dedonnées.

Dans la plupart des exemples passés en revue jusqu’ici, les opérateurs de requête ont étéexécutés de façon séquentielle. Examinons le code ci-après :

int[] nums = new int[] { 6, 2, 7, 1, 9, 3 };IEnumerable<int> numsLessThanFour = nums .Where(i => i < 4) .OrderBy(i => i);

Cette requête utilise les opérateurs Where et OrderBy, qui attendent des méthodes délé-guées en argument. Lorsque ce code est compilé, L’IL (Intermediate Language) .NETfabriqué est identique à celui que produirait une méthode anonyme pour chacun desopérateurs des expressions lambda.

À l’exécution, les opérateurs Where puis OrderBy sont appelés successivement.

Cette exécution séquentielle des opérateurs semble convenir dans cet exemple, maissupposez que cette requête soit appliquée dans une source de données volumineuse(une base de données, par exemple). Cela aurait-il un sens de filtrer les données unepremière fois avec l’opérateur Where, puis une seconde avec l’opérateur OrderBy. Cettetechnique n’est évidemment pas applicable aux requêtes de bases de données ni poten-tiellement à d’autres types de requêtes. C’est ici que les arbres d’expressions prennenttoute leur importance. Ils autorisent en effet l’évaluation et l’exécution simultanées detous les opérateurs d’une requête.

Le compilateur est donc maintenant en mesure de coder deux types de codes pour uneexpression lambda : du code IL ou un arbre d’expressions. C’est le prototype del’opérateur qui détermine quel type de code sera généré. Si sa déclaration l’autorise àaccepter une méthode déléguée, du code IL sera généré. Si sa déclaration l’autoriseà accepter une expression d’une méthode déléguée, un arbre d’expressions sera généré.

À titre d’exemple, nous allons nous intéresser à deux implémentations différentes del’opérateur Where. La première est l’opérateur de requête standard Where de l’APILINQ to Objects, définie dans la classe System.Linq.Enumerable :

public static IEnumerable<T> Where<T>(this IEnumerable<T> source,Func<T, bool> predicate);

Linq.book Page 25 Mercredi, 18. février 2009 7:58 07

Page 41: LINQ Language Integrated Query en C

26 LINQ et C# 2008 Partie I

La seconde implémentation de l’opérateur Where provient de l’API LINQ to SQL et dela classe System.Linq.Queryable :

public static IQueryable<T> Where<T>(this IQueryable<T> source,System.Linq.Expressions.Expression<Func<int, bool>> predicate);

Comme vous pouvez le voir, le premier opérateur Where accepte la méthode déléguéeFunc en argument. Du code IL sera donc généré par le compilateur pour l’expressionlambda de cet opérateur. Reportez-vous au Chapitre 3 pour avoir plus d’informationssur le délégué Func. Pour l’instant, il vous suffit de comprendre que le délégué Funcdéfinit la signature de l’argument. Le deuxième opérateur Where accepte un arbred’expressions (Expression) en argument. Le compilateur générera donc un arbre d’expres-sions pour représenter les données.

Les opérateurs qui admettent une séquence IEnumerable<T> comme premier argumentutilisent des délégués pour manipuler les expressions lambda. En revanche, les opéra-teurs qui admettent une séquence IQueryable<T> comme premier argument utilisentdes arbres d’expressions.

INFO

Le compilateur produit du code IL pour les méthodes d’extension des séquences IEnumera-ble<T>, alors qu’il produit des arbres d’expressions pour les méthodes d’extension desséquences IQueryable<T>.

Le développeur qui se contente d’utiliser LINQ n’est pas obligé de connaître les tenantset les aboutissants des arbres d’expressions. C’est la raison pour laquelle cet ouvragen’ira pas plus loin dans les fonctionnalités avancées des arbres d’expressions.

Le mot-clé var, l’initialisation d’objets et les types anonymes

Il est quasiment impossible de s’intéresser au mot-clé var et à l’inférence de type sansaborder l’initialisation des objets et les types anonymes. De même, il est quasimentimpossible de s’intéresser à l’initialisation d’objets et aux types anonymes en passantsous silence le mot-clé var.

Étant donné leurs fortes imbrications, plutôt que décrire séparément ces trois nouveau-tés du langage C#, je vais vous les présenter simultanément. Examinez la déclaration ci-après :

var1 mySpouse = new {2 FirstName = "Vickey"3, LastName = "Rattz" };

1. Le mot-clé var apparaît clairement devant le nom de la variable.2. Un type anonyme sera utilisé, car l’opérateur new est utilisé sans préciser une classe nommée.3. L’objet anonyme sera explicitement initialisé en utilisant la nouvelle fonctionnalité d’initialisationd’objet.

Linq.book Page 26 Mercredi, 18. février 2009 7:58 07

Page 42: LINQ Language Integrated Query en C

Chapitre 2 Améliorations de C# 3.0 pour LINQ 27

Dans cet exemple, la variable mySpouse est déclarée en utilisant le mot-clé var. Cettevariable se voit assigner un type anonyme… dont le type est connu grâce aux nouveau-tés de C# en matière d’initialisation d’objets. Cette simple ligne de code tire parti dumot-clé var, des types anonymes et de l’initialisation d’objets.

Pour résumer, le mot-clé var permet de déduire le type d’un objet en tenant compte dutype des données utilisées pour l’initialiser. Les types anonymes permettent donc de créerdes types de classes à la volée. Comme le laisse prévoir le mot "anonyme", ces nouveauxtypes de données n’ont pas de nom. Il n’est pas simple de créer une donnée anonymesans connaître ses variables membres, et vous ne pouvez pas connaître ses variablesmembres sans connaître leurs types. Enfin, vous ne pouvez pas connaître le type de sesmembres jusqu’à ce qu’ils soient initialisés. Mais, rassurez-vous, la fonctionnalitéd’initialisation de C# 3.0 gère tout ce fatras pour vous !

Lorsque cette ligne de code passera entre les mains du compilateur, une nouvelle classede type anonyme sera créée. Elle contiendra deux membres de type String : FirstNameet LastName.

Le mot-clé var est implicitement typé pour les variables localesL’introduction des types anonymes dans le langage C# a induit un problème sous-jacent : si une variable dont le type n’est pas défini est instanciée avec un objet de typeanonyme, quel sera le type de la variable ? Considérez le code ci-après :

// Ce code n’est pas compilable !??? unnamedTypeVar = new {firstArg = 1, secondArg = "Joe" };

Quel type déclareriez-vous pour la variable unnamedTypeVar ? Pour résoudre ceproblème, le mot-clé var a été défini par les ingénieurs en charge du développement dulangage C# chez Microsoft. Ce mot-clé informe le compilateur qu’il doit implicitementdéfinir le type de la variable en utilisant l’initialiseur de la variable.

Si vous ne définissez pas un initialiseur, il en résultera une erreur à la compilation. LeListing 2.4 représente un code qui déclare une variable avec le mot-clé var sans l’initia-liser.

Listing 2.4 : Une déclaration de variable invalide utilisant le mot-clé var.

var name;

Voici l’erreur générée par le compilateur.

Étant donné que le type des variables est vérifié de façon statique à la compilation, il estnécessaire de définir un initialiseur pour que le compilateur puisse faire son travailjusqu’au bout. Mais, attention, vous ne devrez pas affecter une valeur d’un autre type à

Implicitly-typed local variables must be initialized

Linq.book Page 27 Mercredi, 18. février 2009 7:58 07

Page 43: LINQ Language Integrated Query en C

28 LINQ et C# 2008 Partie I

cette variable dans la suite du code, sans quoi une erreur se produira à la compilation.Examinons le code du Listing 2.5.

Listing 2.5 : Une affectation incorrecte à une variable déclarée avec le mot-clé var.

var name = "Joe"; // Jusqu’ici, tout va bienname = 1; // Ceci est incorrect !Console.WriteLine(name);

Ce code ne passera pas l’étape de la compilation, car le type de la variable est implici-tement défini à String par sa première affectation. Il est donc impossible de lui affecterune valeur entière par la suite. Voici l’erreur générée par le compilateur :

Comme vous le voyez, le compilateur s’occupe de la cohérence du type des donnéesaffectées à la variable. Pour en revenir à la déclaration du type anonyme unnamedType-Var, la syntaxe à utiliser est celle du Listing 2.6.

Listing 2.6 : Un type anonyme affecté à une variable déclarée avec le mot-clé var.

var unnamedTypeVar = new {firstArg = 1, secondArg = "Joe" };Console.WriteLine(unnamedTypeVar.firstArg + ". " + unnamedTypeVar.secondArg);

Voici le résultat de ce code :

L’utilisation du mot-clé var apporte deux avantages : la vérification de type statique etla flexibilité apportée par le support des types anonymes. Ce dernier point deviendratrès important lorsque nous nous intéresserons aux opérateurs de projection dans lasuite de l’ouvrage.

Dans les exemples passés en revue jusqu’ici, le mot-clé var était obligatoire. En effet, sivous affectez un objet résultant d’une classe anonyme à une variable, cette dernière doitêtre déclarée avec le mot-clé var. Notez cependant que le mot-clé var peut être utilisé àchaque déclaration de variable, à condition que cette dernière soit correctement initialisée.Pour des questions de maintenance du code, il n’est cependant pas conseillé d’abuser decette technique : les développeurs devraient toujours connaître le type des données qu’ilsmanipulent. Bien sûr, vous connaissez le type de vos données aujourd’hui, mais qu’ensera-t-il dans six mois ? Et si un autre programmeur prend la relève ?

ASTUCE

Afin de faciliter la maintenance de votre code, n’abusez pas du mot-clé var. Ne l’utilisez quelorsque cela est nécessaire. Par exemple lorsque vous affectez un objet de type anonyme àune variable.

Cannot implicitly convert type ’int’ to ’string’

1.Joe

Linq.book Page 28 Mercredi, 18. février 2009 7:58 07

Page 44: LINQ Language Integrated Query en C

Chapitre 2 Améliorations de C# 3.0 pour LINQ 29

Expressions d’initialisation d’objets et de collectionsLes types anonymes autorisant l’utilisation de types de données dynamiques, le moded’initialisation des objets et des collections a été simplifié, essentiellement grâce auxexpressions lambda ou aux arbres d’expressions.

Initialisation d’objetsVous pouvez désormais spécifier les valeurs des membres et propriétés public d’uneclasse pendant son instanciation :

public class Address{ public string address; public string city; public string state; public string postalCode;}

Sans la fonctionnalité d’initialisation ajoutée à C# 3.0, vous n’auriez pas pu utiliser unconstructeur spécialisé, et vous auriez dû définir un objet de type Address, comme dansle Listing 2.7.

Listing 2.7 : Instanciation et initialisation de la classe avec l’ancienne méthode.

Address address = new Address();address.address = "105 Elm Street";address.city = "Atlanta";address.state = "GA";address.postalCode = "30339";

Cette technique serait très lourde dans une expression lambda. Supposons que vousayez défini une requête à partir d’une source de données et que vous vouliez projetercertains membres dans un objet Address en utilisant l’opérateur Select :

// Ce code ne passera pas la compilationIEnumerable<Address> addresses = somedatasource .Where(a => a.State = "GA") .Select(a => new Address(???)???);

Il n’existe aucun moyen simple d’initialiser les membres de l’objet Address. N’ayezcrainte : l’initialisation d’objet de C# 3.0 est la solution. Bien sûr, il serait possible decréer un constructeur qui vous permettrait de passer les valeurs à initialiser à l’instan-ciation de l’objet. Mais quel travail !

Le Listing 2.8 montre comment résoudre le problème par l’intermédiaire d’un typeanonyme construit à la volée.

Listing 2.8 : Instanciation et initialisation de la classe avec la nouvelle méthode.

Address address = new Address { address = "105 Elm Street", city = "Atlanta", state = "GA", postalCode = "30339" };

Linq.book Page 29 Mercredi, 18. février 2009 7:58 07

Page 45: LINQ Language Integrated Query en C

30 LINQ et C# 2008 Partie I

Les expressions lambda autorisent ce genre de manipulation, y compris en dehors desrequêtes LINQ !

Le compilateur instancie les membres nommés avec les valeurs spécifiées. Les éven-tuels membres non spécifiés utiliseront le type de données par défaut.

Initialisation de collectionsLes ingénieurs de Microsoft ont également mis au point une technique d’initialisationde collections. Il vous suffit pour cela de spécifier les valeurs de la collection, toutcomme vous le feriez pour un objet. Une restriction : la collection doit implémenterl’interface System.Collections.Generic.ICollection<T>. Les collections C# héri-tées (celles qui se trouvent dans l’espace de noms System.Collection) ne sont pasconcernées. Le Listing 2.9 donne un exemple d’initialisation de collection.

Listing 2.9 : Un exemple d’initialisation de collection.

using System.Collections.Generic;

List<string> presidents = new List<string> { "Adams", "Arthur", "Buchanan" };foreach(string president in presidents){ Console.WriteLine(president);}

Voici le résultat obtenu lorsque vous exécutez le programme en appuyant sur Ctrl+F5 :

Vous pouvez également utiliser cette technique pour créer facilement des collectionsinitialisées dans le code, même si vous n’utilisez pas LINQ.

Types anonymesC# étant dans l’impossibilité de créer de nouveaux types de données à la compilation, ilest difficile de définir une nouvelle API agissant au niveau du langage pour les requêtesgénériques. Les ingénieurs qui ont mis au point le langage C# 3.0 ont relevé cetteprouesse : désormais, il est possible de créer dynamiquement des classes non nomméeset des propriétés dans ces classes. Ce type de classe est appelé "type anonyme".

Un type anonyme n’a pas de nom et est généré à la compilation, en initialisant un objeten cours d’instanciation. Étant donné que la classe n’a pas de type, toute variable affec-tée à un objet d’un type anonyme doit pouvoir le déclarer. C’est là qu’intervient le mot-clé new de C# 3.0.

Un type anonyme ne peut pas être estimé s’il est issu d’un opérateur Select ou Select-Many. Sans les types anonymes, des classes nommées devraient être définies pour rece-

AdamsArthurBuchanan

Linq.book Page 30 Mercredi, 18. février 2009 7:58 07

Page 46: LINQ Language Integrated Query en C

Chapitre 2 Améliorations de C# 3.0 pour LINQ 31

voir des données issues des opérateurs Select ou SelectMany. Ceci se révélerait trèslourd et peu pratique à mettre en place.

Dans la section relative à l’initialisation d’objets, j’ai introduit le code d’instanciation etd’initialisation suivant :

Address address = new Address { address = "105 Elm Street", city = "Atlanta", state = "GA", postalCode = "30339" };

Pour utiliser un type anonyme à la place de la classe nommée Address, il suffit d’omet-tre le nom de la classe. Notez cependant qu’il est impossible de stocker le nouvel objetinstancié dans une variable de type Address, car l’objet n’est pas encore de typeAddress. Son type n’est connu que du compilateur. Il est donc également nécessaire dechanger le type de données de la variable address en utilisant le mot-clé var (voirListing 2.10).

Listing 2.10 : Instanciation et initialisation d’un type anonyme en utilisant l’initialisation d’objets.

var address = new { address = "105 Elm Street", city = "Atlanta", state = "GA", postalCode = "30339" };

Console.WriteLine("address = {0} : city = {1} : state = {2} : zip = {3}", address.address, address.city, address.state, address.postalCode);

Console.WriteLine("{0}", address.GetType().ToString());

La dernière ligne a été ajoutée pour afficher le nom de la classe anonyme générée par lecompilateur. Voici le résultat :

Ce nom peu orthodoxe laisse clairement entendre qu’il a été généré par un compilateur(le nom généré par votre compilateur a de grandes chances d’être différent).

Méthodes d’extension

Une méthode d’extension est une méthode ou une classe statique qui peut être invoquéecomme s’il s’agissait d’une méthode d’instance d’une classe différente. Vous pourriezpar exemple créer la méthode statique d’extension ToDouble dans la classe statiqueStringConversions. Cette méthode serait appelée comme s’il s’agissait d’une méthoded’un objet de type string.

address = 105 Elm Street : city = Atlanta : state = GA : zip = 30339<>f__AnonymousType5`4[System.String,System.String,System.String,System.String]

Linq.book Page 31 Mercredi, 18. février 2009 7:58 07

Page 47: LINQ Language Integrated Query en C

32 LINQ et C# 2008 Partie I

Avant d’entrer dans le détail des méthodes d’extension, nous allons nous intéresser auproblème qui leur a donné naissance. Nous allons comparer les méthodes statiques(class) aux méthodes d’instance (object). Les méthodes d’instance peuvent seulementêtre appelées dans les instances d’une classe, aussi appelées objets. Il est impossibled’appeler une méthode d’instance dans la classe elle-même. Au contraire, les méthodesstatiques ne peuvent être appelées qu’à l’intérieur d’une classe.

Rappel sur les méthodes d’instance et les méthodes statiquesLa méthode ToUpper de la classe string est un exemple d’une méthode d’instance :elle ne peut être appelée que sur un objet string. En aucun cas sur la classe stringelle-même.

Dans le code du Listing 2.11, la méthode ToUpper est appelée sur l’objet name.

Listing 2.11 : Appel d’une méthode d’instance d’un objet.

// Ce code passe l’étape de la compilationstring name = "Joe";Console.WriteLine(name.ToUpper());

Ce code est compilable. Son exécution affiche la conversion en majuscules de la varia-ble name :

Si vous essayez d’appeler la méthode ToUpper sur la classe string, vous obtiendrezune erreur de compilation, car ToUpper est une méthode d’instance. Elle ne peut doncêtre appelée qu’à partir d’un objet et non d’une classe. Le Listing 2.12 donne un exem-ple d’un tel code.

Listing 2.12 : Tentative d’appel d’une méthode d’instance sur une classe.

// Ce code ne passe pas l’étape de la compilationstring.ToUpper();

Voici l’erreur affichée par le compilateur :

Cet exemple peut sembler un peu bizarre, puisque aucune valeur n’a été communiquéeà ToUpper. Si vous essayiez de passer une valeur à ToUpper, cela reviendrait à appelerune variante de la méthode ToUpper. Ceci est impossible puisqu’il n’existe aucun proto-type de ToUpper dont la signature contienne un string.

Faites la différence entre la méthode ToUpper et la méthode Format de la classe string.Cette dernière est statique. Elle doit donc être appliquée à la classe string et non à un

JOE

An object reference is required for the nonstatic field, method, or property’string.ToUpper()’

Linq.book Page 32 Mercredi, 18. février 2009 7:58 07

Page 48: LINQ Language Integrated Query en C

Chapitre 2 Améliorations de C# 3.0 pour LINQ 33

objet string. Essayons d’invoquer cette méthode sur un objet string (voirListing 2.13).

Listing 2.13 : Tentative d’appel d’une méthode de classe sur un objet.

string firstName = "Joe";string lastName = "Rattz";string name = firstName.Format("{0} {1}", firstName, lastName);Console.WriteLine(name);

Ce code produit l’erreur suivante lors de la compilation :

Appliquons maintenant la méthode Format sur la classe string elle-même (voirListing 2.14).

Listing 2.14 : Appel d’une méthode de classe sur une classe.

string firstName = "Joe";string lastName = "Rattz";string name = string.Format("{0} {1}", firstName, lastName);Console.WriteLine(name);

Ce code passe la compilation et donne le résultat suivant à l’exécution :

Outre le mot-clé static, il suffit souvent d’observer la signature d’une méthode poursavoir qu’il s’agit d’une méthode d’instance. Considérez par exemple la méthode ToUp-per. Elle ne comprend aucun autre argument que la version surchargée de la référence àl’objet. Si elle ne dépend pas d’une instance string d’une donnée interne, quelle valeurstring pourrait-elle mettre en majuscules ?

Résolution du problème par les méthodes d’extensionSupposons que vous soyez un développeur et que vous deviez mettre en place unenouvelle façon d’interroger des objets. Supposons que vous décidiez de créer uneméthode Where pour traiter la clause Where. Comment procéderiez-vous ?

L’opérateur Where devrait-il être traité dans une méthode d’instance ? Dans ce cas, àquelle classe ajouteriez-vous cette méthode, étant donné que vous voulez que laméthode Where puisse interroger toute collection d’objets. Aucune réponse logique àcette question ! En adoptant cette approche, vous devriez modifier un très grand nombrede classes si vous vouliez que la méthode soit universelle.

La méthode doit donc être statique. Comme nous allons le voir dans les lignes suivan-tes, si l’on se réfère aux requêtes SQL traditionnelles, incluant plusieurs clauses where,jointures, regroupements et/ou tris, une méthode statique n’est pas vraiment appropriée.

Member ’string.Format(string, object, object)’ cannot be accessed with an instancereference; qualify it with a type name instead

Joe Ratz

Linq.book Page 33 Mercredi, 18. février 2009 7:58 07

Page 49: LINQ Language Integrated Query en C

34 LINQ et C# 2008 Partie I

Supposons que vous ayez défini un nouveau type de données : une séquence d’objetsgénériques que nous appellerons Enumerable. La méthode Where devrait opérer sur unEnumerable et retourner un autre Enumerable filtré. De plus, la méthode Where devraitaccepter un argument qui permette au développeur de préciser la logique utilisée pourfiltrer les enregistrements de données depuis ou dans l’Enumerable. Cet argument, quej’appellerai le prédicat, pourrait être spécifié dans une méthode nommée, une méthodeanonyme ou une expression lambda.

ATTENTIONATTENTION

Les trois codes qui suivent sont purement démonstratifs. Ils ne passeront pas l’étape de lacompilation.

Étant donné que la méthode Where demande une entrée à filtrer de type Enumerable, etque la méthode est statique, cette entrée doit être spécifiée dans un argument de laméthode Where. Ceci pourrait se matérialiser comme suit :

static Enumerable Enumerable.Where(Enumerable input, LambdaExpression predicate) {…}

En ignorant pour l’instant la sémantique d’une expression lambda, un appel à laméthode Where pourrait s’effectuer par les instructions suivantes :

Enumerable enumerable = {"one", "two", "three"};Enumerable filteredEnumerable = Enumerable.Where(enumerable, lambdaExpression);

Cela ne s’annonce pas trop mal. Mais que faire si nous avons besoin de plusieurs clau-ses Where ? Puisque l’Enumerable sur lequel travaille la méthode Where doit être unargument de la méthode, le chaînage des méthodes revient à les imbriquer. Voicicomment appeler trois clauses Where :

Enumerable enumerable = {"one", "two", "three"};Enumerable finalEnumerable = Enumerable.Where(Enumerable.Where(Enumerable.Where(enumerable, lX1), lX2), lX3);

Vous devez lire la dernière instruction de la partie la plus interne vers la partie la plusexterne. Très difficile à lire ! Pouvez-vous imaginer à quoi ressemblerait une requêteplus complexe ? Si seulement il y avait un autre moyen…

La solutionUne solution élégante consisterait à appeler la méthode statique Where sur chaque objetEnumerable, plutôt que sur la classe. Il ne serait alors plus nécessaire de passer chaqueEnumerable dans la méthode Where, puisque l’objet Enumerable aurait accès à sespropres Enumerable. La requête précédente deviendrait donc :

Enumerable enumerable = {"one", "two", "three"};Enumerable finalEnumerable = enumerable.Where(lX1).Where(lX2).Where(lX3);

Linq.book Page 34 Mercredi, 18. février 2009 7:58 07

Page 50: LINQ Language Integrated Query en C

Chapitre 2 Améliorations de C# 3.0 pour LINQ 35

ATTENTIONATTENTION

Les codes qui précèdent ainsi que le code qui suit sont purement démonstratifs. Ils ne passe-ront pas l’étape de la compilation.

Ce code pourrait être réécrit comme suit :

Enumerable enumerable = {"one", "two", "three"};Enumerable finalEnumerable = enumerable .Where(lX1) .Where(lX2) .Where(lX3);

Ce code est bien plus lisible : la déclaration peut maintenant être lue de gauche à droiteet de haut en bas. Comme vous pouvez le voir, cette syntaxe est très simple à suivre.C’est la raison pour laquelle vous verrez de nombreuses requêtes LINQ exprimées de lasorte dans la documentation officielle et dans cet ouvrage.

Pour terminer, vous avez besoin d’une méthode statique qui puisse être appelée dansune méthode de classe. Ce sont exactement les possibilités offertes par les méthodesd’extension. Elles ont été ajoutées à C# pour permettre d’appeler élégamment uneméthode statique sans avoir à passer le premier argument de la méthode. Celapermet d’appeler la méthode d’extension comme s’il s’agissait de la méthode dupremier argument. Les appels chaînés aux méthodes d’extension sont donc bien pluslisibles.

Les méthodes d’extension permettent à LINQ d’appliquer des opérateurs de requêtestandard aux types qui implémentent l’interface IEnumerable<T>.

INFO

Les méthodes d’extension peuvent être appelées sur une instance de classe (un objet) et nonsur la classe elle-même.

Déclarations et invocations de méthodes d’extensionIl suffit d’utiliser le mot-clé this comme premier argument d’une méthode pour latransformer en une méthode d’extension.

La méthode d’extension peut être utilisée sur n’importe quel objet dont le type est lemême que celui de son premier argument. Si, par exemple, le premier argument de laméthode d’extension est de type string, elle apparaîtra comme une méthode d’instancestring et pourra être appliquée à tout objet string.

Ayez toujours à l’esprit que les méthodes d’extension ne peuvent être déclarées quedans des classes statiques.

Linq.book Page 35 Mercredi, 18. février 2009 7:58 07

Page 51: LINQ Language Integrated Query en C

36 LINQ et C# 2008 Partie I

Voici un exemple d’une méthode d’extension :

namespace Netsplore.Utilities{ public static class StringConversions { public static double ToDouble(this string s) { return Double.Parse(s); }

public static bool ToBool(this string s) { return Boolean.Parse(s); } }}

Les classes et méthodes utilisées sont toutes statiques. Pour utiliser ces méthodesd’extension, il suffit d’appeler les méthodes statiques sur des instances d’objets, commedans le Listing 2.15. Étant donné que la méthode ToDouble est statique et que sonpremier argument est this, ToDouble est une méthode d’extension.

Listing 2.15 : Appel d’une méthode d’extension.

using Netsplore.Utilities;

double pi = "3.1415926535".ToDouble();Console.WriteLine(pi);

Voici le résultat du WriteLine :

Il est important de spécifier la directive using sur l’espace de noms Netsplore.Utili-ties. Si vous l’omettez, le compilateur ne trouvera pas les méthodes d’extension etvous obtiendrez une erreur du type suivant :

Comme indiqué précédemment, il n’est pas permis de déclarer une méthode d’exten-sion à l’intérieur d’une classe non statique. Si vous le faites, vous obtiendrez le messaged’erreur suivant :

Précédence des méthodes d’extensionLes instances d’objets conventionnelles ont une précédence sur les méthodes d’extensionlorsque leur signature est identique à la signature d’appel.

Les méthodes d’extension sont un concept très utile, en particulier si vous voulez étendreune classe "scellée" ou dont vous ne connaissez pas le code. Les méthodes d’extension

3.1415926535

’string’ does not contain a definition for ’ToDouble’ and no extension method’ToDouble’ accepting a first argument of type ’string’ could be found (are youmissing a using directive or an assembly reference?)

Extension methods must be defined in a non-generic static class

Linq.book Page 36 Mercredi, 18. février 2009 7:58 07

Page 52: LINQ Language Integrated Query en C

Chapitre 2 Améliorations de C# 3.0 pour LINQ 37

précédentes ajoutent des méthodes à la classe string. Si les méthodes d’extensionn’existaient pas, vous ne pourriez pas le faire, car la classe string est scellée.

Méthodes partielles

Les méthodes partielles ajoutent un mécanisme de gestion d’événements ultraléger aulangage C#. Oubliez les conclusions que vous êtes certainement en train de tirer sur lesméthodes partielles : le seul point commun entre les méthodes partielles et les classespartielles est qu’une méthode partielle ne peut exister que dans une classe partielle.

Avant de passer en revue les autres règles sur les méthodes partielles, nous allons nousintéresser à leur nature. Le prototype ou la définition d’une méthode partielle est spéci-fié dans sa déclaration, mais cette dernière n’inclut pas l’implémentation de la méthode.Aucun code IL n’est donc émis par le compilateur lors de la déclaration de la méthode,l’appel de la méthode ou l’évaluation des arguments passés à la méthode. C’est commesi la méthode n’avait jamais existé !

Le terme "méthode partielle" peut sembler inapproprié si l’on compare le comporte-ment d’une méthode partielle à celui d’une classe partielle. Le terme "méthodefantôme" aurait certainement été plus judicieux…

Un exemple de méthode partielleVoici un exemple de classe partielle dans lequel est définie une méthode partielle.

La classe MyWidget

public partial class MyWidget{ partial void MyWidgetStart(int count); partial void MyWidgetEnd(int count);

public MyWidget() { int count = 0; MyWidgetStart(++count); Console.WriteLine("In the constructor of MyWidget."); MyWidgetEnd(++count); Console.WriteLine("count = " + count); }}

Cette classe partielle MyWidget contient une méthode partielle également nomméeMyWidget. Les deux premières lignes définissent les méthodes partielles MyWidget-Start et MyWidgetStop. Toutes deux acceptent un paramètre et retournent void (cettedernière caractéristique est une obligation des méthodes partielles).

Le bloc de code suivant est le constructeur. Comme vous pouvez le voir, il définit l’intcount et l’initialise à 0. La méthode MyWidgetStart est alors appelée, un message estaffiché dans la console, la méthode MyWidgetStop est appelée puis la valeur de countest affichée dans la console. La valeur de count est incrémentée à chaque passage dans

Linq.book Page 37 Mercredi, 18. février 2009 7:58 07

Page 53: LINQ Language Integrated Query en C

38 LINQ et C# 2008 Partie I

la méthode partielle. Ceci afin de prouver que, si une méthode partielle n’est pas implé-mentée, ses arguments ne sont pas évalués.

Le code du Listing 2.16 définit un objet de classe MyWidget.

Listing 2.16 : Instanciation de la classe MyWidget.

MyWidget myWidget = new MyWidget();

Appuyez sur Ctrl+F5 pour exécuter le code. Voici le résultat obtenu dans la console :

Comme vous pouvez le voir, après que le constructeur de MyWidget eut incrémenté àdeux reprises la variable count, la valeur affichée à la fin du constructeur est égale àzéro. Ceci vient du fait que les arguments des méthodes partielles ne sont pas implé-mentés. Aucun code IL n’est donc émis par le compilateur.

Nous allons maintenant ajouter une implémentation pour les deux méthodes partielles :

Une autre déclaration de MyWidget contenant l’implémentation des méthodes partielles

public partial class MyWidget{ partial void MyWidgetStart(int count) { Console.WriteLine("In MyWidgetStart(count is {0})", count); }

partial void MyWidgetEnd(int count) { Console.WriteLine("In MyWidgetEnd(count is {0})", count); }}

L’implémentation ayant été rajoutée, exécutez à nouveau le code du Listing 2.16. Vousobtiendrez l’affichage suivant dans la console :

Comme vous pouvez le voir, les méthodes partielles ont été implémentées et les arguments,passés et évalués (la variable count vaut 2 à la fin de la sortie écran).

Pourquoi utiliser les méthodes partielles ?Vous vous demandez peut-être pourquoi utiliser des méthodes partielles. Certains rétor-queront qu’elles s’apparentent à l’héritage et aux méthodes virtuelles. Mais, alors,pourquoi alourdir le langage avec les méthodes partielles ? Tout simplement parcequ’elles sont plus efficaces si vous prévoyez d’utiliser des procédures potentiellement

In the constructor of MyWidget.count = 0

In MyWidgetStart(count is 1)In the constructor of MyWidget.In MyWidgetEnd(count is 2)count = 2

Linq.book Page 38 Mercredi, 18. février 2009 7:58 07

Page 54: LINQ Language Integrated Query en C

Chapitre 2 Améliorations de C# 3.0 pour LINQ 39

non implémentées. Elles permettent d’écrire du code pouvant être étendu par unepersonne tierce via le paradigme des classes partielles sans dégradation de performances.

Les méthodes partielles ont certainement été ajoutées à C# pour les besoins des outilsde génération de classes d’entités de LINQ to SQL. À titre d’exemple, chaque propriétémappée d’une classe d’entités possède une méthode partielle qui est appelée avant quela propriété ne change et une autre qui est appelée après que la propriété eut changé.Ceci permet d’ajouter un autre module en déclarant la même classe d’entité, d’implé-menter ces méthodes partielles et d’être averti chaque fois qu’une propriété est sur lepoint d’être modifiée et après sa modification. Cela n’est-il pas intéressant ? Le code nesera ni plus volumineux ni plus lent. Alors, ne vous en privez pas !

Les règlesLes méthodes partielles doivent respecter quelques règles. Ces dernières ne sont pas tropcontraignantes, et l’on y gagne vraiment au change en termes de flexibilité et de possi-bilités offertes au programmeur. Les voici :

m Elles ne doivent être définies et implémentées que dans des classes partielles.

m Elles doivent être préfixées par le mot-clé partiel.

m Elles sont privées mais ne doivent pas utiliser le mot-clé private, sinon une erreursera générée à la compilation.

m Elles doivent retourner void.

m Elles peuvent ne pas être implémentées.

m Elles peuvent être static.

m Elles peuvent avoir des arguments.

Expressions de requête

Un des avantages du langage C# est la déclaration foreach. Cette instruction estremplacée par le compilateur par une boucle qui appelle des méthodes telles que Get-Enumerator et MoveNext. La simplicité de cette instruction l’a rendue universellelorsqu’il s’agit d’énumérer des tableaux et collections.

La syntaxe des requêtes LINQ est très proche de celle de SQL et vraiment appréciée parles développeurs. Les exemples des pages précédentes utilisent cette syntaxe, propre àC# 3.0, connue sous le nom "expressions de requêtes".

Pour réaliser une requête LINQ, il n’est pas obligatoire d’utiliser une expression derequête. Une alternative consiste à utiliser la notation "à point" standard de C#, enappliquant des méthodes à des objets et des classes. Dans de nombreux cas, l’utilisationde la notation standard est favorable au niveau des instructions, car très démonstrative.Plusieurs exemples de ce livre préfèrent la syntaxe "à point" traditionnelle aux expressions

Linq.book Page 39 Mercredi, 18. février 2009 7:58 07

Page 55: LINQ Language Integrated Query en C

40 LINQ et C# 2008 Partie I

de requête. Il n’y a aucune concurrence entre ces deux types d’écritures. Cependant, lafacilité avec laquelle vous écrirez vos premières expressions de requête peut se révélerenthousiasmante…

Pour avoir une idée des différences entre les deux types de notations, le Listing 2.17met en œuvre une requête fondée sur la syntaxe traditionnelle de C#.

Listing 2.17 : Une requête utilisant la notation à point traditionnelle.

string[] names = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"};

IEnumerable<string> sequence = names .Where(n => n.Length < 6) .Select(n => n);

foreach (string name in sequence){ Console.WriteLine("{0}", name);}

Le Listing 2.18 est la requête équivalente fondée sur les expressions de requête.

Listing 2.18 : La requête équivalente fondée sur les expressions de requête.

string[] names = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"};

IEnumerable<string> sequence = from n in names where n.Length < 6 select n;

foreach (string name in sequence){ Console.WriteLine("{0}", name);}

La première chose qui saute aux yeux quant à l’expression de requête est que, contrai-rement au SQL, la déclaration from précède le select. Une des raisons majeures ayantmotivé ce changement vient de l’IntelliSense. Sans cette inversion, si vous tapiezselect suivi d’une espace dans l’éditeur de Visual Studio 2008, IntelliSense n’auraitaucune idée des éléments à afficher dans la liste déroulante. En indiquant d’où provien-nent les données, IntelliSense a une idée précise des variables à proposer dans la listedéroulante.

Linq.book Page 40 Mercredi, 18. février 2009 7:58 07

Page 56: LINQ Language Integrated Query en C

Chapitre 2 Améliorations de C# 3.0 pour LINQ 41

Ces deux exemples donnent le résultat suivant :

Grammaire des expressions de requêteLes expressions de requête doivent se conformer aux règles de grammaire suivantes :

1. Une expression de requête doit toujours commencer par la clause from.

2. Peuvent ensuite venir zéro, une ou plusieurs clauses from, let et/ou where. Laclause from définit une ou plusieurs énumérations qui passent en revue les élémentsd’une ou de plusieurs séquences. La clause let définit une variable et lui affecte unevaleur. La clause where filtre les éléments d’une séquence ou réalise une jointure deplusieurs séquences dans la séquence de sortie.

3. La suite de l’expression de requête peut contenir une clause orderby qui trie lesdonnées sur un ou plusieurs champs. Le tri peut être ascendant (ascending) oudescendant (descending).

4. Une clause select ou group doit alors faire suite.

5. La suite de l’expression de requête peut contenir une clause de continuation option-nelle into, zéro, une ou plusieurs clauses join, ainsi qu’un ou plusieurs autres blocssyntaxiques, à partir du point numéro 2. La clause into redirige les résultats de larequête dans une séquence de sortie imaginaire. Cette séquence se comporte commeune clause from pour l’expression suivante commençant par le point numéro 2.

Pour une description plus technique de la grammaire des expressions de requête, utilisez lediagramme suivant provenant de la documentation officielle MSDN sur LINQ.Expression de requête from-clause query-body

Clause from from typeopt identifier in expression join-clausesopt

Clauses join join-clause join-clauses join-clause

Clause join join typeopt identifier in expression on expression equals expression join typeopt identifier in expression on expression equals expression into identifier

Corps de la requête from-let-where-clausesopt orderby-clauseopt select-or-group-clause query-continuationopt

AdamsBushFordGrantHayesNixonPolkTaftTyler

Linq.book Page 41 Mercredi, 18. février 2009 7:58 07

Page 57: LINQ Language Integrated Query en C

42 LINQ et C# 2008 Partie I

Clauses from, let et where from-let-where-clause from-let-where-clauses from-let-where-clauseClause from, let et where from-clause let-clause where-clause

Clause let let identifier = expression

Clause where where boolean-expression

Clause orderby orderby orderings

Tris ordering orderings , ordering

Tri expression ordering-directionopt

Direction du tri ascending descendingClause select ou group select-clause group-clause

Clause select select expression

Clause group group expression by expressionContinuation de la requête into identifier join-clausesopt query-body

Traduction des expressions de requêteSupposons que vous ayez créé une expression de requête syntaxiquement correcte.Pour la traduire en code "à point" C#, le compilateur recherche des "motifs". La traduc-tion s’effectue en plusieurs étapes. Chacune d’entre elles recherche un ou plusieursmotifs spécifiques. Le compilateur réitère la traduction pour tous les motifs correspon-dant à l’étape actuelle avant de passer à la suivante. Par ailleurs, l’étape n de la traductionne peut se faire que si les n–1 étapes précédentes ont été achevées.

Identificateurs transparentsCertaines traductions insèrent des variables d’énumération comprenant des identifica-teurs transparents. Dans les descriptions de la section suivante, les identificateurs trans-parents sont identifiés par des astérisques (*). Ce signe ne doit pas être confondu avec lecaractère de remplacement "*". Lors de la traduction, il arrive que certaines énuméra-tions additionnelles soient générées par le compilateur et que des identificateurs trans-parents soient utilisés pour les énumérer (ces identificateurs n’existent que pendant leprocessus de traduction).

Linq.book Page 42 Mercredi, 18. février 2009 7:58 07

Page 58: LINQ Language Integrated Query en C

Chapitre 2 Améliorations de C# 3.0 pour LINQ 43

Étapes de la traductionDans cette section, nous allons utiliser les conventions du Tableau 2.1, où des lettresreprésentent les variables utilisées dans des portions spécifiques d’une requête.

Attention ! Le processus de traduction est complexe. Que cela ne vous décourage pas !En effet, vous n’avez pas besoin de comprendre ce qui va être dit dans les détails pourécrire des requêtes LINQ. Les informations données dans cette section sont un plus. Ily a fort à parier que vous n’en aurez que rarement besoin, voire jamais.

Dans la suite, les étapes de la traduction seront spécifiées sous la forme motif –>traduction. Je vais présenter ces étapes en me conformant à l’enchaînement logiquedu compilateur. Il serait sans doute plus simple de comprendre le processus de traduc-tion en utilisant l’enchaînement inverse de celui du compilateur. En effet, la premièreétape ne met en œuvre que le premier motif. Elle donne naissance à plusieurs autresmotifs non traduits qu’il faut encore traiter. Étant donné que chaque étape de traductionnécessite que l’étape précédente soit entièrement traduite, lorsque le processus estterminé il ne reste plus aucun terme à traduire. C’est la raison pour laquelle la dernièreétape de la traduction est plus aisée à comprendre que la première. Et la descriptioninversée des étapes de traduction est également la meilleure façon de comprendre ce quise passe.

Ceci étant dit, voici les étapes de traduction, décrites dans l’ordre du compilateur.

Tableau 2.1 : Variables de traduction.

Variable Description Exemple

c Variable temporaire générée par le compilateur aucun

e Variable d’énumération from e in customers

f Champ sélectionné ou nouveau type anonyme from e in customers

select f

g Un élément groupé from e in s group g by k

i Un imaginaire dans une séquence from e in s into i

k Élément clé groupé ou joint from e in s group g by k

l Une variable définie avec let from e in s let l = v

o Un élément classé from e in s orderby o

s La séquence d’entrée from e in s

v Une valeur affectée à une variable par let from e in s let l = v

w Une clause where from e in s where w

Linq.book Page 43 Mercredi, 18. février 2009 7:58 07

Page 59: LINQ Language Integrated Query en C

44 LINQ et C# 2008 Partie I

Clauses Select et Group avec une clause intoSi une expression de requête contient une clause into, la traduction suivante esteffectuée :

from …1 into i …2 from i in–> from …1

…2

Voici un exemple :

from c in customers from g in group c by c.Country into g from c in customersselect new group c by c.Country { Country = g.Key, –> select new CustCount = g.Count() } { Country = g.Key,

custCount = g.Count() }

Lorsque toutes les étapes de la traduction ont été passées, le code est finalement traduiten :

customers.GroupBy(c => c.Country) .Select(g => new { Country = g.Key, CustCount = g.Count() })

Types explicites de variables d’énumérationSi votre expression de requête contient une clause from qui spécifie explicitement letype d’une variable d’énumération, la traduction suivante sera effectuée :

from T e in s –> from e in s.Cast<T>()

Voici un exemple :

from Customer c in customers –> from c in customers.Cast<Customer>()select c

Lorsque toutes les étapes de la traduction ont été passées, le code est finalement traduiten :

customers.Cast<Customer>()

Si l’expression de requête contient une clause join qui spécifie explicitement un typede variable d’énumération, la traduction suivante est effectuée :

join T e in s join e in s.Cast<T>()on k1 equals k2 –> on k1 equals k2

Voici un exemple :

from c in customers from c in customersjoin Order o in orders join o in orders.Cast<Order>()on c.CustomerID equals o.CustomerID –> on c.CustomerID equals o.CustomerIDselect new { c.Name, select new o.OrderDate, { c.Name, o.OrderDate, o.Total } o.Total }

Lorsque toutes les étapes de la traduction ont été passées, le code est finalement traduiten :

.Join(orders.Cast<Order>(),c => c.CustomerID,o => o.CustomerID,<c, o) => new { c.Name, o.OrderDate, o.Total })

Linq.book Page 44 Mercredi, 18. février 2009 7:58 07

Page 60: LINQ Language Integrated Query en C

Chapitre 2 Améliorations de C# 3.0 pour LINQ 45

ASTUCE

La saisie explicite de variables d’énumération est nécessaire lorsque la collection de donnéesénumérée est héritée des collections de C# (ArrayList, par exemple). Le casting opéréconvertit la collection héritée en une séquence qui implémente IEnumerable<T> afind’assurer la compatibilité avec les opérateurs de requête.

Clauses joinSi l’expression de requête contient une clause from suivie d’une clause join, mais pasd’une clause into suivie d’une clause select, la traduction suivante est opérée (t estune variable temporaire créée par le compilateur) :

from e1 in s1 from t in s1join e2 in s2 .Join(s2,on k1 equals k2 –> e1 => k1,select f e2 => k2,

(e1, e2) => f)select t

Voici un exemple :from c in customers from t in customersjoin o in orders .Join(orders,on c.CustomerID equals o.CustomerID c => c.CustomerID,select new { c.Name, –> o => o.CustomerID, o.OrderDate, (c, o) =>new o.Total } { c.Name,

o.orderDate o.Total })select t

Lorsque toutes les étapes de la traduction ont été passées, le code est finalement traduit en :customers.Join(orders, c => c.CustomerID, o => o.CustomerID, (c, o) => new { c.Name, o.OrderDate, o.Total })

Si l’expression de requête contient une clause from suivie d’une clause join, puis d’uneclause into suivie d’une clause select, la traduction suivante est opérée (t est unevariable temporaire créée par le compilateur) :

from e1 in s1 from t in s1join e2 in s2 .GroupJoin(s2,on k1 equals k2 –> e1 => k1,into i e2 => k2,select f (e1, i) => f)

select t

Voici un exemple :from c in customers from t in customersjoin o in orders .groupJoin(orders,on c.CustomerID equals o.CustomerID –> c => c.CustomerID,into co o => o.CustomerID,select new (c, co) => new { c.Name, Sum = co.Sum(o => o.Total) } { c.Name,

Sum = co.Sum( o => co.Total)

Select t

Linq.book Page 45 Mercredi, 18. février 2009 7:58 07

Page 61: LINQ Language Integrated Query en C

46 LINQ et C# 2008 Partie I

En utilisant les étapes de traduction suivantes, le code est finalement traduit en :

Customers.GroupJoin(orders, c => c.CustomerIDc.CustomerID, o => o.CustomerID, (c, co) => new { c.Name, Sum = co.Sum(o = o.Total) })

Si l’expression de requête contient une clause from suivie d’une clause join mais pasd’une clause into suivie par quelque chose d’autre qu’une clause select, la traductionsuivante est opérée (* est un opérateur transparent) :

from e1 in s1 from * injoin e2 in s2 from e1 in s1on k1 equals k2 –> join e2 in s2… on k1 equals k2

select new { e1, e2 }

Le motif généré correspond au premier motif de la section "Clauses Join" : la requêtecontient une clause from suivie d’une clause join. La clause into est absente, mais uneclause select est présente. Une nouvelle traduction sera donc opérée.

Si l’expression de requête contient une clause from suivie d’une clause join, puis d’uneclause into suivie par quelque chose d’autre qu’une clause select, la traductionsuivante est opérée (* est un opérateur transparent) :

from e1 in s1 from *join e2 in s2 from e1 in s1on k1 equals k2 –> join e2 in s2into i on k1 equals k2… into i

select new { e1, i }

Le motif généré correspond au deuxième motif de la section "Clauses Join" : on trouveune clause from suivie d’une clause join, d’une clause into puis d’une clause select.Une nouvelle traduction sera donc opérée.

Les clauses Let et WhereSi l’expression de requête contient une clause from suivie immédiatement d’une clauselet, la traduction suivante est effectuée (* est un identificateur transparent) :

from e in s from * inlet l = v –> from e1 in s1

select new { e, l = v }

Voici un exemple (t est un identificateur généré par le compilateur. Il reste invisible etinaccessible par le code) :

from c in customers from * inlet cityStateZip = from c in customers c.City + ", " + c.State + " " + c.Zip –> select new {select new { c.Name, cityStateZip } c,

cityStateZip = c.City + ", " + c.State + " " + c.Zip }

select new { c.Name, cityStateZip }

Linq.book Page 46 Mercredi, 18. février 2009 7:58 07

Page 62: LINQ Language Integrated Query en C

Chapitre 2 Améliorations de C# 3.0 pour LINQ 47

Une fois les autres étapes de traduction terminées, le code est finalement transforméen :

customers.Select(c => new { c, cityStateZip = c.City + ", " + c.State + " " + c.Zip }).Select(t => new { t.c.Name, t.cityStateZip })

Si l’expression de requête contient une clause from suivie d’une clause where, latraduction suivante est opérée :

from e in s from e in swhere w –> .Where(e => w)

Voici un exemple :

from c in customers from c in customerswhere c.Country == "USA" –> .Where (c => c.Country == "USA")select new { c.Name, c.Country } select new { c.Name, c.Country }

Une fois les autres étapes de traduction terminées, le code est finalement transformé en :

customers.Where(c => c.Country == "USA").Select(c => new { c.Name, c.Country })

Clauses from multiplesSi l’expression de requête contient deux clauses from suivies par une requête select, latraduction suivante est opérée :

from e1 in s1 from c in s1from e2 in s2 –> .SelectMany(e1 => from e2 in s2select f select f)

select c

Voici un exemple (t est une variable temporaire générée par le compilateur) :

from c in customers from t in customersfrom o in c.Orders .SelectMany(c => from o in c.Ordersselect new select new { { c.Name, o.OrderID, o.OrderDate } c.Name,

o.OrderID,

–> o.OrderDate })Select t

Une fois les autres étapes de traduction terminées, le code est finalement transforméen :

customers.SelectMany(c => c.Orders.Select(o => new { c.Name, o.OrderID, o.OrderDate }))

Si l’expression de requête contient deux clauses from suivies par quelque chose d’autrequ’une clause select, la traduction suivante est opérée (* est un identificateur transpa-rent) :

from e1 in s1 from * infrom e2 in s2 from e1 in s1… –> from e2 in s2

select new { e1, e2 }

Linq.book Page 47 Mercredi, 18. février 2009 7:58 07

Page 63: LINQ Language Integrated Query en C

48 LINQ et C# 2008 Partie I

Voici un exemple (* est un identificateur transparent) :

from c in customers from * infrom o in c.Orders from c in customersorderby o.OrderDate descending –> from o in c.Ordersselect new select new { c, o } {c.Name, o.OrderID, o.OrderDate } orderby o.OrderDate descending

select new { c.Name, o.OrderID, o.OrderDate }

Le code ainsi obtenu doit réitérer la première étape de traduction. En effet, le motifrésultant contient une clause from suivie par une autre clause from puis par une clauseselect, ce qui correspond au premier modèle de la section "Clauses from multiples". Ils’agit donc d’un exemple dans lequel certaines étapes doivent être appelées plusieursfois pour que la traduction soit complète.

Une fois toutes les étapes de traduction terminées, le code est finalement transforméen :

customers.SelectMany(c => c.Orders.Select(o => new { c, o })).OrderByDescending(t => t.o.OrderDate).Select(t => new { t.c.Name, t.o.OrderID, t.o.OrderDate})

Clauses OrderByLes traductions suivantes prennent place dans un tri ascendant :

from e in s–> from e in s

orderby o1, o2 .OrderBy(e => o1).ThenBy(e => o2)

Voici un exemple :

from c in customers from c in customersorderby c.Country, c. Name .OrderBy(c => c.Country)select new { c.Country, c.Name} .TheBy(c.Name)

select new { c.Country, c.Name }

Une fois toutes les étapes de traduction terminées, le code est finalement transforméen :

customers.OrderBy(c => c.Country).ThenByDescending(c.Name).Select(c => new { c.Country, c.Name }

Clauses SelectDans une expression de requête, si vous sélectionnez la totalité de l’élément stockédans la séquence, l’élément sélectionné a le même identificateur que la variabled’énumération de la séquence. La traduction suivante est opérée :

from e in s –> sselect f

Voici un exemple :

from c in customers –> customersselect c

Linq.book Page 48 Mercredi, 18. février 2009 7:58 07

Page 64: LINQ Language Integrated Query en C

Chapitre 2 Améliorations de C# 3.0 pour LINQ 49

Si l’élément sélectionné n’a pas le même identificateur que la variable d’énumérationde la séquence, cela signifie qu’il n’est pas sélectionné en totalité (la sélection peutporter sur un membre de l’élément ou sur un type anonyme construit à partir deplusieurs membres de l’élément). La traduction suivante est effectuée :

from e in s –> s.Select(e => f)select f

Voici un exemple :

from c in customers –> customers.Select(c => c.Name)select c.Name

Clauses groupDans l’expression de requête, si l’élément regroupé a le même identificateur quel’énumérateur de la séquence, cela signifie que le regroupement porte sur la totalité del’élément stocké dans la séquence. La traduction est la suivante :

from e in s –> s.GroupBy(e => k)group g by k

Voici un exemple :

from c in customers –> customers.GroupBy(c => c.Country)group c by c.Country

Si l’élément regroupé n’a pas le même identificateur que l’énumérateur de la séquence,cela signifie qu’il n’est pas regroupé en totalité. La traduction suivante est effectuée :

from e in s –> s.GroupBy(e => k, e => g)group g by k

Voici un exemple :

from c in customers customersgroup new { c.Country, c. Name} .GroupBy(c => c.Country,by c.Country –> c => new {

c.Country c.Name })

Toutes les étapes de la traduction ont été effectuées et l’expression de requête a étéentièrement traduite en une notation "à point" traditionnelle.

Résumé

De nombreuses fonctionnalités ont été ajoutées au langage C#. Bien que ces ajoutsaient été dictés par l’implémentation de LINQ, vous avez tout intérêt à les utiliser endehors du contexte LINQ.

Les expressions d’initialisation d’objets et de collections sont particulièrement intéres-santes, car elles réduisent la taille du code de façon drastique. Cette fonctionnalité,combinée avec le mot-clé var et aux types anonymes, facilite grandement la création dedonnées et de types de données à la volée.

Linq.book Page 49 Mercredi, 18. février 2009 7:58 07

Page 65: LINQ Language Integrated Query en C

50 LINQ et C# 2008 Partie I

Les méthodes d’extension permettent d’ajouter des fonctionnalités aux classes scelléeset aux classes dont vous n’avez pas le code source.

Si elles n’éliminent pas la raison d’être des méthodes anonymes, les expressionslambda représentent une nouvelle façon de définir de nouvelles fonctionnalités, simple-ment et de façon concise. Lorsque vous commencerez à les utiliser, vous serez peut-êtredéconcerté, mais, le temps aidant, vous les apprécierez à leur juste valeur.

Les arbres d’expressions permettent aux éditeurs de logiciels tiers de conserver unmode de stockage propriétaire tout en supportant les performances avancées de LINQ.

Les méthodes partielles ajoutent un mécanisme de gestion d’événements ultraléger aulangage C#. Elles sont utilisées pour accéder à des moments clés dans les classes d’entitésLINQ to SQL.

Si les expressions de requête peuvent sembler confuses de prime abord, il ne faut pasbien longtemps pour qu’un développeur se sente à l’aise à leur contact. Elles ont eneffet un air de parenté avec les requêtes SQL.

Chacune de ces améliorations du langage est intéressante en soi, mais c’est leur utilisa-tion conjointe qui est à la base de LINQ. LINQ devrait être la prochaine grandetendance en programmation. Les développeurs .NET apprécieront certainement depouvoir l’inscrire dans leur CV. En tout cas, moi, j’en suis fier !

Vous avez maintenant une idée de ce qu’est LINQ, ainsi que des fonctionnalités etsyntaxes C# afférentes. Il est temps de passer à la prochaine étape. En tournant lespages, vous allez apprendre à appliquer des requêtes LINQ à des collections enmémoire (array ou arraylist, par exemple) et aux collections génériques de C# 2.0, etvous découvrirez différentes fonctions pour alimenter vos requêtes. Cette portion deLINQ est aujourd’hui connue sous le nom de "LINQ to Objects".

Linq.book Page 50 Mercredi, 18. février 2009 7:58 07

Page 66: LINQ Language Integrated Query en C

II

LINQ to Objects

Linq.book Page 51 Mercredi, 18. février 2009 7:58 07

Page 67: LINQ Language Integrated Query en C

Linq.book Page 52 Mercredi, 18. février 2009 7:58 07

Page 68: LINQ Language Integrated Query en C

3

Introduction à LINQ to Objects

Listing 3.1 : Une requête LINQ to Objects élémentaire.

string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"};string president = presidents.Where(p => p.StartsWith("Lin")).First();Console.WriteLine(president);

INFO

Ce code a été ajouté au prototype d’une application console Visual Studio 2008.

Le Listing 3.1 donne une idée de ce qu’est LINQ to Objects : par son intermédiaire, ilest possible d’interroger des données en mémoire à l’aide de requêtes proches dulangage SQL. Lancez le programme avec Ctrl+F5. Vous obtenez le résultat suivant :

Vue d’ensemble de LINQ to Objects

Si LINQ est aussi agréable et facile à utiliser, c’est en partie parce qu’il est parfaitementintégré dans le langage C#. Plutôt qu’avoir à composer avec de nouvelles classes spéci-fiques à LINQ, vous pouvez utiliser les mêmes collections1 et tableaux que précédem-ment. Vous avez donc les avantages inhérents à LINQ sans devoir retoucher (ou très

Lincoln

1. Les collections doivent implémenter l’interface IEnumerable<T> ou IEnumerable pour pouvoirêtre interrogeables par LINQ.

Linq.book Page 53 Mercredi, 18. février 2009 7:58 07

Page 69: LINQ Language Integrated Query en C

54 LINQ to Objects Partie II

peu) le code existant. LINQ to Objects s’exécute à travers l’interface IEnumerable<T>,les séquences et les opérateurs de requête standard.

À titre d’exemple, pour trier un tableau d’entiers, vous pouvez utiliser une requêteLINQ, tout comme s’il s’agissait d’une requête SQL. Un autre exemple. Si vous vouleztrouver un objet Customer spécifique dans un ArrayList of Customer, LINQ toObjects est assurément la réponse.

Pour beaucoup d’entre vous, les chapitres sur LINQ to Objects seront utilisés en tantque référence. Ils ont été construits dans cette optique et je vous conseille de les parcou-rir en totalité. Ne vous contentez pas de lire les sections des seuls opérateurs qui vousintéressent, sans quoi votre formation sera incomplète.

IEnumerable<T>, séquences et opérateurs de requête standard

IEnumerable<T>, prononcé "Iénumérable de T", est une interface implémentée par lestableaux et les classes de collections génériques de C# 2.0. Cette interface permetd’énumérer les éléments d’une collection.

Une séquence est un terme logique d’une collection qui implémente l’interface IEnume-rable<T>. Si vous avez une variable de type IEnumerable<T>, vous pouvez dire quevous avez une séquence de T. Par exemple, si vous avez un IEnumerable de string, cequi s’écrit IEnumerable<string>, vous pouvez dire que vous avez une séquence destring.

INFO

Toutes les variables déclarées en tant que IEnumerable<T> sont considérées comme séquencesde T.

La plupart des opérateurs de requête standard sont des méthodes d’extension de laclasse statique System.Linq.Enumerable et ont un premier argument prototypé par unIEnumerable<T>. Étant donné que ces opérateurs sont des méthodes d’extension, il estpréférable de les appeler à travers une variable de type IEnumerable<T> plutôt quepasser une variable de type IEnumerable<T> en premier argument.

Les méthodes d’opérateurs de requête standard de la classe System.Linq.Enumerablequi ne sont pas des méthodes d’extension sont des méthodes statiques. Elles doiventêtre appelées dans la classe System.Linq.Enumerable. La combinaison de ces méthodesd’opérateurs de requête standard vous permet d’effectuer des requêtes complexes surune séquence IEnumerable<T>.

Les collections héritées – ces collections non génériques qui existaient avant C# 2.0 –supportent l’interface IEnumerable, et non l’interface IEnumerable<T>. Cela signifieque vous ne pouvez pas appeler directement ces méthodes d’extension dont le premier

Linq.book Page 54 Mercredi, 18. février 2009 7:58 07

Page 70: LINQ Language Integrated Query en C

Chapitre 3 Introduction à LINQ to Objects 55

argument est un IEnumerable<T> sur une collection héritée. Cependant, vous pouveztoujours exécuter des requêtes LINQ sur des collections héritées en invoquant l’opéra-teur de requête standard Cast ou OfType. Cet opérateur produira une séquence quiimplémente l’interface IEnumerable<T>, vous permettant ainsi d’accéder à la panopliecomplète des opérateurs de requête standard.

INFO

Utilisez les opérateurs Cast ou OfType pour exécuter des requêtes LINQ sur des collectionsC# héritées et non génériques.

Pour accéder aux opérateurs de requête standard, vous devez ajouter une directiveusing System.Linq; dans votre code (si cette dernière n’est pas déjà présente). Il n’estpas nécessaire d’ajouter une référence à un assembly car le code nécessaire est contenudans l’assembly System.Core.dll, qui est automatiquement ajouté aux projets parVisual Studio 2008.

IEnumerable<T>, yield et requêtes différées

La plupart des opérateurs de requête standard sont prototypés pour retourner un IEnu-merable<T> (une séquence). Mais, attention, les éléments de la séquence ne sont pasretournés dès l’exécution de l’opérateur : ils ne seront "cédés" que lors de l’énuméra-tion de la séquence. C’est la raison pour laquelle on dit que ces requêtes sont différées.

Le terme "céder" fait référence au mot-clé yield, ajouté dans C# 2.0 pour faciliterl’écriture d’énumérateurs.

Examinez le code du Listing 3.2.

Listing 3.2 : Une requête triviale.

string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"};

IEnumerable<string> items = presidents.Where(p => p.StartsWith("A"));

foreach(string item in items) Console.WriteLine(item);

La requête apparaît en gras dans ce listing. Lorsque cette ligne s’exécute, elle retourneun objet. Ce n’est que pendant l’énumération de cet objet que la requête Where est réel-lement exécutée. Si une erreur se produit dans la requête, elle ne sera détectée qu’àl’énumération.

Linq.book Page 55 Mercredi, 18. février 2009 7:58 07

Page 71: LINQ Language Integrated Query en C

56 LINQ to Objects Partie II

Voici le résultat de la requête :

Cette requête s’est comportée comme prévu.

Nous allons maintenant introduire une erreur intentionnelle dans la requête. Le code quisuit va essayer d’effectuer un tri en se basant sur le cinquième caractère du nom desprésidents. Lorsque l’énumération atteint un nom dont la longueur est inférieure à cinqcaractères, une exception sera générée. Rappelez-vous que l’exception ne se produirapas avant l’énumération de la séquence résultat (voir Listing 3.3).

Listing 3.3 : Une requête triviale avec une exception introduite intentionnellement.

string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"};

IEnumerable<string> items = presidents.Where(s => Char.IsLower(s[4]));

Console.WriteLine("After the query.");

foreach (string item in items) Console.WriteLine(item);

Ce code ne produit aucune erreur à la compilation, mais voici les résultats affichés dansla console :

Tout se passe bien jusqu’au quatrième élément. Bush produit une exception lors del’énumération. La leçon à tirer de cet exemple est qu’une compilation réussie ne suffitpas pour assurer qu’une requête est vierge de tout bogue.

Sachez par ailleurs que, les requêtes qui retournent un IEnumerable<T> étant différées,il suffit d’exécuter une seule fois le code de la requête. Vous pouvez ensuite énumérerles données autant de fois que vous le souhaitez. Si, entre deux énumérations, lesdonnées changent, les résultats seront différents (voir Listing 3.4).

Listing 3.4 : Un exemple dans lequel les résultats de la requête changent d’une énumération à l’autre.

// Création d’un tableau de intint[] intArray = new int[] { 1,2,3 };

AdamsArthur

AdamsArthurBuchanan

Unhandled Exception: System.IndexOutOfRangeException: Index was outside the boundsof the array.…

Linq.book Page 56 Mercredi, 18. février 2009 7:58 07

Page 72: LINQ Language Integrated Query en C

Chapitre 3 Introduction à LINQ to Objects 57

IEnumerable<int> ints = intArray.Select(i => i);

// Affichage des résultatsforeach(int i in ints) Console.WriteLine(i);

// Modification d’un élément dans la sourceintArray[0] = 5;

Console.WriteLine("---------");

// Nouvel affichage des résultatsforeach(int i in ints) Console.WriteLine(i);

Lorsque l’opérateur Select est appelé, un objet est retourné et stocké dans la variableIEnumerable<T> ints. La requête n’a pas encore été exécutée. Elle est juste stockéedans l’objet ints. Les résultats de la requête n’existent donc pas encore, mais l’objetints sait comment les obtenir.

Lorsque l’instruction foreach est appelée pour la première fois, ints exécute la requêteet obtient successivement les différents éléments de la séquence.

Un peu plus bas, un des éléments est modifié dans son tableau d’origine, intArray[].L’instruction foreach est appelée à nouveau. Cela provoque une nouvelle exécution dela requête. Cette énumération retourne tous les éléments de intArray[] et donc égale-ment l’élément qui a été modifié.

Dans cet ouvrage (et dans beaucoup d’autres relatifs à LINQ), vous pourrez lire qu’unerequête retourne une séquence et non un objet qui implémente l’interface IEnumera-ble<T>. Ceci est un abus de langage : les éléments de la séquence ne sont obtenus qu’àson énumération.

Voici les résultats affichés par ce code :

La requête n’a été appelée qu’une fois et, pourtant, les résultats des deux énumérationssont différents. Cela confirme – si besoin était – que la requête est bien différée. Dans lecas contraire, les résultats des deux énumérations seraient identiques. Selon les cas, cecipeut être un avantage ou un inconvénient. Si vous ne voulez pas que la requête soitdifférée, utilisez un opérateur qui ne retourne pas un IEnumerable<T>. Par exempleToArray, ToList, ToDictionary ou ToLookup. Les résultats seront alors figés dans unemémoire cache et ne changeront pas.

123---------523

Linq.book Page 57 Mercredi, 18. février 2009 7:58 07

Page 73: LINQ Language Integrated Query en C

58 LINQ to Objects Partie II

Le Listing 3.5 est le même que le précédent, à un détail près : en utilisant un opérateurToList, la requête retourne non pas un IEnumerable<int> mais un List<int>.

Listing 3.5 : En retournant un objet List, la requête est exécutée immédiatement et les résultats sont mis dans un cache.

// Création d’un tableau de intint[] intArray = new int[] { 1,2,3 };

List<int> ints = intArray.Select(i => i).ToList;

// Affichage des résultatsforeach(int i in ints) Console.WriteLine(i);

// Modification d’un élément dans la sourceintArray[0] = 5;

Console.WriteLine("---------");

// Nouvel affichage des résultatsforeach(int i in ints) Console.WriteLine(i);

Voici les résultats :

Comme on pouvait s’y attendre, les résultats ne changent pas d’une énumération à lasuivante. La requête est donc bien exécutée immédiatement.

L’opérateur Select est différé, et l’opérateur ToList ne l’est pas. En appliquant ToListau résultat du Select, l’objet retourné par Select est énuméré et la requête n’est plusdifférée.

Délégués Func

Plusieurs des opérateurs de requête standard sont prototypés pour accepter un déléguéFunc comme argument. Cela vous évite d’avoir à déclarer des délégués explicitement.Voici les déclarations de délégués Func :

public delegate TR Func<TR>();public delegate TR Func<T0, TR>(T0 a0);public delegate TR Func<T0, T1, TR>(T0 a0, T1 a1);public delegate TR Func<T0, T1, T2, TR>(T0 a0, T1 a1, T2 a2);public delegate TR Func<T0, T1, T2, T3, TR>(T0 a0, T1 a1, T2 a2, T3 a3);

Dans ces déclarations, TR fait référence au type de donnée retournée. Cet argument esttoujours le dernier de la liste. Quant à T0 à T3, ils représentent les paramètres passés à laméthode. Plusieurs déclarations sont nécessaires, car tous les opérateurs de requête

123---------123

Linq.book Page 58 Mercredi, 18. février 2009 7:58 07

Page 74: LINQ Language Integrated Query en C

Chapitre 3 Introduction à LINQ to Objects 59

standard n’utilisent pas le même nombre de paramètres en entrée. Dans tous les cas, lesdélégués admettent un nombre maximal de 4 paramètres.

Examinons un des prototypes de l’opérateur Where :

public static IEnumerable<T> Where<T>( this IEnumerable<T> source, Func<T, bool> predicate);

En observant le prédicat Func<T, bool>, vous pouvez en déduire que la méthode oul’expression lambda n’accepte qu’un seul argument, T, et retourne un booléen. Cettedernière déduction vient du fait que le type de retour est toujours le dernier paramètrede la liste.

Vous utiliserez la déclaration Func, comme indiqué dans le Listing 3.6.

Listing 3.6 : Cet exemple utilise une déclaration de délégué Func.

// Création d’un tableau d’entiersint[] ints = new int[] { 1,2,3,4,5,6 };

// Déclaration du déléguéFunc<int, bool> GreaterThanTwo = i => i > 2;

// Mise en place (et non exécution) de la requêteIEnumerable<int> intsGreaterThanTwo = ints.Where(GreaterThanTwo);

// Affichage des résultatsforeach(int i in intsGreaterThanTwo) Console.WriteLine(i);

L’exécution de ce code produit les résultats suivants :

Les opérateurs de requête standard

Le Tableau 3.1 dresse la liste alphabétique des principaux opérateurs de requête stan-dard. Les prochains chapitres vont séparer les opérateurs différés des opérateurs nondifférés. Ce tableau facilitera donc votre repérage dans le livre.

2456

Tableau 3.1 : Les opérateurs de requête standard

Opérateur Objet Différé Supporte l’expression de requête

Aggregate Agrégat

All Dénombrement

Any Dénombrement

AsEnumerable Conversion u

Linq.book Page 59 Mercredi, 18. février 2009 7:58 07

Page 75: LINQ Language Integrated Query en C

60 LINQ to Objects Partie II

Average Agrégat

Cast Conversion u

Concat Concaténation u

Contains Dénombrement

Count Agrégat

DefaultIfEmpty Élément u

Distinct Ensemble u

ElementAt Élément

ElementAtOrDefault Élément

Empty Génération u

Except Ensemble u

First Élément

FirstOrDefault Élément

GroupBy Regroupement u u

GroupJoin Jointure u u

Intersect Ensemble u

Join Jointure u u

Last Élément

LastOrDefault Élément

LongCount Agrégat

Max Agrégat

Min Agrégat

OfType Conversion u

OrderBy Tri u u

OrderByDescending Tri u u

Range Génération u

Repeat Génération u

Reverse Tri u

Select Projection u u

Tableau 3.1 : Les opérateurs de requête standard (suite)

Opérateur Objet Différé Supporte l’expression de requête

Linq.book Page 60 Mercredi, 18. février 2009 7:58 07

Page 76: LINQ Language Integrated Query en C

Chapitre 3 Introduction à LINQ to Objects 61

Résumé

Ce chapitre a introduit le terme "séquence" et le type de données associé, IEnumera-ble<T>. Si vous n’êtes pas à l’aise avec ces expressions, soyez rassuré : elles devien-dront vite une seconde nature pour vous ! Pour l’instant, contentez-vous de voir lesIEnumerable<T> comme une séquence d’objets auxquels vous allez appliquer des trai-tements via des méthodes.

Ce chapitre a mis en évidence l’importance de l’exécution différée des requêtes. Selonles cas, elle peut constituer un avantage ou un inconvénient. Cette caractéristique estvraiment importante. C’est pourquoi nous allons séparer les opérateurs différés (auChapitre 4) des opérateurs non différés (au Chapitre 5) dans la suite de cet ouvrage.

SelectMany Projection u u

SequenceEqual Égalité

Single Élément

SingleOrDefault Élément

Skip Partage u

SkipWhile Partage u

Sum Agrégat

Take Partage u

TakeWhile Partage u

ThenBy Tri u u

ThenByDescending Tri u u

ToArray Conversion

ToDictionary Conversion

ToList Conversion

ToLookup Conversion

Union Ensemble u

Where Restriction u u

Tableau 3.1 : Les opérateurs de requête standard (suite)

Opérateur Objet Différé Supporte l’expression de requête

Linq.book Page 61 Mercredi, 18. février 2009 7:58 07

Page 77: LINQ Language Integrated Query en C

Linq.book Page 62 Mercredi, 18. février 2009 7:58 07

Page 78: LINQ Language Integrated Query en C

4

Les opérateurs différés

Au chapitre précédent, nous nous sommes intéressés aux séquences, aux types dedonnées qui les représentent et aux conséquences de leur exécution différée. Conscientde l’importance de ce dernier point, j’ai choisi de traiter des opérateurs différés et nondifférés dans deux chapitres séparés.

Ce chapitre va s’intéresser aux opérateurs différés, par groupes fonctionnels. Il estfacile de reconnaître un tel opérateur : il retourne un IEnumerable<T> ou un IOrder-Enumerable<T>.

Attention, pour pouvoir exécuter les exemples de ce chapitre, assurez-vous que vousavez référencé les espaces de noms (directive using), les assemblies et les codescommuns nécessaires !

Espaces de noms référencés

Les exemples de ce chapitre vont utiliser les espaces de noms System.Linq,System.Collections, System.Collections.Generic et System.Data.Linq. Si ellesne sont pas déjà présentes, vous devez donc ajouter les directives using suivantes dansvotre code :

using System.Linq;

using System.Collections;

using System.Collections.Generic;

using System.Data.Linq;

Si vous parcourez le code source (disponible sur le site www.pearson.fr), vous verrezque j’ai également ajouté une directive using sur l’espace de noms System.Diagnos-tic. Cette directive n’est pas nécessaire si vous saisissez directement les exemples dece chapitre. Elle n’est là que pour les besoins propres du code source.

Linq.book Page 63 Mercredi, 18. février 2009 7:58 07

Page 79: LINQ Language Integrated Query en C

64 LINQ to Objects Partie II

Assemblies référencés

Pour que le code de ce chapitre fonctionne, vous devez également référencer l’assemblySystem.Data.Linq.dll.

Classes communesCertains exemples de ce chapitre nécessitent des classes additionnelles pour fonction-ner en totalité. En voici la liste.

La classe Employee permet de travailler sur les employés d’une entreprise. Elle contientdes méthodes statiques qui retournent un tableau d’employés de type ArrayList.

public class Employee{ public int id; public string firstName; public string lastName;

public static ArrayList GetEmployeesArrayList() { ArrayList al = new ArrayList();

al.Add(new Employee { id = 1, firstName = "Joe", lastName = "Rattz" }); al.Add(new Employee { id = 2, firstName = "William", lastName = "Gates" }); al.Add(new Employee { id = 3, firstName = "Anders", lastName = "Hejlsberg" }); al.Add(new Employee { id = 4, firstName = "David", lastName = "Lightman" }); al.Add(new Employee { id = 101, firstName = "Kevin", lastName = "Flynn" }); return (al); }

public static Employee[] GetEmployeesArray() { return ((Employee[])GetEmployeesArrayList().ToArray()); }}

La classe EmployeeOptionEntry représente le montant des stock-options des employés.Elle contient une méthode statique qui retourne un tableau de stock-options.

public class EmployeeOptionEntry{ public int id; public long optionsCount; public DateTime dateAwarded;

public static EmployeeOptionEntry[] GetEmployeeOptionEntries() { EmployeeOptionEntry[] empOptions = new EmployeeOptionEntry[] { new EmployeeOptionEntry { id = 1, optionsCount = 2, dateAwarded = DateTime.Parse("1999/12/31") }, new EmployeeOptionEntry { id = 2, optionsCount = 10000, dateAwarded = DateTime.Parse("1992/06/30") }, new EmployeeOptionEntry { id = 2, optionsCount = 10000, dateAwarded = DateTime.Parse("1994/01/01") }, new EmployeeOptionEntry { id = 3, optionsCount = 5000, dateAwarded = DateTime.Parse("1997/09/30") },

Linq.book Page 64 Mercredi, 18. février 2009 7:58 07

Page 80: LINQ Language Integrated Query en C

Chapitre 4 Les opérateurs différés 65

new EmployeeOptionEntry { id = 2, optionsCount = 10000, dateAwarded = DateTime.Parse("2003/04/01") }, new EmployeeOptionEntry { id = 3, optionsCount = 7500, dateAwarded = DateTime.Parse("1998/09/30") }, new EmployeeOptionEntry { id = 3, optionsCount = 7500, dateAwarded = DateTime.Parse("1998/09/30") }, new EmployeeOptionEntry { id = 4, optionsCount = 1500, dateAwarded = DateTime.Parse("1997/12/31") }, new EmployeeOptionEntry { id = 101, optionsCount = 2, dateAwarded = DateTime.Parse("1998/12/31") } };

return (empOptions); }}

Les opérateurs différés, par groupes fonctionnels

Dans les pages qui suivent, nous avons organisé les différents opérateurs de requêtestandard différés par grands groupes fonctionnels.

RestrictionLes opérateurs de restriction sont utilisés pour ajouter ou enlever des éléments dans uneséquence d’entrée.

L’opérateur WhereL’opérateur Where est utilisé pour filtrer des éléments d’une séquence.

PrototypesDeux prototypes de l’opérateur Where seront étudiés dans ce livre.

Premier prototype

public static IEnumerable<T> Where<T>( this IEnumerable<T> source, Func<T, bool> predicate);

Ce prototype demande deux paramètres : une séquence d’entrée et un prédicat (déléguégénérique). Il renvoie un objet énumérable dont seuls les éléments pour lesquels leprédicat renvoie true sont accessibles.

INFO

Comme Where est une méthode d’extension, la séquence d’entrée n’est pas réellementpassée dans le premier argument : tant que Where est appliqué sur un objet du même typeque le premier argument, ce dernier peut être remplacé par le mot-clé this.

Linq.book Page 65 Mercredi, 18. février 2009 7:58 07

Page 81: LINQ Language Integrated Query en C

66 LINQ to Objects Partie II

Lorsque vous appelez la méthode Where, un délégué est passé à un prédicat. Cettedernière doit accepter une entrée de type T (où T est le type des éléments contenus dansla séquence d’entrée) et retourner un booléen. L’opérateur Where communique chacundes éléments contenus dans la séquence d’entrée au prédicat. L’élément n’est retournédans la séquence de sortie que dans le cas où le prédicat retourne la valeur true.

Second prototype

public static IEnumerable<T> Where<T>( this IEnumerable<T> source, Func<T, int, bool> predicate);

Ce second prototype est semblable au premier si ce n’est que le prédicat reçoit un argumentcomplémentaire entier. Cet argument correspond à l’index de l’élément dans la séquence. Ilcommence à zéro et se termine au nombre d’éléments de la séquence moins un.

ExceptionsL’exception ArgumentNullException est levée si un des arguments a pour valeur null.

ExemplesLe Listing 4.1 est un exemple d’appel du premier prototype Where.

Listing 4.1 : Un exemple d’appel du premier prototype Where.

string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"};

IEnumerable<string> sequence = presidents.Where(p => p.StartsWith("J"));

foreach (string s in sequence) Console.WriteLine("{0}", s);

Cet exemple applique la méthode Where à la séquence d’entrée et définit une expressionlambda. Cette dernière retourne un booléen dont la valeur indique si l’élément doit oune doit pas être inclus dans la séquence de sortie. Dans cet exemple, seuls les élémentsqui commencent par la lettre "J" seront retournés. Voici les résultats affichés dans laconsole lorsque vous appuyez sur Ctrl+F5 :

Le Listing 4.2 est un exemple d’appel du second prototype Where. Ce code se contented’utiliser l’index i pour filtrer les éléments de la séquence. Seuls les éléments d’indiceimpair seront retournés.

JacksonJeffersonJohnson

Linq.book Page 66 Mercredi, 18. février 2009 7:58 07

Page 82: LINQ Language Integrated Query en C

Chapitre 4 Les opérateurs différés 67

Listing 4.2 : Un exemple d’appel du second prototype Where.

string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"};

IEnumerable<string> sequence = presidents.Where((p, i) => (i & 1) == 1);

foreach (string s in sequence) Console.WriteLine("{0}", s);

L’exécution de ce code produit la sortie suivante dans la console :

Projection

Les opérateurs de projection retournent une séquence d’éléments sélectionnés dans laséquence d’entrée ou instanciés à partir de portions d’éléments de la séquence d’entrée.

Le type des éléments de la séquence de sortie peut être différent du type des élémentsde la séquence d’entrée.

L’opérateur SelectL’opérateur Select est utilisé pour créer une séquence de sortie S d’un type d’élémenten partant d’une séquence d’entrée T d’un autre type d’élément. Ces deux types ne sontpas forcément identiques.

PrototypesDeux prototypes de l’opérateur Select seront étudiés dans ce livre.

ArthurBushClevelandCoolidgeFillmoreGarfieldHardingHayesJacksonJohnsonLincolnMcKinleyNixonPolkRooseveltTaylorTylerWashington

Linq.book Page 67 Mercredi, 18. février 2009 7:58 07

Page 83: LINQ Language Integrated Query en C

68 LINQ to Objects Partie II

Premier prototype

public static IEnumerable<S> Select<T, S>( this IEnumerable<T> source, Func<T, S> selector);

Ce prototype admet deux arguments en entrée : une séquence source et un délégué. Ilretourne un objet dont l’énumération produit une séquence d’éléments de type S.Comme signalé précédemment, les types T et S ne sont pas forcément identiques.

Pour utiliser ce prototype, vous devez passer un délégué à une méthode de sélection vial’argument selector. Ce dernier doit accepter un élément de type T (où T est le type deséléments contenus dans la séquence d’entrée) et retourner un élément de type S. L’opéra-teur Select appelle la méthode selector pour chacun des éléments de la séquenced’entrée. La méthode selector choisit une portion de l’élément passé, crée un nouvelélément, éventuellement d’un autre type (y compris le type anonyme) et le retourne.

Second prototype

public static IEnumerable<S> Select<T, S>( this IEnumerable<T> source, Func<T, int, S> selector);

Ce second prototype est semblable au premier si ce n’est qu’un argument complémen-taire de type entier est passé au délégué. Cet argument correspond à l’index del’élément dans la séquence (l’index du premier élément est 0).

ExceptionsL’exception ArgumentNullException est levée si un des arguments a pour valeur null.

ExemplesLe Listing 4.3 est un exemple d’appel du premier prototype.

Listing 4.3 : Un exemple d’utilisation du premier prototype.

string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"};

IEnumerable<int> nameLengths = presidents.Select(p => p.Length);

foreach (int item in nameLengths) Console.WriteLine(item);

La méthode selector est passée par l’intermédiaire d’une expression lambda. Cettedernière retourne la longueur des éléments de la séquence d’entrée. Remarquez que lestypes des séquences d’entrée et de sortie diffèrent : string pour la première, integerpour la deuxième.

Linq.book Page 68 Mercredi, 18. février 2009 7:58 07

Page 84: LINQ Language Integrated Query en C

Chapitre 4 Les opérateurs différés 69

Voici le résultat de ce code lorsque vous appuyez sur Ctrl+F5 :

Cet exemple est très simple, puisqu’il ne génère aucune classe. Le Listing 4.4 donne unexemple plus élaboré du premier prototype de l’opérateur Select.

Listing 4.4 : Un autre exemple d’utilisation du premier prototype.

string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"};

var nameObjs = presidents.Select(p => new { p, p.Length });

foreach (var item in nameObjs) Console.WriteLine(item);

Ici, l’expression lambda instancie un nouveau type anonyme. Le compilateur génèredynamiquement un objet de type anonyme qui contient un string p et un int

568469781084857856797777865646946659106

Linq.book Page 69 Mercredi, 18. février 2009 7:58 07

Page 85: LINQ Language Integrated Query en C

70 LINQ to Objects Partie II

p.Length, et la méthode selector retourne cet objet. Étant donné que l’élémentretourné est de type anonyme, il n’existe aucun type pour y faire référence. Contraire-ment à l’exemple précédent, où la séquence de sortie avait été affectée à un IEnumera-ble<int>, il est impossible d’affecter la séquence de sortie à un IEnumerable d’un typeconnu. C’est la raison pour laquelle le mot-clé var a été utilisé.

INFO

Les opérateurs de projection dont les méthodes selector instancient des types anonymesdoivent affecter leur séquence de sortie à une variable déclarée avec le mot-clé var.

Voici la sortie dans la console lorsque vous appuyez sur Ctrl+F5 :

Dans son état actuel, ce code a un inconvénient : il ne permet pas d’agir sur lesmembres de la classe anonyme générée dynamiquement. Cependant, grâce à la fonc-tionnalité d’initialisation d’objets de C# 3.0, il est possible de spécifier les noms desmembres de la classe anonyme dans une expression lambda (voir Listing 4.5).

{ p = Adams, Length = 5 }{ p = Arthur, Length = 6 }{ p = Buchanan, Length = 8 }{ p = Bush, Length = 4 }{ p = Carter, Length = 6 }{ p = Cleveland, Length = 9 }{ p = Clinton, Length = 7 }{ p = Coolidge, Length = 8 }{ p = Eisenhower, Length = 10 }{ p = Fillmore, Length = 8 }{ p = Ford, Length = 4 }{ p = Garfield, Length = 8 }{ p = Grant, Length = 5 }{ p = Harding, Length = 7 }{ p = Harrison, Length = 8 }{ p = Hayes, Length = 5 }{ p = Hoover, Length = 6 }{ p = Jackson, Length = 7 }{ p = Jefferson, Length = 9 }{ p = Johnson, Length = 7 }{ p = Kennedy, Length = 7 }{ p = Lincoln, Length = 7 }{ p = Madison, Length = 7 }{ p = McKinley, Length = 8 }{ p = Monroe, Length = 6 }{ p = Nixon, Length = 5 }{ p = Pierce, Length = 6 }{ p = Polk, Length = 4 }{ p = Reagan, Length = 6 }{ p = Roosevelt, Length = 9 }{ p = Taft, Length = 4 }{ p = Taylor, Length = 6 }{ p = Truman, Length = 6 }{ p = Tyler, Length = 5 }{ p = Van Buren, Length = 9 }{ p = Washington, Length = 10 }{ p = Wilson, Length = 6 }

Linq.book Page 70 Mercredi, 18. février 2009 7:58 07

Page 86: LINQ Language Integrated Query en C

Chapitre 4 Les opérateurs différés 71

Listing 4.5 : Un troisième exemple du premier prototype de l’opérateur Select.

string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"};

var nameObjs = presidents.Select(p => new { LastName = p, Length = p.Length });

foreach (var item in nameObjs) Console.WriteLine("{0} contient {1} caractères", item.LastName, item.Length);

Comme vous pouvez le voir, le nom des membres a été spécifié dans l’expressionlambda, et on a accédé aux membres par leurs noms dans la méthode Console.Write-Line. Voici le résultat de ce code :

Pour illustrer le second prototype, nous allons insérer l’index passé à la méthodeselector dans la séquence de sortie (voir Listing 4.6).

Adams contient 5 caractèresArthur contient 6 caractèresBuchanan contient 8 caractèresBush contient 4 caractèresCarter contient 6 caractèresCleveland contient 9 caractèresClinton contient 7 caractèresCoolidge contient 8 caractèresEisenhower contient 10 caractèresFillmore contient 8 caractèresFord contient 4 caractèresGarfield contient 8 caractèresGrant contient 5 caractèresHarding contient 7 caractèresHarrison contient 8 caractèresHayes contient 5 caractèresHoover contient 6 caractèresJackson contient 7 caractèresJefferson contient 9 caractèresJohnson contient 7 caractèresKennedy contient 7 caractèresLincoln contient 7 caractèresMadcontienton contient 7 caractèresMcKinley contient 8 caractèresMonroe contient 6 caractèresNixon contient 5 caractèresPierce contient 6 caractèresPolk contient 4 caractèresReagan contient 6 caractèresRoosevelt contient 9 caractèresTaft contient 4 caractèresTaylor contient 6 caractèresTruman contient 6 caractèresTyler contient 5 caractèresVan Buren contient 9 caractèresWashington contient 10 caractèresWilson contient 6 caractères

Linq.book Page 71 Mercredi, 18. février 2009 7:58 07

Page 87: LINQ Language Integrated Query en C

72 LINQ to Objects Partie II

Listing 4.6 : Un exemple d’utilisation du second prototype de l’opérateur Select.

string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"};

var nameObjs = presidents.Select((p, i) => new { Index = i, LastName = p });

foreach (var item in nameObjs) Console.WriteLine("{0}. {1}", item.Index + 1, item.LastName);

Pour chaque élément de la séquence d’entrée, cet exemple affiche la valeur del’index augmentée de 1, puis le nom de l’élément. Voici les résultats affichés dans laconsole :

Opérateur SelectManyL’opérateur SelectMany est utilisé pour créer une ou plusieurs séquences à partir de laséquence passée en entrée. Contrairement à l’opérateur Select, qui retourne un élémenten sortie pour chaque élément en entrée, SelectMany peut retourner zéro, un ouplusieurs éléments en sortie pour chaque élément en entrée.

PrototypesDeux prototypes de l’opérateur Select seront étudiés dans ce livre.

Premier prototype

public static IEnumerable<S> SelectMany<T, S>( this IEnumerable<T> source, Func<T, IEnumerable<S>> selector);

Ce prototype admet deux entrées : une séquence source d’éléments de type T et un délé-gué pour effectuer la sélection des données. Il retourne un objet dont l’énumérationpasse chaque élément de la séquence d’entrée au délégué. Lors de l’énumération de laméthode selector, zéro, un ou plusieurs éléments de type S sont retournés dans uneséquence de sortie intermédiaire. L’opérateur SelectMany retourne les différentesséquences de sortie concaténées.

1. Adams2. Arthur3. Buchanan4. Bush5. Carter…34. Tyler35. Van Buren36. Washington37. Wilson

Linq.book Page 72 Mercredi, 18. février 2009 7:58 07

Page 88: LINQ Language Integrated Query en C

Chapitre 4 Les opérateurs différés 73

Second prototype

public static IEnumerable<S> SelectMany<T, S>( this IEnumerable<T> source, Func<T, int, IEnumerable<S>> selector);

Ce prototype est en tout point semblable au précédent, si ce n’est qu’un index deséléments de la séquence d’entrée est passé à la méthode selector (l’index du premierélément est 0).

ExceptionsL’exception ArgumentNullException est levée si un des arguments a pour valeur null.

ExemplesLe Listing 4.7 donne un exemple d’appel du premier prototype.

Listing 4.7 : Un exemple du premier prototype de l’opérateur SelectMany.

string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"};

IEnumerable<char> chars = presidents.SelectMany(p => p.ToArray());

foreach (char ch in chars) Console.WriteLine(ch);

Dans cet exemple, la méthode de sélection reçoit un paramètre string. En lui appli-quant la méthode ToArray, on obtient un tableau de chaînes qui est transformé en unechaîne de sortie de type char.

Pour une unique séquence en entrée (ici, un string), le sélecteur retourne une séquencede caractères. L’opérateur SelectMany concatène toutes ces séquences de caractèresdans une seule qui devient la séquence de sortie.

Voici le texte affiché dans la console suite à l’exécution du code :

AdamsArthur

Linq.book Page 73 Mercredi, 18. février 2009 7:58 07

Page 89: LINQ Language Integrated Query en C

74 LINQ to Objects Partie II

Cette requête est simple à comprendre, mais pas très démonstrative de la façon dontl’opérateur SelectMany est généralement utilisé. Dans le prochain exemple, nous utili-serons les classes communes Employee et EmployeeOptionEntry pour être plus prochesde la réalité.

L’opérateur SelectMany va être appliqué sur un tableau d’éléments Employee. Pourchacun de ces éléments, la méthode de sélection (le délégué) retournera zéro, un ouplusieurs éléments de la classe anonyme. Ces éléments contiendront les champs id etoptionsCount du tableau d’éléments EmployeeOptionEntry de l’objet Employee (voirListing 4.8).

Listing 4.8 : Un exemple plus complexe du premier prototype de l’opérateur SelectMany.

Employee[] employees = Employee.GetEmployeesArray();EmployeeOptionEntry[] empOptions = EmployeeOptionEntry.GetEmployeeOptionEntries();

var employeeOptions = employees .SelectMany(e => empOptions .Where(eo => eo.id == e.id) .Select(eo => new { id = eo.id, optionsCount = eo.optionsCount }));

foreach (var item in employeeOptions) Console.WriteLine(item);

Chaque employé du tableau Employee est passé dans l’expression lambda utilisée dansl’opérateur SelectMany. Par l’intermédiaire de l’opérateur Where, l’expression lambda

BuchananBush…WashingtonWilson

Linq.book Page 74 Mercredi, 18. février 2009 7:58 07

Page 90: LINQ Language Integrated Query en C

Chapitre 4 Les opérateurs différés 75

retrouve alors les éléments EmployeeOptionEntry dont le champ id correspond auchamp id de l’employé actuel. Ce code effectue donc une jointure des tableauxEmployee et EmployeeOptionEntry sur le champ id.

L’opérateur Select de l’expression lambda crée alors un objet anonyme composé desmembres id et optionsCount pour chacun des enregistrements sélectionnés dans letableau EmployeeOptionEntry. L’expression lambda retourne donc une séquence dezéro, un ou plusieurs objets anonymes pour chacun des employés sélectionnés. Lerésultat final est une séquence de séquences concaténées par l’opérateur SelectMany.

Voici le résultat de ce code, affiché dans la console :

Bien qu’un peu tiré par les cheveux, le Listing 4.9 donne un exemple d’appel du secondprototype de l’opérateur SelectMany.

Listing 4.9 : Un exemple d’appel du second prototype de l’opérateur SelectMany.

string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"};

IEnumerable<char> chars = presidents .SelectMany((p, i) => i < 5 ? p.ToArray() : new char[] { });

foreach (char ch in chars) Console.WriteLine(ch);

L’expression lambda teste la valeur de l’index. S’il est inférieur à 5, le tableau de carac-tères de la chaîne passée en entrée est retourné. Voici le résultat affiché dans la console :

{ id = 1, optionsCount = 2 }{ id = 2, optionsCount = 10000 }{ id = 2, optionsCount = 10000 }{ id = 2, optionsCount = 10000 }{ id = 3, optionsCount = 5000 }{ id = 3, optionsCount = 7500 }{ id = 3, optionsCount = 7500 }{ id = 4, optionsCount = 1500 }{ id = 101, optionsCount = 2 }

AdamsArthur

Linq.book Page 75 Mercredi, 18. février 2009 7:58 07

Page 91: LINQ Language Integrated Query en C

76 LINQ to Objects Partie II

Cette expression lambda n’est pas particulièrement efficace, en particulier si le nombred’éléments en entrée est élevé. Elle est en effet appelée pour chacun des élémentspassés en entrée, y compris pour ceux dont l’index est supérieur à 5. Dans ce cas, untableau vide est retourné. Pour une plus grande efficacité, vous préférerez l’opérateurTake (voir la section suivante).

L’opérateur SelectMany peut également être utilisé lorsqu’il s’agit de concaténerplusieurs séquences. Reportez-vous à la section relative à l’opérateur Concat, un peuplus loin dans ce chapitre, pour avoir un exemple de concaténation.

Partage

Les opérateurs de partage retournent une séquence qui est un sous-ensemble de laséquence d’entrée.

Opérateur TakeL’opérateur Take retourne un certain nombre d’éléments de la séquence d’entrée, àpartir du premier.

PrototypeUn seul prototype de l’opérateur Take sera étudié dans ce livre :

public static IEnumerable<T> Take<T>( this IEnumerable<T> source, int count);

L’opérateur Take admet deux paramètres en entrée : une séquence source et l’entiercount, qui indique combien d’éléments doivent être retournés. Il renvoie un objet dontl’énumération produira les count premiers éléments de la séquence d’entrée.

Si count est plus grand que le nombre d’éléments contenus dans la séquence d’entrée,la totalité de la séquence d’entrée est retournée.

BuchananBushCarter

Linq.book Page 76 Mercredi, 18. février 2009 7:58 07

Page 92: LINQ Language Integrated Query en C

Chapitre 4 Les opérateurs différés 77

ExceptionsL’exception ArgumentNullException est levée si la séquence source a pour valeur null.

ExemplesLe Listing 4.10 donne un exemple d’appel de l’opérateur Take.

Listing 4.10 : Un exemple d’appel de l’unique prototype Take.

string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"};

IEnumerable<string> items = presidents.Take(5);

foreach (string item in items) Console.WriteLine(item);

Ce code retourne les cinq premiers éléments du tableau presidents :

Dans l’exemple précédent, j’ai indiqué que le code serait plus efficace si l’opérateurTake était utilisé pour limiter le nombre d’entrées soumises à l’expression lambda. Lecode auquel je faisais référence se trouve dans le Listing 4.11.

Listing 4.11 : Un autre exemple d’appel du prototype Take.

string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"};

IEnumerable<char> chars = presidents.Take(5).SelectMany(s => s.ToArray());

foreach (char ch in chars) Console.WriteLine(ch);

La sortie console est identique à celle du Listing 4.9 :

AdamsArthurBuchananBushCarter

Adams

Linq.book Page 77 Mercredi, 18. février 2009 7:58 07

Page 93: LINQ Language Integrated Query en C

78 LINQ to Objects Partie II

Contrairement au Listing 4.9, seuls les cinq premiers éléments sont passés en entrée del’opérateur SelectMany. Cette technique est bien plus efficace, en particulier si denombreux éléments ne doivent pas être passés à SelectMany.

L’opérateur TakeWhileL’opérateur TakeWhile renvoie les éléments de la séquence d’entrée, en commençantpar le premier, tant qu’une condition est vérifiée. Les éléments restants sont ignorés.

PrototypesDeux prototypes de l’opérateur TakeWhile seront étudiés dans ce livre.

Premier prototype

public static IEnumerable<T> TakeWhile<T>( this IEnumerable<T> source, Func<T, bool> predicate);

Dans ce prototype, l’opérateur TakeWhile admet deux paramètres en entrée : uneséquence source et un prédicat. Il retourne un objet dont l’énumération fournit deséléments jusqu’à ce que le prédicat renvoie la valeur false. Les éléments suivants nesont pas traités.

Second prototype

public static IEnumerable<T> TakeWhile<T>( this IEnumerable<T> source, Func<T, int, bool> predicate);

Ce second prototype est semblable au premier si ce n’est que le prédicat reçoit égale-ment un entier qui correspond à l’index de l’élément dans la séquence source.

ArthurBuchananBushCarter

Linq.book Page 78 Mercredi, 18. février 2009 7:58 07

Page 94: LINQ Language Integrated Query en C

Chapitre 4 Les opérateurs différés 79

ExceptionsL’exception ArgumentNullException est levée si un des arguments a pour valeur null.

ExemplesLe Listing 4.12 donne un exemple d’appel du premier prototype.

Listing 4.12 : Un exemple d’appel du premier prototype de l’opérateur TakeWhile.

string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"};

IEnumerable<string> items = presidents.TakeWhile(s => s.Length < 10);

foreach (string item in items) Console.WriteLine(item);

Seuls les éléments contenant dix caractères au maximum sont retournés :

L’énumération s’est arrêtée sur le nom Eisenhower, long de 10 caractères.

Voici maintenant un exemple d’appel du second prototype de l’opérateur TakeWhile.

Listing 4.13 : Un exemple d’appel du second prototype TakeWhile.

string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"};

IEnumerable<string> items = presidents .TakeWhile((s, i) => s.Length < 10 && i < 5);

foreach (string item in items) Console.WriteLine(item);

AdamsArthurBuchananBushCarterClevelandClintonCoolidge

Linq.book Page 79 Mercredi, 18. février 2009 7:58 07

Page 95: LINQ Language Integrated Query en C

80 LINQ to Objects Partie II

Cet exemple arrête l’énumération lorsqu’un élément en entrée a une longueur supé-rieure à 9 caractères ou lorsque la sixième entrée est atteinte. Voici le résultat :

Ici, l’énumération s’est arrêtée lorsque la sixième entrée a été atteinte.

Opérateur SkipL’opérateur Skip saute un certain nombre d’éléments dans la séquence d’entrée etretourne les suivants.

PrototypeUn seul prototype de l’opérateur Skip sera étudié dans ce livre :

public static IEnumerable<T> Skip<T>( this IEnumerable<T> source, int count);

L’opérateur Skip admet deux paramètres : une séquence source et l’entier count, quiindique le nombre d’éléments à sauter. Ce prototype renvoie un objet dont l’énumérationexclut les count premiers éléments.

Si la valeur de count est supérieure au nombre d’éléments de la séquence d’entrée, cettedernière ne sera pas énumérée et la séquence de sortie sera vide.

ExceptionsL’exception ArgumentNullException est levée si la séquence d’entrée a pour valeurnull.

ExemplesLe Listing 4.14 est un exemple d’appel du prototype Skip.

Listing 4.14 : Un exemple d’utilisation du prototype de l’opérateur Skip.

string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"};

IEnumerable<string> items = presidents.Skip(1);

foreach (string item in items) Console.WriteLine(item);

AdamsArthurBuchananBushCarter

Linq.book Page 80 Mercredi, 18. février 2009 7:58 07

Page 96: LINQ Language Integrated Query en C

Chapitre 4 Les opérateurs différés 81

Dans cet exemple, seul le premier élément est ignoré. Tous les éléments suivants sontdonc renvoyés par l’opérateur Skip :

Opérateur SkipWhileL’opérateur SkipWhile ignore les éléments de la séquence d’entrée tant qu’unecondition est vérifiée. Les éléments suivants sont alors renvoyés dans la séquence desortie.

PrototypesDeux prototypes de l’opérateur SkipWhile seront étudiés dans ce livre.

Premier prototype

public static IEnumerable<T> SkipWhile<T>( this IEnumerable<T> source, Func<T, bool> predicate);

Ce premier prototype admet deux paramètres : une séquence source et un prédicat. Ilrenvoie un objet dont l’énumération exclut les éléments de la séquence d’entrée tant quele prédicat retourne la valeur true. Dès qu’une valeur false est retournée, tous leséléments suivants sont envoyés dans la séquence de sortie.

Second prototype

public static IEnumerable<T> SkipWhile<T>( this IEnumerable<T> source, Func<T, int, bool> predicate);

Ce second prototype est semblable au premier si ce n’est que le prédicat reçoitégalement un entier qui correspond à l’index de l’élément dans la séquence source.

ExceptionsL’exception ArgumentNullException est levée si un des arguments a pour valeur null.

ExemplesLe Listing 4.15 donne un exemple d’appel du premier prototype de l’opérateurSkipWhile.

Listing 4.15 : Un exemple d’appel du premier prototype de l’opérateur SkipWhile.

string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson",

ArthurBuchananBush…Van BurenWashingtonWilson

Linq.book Page 81 Mercredi, 18. février 2009 7:58 07

Page 97: LINQ Language Integrated Query en C

82 LINQ to Objects Partie II

"Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"};

IEnumerable<string> items = presidents.SkipWhile(s => s.StartsWith("A"));

foreach (string item in items) Console.WriteLine(item);

Dans cet exemple, tous les éléments qui commencent par la lettre A sont ignorés. Leséléments suivants sont passés à la séquence de sortie :

Le Listing 4.16 donne un exemple d’utilisation du second prototype de l’opérateurSkipWhile.

Listing 4.16 : Un exemple d’utilisation du second prototype de l’opérateur SkipWhile.

string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"};

IEnumerable<string> items = presidents .SkipWhile((s, i) => s.Length > 4 && i < 10);

foreach (string item in items) Console.WriteLine(item);

Dans cet exemple, tous les éléments dont la longueur est inférieure ou égale à 4 carac-tères ou supérieure ou égale à 10 caractères sont ignorés. Les éléments suivants consti-tuent la séquence de sortie :

L’élément Bush compte 4 caractères. Il a donc mis fin au SkipWhile.

BuchananBushCarter…Van BurenWashingtonWilson

BushCarterCleveland…Van BurenWashingtonWilson

Linq.book Page 82 Mercredi, 18. février 2009 7:58 07

Page 98: LINQ Language Integrated Query en C

Chapitre 4 Les opérateurs différés 83

Concaténation

Les opérateurs de concaténation accolent plusieurs séquences d’entrée dans la séquencede sortie.

Opérateur ConcatL’opérateur Concat accole deux séquences d’entrée dans la séquence de sortie.

PrototypeUn seul prototype de l’opérateur Concat sera étudié dans ce livre :

public static IEnumerable<T> Concat<T>( this IEnumerable<T> first, IEnumerable<T> second);

Deux séquences de même type T sont fournies en entrée de ce prototype : first etsecond. L’énumération de l’objet retourné renvoie tous les éléments de la premièreséquence d’entrée suivis de tous les éléments de la seconde séquence d’entrée.

ExceptionsL’exception ArgumentNullException est levée si un des arguments a pour valeur null.

ExemplesLe Listing 4.17 donne un exemple d’utilisation des opérateurs Concat, Take et Skip.

Listing 4.17 : Un exemple d’utilisation du prototype de l’opérateur Concat.

string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"};

IEnumerable<string> items = presidents.Take(5).Concat(presidents.Skip(5));

foreach (string item in items) Console.WriteLine(item);

Ce code concatène les cinq premiers éléments de la séquence d’entrée presidents auxéléments de cette même séquence d’entrée, en excluant les cinq premiers. Le résultatcontient donc tous les éléments de la séquence d’entrée :

AdamsArthurBuchananBushCarterClevelandClinton

Linq.book Page 83 Mercredi, 18. février 2009 7:58 07

Page 99: LINQ Language Integrated Query en C

84 LINQ to Objects Partie II

Pour effectuer une concaténation, vous pouvez également utiliser l’opérateur Select-Many (voir Listing 4.18).

Listing 4.18 : Un exemple effectuant une concaténation sans l’opérateur Concat.

string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"};

IEnumerable<string> items = new[] { presidents.Take(5), presidents.Skip(5) } .SelectMany(s => s);

foreach (string item in items) Console.WriteLine(item);

Le tableau item a été instancié par l’intermédiaire de deux séquences : une créée avecl’opérateur Take et une autre, avec l’opérateur Skip. Cet exemple est comparable auprécédent mais, ici, on fait appel à l’opérateur SelectMany.

CoolidgeEisenhowerFillmoreFordGarfieldGrantHardingHarrisonHayesHooverJacksonJeffersonJohnsonKennedyLincolnMadisonMcKinleyMonroeNixonPiercePolkReaganRooseveltTaftTaylorTrumanTylerVan BurenWashingtonWilson

Linq.book Page 84 Mercredi, 18. février 2009 7:58 07

Page 100: LINQ Language Integrated Query en C

Chapitre 4 Les opérateurs différés 85

ASTUCE

Si vous devez concaténer plusieurs séquences, vous utiliserez l’opérateur SelectMany.L’opérateur Concat, quant à lui, est limité à la concaténation de deux séquences.

Voici le résultat affiché dans la console :

Tri

Les opérateurs de tri permettent de classer des séquences. Les opérateurs OrderBy etOrderByDescending nécessitent tous deux une séquence d’entrée de type IEnumera-ble<T> et retournent une séquence de type IOrderedEnumerable<T>. Il est impossiblede passer un IOrderedEnumerable<T> en entrée des opérateurs OrderBy et OrderBy-Descending. Tout chaînage est donc impossible.

Si vous avez besoin de trier conjointement plusieurs éléments, utilisez les opérateursThenBy ou ThenByDescending. Ces opérateurs peuvent être chaînés car ils admettent etretournent des IOrderedEnumerable<T>.

AdamsArthurBuchananBushCarterClevelandClintonCoolidgeEisenhowerFillmoreFordGarfieldGrantHardingHarrisonHayesHooverJacksonJeffersonJohnsonKennedyLincolnMadisonMcKinleyMonroeNixonPiercePolkReaganRooseveltTaftTaylorTrumanTylerVan BurenWashingtonWilson

Linq.book Page 85 Mercredi, 18. février 2009 7:58 07

Page 101: LINQ Language Integrated Query en C

86 LINQ to Objects Partie II

À titre d’exemple, cet appel n’est pas valide :

inputSequence.OrderBy(s => s.LastName).OrderBy(s => s.FirstName)…

Pour effectuer ce traitement, vous utiliserez la syntaxe suivante :

inputSequence.OrderBy(s => s.LastName).ThenBy(s => s.FirstName)…

L’opérateur OrderByL’opérateur OrderBy trie une séquence d’entrée en utilisant la méthode keySelector.Cette méthode retourne une valeur clé pour chaque élément en entrée et une séquencede sortie de type IOrderedEnumerable<T>. Dans cette dernière, les éléments serontclassés dans un ordre croissant, en se basant sur les valeurs clés retournées.

Le tri effectué par l’opérateur OrderBy est connu pour être "instable" : si deuxéléments ayant la même valeur clé sont passés à OrderBy, leur ordre initial peut aussibien être maintenu qu’inversé. Vous ne devez donc jamais vous fier à l’ordre deséléments issus de ces opérateurs OrderBy et OrderByDescending pour les champs quine sont pas spécifiés dans la méthode.

ATTENTIONATTENTION

Le tri effectué par les opérateurs OrderBy et OrderByDescending est "instable".

PrototypesDeux prototypes de l’opérateur OrderBy seront étudiés dans ce livre.

Premier prototype

public static IOrderedEnumerable<T> OrderBy<T, K>( this IEnumerable<T> source, Func<T, K> keySelector)where K : IComparable<K>;

Ce prototype admet deux entrées : une séquence source et le délégué keySelector.L’énumération de l’objet retourné passe tous les éléments de la séquence d’entrée à laméthode KeySelector afin d’obtenir leurs clés et de procéder à leur tri.

La méthode KeySelector se voit passer un élément de type T. Elle retourne la valeur clé detype K. Les types T et K peuvent être similaires ou différents. En revanche, le type de lavaleur retournée par la méthode KeySelector doit implémenter l’interface IComparable.

Second prototype

public static IOrderedEnumerable<T> OrderBy<T, K>(this IEnumerable<T> source,Func<T, K> keySelector,IComparer<K> comparer);

Ce prototype est le même que le précédent, si ce n’est qu’un objet comparer complé-mentaire lui est passé. Si vous utilisez cette version de l’opérateur OrderBy, le type Kn’est pas forcé d’implémenter l’interface IComparable.

Linq.book Page 86 Mercredi, 18. février 2009 7:58 07

Page 102: LINQ Language Integrated Query en C

Chapitre 4 Les opérateurs différés 87

ExceptionsL’exception ArgumentNullException est levée si un des arguments a pour valeur null.

ExemplesLe Listing 4.19 est un exemple d’utilisation du premier prototype.

Listing 4.19 : Un exemple du premier prototype de l’opérateur OrderBy.

string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"};

IEnumerable<string> items = presidents.OrderBy(s => s.Length);

foreach (string item in items) Console.WriteLine(item);

Cet exemple classe les présidents par la longueur de leurs noms. Voici les résultats :

BushFordPolkTaftAdamsGrantHayesNixonTylerArthurCarterHooverMonroePierceReaganTaylorTrumanWilsonClintonHardingJacksonJohnsonKennedyLincolnMadisonBuchananCoolidgeFillmoreGarfieldHarrisonMcKinleyClevelandJeffersonRooseveltVan BurenEisenhowerWashington

Linq.book Page 87 Mercredi, 18. février 2009 7:58 07

Page 103: LINQ Language Integrated Query en C

88 LINQ to Objects Partie II

Nous allons maintenant donner un exemple d’utilisation du deuxième prototype. Mais,auparavant, prenons quelques instants pour examiner l’interface IComparer :

interface IComparer<T> { int Compare(T x, T y);}

Cette interface utilise la méthode Compare. Cette dernière admet deux arguments detype T en entrée et retourne une valeur int. Sa valeur est :

m négative si le premier argument est inférieur au second ;

m nulle si les deux arguments sont égaux ;

m positive si le second argument est supérieur au premier.

Remarquez à quel point les génériques de C# 2.0 sont utiles dans cette interface et ceprototype.

Pour faire fonctionner cet exemple, une classe spécifique qui implémente l’interfaceIComparer a été créée. Cette classe réarrangera les éléments par rapport à leur rationombre de voyelles/nombre de consonnes.

Implémentation de l’interface IComparer pour illustrer le second prototype OrderBy

public class MyVowelToConsonantRatioComparer : IComparer<string>{ public int Compare(string s1, string s2) { int vCount1 = 0; int cCount1 = 0; int vCount2 = 0; int cCount2 = 0;

GetVowelConsonantCount(s1, ref vCount1, ref cCount1); GetVowelConsonantCount(s2, ref vCount2, ref cCount2);

double dRatio1 = (double)vCount1/(double)cCount1; double dRatio2 = (double)vCount2/(double)cCount2;

if(dRatio1 < dRatio2) return(-1); else if (dRatio1 > dRatio2) return(1); else return(0); }

// Cette méthode est publique. Le code qui utilise ce comparateur // pourra donc y accéder si cela est nécessaire public void GetVowelConsonantCount(string s, ref int vowelCount, ref int consonantCount) { string vowels = "AEIOUY";

// Initialize the counts. vowelCount = 0; consonantCount = 0;

Linq.book Page 88 Mercredi, 18. février 2009 7:58 07

Page 104: LINQ Language Integrated Query en C

Chapitre 4 Les opérateurs différés 89

// Conversion en majuscules pour ne pas être sensible à la casse string sUpper = s.ToUpper();

foreach(char ch in sUpper) { if(vowels.IndexOf(ch) < 0) consonantCount++; else vowelCount++; }

return; }}

Cette classe contient deux méthodes : Compare et GetVowelConsonantCount. Laméthode Compare est nécessaire pour l’interface IComparer. La méthode GetConsonan-tVowelCount calcule le nombre de voyelles et de consonnes de la chaîne qui lui estpassée. Par son intermédiaire, il est ainsi possible d’obtenir les valeurs à afficher lors del’énumération de la séquence réordonnée.

La logique utilisée à l’intérieur de la méthode n’a pas d’importance. Il est en effet peuprobable que vous ayez un jour à classer des données en tenant compte de leur rationombre de voyelles/nombre de consonnes, et encore moins de comparer deux chaînesselon ce ratio. Ce qui est important, en revanche, c’est la technique qui a permis decréer une classe qui implémente l’interface IComparer en implémentant la méthodeCompare. Pour cela, examinez le bloc if … else à la fin de la méthode Compare.Comme vous le voyez, les valeurs retournées sont -1, 1 ou 0, ce qui assure la compati-bilité avec l’interface IComparer. Le Listing 4.20 donne un exemple d’appel du code.

Listing 4.20 : Un exemple d’appel du second prototype OrderBy.

string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"};

MyVowelToConsonantRatioComparer myComp = new MyVowelToConsonantRatioComparer();

IEnumerable<string> namesByVToCRatio = presidents .OrderBy((s => s), myComp);

foreach (string item in namesByVToCRatio){ int vCount = 0; int cCount = 0; myComp.GetVowelConsonantCount(item, ref vCount, ref cCount); double dRatio = (double)vCount / (double)cCount;

Console.WriteLine(item + " - " + dRatio + " - " + vCount + ":" + cCount);}

Linq.book Page 89 Mercredi, 18. février 2009 7:58 07

Page 105: LINQ Language Integrated Query en C

90 LINQ to Objects Partie II

L’objet mycomp a été instancié avant d’appeler l’opérateur OrderBy. Une référence est donccréée, et il est possible de l’utiliser dans la boucle foreach. Voici les résultats de ce code :

Les présidents sont classés par ratio voyelle/consonne croissant.

L’opérateur OrderByDescendingCet opérateur a les mêmes prototypes et comportement que OrderBy, excepté que leséléments sont classés dans un ordre décroissant.

PrototypesDeux prototypes de l’opérateur OrderByDescending seront étudiés dans ce livre.

Premier prototype

public static IOrderedEnumerable<T> OrderByDescending<T, K>( this IEnumerable<T> source, Func<T, K> keySelector)where K : IComparable<K>;

Grant - 0.25 - 1:4Bush - 0.333333333333333 - 1:3Ford - 0.333333333333333 - 1:3Polk - 0.333333333333333 - 1:3Taft - 0.333333333333333 - 1:3Clinton - 0.4 - 2:5Harding - 0.4 - 2:5Jackson - 0.4 - 2:5Johnson - 0.4 - 2:5Lincoln - 0.4 - 2:5Washington - 0.428571428571429 - 3:7Arthur - 0.5 - 2:4Carter - 0.5 - 2:4Cleveland - 0.5 - 3:6Jefferson - 0.5 - 3:6Truman - 0.5 - 2:4Van Buren - 0.5 - 3:6Wilson - 0.5 - 2:4Buchanan - 0.6 - 3:5Fillmore - 0.6 - 3:5Garfield - 0.6 - 3:5Harrison - 0.6 - 3:5McKinley - 0.6 - 3:5Adams - 0.666666666666667 - 2:3Nixon - 0.666666666666667 - 2:3Tyler - 0.666666666666667 - 2:3Kennedy - 0.75 - 3:4Madison - 0.75 - 3:4Roosevelt - 0.8 - 4:5Coolidge - 1 - 4:4Eisenhower - 1 - 5:5Hoover - 1 - 3:3Monroe - 1 - 3:3Pierce - 1 - 3:3Reagan - 1 - 3:3Taylor - 1 - 3:3Hayes - 1.5 - 3:2

Linq.book Page 90 Mercredi, 18. février 2009 7:58 07

Page 106: LINQ Language Integrated Query en C

Chapitre 4 Les opérateurs différés 91

ATTENTIONATTENTION

Le tri effectué par les opérateurs OrderBy et OrderByDescending est "instable".

Second prototype

public static IOrderedEnumerable<T> OrderByDescending<T, K>( this IEnumerable<T> source, Func<T, K> keySelector, IComparer<K> comparer);

Ce prototype est le même que le précédent, si ce n’est qu’un objet comparer complé-mentaire lui est passé. Si vous utilisez cette version de l’opérateur OrderByDescending,le type K n’est pas forcé d’implémenter l’interface IComparable.

ExceptionsL’exception ArgumentNullException est levée si un des arguments a pour valeur null.

ExemplesDans l’exemple du Listing 4.21, nous allons classer les présidents des États-Unis enutilisant un ordre inverse alphabétique sur leurs noms.

Listing 4.21 : Un exemple d’utilisation du premier prototype d’OrderDescending.

string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"};

IEnumerable<string> items = presidents.OrderByDescending(s => s);

foreach (string item in items) Console.WriteLine(item);

Les présidents sont bien classés en utilisant un ordre inverse alphabétique sur leurs noms.

WilsonWashingtonVan BurenTylerTrumanTaylorTaftRooseveltReaganPolkPierceNixonMonroeMcKinleyMadisonLincoln

Linq.book Page 91 Mercredi, 18. février 2009 7:58 07

Page 107: LINQ Language Integrated Query en C

92 LINQ to Objects Partie II

Nous allons maintenant donner un exemple d’appel du second prototype d’OrderByDes-cending. Nous utiliserons le même code (y compris au niveau du comparateur MyVowel-ToConsonantRatioComparer) que dans la section relative à l’opérateur OrderBy. Mais,ici, c’est l’opérateur OrderByDescending qui sera appelé (voir Listing 4.22).

Listing 4.22 : Un exemple d’appel du second prototype d’OrderByDescending.

string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"};

MyVowelToConsonantRatioComparer myComp = new MyVowelToConsonantRatioComparer();

IEnumerable<string> namesByVToCRatio = presidents .OrderByDescending((s => s), myComp);

foreach (string item in namesByVToCRatio){ int vCount = 0; int cCount = 0;

myComp.GetVowelConsonantCount(item, ref vCount, ref cCount); double dRatio = (double)vCount / (double)cCount;

Console.WriteLine(item + " - " + dRatio + " - " + vCount + ":" + cCount);}

Voici les résultats de cet exemple :

KennedyJohnsonJeffersonJacksonHooverHayesHarrisonHardingGrantGarfieldFordFillmoreEisenhowerCoolidgeClintonClevelandCarterBushBuchananArthurAdams

Hayes - 1.5 - 3:2Coolidge - 1 - 4:4Eisenhower - 1 - 5:5

Linq.book Page 92 Mercredi, 18. février 2009 7:58 07

Page 108: LINQ Language Integrated Query en C

Chapitre 4 Les opérateurs différés 93

Ces résultats sont les mêmes que dans l’exemple de la section précédente mais, ici, leclassement a été effectué du plus grand au plus petit ratio voyelles/consonnes.

Opérateur ThenByL’opérateur ThenBy trie une séquence de type IOrderedEnumerable<T> en se basant surune méthode keySelector qui lui retourne une valeur clé. Il renvoie une séquence desortie de type IOrderedEnumerable<T>.

INFO

Les opérateurs ThenBy et ThenByDescending demandent tous deux un paramètre dont letype est inhabituel : IOrderedEnumerable<T>. L’opérateur OrderBy ou OrderByDescendingdoit être appelé en premier lieu pour créer un objet IOrderedEnumerable.

INFO

Contrairement aux opérateurs OrderBy et OrderByDescending, ThenBy et ThenByDes-cending sont stables. Ils préservent donc l’ordre original des éléments qui possèdent lamême clé.

Hoover - 1 - 3:3Monroe - 1 - 3:3Pierce - 1 - 3:3Reagan - 1 - 3:3Taylor - 1 - 3:3Roosevelt - 0.8 - 4:5Kennedy - 0.75 - 3:4Madison - 0.75 - 3:4Adams - 0.666666666666667 - 2:3Nixon - 0.666666666666667 - 2:3Tyler - 0.666666666666667 - 2:3Buchanan - 0.6 - 3:5Fillmore - 0.6 - 3:5Garfield - 0.6 - 3:5Harrison - 0.6 - 3:5McKinley - 0.6 - 3:5Arthur - 0.5 - 2:4Carter - 0.5 - 2:4Cleveland - 0.5 - 3:6Jefferson - 0.5 - 3:6Truman - 0.5 - 2:4Van Buren - 0.5 - 3:6Wilson - 0.5 - 2:4Washington - 0.428571428571429 - 3:7Clinton - 0.4 - 2:5Harding - 0.4 - 2:5Jackson - 0.4 - 2:5Johnson - 0.4 - 2:5Lincoln - 0.4 - 2:5Bush - 0.333333333333333 - 1:3Ford - 0.333333333333333 - 1:3Polk - 0.333333333333333 - 1:3Taft - 0.333333333333333 - 1:3Grant - 0.25 - 1:4

Linq.book Page 93 Mercredi, 18. février 2009 7:58 07

Page 109: LINQ Language Integrated Query en C

94 LINQ to Objects Partie II

PrototypesDeux prototypes de l’opérateur ThenBy seront étudiés dans ce livre.

Premier prototype

public static IOrderedEnumerable<T> ThenBy<T, K>( this IOrderedEnumerable<T> source, Func<T, K> keySelector)where K : IComparable<K>;

Dans ce prototype, l’opérateur ThenBy reçoit une séquence d’entrée de type IOrdere-dEnumerable<T> et un délégué keySelector. Ce dernier se voit passer l’élémentd’entrée de type T et retourne le champ de type K de cet élément qui sera utilisé commevaleur clé. Les types T et K peuvent être identiques ou différents. La valeur retournéepar la méthode KeySelector doit implémenter l’interface ICompare. L’opérateurThenBy classe la séquence d’entrée par ordre croissant selon la clé retournée parkeySelector.

Second prototype

public static IOrderedEnumerable<T> ThenBy<T, K>( this IOrderedEnumerable<T> source, Func<T, K> keySelector, IComparer<K> comparer);

Ce prototype est identique au précédent, si ce n’est qu’un objet comparer complémen-taire lui est passé. Si vous utilisez cette version de l’opérateur ThenBy, le type K n’est pasforcé d’implémenter l’interface IComparable.

ExceptionsL’exception ArgumentNullException est levée si un des arguments a pour valeur null(voir Listing 4.23).

Exemples

Listing 4.23 : Un exemple d’appel du premier prototype.

string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"};

IEnumerable<string> items = presidents.OrderBy(s => s.Length).ThenBy(s => s);

foreach (string item in items) Console.WriteLine(item);

Dans un premier temps, ce code classe les éléments (ici, les noms des présidents desÉtats-Unis) selon leur longueur. Dans un second temps, les éléments sont classés dans

Linq.book Page 94 Mercredi, 18. février 2009 7:58 07

Page 110: LINQ Language Integrated Query en C

Chapitre 4 Les opérateurs différés 95

un ordre alphabétique. Si plusieurs noms ont la même longueur, ils apparaîtront doncdans l’ordre alphabétique.

Pour illustrer le second prototype de l’opérateur ThenBy, nous allons utiliser le compa-rateur MyVowelConsonantRatioComparer, introduit quelques pages précédemment.Pour être en mesure d’appeler l’opérateur ThenBy, il faut au préalable appeler l’opéra-teur OrderBy ou OrderByDescending. Le but de cet exemple est de classer les noms parlongueurs croissantes puis, à l’intérieur de chaque groupe de longueurs, par ratio voyel-les/consonnes (voir Listing 4.24).

Listing 4.24 : Un exemple d’appel du second prototype de ThenBy.

string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft",

BushFordPolkTaftAdamsGrantHayesNixonTylerArthurCarterHooverMonroePierceReaganTaylorTrumanWilsonClintonHardingJacksonJohnsonKennedyLincolnMadisonBuchananCoolidgeFillmoreGarfieldHarrisonMcKinleyClevelandJeffersonRooseveltVan BurenEisenhowerWashington

Linq.book Page 95 Mercredi, 18. février 2009 7:58 07

Page 111: LINQ Language Integrated Query en C

96 LINQ to Objects Partie II

"Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"};MyVowelToConsonantRatioComparer myComp = new MyVowelToConsonantRatioComparer();

IEnumerable<string> namesByVToCRatio = presidents .OrderBy(n => n.Length) .ThenBy((s => s), myComp);

foreach (string item in namesByVToCRatio){ int vCount = 0; int cCount = 0;

myComp.GetVowelConsonantCount(item, ref vCount, ref cCount); double dRatio = (double)vCount / (double)cCount;

Console.WriteLine(item + " - " + dRatio + " - " + vCount + ":" + cCount);}

Voici le résultat de ce code :

Comme prévu, les noms sont classés par longueurs, puis par ratio voyelles/consonnes.

Bush - 0.333333333333333 - 1:3Ford - 0.333333333333333 - 1:3Polk - 0.333333333333333 - 1:3Taft - 0.333333333333333 - 1:3Grant - 0.25 - 1:4Adams - 0.666666666666667 - 2:3Nixon - 0.666666666666667 - 2:3Tyler - 0.666666666666667 - 2:3Hayes - 1.5 - 3:2Arthur - 0.5 - 2:4Carter - 0.5 - 2:4Truman - 0.5 - 2:4Wilson - 0.5 - 2:4Hoover - 1 - 3:3Monroe - 1 - 3:3Pierce - 1 - 3:3Reagan - 1 - 3:3Taylor - 1 - 3:3Clinton - 0.4 - 2:5Harding - 0.4 - 2:5Jackson - 0.4 - 2:5Johnson - 0.4 - 2:5Lincoln - 0.4 - 2:5Kennedy - 0.75 - 3:4Madison - 0.75 - 3:4Buchanan - 0.6 - 3:5Fillmore - 0.6 - 3:5Garfield - 0.6 - 3:5Harrison - 0.6 - 3:5McKinley - 0.6 - 3:5Coolidge - 1 - 4:4Cleveland - 0.5 - 3:6Jefferson - 0.5 - 3:6Van Buren - 0.5 - 3:6Roosevelt - 0.8 - 4:5Washington - 0.428571428571429 - 3:7Eisenhower - 1 - 5:5

Linq.book Page 96 Mercredi, 18. février 2009 7:58 07

Page 112: LINQ Language Integrated Query en C

Chapitre 4 Les opérateurs différés 97

Opérateur ThenByDescendingCet opérateur utilise les mêmes prototypes et se comporte comme l’opérateur ThenBy,mais il classe les données dans un ordre décroissant.

PrototypesDeux prototypes de l’opérateur ThenByDescending seront étudiés dans ce livre.

Premier prototype

public static IOrderedEnumerable<T> ThenByDescending<T, K>( this IOrderedEnumerable<T> source, Func<T, K> keySelector)where K : IComparable<K>;

Ce prototype se comporte comme le premier prototype de l’opérateur ThenBy, mais ilclasse les données dans un ordre décroissant.

Second prototype

public static IOrderedEnumerable<T> ThenByDescending<T, K>( this IOrderedEnumerable<T> source, Func<T, K> keySelector, IComparer<K> comparer);

Ce prototype est identique au précédent, si ce n’est qu’un objet comparer complémen-taire lui est passé. Si vous utilisez cette version de l’opérateur ThenByDescending, letype K n’est pas forcé d’implémenter l’interface IComparable.

ExceptionsL’exception ArgumentNullException est levée si un des arguments a pour valeur null.

ExemplesNous allons utiliser le même exemple que dans la section précédente, mais ici l’opéra-teur ThenByDescending sera utilisé à la place de ThenBy (voir Listing 4.25).

Listing 4.25 : Un exemple d’appel du premier prototype de l’opérateur ThenByDescending.

string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"};

IEnumerable<string> items = presidents.OrderBy(s => s.Length).ThenByDescending(s => s);

foreach (string item in items) Console.WriteLine(item);

Linq.book Page 97 Mercredi, 18. février 2009 7:58 07

Page 113: LINQ Language Integrated Query en C

98 LINQ to Objects Partie II

Ce code classe les noms des présidents par longueur croissante puis, à l’intérieur dechaque groupe, par ordre inverse alphabétique.

Pour illustrer le second prototype de l’opérateur ThenByDescending, nous utiliseronsle même code que dans l’exemple du second prototype de l’opérateur ThenBy, à ceciprès que l’opérateur ThenByDescending remplacera l’opérateur ThenBy (voirListing 4.26).

Listing 4.26 : Un exemple d’appel du second prototype de l’opérateur ThenByDescending.

string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"};

TaftPolkFordBushTylerNixonHayesGrantAdamsWilsonTrumanTaylorReaganPierceMonroeHooverCarterArthurMadisonLincolnKennedyJohnsonJacksonHardingClintonMcKinleyHarrisonGarfieldFillmoreCoolidgeBuchananVan BurenRooseveltJeffersonClevelandWashingtonEisenhower

Linq.book Page 98 Mercredi, 18. février 2009 7:58 07

Page 114: LINQ Language Integrated Query en C

Chapitre 4 Les opérateurs différés 99

MyVowelToConsonantRatioComparer myComp = new MyVowelToConsonantRatioComparer();

IEnumerable<string> namesByVToCRatio = presidents .OrderBy(n => n.Length) .ThenByDescending((s => s), myComp);

foreach (string item in namesByVToCRatio){ int vCount = 0; int cCount = 0;

myComp.GetVowelConsonantCount(item, ref vCount, ref cCount); double dRatio = (double)vCount / (double)cCount;

Console.WriteLine(item + " - " + dRatio + " - " + vCount + ":" + cCount);}

Voici les informations affichées dans la console suite à l’exécution de ce code :

Comme vous pouvez le voir, les noms sont classés par longueur croissante, puis parratio voyelles/consonnes décroissant.

Bush - 0.333333333333333 - 1:3Ford - 0.333333333333333 - 1:3Polk - 0.333333333333333 - 1:3Taft - 0.333333333333333 - 1:3Hayes - 1.5 - 3:2Adams - 0.666666666666667 - 2:3Nixon - 0.666666666666667 - 2:3Tyler - 0.666666666666667 - 2:3Grant - 0.25 - 1:4Hoover - 1 - 3:3Monroe - 1 - 3:3Pierce - 1 - 3:3Reagan - 1 - 3:3Taylor - 1 - 3:3Arthur - 0.5 - 2:4Carter - 0.5 - 2:4Truman - 0.5 - 2:4Wilson - 0.5 - 2:4Kennedy - 0.75 - 3:4Madison - 0.75 - 3:4Clinton - 0.4 - 2:5Harding - 0.4 - 2:5Jackson - 0.4 - 2:5Johnson - 0.4 - 2:5Lincoln - 0.4 - 2:5Coolidge - 1 - 4:4Buchanan - 0.6 - 3:5Fillmore - 0.6 - 3:5Garfield - 0.6 - 3:5Harrison - 0.6 - 3:5McKinley - 0.6 - 3:5Roosevelt - 0.8 - 4:5Cleveland - 0.5 - 3:6Jefferson - 0.5 - 3:6Van Buren - 0.5 - 3:6Eisenhower - 1 - 5:5Washington - 0.428571428571429 - 3:7

Linq.book Page 99 Mercredi, 18. février 2009 7:58 07

Page 115: LINQ Language Integrated Query en C

100 LINQ to Objects Partie II

Opérateur ReverseCet opérateur renvoie une séquence du même type que celle passée en entrée, mais eninversant ses éléments.

PrototypeUn seul prototype de l’opérateur Reverse sera étudié dans ce livre :

public static IEnumerable<T> Reverse<T>( this IEnumerable<T> source);

Ce prototype retourne une séquence IEnumerable<T> dont l’énumération produitl’ordre inverse des éléments de la séquence d’entrée.

ExceptionsL’exception ArgumentNullException est levée si l’argument a pour valeur null (voirListing 4.27).

Exemples

Listing 4.27 : Un exemple d’appel de l’opérateur Reverse.

string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"};

IEnumerable<string> items = presidents.Reverse();

foreach (string item in items) Console.WriteLine(item);

Ce code affiche les informations suivantes dans la fenêtre Console. Comme on pouvaits’y attendre, les noms des présidents apparaissent dans l’ordre inverse de ceux passésen entrée :

Opérateurs de jointure

Les opérateurs de jointure effectuent un assemblage de plusieurs séquences.

WilsonWashingtonVan Buren…BushBuchananArthurAdams

Linq.book Page 100 Mercredi, 18. février 2009 7:58 07

Page 116: LINQ Language Integrated Query en C

Chapitre 4 Les opérateurs différés 101

Opérateur JoinL’opérateur Join effectue une jointure entre deux séquences, en se basant sur les clésextraites des différents éléments des deux séquences.

PrototypeUn seul prototype de l’opérateur Join sera abordé dans cet ouvrage :

public static IEnumerable<V> Join<T, U, K, V>( this IEnumerable<T> outer, IEnumerable<U> inner, Func<T, K> outerKeySelector, Func<U, K> innerKeySelector, Func<T, U, V> resultSelector);

Le premier élément de la méthode a pour nom outer (extérieur). Comme il s’agit d’uneméthode d’extension, on parlera de "séquence extérieure" pour faire référence à laséquence sur laquelle l’opérateur Join est appelé.

L’opérateur Join retourne un objet. Son énumération produit, dans un premier temps,une séquence inner d’éléments de type U. Pour ce faire, la méthode innerKeySelectorest appelée sur chaque élément de la séquence inner et un tableau de référencement estcréé pour mémoriser les couples élément/valeur clé. Dans un second temps, l’objetretourné énumère la séquence outer d’éléments de type T. Pour ce faire, la méthodeouterKeySelector est appelée sur chaque élément de la séquence outer afin d’obtenirsa clé et de retrouver la séquence inner correspondante dans le tableau de référence-ment. Pour chaque élément de la paire séquence outer/séquence inner, l’objet retournéappelle enfin la méthode resultSelector, en lui passant les éléments outer et inner.Un objet instancié de type V est alors retourné par la méthode resultSelector, puisplacé dans la séquence de sortie de type V.

L’ordre des éléments de la séquence outer est préservé, ainsi que celui des élémentsinner de chaque séquence outer.

ExceptionsL’exception ArgumentNullException est levée si un des arguments a pour valeur null.

ExemplesCet exemple utilise les deux classes communes définies au début de ce chapitre :Employee et EmployeeOptionEntry. Le code du Listing 4.28 a été mis en forme un peudifféremment afin d’améliorer la lisibilité des arguments de l’opérateur Join.

Listing 4.28 : Un exemple d’appel de l’opérateur Join.

Employee[] employees = Employee.GetEmployeesArray();EmployeeOptionEntry[] empOptions = EmployeeOptionEntry.GetEmployeeOptionEntries();

var employeeOptions = employees .Join( empOptions, // séquence inner

Linq.book Page 101 Mercredi, 18. février 2009 7:58 07

Page 117: LINQ Language Integrated Query en C

102 LINQ to Objects Partie II

e => e.id, // outerKeySelector o => o.id, // innerKeySelector (e, o) => new // resultSelector { id = e.id, name = string.Format("{0} {1}", e.firstName, e.lastName), options = o.optionsCount });foreach (var item in employeeOptions) Console.WriteLine(item);

Ce code effectue une jointure sur deux tableaux de données en utilisant deux classescommunes. L’opérateur Join étant appliqué au tableau employees, ce dernier joue lerôle de la séquence externe. Quant à empOptions, il correspond à la séquence interne.

Voici les résultats de la jointure :

La méthode resultSelector crée une classe anonyme du même type que la séquencede sortie. Il est facile de voir qu’il s’agit d’une classe anonyme, car aucun nom de classen’est spécifié dans l’instruction new. Par ailleurs, le mot-clé var est utilisé, ce quiconfirme nos soupçons. Il n’est pas possible de le déclarer en tant qu’IEnumerable<>,puisque aucun type nommé ne donne les précisions nécessaires pour le déclarer commetel.

ASTUCE

Lorsque le dernier opérateur appelé retourne une séquence de type anonyme, vous devezutiliser le mot-clé var pour mémoriser la séquence dans un objet.

L’opérateur GroupJoinL’opérateur GroupJoin effectue une jointure sur deux séquences en se basant sur lesclés extraites de chacun des éléments des deux séquences.

Cet opérateur travaille d’une manière comparable à l’opérateur Join, à ceci près quel’opérateur Join ne passe qu’un seul élément de la séquence externe et un élément de laséquence interne à la méthode resultSelector. Cela signifie que, si plusieurs élémentsde la séquence interne correspondent à un élément de la séquence interne, plusieursappels à resultSelect seront nécessaires. Avec l’opérateur GroupJoin, tous leséléments de la séquence interne qui correspondent à un élément de la séquence externe

{ id = 1, name = Joe Rattz, options = 2 }{ id = 2, name = William Gates, options = 10000 }{ id = 2, name = William Gates, options = 10000 }{ id = 2, name = William Gates, options = 10000 }{ id = 3, name = Anders Hejlsberg, options = 5000 }{ id = 3, name = Anders Hejlsberg, options = 7500 }{ id = 3, name = Anders Hejlsberg, options = 7500 }{ id = 4, name = David Lightman, options = 1500 }{ id = 101, name = Kevin Flynn, options = 2 }

Linq.book Page 102 Mercredi, 18. février 2009 7:58 07

Page 118: LINQ Language Integrated Query en C

Chapitre 4 Les opérateurs différés 103

sont passés conjointement sous la forme d’une séquence à resultSelector. Un seulappel à cette méthode est donc nécessaire.

PrototypeUn seul prototype de l’opérateur GroupJoin sera étudié dans cet ouvrage :

public static IEnumerable<V> GroupJoin<T, U, K, V>( this IEnumerable<T> outer, IEnumerable<U> inner, Func<T, K> outerKeySelector, Func<U, K> innerKeySelector, Func<T, IEnumerable<U>, V> resultSelector);

Le premier élément de la méthode a pour nom outer (extérieur). Comme il s’agit d’uneméthode d’extension, on parlera de "séquence extérieure" pour faire référence à laséquence sur laquelle l’opérateur Join est appelé.

L’opérateur GroupJoin retourne un objet. Son énumération produit, dans un premiertemps, une séquence inner d’éléments de type U. Pour ce faire, la méthode innerKey-Selector est appelée sur chaque élément de la séquence inner et un tableau de référen-cement est créé pour mémoriser les couples élément/valeur clé. Dans un second temps,l’objet retourné énumère la séquence outer d’éléments de type T. Pour ce faire, laméthode outerKeySelector est appelée sur chaque élément de la séquence outer afind’obtenir sa clé et de retrouver la séquence inner correspondante dans le tableau deréférencement. Pour chaque élément de la paire séquence outer/séquence inner,l’objet retourné appelle enfin la méthode resultSelector, en lui passant l’élémentouter et la séquence des éléments inner correspondants. Un objet instancié de type Vest alors retourné par la méthode resultSelector, puis placé dans la séquence de sortiede type V.

L’ordre des éléments de la séquence outer est préservé, ainsi que celui des élémentsinner de chaque séquence outer.

ExceptionsL’exception ArgumentNullException est levée si un des arguments a pour valeur null.

ExemplesNous utiliserons les classes Employee et EmployeeOptionEntry déjà évoquées dans lasection précédente. Le code du Listing 4.29 réalise une jointure entre les employés etles options et calcule la somme des options de chacun des employés en utilisant l’opérateurGroupJoin.

Listing 4.29 : Un exemple d’utilisation de l’opérateur GroupJoin.

Employee[] employees = Employee.GetEmployeesArray();EmployeeOptionEntry[] empOptions = EmployeeOptionEntry.GetEmployeeOptionEntries();

var employeeOptions = employees .GroupJoin(

Linq.book Page 103 Mercredi, 18. février 2009 7:58 07

Page 119: LINQ Language Integrated Query en C

104 LINQ to Objects Partie II

empOptions, e => e.id, o => o.id, (e, os) => new { id = e.id, name = string.Format("{0} {1}", e.firstName, e.lastName), options = os.Sum(o => o.optionsCount) });foreach (var item in employeeOptions) Console.WriteLine(item);

Ce code est très proche du précédent. Cependant, si vous examinez le deuxième argu-ment passé à l’expression lambda (issu de la méthode resultSelector), vous verrezque l’argument o de l’exemple sur l’opérateur Join est remplacé par os. Cette diffé-rence s’explique par le fait que l’opérateur Join travaille sur un seul objet option, alorsque l’opérateur GroupJoin travaille sur une séquence d’objets option. L’opérateur Suminitialise donc le dernier membre de l’objet anonyme instancié avec la somme desobjets option. Pour l’instant, il vous suffit de savoir que cet opérateur est en mesure decalculer la somme des éléments (ou d’un membre des éléments) qui lui sont passés.Pour en savoir plus sur l’opérateur non différé Join, reportez-vous au Chapitre 5.

Voici le résultat du code précédent :

Dans ces résultats, les valeurs options correspondent à la somme de tous les champsoption de chaque employé. Ces résultats sont différents de ceux issus de l’opérateurJoin, où une ligne était créée pour chacune des options de chaque employé.

Opérateurs de regroupement

Ces opérateurs permettent de regrouper les éléments d’une séquence qui possèdent unemême clé.

Opérateur GroupByCet opérateur est utilisé pour regrouper les éléments d’une séquence d’entrée.

PrototypesTous les prototypes de l’opérateur GroupBy retournent une séquence d’éléments IGrou-ping<K, T>. L’interface IGrouping<K, T> est définie comme suit :

public interface IGrouping<K, T> : IEnumerable<T>{ K Key { get; }}

{ id = 1, name = Joe Rattz, options = 2 }{ id = 2, name = William Gates, options = 30000 }{ id = 3, name = Anders Hejlsberg, options = 20000 }{ id = 4, name = David Lightman, options = 1500 }{ id = 101, name = Kevin Flynn, options = 2 }

Linq.book Page 104 Mercredi, 18. février 2009 7:58 07

Page 120: LINQ Language Integrated Query en C

Chapitre 4 Les opérateurs différés 105

Un IGrouping est donc une séquence de type T avec une clé de type K. Quatre prototypesde GroupBy seront étudiés dans cet ouvrage.

Premier prototype

public static IEnumerable<IGrouping<K, T>> GroupBy<T, K>( this IEnumerable<T> source, Func<T, K> keySelector);

Ce prototype retourne un objet dont l’énumération passe en revue les éléments de laséquence d’entrée, appelle la méthode keySelector, mémorise chaque élément avec saclé et produit une séquence d’instances IGrouping<K, E> dans laquelle chaque élémentIGrouping<K, E> est une séquence d’éléments qui partagent la même clé. Les clés sontcomparées par l’intermédiaire du comparateur d’égalité par défaut, EqualityComparer-Default.

Pour dire les choses autrement, la valeur retournée par la méthode GroupBy est uneséquence d’objets IGrouping. Chacun d’entre eux contient une clé et une séquenced’éléments issus de la séquence d’entrée et partageant la même clé.

L’ordre des instances IGrouping est le même que celui des clés dans la séquenced’entrée. Quant à l’ordre des éléments d’une séquence IGrouping, il est identique àcelui des éléments dans la séquence d’entrée.

Deuxième prototype

public static IEnumerable<IGrouping<K, T>> GroupBy<T, K>( this IEnumerable<T> source, Func<T, K> keySelector, IEqualityComparer<K> comparer);

Ce prototype est identique au premier, à ceci près qu’il est possible de choisir le compa-rateur à utiliser.

Troisième prototype

public static IEnumerable<IGrouping<K, E>> GroupBy<T, K, E>( this IEnumerable<T> source, Func<T, K> keySelector, Func<T, E> elementSelector);

Ce prototype est identique au premier mais, ici, la méthode elementSelector est utili-sée pour choisir les éléments de la séquence d’entrée qui doivent apparaître dans laséquence de sortie.

Quatrième prototype

public static IEnumerable<IGrouping<K, E>> GroupBy<T, K, E>( this IEnumerable<T> source, Func<T, K> keySelector, Func<T, E> elementSelector, IEqualityComparer<K> comparer);

Ce prototype regroupe les possibilités offertes par les deuxième et troisième prototypes :il est donc possible de choisir un comparateur avec l’argument comparer et de limiterles éléments de la séquence de sortie avec l’argument elementSelector.

Linq.book Page 105 Mercredi, 18. février 2009 7:58 07

Page 121: LINQ Language Integrated Query en C

106 LINQ to Objects Partie II

ExceptionsL’exception ArgumentNullException est levée si un des arguments a pour valeur null.

ExemplesLe premier exemple (voir Listing 4.30) utilise la classe commune EmployeeOption-Entries. Les employés seront regroupés par id et affichés.

Listing 4.30 : Un exemple d’utilisation du premier prototype.

EmployeeOptionEntry[] empOptions = EmployeeOptionEntry.GetEmployeeOptionEntries();

IEnumerable<IGrouping<int, EmployeeOptionEntry>> outerSequence =

empOptions.GroupBy(o => o.id);

// Première énumération de la séquence extérieure de IGroupings

foreach (IGrouping<int, EmployeeOptionEntry> keyGroupSequence in outerSequence)

{

Console.WriteLine("Enregistrements Option pour l’employé " + keyGroupSequence.Key);

// Énumération des séquences IGrouping d’éléments EmployeeOptionEntry

foreach (EmployeeOptionEntry element in keyGroupSequence)

Console.WriteLine("id={0} : optionsCount={1} : dateAwarded={2:d}",

element.id, element.optionsCount, element.dateAwarded);

}

Ce code énumère la séquence outerSequence. Les éléments obtenus sont des objets quiimplémentent l’interface IGrouping. Ils contiennent une clé et une séquenced’éléments EmployeeOptionEntry qui partagent cette même clé.

Voici les résultats :

Pour illustrer le deuxième prototype de l’opérateur GroupBy, nous allons supposer quetous les employés dont le champ id est inférieur à 100 sont des membres fondateurs del’entreprise. Nous allons lister tous les enregistrements option regroupés selon l’étatfondateur/non fondateur des employés.

Enregistrements Option pour l’employé 1id=1 : optionsCount=2 : dateAwarded=12/31/1999Enregistrements Option pour l’employé 2id=2 : optionsCount=10000 : dateAwarded=6/30/1992id=2 : optionsCount=10000 : dateAwarded=1/1/1994id=2 : optionsCount=10000 : dateAwarded=4/1/2003Enregistrements Option pour l’employé 3id=3 : optionsCount=5000 : dateAwarded=9/30/1997id=3 : optionsCount=7500 : dateAwarded=9/30/1998id=3 : optionsCount=7500 : dateAwarded=9/30/1998Enregistrements Option pour l’employé 4id=4 : optionsCount=1500 : dateAwarded=12/31/1997Enregistrements Option pour l’employé 101id=101 : optionsCount=2 : dateAwarded=12/31/1998

Linq.book Page 106 Mercredi, 18. février 2009 7:58 07

Page 122: LINQ Language Integrated Query en C

Chapitre 4 Les opérateurs différés 107

Pour ce faire, nous devons définir un comparateur spécifique qui de plus doit implé-menter l’interface IEqualityComparer. Avant de parler du comparateur, jetons un œilà cette interface :

interface IEqualityComparer<T> { bool Equals(T x, T y); int GetHashCode(T x);}

Cette interface nécessite l’implémentation de deux méthodes : Equals et GetHashCode.La méthode Equals reçoit deux objets de type T. Elle retourne la valeur true si les deuxobjets sont considérés comme égaux et la valeur false dans le cas contraire. Laméthode GetHashCode reçoit un objet de type T et retourne un code (appelé hash codeou clé) de type entier pour cet objet.

Le hash code est une valeur numérique qui identifie (généralement) de manière uniqueun objet. Ordinairement calculé à partir du contenu de l’objet, il est utilisé comme unindex qui permettra de retrouver facilement une structure de données.

Voici la classe qui implémente l’interface IEqualityComparer :

public class MyFounderNumberComparer : IEqualityComparer<int>{ public bool Equals(int x, int y) { return(isFounder(x) == isFounder(y)); } public int GetHashCode(int i) { int f = 1; int nf = 100; return (isFounder(i) ? f.GetHashCode() : nf.GetHashCode()); } public bool isFounder(int id) { return(id < 100); }}

La méthode IsFounder a été ajoutée aux méthodes Equals et GetHashCode. Elle déter-mine si un employé est un fondateur en se basant sur son champ id. Ceci facilite lacompréhension du code. La méthode IsFounder est publique. Il est donc possible del’appeler en dehors de l’interface. Nous verrons cela un peu plus loin dans le code del’exemple.

Le comparateur d’égalité considère que tout entier inférieur à 100 représente unmembre fondateur. Si deux entiers font partie d’une de ces deux catégories, ils sontconsidérés comme égaux. La fonction GetHashCode retourne un entier égal à 1 sil’employé est un membre fondateur ou égal à 100 dans le cas contraire.

Listing 4.31 : Un exemple d’utilisation du deuxième prototype.

MyFounderNumberComparer comp = new MyFounderNumberComparer();

EmployeeOptionEntry[] empOptions = EmployeeOptionEntry.GetEmployeeOptionEntries();

Linq.book Page 107 Mercredi, 18. février 2009 7:58 07

Page 123: LINQ Language Integrated Query en C

108 LINQ to Objects Partie II

IEnumerable<IGrouping<int, EmployeeOptionEntry>> opts = empOptions .GroupBy(o => o.id, comp);

// Énumération de la séquence d’IGroupingforeach (IGrouping<int, EmployeeOptionEntry> keyGroup in opts){ Console.WriteLine("Options pour les " + (comp.isFounder(keyGroup.Key) ? "fondateurs" : "non fondateurs "));

// Énumération de la séquence d’éléments EmployeeOptionEntry foreach (EmployeeOptionEntry element in keyGroup) Console.WriteLine("id={0} : optionsCount={1} : dateAwarded={2:d}",element.id, element.optionsCount, element.dateAwarded);}

Dans cet exemple, le comparateur est instancié en dehors de la méthode GroupBy. Laméthode IsFounder peut ainsi être appelée dans la boucle d’affichage foreach. Voiciles résultats affichés par ce code :

Comme vous le voyez, les employés dont le champ id est inférieur à 100 sont regroupéssous le libellé "Options pour les fondateurs" et les autres sous le libellé "Options pourles non fondateurs".

Pour illustrer le troisième prototype, nous allons extraire les dates de délivrance desoptions. Le code sera très proche de celui utilisé pour illustrer le premier prototype.

Contrairement au Listing 4.30, qui retournait un regroupement d’objets EmployeeOp-tionEntry, le Listing 4.32 retourne un regroupement de dates.

Listing 4.32 : Un exemple d’utilisation du troisième prototype.

EmployeeOptionEntry[] empOptions = EmployeeOptionEntry.GetEmployeeOptionEntries();IEnumerable<IGrouping<int, DateTime>> opts = empOptions .GroupBy(o => o.id, e => e.dateAwarded);

// Énumération de la séquence de IGroupingforeach (IGrouping<int, DateTime> keyGroup in opts){ Console.WriteLine("Enregistrements Option pour l’employé " + keyGroup.Key);

// Énumération des éléments DateTime foreach (DateTime date in keyGroup) Console.WriteLine(date.ToShortDateString());}

Options pour les fondateursid=1 : optionsCount=2 : dateAwarded=12/31/1999id=2 : optionsCount=10000 : dateAwarded=6/30/1992id=2 : optionsCount=10000 : dateAwarded=1/1/1994id=3 : optionsCount=5000 : dateAwarded=9/30/1997id=2 : optionsCount=10000 : dateAwarded=4/1/2003id=3 : optionsCount=7500 : dateAwarded=9/30/1998id=3 : optionsCount=7500 : dateAwarded=9/30/1998id=4 : optionsCount=1500 : dateAwarded=12/31/1997Options pour les non fondateursid=101 : optionsCount=2 : dateAwarded=12/31/1998

Linq.book Page 108 Mercredi, 18. février 2009 7:58 07

Page 124: LINQ Language Integrated Query en C

Chapitre 4 Les opérateurs différés 109

Dans l’appel à l’opérateur GroupBy, remarquez que le deuxième argument ne retourneque la date de l’option (dateAwarded). Le IGrouping est donc de type DateTime (et nonEmployeeOptionEntry).

Voici le résultat de l’exécution :

Pour illustrer le quatrième prototype, nous allons utiliser la méthode elementSelector etun objet comparer. Cela revient à utiliser une combinaison des exemples du deuxième et dutroisième prototypes. Dans le Listing 4.33, nous regroupons les dates des options dansdeux groupes : les fondateurs (id < 100) et les non-fondateurs (id > 100).

Listing 4.33 : Un exemple d’utilisation du quatrième prototype.

MyFounderNumberComparer comp = new MyFounderNumberComparer();EmployeeOptionEntry[] empOptions = EmployeeOptionEntry.GetEmployeeOptionEntries();IEnumerable<IGrouping<int, DateTime>> opts = empOptions .GroupBy(o => o.id, o => o.dateAwarded, comp);

// Énumération de la séquence de IGroupingforeach (IGrouping<int, DateTime> keyGroup in opts){ Console.WriteLine("Enregistrements Option pour les " + (comp.isFounder(keyGroup.Key) ? "fondateurs" : "non fondateurs"));

// Énumération de la séquence des éléments EmployeeOptionEntry foreach (DateTime date in keyGroup) Console.WriteLine(date.ToShortDateString());}

La sortie console n’affiche que des dates regroupées par fondateurs et non-fondateurs :

Enregistrements Option pour l’employé 112/31/1999Enregistrements Option pour l’employé 26/30/19921/1/19944/1/2003Enregistrements Option pour l’employé 39/30/19979/30/19989/30/1998Enregistrements Option pour l’employé 412/31/1997Enregistrements Option pour l’employé 10112/31/1998

Enregistrements Option pour les fondateurs12/31/19996/30/19921/1/19949/30/19974/1/20039/30/19989/30/199812/31/1997Enregistrements Option pour les non fondateurs12/31/1998

Linq.book Page 109 Mercredi, 18. février 2009 7:58 07

Page 125: LINQ Language Integrated Query en C

110 LINQ to Objects Partie II

Opérateurs d’initialisation

Les opérateurs d’initialisation sont utilisés pour obtenir des valeurs calculées à partir deséquences.

ASTUCE

Les prototypes des opérateurs d’initialisation passés en revue dans cet ouvrage ne sont pasadaptés aux DataSets. Préférez-leur les prototypes présentés au Chapitre 10.

Opérateur DistinctL’opérateur Distinct supprime les doublons dans la séquence d’entrée.

PrototypeUn seul prototype de l’opérateur Distinct sera étudié dans cet ouvrage :

public static IEnumerable<T> Distinct<T>( this IEnumerable<T> source);

Cet opérateur retourne un objet dont l’énumération exclut les doublons de la séquenced’entrée. Le critère d’égalité entre deux éléments est déterminé avec les méthodesGetHashCode et Equals.

ExceptionsL’exception ArgumentNullException est levée si la source a pour valeur null.

ExemplesCet exemple fonctionne selon les cinq étapes suivantes :

m affichage du nombre d’éléments contenus dans le tableau presidents ;

m duplication des éléments du tableau ;

m affichage de la séquence résultante ;

m appel de l’opérateur Distinct sur la séquence concaténée ;

m affichage du nombre d’éléments en sortie de l’opérateur.

Si tout fonctionne correctement, le nombre d’éléments renvoyés par l’opérateurDistinct devrait être égal au nombre initial d’éléments du tableau presidents.

Pour obtenir le nombre d’éléments des deux séquences, nous utiliserons l’opérateur derequête standard non différé Count. Si nécessaire, reportez-vous au chapitre suivantpour avoir de plus amples informations sur cet opérateur.

Listing 4.34 : Un exemple d’utilisation de l’opérateur Distinct.

string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield",

Linq.book Page 110 Mercredi, 18. février 2009 7:58 07

Page 126: LINQ Language Integrated Query en C

Chapitre 4 Les opérateurs différés 111

"Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"};

// Affichage du nombre d’éléments du tableau presidentsConsole.WriteLine("Nombre de présidents : " + presidents.Count());

// Duplication des éléments du tableau presidentsIEnumerable<string> presidentsWithDupes = presidents.Concat(presidents);

// Affichage du nombre d’éléments du tableau presidentsConsole.WriteLine("Nombre de présidents après la duplication : " + presidentsWithDupes.Count());

// Suppression des doublons et affichage du nombre d’élémentsIEnumerable<string> presidentsDistinct = presidentsWithDupes.Distinct();Console.WriteLine("Nombre de présidents distincts : " + presidentsDistinct.Count());

Voici le résultat de ce code :

Opérateur UnionL’opérateur Union retourne la réunion de deux séquences d’entrée.

PrototypeNous étudierons un seul prototype de l’opérateur Union dans cet ouvrage :

public static IEnumerable<T> Union<T>( this IEnumerable<T> first, IEnumerable<T> second);

Ce prototype fournit un objet dont l’énumération retourne les éléments de la premièreséquence, privés de leurs doublons, suivis des éléments de la seconde séquence, égale-ment privés de leurs doublons. L’égalité des éléments est déterminée par les méthodesHashCode et Equals.

ExceptionsL’exception ArgumentNullException est levée si un des arguments a pour valeur null.

ExemplesPour montrer la différence entre les opérateurs Union et Concat (voir Listing 4.35),nous allons créer les séquences first et second à partir du tableau presidents. Cesdeux séquences auront en commun le cinquième élément du tableau presidents.Nous afficherons le nombre d’éléments du tableau presidents, des séquencespremier et second et des séquences premier et second soumises aux opérateursConcat et Union.

Nombre de présidents : 37Nombre de présidents après la duplication : 74Nombre de présidents distincts : 37

Linq.book Page 111 Mercredi, 18. février 2009 7:58 07

Page 127: LINQ Language Integrated Query en C

112 LINQ to Objects Partie II

Listing 4.35 : Un exemple d’utilisation de l’opérateur Union.

string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"};

IEnumerable<string> first = presidents.Take(5);IEnumerable<string> second = presidents.Skip(4);// Seul le cinquième élément du tableau presidents// est commun aux séquences premier et second

IEnumerable<string> concat = first.Concat<string>(second);IEnumerable<string> union = first.Union<string>(second);

Console.WriteLine("Nombre d’éléments du tableau presidents : " + ➥presidents.Count());Console.WriteLine("Nombre d’éléments de la première séquence : " + first.Count());Console.WriteLine("Nombre d’éléments de la deuxième séquence : " + second.Count());Console.WriteLine("Nombre d’éléments après concaténation des deux séquences : " + ➥concat.Count());Console.WriteLine("Nombre d’éléments après union des deux séquences : " + ➥union.Count());

Ce code affiche le texte ci-après dans la fenêtre Console :

Comme on pouvait s’y attendre :

m La séquence issue de l’opérateur Concat a un élément de plus que le tableau presi-dents.

m La séquence issue de l’opérateur Union a le même nombre d’éléments que le tableaupresidents.

Opérateur IntersectL’opérateur Intersect retourne l’intersection des deux séquences passées en entrée.

PrototypeNous étudierons un seul prototype de l’opérateur Intersect dans cet ouvrage :

public static IEnumerable<T> Intersect<T>( this IEnumerable<T> first, IEnumerable<T> second);

Nombre d’éléments du tableau presidents : 37Nombre d’éléments de la première séquence : 5Nombre d’éléments de la deuxième séquence : 33Nombre d’éléments après concaténation des deux séquences : 38Nombre d’éléments après union des deux séquences : 37

Linq.book Page 112 Mercredi, 18. février 2009 7:58 07

Page 128: LINQ Language Integrated Query en C

Chapitre 4 Les opérateurs différés 113

Cet opérateur retourne un objet dont l’énumération est obtenue :

1. en dressant la liste des singletons de la première séquence ;

2. en énumérant les éléments de la deuxième séquence et en marquant ceux qui setrouvent dans la liste de la première étape ;

3. en énumérant les éléments marqués dans l’ordre où ils ont été collectés à l’étape 2.

L’égalité des éléments est déterminée à l’aide des méthodes GetHashCode et Equals.

ExceptionsL’exception ArgumentNullException est levée si un des arguments a pour valeur null.

ExemplesNous allons utiliser les opérateurs Take et Skip pour générer deux séquences qui possè-dent un seul élément en commun. Lorsque nous appliquerons l’opérateur Intersect àces deux séquences, seul cet élément sera retourné.

Listing 4.36 : Un exemple d’utilisation de l’opérateur Intersect.

string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"};

IEnumerable<string> first = presidents.Take(5);IEnumerable<string> second = presidents.Skip(4);// Seul le cinquième élément du tableau presidents// est commun aux séquences premier et second

IEnumerable<string> intersect = first.Intersect(second);

Console.WriteLine("Nombre d’éléments dans le tableau presidents : " + ➥presidents.Count());Console.WriteLine("Nombre d’éléments dans la première séquence : " + first.Count());Console.WriteLine("Nombre d’éléments dans la deuxième séquence : " + ➥second.Count());Console.WriteLine("Nombre d’éléments après intersection des deux séquences : " + ➥intersect.Count());

// Affichage de la séquence résultant de l’opérateur Intersectforeach (string name in intersect) Console.WriteLine(name);

Voici le résultat de l’exécution de ce code :

Nombre d’éléments dans le tableau presidents : 37Nombre d’éléments dans la première séquence : 5Nombre d’éléments dans la deuxième séquence : 33Nombre d’éléments après intersection des deux séquences : 1Carter

Linq.book Page 113 Mercredi, 18. février 2009 7:58 07

Page 129: LINQ Language Integrated Query en C

114 LINQ to Objects Partie II

Opérateur ExceptCet opérateur retourne une séquence qui contient tous les éléments de la premièreséquence qui n’apparaissent pas dans la seconde.

PrototypeNous étudierons un seul prototype de l’opérateur Except dans cet ouvrage :

public static IEnumerable<T> Except<T>( this IEnumerable<T> first, IEnumerable<T> second);

Cet opérateur retourne un objet dont l’énumération effectue les actions suivantes :

1. Énumération des éléments de la première séquence en éliminant les doublons.

2. Énumération des éléments de la deuxième séquence en ne conservant que leséléments qui n’ont pas été retenus à la première étape.

3. Création d’une collection de sortie qui contient les éléments retenus à la deuxièmeétape.

L’égalité des éléments est déterminée à l’aide des méthodes GetHashCode et Equals.

ExceptionsL’exception ArgumentNullException est levée si un des arguments a pour valeur null.

ExemplesUne fois encore, nous utiliserons le tableau presidents. Supposons que vous effectuiezun traitement sur les éléments du tableau presidents et que les éléments obtenus soientplacés dans une séquence. Si de nouveaux éléments sont ajoutés à cette séquence, ilsera inutile d’appliquer le traitement aux éléments qui l’ont déjà subi. Pour ne sélec-tionner que les nouveaux éléments, il suffira de transmettre l’ancienne liste et lanouvelle liste à l’opérateur Except. Vous pourrez alors appliquer le traitement auxnouveaux venus.

Dans cet exemple, nous allons supposer que les quatre premiers éléments ont déjà subile traitement. Pour obtenir la liste des autres éléments, il suffit d’utiliser l’opérateurExcept (voir Listing 4.37).

Listing 4.37 : Un exemple d’utilisation de l’opérateur Except.

string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"};

Linq.book Page 114 Mercredi, 18. février 2009 7:58 07

Page 130: LINQ Language Integrated Query en C

Chapitre 4 Les opérateurs différés 115

// Définition de la séquence processedIEnumerable<string> processed = presidents.Take(4);

IEnumerable<string> exceptions = presidents.Except(processed);foreach (string name in exceptions) Console.WriteLine(name);

Comme on pouvait s’y attendre, les noms des présidents affichés dans la consolecommencent au cinquième :

Opérateurs de conversion

Les opérateurs de conversion représentent une façon simple et pratique de convertir desséquences en des collections d’un autre type.

Opérateur CastL’opérateur Cast convertit tous les éléments de la séquence d’entrée dans le type spécifiéet les place dans la séquence de sortie.

PrototypeNous étudierons un seul prototype de l’opérateur Cast dans cet ouvrage :

public static IEnumerable<T> Cast<T>( this IEnumerable source);

CarterClevelandClintonCoolidgeEisenhowerFillmoreFordGarfieldGrantHardingHarrisonHayesHooverJacksonJeffersonJohnsonKennedyLincolnMadisonMcKinleyMonroeNixonPiercePolkReaganRooseveltTaftTaylorTrumanTylerVan BurenWashingtonWilson

Linq.book Page 115 Mercredi, 18. février 2009 7:58 07

Page 131: LINQ Language Integrated Query en C

116 LINQ to Objects Partie II

Contrairement à la majorité des opérateurs de requête standard différés, le premierargument de l’opérateur Cast est de type IEnumerable, et non IEnumerable<T>. Cecis’explique par le fait que l’opérateur Cast a été défini pour être appelé sur des classesqui implémentent l’interface IEnumerable. En particulier les collections C# héritées,définies avant la sortie de C# 2.0 et des génériques.

Vous pouvez utiliser l’opérateur Cast sur toute collection C# héritée, à conditionqu’elle implémente l’interface IEnumerable. Une séquence IEnumerable<T> sera alorscréée. Étant donné que la plupart des opérateurs de requête standard ne travaillentqu’avec des séquences de type IEnumerable<T>, vous devrez utiliser l’opérateur Castou OfType (voir la section suivante) pour obtenir un type IEnumerable<T> compatible.Ayez bien cela en tête si vous prévoyez d’appliquer des opérateurs de requête standardsur des collections héritées.

L’opérateur Cast retourne un objet dont l’énumération transforme les éléments de laséquence d’entrée pour qu’ils soient du type T. Si un élément ne peut pas être converti,une exception est levée. Il est donc important de n’utiliser cet opérateur que lorsquel’on est sûr que tous les éléments de la séquence d’entrée peuvent être convertis.

ASTUCE

Lorsque vous appliquez une requête LINQ à une collection héritée, n’oubliez pas d’utiliserun opérateur Cast ou OfType pour convertir la collection héritée en une séquence IEnume-rable<T> compatible avec les opérateurs de requête standard.

ExceptionsL’exception ArgumentNullException est levée si l’argument source a pour valeur null.L’exception InvalidCastException est levée si un des éléments de la séquenced’entrée ne peut pas être converti dans le type T.

ExemplesDans cet exemple, nous utiliserons la méthode GetEmployeesArrayList de la classecommune Employee pour obtenir un objet ArrayList hérité (non générique). Cet objetsera alors converti en un IEnumerable<T> avec l’opérateur Cast (voir Listing 4.38).

Listing 4.38 : Ce code convertit un ArrayList en un IEnumerable<T> qui peut être utilisé avec les opérateurs de requête standard.

ArrayList employees = Employee.GetEmployeesArrayList();Console.WriteLine("Le type de l’objet employees est " + employees.GetType());

var seq = employees.Cast<Employee>();Console.WriteLine("Le type de l’objet seq est " + seq.GetType());

var emps = seq.OrderBy(e => e.lastName);foreach (Employee emp in emps) Console.WriteLine("{0} {1}", emp.firstName, emp.lastName);

Linq.book Page 116 Mercredi, 18. février 2009 7:58 07

Page 132: LINQ Language Integrated Query en C

Chapitre 4 Les opérateurs différés 117

La première ligne utilise la méthode GetEmployeesArrayList pour obtenir un Array-List d’objets Employee. Le type de l’objet ainsi obtenu est affiché par la deuxièmeligne. Dans la troisième ligne, l’objet employees est converti en une séquence IEnume-rable<T> en appelant l’opérateur Cast. Le type de l’objet obtenu est affiché dans laquatrième ligne. Les autres lignes énumèrent l’objet IEnumerable<T> afin de prouverque la conversion a réussi.

Voici le résultat de l’exécution de ce code :

Le type de l’objet employees est clairement identifiable. Il en va autrement de celui del’objet seq. Ce que nous pouvons dire, c’est qu’il est différent du précédent et qu’ilressemble à une séquence. Nous pouvons également remarquer le mot CastIteratordans l’intitulé de son type. Vous rappelez-vous de ce qui a été dit à propos des opéra-teurs différés : ces opérateurs retournent non pas une séquence en sortie, mais un objetdont l’énumération fournit les éléments de la séquence de sortie. L’objet seq est préci-sément de ce type.

ATTENTIONATTENTION

L’opérateur Cast essaye de convertir tous les éléments de la séquence d’entrée dans le typespécifié. Si un de ces éléments ne peut pas être converti, une exception InvalidCastExcep-tion est levée. Si une telle situation est possible, préférez l’opérateur OfType à l’opérateurCast.

Opérateur OfTypeCet opérateur change le type des éléments de la séquence d’entrée qui le permettent etles place dans la séquence de sortie.

PrototypeNous étudierons un seul prototype de l’opérateur OfType dans cet ouvrage :

public static IEnumerable<T> OfType<T>( this IEnumerable source);

Le premier argument de l’opérateur OfType est de type IEnumerable, et non IEnumera-ble<T>. Tout comme Cast, OfType est destiné à être appelé sur des classes qui implé-mentent l’interface IEnumerable. En particulier les collections C# héritées, définiesavant la sortie de C# 2.0 et des génériques.

Le type de l’objet employees est System.Collections.ArrayListLe type de l’objet seq est System.Linq.Enumerable+<CastIterator>d__b0`1[LINQChapter4.Employee]Kevin FlynnWilliam GatesAnders HejlsbergDavid LightmanJoe Rattz

Linq.book Page 117 Mercredi, 18. février 2009 7:58 07

Page 133: LINQ Language Integrated Query en C

118 LINQ to Objects Partie II

Vous pouvez utiliser l’opérateur OfType sur toute collection C# héritée, à conditionqu’elle implémente l’interface IEnumerable. Une séquence IEnumerable<T> sera alorscréée. Étant donné que la plupart des opérateurs de requête standard ne travaillentqu’avec des séquences de type IEnumerable<T>, vous devrez utiliser l’opérateurOfType ou Cast (voir la section précédente) pour obtenir un type IEnumerable<T>compatible. Ayez bien cela en tête si vous prévoyez d’appliquer des opérateurs derequête standard sur des collections héritées.

L’opérateur OfType retourne un objet dont l’énumération transforme les éléments de laséquence d’entrée pour qu’ils soient du type T (seuls les éléments qui supportent laconversion sont convertis).

ExceptionsL’exception ArgumentNullException est levée si l’argument source a pour valeur null.

ExemplesDans l’exemple du Listing 4.39, nous créons un ArrayList contenant des objets issusdes classes communes Employee et EmployeeOptionEntry. Appliqué à cet objet,l’opérateur Cast ne parvient pas à effectuer la conversion de type. Quelques lignes plusbas, l’opérateur OfType, appliqué à ce même objet, passe haut la main la conversion.

Listing 4.39 : Un exemple d’appel des opérateurs Cast et OfType.

ArrayList al = new ArrayList();al.Add(new Employee { id = 1, firstName = "Joe", lastName = "Rattz" });al.Add(new Employee { id = 2, firstName = "William", lastName = "Gates" });al.Add(new EmployeeOptionEntry { id = 1, optionsCount = 0 });al.Add(new EmployeeOptionEntry { id = 2, optionsCount = 99999999999 });al.Add(new Employee { id = 3, firstName = "Anders", lastName = "Hejlsberg" });al.Add(new EmployeeOptionEntry { id = 3, optionsCount = 848475745 });

var items = al.Cast<Employee>();

Console.WriteLine("Tentative d’énumération de la séquence issue de l’opérateur Cast ...");try{ foreach (Employee item in items) Console.WriteLine("{0} {1} {2}", item.id, item.firstName, item.lastName);}catch (Exception ex){ Console.WriteLine("{0}{1}", ex.Message, System.Environment.NewLine);}

Console.WriteLine("Tentative d’énumération de la séquence issue de l’opérateur ➥OfType ...");var items2 = al.OfType<Employee>();foreach (Employee item in items2) Console.WriteLine("{0} {1} {2}", item.id, item.firstName, item.lastName);

Le premier bloc d’instructions crée et remplit l’objet ArrayList al. L’opérateur Castest alors appliqué à cet objet. Le bloc d’instructions suivant tente d’énumérer leséléments de la séquence issue de l’opérateur Cast (sans ces instructions, l’erreur de

Linq.book Page 118 Mercredi, 18. février 2009 7:58 07

Page 134: LINQ Language Integrated Query en C

Chapitre 4 Les opérateurs différés 119

conversion n’aurait pas été identifiée). L’énumération est protégée par une structuretry/catch. Ainsi, un message est affiché lorsqu’une erreur de conversion est détectée.

Le code se poursuit par l’application de l’opérateur OfType sur la séquence al. Ici,aucune erreur de conversion n’étant possible, les éléments de la séquence retournée parOfType sont simplement énumérés (la structure try/catch ne devrait pas être retiréed’un code dont la portée dépasse le cadre pédagogique).

Voici les résultats de ce code :

Il n’a pas été possible d’énumérer tous les résultats de la séquence retournée parl’opérateur Cast sans qu’une exception ne soit générée. En revanche, tous les résultatsde la séquence retournée par l’opérateur OfType ont pu être énumérés, et seuls leséléments de type employee ont été inclus dans la séquence de sortie.

ASTUCE

Si vous voulez convertir une collection non générique (une collection héritée, par exemple)en une séquence IEnumerable<T>, utilisez l’opérateur OfType et non l’opérateur Cast siles données à convertir peuvent être de plusieurs types différents.

Opérateur AsEnumerableL’opérateur AsEnumerable retourne la séquence d’entrée IEnumerable<T> en tantqu’IEnumerable<T>.

PrototypeUn seul prototype de l’opérateur AsEnumerable sera étudié dans cet ouvrage :

public static IEnumerable<T> AsEnumerable<T>( this IEnumerable<T> source);

Un rapide coup d’œil à ce prototype montre qu’AsEnumerable utilise la séquenced’entrée IEnumerable<T> source et la retourne typée en IEnumerable<T>. Cela peutsembler quelque peu étrange. En effet, quel est l’intérêt de transformer un IEnumera-ble<T> en un autre IEnumerable<T> ?

Les opérateurs de requête standard sont définis pour opérer sur des séquences LINQ toObjects "normales", c’est-à-dire qui implémentent l’interface IEnumerable<T>.

Tentative d’énumération de la séquence issue de l’opérateur Cast ...1 Joe Rattz2 William GatesUnable to cast object of type ’LINQChapter4.EmployeeOptionEntry’ to type’LINQChapter4.Employee’.

Tentative d’énumération de la séquence issue de l’opérateur OfType ...1 Joe Rattz2 William Gates3 Anders Hejlsberg

Linq.book Page 119 Mercredi, 18. février 2009 7:58 07

Page 135: LINQ Language Integrated Query en C

120 LINQ to Objects Partie II

D’autres types de collections, par exemple celles qui accèdent à des bases de données,peuvent utiliser des séquences et des opérateurs qui leur sont propres. Généralement,lorsque vous appliquez un opérateur de requête sur ces types de collections, cet opéra-teur est spécifique à la collection. En utilisant l’opérateur AsEnumerable, vous allezpouvoir convertir une séquence d’entrée en une séquence IEnumerable<T> "normale",directement utilisable dans un opérateur de requête standard.

À titre d’exemple, lorsque nous nous intéresserons à LINQ to SQL un peu plus loindans ce livre, vous verrez que cette partie de LINQ utilise des séquences de type IQue-ryable<T> et implémente ses propres opérateurs. Ces derniers sont spécifiques auxséquences IQueryable<T>. Lorsque vous appelez l’opérateur Where sur une séquenceIQueryable<T>, c’est la méthode Where de LINQ to SQL qui est invoquée, et nonl’opérateur de requête standard Where de LINQ to Objects ! Si vous essayez d’invoquerun opérateur de requête standard sur un objet IQueryable, une exception sera générée,à moins qu’un opérateur LINQ to SQL de même nom n’existe. L’opérateur AsEnumerablepermet de convertir une séquence IQueryable<T> en une séquence IEnumerable<T>,permettant ainsi l’utilisation des opérateurs de requête standard. AsEnumerable serévèle très pratique si vous devez contrôler dans quelle API un opérateur doit êtreappelé.

ExceptionsAucune exception n’est générée par cet opérateur.

ExemplesPour mieux comprendre comment fonctionne cet opérateur, nous allons raisonner surun cas pratique. Nous utiliserons l’exemple LINQ to SQL donné au Chapitre 1. Voici lecode utilisé :

using System;using System.Linq;using System.Data.Linq;using nwind;

Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind");

var custs = from c in db.Customers where c.City == "Rio de Janeiro" select c;

foreach (var cust in custs) Console.WriteLine("{0}", cust.CompanyName);

Et voici les résultats de cet exemple :

Hanari CarnesQue DelíciaRicardo Adocicados

Linq.book Page 120 Mercredi, 18. février 2009 7:58 07

Page 136: LINQ Language Integrated Query en C

Chapitre 4 Les opérateurs différés 121

Pour que ce code soit en mesure de fonctionner, vous devez ajouter à votre projet :

m l’assembly System.Data.Linq.dll ;

m une directive using qui pointe sur l’espace de noms nwind ;

m les classes d’entités générées, qui seront étudiées dans les chapitres relatifs à LINQto SQL.

Supposons que vous deviez inverser l’ordre des enregistrements issus de la base dedonnées. Vous utiliserez l’opérateur Reverse, abordé un peu plus loin dans ce chapitre.Le Listing 4.40 représente le code précédent, modifié pour appeler l’opérateur Reverse.

Listing 4.40 : Appel de l’opérateur Reverse.

Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind");

var custs = (from c in db.Customers where c.City == "Rio de Janeiro" select c) .Reverse();

foreach (var cust in custs) Console.WriteLine("{0}", cust.CompanyName);

Comme vous pouvez le voir, l’unique modification a consisté à appeler la méthodeReverse. Voici les résultats renvoyés dans la console :

Que s’est-il passé ? Étant donné qu’il n’existe aucune méthode Reverse pour l’interfaceIQueryable<T>, une exception a été générée. C’est là qu’intervient la méthode AsEnu-merable. Grâce à elle, la séquence IQueryable<T> va être convertie en une séquenceIEnumerable<T>, et il sera possible de lui appliquer la méthode Reverse. Voici dans leListing 4.41 le code modifié.

Listing 4.41 : Appel de l’opérateur AsEnumerable avant l’opérateur Reverse.

Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind");

var custs = (from c in db.Customers where c.City == "Rio de Janeiro" select c) .AsEnumerable() .Reverse();

foreach (var cust in custs) Console.WriteLine("{0}", cust.CompanyName);

Exception non gérée : System.NotSupportedException : L’opérateur ’Reverse’ n’est pas ➥supporté.…

Linq.book Page 121 Mercredi, 18. février 2009 7:58 07

Page 137: LINQ Language Integrated Query en C

122 LINQ to Objects Partie II

La méthode AsEnumerable est appelée avant l’opérateur Reverse. C’est donc l’opéra-teur Reverse de LINQ to Objects qui va être invoqué. Voici les résultats affichés dans laconsole :

Ces résultats sont bien affichés dans l’ordre inverse de la séquence originale. L’opérateurReverse a donc bien fonctionné.

Opérateurs dédiés aux éléments

Ces opérateurs permettent d’extraire des éléments dans la séquence d’entrée.

Opérateur DefaultIfEmptyL’opérateur DefaultIfEmpty retourne une séquence qui contient un élément par défautsi la séquence d’entrée est vide.

PrototypesDeux prototypes de l’opérateur DefaultIfEmpty seront étudiés dans cet ouvrage.

Premier prototype

public static IEnumerable<T> DefaultIfEmpty<T>( this IEnumerable<T> source);

Ce prototype retourne un objet dont l’énumération renvoie chacun des éléments de laséquence d’entrée. Si cette dernière est vide, une séquence de type default(T) conte-nant un seul élément est retournée. Pour les références et les types nullables, la valeurpar défaut est null.

Contrairement aux autres opérateurs dédiés aux éléments, DefaultIfEmpty

retourne une séquence de type IEnumerable<T> et non de type T. Il existe d’autresopérateurs de type, mais nous ne les étudierons pas dans ce chapitre, car ils ne sontpas différés.

Le second prototype permet de spécifier la valeur par défaut.

Second prototype

public static IEnumerable<T> DefaultIfEmpty<T>(this IEnumerable<T> source,T defaultValue);

Cet opérateur est utile aux opérateurs qui génèrent des exceptions lorsque la séquenced’entrée est vide. Il permet également à l’opérateur GroupJoin de générer des jointuresexternes à gauche (left outer join).

Ricardo AdocicadosQue DelíciaHanari Carnes

Linq.book Page 122 Mercredi, 18. février 2009 7:58 07

Page 138: LINQ Language Integrated Query en C

Chapitre 4 Les opérateurs différés 123

ExceptionsUne exception ArgumentNullException est levée si l’argument source a pour valeurnull.

ExemplesDans ce premier exemple, nous allons rechercher le nom "Jones" dans le tableau presi-dents (voir Listing 4.42). Un message indiquera si ce nom a été ou n’a pas été trouvé.

Listing 4.42 : Premier exemple du prototype DefaultIfEmpty, sans l’opérateur DefaultIfEmpty.

string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"};

string jones = presidents.Where(n => n.Equals("Jones")).First();if (jones != null) Console.WriteLine("Jones was found");else Console.WriteLine("Jones was not found");

Voici les résultats affichés dans la console :

Le nom "Jones" n’ayant pas été trouvé, une séquence vide est passée à l’opérateurFirst. Ce dernier n’appréciant pas les séquences vides, il a généré une exception.

Nous allons maintenant ajouter un appel à l’opérateur DefaultIfEmpty entre les opéra-teurs Where et First. Ainsi, c’est non pas une séquence vide, mais une séquence contenantun élément null qui sera passée à l’opérateur First (voir Listing 4.43).

Listing 4.43 : Second exemple du premier prototype, cette fois en utilisant DefaultIfEmpty.

string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"};

string jones = presidents.Where(n => n.Equals("Jones")).DefaultIfEmpty().First();if (jones != null) Console.WriteLine("Jones was found.");else Console.WriteLine("Jones was not found.");

Exception non gérée : System.InvalidOperationException : La séquence ne contient ➥aucun élément…

Linq.book Page 123 Mercredi, 18. février 2009 7:58 07

Page 139: LINQ Language Integrated Query en C

124 LINQ to Objects Partie II

Voici le résultat :

Voici maintenant un exemple pour le second prototype (voir Listing 4.44). Ici, nouspouvons choisir la valeur retournée lorsque la séquence d’entrée est vide.

Listing 4.44 : Un exemple du second prototype de l’opérateur DefaultIfEmpty.

string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"};

string name = presidents.Where(n => n.Equals("Jones")).DefaultIfEmpty("Missing").First();Console.WriteLine(name);

Voici le résultat :

Nous allons maintenant réaliser une jointure externe à gauche en utilisant les opérateursGroupJoin et DefaultIfEmpty. Nous travaillerons avec deux classes communes,Employee et EmployeeOptionEntry. Dans le Listing 4.45, l’opérateur DefaultIfEmptyn’est pas utilisé.

Listing 4.45 : Un exemple sans l’opérateur DefaultIfEmpty.

ArrayList employeesAL = Employee.GetEmployeesArrayList();// Ajout d’un nouvel employé sans enregistrement EmployeeOptionEntry correspondantemployeesAL.Add(new Employee { id = 102, firstName = "Michael", lastName = "Bolton" });Employee[] employees = employeesAL.Cast<Employee>().ToArray();EmployeeOptionEntry[] empOptions = EmployeeOptionEntry.GetEmployeeOptionEntries();

var employeeOptions = employees .GroupJoin( empOptions, e => e.id, o => o.id, (e, os) => os .Select(o => new { id = e.id, name = string.Format("{0} {1}", e.firstName, e.lastName), options = o != null ? o.optionsCount : 0 })) .SelectMany(r => r);

foreach (var item in employeeOptions) Console.WriteLine(item);

Jones n’a pas été trouvé.

Absent

Linq.book Page 124 Mercredi, 18. février 2009 7:58 07

Page 140: LINQ Language Integrated Query en C

Chapitre 4 Les opérateurs différés 125

Quelques précisions à propos de cet exemple :

m Le code est très proche de celui qui a été utilisé pour illustrer l’opérateur Group-Join.

m Étant donné que chaque employé de la classe commune employee a une correspon-dance dans la classe commune EmployeeOptionEntry, nous allons ajouter unnouvel employé, Michael Bolton, à l’objet ArrayList des employés, de telle sortequ’aucun objet EmployeeOptionEntry ne lui corresponde.

m L’opérateur DefaultIfEmpty ne sera pas appelé dans cet exemple.

Voici les résultats de la requête :

Comme aucun objet ne correspond à l’employé Michael Bolton dans le tableauEmployeeOptionArray, aucune information concernant cet employé n’est affichée dansla console. Nous allons maintenant utiliser l’opérateur DefaultIfEmpty pour créer unenregistrement par défaut pour cet employé (voir Listing 4.46).

Listing 4.46 : Un exemple d’utilisation de l’opérateur DefaultIfEmpty.

ArrayList employeesAL = Employee.GetEmployeesArrayList();// Ajout d’un nouvel employé sans enregistrement EmployeeOptionEntry correspondantemployeesAL.Add(new Employee { id = 102, firstName = "Michael", lastName = "Bolton" });Employee[] employees = employeesAL.Cast<Employee>().ToArray();EmployeeOptionEntry[] empOptions = EmployeeOptionEntry.GetEmployeeOptionEntries();

var employeeOptions = employees .GroupJoin( empOptions, e => e.id, o => o.id, (e, os) => os .DefaultIfEmpty() .Select(o => new { id = e.id,

{ id = 1, name = Joe Rattz, options = 2 }{ id = 2, name = William Gates, options = 10000 }{ id = 2, name = William Gates, options = 10000 }{ id = 2, name = William Gates, options = 10000 }{ id = 3, name = Anders Hejlsberg, options = 5000 }{ id = 3, name = Anders Hejlsberg, options = 7500 }{ id = 3, name = Anders Hejlsberg, options = 7500 }{ id = 4, name = David Lightman, options = 1500 }{ id = 101, name = Kevin Flynn, options = 2 }

Linq.book Page 125 Mercredi, 18. février 2009 7:58 07

Page 141: LINQ Language Integrated Query en C

126 LINQ to Objects Partie II

name = string.Format("{0} {1}", e.firstName, e.lastName),

options = o != null ? o.optionsCount : 0

}))

.SelectMany(r => r);

foreach (var item in employeeOptions) Console.WriteLine(item);

Le premier bloc de code ajoute l’employé Michael Bolton sans lui associer un objetEmployeeOptionEntry. Le deuxième bloc de code effectue une requête sur les donnéesen faisant appel à l’opérateur DefaultIfEmpty. Voici les résultats :

L’opérateur DefaultIfEmpty a bien ajouté un objet EmployeeOptionEntry pourl’employé Michael Bolton.

Opérateurs de génération

Ces opérateurs sont utilisés pour générer des séquences.

Opérateur RangeL’opérateur Range génère une séquence d’entiers.

PrototypeUn seul prototype de l’opérateur Range sera étudié dans cet ouvrage :

public static IEnumerable<int> Range( int start, int count);

Ce prototype génère une séquence de count entiers à partir de start.

L’opérateur Range n’est pas une méthode d’extension. Il n’étend pas le type IEnume-rable<T>.

INFO

L’opérateur Range n’est pas une méthode d’extension. C’est une méthode statique appeléedans l’assembly System.Linq.Enumerable.

{ id = 1, name = Joe Rattz, options = 2 }{ id = 2, name = William Gates, options = 10000 }{ id = 2, name = William Gates, options = 10000 }{ id = 2, name = William Gates, options = 10000 }{ id = 3, name = Anders Hejlsberg, options = 5000 }{ id = 3, name = Anders Hejlsberg, options = 7500 }{ id = 3, name = Anders Hejlsberg, options = 7500 }{ id = 4, name = David Lightman, options = 1500 }{ id = 101, name = Kevin Flynn, options = 2 }{ id = 102, name = Michael Bolton, options = 0 }

Linq.book Page 126 Mercredi, 18. février 2009 7:58 07

Page 142: LINQ Language Integrated Query en C

Chapitre 4 Les opérateurs différés 127

ExceptionsUne exception ArgumentOutOfRangeException est levée si count est inférieur à zéro ousi start+count-1 est supérieur à int.MaxValue.

Exemples

Listing 4.47 : Un exemple d’appel de l’opérateur Range.

IEnumerable<int> ints = Enumerable.Range(1, 10); foreach(int i in ints) Console.WriteLine(i);

Je tiens à rappeler que l’opérateur Range n’est pas appliqué à une séquence : il s’agitd’une méthode statique de la classe System.Linq.Enumerable. Voici les résultatsaffichés dans la console :

Opérateur RepeatL’opérateur Repeat génère une séquence en répétant plusieurs fois un même élément.

PrototypeUn seul prototype de l’opérateur Repeat sera étudié dans cet ouvrage :

public static IEnumerable<T> Repeat<T>( T element, int count);

Ce prototype retourne un objet dont l’énumération produit count éléments T.

L’opérateur Repeat n’est pas une méthode d’extension. Il n’étend pas le type IEnume-rable<T>.

INFO

L’opérateur Repeat n’est pas une méthode d’extension. C’est une méthode statique appeléedans l’assembly System.Linq.Enumerable.

ExceptionsUne exception ArgumentOutOfRangeException est levée si count est inférieur à zéro.

12345678910

Linq.book Page 127 Mercredi, 18. février 2009 7:58 07

Page 143: LINQ Language Integrated Query en C

128 LINQ to Objects Partie II

Exemples

Listing 4.48 : Génération d’une séquence de dix éléments Integer initialisés à la valeur 2.

IEnumerable<int> ints = Enumerable.Repeat(2, 10); foreach(int i in ints) Console.WriteLine(i);

Voici les résultats affichés dans la console :

Opérateur EmptyL’opérateur Empty génère une séquence vide de type T.

PrototypeUn seul prototype de l’opérateur Empty sera étudié dans cet ouvrage :

public static IEnumerable<T> Empty<T>();

Ce prototype renvoie un objet dont l’énumération produit 0 élément de type T.

L’opérateur Empty n’est pas une méthode d’extension. Il n’étend pas le type IEnumera-ble<T>.

INFO

L’opérateur Empty n’est pas une méthode d’extension. C’est une méthode statique appeléedans l’assembly System.Linq.Enumerable.

ExceptionsAucune.

ExemplesCet exemple génère une séquence de type String par l’intermédiaire de l’opérateurEmpty. La séquence générée ainsi que son nombre d’éléments sont ensuite affichés dansla console.

Listing 4.49 : Génération d’une séquence vide de String.

IEnumerable<string> strings = Enumerable.Empty<string>();foreach(string s in strings)

2222222222

Linq.book Page 128 Mercredi, 18. février 2009 7:58 07

Page 144: LINQ Language Integrated Query en C

Chapitre 4 Les opérateurs différés 129

Console.WriteLine(s);Console.WriteLine(strings.Count());

Voici le résultat affiché dans la console :

Comme vous le voyez, la boucle foreach ne produit aucun résultat. Ceci est normal,puisqu’il n’y a aucun élément à afficher.

Résumé

Ce chapitre a illustré la plupart des prototypes des opérateurs différés, du plus simple auplus complexe.

En isolant les opérateurs de requête standard différés de leurs acolytes non différés, j’aimis l’accent sur l’impact que pouvait avoir l’exécution non instantanée d’une requête.

Au chapitre suivant, vous découvrirez les opérateurs de requête standard non différés.Ce sera le dernier chapitre dédié à LINQ to Objects.

0

Linq.book Page 129 Mercredi, 18. février 2009 7:58 07

Page 145: LINQ Language Integrated Query en C

Linq.book Page 130 Mercredi, 18. février 2009 7:58 07

Page 146: LINQ Language Integrated Query en C

5

Les opérateurs non différés

Au chapitre précédent, nous nous sommes intéressés aux opérateurs de requête différés.Ces opérateurs sont faciles à identifier, car ils retournent un IEnumerable<T> ou unOrderedSequence<T>. Nous allons maintenant nous intéresser aux opérateurs derequête standard non différés. Ces opérateurs sont faciles à reconnaître, car le résultatretourné a un type différent de IEnumerable<T> et OrderedSequence<T>.

Pour pouvoir exécuter les exemples de ce chapitre, assurez-vous que vous avez réfé-rencé les espaces de noms (directive using), les assemblies et les codes communsnécessaires.

Espaces de noms référencés

Les exemples de ce chapitre vont utiliser les espaces de noms System.Linq,System.Collections et System.Collections.Generic. Si elles ne sont pas déjàprésentes, vous devez donc ajouter les directives using suivantes dans votre code :

using System.Linq;using System.Collections;using System.Collections.Generic;

Si vous parcourez le code source mis à disposition sur le site www.pearson.fr, vousverrez que j’ai également ajouté une directive using sur l’espace de nomsSystem.Diagnostic. Cette directive n’est pas nécessaire si vous saisissez directementles exemples de ce chapitre. Elle n’est là que pour les besoins propres du code source.

Classes communes

Pour fonctionner entièrement, certains exemples de ce chapitre nécessitent des classesadditionnelles. Cette section décrit les quatre classes qui seront utilisées par certainsexemples de ce chapitre.

Linq.book Page 131 Mercredi, 18. février 2009 7:58 07

Page 147: LINQ Language Integrated Query en C

132 LINQ to Objects Partie II

La classe Employee permet de travailler sur les employés d’une entreprise. Elle contientdes méthodes statiques qui retournent un tableau d’employés de type ArrayList.

public class Employee{ public int id; public string firstName; public string lastName;

public static ArrayList GetEmployeesArrayList() { ArrayList al = new ArrayList();

al.Add(new Employee { id = 1, firstName = "Joe", lastName = "Rattz" }); al.Add(new Employee { id = 2, firstName = "William", lastName = "Gates" }); al.Add(new Employee { id = 3, firstName = "Anders", lastName = "Hejlsberg" }); al.Add(new Employee { id = 4, firstName = "David", lastName = "Lightman" }); al.Add(new Employee { id = 101, firstName = "Kevin", lastName = "Flynn" }); return (al); }

public static Employee[] GetEmployeesArray() { return ((Employee[])GetEmployeesArrayList().ToArray()); }}

La classe EmployeeOptionEntry représente le montant des stock-options des employés.Elle contient une méthode statique qui retourne un tableau de stock-options.

public class EmployeeOptionEntry{ public int id; public long optionsCount; public DateTime dateAwarded;

public static EmployeeOptionEntry[] GetEmployeeOptionEntries() { EmployeeOptionEntry[] empOptions = new EmployeeOptionEntry[] { new EmployeeOptionEntry { id = 1, optionsCount = 2, dateAwarded = DateTime.Parse("1999/12/31") }, new EmployeeOptionEntry { id = 2, optionsCount = 10000, dateAwarded = DateTime.Parse("1992/06/30") }, new EmployeeOptionEntry { id = 2, optionsCount = 10000, dateAwarded = DateTime.Parse("1994/01/01") }, new EmployeeOptionEntry { id = 3, optionsCount = 5000, dateAwarded = DateTime.Parse("1997/09/30") }, new EmployeeOptionEntry { id = 2, optionsCount = 10000, dateAwarded = DateTime.Parse("2003/04/01") }, new EmployeeOptionEntry { id = 3, optionsCount = 7500, dateAwarded = DateTime.Parse("1998/09/30") },

Linq.book Page 132 Mercredi, 18. février 2009 7:58 07

Page 148: LINQ Language Integrated Query en C

Chapitre 5 Les opérateurs non différés 133

new EmployeeOptionEntry { id = 3, optionsCount = 7500, dateAwarded = DateTime.Parse("1998/09/30") }, new EmployeeOptionEntry { id = 4, optionsCount = 1500, dateAwarded = DateTime.Parse("1997/12/31") }, new EmployeeOptionEntry { id = 101, optionsCount = 2, dateAwarded = DateTime.Parse("1998/12/31") } };

return (empOptions); }}

Plusieurs opérateurs utilisent des classes qui implémentent l’interface IEquality-Comparer<T>. Ceci afin de tester l’égalité entre deux éléments. Cette interface est utilelorsque le terme "égalité" doit être pris au sens large. Par exemple, deux chaînespeuvent être considérées égales, même si leur casse diffère.

L’interface IEqualityComparer<T> ayant été abordée en détail au chapitre précédent,nous n’y reviendrons pas.

Dans les exemples de ce chapitre, nous aurons besoin d’une classe permettant decomparer plusieurs nombres stockés dans des chaînes de caractères. Ainsi, par exemple,les chaînes "17" et "00017" seront considérées comme égales. La classe MyStringi-fieldNumberComparer se chargera de ce type de comparaison.

public class MyStringifiedNumberComparer : IEqualityComparer<string>{ public bool Equals(string x, string y) { return(Int32.Parse(x) == Int32.Parse(y)); }

public int GetHashCode(string obj) { return Int32.Parse(obj).ToString().GetHashCode(); }}

Cette implémentation de l’interface IEqualityComparer ne fonctionne que sur desvariables de type string. La technique utilisée consiste à convertir les valeurs stringen int32. Ainsi, par exemple, la valeur "002" sera convertie en un entier de valeur 2, etles éventuels zéros en tête de la chaîne n’affecteront pas la conversion.

Dans plusieurs exemples de ce chapitre, nous aurons besoin d’une classe dans laquellele champ clé des enregistrements n’est pas forcément unique. La classe Actor a étécréée dans ce but (le champ birthYear sera utilisé comme clé).

public class Actor{ public int birthYear; public string firstName; public string lastName;

Linq.book Page 133 Mercredi, 18. février 2009 7:58 07

Page 149: LINQ Language Integrated Query en C

134 LINQ to Objects Partie II

public static Actor[] GetActors() { Actor[] actors = new Actor[] { new Actor { birthYear = 1964, firstName = "Keanu", lastName = "Reeves" }, new Actor { birthYear = 1968, firstName = "Owen", lastName = "Wilson" }, new Actor { birthYear = 1960, firstName = "James", lastName = "Spader" }, new Actor { birthYear = 1964, firstName = "Sandra", lastName = "Bullock" }, };

return (actors); }}

Les opérateurs non différés, par groupes fonctionnels

Dans cette section, nous avons organisé les différents opérateurs de requête standardnon différés par grands groupes fonctionnels.

Opérateurs de conversion

Les opérateurs de conversion sont utilisés pour convertir des séquences dans des collectionsd’un autre type.

L’opérateur ToArrayL’opérateur ToArray crée un tableau de type T à partir d’une séquence d’entrée de typeT.

PrototypeUn seul prototype de l’opérateur ToArray sera étudié dans ce livre :

public static T[] ToArray<T>( this IEnumerable<T> source);

Ce prototype admet un seul paramètre : une séquence source d’éléments de type T.Il renvoie un tableau d’éléments de type T.

ExceptionsL’exception ArgumentNullExpression est levée si l’argument a pour valeur null.

ExemplesNous allons créer une séquence IEnumerable<T> en appliquant l’opérateur OfType à untableau. Une fois la séquence obtenue, nous la passerons à l’opérateur ToArray pourplacer les différents éléments dans un tableau (voir Listing 5.1).

Listing 5.1 : Un exemple d’appel à l’opérateur ToArray.

string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson",

Linq.book Page 134 Mercredi, 18. février 2009 7:58 07

Page 150: LINQ Language Integrated Query en C

Chapitre 5 Les opérateurs non différés 135

"Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"};

string[] names = presidents.OfType<string>().ToArray();

foreach (string name in names) Console.WriteLine(name);

Dans un premier temps, le tableau presidents est converti en une séquence IEnumera-ble<string> avec l’opérateur OfType. Dans un second temps, cette séquence estconvertie en un tableau en utilisant l’opérateur ToArray. Le tableau est immédiatementinitialisé, car ToArray est un opérateur non différé.

Voici le résultat affiché dans la console :

Vous aurez certainement remarqué que ce code est redondant. En effet, le tableaupresidents est déjà une séquence, puisque dans C# 3.0 les tableaux implémententl’interface IEnumerable<T>. L’appel à l’opérateur ToArray aurait donc pu être évité.

AdamsArthurBuchananBushCarterClevelandClintonCoolidgeEisenhowerFillmoreFordGarfieldGrantHardingHarrisonHayesHooverJacksonJeffersonJohnsonKennedyLincolnMadisonMcKinleyMonroeNixonPiercePolkReaganRooseveltTaftTaylorTrumanTylerVan BurenWashingtonWilson

Linq.book Page 135 Mercredi, 18. février 2009 7:58 07

Page 151: LINQ Language Integrated Query en C

136 LINQ to Objects Partie II

Mais alors qu’auriez-vous pensé de ce code qui se serait contenté de convertir untableau en… un tableau ?

L’opérateur ToArray a deux avantages : il permet de mémoriser une séquence jusqu’àson énumération et de s’assurer que plusieurs énumérations du tableau travailleront surles mêmes données.

Opérateur ToListL’opérateur ToList crée une liste d’éléments de type T à partir d’une séquence d’entréede type T.

PrototypeUn seul prototype de l’opérateur ToList sera étudié dans ce livre :

public static List<T> ToList<T>( this IEnumerable<T> source);

Cet opérateur admet un argument : une séquence d’entrée source de type T. Il renvoieune liste d’éléments de type T.

ExceptionsL’exception ArgumentNullExpression est levée si l’argument source a pour valeurnull.

Exemples

Listing 5.2 : Un appel à l’opérateur ToList.

string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"};

List<string> names = presidents.ToList();

foreach (string name in names) Console.WriteLine(name);

Ce code utilise les mêmes données que l’exemple précédent. Mais, ici, l’opérateurOfType n’est pas appelé pour créer une séquence intermédiaire de type IEnumera-ble<T> : le tableau presidents est directement converti en une liste de typeList<string>.

Voici les résultats affichés dans la console :

AdamsArthur

Linq.book Page 136 Mercredi, 18. février 2009 7:58 07

Page 152: LINQ Language Integrated Query en C

Chapitre 5 Les opérateurs non différés 137

Tout comme ToArray, ToList a deux avantages : il permet de mémoriser une séquencejusqu’à son énumération et de s’assurer que plusieurs énumérations travailleront sur lesmêmes données.

Opérateur ToDictionaryCet opérateur admet au minimum deux paramètres en entrée : une séquence d’entrée detype T et une clé de type K. Il crée un dictionnaire de type <K, T>.

Si l’argument facultatif elementSelector est spécifié dans le prototype, le dictionnairecréé est de type <K, E>. Les valeurs stockées sont de type E, différent du type d’entréeT.

INFO

Si la classe C# Dictionary ne vous est pas familière, sachez qu’elle permet de mémoriser descouples élément/clé (où clé est unique pour chaque élément). Pour retrouver un élémentdans la liste, il suffit d’indexer le tableau en utilisant la clé.

BuchananBushCarterClevelandClintonCoolidgeEisenhowerFillmoreFordGarfieldGrantHardingHarrisonHayesHooverJacksonJeffersonJohnsonKennedyLincolnMadisonMcKinleyMonroeNixonPiercePolkReaganRooseveltTaftTaylorTrumanTylerVan BurenWashingtonWilson

Linq.book Page 137 Mercredi, 18. février 2009 7:58 07

Page 153: LINQ Language Integrated Query en C

138 LINQ to Objects Partie II

PrototypesQuatre prototypes de l’opérateur ToDictionary seront étudiés dans ce livre.

Premier prototype

public static Dictionary<K, T> ToDictionary<T, K>( this IEnumerable<T> source, Func<T, K> keySelector);

Ce prototype crée un dictionnaire de type <K, T> en énumérant la séquence d’entréesource. Le délégué keySelector est appelé pour obtenir une valeur clé pour chaqueélément (c’est cette valeur qui sera inscrite dans le dictionnaire). Les éléments stockésdans le dictionnaire sont de même type que ceux de la séquence d’entrée.

Aucun comparateur n’étant spécifié dans le prototype, c’est le comparateur par défaut,EqualityComparer<K>.Default, qui sera utilisé.

Le deuxième prototype est semblable au premier, mais il permet de spécifier le compa-rateur à utiliser.

Deuxième prototype

public static Dictionary<K, T> ToDictionary<T, K>( this IEnumerable<T> source, Func<T, K> keySelector, IEqualityComparer<K> comparer);

Vous utiliserez ce prototype si le comparateur par défaut, EqualityComparer<K>.Default,ne convient pas. Dans ce cas, le comparateur IEqualityComparer sera utilisé pour toutajout ou lecture d’élément dans le dictionnaire. La classe StringComparer implémenteplusieurs classes de comparaison. L’une d’entre elles, par exemple, ignore la casse deschaînes comparées. Ainsi, les clés "Joe" et "joe" seront considérées comme égales.

Le troisième prototype est semblable au premier, mais il ajoute un sélectionneurd’élément. Par son intermédiaire, les valeurs stockées dans le dictionnaire peuvent êtred’un autre type que celles de la séquence d’entrée.

Troisième prototype

public static Dictionary<K, E> ToDictionary<T, K, E>( this IEnumerable<T> source, Func<T, K> keySelector, Func<T, E> elementSelector);

L’argument elementSelector fait référence à un délégué qui retourne un fragment del’élément soumis, ou un objet d’un tout autre type. C’est cet élément qui sera stockédans le dictionnaire.

Le quatrième prototype cumule les avantages des deux précédents. Par son intermé-diaire, vous pouvez spécifier un elementSelector et un comparateur.

Quatrième prototype

public static Dictionary<K, E> ToDictionary<T, K, E>(this IEnumerable<T> source,Func<T, K> keySelector,

Linq.book Page 138 Mercredi, 18. février 2009 7:58 07

Page 154: LINQ Language Integrated Query en C

Chapitre 5 Les opérateurs non différés 139

Func<T, E> elementSelector,IEqualityComparer<K> comparer);

ExceptionsL’exception ArgumentNullExpression est levée si l’argument source, keySelector ouelementSelector, a pour valeur null ou si la clé retournée par keySelector a pourvaleur null.

L’exception ArgumentException est levée si un sélecteur retourne la même clé pourdeux éléments.

ExemplesDans cet exemple, nous utiliserons la classe commune Employee. Nous allons créer undictionnaire de type Dictionary<int, Employee>. La clé int représentera l’identifiantid de l’employé et l’objet Employee, l’élément stocké dans le dictionnaire.

Listing 5.3 : Un exemple d’utilisation du premier prototype de l’opérateur ToDictionary.

Dictionary<int, Employee> eDictionary = Employee.GetEmployeesArray().ToDictionary(k => k.id);

Employee e = eDictionary[2];Console.WriteLine("Employé dont le champ id vaut 2 : {0} {1}", e.firstName, ➥e.lastName);

Le champ id est utilisé comme clé. Le premier argument de Dictionary est donc detype int. Ce prototype étant limité à l’enregistrement intégral des données qui lui sontpassées, le deuxième argument est de type Employee. En fournissant l’identifiant d’unemployé, le prototype Dictionary<int, Employee> donne donc accès aux donnéescorrespondantes. Voici le résultat affiché dans la console :

Pour illustrer le deuxième prototype, nous avons besoin d’une situation dans laquellel’utilisation d’un comparateur personnalisé se justifie. Supposons que la clé soit unevaleur numérique stockée dans une chaîne. Les valeurs "1", "01", "001", etc. ne sont pasidentiques, même si elles représentent le même nombre. Nous devons donc utiliser uncomparateur qui autorise ce type de "largesse d’écriture".

Nous allons légèrement modifier la classe commune Employee pour qu’elle admetteune clé de type string. Cette modification va donner naissance à la classe Employee2.

La classe utilisée par le deuxième prototype de l’opérateur ToDictionary

public class Employee2{ public string id; public string firstName; public string lastName;

public static ArrayList GetEmployeesArrayList()

Employé dont le champ id vaut 2 : William Gates

Linq.book Page 139 Mercredi, 18. février 2009 7:58 07

Page 155: LINQ Language Integrated Query en C

140 LINQ to Objects Partie II

{ ArrayList al = new ArrayList();

al.Add(new Employee2 { id = "1", firstName = "Joe", lastName = "Rattz" }); al.Add(new Employee2 { id = "2", firstName = "William", lastName = "Gates" }); al.Add(new Employee2 { id = "3", firstName = "Anders", lastName = "Hejlsberg" }); al.Add(new Employee2 { id = "4", firstName = "David", lastName = "Lightman" }); al.Add(new Employee2 { id = "101", firstName = "Kevin", lastName = "Flynn" }); return (al); }

public static Employee2[] GetEmployeesArray() { return ((Employee2[])GetEmployeesArrayList().ToArray(typeof(Employee2))); }}

Le type de la clé a été modifié dans un but purement démonstratif, afin d’étayer le fonc-tionnement du comparateur MyStringifieldNumberComparer. Ce dernier considéreracomme égales deux clés qui, littéralement, ne le sont pas.

Voyons maintenant comment utiliser la classe Employee2 (voir Listing 5.4).

Listing 5.4 : Un exemple d’utilisation du deuxième prototype de l’opérateur ToDictionary.

Dictionary<string, Employee2> eDictionary = Employee2.GetEmployeesArray() .ToDictionary(k => k.id, new MyStringifiedNumberComparer());

Employee2 e = eDictionary["2"];Console.WriteLine("Employé dont le champ id vaut \"2\" : {0} {1}", e.firstName, e.lastName);

e = eDictionary["000002"];Console.WriteLine("Employé dont le champ id vaut \"000002\" : {0} {1}", e.firstName, e.lastName);

Dans cet exemple, nous tentons d’accéder à l’élément du dictionnaire dont la clé a pourvaleur "2", puis "000002". Si la classe de comparaison fonctionne, ces deux clésdevraient pointer vers le même employé. Voici les résultats :

Les deux clés ayant une même valeur numérique, elles renvoient vers la même entréedans le dictionnaire.

Le troisième prototype permet de stocker dans le dictionnaire un élément d’un autretype que celui de la séquence d’entrée. Pour illustrer son fonctionnement, nous allonstravailler avec la classe Employee (voir Listing 5.5).

Listing 5.5 : Un exemple d’utilisation du troisième prototype de l’opérateur ToDictionary.

Dictionary<int, string> eDictionary = Employee.GetEmployeesArray() .ToDictionary(k => k.id,

Employé dont le champ id vaut "2" : William GatesEmployé dont le champ id vaut "000002" : William Gates

Linq.book Page 140 Mercredi, 18. février 2009 7:58 07

Page 156: LINQ Language Integrated Query en C

Chapitre 5 Les opérateurs non différés 141

i => string.Format("{0} {1}", // elementSelector i.firstName, i.lastName));

string name = eDictionary[2];Console.WriteLine("Employé dont le champ id vaut 2 : {0}", name);

Dans cet exemple, une expression lambda concatène les champs firstName et last-Name et les stocke dans une chaîne. La séquence d’entrée est de type Employee, maisc’est un type string qui est stocké dans le dictionnaire. Voici le résultat :

Pour illustrer le quatrième prototype, nous allons utiliser la classe Employee2 et laclasse commune MyStringfieldNumberComparer (voir Listing 5.6).

Listing 5.6 : Un exemple d’utilisation du quatrième prototype de l’opérateur ToDictionary.

Dictionary<string, string> eDictionary = Employee2.GetEmployeesArray() .ToDictionary(k => k.id, // sélection de la clé i => string.Format("{0} {1}", // sélection de l’élément i.firstName, i.lastName), new MyStringifiedNumberComparer()); // comparateur

string name = eDictionary["2"];Console.WriteLine("Employé dont le champ id vaut \"2\" : {0}", name);

name = eDictionary["000002"];Console.WriteLine("Employé dont le champ id vaut \"000002\" : {0}", name);

Dans ce code :

m le sélecteur elementSelector stocke des valeurs chaînes dans le dictionnaire ;

m le comparateur MyStringifiedNumberComparer est utilisé pour trouver un élémentdans le dictionnaire.

Les deux derniers blocs recherchent l’employé dont l’identifiant vaut "2", puis"000002". Les chaînes renvoyées sont identiques puisque le comparateur considère cesdeux chaînes comme égales. Voici le résultat :

Opérateur ToLookupCet opérateur admet au minimum deux paramètres en entrée : une séquence d’entrée detype T et une clé de type K. Il crée un objet Lookup de type <K, T>.

Si l’argument facultatif elementSelector est spécifié dans le prototype, l’objet Lookupcréé est de type <K, E>. Les valeurs stockées sont de type E, différent du type d’entrée T.

Tous les prototypes de l’opérateur ToLookup créent un objet Lookup qui implémentel’interface ILookup. Nous leur ferons souvent référence en utilisant le simple mot"Lookup".

Employé dont le champ id vaut 2 : William Gates

Employé dont le champ id vaut 2 : William GatesEmployé dont le champ id vaut 000002 : William Gates

Linq.book Page 141 Mercredi, 18. février 2009 7:58 07

Page 157: LINQ Language Integrated Query en C

142 LINQ to Objects Partie II

INFO

Si la classe C# Lookup ne vous est pas familière, sachez qu’elle permet de mémoriser descouples élément/clé (où clé n’est pas forcément unique pour chaque élément). Pour retrou-ver le ou les éléments qui correspondent à une clé, il suffit d’indexer le tableau en utilisantcette clé.

PrototypesQuatre prototypes de l’opérateur ToLookup seront étudiés dans ce livre.

Premier prototype

public static ILookup<K, T> ToLookup<T, K>( this IEnumerable<T> source, Func<T, K> keySelector);

Ce prototype crée un Lookup de type <K, T> en énumérant la séquence d’entrée,source. Le délégué keySelector est appelé pour extraire la valeur clé de chaqueélément (c’est cette valeur qui sera inscrite dans le Lookup). Les éléments stockés dansle Lookup sont de même type que ceux de la séquence d’entrée.

Aucun comparateur n’étant spécifié dans le prototype, c’est le comparateur par défaut,EqualityComparer<K>.Default, qui sera utilisé.

Le deuxième prototype est semblable au premier, mais il permet de spécifier le compa-rateur à utiliser.

Deuxième prototype

public static ILookup<K, T> ToLookup<T, K>( this IEnumerable<T> source, Func<T, K> keySelector, IEqualityComparer<K> comparer);

Vous utiliserez ce prototype si le comparateur par défaut, EqualityCompa-

rer<K>.Default, ne convient pas. Dans ce cas, le comparateur IEqualityComparersera utilisé pour tout ajout ou lecture d’élément dans le Lookup. La classe StringCom-parer implémente plusieurs classes de comparaison. L’une d’entre elles, par exemple,ignore la casse des chaînes comparées. Ainsi, les clés "Joe" et "joe" seront considéréescomme égales.

Le troisième prototype est semblable au premier, mais il ajoute un sélectionneurd’élément. Par son intermédiaire, les valeurs stockées dans le dictionnaire peuvent êtred’un autre type que celles de la séquence d’entrée.

Troisième prototype

public static ILookup<K, E> ToLookup<T, K, E>( this IEnumerable<T> source, Func<T, K> keySelector, Func<T, E> elementSelector);

Linq.book Page 142 Mercredi, 18. février 2009 7:58 07

Page 158: LINQ Language Integrated Query en C

Chapitre 5 Les opérateurs non différés 143

L’argument elementSelector fait référence à un délégué qui retourne un fragment del’élément soumis, ou un objet d’un tout autre type. C’est cet élément qui sera stockédans le dictionnaire.

Le quatrième prototype cumule les avantages des deux précédents. Par son intermé-diaire, vous pouvez spécifier un elementSelector et un comparateur.

Quatrième prototype

public static ILookup<K, E> ToLookup<T, K, E>( this IEnumerable<T> source, Func<T, K> keySelector, Func<T, E> elementSelector, IEqualityComparer<K> comparer);

ExceptionsL’exception ArgumentNullExpression est levée si l’argument source, keySelector ouelementSelector, a pour valeur null ou si la clé retournée par keySelector a pourvaleur null.

L’exception ArgumentException est levée si un sélecteur retourne la même clé pourdeux éléments.

ExemplesPour illustrer le premier prototype de l’opérateur ToLookup, nous avons besoin d’uneclasse dont les éléments contiennent des membres qui peuvent être utilisés comme clés,mais qui ne sont pas forcément uniques. Nous utiliserons pour cela la classe Actor (voirListing 5.7).

Listing 5.7 : Un exemple d’appel du premier prototype de l’opérateur ToLookup.

ILookup<int, Actor> lookup = Actor.GetActors().ToLookup(k => k.birthYear);

// Recherche d’un acteur né en 1964IEnumerable<Actor> actors = lookup[1964];foreach (var actor in actors) Console.WriteLine("{0} {1}", actor.firstName, actor.lastName);

La première instruction crée un Lookup en utilisant le membre Actor.birthYearcomme clé. La deuxième instruction indexe le Lookup en utilisant la clé. Il ne reste plusqu’à énumérer l’objet actors pour afficher le ou les résultats :

Pour illustrer le deuxième prototype, nous allons légèrement modifier la classe Actor.Son membre birthYear, initialement de type int, sera de type string dans la classemodifiée.

Keanu ReevesSandra Bullock

Linq.book Page 143 Mercredi, 18. février 2009 7:58 07

Page 159: LINQ Language Integrated Query en C

144 LINQ to Objects Partie II

La classe utilisée par le deuxième prototype de l’opérateur ToLookup

public class Actor2{ public string birthYear; public string firstName; public string lastName;

public static Actor2[] GetActors() { Actor2[] actors = new Actor2[] { new Actor2 { birthYear = "1964", firstName = "Keanu", lastName = "Reeves" }, new Actor2 { birthYear = "1968", firstName = "Owen", lastName = "Wilson" }, new Actor2 { birthYear = "1960", firstName = "James", lastName = "Spader" }, // Une date exprimée sur 5 chiffres new Actor2 { birthYear = "01964", firstName = "Sandra", lastName = "Bullock" }, }; return(actors); }}

Le membre birthYear est maintenant une chaîne de caractères. Il ne reste plus qu’àappeler l’opérateur ToLookup (voir Listing 5.8).

Listing 5.8 : Un exemple d’utilisation du deuxième prototype de l’opérateur ToLookup.

ILookup<string, Actor2> lookup = Actor2.GetActors() .ToLookup(k => k.birthYear, new MyStringifiedNumberComparer());

// Recherche d’un acteur né en 1964IEnumerable<Actor2> actors = lookup["0001964"];foreach (var actor in actors) Console.WriteLine("{0} {1}", actor.firstName, actor.lastName);

La méthode de comparaison est la même que celle qui avait été utilisée pour illustrerl’opérateur Dictionary. En effet, l’éventuel ou les éventuels "0" en tête de clé n’étantpas significatifs, il est nécessaire de tester l’égalité "au sens large". Voici les résultats :

La recherche d’éléments dont la clé vaut "0001964" retourne les acteurs Keanu Reeveset Sandra Bullock, dont les clés respectives valent "1964" et "01964". L’objet decomparaison a donc bien fonctionné.

Pour illustrer le troisième prototype, nous ferons appel à la classe Actor, qui avait déjàété utilisée dans l’exemple du premier prototype (voir Listing 5.9).

Listing 5.9 : Un exemple d’utilisation du troisième prototype de l’opérateur ToLookup.

ILookup<int, string> lookup = Actor.GetActors() .ToLookup(k => k.birthYear, a => string.Format("{0} {1}", a.firstName, a.lastName));

// Recherche d’un acteur né en 1964IEnumerable<string> actors = lookup[1964];

Keanu ReevesSandra Bullock

Linq.book Page 144 Mercredi, 18. février 2009 7:58 07

Page 160: LINQ Language Integrated Query en C

Chapitre 5 Les opérateurs non différés 145

foreach (var actor in actors) Console.WriteLine("{0}", actor);

Dans cet exemple, l’argument elementSelector est une expression lambda qui conca-tène les champs firstName et lastName. Voici le résultat :

En utilisant cette troisième variante de l’opérateur ToLookup, le type de données mémoriséesdans l’objet Lookup (string) est différent de celui des éléments passés en entrée (Actor).

Pour illustrer le quatrième prototype, nous allons utiliser la classe Actor2 et la classecommune MyStringfieldNumberComparer (voir Listing 5.10).

Listing 5.10 : Un exemple d’appel du quatrième prototype de l’opérateur ToLookup.

ILookup<string, string> lookup = Actor2.GetActors() .ToLookup(k => k.birthYear, a => string.Format("{0} {1}", a.firstName, a.lastName), new MyStringifiedNumberComparer());

// Recherche d’un acteur né en 1964IEnumerable<string> actors = lookup["0001964"];foreach (var actor in actors) Console.WriteLine("{0}", actor);

Voici le résultat :

Cet exemple recherche des éléments dont la clé vaut "0001964". Les acteurs KeanuReeves et Sandra Bullock, dont les clés respectives valent "1964" et "01964", corres-pondent au critère. La comparaison a donc bien fonctionné. Par ailleurs, seules les chaînesnécessaires à la requête (firstName et lastName) sont stockées dans le Lookup.

Opérateurs d’égalité

Les opérateurs de cette catégorie sont utilisés pour tester l’égalité entre deux séquences.

Opérateur SequenceEqualL’opérateur SequenceEqual détermine si deux séquences d’entrée sont égales.

PrototypesDeux prototypes de l’opérateur SequenceEqual seront étudiés dans ce livre.

Premier prototype

public static bool SequenceEqual<T>( this IEnumerable<T> first, IEnumerable<T> second);

Keanu ReevesSandra Bullock

Keanu ReevesSandra Bullock

Linq.book Page 145 Mercredi, 18. février 2009 7:58 07

Page 161: LINQ Language Integrated Query en C

146 LINQ to Objects Partie II

Cet opérateur énumère les deux séquences en parallèle et compare leurs éléments enutilisant la méthode System.Object.Equals. Si tous les éléments sont égaux et si lesdeux séquences ont le même nombre d’éléments, l’opérateur retourne la valeur true.Dans le cas contraire, il retourne la valeur false.

Second prototype

public static bool SequenceEqual<T>( this IEnumerable<T> first, IEnumerable<T> second, IEqualityComparer<T> comparer);

ExceptionsL’exception ArgumentNullExpression est levée si un des arguments a pour valeurnull.

Exemples

Listing 5.11 : Un exemple d’utilisation du premier prototype de l’opérateur SequenceEqual.

string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"};

bool eq = presidents.SequenceEqual(presidents);Console.WriteLine(eq);

Voici le résultat :

Ceci vous semble un peu trop simple ? Nous allons légèrement compliquer les chosesdans le Listing 5.12.

Listing 5.12 : Un autre exemple d’utilisation du premier prototype de l’opérateur SequenceEqual.

string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"};

bool eq = presidents.SequenceEqual(presidents.Take(presidents.Count()));Console.WriteLine(eq);

True

Linq.book Page 146 Mercredi, 18. février 2009 7:58 07

Page 162: LINQ Language Integrated Query en C

Chapitre 5 Les opérateurs non différés 147

Si l’écriture vous semble plus complexe, un rapide examen va vous persuader ducontraire. L’opérateur Take limite la comparaison à… tous les éléments du tableaupresidents (presidents.Count()). Ce code est donc strictement équivalent au précé-dent et, bien entendu, il produit le même résultat :

Nous allons maintenant comparer le tableau presidents avec ses presidents.Count()– 1 premiers éléments (voir Listing 5.13).

Listing 5.13 : Un autre exemple d’utilisation du premier prototype de l’opérateur SequenceEqual.

string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"};

bool eq = presidents.SequenceEqual(presidents.Take(presidents.Count() - 1)); Console.WriteLine(eq);

Voici le résultat :

Les deux séquences n’ayant pas le même nombre d’éléments, il est tout à fait normalque la valeur False soit retournée.

Au chapitre précédent, lors de l’étude des opérateurs Take et Skip, il a été dit que, si cesopérateurs étaient utilisés correctement, ils permettaient de retrouver la séquence origi-nale. Nous allons maintenant le prouver en leur adjoignant les opérateurs Concat etSequenceEqual (voir Listing 5.14).

Listing 5.14 : Un exemple plus complexe du premier prototype de l’opérateur SequenceEqual.

string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"};

bool eq = presidents.SequenceEqual(presidents.Take(5).Concat(presidents.Skip(5)));Console.WriteLine(eq);

Dans cet exemple, Take(5) extrait les cinq premiers éléments de la séquence originale.Ces éléments sont alors concaténés à la séquence originale (Concat) en sautant les cinq

True

False

Linq.book Page 147 Mercredi, 18. février 2009 7:58 07

Page 163: LINQ Language Integrated Query en C

148 LINQ to Objects Partie II

premiers éléments (Skip(5)). La séquence obtenue est comparée à la séquence origi-nale (presidents.SequenceEqual()). Comme il se doit, la valeur True est retournéepar l’opérateur SequenceEqual :

Pour illustrer le second prototype, nous allons utiliser deux tableaux de string dontchaque élément est un nombre exprimé sous la forme d’une chaîne. Les éléments desdeux tableaux seront définis de telle sorte qu’ils soient égaux, après conversion enentiers. Pour effectuer la comparaison, nous utiliserons la classe MyStringifieldNum-berComparer (voir Listing 5.15).

Listing 5.15 : Un exemple du second prototype de l’opérateur SequenceEqual.

string[] stringifiedNums1 = { "001", "49", "017", "0080", "00027", "2" };

string[] stringifiedNums2 = { "1", "0049", "17", "080", "27", "02" };

bool eq = stringifiedNums1.SequenceEqual(stringifiedNums2, new MyStringifiedNumberComparer());

Console.WriteLine(eq);

En examinant rapidement les deux tableaux, vous pouvez voir que leurs éléments sontégaux, après conversion en entiers. Voici le résultat :

Opérateurs agissant au niveau des éléments

Cette catégorie d’opérateurs vous permet d’obtenir des éléments à partir de la séquenced’entrée.

Opérateur FirstSelon le prototype utilisé, l’opérateur First retourne le premier élément de la séquenced’entrée ou de la séquence correspondant à un prédicat.

PrototypesDeux prototypes de l’opérateur First seront étudiés dans ce livre.

Premier prototype

public static T First<T>( this IEnumerable<T> source);

Ce prototype retourne le premier élément de la séquence d’entrée source.

True

True

Linq.book Page 148 Mercredi, 18. février 2009 7:58 07

Page 164: LINQ Language Integrated Query en C

Chapitre 5 Les opérateurs non différés 149

Second prototype

public static T First<T>( this IEnumerable<T> source, Func<T, bool> predicate);

Ce prototype retourne le premier élément de la séquence d’entrée pour lequel le prédi-cat vaut true.

ExceptionsL’exception ArgumentNullExpression est levée si un des arguments a pour valeurnull.

L’exception InvalidOperationException est levée si le prédicat ne retourne la valeurtrue pour aucun des éléments de la séquence d’entrée.

Exemples

Listing 5.16 : Un exemple d’utilisation du premier prototype de l’opérateur First.

string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"};

string name = presidents.First();Console.WriteLine(name);

Voici le résultat :

Vous vous demandez peut-être si les opérateurs First et Take(1) sont différents ? Ehbien, oui ! L’opérateur Take retourne une séquence d’éléments (y compris dans le casoù cette séquence ne contient qu’un seul élément). En revanche, l’opérateur Firstretourne un élément ou génère une exception si aucun élément ne peut être retourné.

Listing 5.17 : Un exemple d’utilisation du deuxième prototype de l’opérateur First.

string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"};

string name = presidents.First(p => p.StartsWith("H"));Console.WriteLine(name);

Adams

Linq.book Page 149 Mercredi, 18. février 2009 7:58 07

Page 165: LINQ Language Integrated Query en C

150 LINQ to Objects Partie II

Ce code devrait retourner le premier élément de la séquence d’entrée qui commence parla lettre "H". Voici le résultat :

Si aucun élément ne peut être renvoyé par l’opérateur First, une exception Invalid-OperationException est levée. Pour éviter ce problème, utilisez l’opérateur First-OrDefault.

Opérateur FirstOrDefaultL’opérateur FirstOrDefault est semblable à l’opérateur First, excepté en ce quiconcerne son comportement lorsque aucun élément n’est trouvé.

PrototypesDeux prototypes de l’opérateur FirstOrDefault seront étudiés dans ce livre.

Premier prototype

public static T FirstOrDefault<T>( this IEnumerable<T> source);

Ce prototype retourne le premier élément de la séquence d’entrée source. Si la séquenced’entrée est vide, l’objet default(T) est retourné (la valeur par défaut des types réfé-rence et nullable est null).

Second prototype

public static T FirstOrDefault<T>( this IEnumerable<T> source, Func<T, bool> predicate);

Ce prototype retourne le premier élément de la séquence d’entrée pour lequel le prédicatvaut true.

ExceptionsL’exception ArgumentNullExpression est levée si un des arguments a pour valeurnull.

Exemples

Listing 5.18 : Appel du premier prototype de l’opérateur FirstOrDefault. L’élément recherché n’est pas trouvé.

string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"};

Harding

Linq.book Page 150 Mercredi, 18. février 2009 7:58 07

Page 166: LINQ Language Integrated Query en C

Chapitre 5 Les opérateurs non différés 151

string name = presidents.Take(0).FirstOrDefault();Console.WriteLine(name == null ? "NULL" : name);

Voici le résultat :

Listing 5.19 : Appel du premier prototype de l’opérateur FirstOrDefault. L’élément recherché est trouvé.

string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"};

string name = presidents.FirstOrDefault();Console.WriteLine(name == null ? "NULL" : name);

Voici le résultat :

Pour illustrer le second prototype de l’opérateur FirstOrDefault, nous allons rechercherle premier élément qui commence par la lettre "B".

Listing 5.20 : Appel du second prototype. L’élément recherché est trouvé.

string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"};

string name = presidents.FirstOrDefault(p => p.StartsWith("B"));Console.WriteLine(name == null ? "NULL" : name);

Voici le résultat :

Listing 5.21 : Appel du second prototype. L’élément recherché n’est pas trouvé.

string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"};

string name = presidents.FirstOrDefault(p => p.StartsWith("Z"));Console.WriteLine(name == null ? "NULL" : name);

NULL

Adams

Buchanan

Linq.book Page 151 Mercredi, 18. février 2009 7:58 07

Page 167: LINQ Language Integrated Query en C

152 LINQ to Objects Partie II

Aucune réponse n’étant trouvée, voici le résultat :

Opérateur LastSelon le prototype utilisé, l’opérateur Last retourne le dernier élément de la séquenced’entrée ou de la séquence correspondant à un prédicat.

PrototypesDeux prototypes de l’opérateur Last seront étudiés dans ce livre.

Premier prototype

public static T Last<T>( this IEnumerable<T> source);

Ce prototype retourne le dernier élément de la séquence d’entrée source.

Second prototype

public static T Last<T>( this IEnumerable<T> source, Func<T, bool> predicate);

Ce prototype retourne le dernier élément de la séquence d’entrée pour lequel le prédicatvaut true.

ExceptionsL’exception ArgumentNullExpression est levée si un des arguments a pour valeurnull.

L’exception InvalidOperationException est levée si le prédicat ne retourne la valeurtrue pour aucun des éléments de la séquence d’entrée.

Exemples

Listing 5.22 : Un exemple d’utilisation du premier prototype de l’opérateur Last.

string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"};

string name = presidents.Last();Console.WriteLine(name);

Voici le résultat :

NULL

Wilson

Linq.book Page 152 Mercredi, 18. février 2009 7:58 07

Page 168: LINQ Language Integrated Query en C

Chapitre 5 Les opérateurs non différés 153

Listing 5.23 : Un exemple d’utilisation du second prototype de l’opérateur Last.

string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"};

string name = presidents.Last(p => p.StartsWith("H"));Console.WriteLine(name);

Ce code devrait retourner le dernier élément de la séquence d’entrée qui commence parla lettre "H". Voici le résultat :

Si aucun élément ne peut être renvoyé par l’opérateur Last, une exception Invalid-OperationException est levée. Pour éviter ce problème, utilisez l’opérateur LastOr-Default.

Opérateur LastOrDefaultL’opérateur LastOrDefault est semblable à l’opérateur Last, excepté en ce quiconcerne son comportement lorsque aucun élément n’est trouvé.

PrototypesDeux prototypes de l’opérateur LastOrDefault seront étudiés dans ce livre.

Premier prototype

public static T LastOrDefault<T>( this IEnumerable<T> source);

Ce prototype retourne le dernier élément de la séquence d’entrée source. Si la séquenced’entrée est vide, l’objet default(T) est retourné (la valeur par défaut des types réfé-rence et nullable est null).

Second prototype

public static T LastOrDefault<T>( this IEnumerable<T> source, Func<T, bool> predicate);

Ce prototype retourne le dernier élément de la séquence d’entrée pour lequel le prédicatvaut true.

ExceptionsL’exception ArgumentNullExpression est levée si un des arguments a pour valeurnull.

Hoover

Linq.book Page 153 Mercredi, 18. février 2009 7:58 07

Page 169: LINQ Language Integrated Query en C

154 LINQ to Objects Partie II

Exemples

Listing 5.24 : Appel du premier prototype de l’opérateur LastOrDefault. L’élément recherché n’est pas trouvé.

string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"};

string name = presidents.Take(0).LastOrDefault();Console.WriteLine(name == null ? "NULL" : name);

Voici le résultat :

Listing 5.25 : Appel du premier prototype de l’opérateur LastOrDefault. L’élément recherché est trouvé.

string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"};

string name = presidents.LastOrDefault();Console.WriteLine(name == null ? "NULL" : name);

Voici le résultat :

Pour illustrer le second prototype de l’opérateur LastOrDefault, nous allons rechercherle dernier élément qui commence par la lettre "B".

Listing 5.26 : Appel du second prototype. L’élément recherché est trouvé.

string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"};

string name = presidents.LastOrDefault(p => p.StartsWith("B"));Console.WriteLine(name == null ? "NULL" : name);

Voici le résultat :

NULL

Wilson

Bush

Linq.book Page 154 Mercredi, 18. février 2009 7:58 07

Page 170: LINQ Language Integrated Query en C

Chapitre 5 Les opérateurs non différés 155

Listing 5.27 : Appel du second prototype. L’élément recherché n’est pas trouvé.

string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"};

string name = presidents.LastOrDefault(p => p.StartsWith("Z"));Console.WriteLine(name == null ? "NULL" : name);

Aucune réponse n’étant trouvée, voici le résultat :

Opérateur SingleSelon le prototype utilisé, l’opérateur Single retourne le seul élément de la séquenced’entrée, ou le seul élément de la séquence d’entrée correspondant à un prédicat.

PrototypesDeux prototypes de l’opérateur Single seront étudiés dans ce livre.

Premier prototype

public static T Single<T>( this IEnumerable<T> source);Ce prototype énumère la séquence d’entrée source et renvoie l’unique élément trouvé.

Second prototype

public static T Single<T>( this IEnumerable<T> source, Func<T, bool> predicate);

Ce second prototype retourne l’unique élément pour lequel le prédicat a pour valeurtrue. L’exception InvalidOperationException est levée si le prédicat ne retourne lavaleur true pour aucun ou pour plusieurs des éléments de la séquence d’entrée.

ExceptionsL’exception ArgumentNullExpression est levée si un des arguments a pour valeur null.

L’exception InvalidOperationException est levée si le prédicat ne retourne la valeurtrue pour aucun ou pour plusieurs des éléments de la séquence d’entrée, ou si laséquence d’entrée est vide.

Exemples

Listing 5.28 : Un exemple d’utilisation du premier prototype de l’opérateur Single sur la classe commune Employee.

Employee emp = Employee.GetEmployeesArray() .Where(e => e.id == 3).Single();

Console.WriteLine("{0} {1}", emp.firstName, emp.lastName);

NULL

Linq.book Page 155 Mercredi, 18. février 2009 7:58 07

Page 171: LINQ Language Integrated Query en C

156 LINQ to Objects Partie II

La requête retourne un seul et unique élément. Dans cet exemple, tout se passe bien, carun seul employé a un identifiant égal à 3 (Where(e => e.id == 3)). Voici le résultat :

Listing 5.29 : Un exemple d’appel du second prototype de l’opérateur Single.

Employee emp = Employee.GetEmployeesArray() .Single(e => e.id == 3);

Console.WriteLine("{0} {1}", emp.firstName, emp.lastName);

Ce code est équivalent au précédent. Mais, ici, au lieu d’invoquer l’opérateur Wherepour s’assurer de l’unicité de la réponse, un prédicat est passé en argument de l’opérateurSingle. Voici le résultat :

Opérateur SingleOrDefaultL’opérateur SingleOrDefault est semblable à l’opérateur Single, excepté en ce quiconcerne son comportement lorsque aucun élément n’est trouvé.

PrototypesDeux prototypes de l’opérateur SingleOrDefault seront étudiés dans ce livre.

Premier prototype

public static T SingleOrDefault<T>( this IEnumerable<T> source);

Ce prototype énumère la séquence d’entrée source et renvoie l’unique élément trouvé.Si la séquence est vide, l’objet default(T) est retourné (la valeur par défaut des typesréférence et nullable est null). Si plusieurs éléments sont trouvés, une exception Invalid-OperationException est levée.

Le second prototype de l’opérateur SingleOrDefault permet de spécifier un prédicatpour indiquer quel élément doit être retourné.

Second prototype

public static T SingleOrDefault<T>( this IEnumerable<T> source, Func<T, bool> predicate);

ExceptionsL’exception ArgumentNullExpression est levée si un des arguments a pour valeurnull.

L’exception InvalidOperationException est levée si le prédicat retourne la valeurtrue pour plusieurs des éléments de la séquence d’entrée.

Anders Hejlsberg

Anders Hejlsberg

Linq.book Page 156 Mercredi, 18. février 2009 7:58 07

Page 172: LINQ Language Integrated Query en C

Chapitre 5 Les opérateurs non différés 157

ExemplesLe Listing 5.30 illustre le fonctionnement du premier prototype dans le cas où aucunélément n’est trouvé dans la séquence d’entrée. Pour ce faire, nous utilisons l’opérateurWhere en spécifiant une clé inexistante.

Listing 5.30 : Un exemple d’utilisation du premier prototype de l’opérateur SingleOrDefault. Ici, aucun élément n’est trouvé.

Employee emp = Employee.GetEmployeesArray() .Where(e => e.id == 5).SingleOrDefault();

Console.WriteLine(emp == null ? "NULL" : string.Format("{0} {1}", emp.firstName, emp.lastName));

Ce code effectue une requête sur un employé dont l’identifiant vaut 5, en sachant perti-nemment qu’un tel identifiant n’existe pas. Une séquence vide est donc retournée. Voicile résultat :

Le Listing 5.31 illustre le fonctionnement du premier prototype dans le cas où unélément est trouvé dans la séquence d’entrée. Pour ce faire, nous utilisons l’opérateurWhere en spécifiant une clé existante et unique.

Listing 5.31 : Un exemple d’utilisation du premier prototype de l’opérateur SingleOrDefault. Ici, un élément est trouvé.

Employee emp = Employee.GetEmployeesArray() .Where(e => e.id == 4).SingleOrDefault();

Console.WriteLine(emp == null ? "NULL" : string.Format("{0} {1}", emp.firstName, emp.lastName));

L’identifiant spécifié dans l’opérateur Where existe et est unique. Voici le résultat :

Pour illustrer le fonctionnement du second prototype, nous allons cette fois passer unprédicat à l’opérateur SingleOrDefault en choisissant un identifiant qui existe.

Listing 5.32 : Un exemple d’utilisation du second prototype de l’opérateur SingleOrDefault. Ici, un élément est trouvé.

Employee emp = Employee.GetEmployeesArray() .SingleOrDefault(e => e.id == 4);

Console.WriteLine(emp == null ? "NULL" : string.Format("{0} {1}", emp.firstName, emp.lastName));

NULL

David Lightman

Linq.book Page 157 Mercredi, 18. février 2009 7:58 07

Page 173: LINQ Language Integrated Query en C

158 LINQ to Objects Partie II

Ce code est équivalent au précédent. Mais, ici, au lieu d’invoquer l’opérateur Wherepour filtrer les données, un prédicat est passé comme argument de l’opérateurSingleOrDefault. Voici le résultat :

Nous allons maintenant essayer un prédicat qui ne trouve aucune correspondance dansles données (voir Listing 5.33).

Listing 5.33 : Un exemple d’utilisation du second prototype de l’opérateur SingleOrDefault. Ici, aucun élément n’est trouvé.

Employee emp = Employee.GetEmployeesArray() .SingleOrDefault(e => e.id == 5);

Console.WriteLine(emp == null ? "NULL" : string.Format("{0} {1}", emp.firstName, emp.lastName));

Aucune réponse n’étant trouvée, voici le résultat (remarquez qu’aucune exception n’aété générée) :

Opérateur ElementAtL’opérateur ElementAt retourne l’élément de la séquence d’entrée dont l’index est spécifié.

PrototypeUn seul prototype de l’opérateur ElementAt sera étudié dans ce livre :

public static T ElementAt<T>( this IEnumerable<T> source, int index);

Si la séquence implémente IList<T>, l’interface IList est utilisée pour retrouverl’élément indexé. Dans le cas contraire, la séquence est énumérée jusqu’à ce quel’élément indexé soit atteint. Une exception ArgumentOutOfRangeExeption est levée sil’index est négatif ou supérieur au nombre d’éléments dans la séquence.

INFO

Dans le langage C#, le premier élément d’un index a pour valeur zéro et le dernier, lenombre d’éléments de la séquence moins un.

ExceptionsL’exception ArgumentNullExpression est levée si l’argument source a pour valeur null.

L’exception ArgumentOutOfRangeExeption est levée si l’index est négatif ou supérieurau nombre d’éléments dans la séquence.

David Lightman

NULL

Linq.book Page 158 Mercredi, 18. février 2009 7:58 07

Page 174: LINQ Language Integrated Query en C

Chapitre 5 Les opérateurs non différés 159

Exemples

Listing 5.34 : Exemple d’appel de l’opérateur ElementAt.

Employee emp = Employee.GetEmployeesArray() .ElementAt(3);

Console.WriteLine("{0} {1}", emp.firstName, emp.lastName);

L’élément de rang 3 (c’est-à-dire le quatrième élément) a été demandé. Voici le résultat :

Opérateur ElementAtOrDefaultL’opérateur ElementAtOrDefault retourne l’élément de la séquence d’entrée dontl’index est spécifié.

PrototypeUn seul prototype de l’opérateur ElementAtOrDefault sera étudié dans ce livre :

public static T ElementAtOrDefault<T>( this IEnumerable<T> source, int index);

Si la séquence implémente IList<T>, l’interface IList est utilisée pour retrouverl’élément indexé. Dans le cas contraire, la séquence est énumérée jusqu’à ce quel’élément indexé soit atteint. Si l’index est négatif, supérieur ou égal au nombred’éléments dans la séquence, l’objet default(T) est retourné (la valeur par défaut destypes référence et nullable est null). Cette seule caractéristique différencie les opérateursElementAtOrDefault et ElementAt.

INFO

Dans le langage C#, le premier élément d’un index a pour valeur zéro et le dernier, lenombre d’éléments de la séquence moins un.

ExceptionsL’exception ArgumentNullExpression est levée si l’argument source a pour valeur null.

Exemples

Listing 5.35 : Exemple d’appel de l’opérateur ElementAt avec un index valide.

Employee emp = Employee.GetEmployeesArray() .ElementAtOrDefault(3);

Console.WriteLine(emp == null ? "NULL" : string.Format("{0} {1}", emp.firstName, emp.lastName));

David Lightman

Linq.book Page 159 Mercredi, 18. février 2009 7:58 07

Page 175: LINQ Language Integrated Query en C

160 LINQ to Objects Partie II

Voici le résultat :

L’élément dont l’index vaut 3 est bien retourné par la requête. Nous allons maintenanttransmettre un index invalide à cette même requête (voir Listing 5.36).

Listing 5.36 : Exemple d’appel de l’opérateur ElementAt avec un index invalide.

Employee emp = Employee.GetEmployeesArray() .ElementAtOrDefault(5);

Console.WriteLine(emp == null ? "NULL" :

Étant donné que l’index 5 ne correspond à aucun élément, voici le résultat retourné :

Quantificateurs

Les quantificateurs permettent de tester l’existence d’une valeur dans une séquenced’entrée.

Opérateur AnyL’opérateur Any retourne la valeur true si au moins un élément de la séquence d’entréevérifie une condition.

PrototypesDeux prototypes de l’opérateur Any seront étudiés dans ce livre.

Premier prototype

public static bool Any<T>( this IEnumerable<T> source);

Ce prototype retourne la valeur true si la séquence d’entrée contient au moins unélément.

Second prototype

public static bool Any<T>( this IEnumerable<T> source, Func<T, bool> predicate);

Le second prototype énumère la séquence d’entrée source. Il retourne la valeur true sile prédicat retourne la valeur true sur au moins un élément de la séquence. L’énumérationstoppe dès que cette condition est atteinte.

ExceptionsL’exception ArgumentNullExpression est levée si un des arguments a pour valeurnull.

David Lightman

NULL

Linq.book Page 160 Mercredi, 18. février 2009 7:58 07

Page 176: LINQ Language Integrated Query en C

Chapitre 5 Les opérateurs non différés 161

Exemples

Listing 5.37 : Exemple d’appel du premier prototype avec une séquence d’entrée vide.

bool any = Enumerable.Empty<string>().Any();Console.WriteLine(any);

Voici le résultat :

Listing 5.38 : Exemple d’appel du premier prototype avec une séquence d’entrée non vide.

string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"};

bool any = presidents.Any();Console.WriteLine(any);

Voici le résultat :

Listing 5.39 : Exemple d’appel du second prototype. Ici, aucun élément ne correspond au prédicat.

string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"};

bool any = presidents.Any(s => s.StartsWith("Z"));Console.WriteLine(any);

Le prédicat limite la requête aux éléments dont le nom commence par la lettre "Z".Comme aucun élément ne correspond, la valeur False est retournée :

Listing 5.40 : Exemple d’appel du second prototype. Ici, au moins un élément correspond au prédicat.

string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"};

bool any = presidents.Any(s => s.StartsWith("A"));Console.WriteLine(any);

False

True

False

Linq.book Page 161 Mercredi, 18. février 2009 7:58 07

Page 177: LINQ Language Integrated Query en C

162 LINQ to Objects Partie II

Au moins un élément du tableau presidents correspondant au prédicat, la valeur Trueest retournée :

Opérateur AllL’opérateur All retourne la valeur true si tous les éléments de la séquence d’entréevérifient une condition.

PrototypeUn seul prototype de l’opérateur All sera étudié dans ce livre :

public static bool All<T>( this IEnumerable<T> source, Func<T, bool> predicate);

L’opérateur All énumère la séquence d’entrée. Il retourne la valeur true si le prédicatest vérifié sur tous les éléments de la séquence. Si le prédicat retourne la valeur falsepour un élément, l’énumération cesse immédiatement.

ExceptionsL’exception ArgumentNullExpression est levée si un des arguments a pour valeur null.

Exemples

Listing 5.41 : Exemple d’appel du prototype de l’opérateur All. Ici, le prédicat ne retourne pas la valeur True pour tous les éléments.

string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"};

bool all = presidents.All(s => s.Length > 5); Console.WriteLine(all);

Tous les éléments du tableau presidents n’ayant pas une longueur supérieure à5 caractères, le prédicat n’est pas toujours vérifié. Le résultat est sans appel :

Listing 5.42 : Exemple d’appel du prototype de l’opérateur All. Ici, le prédicat retourne la valeur True pour tous les éléments.

string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley",

True

False

Linq.book Page 162 Mercredi, 18. février 2009 7:58 07

Page 178: LINQ Language Integrated Query en C

Chapitre 5 Les opérateurs non différés 163

"Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"};

bool all = presidents.All(s => s.Length > 3); Console.WriteLine(all);

Les noms des présidents comprenant un minimum de 4 caractères, le prédicat est vérifiépour tous les éléments du tableau. Voici le résultat :

Opérateur ContainsL’opérateur Contains retourne la valeur true si un des éléments de la séquenced’entrée vérifie la condition.

PrototypesDeux prototypes de l’opérateur All seront étudiés dans ce livre.

Premier prototype

public static bool Contains<T>( this IEnumerable<T> source, T value);

Dans un premier temps, ce prototype teste si la séquence d’entrée implémente l’interfaceICollection<T>. Dans l’affirmative, la méthode Contains de cette interface est appelée.Dans le cas contraire, la séquence d’entrée est énumérée pour voir si un de ses élémentsvérifie la condition. Dès qu’une telle situation est atteinte, l’énumération prend fin.

La valeur spécifiée est comparée aux éléments de la séquence d’entrée en utilisant laclasse de comparaison par défaut : EqualityComparer<K>.Default.

Second prototype

Le second prototype est en tout point comparable au premier, si ce n’est qu’il permet despécifier un objet IEqualityComparer<T>. Dans ce cas, c’est ce comparateur qui estutilisé pour comparer les éléments de la séquence d’entrée :

public static bool Contains<T>( this IEnumerable<T> source, T value, IEqualityComparer<T> comparer);

ExceptionsL’exception ArgumentNullExpression est levée si l’argument source a pour valeur null.

Exemples

Listing 5.43 : Exemple d’appel du premier prototype. La valeur spécifiée ne se trouve pas dans la séquence.

string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland",

True

Linq.book Page 163 Mercredi, 18. février 2009 7:58 07

Page 179: LINQ Language Integrated Query en C

164 LINQ to Objects Partie II

"Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"};

bool contains = presidents.Contains("Rattz");Console.WriteLine(contains);

Aucun élément contenant la valeur "Rattz" dans le tableau. Le résultat est donc lesuivant :

Listing 5.44 : Exemple d’appel du premier prototype. La valeur spécifiée se trouve dans la séquence.

string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"};

bool contains = presidents.Contains("Hayes"); Console.WriteLine(contains);

Un des éléments du tableau contenant la valeur "Hayes", le résultat est le suivant :

Pour illustrer le second prototype, nous allons utiliser la classe commune MyStringi-fieldNumberComparer (voir Listing 5.45). La requête recherchera un nombre stocké auformat chaîne et précédé de plusieurs zéros. Le comparateur ne prenant pas en considé-ration les zéros de tête, ce nombre sera retrouvé dans le tableau.

Listing 5.45 : Exemple d’appel du second prototype. La valeur spécifiée est trouvée dans la séquence.

string[] stringifiedNums = { "001", "49", "017", "0080", "00027", "2" };

bool contains = stringifiedNums.Contains("0000002", new MyStringifiedNumberComparer());

Console.WriteLine(contains);

Le comparateur convertit la chaîne recherchée en un nombre. Les zéros de tête dispa-raissent et la valeur est trouvée dans la séquence. La variable contains devrait doncavoir pour valeur true. Voici le résultat :

False

True

True

Linq.book Page 164 Mercredi, 18. février 2009 7:58 07

Page 180: LINQ Language Integrated Query en C

Chapitre 5 Les opérateurs non différés 165

Nous allons maintenant rechercher un élément inexistant dans la séquence d’entrée(voir Listing 5.46).

Listing 5.46 : Exemple d’appel du second prototype. La valeur spécifiée n’est pas trouvée dans la séquence.

string[] stringifiedNums = { "001", "49", "017", "0080", "00027", "2" };

bool contains = stringifiedNums.Contains("000271", new MyStringifiedNumberComparer());

Console.WriteLine(contains);

L’élément "000271" n’étant pas trouvé dans la séquence d’entrée, voici le résultat :

Fonctions de comptage

Les opérateurs de ce groupe effectuent des comptes (nombre d’éléments, somme, mini-mum, maximum) sur les éléments de la séquence d’entrée.

Opérateur CountL’opérateur Count retourne le nombre d’éléments de la séquence d’entrée.

PrototypesDeux prototypes de l’opérateur Count seront étudiés dans ce livre.

Premier prototype

public static int Count<T>( this IEnumerable<T> source);

Ce prototype teste si la séquence d’entrée implémente l’interface ICollection<T>.Dans l’affirmative, il obtient le nombre d’éléments de la séquence en utilisant la fonc-tion de comptage de cette interface. Dans la négative, le nombre d’éléments est obtenuen énumérant la séquence d’entrée.

Le second prototype renvoie le nombre d’éléments de la séquence d’entrée pourlesquels le prédicat retourne la valeur true.

Second prototype

public static int Count<T>( this IEnumerable<T> source, Func<T, bool> predicate);

ExceptionsL’exception ArgumentNullExpression est levée si un des arguments a pour valeurnull.

False

Linq.book Page 165 Mercredi, 18. février 2009 7:58 07

Page 181: LINQ Language Integrated Query en C

166 LINQ to Objects Partie II

L’exception OverflowException est levée si le nombre d’éléments est supérieur à lavaleur maximale autorisée par int.MaxValue.

ExemplesL’exemple du Listing 5.47 compte le nombre de présidents stockés dans la séquenced’entrée.

Listing 5.47 : Exemple d’appel du premier prototype.

string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"};

int count = presidents.Count();Console.WriteLine(count);

Voici le résultat :

L’exemple du Listing 5.48 compte le nombre de présidents stockés dans la séquenced’entrée dont le nom commence par la lettre "J".

Listing 5.48 : Exemple d’appel du second prototype.

string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"};

int count = presidents.Count(s => s.StartsWith("J"));Console.WriteLine(count);

Voici le résultat :

Si le nombre d’éléments dépasse la capacité de int.MaxValue, vous utiliserez l’opérateurLongCount.

Opérateur LongCountL’opérateur Count retourne le nombre d’éléments de la séquence d’entrée au formatlong.

37

3

Linq.book Page 166 Mercredi, 18. février 2009 7:58 07

Page 182: LINQ Language Integrated Query en C

Chapitre 5 Les opérateurs non différés 167

PrototypesDeux prototypes de l’opérateur LongCount seront étudiés dans ce livre.

Premier prototype

public static long LongCount<T>( this IEnumerable<T> source);

Le premier prototype énumère la séquence d’entrée et retourne le nombre d’élémentscomptés.

Le second prototype renvoie le nombre d’éléments de la séquence d’entrée pourlesquels le prédicat retourne la valeur true.

Second prototype

public static long LongCount<T>( this IEnumerable<T> source, Func<T, bool> predicate);

ExceptionsL’exception ArgumentNullExpression est levée si un des arguments a pour valeurnull.

ExemplesDans l’exemple du Listing 5.49, nous utilisons un opérateur de requête standard pourgénérer une séquence pour laquelle l’opérateur Count produirait une exception de typeOverflowException. Au chapitre précédent, nous avons étudié l’opérateur Range, quipermettait de créer une séquence en spécifiant son nombre d’éléments sous la formed’un int. Nous allons concaténer deux de ces séquences pour dépasser les capacités dutype int, et cela va donc nécessiter l’utilisation de l’opérateur LongCount.

Listing 5.49 : Exemple d’appel du premier prototype.

long count = Enumerable.Range(0, int.MaxValue).Concat(Enumerable.Range(0, int.MaxValue)).LongCount();

Console.WriteLine(count);

L’opérateur Range est appelé à deux reprises pour générer deux séquences contenantchacune le nombre maximal d’éléments du type int. Ces deux séquences sont alorsconcaténées à l’aide de l’opérateur Concat.

ATTENTIONATTENTION

L’exécution de cet exemple est assez longue. Sur ma machine, un Pentium 4 doté de 1 Go deRAM, il a fallu attendre deux minutes et demie !

Ne soyez pas surpris si cet exemple est très long à s’exécuter : il génère en effet deuxséquences de 2 147 483 647 éléments !

Linq.book Page 167 Mercredi, 18. février 2009 7:58 07

Page 183: LINQ Language Integrated Query en C

168 LINQ to Objects Partie II

Voici le résultat :

Si vous essayez d’exécuter cet exemple en utilisant l’opérateur Count, une exceptionOverflowException sera levée.

Pour illustrer le second prototype, nous reprendrons le même code que dans l’exem-ple précédent, mais nous limiterons l’énumération aux entiers supérieurs à 1 et infé-rieurs à 4. Seuls les éléments 2 et 3 seront donc sélectionnés. Étant donné que lecode définit deux séquences, l’énumération devrait donc compter quatre éléments(voir Listing 5.50).

Listing 5.50 : Exemple d’appel du second prototype.

long count = Enumerable.Range(0, int.MaxValue). Concat(Enumerable.Range(0, int.MaxValue)).LongCount(n => n > 1 && n < 4);

Console.WriteLine(count);

À l’exception du prédicat, ce code est très proche du précédent. Il est également trèslong à exécuter, et même plus long que celui de l’exemple précédent. Voici le résultataffiché dans la console :

Opérateur SumL’opérateur Sum retourne la somme des valeurs numériques contenues dans les élémentsde la séquence d’entrée.

PrototypesDeux prototypes de l’opérateur Sum seront étudiés dans ce livre.

Premier prototype

public static Numeric Sum( this IEnumerable<Numeric> source);

Numeric doit être choisi parmi les types suivants : int, long, double, décimal ou un deleurs équivalents nullables, int?, long?, double? ou decimal?.

Le premier prototype retourne la somme de tous les éléments de la séquence d’entréesource.

Si la séquence d’entrée est vide, la valeur retournée est 0. Les valeurs null des typesnullables ne sont pas incluses dans la somme.

Le second prototype est semblable au premier, mais les valeurs additionnées sont sélec-tionnées par l’intermédiaire d’un délégué.

4294967294

4

Linq.book Page 168 Mercredi, 18. février 2009 7:58 07

Page 184: LINQ Language Integrated Query en C

Chapitre 5 Les opérateurs non différés 169

Second prototype

public static Numeric Sum<T>( this IEnumerable<T> source, Func<T, Numeric> selector);

ExceptionsL’exception ArgumentNullExpression est levée si un des arguments a pour valeur null.

Si la somme des éléments dépasse la capacité du type Numeric :

m une valeur –infini ou +infini est retournée si Numeric est de type decimal oudecimal? ;

m une exception OverflowException est levée si Numeric est d’un autre type.

ExemplesL’exemple du Listing 5.51 génère une séquence d’entiers avec l’opérateur Range etcalcule leur somme en utilisant le premier prototype de l’opérateur Sum.

Listing 5.51 : Exemple d’appel du premier prototype.

IEnumerable<int> ints = Enumerable.Range(1, 10);

foreach (int i in ints) Console.WriteLine(i);

Console.WriteLine("--");

int sum = ints.Sum();Console.WriteLine(sum);

Voici les résultats :

Le Listing 5.52 illustre le second prototype. Ici, le calcul porte sur la somme desoptions des employés de la classe commune EmployeeOptionEntry.

Listing 5.52 : Exemple d’appel du second prototype.

IEnumerable<EmployeeOptionEntry> options = EmployeeOptionEntry.GetEmployeeOptionEntries();

long optionsSum = options.Sum(o => o.optionsCount);Console.WriteLine("Somme des options des employés : {0}", optionsSum);

12345678910--55

Linq.book Page 169 Mercredi, 18. février 2009 7:58 07

Page 185: LINQ Language Integrated Query en C

170 LINQ to Objects Partie II

Plutôt que calculer la somme de tous les membres des éléments, nous utilisons ici lesélecteur du second prototype pour limiter la somme au membre OptionsCount. Voicile résultat :

Opérateur MinL’opérateur Min retourne la plus petite valeur de la séquence d’entrée.

PrototypesQuatre prototypes de l’opérateur Min seront étudiés dans ce livre.

Premier prototype

public static Numeric Min( this IEnumerable<Numeric> source);

Numeric doit être choisi parmi les types suivants : int, long, double, décimal ou un deleurs équivalents nullables, int?, long?, double? ou decimal?.

Le premier prototype retourne la plus petite valeur de la séquence d’entrée. Si leséléments implémentent l’interface IComparable<T>, cette interface est utilisée pourcomparer les éléments. Dans le cas contraire, c’est l’interface non générique IComparablequi est utilisée.

La valeur null est retournée si la séquence est vide ou uniquement composée de valeursnull.

Le deuxième prototype de l’opérateur Min se comporte comme le premier, mais ils’applique aux types non numériques.

Deuxième prototype

public static T Min<T>( this IEnumerable<T> source);

Le troisième prototype est dédié aux types numériques. Il implémente une méthode desélection qui permet de limiter la comparaison à un seul membre de chaque élément.

Troisième prototype

public static Numeric Min<T>( this IEnumerable<T> source, Func<T, Numeric> selector);

Le quatrième prototype est dédié aux types non numériques. Tout comme le précédent,il implémente une méthode de sélection qui permet de limiter la comparaison à un seulmembre de chaque élément.

Quatrième prototype

public static S Min<T, S>( this IEnumerable<T> source, Func<T, S> selector);

Somme des options des employés : 51504

Linq.book Page 170 Mercredi, 18. février 2009 7:58 07

Page 186: LINQ Language Integrated Query en C

Chapitre 5 Les opérateurs non différés 171

ExceptionsL’exception ArgumentNullExpression est levée si un des arguments a pour valeurnull.

Si le type T est nullable (int?, long?, double? ou decimal?), les premier et troisièmeprototypes retournent la valeur null si la source est vide. Si le type T n’est pas nullable(int, long, double ou decimal), les premier et troisième prototypes lèvent une exceptionInvalidOperationException si la séquence source est vide.

ExemplesDans l’exemple du Listing 5.53, la plus petite valeur stockée dans un tableau d’entiersest retournée par le premier prototype de l’opérateur Min.

Listing 5.53 : Exemple d’appel du premier prototype.

int[] myInts = new int[] { 974, 2, 7, 1374, 27, 54 }; int minInt = myInts.Min(); Console.WriteLine(minInt);

Voici le résultat retourné :

Pour illustrer le deuxième prototype, nous appliquerons l’opérateur Min sur le tableaupresidents. La valeur retournée sera la "plus petite", alphabétiquement parlant.

Listing 5.54 : Exemple d’appel du deuxième prototype.

string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"};

string minName = presidents.Min();Console.WriteLine(minName);

Voici le résultat :

Le résultat est le même que celui qui aurait été renvoyé par l’opérateur First. Mais ceciest un cas particulier : si les éléments du tableau presidents avaient été classés dans unautre ordre ou de façon aléatoire, le résultat de la fonction Min resterait "Adams".

Pour illustrer le troisième prototype, nous rechercherons la date de naissance la plusancienne dans la classe Actor (voir Listing 5.55).

2

Adams

Linq.book Page 171 Mercredi, 18. février 2009 7:58 07

Page 187: LINQ Language Integrated Query en C

172 LINQ to Objects Partie II

Listing 5.55 : Exemple d’appel du troisième prototype.

int oldestActorAge = Actor.GetActors().Min(a => a.birthYear);Console.WriteLine(oldestActorAge);

Voici le résultat :

Pour illustrer le quatrième prototype, nous allons rechercher le "premier" nom d’acteur(alphabétiquement parlant) dans la classe Actor (voir Listing 5.56).

Listing 5.56 : Exemple d’appel du quatrième prototype.

string firstAlphabetically = Actor.GetActors().Min(a => a.lastName);Console.WriteLine(firstAlphabetically);

Voici le résultat :

Opérateur MaxL’opérateur Max retourne la plus grande valeur de la séquence d’entrée.

PrototypesQuatre prototypes de l’opérateur Max seront étudiés dans ce livre.

Premier prototype

public static Numeric Max( this IEnumerable<Numeric> source);

Numeric doit être choisi parmi les types suivants : int, long, double, décimal ou un deleurs équivalents nullables, int?, long?, double? ou decimal?.

Le premier prototype retourne la plus grande valeur de la séquence d’entrée. Si leséléments implémentent l’interface IComparable<T>, cette interface est utilisée pourcomparer les éléments. Dans le cas contraire, c’est l’interface non générique IComparablequi est utilisée.

La valeur null est retournée si la séquence est vide ou uniquement composée de valeursnull.

Le deuxième prototype de l’opérateur Max se comporte comme le premier, mais ils’applique aux types non numériques.

Deuxième prototype

public static T Max<T>( this IEnumerable<T> source);

Le troisième prototype est dédié aux types numériques. Il implémente une méthode desélection qui permet de limiter la comparaison à un seul membre de chaque élément.

1960

Bullock

Linq.book Page 172 Mercredi, 18. février 2009 7:58 07

Page 188: LINQ Language Integrated Query en C

Chapitre 5 Les opérateurs non différés 173

Troisième prototype

public static Numeric Max<T>( this IEnumerable<T> source, Func<T, Numeric> selector);

Le quatrième prototype est dédié aux types non numériques. Tout comme le précédent,il implémente une méthode de sélection qui permet de limiter la comparaison à un seulmembre de chaque élément.

Quatrième prototype

public static S Max<T, S>( this IEnumerable<T> source, Func<T, S> selector);

ExceptionsL’exception ArgumentNullExpression est levée si un des arguments a pour valeur null.

Si le type T est nullable (int?, long?, double? ou decimal?), les premier et troisièmeprototypes retournent la valeur null si la source est vide. Si le type T n’est pas nullable(int, long, double ou decimal), les premier et troisième prototypes lèvent une exceptionInvalidOperationException si la séquence source est vide.

ExemplesDans l’exemple du Listing 5.57, la plus grande valeur stockée dans un tableau d’entiersest retournée par le premier prototype de l’opérateur Max.

Listing 5.57 : Exemple d’appel du premier prototype.

int[] myInts = new int[] { 974, 2, 7, 1374, 27, 54 }; int minInt = myInts.Max(); Console.WriteLine(minInt);

Voici le résultat retourné :

Pour illustrer le deuxième prototype (voir Listing 5.58), nous appliquerons l’opérateurMax sur le tableau presidents. La valeur retournée sera la "plus grande", alphabétiquementparlant.

Listing 5.58 : Exemple d’appel du deuxième prototype.

string[] presidents = { "Adams", "Arthur", "Buchanan", "Bush", "Carter", "Cleveland", "Clinton", "Coolidge", "Eisenhower", "Fillmore", "Ford", "Garfield", "Grant", "Harding", "Harrison", "Hayes", "Hoover", "Jackson", "Jefferson", "Johnson", "Kennedy", "Lincoln", "Madison", "McKinley", "Monroe", "Nixon", "Pierce", "Polk", "Reagan", "Roosevelt", "Taft", "Taylor", "Truman", "Tyler", "Van Buren", "Washington", "Wilson"};

string minName = presidents.Max();Console.WriteLine(minName);

1374

Linq.book Page 173 Mercredi, 18. février 2009 7:58 07

Page 189: LINQ Language Integrated Query en C

174 LINQ to Objects Partie II

Voici le résultat :

Le résultat est le même que celui qui aurait été renvoyé par l’opérateur Last. Mais ceciest un cas particulier : si les éléments du tableau presidents avaient été classés dans unautre ordre ou de façon aléatoire, le résultat de la fonction Max resterait "Wilson".

Pour illustrer le troisième prototype, nous rechercherons la date de naissance la plusrécente dans la classe Actor (voir Listing 5.59).

Listing 5.59 : Exemple d’appel du troisième prototype.

int oldestActorAge = Actor.GetActors().Max(a => a.birthYear);Console.WriteLine(oldestActorAge);

Voici le résultat :

Pour illustrer le quatrième prototype, nous allons rechercher le "dernier" nom d’acteur(alphabétiquement parlant) dans la classe Actor (voir Listing 5.60).

Listing 5.60 : Exemple d’appel du quatrième prototype.

string firstAlphabetically = Actor.GetActors().Max(a => a.lastName);Console.WriteLine(firstAlphabetically);

Voici le résultat :

Opérateur AverageL’opérateur Average retourne la moyenne des valeurs numériques contenues dans laséquence d’entrée.

PrototypesDeux prototypes de l’opérateur Average seront étudiés dans ce livre.

Premier prototype

public static Result Average(this IEnumerable<Numeric> source);

Numeric doit être choisi parmi les types suivants : int, long, double, décimal ou un deleurs équivalents nullables, int?, long?, double? ou decimal?. Si Numeric est de typeint ou long, Result sera de type double. Si Numeric est de type int? ou long?, Resultsera de type double?. Dans tous les autres cas, Result sera du même type que Numeric.

Wilson

1968

Wilson

Linq.book Page 174 Mercredi, 18. février 2009 7:58 07

Page 190: LINQ Language Integrated Query en C

Chapitre 5 Les opérateurs non différés 175

Le premier prototype énumère la séquence d’entrée source et calcule la moyenne deséléments de type Numeric.

Le second prototype énumère la séquence d’entrée source et calcule la moyenne deséléments de type Numeric désignés par la méthode selector.

Second prototype

public static Result Average<T>( this IEnumerable<T> source, Func<T, Numeric> selector);

ExceptionsL’exception ArgumentNullExpression est levée si un des arguments a pour valeurnull.

L’exception OverflowException est levée si la somme des valeurs dont on calcule lamoyenne dépasse la capacité du type long lorsque Numeric a un type int, int?, longou long?.

ExemplesAfin d’illustrer le premier prototype, nous allons utiliser l’opérateur Range pour créerune séquence d’entiers dont nous calculerons la moyenne (voir Listing 5.61).

Listing 5.61 : Exemple d’appel du premier prototype.

IEnumerable<int> intSequence = Enumerable.Range(1, 10);Console.WriteLine("Séquence d’entiers :");foreach (int i in intSequence) Console.WriteLine(i);

double average = intSequence.Average();Console.WriteLine("Moyenne : {0}", average);

Voici les résultats :

Pour illustrer le second prototype, nous travaillerons avec la classe EmployeeOptio-nEntry (voir Listing 5.62).

Séquence d’entiers :12345678910Moyenne : 5.5

Linq.book Page 175 Mercredi, 18. février 2009 7:58 07

Page 191: LINQ Language Integrated Query en C

176 LINQ to Objects Partie II

Listing 5.62 : Exemple d’appel du second prototype.

IEnumerable<EmployeeOptionEntry> options =EmployeeOptionEntry.GetEmployeeOptionEntries();

Console.WriteLine("Identifiants et options des employés :");

foreach (EmployeeOptionEntry eo in options)Console.WriteLine("Identifiant employé : {0}, Options: {1}", eo.id, ➥eo.optionsCount);

// Calcul de la moyenne des optionsdouble optionAverage = options.Average(o => o.optionsCount);Console.WriteLine("La moyenne des options des employés est : {0}", optionAverage);

Dans un premier temps, l’objet options est défini et initialisé avec la méthode GetOption-Entries(). Les identifiants et options des employés sont ensuite affichés à l’aide d’uneboucle foreach. Enfin, la moyenne des options est calculée avec le second prototype del’opérateur Average, en ne travaillant que sur le membre optionsCount des éléments.Voici les résultats :

Opérateur AggregateL’opérateur Aggregate exécute une fonction spécifiée par l’utilisateur sur chacun deséléments de la séquence d’entrée. Il passe la valeur retournée par la fonction au rangprécédent et retourne la valeur calculée pour le dernier élément.

PrototypesDeux prototypes de l’opérateur Average seront étudiés dans ce livre.

Premier prototype

public static T Aggregate<T>( this IEnumerable<T> source, Func<T, T, T> func);

Dans cette version du prototype, l’opérateur Aggregate énumère les éléments de laséquence d’entrée source. Le délégué func est appelé sur chaque élément. Deux argu-ments lui sont passés : la valeur retournée par la fonction à l’élément précédent etl’élément lui-même. La valeur retournée par func est mémorisée dans une mémoireinterne, afin d’être passée au prochain élément. C’est le premier élément qui est passélors de la première invocation de la méthode func.

Le second prototype est identique au premier mais, ici, la valeur à passer lors de lapremière invocation de la méthode func est spécifiée.

Identifiants et options des employés :Identifiant employé : 1, Options : 2Identifiant employé : 2, Options : 10000Identifiant employé : 2, Options : 10000Identifiant employé : 3, Options : 5000Identifiant employé : 2, Options : 10000Identifiant employé : 3, Options : 7500Identifiant employé : 3, Options : 7500Identifiant employé : 4, Options : 1500Identifiant employé : 101, Options : 2La moyenne des options des employés est : 5722.66666666667

Linq.book Page 176 Mercredi, 18. février 2009 7:58 07

Page 192: LINQ Language Integrated Query en C

Chapitre 5 Les opérateurs non différés 177

Second prototype

public static U Aggregate<T, U>( this IEnumerable<T> source, U seed, Func<U, T, U> func);

ExceptionsL’exception ArgumentNullExpression est levée si un des arguments a pour valeurnull.

L’exception InvalidOperationException est levée dans le premier prototype si laséquence d’entrée est vide.

ExemplesLe Listing 5.63 illustre le premier prototype. Ici, nous calculons la valeur 5! (factorielle5). Ce résultat est obtenu en multipliant entre eux tous les entiers positifs inférieurs ouégaux à 5. La valeur 5! est donc égale à 1 × 2 × 3 × 4 × 5.

Listing 5.63 : Exemple d’appel du premier prototype.

int N = 5;IEnumerable<int> intSequence = Enumerable.Range(1, N);

// Liste des éléments de la séquenceforeach (int item in intSequence) Console.WriteLine(item);

// Calcul et affichage de la factorielle// av == valeur de l’agrégat, e == élémentint agg = intSequence.Aggregate((av, e) => av * e);Console.WriteLine("{0}! = {1}", N, agg);

Ce code génère une séquence d’entiers compris entre 1 et 5 en utilisant l’opérateurRange. Après avoir affiché ces éléments, l’opérateur Aggregate est appelé, en lui four-nissant une expression lambda qui multiplie l’agrégat par l’élément. Voici les résultats :

ATTENTIONATTENTION

Lorsque vous utilisez le premier prototype de l’opérateur Aggregate, vous devez faireattention à ce que le premier élément ne soit pas traité à deux reprises par la méthode func.Dans l’exemple précédent, les paramètres 1 et 1 sont transmis en entrée de la méthodefunc. Cela n’affecte en rien le résultat final, puisque les valeurs sont multipliées entre elles.Le résultat aurait en revanche été faussé si les valeurs avaient été additionnées.

123455! = 120

Linq.book Page 177 Mercredi, 18. février 2009 7:58 07

Page 193: LINQ Language Integrated Query en C

178 LINQ to Objects Partie II

Pour illustrer le second prototype, nous allons utiliser un opérateur Sum "fait maison"(voir Listing 5.64).

Listing 5.64 : Exemple d’appel du second prototype.

IEnumerable<int> intSequence = Enumerable.Range(1, 10);

// Affichage des éléments de la séquenceforeach (int item in intSequence) Console.WriteLine(item);Console.WriteLine("--");

// Calcul et affichage de la sommeint sum = intSequence.Aggregate(0, (s, i) => s + i);Console.WriteLine(sum);

La valeur "0" a été définie comme premier argument de l’opérateur Aggregate afin quele premier appel de la méthode func n’altère pas le résultat final. Voici les résultats affichésdans la console :

Comme vous pouvez le voir, le résultat est le même que celui obtenu pour illustrerl’opérateur Sum, dans le Listing 5.51.

Résumé

Ce chapitre et le précédent vous semblent peut-être quelque peu indigestes. Ils contien-nent cependant les bases de LINQ. J’espère avoir couvert tous les opérateurs qui vousseront utiles. Pour que LINQ révèle toute sa puissance, vous devez bien comprendre cesopérateurs et savoir comment les utiliser. Il n’est pas nécessaire de retenir le détail dechaque opérateur. Sachez juste qu’ils existent et quels services ils peuvent vous rendre.

En se fondant sur ce qui a été vu jusqu’ici à propos de LINQ to Objects et des opéra-teurs de requête standard, vous avez pu voir à quel point LINQ s’est révélé puissant etpratique pour interroger des données de tout type stockées dans des collections enmémoire.

En utilisant les quelque 50 opérateurs de LINQ to Objects, vos requêtes seront pluscohérentes, plus fiables et plus rapides à écrire.

12345678910--55

Linq.book Page 178 Mercredi, 18. février 2009 7:58 07

Page 194: LINQ Language Integrated Query en C

Chapitre 5 Les opérateurs non différés 179

Je n’insisterai jamais assez sur le fait que la plupart des opérateurs de requête standardtravaillent sur des collections qui implémentent l’interface IEnumerable<T>. Aucunedes collections C# héritées (celles de l’espace de noms System.Collection) n’implé-mentent cette interface ; elles sont donc exclues. Je sais pourtant que certains lecteursessayeront (sans succès !) d’appliquer des requêtes LINQ à des ArrayList provenant decode hérité. Si vous vous trouvez dans une telle situation, jetez un œil aux opérateursCast et OfType.

Au chapitre suivant, nous allons nous intéresser à la génération et à l’interrogation deséquences XML. Cette partie de LINQ a pour nom "LINQ to XML".

Linq.book Page 179 Mercredi, 18. février 2009 7:58 07

Page 195: LINQ Language Integrated Query en C

Linq.book Page 180 Mercredi, 18. février 2009 7:58 07

Page 196: LINQ Language Integrated Query en C

III

LINQ to XML

Linq.book Page 181 Mercredi, 18. février 2009 7:58 07

Page 197: LINQ Language Integrated Query en C

Linq.book Page 182 Mercredi, 18. février 2009 7:58 07

Page 198: LINQ Language Integrated Query en C

6

Introduction à LINQ to XML

Ce chapitre aborde la facette LINQ to XML du langage LINQ. En préambule, leListing 6.1 montre comment créer une hiérarchie XML en utilisant l’API MicrosoftDOM (Document Object Model) W3C DOM XML. Il n’est pas nécessaire d’aller bienloin dans le code pour se rendre compte à quel point le processus est douloureux !

Listing 6.1 : Un exemple XML basique.

using System.Xml;

// Déclaration de variablesXmlElement xmlBookParticipant;XmlAttribute xmlParticipantType;XmlElement xmlFirstName;XmlElement xmlLastName;

// Instanciation d’un objet XmlDocumentXmlDocument xmlDoc = new XmlDocument();

// Création de l’élément parent et ajout au documentXmlElement xmlBookParticipants = xmlDoc.CreateElement("BookParticipants");xmlDoc.AppendChild(xmlBookParticipants);

// Création d’un participant et ajout à la liste des participantsxmlBookParticipant = xmlDoc.CreateElement("BookParticipant");xmlParticipantType = xmlDoc.CreateAttribute("type");xmlParticipantType.InnerText = "Author";xmlBookParticipant.Attributes.Append(xmlParticipantType);xmlFirstName = xmlDoc.CreateElement("FirstName");xmlFirstName.InnerText = "Joe";xmlBookParticipant.AppendChild(xmlFirstName);xmlLastName = xmlDoc.CreateElement("LastName");xmlLastName.InnerText = "Rattz";xmlBookParticipant.AppendChild(xmlLastName);xmlBookParticipants.AppendChild(xmlBookParticipant);

// Création d’un participant autre et ajout à la liste des participantsxmlBookParticipant = xmlDoc.CreateElement("BookParticipant");xmlParticipantType = xmlDoc.CreateAttribute("type");xmlParticipantType.InnerText = "Editor";

Linq.book Page 183 Mercredi, 18. février 2009 7:58 07

Page 199: LINQ Language Integrated Query en C

184 LINQ to XML Partie III

xmlBookParticipant.Attributes.Append(xmlParticipantType);xmlFirstName = xmlDoc.CreateElement("FirstName");xmlFirstName.InnerText = "Ewan";xmlBookParticipant.AppendChild(xmlFirstName);xmlLastName = xmlDoc.CreateElement("LastName");xmlLastName.InnerText = "Buckingham";xmlBookParticipant.AppendChild(xmlLastName);xmlBookParticipants.AppendChild(xmlBookParticipant);

// Recherche des auteurs et affichage de leur nomXmlNodeList authorsList =xmlDoc.SelectNodes("BookParticipants/BookParticipant[@type=\"Author\"]");foreach (XmlNode node in authorsList){XmlNode firstName = node.SelectSingleNode("FirstName");XmlNode lastName = node.SelectSingleNode("LastName");Console.WriteLine("{0} {1}", firstName, lastName);}

Ce code construit la hiérarchie XML et affiche le nom de chaque participant.

La structure XML désirée

<BookParticipants> <BookParticipant type="Author"> <FirstName>Joe</FirstName> <LastName>Rattz</LastName> </BookParticipant> <BookParticipant type="Editor"> <FirstName>Ewan</FirstName> <LastName>Buckingham</LastName> </BookParticipant></BookParticipants>

L’écriture, la compréhension et la maintenance de ce code sont un vrai cauchemar ! Parailleurs, il ne suffit pas d’observer son contenu pour en déduire la structure XML géné-rée. Si la méthode DOM est si lourde, c’est en partie parce qu’il n’est pas possible decréer un élément, de l’initialiser et de l’attacher à la hiérarchie en utilisant une seule etmême instruction. Au lieu de cela, trois étapes sont nécessaires : chaque élément doitêtre créé, son membre InnerText, initialisé à la valeur souhaitée puis l’élément ajouté àun nœud de l’arborescence. Cette technique génère beaucoup de code. Sans compterqu’il faut également créer un document XML : sans lui, impossible de créer un simpleélément ! Observez le listing et son résultat. Ne trouvez-vous pas la quantité de codedisproportionnée ?

Appuyez sur Ctrl+F5 pour exécuter ce programme. Voici le résultat affiché dans laconsole :

Les noms et prénoms des participants n’apparaissent pas. Nous allons tenter de modi-fier la ligne Console.WriteLine pour obtenir les données souhaitées :

Console.WriteLine("{0} {1}", firstName.ToString(), lastName.ToString());

System.Xml.XmlElement System.Xml.XmlElement

Linq.book Page 184 Mercredi, 18. février 2009 7:58 07

Page 200: LINQ Language Integrated Query en C

Chapitre 6 Introduction à LINQ to XML 185

Un nouveau Ctrl+F5 produit… le même résultat :

La tentative a échoué !

Introduction

Microsoft aurait pu se contenter de fournir une API LINQ de requêtage XML. Heureu-sement, les développeurs sont allés beaucoup plus loin.

Après plusieurs années d’utilisation de l’API W3C DOM XML, il est apparu clairementqu’une amélioration s’imposait. Plutôt qu’utiliser l’artillerie DOM, n’avez-vous jamaiscréé directement des éléments XML en passant par des chaînes ? Ou été tenté de le faire ?

Plusieurs déficiences de l’API W3C DOM XML ont été examinées et un nouveaumodèle d’objet a été créé. Il en a résulté une méthode bien plus simple et élégante pourcréer des arbres XML : la "construction fonctionnelle". Et, croyez-moi, cette techniquevaut son pesant d’or !

Bien entendu, la nouvelle API se devait de supporter les requêtes LINQ, sans quoi ellen’aurait pas pu faire partie du langage LINQ. Par l’intermédiaire de méthodes d’extension,des opérateurs de requêtes spécifiques XML ont ainsi été implémentés.

En combinant ces opérateurs et les opérateurs de requête standard de LINQ to Objects(voir Chapitre 2), vous aurez à votre disposition tout ce qu’il faut pour manipuler desdonnées XML de façon élégante et efficace.

Se passer de l’API W3C DOM XML

Nous allons raisonner sur un cas pratique : le projet sur lequel j’ai personnellementtravaillé dans la division IT d’une grande entreprise. J’ai dû mettre au point une classepermettant de pister toutes les actions des utilisateurs dans une application ASP.NET.C’est ainsi qu’est née la classe logging. Cette classe avait deux buts : identifier toututilisateur qui abuserait du système et être prévenu par e-mail si une exception avait étélevée. Ce second point se justifiait par le fait que les utilisateurs qui avaient provoquéune exception n’étaient jamais en mesure de m’indiquer clairement dans quelles conditionss’était produit l’incident.

Je voulais donc un procédé capable de traquer les moindres mouvements des utilisa-teurs côté serveur. Toutes les actions entreprises par l’utilisateur (une demande defacture ou la soumission d’une commande, par exemple) devaient être considéréescomme un événement. Chaque événement était mémorisé dans les champs d’une basede données : références de l’utilisateur, date, heure, type de l’événement, etc. Malheu-reusement, ces informations ne me permettaient pas de connaître le détail de chaque

System.Xml.XmlElement System.Xml.XmlElement

Linq.book Page 185 Mercredi, 18. février 2009 7:58 07

Page 201: LINQ Language Integrated Query en C

186 LINQ to XML Partie III

action. Par exemple, pour une commande, j’aurais voulu connaître son numéro et lesdifférents articles commandés. En fait, j’avais besoin de toutes les informations qui mepermettraient de réitérer la situation qui avait déclenché une exception. Chaque événe-ment manipulait des données différentes, mais je ne voulais pas que ces données soientstockées dans des tableaux différents. La solution XML s’imposait d’elle-même.

Par exemple, pour une demande de facture, les données XML pouvaient avoir l’alluresuivante :

<StartDate>10/2/2006</StartDate><EndDate>10/9/2006</EndDate><IncludePaid>False</IncludePaid>

Et, pour une commande :

<PartId>4754611903</PartId><Quantity>12</Quantity><DistributionCenter>Atlanta<DistributionCenter><ShippingCode>USPS First Class<ShippingCode>

Étant donné que les données étaient liées au type des événements, il était impossible deles valider. L’utilisation de l’API XML DOM était donc avantageuse.

Ce gestionnaire d’événements est devenu un outil très utile. Il a permis d’identifier et derésoudre plus facilement les bugs. Il est assez amusant d’appeler un client et de l’infor-mer que l’erreur survenue sur la commande 32728 qu’il a passée la veille est désormaisréparée. Le trouble qui résulte lorsque le client prend conscience qu’il est possible deconnaître le détail de ses actions est une vraie récompense en soi.

Si vous connaissez déjà le XML, vous avez certainement remarqué que ces donnéesn’ont aucun nœud parent. Cela constitue un problème si vous utilisez l’API W3CDOM. Mais, dans mon cas, j’ai utilisé l’API String.Format XML, qui vous est peut-êtreégalement familière. Voici le code utilisé :

string xmlData = string.Format( "<StartDate>{0}</StartDate><EndDate>{1}</EndDate><IncPaid>{2}</IncPaid>", Date.ToShortDateString(), endDate.ToShortDateString(), includePaid.ToString());

Je sais que ce n’est pas la meilleure des façons de définir des données XML et qu’il estfacile de se tromper dans son écriture. Pour faciliter la saisie, j’ai donc créé uneméthode à laquelle je passe en paramètres une liste d’éléments et les données corres-pondantes :

string xmlData = XMLHelper( "StartDate", startDate.ToShortDateString(), "EndDate", endDate.ToShortDateString(), "IncPaid", includePaid.ToString());

La méthode XMLHelper crée également un nœud parent. Les améliorations ne sont pasflagrantes. Comme vous pouvez le voir, je n’ai rien fait pour encoder mes données danscet appel.

Linq.book Page 186 Mercredi, 18. février 2009 7:58 07

Page 202: LINQ Language Integrated Query en C

Chapitre 6 Introduction à LINQ to XML 187

Bien que l’utilisation de la méthode String.Format (ou une autre technique externe àl’API XML DOM) ne soit pas une très bonne alternative, DOM se révèle trop complexelorsqu’il s’agit de manipuler quelques lignes de XML.

Si vous pensez que mon approche est un peu trop personnelle, sachez que, récemment,lors d’un séminaire Microsoft, l’intervenant a présenté un code qui construisait unestructure XML… en concaténant plusieurs chaînes !

Résumé

La plupart des développeurs associent LINQ au requêtage de données, et en particulierde données provenant de bases de données. En tournant les pages de cet ouvrage, vousverrez que LINQ to XML apporte également une vraie réponse quant à la manipulationet à l’interrogation de données XML.

Dans ce chapitre, je vous ai montré à quel point il était douloureux de manipuler duXML via l’API W3C DOM XML. Vous avez également vu qu’il était possible de sepasser de cette API. Au chapitre suivant, nous nous intéresserons à l’API LINQ toXML. Par son intermédiaire, vous apprendrez à créer des hiérarchies XML en quelqueslignes. À titre indicatif, si la hiérarchie créée dans le Listing 6.1 demandait 29 lignes decode, elle sera réduite à 10 lignes seulement en passant par LINQ to XML.

Après avoir lu les deux prochains chapitres, vous serez certainement convaincu del’avancée révolutionnaire de LINQ, tant au niveau de la manipulation du XML qu’àcelui de l’interrogation des bases de données.

Linq.book Page 187 Mercredi, 18. février 2009 7:58 07

Page 203: LINQ Language Integrated Query en C

Linq.book Page 188 Mercredi, 18. février 2009 7:58 07

Page 204: LINQ Language Integrated Query en C

7

L’API LINQ to XML

Au chapitre précédent, vous avez vu à quel point il était difficile de créer un documentXML en utilisant l’API W3C DOM XML. Vous avez également appris à vous passer decette API pour alléger le code. En outre, vous avez pu constater que LINQ sait faireautre chose qu’interroger des collections : il peut également manipuler des hiérarchiesXML, à travers l’API LINQ to XML.

Dans ce chapitre, vous allez découvrir comment utiliser LINQ to XML pour créer,parcourir, manipuler et interroger des documents XML, et effectuer des recherchesdans des objets XML.

Pour illustrer ce chapitre, nous utiliserons une application console. Afin de pouvoir tirerparti de LINQ to XML, vous devez y ajouter une référence vers l’assemblySystem.Xml.Linq, si celle-ci n’est pas déjà présente.

Espaces de noms référencés

Les exemples de ce chapitre vont utiliser les espaces de noms System.Linq,System.Xml.Linq et System.Collections.Generic. Si elles ne sont pas déjà présentes,vous devez donc ajouter les directives using suivantes dans votre code :

using System.Linq;using System.Xml.Linq;using System.Collections.Generic;

Si vous parcourez le code source (www.pearson.fr), vous verrez qu’une directiveusing a également été ajoutée sur l’espace de noms System.Diagnostic. Cette direc-tive n’est pas nécessaire si vous saisissez directement les exemples de ce chapitre. Ellen’est là que pour les besoins propres au code source.

Linq.book Page 189 Mercredi, 18. février 2009 7:58 07

Page 205: LINQ Language Integrated Query en C

190 LINQ to XML Partie III

Améliorations de l’API

Après avoir expérimenté l’API Microsoft W3C XML DOM pendant plusieurs années,des points négatifs et des faiblesses se sont peu à peu dessinés. Pour y remédier, lespoints suivants ont été examinés par les équipes de développement de Microsoft :

m construction d’arbres XML ;

m solutions "centrées-document" ;

m espaces de noms et préfixes ;

m extraction de valeurs de nœuds.

Non contents de grossir et parfois d’obscurcir le code, ces points sont une véritablegêne lorsque l’on travaille avec des données XML. Il était donc important de les exami-ner de près afin que LINQ to XML fonctionne d’une manière irréprochable. Un exem-ple : supposons que vous vouliez utiliser une projection, afin qu’une requête LINQretourne du code XML. L’API XML existante ne permettant pas d’instancier un nouvelélément avec une déclaration new, il fallait corriger cette limitation pour que LINQ toXML manipule des données XML aussi simplement que possible. Dans les pagessuivantes, nous allons passer en revue chacune de ces problématiques et voir commentLINQ to XML les solutionne.

La construction fonctionnelle simplifie la création d’arbres XML

Si vous vous reportez au Listing 6.1, au chapitre précédent, vous verrez qu’il est trèsdifficile d’en tirer un schéma XML. Vous constaterez également que le code est très"verbeux". Après avoir instancié un nouveau document XML, plusieurs nœuds doiventêtre définis. À titre d’exemple, pour ajouter un élément il est nécessaire de le définir, del’initialiser et de le lier avec un élément parent. Ces étapes doivent être répétées autantde fois que nécessaire pour définir toute la structure XML. Un tel procédé rend diffici-lement perceptible le schéma XML et fait exagérément grossir le code. Cette API n’estmalheureusement pas capable de créer un élément (ou un autre type de nœud) en lepositionnant dans l’arbre XML et de l’initialiser par la même occasion.

Cette technique est toujours utilisable dans l’API LINQ to XML, mais une autre, bien plusefficace, connue sous le nom de "construction fonctionnelle", a fait son apparition. Cettetechnique permet de définir le schéma XML pendant les phases de construction et d’initiali-sation des objets XML. Pour ce faire, la nouvelle API fournit des constructeurs d’objetsXML qui acceptent un ou plusieurs objets, accompagnés de leurs valeurs. Le type de l’objetou des objets étant spécifié, il détermine leur point d’appartenance. Voici le modèle général :

XMLOBJECT o = new XMLOBJECT(OBJECTNAME, XMLOBJECT1, XMLOBJECT2, ... XMLOBJECTN);

Linq.book Page 190 Mercredi, 18. février 2009 7:58 07

Page 206: LINQ Language Integrated Query en C

Chapitre 7 L’API LINQ to XML 191

INFO

Le code précédent n’a qu’un objet purement démonstratif. Aucune des classes référencéesdans les arguments n’existe réellement. Elles ne sont là que pour matérialiser des classesXML purement abstraites.

Si vous utilisez la classe LINQ to XML XAttribute pour ajouter un attribut XML à unélément de type XElement, l’attribut devient un attribut de l’élément. Par exemple, dans lecode précédent, si l’attribut XMLOBJECT1 est ajouté à l’élément XMLOBJECT o, si o est unXElement et XMLOBJECT1, un XAttribute, XMLOBJECT1 devient un attribut du XElement o.

Si vous ajoutez un XElement à un XElement, l’élément ajouté devient un enfant del’élément auquel il est ajouté. Par exemple, si XMLOBJECT1 et o sont deux éléments,XMLOBJECT1 devient l’enfant de l’élément o.

Lorsqu’un XMLOBJECT est instancié, son contenu peut être défini par un ou plusieursXMLOBJECT. Comme vous le verrez un peu plus loin, dans la section "Création de textesavec XText", il est également possible de spécifier son contenu en ajoutant une chaîne.Cette dernière sera automatiquement convertie en un XMLOBJECT.

Le Listing 7.1 donne un exemple de création d’un schéma XML.

Listing 7.1 : Utilisation de la construction fonctionnelle pour définir un schéma XML.

XElement xBookParticipant = new XElement("BookParticipant", new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz"));

Console.WriteLine(xBookParticipant.ToString());

Deux objets XElement ont été passés lors de la construction de l’élément BookPartici-pant. Chacun d’eux est donc un enfant de BookParticipant. Notez également que, lorsde la construction des XElement FirstName et LastName, une valeur texte (et non deuxobjets enfants) a été passée. Voici les résultats de ce code :

Le schéma XML apparaît clairement dans le code. Remarquez également à quel point lecode est concis. Le Listing 7.2 représente le code LINQ to XML équivalent au Listing 6.1.

Listing 7.2 : Définition de l’arbre du Listing 6.1, avec un code bien moins important.

XElement xBookParticipants = new XElement("BookParticipants", new XElement("BookParticipant", new XAttribute("type", "Author"), new XElement("FirstName", "Joe"),

<BookParticipant><FirstName>Joe</FirstName><LastName>Rattz</LastName></BookParticipant>

Linq.book Page 191 Mercredi, 18. février 2009 7:58 07

Page 207: LINQ Language Integrated Query en C

192 LINQ to XML Partie III

new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham")));

Console.WriteLine(xBookParticipants.ToString());

Ce code est bien plus concis et facile à maintenir que celui du Listing 6.1. Par ailleurs,la structure des données peut être facilement devinée par simple lecture du code. Voicile résultat :

La nouvelle API a également un autre avantage : les données créées sont formatéescomme un arbre XML traditionnel. Il en va tout autrement de l’arbre créé par le code duListing 6.1 :

Au chapitre suivant, quand nous nous intéresserons aux requêtes LINQ qui produisentdes sorties XML, vous verrez à quel point la construction fonctionnelle est importante.

L’élément, point central d’un objet XML

Avec l’API W3C DOM, il était impossible de définir un élément XML XmlElement sansle rattacher à un document XML XmlDocument. Si vous essayez d’instancier un XmlEle-ment avec cette instruction :

XmlElement xmlBookParticipant = new XmlElement("BookParticipant");

vous obtenez l’erreur de compilation ci-après :

Avec l’API W3C DOM, la seule façon de créer un XmlElement consiste à appeler laméthode CreateElement d’un objet XmlDocument :

XmlDocument xmlDoc = new XmlDocument();XmlElement xmlBookParticipant = xmlDoc.CreateElement("BookParticipant");

Ce code fonctionne à la perfection, mais il n’est pas toujours pratique de devoir créer undocument XML avant de pouvoir définir un élément XML. La nouvelle API LINQ to XMLpermet d’instancier un élément sans le rattacher nécessairement à un document XML.

<BookParticipants> <BookParticipant type="Author"> <FirstName>Joe</FirstName> <LastName>Rattz</LastName> </BookParticipant> <BookParticipant type="Editor"> <FirstName>Ewan</FirstName> <LastName>Buckingham</LastName> </BookParticipant></BookParticipants>

<BookParticipants><BookParticipant type="Author"><FirstName>Joe</FirstName>…

’System.Xml.XmlElement’ ne contient pas un constructeur qui accepte des arguments ’1’

Linq.book Page 192 Mercredi, 18. février 2009 7:58 07

Page 208: LINQ Language Integrated Query en C

Chapitre 7 L’API LINQ to XML 193

XElement xeBookParticipant = new XElement("BookParticipant");

Les éléments XML ne sont pas les seuls nœuds affectés par cette restriction de l’APIX3C DOM. Les attributs, les commentaires, les sections CData, les instructions decalcul et les références d’entités doivent tous être rattachés à un document XML. AvecLINQ to XML, tous ces objets pourront être instanciés à la volée, sans qu’un documentXML n’ait été défini au préalable.

Bien entendu, rien ne vous empêche de créer un document XML avec la nouvelle API.À titre d’exemple, le Listing 7.3 crée un document XML, y ajoute l’élément BookPar-ticipants et insère un élément BookParticipant dans ce dernier.

Listing 7.3 : Création d’un document XML et de sa structure avec l’API LINQ to XML.

XDocument xDocument = new XDocument( new XElement("BookParticipants", new XElement("BookParticipant", new XAttribute("type", "Author"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz"))));

Console.WriteLine(xDocument.ToString());

Voici le résultat affiché dans la console suite à l’appui sur Ctrl+F5 :

Le code XML issu du Listing 7.3 est très proche de celui en sortie du Listing 6.1, à ceciprès qu’un seul participant a été ajouté au document. La construction fonctionnelle lerend cependant bien plus lisible, et il suffit d’observer le code pour en déduire leschéma correspondant. Étant donné qu’il n’est plus nécessaire de définir un documentXML, il est encore possible de simplifier le code (voir Listing 7.4).

Listing 7.4 : Le même exemple que le précédent, mais sans la définition du document XML.

XElement xElement = new XElement("BookParticipants", new XElement("BookParticipant", new XAttribute("type", "Author"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")));

Console.WriteLine(xElement.ToString());

L’exécution de ce code produit le même résultat que précédemment :

<BookParticipants> <BookParticipant type="Author"> <FirstName>Joe</FirstName> <LastName>Rattz</LastName> </BookParticipant></BookParticipants>

<BookParticipants> <BookParticipant type="Author">

Linq.book Page 193 Mercredi, 18. février 2009 7:58 07

Page 209: LINQ Language Integrated Query en C

194 LINQ to XML Partie III

Là ne s’arrêtent pas les prouesses de LINQ to XML. Par son intermédiaire, vous pouvezégalement lire et écrire des données XML dans un fichier.

Noms, espaces de noms et préfixes

Les termes "noms", "espaces de noms" et "préfixes d’espaces de noms" sont souventabscons, sinon difficiles à appréhender pour le programmeur XML. Pour éviter touteconfusion, sachez que les préfixes des espaces de noms sont gérés à l’extérieur del’API. Ils ne font que s’ajouter aux espaces de noms et n’ont aucune existence à l’intérieurde l’API.

Les espaces de noms sont utilisés pour identifier de manière unique le schéma XMLd’une portion d’arbre XML. Une URI peut donc être utilisée pour chaque espace denoms, puisqu’il est unique au sein d’une société. Dans plusieurs exemples, nous utiliseronsl’arbre XML suivant :

<BookParticipants> <BookParticipant type="Author"> <FirstName>Joe</FirstName> <LastName>Rattz</LastName> </BookParticipant></BookParticipants>

Les codes écrits pour traiter ces données XML s’attendront à ce que le nœud BookPar-ticipants contienne plusieurs nœuds BookParticipant, chacun d’entre eux ayant unattribut type et des nœuds FirstName et LastName. Que se passerait-il si ce code devaitégalement traiter des données XML issues d’une autre source, contenant un nœudBookParticipants, mais dont le schéma diffère du précédent ? Eh bien, un espace denoms informerait le code sur la structure du schéma, et le traitement serait alors approprié.

Dans XML, chaque élément a besoin d’un nom. Lorsqu’un élément est créé, si son nomest spécifié dans le constructeur, son type string est implicitement converti en un objetXName. Ce dernier consiste en un espace de noms XNameSpace, l’objet et son nom local(c’est-à-dire le nom que vous avez choisi). À titre d’exemple, l’élément BookPartici-pants pourrait être créé comme suit :

XElement xBookParticipants = new XElement("BookParticipants");

Lorsque l’élément est créé, un objet XName contenant un espace de noms non référencéet le nom local BookParticipants est défini. Si vous utilisez le débogueur sur cetteligne de code et que vous examiniez la variable xBookParticipants dans la fenêtreEspion Express, vous verrez que son membre Name est initialisé à {BookParticipants}.Développez le membre Name. Vous verrez qu’il contient le membre LocalName initialiséà BookParticipants, et un membre NameSpace vide. Ici, l’espace de noms n’a pas étédéfini.

<FirstName>Joe</FirstName> <LastName>Rattz</LastName> </BookParticipant></BookParticipants>

Linq.book Page 194 Mercredi, 18. février 2009 7:58 07

Page 210: LINQ Language Integrated Query en C

Chapitre 7 L’API LINQ to XML 195

Pour spécifier un espace de noms, il vous suffit de créer un objet XNameSpace et del’utiliser comme préfixe du nom local choisi :

XNamespace nameSpace = "http://www.linqdev.com";XElement xBookParticipants = new XElement(nameSpace + "BookParticipants");

Maintenant, lorsque vous visualisez l’élément xBookParticipants dans la fenêtreEspion Express du débogueur, le nom a pour valeur {{http://www.linqdev.com}Book-Participants}. Développez le membre Name. Le membre LocalName a toujours pourvaleur BookParticipants mais, maintenant, le membre Namespace est initialisé à{http://www.linqdev.com}.

Il n’est pas obligatoire d’utiliser un objet NameSpace pour spécifier l’espace de noms.Vous auriez tout aussi bien pu le spécifier dans l’implémentation du XElement :

XElement xBookParticipants = new XElement("{http://www.linqdev.com}" +"BookParticipants");

Des accolades entourent l’espace de noms, afin d’indiquer au constructeur XElementqu’il s’agit d’un espace de noms et pas du nom de l’élément. Si vous examinez ànouveau le membre Name dans la fenêtre Espion Express du débogueur, vous verrez quele membre Name et ses enfants LocalName et NameSpace sont tous initialisés commeauparavant, lorsque l’élément avait été créé avec un objet XNamespace.

Ayez bien en tête qu’il ne suffit pas de définir l’URI de votre société ou de votredomaine pour garantir l’unicité d’un espace de noms. Cela garantit simplement quevous n’entrerez pas en conflit avec d’autres sociétés qui utilisent également les règlesinhérentes aux espaces de noms. Notez cependant qu’à l’intérieur de votre société desconflits entre départements pourraient se produire si la seule URI constitue l’espace denoms. C’est à ce point précis que vous devrez faire intervenir votre connaissance desdivisions, départements et autres sous-structures de votre société. L’idéal serait quel’espace de noms s’étende sur tous les niveaux dont vous avez le contrôle. Supposonspar exemple que vous travailliez chez LINQDev.com et que vous deviez créer unschéma relatif aux retraites pour le département des ressources humaines. L’espace denoms pourrait être le suivant :

XNamespace nameSpace = "http://www.linqdev.com/ressourceshumaimes/retraites";

Pour terminer cette discussion sur le fonctionnement des espaces de noms, nous allonsmodifier le Listing 7.2 en y incluant un espace de noms (voir Listing 7.5).

Listing 7.5 : Une version modifiée du Listing 7.2 incluant un espace de noms.

XNamespace nameSpace = "http://www.linqdev.com";

XElement xBookParticipants = new XElement(nameSpace + "BookParticipants", new XElement(nameSpace + "BookParticipant", new XAttribute("type", "Author"), new XElement(nameSpace + "FirstName", "Joe"), new XElement(nameSpace + "LastName", "Rattz")), new XElement(nameSpace + "BookParticipant", new XAttribute("type", "Editor"),

Linq.book Page 195 Mercredi, 18. février 2009 7:58 07

Page 211: LINQ Language Integrated Query en C

196 LINQ to XML Partie III

new XElement(nameSpace + "FirstName", "Ewan"), new XElement(nameSpace + "LastName", "Buckingham")));

Console.WriteLine(xBookParticipants.ToString());

Appuyez sur Ctrl+F5 pour exécuter ce code. Voici les résultats affichés dans la console :

Si un programme lit ce schéma, il saura qu’il a été émis par LINQDev.com.

Pour isoler le préfixe de l’espace de noms, vous utiliserez l’objet XAttribute, commedans le Listing 7.6.

Listing 7.6 : Définition d’un préfixe dans un espace de noms.

XNamespace nameSpace = "http://www.linqdev.com";

XElement xBookParticipants = new XElement(nameSpace + "BookParticipants", new XAttribute(XNamespace.Xmlns + "linqdev", nameSpace), new XElement(nameSpace + "BookParticipant"));

Console.WriteLine(xBookParticipants.ToString());

Le préfixe utilisé dans ce code est "linqdev". Un objet XAttribute est utilisé pourinclure ce préfixe dans le schéma. Voici les résultats affichés dans la console :

Extraction de valeurs de nœuds

Si vous avez parcouru le chapitre précédent, vous avez certainement été étonné par lesrésultats du Listing 6.1. L’obtention des valeurs issues d’un nœud est un vrai casse-tête ! N’ayant pas travaillé sur du code XML DOM depuis un moment, j’ai inévitablementété confronté à une erreur, en oubliant qu’une étape supplémentaire était nécessairepour extraire les données.

L’API LINQ to XML solutionne ce problème d’une manière élégante. Tout d’abord,l’appel de la méthode ToString sur un élément produit la chaîne XML elle-même, etnon le type de l’objet, comme le fait l’API W3C DOM. Ceci est très pratique lorsquevous voulez obtenir une portion de XML à partir d’un certain point dans l’arbre, et ellea bien plus de sens que de fournir le type de l’objet.

<BookParticipants xmlns="http://www.linqdev.com"> <BookParticipant type="Author"> <FirstName>Joe</FirstName> <LastName>Rattz</LastName> </BookParticipant> <BookParticipant type="Editor"> <FirstName>Ewan</FirstName> <LastName>Buckingham</LastName> </BookParticipant></BookParticipants>

<linqdev:BookParticipants xmlns:linqdev="http://www.linqdev.com"> <linqdev:BookParticipant /></linqdev:BookParticipants>

Linq.book Page 196 Mercredi, 18. février 2009 7:58 07

Page 212: LINQ Language Integrated Query en C

Chapitre 7 L’API LINQ to XML 197

Listing 7.7 : La méthode ToString appliquée à un élément produit l’arbre XML correspondant.

XElement name = new XElement("Name", "Joe");Console.WriteLine(name.ToString());

Voici le résultat obtenu par un appui sur Ctrl+F5 :

Quel changement ! Attendez un peu, la suite est encore plus étonnante. Bien entendu,les nœuds enfants sont inclus dans la sortie mais, étant donné qu’aucune surcharge de laméthode WriteLine n’a été définie pour traiter les XElement, la méthode ToString estautomatiquement appelée, comme dans le Listing 7.8.

Listing 7.8 : Appel implicite de la méthode ToString dans un Console.WriteLine pour obtenir l’arbre XML.

XElement name = new XElement("Person", new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz"));Console.WriteLine(name);

Voici les résultats affichés dans la console :

Encore plus important : si vous utilisez un opérateur de casting sur un nœud pour leconvertir dans un type compatible avec son contenu, vous obtenez la valeur du nœud.Le Listing 7.9 donne un exemple dans lequel la valeur du nœud Name est convertie enune chaîne de caractères.

Listing 7.9 : Le casting d’un élément produit la donnée qui y est stockée.

XElement name = new XElement("Name", "Joe");Console.WriteLine(name);Console.WriteLine((string)name);

Voici les résultats de ce code :

String n’est pas le seul opérateur de casting. Les opérateurs suivants sont également àvotre disposition : int, int?, uint, uint?, long, long?, ulong, ulong?, bool, bool?,float, float?, double, double?, decimal, decimal?, TimeSpan, TimeSpan?, DateTime,DateTime?, GUID et GUID?.

<Name>Joe</Name>

<Person> <FirstName>Joe</FirstName> <LastName>Rattz</LastName></Person>

<Name>Joe</Name>Joe

Linq.book Page 197 Mercredi, 18. février 2009 7:58 07

Page 213: LINQ Language Integrated Query en C

198 LINQ to XML Partie III

Le Listing 7.10 donne un exemple des valeurs stockées dans plusieurs nœuds.

Listing 7.10 : Valeurs stockées dans différents nœuds et récupérées par casting.

XElement count = new XElement("Count", 12);Console.WriteLine(count);Console.WriteLine((int)count);

XElement smoker = new XElement("Smoker", false);Console.WriteLine(smoker);Console.WriteLine((bool)smoker);

XElement pi = new XElement("Pi", 3.1415926535);Console.WriteLine(pi);Console.WriteLine((double)pi);

Voici les résultats :

Cette approche est simple et intuitive. En utilisant l’API LINQ to XML, les difficultésrencontrées dans le Listing 6.1 feront à tout jamais partie du passé !

Dans les exemples étudiés jusqu’ici, les éléments ont été convertis dans leurs typesd’origine. Ceci n’est pas une obligation : il suffit que la conversion soit possible. LeListing 7.11 donne un exemple de casting d’une chaîne de caractères en booléen.

Listing 7.11 : Casting d’un nœud en utilisant un type différent du type d’origine.

XElement smoker = new XElement("Smoker", "true");Console.WriteLine(smoker);Console.WriteLine((bool)smoker);

Étant donné que l’élément a pour valeur la chaîne "true" et que cette chaîne peut êtreconvertie en une valeur booléenne, le code s’exécute sans encombre. Voici les résultats :

Ce code ne laisse pas apparaître le nom de la méthode utilisée pour effectuer le casting.Le Listing 7.12 va vous montrer qu’il s’agit de la méthode System.Xml.XmlConvert.

Listing 7.12 : Le casting booléen utilise la classe System.Xml.XmlConvert.

try{ XElement smoker = new XElement("Smoker", "Tue"); Console.WriteLine(smoker); Console.WriteLine((bool)smoker);}

<Count>12</Count>12<Smoker>false</Smoker>False<Pi>3.1415926535</Pi>3.1415926535

<Smoker>true</Smoker>True

Linq.book Page 198 Mercredi, 18. février 2009 7:58 07

Page 214: LINQ Language Integrated Query en C

Chapitre 7 L’API LINQ to XML 199

catch (Exception ex){ Console.WriteLine(ex);}

La valeur affectée à l’élément Smoker a été intentionnellement mal orthographiée afind’obtenir le nom de la méthode utilisée pour effectuer le casting. Un appui sur Ctrl+F5affiche les informations suivantes dans la console :

Comme vous pouvez le voir, le casting a provoqué une exception lors de l’appel à laméthode System.Xml.XmlConvert.ToBoolean.

Le modèle d’objet LINQ to XML

L’API LINQ to XML vient avec un nouveau modèle d’objet contenant plusieurs classesissues de l’espace de noms System.Xml.Linq. L’une d’entre elles est la classe statiquequi héberge les méthodes d’extension (Extensions). Deux autres sont dédiées auxcomparateurs (XNodeDocumentOrderComparer et XNodeEqualityComparer). Les autresclasses sont utilisées pour construire les arbres XML (voir Figure 7.1).

Quelques remarques intéressantes :

1. Les classes XObject, XContainer et XNode sont abstraites. Elles ne peuvent donc pasêtre construites.

2. Les attributs XAttribute ne sont pas dérivés de nœuds XNode. En fait, il s’agit d’untout autre type de classe, constitué de paires nom/valeur.

3. Les éléments XStreamingElement n’héritent pas de XElement.

4. Les classes XDocument et XElement sont les seules à avoir des nœuds enfants dérivésde XNode.

Vous utiliserez ces classes pour construire vos arbres XML. L’API LINQ to XML étantcentrée sur les éléments, la classe XElement vous sera particulièrement utile.

<Smoker>Tue</Smoker>System.FormatException: The string ’tue’ is not a valid Boolean value.at System.Xml.XmlConvert.ToBoolean(String s)...

Figure 7.1 :

Le modèle d’objet LINQ to XML.

XDocument XCDataXElement

XComment XContainer XDocumentType XProcessingInstruction XText

XAttribute XNode

XStreamingElementXObjectXNamespaceXNameXDeclaration

Der

ived

Linq.book Page 199 Mercredi, 18. février 2009 7:58 07

Page 215: LINQ Language Integrated Query en C

200 LINQ to XML Partie III

Exécution différée des requêtes, suppression de nœuds et bogue d’Halloween

L’exécution de toutes requêtes LINQ est différée. Il peut parfois en découler des effetssecondaires indésirables.

Le "bogue d’Halloween" doit son nom à la première équipe qui en a débattu ouverte-ment. Ces spécialistes d’Halloween ont discuté des problèmes qui découlent du change-ment manuel d’un index dans une boucle. Cette situation a été détectée pour la premièrefois par des ingénieurs bases de données alors qu’ils mettaient au point un processusd’optimisation. Une de leurs requêtes de test a modifié la valeur d’une cellule utiliséecomme index par le processus d’optimisation. Cela a engendré une boucle sans fin dontle processus d’optimisation ne pouvait se dégager.

Vous avez peut-être déjà expérimenté ce problème sans connaître son nom. N’avez-vous jamais effectué une boucle sur une collection dans laquelle la suppression d’unélément entraînait la fin ou le mauvais comportement de la boucle ? J’ai personnelle-ment rencontré ce problème récemment, alors que je travaillais avec des contrôlesserveur ASP.NET. J’ai été amené à supprimer les enregistrements sélectionnés parl’utilisateur dans un contrôle DataGrid. Pour ce faire, j’ai bouclé sur les enregistre-ments, du premier au dernier, en supprimant ceux qui étaient sélectionnés. Ce faisant,les pointeurs utilisés dans la boucle ont été désorganisés. Résultat : certains enregistre-ments ont été supprimés par erreur et d’autres qui auraient dû être supprimés ont étéignorés. Le concepteur des contrôles a trouvé une solution qui consistait à parcourir lesenregistrements du dernier au premier.

Avec LINQ to XML, vous tomberez forcément sur ce type de problème lorsque voussupprimerez des nœuds dans un arbre XML, mais peut-être également dans d’autressituations totalement différentes. Il est donc important d’avoir ce problème à l’espritlorsque vous vous lancerez dans le codage.

Listing 7.13 : Mise en évidence du bogue d’Halloween.

XDocument xDocument = new XDocument( new XElement("BookParticipants", new XElement("BookParticipant", new XAttribute("type", "Author"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham"))));

IEnumerable<XElement> elements = xDocument.Element("BookParticipants").Elements("BookParticipant");

foreach (XElement element in elements){ Console.WriteLine("Elément source: {0} : valeur = {1}", element.Name, element.Value);

Linq.book Page 200 Mercredi, 18. février 2009 7:58 07

Page 216: LINQ Language Integrated Query en C

Chapitre 7 L’API LINQ to XML 201

}

foreach (XElement element in elements){ Console.WriteLine("Suppressionde l’élément {0} = {1} ...", element.Name, ➥element.Value); element.Remove();}

Console.WriteLine(xDocument);

La première ligne définit le document XML. Le bloc d’instructions suivant initialise cedocument avec une séquence d’éléments BookParticipant. La première boucleforeach affiche les deux éléments de la séquence. La boucle suivante énumère ànouveau la séquence et supprime l’élément BookParticipant. Enfin, la dernièreinstruction affiche le document XML résultant.

Si le bogue d’Halloween ne vous saute pas aux yeux, regardez de plus près le message desuppression. Normalement, les deux éléments BookParticipant devraient être suppriméset il devrait en résulter un document XML vide. Et, pourtant, voici le résultat :

Sur les deux éléments SourceParticipant, seul le premier, JoeRattz, est effectivementsupprimé. Le bogue d’Halloween a eu raison de la seconde énumération ! Dans certainscas, ce problème peut se manifester différemment : l’énumération peut se terminerprématurément ou une exception peut être levée.

Vous vous demandez certainement quelle solution peut être apportée à ce problème. Ehbien, dans ce cas précis, la solution consiste à mettre les éléments dans une mémoiretampon et à énumérer cette mémoire plutôt que le document XML, pour lequel lespointeurs internes sont altérés par la suppression. Pour ce faire, nous allons utiliser unopérateur de requête standard spécialement conçu pour mettre des éléments dans unemémoire tampon, afin d’éviter les problèmes liés au côté différé de certaines requêtes.

Listing 7.14 : Une solution au bogue d’Halloween.

XDocument xDocument = new XDocument( new XElement("BookParticipants", new XElement("BookParticipant", new XAttribute("type", "Author"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"),

Elément source : BookParticipant : valeur = JoeRattzElément source : BookParticipant : valeur = EwanBuckinghamSuppression de l’élément BookParticipant = JoeRattz ...<BookParticipants> <BookParticipant type="Editor"> <FirstName>Ewan</FirstName> <LastName>Buckingham</LastName> </BookParticipant></BookParticipants>

Linq.book Page 201 Mercredi, 18. février 2009 7:58 07

Page 217: LINQ Language Integrated Query en C

202 LINQ to XML Partie III

new XElement("LastName", "Buckingham"))));

IEnumerable<XElement> elements = xDocument.Element("BookParticipants").Elements("BookParticipant");

foreach (XElement element in elements){ Console.WriteLine("Elément source : {0} : valeur = {1}", element.Name, element.Value);}

foreach (XElement element in elements.ToArray()){ Console.WriteLine("Suppression de l’élément {0} = {1} ...", element.Name, ➥element.Value); element.Remove();}

Console.WriteLine(xDocument);

Ce code est proche du précédent mais, ici, la suppression se fait en énumérant leséléments via l’opérateur ToArray. Voici le résultat :

Cette fois-ci, deux messages de suppression sont affichés dans la console. Les deuxéléments sont bien supprimés, et le bogue d’Halloween a été éradiqué.

Création XML

Comme il a été dit précédemment, la construction fonctionnelle de l’API LINQ toXML facilite grandement la création d’un arbre XML. Cela vous sera confirmé tout aulong de cette section, qui passe en revue la création des principales classes XML parl’intermédiaire de cette nouvelle API.

Étant donné que les éléments sont le point central de l’API LINQ to XML et que voustravaillerez avec ce type d’objet dans la plupart des cas, nous allons nous intéresserdans un premier temps à la création d’éléments avec la classe XElement. Par la suite, lesautres classes XML seront passées en revue par ordre alphabétique.

Création d’éléments avec XElement

La classe XElement est la plus utilisée dans cette nouvelle API. Nous allons examinerdeux des constructeurs de cette classe :

XElement.XElement(XName name, object content);XElement.XElement(XName name, params object[] content);

Le premier constructeur est le plus simple. Il correspond au cas où un élément a unevaleur texte et aucun nœud enfant (voir Listing 7.15).

Elément source : BookParticipant : valeur = JoeRattzElément source : BookParticipant : valeur = EwanBuckinghamSuppression de l’élément BookParticipant = JoeRattz ...Suppression de l’élément BookParticipant = EwanBuckingham ...<BookParticipants />

Linq.book Page 202 Mercredi, 18. février 2009 7:58 07

Page 218: LINQ Language Integrated Query en C

Chapitre 7 L’API LINQ to XML 203

Listing 7.15 : Création d’un élément avec le premier prototype.

XElement firstName = new XElement("FirstName", "Joe");Console.WriteLine((string)firstName);

Le premier argument du constructeur est un objet XName. Comme il a été dit précédem-ment, cet objet sera créé en convertissant de façon implicite la chaîne passée en entréeen un XName. Le deuxième argument représente la valeur de l’élément ; dans cet exem-ple une chaîne initialisée à "Joe". L’API convertit automatiquement cette chaîne en unobjet XText. La deuxième instruction utilise un opérateur de casting pour obtenir lavaleur de l’élément FirstName. Voici le résultat :

Le choix du type des objets est très flexible. C’est le type d’un objet qui contrôle lesrelations avec l’élément auquel il est ajouté. Le Tableau 7.1 dresse la liste de tousles types de contenus autorisés et indique comment ils sont gérés.

Même si bon nombre d’éléments sont stockés sous la forme de chaînes (c’est par exem-ple le cas des entiers, qui font partie de la catégorie "autres types" du Tableau 7.1), vouspouvez les lire dans leur format d’origine en utilisant les opérateurs de casting appro-priés. Par exemple, en appliquant l’opérateur de casting (int) à un élément, vous obte-nez la valeur entière de cet élément. Tant que vous utilisez un opérateur de castinglicite, le casting est la façon la plus simple d’obtenir la valeur d’un élément, expriméedans son type d’origine.

Le deuxième constructeur XElement est semblable au premier, mais il permet de spéci-fier un contenu composé de plusieurs objets. Reportez-vous aux Listings 7.1 ou 7.2pour avoir un exemple du deuxième constructeur.

Joe

Tableau 7.1 : Comportement de l’insertion d’objets LINQ to XML dans un objet parent

Type de l’objet Gestion

String Un objet string ou une chaîne littérale est automatiquement converti en un objet XText et considéré comme tel.

XText Un tel objet peut avoir une valeur string ou XText. Il est ajouté comme nœud enfant de l’élément, mais considéré comme le contenu texte de l’élément.

XCData Un tel objet peut avoir une valeur string ou XCData. Il est ajouté comme nœud enfant de l’élément, mais considéré comme le contenu CData de l’élément.

XElement Cet objet est ajouté en tant qu’élément enfant.

XAttribute Cet objet est ajouté en tant qu’attribut.

XProcessingInstruction Cet objet est ajouté en tant que contenu enfant.

Linq.book Page 203 Mercredi, 18. février 2009 7:58 07

Page 219: LINQ Language Integrated Query en C

204 LINQ to XML Partie III

Un peu plus tôt dans cette section, nous avons rappelé que la construction fonctionnelleallait être très utile pour définir des requêtes LINQ qui produisent des données XML.Pour illustrer ces propos, nous allons créer l’arbre XML BookParticipants. Plutôtqu’écrire "à la main" les valeurs des éléments, nous allons les récupérer en interrogeantune source de données compatible LINQ. Dans cet exemple, la source de données seraun tableau.

Avant de commencer, nous avons besoin d’une classe pour stocker les données. Étantdonné qu’il existe plusieurs types de BookParticipants, nous allons utiliser un enumpour les recenser.

enum ParticipantTypes{ Author = 0, Editor}

class BookParticipant{ public string FirstName; public string LastName; public ParticipantTypes ParticipantType;}

Nous allons maintenant définir et initialiser un tableau de BookParticipant. L’arbreXML sera alors généré en utilisant une requête LINQ qui extraira les données dutableau (voir Listing 7.16).

Listing 7.16 : Création d’un arbre XML avec une requête LINQ.

BookParticipant[] bookParticipants = new[] { new BookParticipant {FirstName = "Joe", LastName = "Rattz", ParticipantType = ParticipantTypes.Author}, new BookParticipant {FirstName = "Ewan", LastName = "Buckingham", ParticipantType = ParticipantTypes.Editor}};

XElement xBookParticipants = new XElement("BookParticipants", bookParticipants.Select(p =>

XComment Cet objet est ajouté en tant que contenu enfant.

IEnumerable Cet objet est énuméré et la manipulation des types est appliquée de façon récursive.

null Cet objet est ignoré. Comme vous le verrez par la suite, ce type d’objet peut se révéler utile lors de transformations XML.

Autres types La méthode ToString est appelée et la valeur résultante est traitée en tant qu’une chaîne de caractères.

Tableau 7.1 : Comportement de l’insertion d’objets LINQ to XML dans un objet parent (suite)

Type de l’objet Gestion

Linq.book Page 204 Mercredi, 18. février 2009 7:58 07

Page 220: LINQ Language Integrated Query en C

Chapitre 7 L’API LINQ to XML 205

new XElement("BookParticipant", new XAttribute("type", p.ParticipantType), new XElement("FirstName", p.FirstName), new XElement("LastName", p.LastName))));

Console.WriteLine(xBookParticipants);

Le premier bloc de code crée le tableau bookParticipants d’éléments BookPartici-pant. Le deuxième bloc interroge ce tableau en utilisant un opérateur select et génèredes éléments BookParticipant à partir des éléments membres du tableau. Voici l’arbreXML généré :

Pour n’avoir aucun regret, reportez-vous au Listing 6.1 : ce code génère le même arbreen utilisant l’API W3C XML DOM !

Création d’attributs avec XAttribute

Contrairement à ce qui se faisait dans l’API W3C XML DOM, les attributs n’héritentpas des nœuds. Implémentés avec la classe XAttribute, ils consistent en des pairesnom/valeur stockées dans une collection d’objets XAttribute appartenant à un objetXElement.

Grâce à la construction fonctionnelle, un attribut peut être créé et ajouté à un élément àla volée, comme dans le Listing 7.17.

Listing 7.17 : Définition d’un attribut avec la construction fonctionnelle.

XElement xBookParticipant = new XElement("BookParticipant", new XAttribute("type", "Author"));

Console.WriteLine(xBookParticipant);

L’exécution de ce code donne le résultat suivant :

Parfois, il n’est pas possible de créer un attribut pendant la construction de l’élément.Comme le montre le Listing 7.18, ces deux actions peuvent tout aussi bien êtreséparées.

<BookParticipants> <BookParticipant type="Author"> <FirstName>Joe</FirstName> <LastName>Rattz</LastName> </BookParticipant> <BookParticipant type="Editor"> <FirstName>Ewan</FirstName> <LastName>Buckingham</LastName> </BookParticipant></BookParticipants>

<BookParticipant type="Author" />

Linq.book Page 205 Mercredi, 18. février 2009 7:58 07

Page 221: LINQ Language Integrated Query en C

206 LINQ to XML Partie III

Listing 7.18 : La définition de l’élément et l’ajout de son attribut sont dissociés.

XElement xBookParticipant = new XElement("BookParticipant");XAttribute xAttribute = new XAttribute("type", "Author");xBookParticipant.Add(xAttribute);

Console.WriteLine(xBookParticipant);

Le résultat est identique au précédent :

À nouveau, remarquez à quel point la méthode XElement.Add est flexible. Elle acceptetout type d’objet et applique les mêmes règles au contenu de l’élément que lors del’instanciation du XElement.

Création de commentaires avec XComment

La création de commentaires avec LINQ to XML est vraiment simple. La classe utiliséeest XComment. Vous pouvez créer un commentaire et le lier à un élément à la volée, enutilisant la construction fonctionnelle (voir Listing 7.19).

Listing 7.19 : Définition d’un commentaire avec la création fonctionnelle.

XElement xBookParticipant = new XElement("BookParticipant", new XComment("Cette personne est ➥retraitée"));

Console.WriteLine(xBookParticipant);

Voici le résultat affiché dans la console :

Parfois, il n’est pas possible de définir un commentaire lors de la construction del’élément. Si nécessaire, vous pouvez utiliser la méthode Add pour ajouter le commen-taire après que l’élément eut été construit (voir Listing 7.20).

Listing 7.20 : La définition de l’élément et l’ajout du commentaire sont dissociés.

XElement xBookParticipant = new XElement("BookParticipant");XComment xComment = new XComment("Cette personne est retraitée");xBookParticipant.Add(xComment);

Console.WriteLine(xBookParticipant);

Le résultat est identique au précédent :

<BookParticipant type="Author" />

<BookParticipant><!—Cette personne est retraitée--></BookParticipant>

<BookParticipant><!—Cette personne est retraitée--></BookParticipant>

Linq.book Page 206 Mercredi, 18. février 2009 7:58 07

Page 222: LINQ Language Integrated Query en C

Chapitre 7 L’API LINQ to XML 207

Création de conteneurs avec XContainer

XContainer est une classe abstraite. Il n’est donc pas possible de l’instancier. En revan-che, vous pouvez instancier une de ses sous-classes : XDocument ou XElement. La classeXContainer hérite de la classe XNode et peut contenir d’autres classes qui héritent deXNode.

Création de déclarations avec XDeclaration

Grâce à la classe XDeclaration de l’API LINQ to XML, la définition de déclarationsest un jeu d’enfant.

Contrairement à la plupart des autres classes de l’API LINQ to XML, les déclarationss’ajoutent au document XML et non à un élément. Vous rappelez-vous à quel point leconstructeur de la classe XElement était flexible ? Toutes les classes pour lesquelles iln’était pas spécialement destiné déclenchaient l’appel de la méthode ToString et letexte retourné était ajouté à l’élément sous une forme textuelle. Par inadvertance, il estdonc possible d’ajouter une déclaration à un élément en utilisant la classe XDeclaration.Cependant, si cela est permis, le résultat ne sera pas celui escompté.

ATTENTIONATTENTION

Les déclarations XML s’appliquent au document XML. Cependant, il est possible de les appliquerà un XElement, sans toutefois obtenir l’effet recherché.

Il est possible de définir une déclaration à la volée et de l’ajouter à un document XMLen utilisant la construction fonctionnelle (voir Listing 7.21).

Listing 7.21 : Définition d’une déclaration avec la construction fonctionnelle.

XDocument xDocument = new XDocument(new XDeclaration("1.0", "UTF-8", "yes"), new XElement("BookParticipant"));

Console.WriteLine(xDocument);

Voici le résultat :

Comme vous pouvez le voir, la déclaration n’apparaît pas dans la sortie console. Cepen-dant, si vous déboguez le code et affichez la fenêtre Espion Express, vous verrez que ladéclaration est bien là.

Parfois, il n’est pas possible de définir la déclaration lors de la construction du docu-ment. Vous devez alors instancier la déclaration, puis l’affecter à la propriété Declarationdu document (voir Listing 7.22).

<BookParticipant />

Linq.book Page 207 Mercredi, 18. février 2009 7:58 07

Page 223: LINQ Language Integrated Query en C

208 LINQ to XML Partie III

Listing 7.22 : Création d’une déclaration et affectation à la propriété Declaration du document.

XDocument xDocument = new XDocument(new XElement("BookParticipant"));

XDeclaration xDeclaration = new XDeclaration("1.0", "UTF-8", "yes");xDocument.Declaration = xDeclaration;

Console.WriteLine(xDocument);

Voici le résultat :

Tout comme dans l’exemple précédent, la déclaration n’apparaît pas dans la sortieconsole. Cependant, si vous déboguez le code et affichez la fenêtre Espion Express,vous verrez que la déclaration est bien là.

Création de types de documents avec XDocumentType

La classe XDocumentType de l’API LINQ to XML facilite grandement la création detypes de documents (DTD).

Contrairement à la plupart des autres classes de l’API LINQ to XML, les types dedocuments s’ajoutent au document XML et non à un élément. Vous rappelez-vous àquel point le constructeur de la classe XElement était flexible ? Toutes les classes pourlesquelles il n’était pas spécialement destiné déclenchaient l’appel de la méthodeToString et le texte retourné était ajouté à l’élément sous une forme textuelle. Par inad-vertance, il est donc possible d’ajouter une déclaration à un élément en utilisant laclasse XDeclaration. Cela est permis, mais ne donnera pas le résultat escompté.

ATTENTIONATTENTION

Les types de documents XML s’appliquent au document XML. Cependant, il est possible deles appliquer à un XElement, sans toutefois obtenir l’effet recherché.

Il est possible de définir un type de document à la volée et de l’ajouter à un documentXML en utilisant la construction fonctionnelle (voir Listing 7.23).

Listing 7.23 : Définition d’un type de document avec la construction fonctionnelle.

XDocument xDocument = new XDocument(new XDocumentType("BookParticipants", null, "BookParticipants.dtd", null), new XElement("BookParticipant"));

Console.WriteLine(xDocument);

<BookParticipant />

Linq.book Page 208 Mercredi, 18. février 2009 7:58 07

Page 224: LINQ Language Integrated Query en C

Chapitre 7 L’API LINQ to XML 209

Voici le résultat :

Parfois, il n’est pas possible de définir le type de document lors de la construction dudocument. Vous devez alors instancier la définition, puis l’ajouter au document XMLavec la méthode add (voir Listing 7.24).

Listing 7.24 : Création d’un type de document et ajout au document.

XDocument xDocument = new XDocument();

XDocumentType documentType = new XDocumentType("BookParticipants", null, "BookParticipants.dtd", null);

xDocument.Add(documentType, new XElement("BookParticipants"));

Console.WriteLine(xDocument);

Voici le résultat :

Dans ce code, aucun élément n’a été ajouté avant de définir le type du document. Sivous tentez de définir le type du document après avoir ajouté un ou plusieurs éléments,l’exception suivante est levée :

Si vous êtes amené à définir un type de document après l’instanciation du document,assurez-vous qu’aucun élément n’a été spécifié durant l’instanciation du document ouavant la déclaration DTD.

Création de documents avec XDocument

Comme il a été dit précédemment, il n’est pas nécessaire de définir un document XMLpour être en mesure de créer un arbre ou un élément XML. Cependant, si vous êtesamené à le faire, LINQ to XML va vous simplifier grandement la tâche (voirListing 7.25).

Listing 7.25 : Création d’un document XML avec XDocument.

XDocument xDocument = new XDocument();Console.WriteLine(xDocument);

Ce code ne produit aucune sortie, puisque le document est vide.

<!DOCTYPE BookParticipants SYSTEM "BookParticipants.dtd"><BookParticipant />

<!DOCTYPE BookParticipants SYSTEM "BookParticipants.dtd"><BookParticipants />

L’exception InvalidOperationException n’a pas été gérée. Cette opération créerait un document incorrectement structuré.

Linq.book Page 209 Mercredi, 18. février 2009 7:58 07

Page 225: LINQ Language Integrated Query en C

210 LINQ to XML Partie III

Cet exemple étant un peu trop simple, nous allons créer un nouveau document et y ajou-ter toutes les classes LINQ to XML spécifiquement conçues pour être ajoutées à unobjet XDocument (voir Listing 7.26).

Listing 7.26 : Un autre exemple légèrement plus complexe de création d’un document XML avec XDocument.

XDocument xDocument = new XDocument( new XDeclaration("1.0", "UTF-8", "yes"), new XDocumentType("BookParticipants", null, "BookParticipants.dtd", null), new XProcessingInstruction("BookCataloger", "out-of-print"), new XElement("BookParticipants"));

Console.WriteLine(xDocument);

L’instruction de traitement et l’élément auraient pu être ajoutés au niveau élément. Ilsont été placés au niveau du document pour lui donner un peu de consistance.

Voici le résultat :

Vous avez peut-être remarqué que la déclaration n’apparaît pas dans la sortie console.Comme indiqué dans les exemples de la section "Définition de déclarations avecXDeclarations", vous pouvez déboguer le code et afficher une fenêtre Espion Expresspour constater que la déclaration est bien là.

Création de noms avec XName

Comme indiqué un peu plus tôt dans ce chapitre, il n’est pas possible de créer des nomsen utilisant un objet XName. Cette classe n’a en effet aucun constructeur public. Vous nepouvez donc pas l’instancier. Un objet XName sera défini à partir d’une chaîne, éventuel-lement complétée d’un espace de noms, lorsque le code le nécessite.

Un objet XName est constitué d’un nom local (une chaîne) et d’un espace de noms (unXNamespace).

Dans le Listing 7.27, le code appelle le constructeur XElement dont l’argument est unXName.

Listing 7.27 : Dans cet exemple, un objet XName est automatiquement créé.

XElement xBookParticipant = new XElement("BookParticipant");Console.WriteLine(xBookParticipant);

Dans cet exemple, un objet XElement est instancié à partir de son nom au format chaîne.L’objet XName BookParticipant est automatiquement créé et affecté à la propriété Namede l’objet XElement. Ici, aucun espace de noms n’étant spécifié, le XName n’a doncaucun espace de noms.

<!DOCTYPE BookParticipants SYSTEM "BookParticipants.dtd"><?BookCataloger out-of-print?><BookParticipants />

Linq.book Page 210 Mercredi, 18. février 2009 7:58 07

Page 226: LINQ Language Integrated Query en C

Chapitre 7 L’API LINQ to XML 211

Voici le résultat :

Le Listing 7.28 montre comment instancier un XElement en fournissant son nom et unespace de noms.

Listing 7.28 : Dans cet exemple, un objet XName est automatiquement créé, accompagné d’un espace de noms.

XNamespace ns = "http://www.linqdev.com/Books";XElement xBookParticipant = new XElement(ns + "BookParticipant");Console.WriteLine(xBookParticipant);

Ce code produit la sortie XML suivante :

Pour avoir de plus amples informations sur la création de noms en utilisant l’API LINQto XML, reportez-vous à la section intitulée "Noms, espaces de noms et préfixes", unpeu plus tôt dans ce chapitre.

Création d’espaces de noms avec XNamespace

Dans l’API LINQ to XML, les espaces de noms sont implémentés avec la classeXNamespace. Vous trouverez un exemple de création et d’utilisation d’un espace denoms dans le Listing 7.28.

Pour avoir de plus amples informations sur la création de noms en utilisant l’API LINQto XML, reportez-vous à la section intitulée "Noms, espaces de noms et préfixes", unpeu plus tôt dans ce chapitre.

Création de nœuds avec XNode

XNode étant une classe abstraite, il n’est pas possible de l’instancier. Vous pouvez enrevanche instancier une de ses sous-classes : XComment, XContainer, XDocumentType,XProcessingInstruction ou XText. Théoriquement, un XNode est une classe quelconquequi fonctionne comme un nœud dans un arbre XML.

Création d’instructions de traitement avec XProcessingInstruction

La définition d’instructions de traitement n’a jamais été aussi simple qu’avec la classeXProcessingInstruction de l’API LINQ to XML.

Vous pouvez définir des instructions de traitement au niveau document ou élément.Le Listing 7.29 illustre ces deux possibilités en utilisant la construction fonction-nelle.

<BookParticipant />

<BookParticipant xmlns="http://www.linqdev.com/Books" />

Linq.book Page 211 Mercredi, 18. février 2009 7:58 07

Page 227: LINQ Language Integrated Query en C

212 LINQ to XML Partie III

Listing 7.29 : Définition d’une instruction de traitement aux niveaux document et élément.

XDocument xDocument = new XDocument( new XProcessingInstruction("BookCataloger", "out-of-print"), new XElement("BookParticipants", new XElement("BookParticipant", new XProcessingInstruction("ParticipantDeleter", "delete"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz"))));

Console.WriteLine(xDocument);

Avant de donner le résultat de ce code, je veux insister sur la simplicité d’utilisation dela construction fonctionnelle. La comparaison de ce code avec celui du Listing 6.1 metclairement en évidence la supériorité de l’API LINQ to XML par rapport à l’ancienneAPI W3C XML.

Voici les résultats :

Je suppose qu’il ne vous sera pas trop difficile d’imaginer le code permettant d’ajouterune instruction de traitement après la construction du document, puisqu’il s’apparente àcelui permettant d’ajouter un autre type de nœud. Le Listing 7.30 donne un exempleplus complexe de création et d’ajout d’une instruction de traitement a fortiori.

Listing 7.30 : Ajout d’instructions de traitement après la construction du document et de l’élément.

XDocument xDocument = new XDocument(new XElement("BookParticipants", new XElement("BookParticipant", new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz"))));

XProcessingInstruction xPI1 = new XProcessingInstruction("BookCataloger", "out-of-print");xDocument.AddFirst(xPI1);

XProcessingInstruction xPI2 = new XProcessingInstruction("ParticipantDeleter", "delete");XElement outOfPrintParticipant = xDocument .Element("BookParticipants") .Elements("BookParticipant") .Where(e => ((string)((XElement)e).Element("FirstName")) == "Joe" && ((string)((XElement)e).Element("LastName")) == "Rattz") .Single<XElement>();

outOfPrintParticipant.AddFirst(xPI2);

Console.WriteLine(xDocument);

<?BookCataloger out-of-print?><BookParticipants> <BookParticipant> <?ParticipantDeleter delete?> <FirstName>Joe</FirstName> <LastName>Rattz</LastName> </BookParticipant></BookParticipants>

Linq.book Page 212 Mercredi, 18. février 2009 7:58 07

Page 228: LINQ Language Integrated Query en C

Chapitre 7 L’API LINQ to XML 213

Plusieurs passages de ce listing sont dignes d’intérêt. Comme vous pouvez le voir, ledocument et l’arbre XML ont été créés en utilisant la construction fonctionnelle. Uneinstruction de traitement a été ajoutée au document après sa construction. Ici, c’est laméthode XElement.AddFirst qui a été choisie pour créer le premier nœud enfant dudocument (cette méthode a été préférée à XElement.Add, qui ajoute un nœud à la fin desnœuds enfants du document. À cet emplacement, il pourrait être trop tard pour honorerune instruction de traitement).

Pour ajouter une instruction de traitement à un des éléments, nous devons y faire réfé-rence. Nous aurions pu construire un objet XElement et mémoriser sa référence, maisj’ai pensé qu’il était temps d’introduire les possibilités des requêtes LINQ à venir.Comme vous pouvez le voir, la requête utilisée est plutôt complexe. Elle extrait dudocument l’élément BookParticipants en utilisant la méthode Element (voir "Dépla-cements XML", un peu plus loin dans cette section). La séquence d’objets XElementBookParticipant, pour laquelle les éléments FirstName et LastName ont respective-ment pour valeur "Joe" et "Ratz", est alors récupérée. Les valeurs de FirstName etLastName ont été obtenues en utilisant l’opérateur de casting (string).

L’opérateur Where retourne un IEnumerable<T>, alors que nous avons besoin d’unXElement. La réponse retournée par la requête étant unique, nous pouvons utiliserl’opérateur de requête standard différé First de LINQ to Object. Une fois la référenceà l’objet XElement obtenue, il est très simple d’ajouter l’instruction de traitement etd’afficher les résultats.

Voici les résultats affichés dans la console :

Création d’éléments streaming avec XStreamingElement

Dans la deuxième partie de cet ouvrage, nous avons vu que plusieurs des opérateurs derequête standard différaient leur exécution jusqu’à l’énumération des données retour-nées. Si vous utilisez de tels opérateurs tout en voulant obtenir une projection au formatXML, il faudra choisir entre le côté différé des opérateurs de requête standard etl’exécution immédiate d’une requête de projection LINQ to XML.

À titre d’exemple, dans le Listing 7.31, le quatrième élément du tableau names estmodifié et, pourtant, lorsque nous affichons les valeurs de l’objet XElement, l’arbreXML contient les données originales. Ceci vient du fait que l’élément XNames a étéentièrement créé avant que l’élément du tableau names n’ait été modifié.

<?BookCataloger out-of-print?><BookParticipants><BookParticipant><?ParticipantDeleter delete?><FirstName>Joe</FirstName><LastName>Rattz</LastName></BookParticipant></BookParticipants>

Linq.book Page 213 Mercredi, 18. février 2009 7:58 07

Page 229: LINQ Language Integrated Query en C

214 LINQ to XML Partie III

Listing 7.31 : Exécution immédiate de l’arbre XML.

string[] names = { "John", "Paul", "George", "Pete" };

XElement xNames = new XElement("Beatles", from n in names select new XElement("Name", n));

names[3] = "Ringo";

Console.WriteLine(xNames);

Ce code produit l’arbre XML suivant :

Comme vous le voyez, chaque objet XElement de la séquence devient un élément enfantde Beatles. L’élément name[3] a été initialisé à "Ringo" avant d’afficher l’arbre XMLet, pourtant, le dernier élément de cet arbre contient toujours la valeur originale "Pete".Ceci vient du fait que la séquence names doit être énumérée pour pouvoir construirel’objet XElement. La requête est donc exécutée immédiatement.

Si vous voulez que la construction de l’arbre XML soit différée, il faut utiliser deséléments streaming implémentés avec la classe XStreamingElement.

Le Listing 7.32 représente le même exemple, mais cette fois-ci nous utilisons des objetsXStreamingElement à la place des objets XElement.

Listing 7.32 : Exécution différée de la construction de l’arbre XML avec la classe XStreamingElement.

string[] names = { "John", "Paul", "George", "Pete" };

XStreamingElement xNames = new XStreamingElement("Beatles", from n in names select new XStreamingElement("Name", n));

names[3] = "Ringo";

Console.WriteLine(xNames);

Si ce code fonctionne, le dernier nœud Name devrait avoir la valeur "Ringo". Voici lerésultat :

<Beatles><Name>John</Name><Name>Paul</Name><Name>George</Name><Name>Pete</Name></Beatles>

<Beatles> <Name>John</Name> <Name>Paul</Name> <Name>George</Name> <Name>Ringo</Name></Beatles>

Linq.book Page 214 Mercredi, 18. février 2009 7:58 07

Page 230: LINQ Language Integrated Query en C

Chapitre 7 L’API LINQ to XML 215

Création de textes avec XText

Comme le prouve le Listing 7.33, la définition de texte est très simple.

Listing 7.33 : Création d’un élément et affectation d’une valeur chaîne.

XElement xFirstName = new XElement("FirstName", "Joe");

Console.WriteLine(xFirstName);

Voici le résultat :

Une chose n’apparaît pas dans ce listing : la chaîne "Joe" est transformée en un objetXText avant d’être ajoutée à l’objet XElement. En examinant l’objet xFirstName dans ledébogueur, on se rend compte qu’il contient un seul nœud : un objet XText de valeur"Joe". Étant donné que cette conversion est automatique, dans la plupart des cas vousne serez pas obligé de construire un objet texte.

Cependant, si cela est nécessaire, il vous suffira d’instancier un objet XText, commedans le Listing 7.34.

Listing 7.34 : Création d’un nœud texte et utilisation dans l’initialisation d’un XElement.

XText xName = new XText("Joe");

XElement xFirstName = new XElement("FirstName", xName);

Console.WriteLine(xFirstName);

Ce code donne le même résultat que le précédent. Si vous utilisez le débogueur pourexaminer l’état interne de l’objet xFirstName, vous verrez qu’il est identique à celui del’objet créé dans l’exemple précédent :

Définition d’un objet CData avec XCData

Le Listing 7.35 donne un exemple de définition d’un objet CData.

Listing 7.35 : Création d’un nœud CData puis initialisation d’un XElement.

XElement xErrorMessage = new XElement("HTMLMessage", new XCData("<H1>Invalid user id or password.</H1>"));Console.WriteLine(xErrorMessage);

Voici le résultat :

<FirstName>Joe</FirstName>

<FirstName>Joe</FirstName>

<HTMLMessage><![CDATA[<H1>Invalid user id or password.</H1>]]></HTMLMessage>

Linq.book Page 215 Mercredi, 18. février 2009 7:58 07

Page 231: LINQ Language Integrated Query en C

216 LINQ to XML Partie III

Sauvegarde de fichiers XML

La création, la modification et la suppression de données XML n’auraient aucun intérêts’il n’était pas possible de sauvegarder les données. Cette section va vous montrerplusieurs techniques de sauvegarde.

Sauvegardes avec XDocument.Save()

Vous pouvez sauvegarder vos données XML en utilisant un des prototypes de laméthode XDocument.Save :

void XDocument.Save(string filename);void XDocument.Save(TextWriter textWriter);void XDocument.Save(XmlWriter writer);void XDocument.Save(string filename, SaveOptions options);void XDocument.Save(TextWriter textWriter, SaveOptions options);

Le Listing 7.36 donne un exemple de sauvegarde du document XML dans le dossier duprojet.

Listing 7.36 : Sauvegarde d’un document avec la méthode XDocument.Save.

XDocument xDocument = new XDocument( new XElement("BookParticipants", new XElement("BookParticipant", new XAttribute("type", "Author"), new XAttribute("experience", "first-time"), new XAttribute("language", "English"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz"))));

xDocument.Save("bookparticipants.xml");

La méthode Save a été appelée sur un objet de type XDocument. Ceci est possible car lesméthodes Save sont des méthodes d’instances. Comme vous le verrez un peu plus loin,les méthodes Load sont en revanche des méthodes statiques. Elles doivent être appeléessur des classes XDocument ou XElement.

Voici le contenu du fichier bookparticipants.xml, ouvert dans un éditeur de texte tel quele Bloc-notes de Windows.

Ce document XML est facile à lire parce que la version de la méthode Save met enforme les données. Si, en revanche, nous appelions la méthode Save suivante :

xDocument.Save("bookparticipants.xml", SaveOptions.DisableFormatting);

<?xml version="1.0" encoding="utf-8"?><BookParticipants> <BookParticipant type="Author" experience="first-time" language="English"> <FirstName>Joe</FirstName> <LastName>Rattz</LastName> </BookParticipant></BookParticipants>

Linq.book Page 216 Mercredi, 18. février 2009 7:58 07

Page 232: LINQ Language Integrated Query en C

Chapitre 7 L’API LINQ to XML 217

les résultats seraient bien moins lisibles :

Les données sont placées sur une seule et même ligne. Pour vous en assurer, ouvrez lefichier dans un éditeur de texte. Si vous l’ouvriez dans un navigateur Internet, ellesseraient automatiquement mises en forme pour apparaître comme dans le résultat duListing 7.36.

INFO

Passée en deuxième argument de la méthode Save, la valeur SaveOptions.None produit lemême résultat que le Listing 7.36.

Sauvegarde avec XElement.Save

Je l’ai répété plusieurs fois, avec l’API LINQ to XML, il n’est pas nécessaire de créerun document XML. Ceci reste d’actualité quant à la sauvegarde de données XML. Laclasse XElement propose plusieurs méthodes qui abondent dans ce sens :

void XElement.Save(string filename);void XElement.Save(TextWriter textWriter);void XElement.Save(XmlWriter writer);void XElement.Save(string filename, SaveOptions options);void XElement.Save(TextWriter textWriter, SaveOptions options);

Le Listing 7.37 est un exemple très proche du précédent mais, ici, aucun documentXML n’est créé.

Listing 7.37 : Sauvegarde d’un élément avec la méthode XElement.

XElement bookParticipants = new XElement("BookParticipants", new XElement("BookParticipant", new XAttribute("type", "Author"), new XAttribute("experience", "first-time"), new XAttribute("language", "English"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")));

bookParticipants.Save("bookparticipants.xml");

Le résultat est identique au précédent :

<?xml version="1.0" encoding="utf-8"?><BookParticipants><BookParticipant type="Author" experience="first-time" language="English"><FirstName>Joe</FirstName><LastName>Rattz</LastName></BookParticipant></BookParticipants>

<?xml version="1.0" encoding="utf-8"?><BookParticipants> <BookParticipant type="Author" experience="first-time" language="English"> <FirstName>Joe</FirstName> <LastName>Rattz</LastName> </BookParticipant></BookParticipants>

Linq.book Page 217 Mercredi, 18. février 2009 7:58 07

Page 233: LINQ Language Integrated Query en C

218 LINQ to XML Partie III

Lecture de fichiers XML

Cette section passe en revue quelques-unes des techniques qui permettent de lire desdonnées stockées dans un fichier XML.

Lecture avec XDocument.Load()

Voici la liste des méthodes qui vous permettront de lire des données stockées dans unfichier XML :

static XDocument XDocument.Load(string uri);static XDocument XDocument.Load(TextReader textReader);static XDocument XDocument.Load(XmlReader reader);static XDocument XDocument.Load(string uri, LoadOptions options);static XDocument XDocument.Load(TextReader textReader, LoadOptions options);static XDocument XDocument.Load(XmlReader reader, LoadOptions options);

Ces méthodes sont les parfaites répliques des méthodes XDocument.Save. Il existecependant quelques différences qui valent la peine d’être signalées. Tout d’abord, lesméthodes Save étant des méthodes d’instance, elles s’appliquent à un objet XDocumentou XElement. Les méthodes Load étant des méthodes statiques, vous devez appeler laclasse XDocument elle-même. Par ailleurs, les méthodes Save dont le premier paramètreest de type string doivent spécifier le nom du fichier, alors que les méthodes Load dontle premier paramètre est de type string acceptent les URI.

Le Tableau 7.2 dresse la liste des valeurs possibles du paramètre LoadOptions.

Ces options peuvent être combinées en utilisant l’opérateur OR (|). Mais, attention, enfonction du contexte certaines options ne donneront pas les résultats escomptés. Parexemple, lorsqu’un élément ou un document est créé à partir d’une chaîne, aucune ligned’information ni aucun URI ne sont disponibles. De même, lorsqu’un document estcréé à partir d’un XmlReader, aucun URI n’est disponible.

Le Listing 7.38 montre comment lire le document XML créé dans l’exemple précédent.

Tableau 7.2 : Le paramètre LoadOptions.

Valeur Description

LoadOptions.None Aucune option de chargement.

LoadOptions.PreserveWhitespace Conservation des sauts de ligne et autres espaces dans la source XML.

LoadOptions.SetLineInfo Cette option permet d’obtenir la ligne et la position des objets hérités de XObject en utilisant l’interface IXmlLineInfo.

LoadOptions.SetBaseUri Cette option permet d’obtenir l’URI des objets qui héritent de XObject.

Linq.book Page 218 Mercredi, 18. février 2009 7:58 07

Page 234: LINQ Language Integrated Query en C

Chapitre 7 L’API LINQ to XML 219

Listing 7.38 : Lecture d’un document avec la méthode XDocument.Load.

XDocument xDocument = XDocument.Load("bookparticipants.xml", LoadOptions.SetBaseUri | LoadOptions.SetLineInfo);

Console.WriteLine(xDocument);

XElement firstName = xDocument.Descendants("FirstName").First();

Console.WriteLine("FirstName : ligne {0}, position{1}", ((IXmlLineInfo)firstName).LineNumber, ((IXmlLineInfo)firstName).LinePosition);

Console.WriteLine("Adresse URI de l’élément FirstName :{0}", firstName.BaseUri);

INFO

Pour que le type IXmlLineInfo puisse être utilisé, vous devez ajouter une directive usingSystem.xml; ou faire référence à l’espace de noms correspondant.

Ce code charge le fichier XML créé dans l’exemple précédent. Après le chargement etl’affichage du document, nous définissons une référence pour l’élément FirstName etaffichons sa ligne et sa position dans le document XML source. Le code se termine parl’affichage de l’adresse URI de l’élément FirstName.

Voici les résultats :

Le document a bien l’allure souhaitée. Cependant, la ligne de l’élément FirstName n’apas l’air de correspondre. Un rapide coup d’œil au résultat du Listing 7.37 aura tôt fait devous convaincre que cette information est correcte. En effet, la première ligne est réservéeà la déclaration du document, et cette ligne n’apparaît pas dans l’affichage du document :

Lecture avec XElement.Load()

Virtuellement, la lecture d’un élément ou d’un document ne présente aucune différence.Voici les méthodes permettant de lire des données stockées dans un XDocument ou unXElement :

static XElement XElement.Load(string uri);static XElement XElement.LoadTextReader textReader);static XElement XElement.Load(XmlReader reader);

<BookParticipants> <BookParticipant type="Author" experience="first-time" language="English"> <FirstName>Joe</FirstName> <LastName>Rattz</LastName> </BookParticipant></BookParticipants>FirstName : ligne 4, position 6Adresse URI de l’élément FirstName : file:///C:/Documents and Settings/…/Projects/LINQChapter7/LINQChapter7/bin/Debug/bookparticipants.xml

<?xml version="1.0" encoding="utf-8"?>

Linq.book Page 219 Mercredi, 18. février 2009 7:58 07

Page 235: LINQ Language Integrated Query en C

220 LINQ to XML Partie III

static XElement XElement.Load(string uri, LoadOptions options);static XElement XElement.Load(TextReader textReader, LoadOptions options);static XElement XElement.Load(XmlReader reader, LoadOptions options);

Tout comme les méthodes XDocument.Save, ces méthodes sont statiques. Elles doiventdonc être appelées à partir de la classe XElement. Le Listing 7.39 montre comment lireles données XML sauvegardées avec la méthode XElement.Save dans le Listing 7.37.

Listing 7.39 : Lecture d’un document avec la méthode XElement.Load.

XElement xElement = XElement.Load("bookparticipants.xml");Console.WriteLine(xElement);

Les résultats sont bien conformes à nos attentes :

Tout comme pour XDocument.Load, il existe des surcharges de la méthode XEle-ment.Load qui permettent d’utiliser le paramètre LoadOptions. Reportez-vous à lasection intitulée "Lecture avec XDocument.Load()" pour avoir de plus amples informationsà ce sujet.

Extraction avec XDocument.Parse() ou XElement.Parse()

Combien de fois avez-vous extrait des données XML en passant par des chaînes de carac-tères ? Il faut bien avouer que cette tâche n’est pas des plus agréables ! Mais, rassurez-vous, l’API LINQ to XML va apporter une réponse élégante à cette problématique.

La méthode statique Parse est accessible aux classes XDocument et XElement. Par sonintermédiaire, il est possible d’extraire des données XML. Fort de ce qui a été vu dansce chapitre, vous ne devez avoir aucune difficulté à imaginer que, si l’extraction dedonnées est possible depuis la classe XDocument, elle l’est aussi depuis la classe XElement.Nous allons donc raisonner sur un seul exemple relatif à la classe XElement.

Dans la section intitulée "Sauvegardes avec XDocument.Save", vous avez pu voirl’influence du paramètre LoadOptions lorsqu’il est initialisé à DisableFormatting : lesdonnées sont sauvegardées sur une seule et même ligne XML. Le Listing 7.40 utilisecette chaîne XML (en ayant pris le soin d’échapper les guillemets), l’extrait dans unélément et affiche le résultat.

Listing 7.40 : Extraction d’une chaîne XML dans un élément.

string xml = "<?xml version=\"1.0\" encoding=\"utf-8\"?><BookParticipants>" + "<BookParticipant type=\"Author\" experience=\"first-time\" language=" + "\"English\"><FirstName>Joe</FirstName><LastName>Rattz</LastName>" + "</BookParticipant></BookParticipants>";

<BookParticipants> <BookParticipant type="Author" experience="first-time" language="English"> <FirstName>Joe</FirstName> <LastName>Rattz</LastName> </BookParticipant></BookParticipants>

Linq.book Page 220 Mercredi, 18. février 2009 7:58 07

Page 236: LINQ Language Integrated Query en C

Chapitre 7 L’API LINQ to XML 221

XElement xElement = XElement.Parse(xml);Console.WriteLine(xElement);

Voici le résultat :

Impressionnant, n’est-ce pas ? Rappelez-vous les vieux jours où vous deviez créer undocument en utilisant la classe XmlDocument de l’API W3C XML DOM. Le documentn’étant plus l’élément de référence, un simple appel à la méthode Parse suffit désormaispour transformer une chaîne XML en un arbre XML !

Déplacements XML

Les déplacements XML sont effectués par l’intermédiaire de 4 propriétés et de11 méthodes. Dans cette section, nous allons nous efforcer d’utiliser le même code pourchacune des propriétés et des méthodes, en modifiant un simple argument chaque foisque cela sera possible. Le Listing 7.41 est un exemple de construction d’un documentXML complet.

Listing 7.41 : Le code dont seront dérivés les prochains exemples.

// Définition d’une référence vers un des éléments de l’arbre XMLXElement firstParticipant;

XDocument xDocument = new XDocument( new XDeclaration("1.0", "UTF-8", "yes"), new XDocumentType("BookParticipants", null, "BookParticipants.dtd", null), new XProcessingInstruction("BookCataloger", "out-of-print"), // Utilisation de la référence au premier élément BookParticipant new XElement("BookParticipants", firstParticipant = new XElement("BookParticipant", new XAttribute("type", "Author"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham"))));

Console.WriteLine(xDocument);

La première ligne crée une référence au premier élément BookParticipant. Ceci afind’avoir un élément par rapport auquel effectuer le déplacement (la variable firstPar-ticipant n’est pas utilisée dans ce premier exemple, mais elle le sera dans lessuivants).

<BookParticipants> <BookParticipant type="Author" experience="first-time" language="English"> <FirstName>Joe</FirstName> <LastName>Rattz</LastName> </BookParticipant></BookParticipants>

Linq.book Page 221 Mercredi, 18. février 2009 7:58 07

Page 237: LINQ Language Integrated Query en C

222 LINQ to XML Partie III

Le document est passé en argument de la méthode Console.WriteLine. Tout le contenudu document XML sera donc affiché. Dans les prochains exemples, nous choisirons unautre argument pour illustrer les différents types de déplacements.

Voici le résultat :

Propriétés de déplacement

Nous commencerons par les propriétés de déplacement primaires. Lorsqu’une direction(up, down, etc.) est spécifiée, elle est relative à l’élément sur lequel la méthode est appe-lée. Dans les exemples suivants, la référence au premier élément BookParticipant seraprise comme élément de base pour le déplacement.

Nœud suivant avec XNode.NextNodeLa propriété NextNode obtient le nœud frère du nœud courant (voir Listing 7.42).

Listing 7.42 : Obtention du nœud frère suivant d’un objet XElement avec la propriété NextNode.

XElement firstParticipant;

// Le document completXDocument xDocument = new XDocument( new XDeclaration("1.0", "UTF-8", "yes"), new XDocumentType("BookParticipants", null, "BookParticipants.dtd", null), new XProcessingInstruction("BookCataloger", "out-of-print"), // Utilisation de la référence au premier élément BookParticipant new XElement("BookParticipants", firstParticipant = new XElement("BookParticipant", new XAttribute("type", "Author"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham"))));

Console.WriteLine(firstParticipant.NextNode);

<!DOCTYPE BookParticipants SYSTEM "BookParticipants.dtd"><?BookCataloger out-of-print?><BookParticipants> <BookParticipant type="Author"> <FirstName>Joe</FirstName> <LastName>Rattz</LastName> </BookParticipant> <BookParticipant type="Editor"> <FirstName>Ewan</FirstName> <LastName>Buckingham</LastName> </BookParticipant></BookParticipants>

Linq.book Page 222 Mercredi, 18. février 2009 7:58 07

Page 238: LINQ Language Integrated Query en C

Chapitre 7 L’API LINQ to XML 223

L’élément de base étant le premier élément BookParticipant, la propriété NextNodedevrait renvoyer vers le deuxième élément BookParticipant. Voici le résultat :

Nœud précédent avec XNode.PreviousNodeLa propriété PreviousNode donne accès au nœud frère précédent. Pour illustrer cettepropriété, nous allons partir du nœud FirstParticipant. Nous lui appliquerons lapropriété NextNode pour obtenir le nœud frère suivant puis la propriété PreviousNodepour obtenir le nœud frère précédent, c’est-à-dire… le nœud de départ (voirListing 7.43).

Listing 7.43 : Obtention du nœud frère précédent d’un objet XElement avec la propriété PreviousNode.

XElement firstParticipant;

// Le document completXDocument xDocument = new XDocument( new XDeclaration("1.0", "UTF-8", "yes"), new XDocumentType("BookParticipants", null, "BookParticipants.dtd", null), new XProcessingInstruction("BookCataloger", "out-of-print"), // Utilisation de la référence au premier élément BookParticipant new XElement("BookParticipants", firstParticipant = new XElement("BookParticipant", new XAttribute("type", "Author"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham"))));

Console.WriteLine(firstParticipant.NextNode.PreviousNode);

C’est bien le premier élément qui est affiché dans la console :

Remonter au niveau du document avec XObject.DocumentPour remonter au niveau du document à partir d’un XElement quelconque, il suffitd’utiliser la propriété Document (voir Listing 7.44, et en particulier l’appel à la méthodeWriteLine).

<BookParticipant type="Editor"> <FirstName>Ewan</FirstName> <LastName>Buckingham</LastName></BookParticipant>

<BookParticipant type="Author"> <FirstName>Joe</FirstName> <LastName>Rattz</LastName></BookParticipant>

Linq.book Page 223 Mercredi, 18. février 2009 7:58 07

Page 239: LINQ Language Integrated Query en C

224 LINQ to XML Partie III

Listing 7.44 : Accès au document à partir d’un objet XElement en utilisant la propriété Document.

XElement firstParticipant;

// Le document completXDocument xDocument = new XDocument( new XDeclaration("1.0", "UTF-8", "yes"), new XDocumentType("BookParticipants", null, "BookParticipants.dtd", null), new XProcessingInstruction("BookCataloger", "out-of-print"), // Utilisation de la référence au premier élément BookParticipant new XElement("BookParticipants", firstParticipant = new XElement("BookParticipant", new XAttribute("type", "Author"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham"))));

Console.WriteLine(firstParticipant.Document);

Tout comme pour le Listing 7.41, ce code affiche la totalité du document :

Remonter d’un niveau avec XObject.ParentPour obtenir l’élément parent d’un objet XElement, il vous suffit d’utiliser la propriétéParent (voir Listing 7.45, et en particulier l’appel à la méthode WriteLine).

Listing 7.45 : Accès au parent de l’objet firstParticipant en utilisant la propriété Parent.

XElement firstParticipant;

// Le document completXDocument xDocument = new XDocument( new XDeclaration("1.0", "UTF-8", "yes"), new XDocumentType("BookParticipants", null, "BookParticipants.dtd", null), new XProcessingInstruction("BookCataloger", "out-of-print"), // Utilisation de la référence au premier élément BookParticipant new XElement("BookParticipants", firstParticipant = new XElement("BookParticipant", new XAttribute("type", "Author"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")),

<!DOCTYPE BookParticipants SYSTEM "BookParticipants.dtd"><?BookCataloger out-of-print?><BookParticipants> <BookParticipant type="Author"> <FirstName>Joe</FirstName> <LastName>Rattz</LastName> </BookParticipant> <BookParticipant type="Editor"> <FirstName>Ewan</FirstName> <LastName>Buckingham</LastName> </BookParticipant></BookParticipants>

Linq.book Page 224 Mercredi, 18. février 2009 7:58 07

Page 240: LINQ Language Integrated Query en C

Chapitre 7 L’API LINQ to XML 225

new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham"))));

Console.WriteLine(firstParticipant.Parent);

Voici le résultat :

Ne vous laissez pas abuser : il s’agit non pas du document complet, mais du parent de l’objetfirstParticipant. Remarquez l’absence du DTD et de l’instruction de transformation.

Méthodes de déplacement

Étant donné que les méthodes de déplacement retournent des séquences composées deplusieurs nœuds, l’instruction Console.WriteLine va être remplacée par une boucleforeach qui permettra d’afficher les différents nœuds :

foreach(XNode node in firstParticipant.Nodes()){ Console.WriteLine(node);}

Dans les différents exemples, seule différera la méthode appliquée à l’objet firstPar-ticipant dans la boucle foreach.

Nœuds enfants avec XContainer.Nodes()La méthode Nodes() retourne une collection de nœuds enfants XNode de l’élémentspécifié (voir Listing 7.46). À toutes fins utiles, nous rappelons qu’une séquence est unobjet IEnumerable<T>.

Listing 7.46 : Accès aux enfants de l’objet firstParticipant en utilisant la propriété Nodes.

XElement firstParticipant;

// Le document completXDocument xDocument = new XDocument( new XDeclaration("1.0", "UTF-8", "yes"), new XDocumentType("BookParticipants", null, "BookParticipants.dtd", null), new XProcessingInstruction("BookCataloger", "out-of-print"), // Utilisation de la référence au premier élément BookParticipant new XElement("BookParticipants", firstParticipant = new XElement("BookParticipant",

<BookParticipants> <BookParticipant type="Author"> <FirstName>Joe</FirstName> <LastName>Rattz</LastName> </BookParticipant> <BookParticipant type="Editor"> <FirstName>Ewan</FirstName> <LastName>Buckingham</LastName> </BookParticipant></BookParticipants>

Linq.book Page 225 Mercredi, 18. février 2009 7:58 07

Page 241: LINQ Language Integrated Query en C

226 LINQ to XML Partie III

new XAttribute("type", "Author"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham"))));

Console.WriteLine(firstParticipant.Nodes());

Voici le résultat :

Cette méthode retourne les éléments enfants (XElement), mais également les autrestypes de nœuds : commentaires (XComment), texte (XText), instructions de traitement(XProcessingInstruction), type de document (XDocumentType). En revanche, elle neretourne pas les attributs puisque ces derniers ne sont pas des nœuds.

Pour mieux illustrer la méthode Nodes(), plusieurs nœuds enfants ont été ajoutés àl’élément firstParticipant dans le Listing 7.47.

Listing 7.47 : Accès aux différents types d’enfants de l’objet firstParticipant en utilisant la propriété Nodes.

XElement firstParticipant;

// Le document completXDocument xDocument = new XDocument( new XDeclaration("1.0", "UTF-8", "yes"), new XDocumentType("BookParticipants", null, "BookParticipants.dtd", null), new XProcessingInstruction("BookCataloger", "out-of-print"), // Utilisation de la référence au premier élément BookParticipant new XElement("BookParticipants", firstParticipant = new XElement("BookParticipant", new XComment("Nouvel auteur"), new XProcessingInstruction("AuthorHandler", "new"), new XAttribute("type", "Author"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham"))));

foreach (XNode node in firstParticipant.Nodes()){ Console.WriteLine(node);}

Cet exemple est différent du précédent. Ici, l’élément firstParticipant a égalementun enfant de type XComment et un autre de type XProcessingInstruction. Voici lerésultat affiché après l’appui sur Ctrl+F5 :

<FirstName>Joe</FirstName><LastName>Rattz</LastName>

<!--Nouvel auteur--><?AuthorHandler new?>

Linq.book Page 226 Mercredi, 18. février 2009 7:58 07

Page 242: LINQ Language Integrated Query en C

Chapitre 7 L’API LINQ to XML 227

Le commentaire et l’instruction de traitement sont également affichés. Nous allonsmaintenant vous montrer comment limiter la sortie à un seul type de nœud en utilisantl’opérateur OfType (voir Chapitre 4). Le Listing 7.48 ne retourne que les nœuds de typeélément. Il a suffi pour cela de changer l’argument de la boucle foreach.

Listing 7.48 : Utilisation de l’opérateur OfType pour ne retourner que les éléments.

XElement firstParticipant;

// Le document completXDocument xDocument = new XDocument( new XDeclaration("1.0", "UTF-8", "yes"), new XDocumentType("BookParticipants", null, "BookParticipants.dtd", null), new XProcessingInstruction("BookCataloger", "out-of-print"), // Utilisation de la référence au premier élément BookParticipant new XElement("BookParticipants", firstParticipant = new XElement("BookParticipant", new XComment("Nouvel auteur"), new XProcessingInstruction("AuthorHandler", "new"), new XAttribute("type", "Author"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham"))));

foreach (XNode node in firstParticipant.Nodes().OfType<XElement>()){ Console.WriteLine(node);}

Bien que les nœuds de type XComment et XProcessingInstruction soient implémentésdans ce code, ils n’apparaissent pas dans les résultats :

Vous commencez certainement à comprendre à quel point les nouvelles caractéristiquesdu langage C# et le langage LINQ vont faciliter les choses. N’est-il pas intéressant depouvoir utiliser les opérateurs de requête standard pour restreindre les nœuds XMLrenvoyés par une méthode LINQ to XML ?

Supposons maintenant que vous ne vouliez obtenir que les commentaires enfants del’élément firstParticipant. Il vous suffit d’utiliser une autre variante de l’opérateurOfType, comme dans le Listing 7.49.

<FirstName>Joe</FirstName><LastName>Rattz</LastName>

<FirstName>Joe</FirstName><LastName>Rattz</LastName>

Linq.book Page 227 Mercredi, 18. février 2009 7:58 07

Page 243: LINQ Language Integrated Query en C

228 LINQ to XML Partie III

Listing 7.49 : Utilisation de l’opérateur OfType pour ne retourner que les éléments.

XElement firstParticipant;

// Le document completXDocument xDocument = new XDocument( new XDeclaration("1.0", "UTF-8", "yes"), new XDocumentType("BookParticipants", null, "BookParticipants.dtd", null), new XProcessingInstruction("BookCataloger", "out-of-print"), // Utilisation de la référence au premier élément BookParticipant new XElement("BookParticipants", firstParticipant = new XElement("BookParticipant", new XComment("Nouvel auteur"), new XProcessingInstruction("AuthorHandler", "new"), new XAttribute("type", "Author"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham"))));

foreach (XNode node in firstParticipant.Nodes().OfType<XComment>()){ Console.WriteLine(node);}

Voici le résultat :

Que diriez-vous d’utiliser l’opérateur OfType pour limiter la sortie aux attributs ? Ehbien, ceci est tout bonnement impossible puisque, selon l’API LINQ to XML, les attri-buts ne sont pas des nœuds de l’arbre XML. Ils consistent en une séquence de pairesnom/valeur attachée à un élément. Pour obtenir les attributs de l’objet firstParticipant,le code doit être modifié comme dans le Listing 7.50.

Listing 7.50 : Accès aux attributs d’un élément avec la méthode Attributes.

XElement firstParticipant;

// Le document completXDocument xDocument = new XDocument( new XDeclaration("1.0", "UTF-8", "yes"), new XDocumentType("BookParticipants", null, "BookParticipants.dtd", null), new XProcessingInstruction("BookCataloger", "out-of-print"), // Utilisation de la référence au premier élément BookParticipant new XElement("BookParticipants", firstParticipant = new XElement("BookParticipant", new XComment("Nouvel auteur"), new XProcessingInstruction("AuthorHandler", "new"), new XAttribute("type", "Author"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham"))));

<!--Nouvel auteur-->

Linq.book Page 228 Mercredi, 18. février 2009 7:58 07

Page 244: LINQ Language Integrated Query en C

Chapitre 7 L’API LINQ to XML 229

foreach (XAttribute attr in firstParticipant.Attributes()){ Console.WriteLine(attr);}

Comme vous pouvez le voir, nous avons changé l’argument de la boucle foreach, maiségalement le type de la variable d’énumération, puisque XAttribute n’hérite pas deXNode. Voici le résultat :

Nœuds enfants avec XContainer.Elements()L’API LINQ to XML étant centrée sur les éléments, Microsoft a défini la méthodeElements() pour retourner une collection constituée des éléments enfants d’un élément.

Le Listing 7.51 donne un exemple d’utilisation de cette méthode. Tout en utilisant uneautre technique, il est cependant équivalent au Listing 7.48.

Listing 7.51 : Accès aux éléments enfants d’un élément avec la méthode Elements.

XElement firstParticipant;

// Le document completXDocument xDocument = new XDocument( new XDeclaration("1.0", "UTF-8", "yes"), new XDocumentType("BookParticipants", null, "BookParticipants.dtd", null), new XProcessingInstruction("BookCataloger", "out-of-print"), // Utilisation de la référence au premier élément BookParticipant new XElement("BookParticipants", firstParticipant = new XElement("BookParticipant", new XComment("This is a new author."), new XProcessingInstruction("AuthorHandler", "new"), new XAttribute("type", "Author"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham"))));

foreach (XNode node in firstParticipant.Elements()){ Console.WriteLine(node);}

Ce code affiche le même résultat que le Listing 7.48 :

Il existe également une version surchargée de la méthode Elements qui permet depasser le nom de l’élément recherché (voir Listing 7.52).

type="Author"

<FirstName>Joe</FirstName><LastName>Rattz</LastName>

Linq.book Page 229 Mercredi, 18. février 2009 7:58 07

Page 245: LINQ Language Integrated Query en C

230 LINQ to XML Partie III

Listing 7.52 : Accès aux éléments enfants d’un élément nommé avec la méthode XContainer.Elements.

XElement firstParticipant;

// Le document completXDocument xDocument = new XDocument( new XDeclaration("1.0", "UTF-8", "yes"), new XDocumentType("BookParticipants", null, "BookParticipants.dtd", null), new XProcessingInstruction("BookCataloger", "out-of-print"), // Utilisation de la référence au premier élément BookParticipant new XElement("BookParticipants", firstParticipant = new XElement("BookParticipant", new XComment("This is a new author."), new XProcessingInstruction("AuthorHandler", "new"), new XAttribute("type", "Author"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham"))));

foreach (XContainer.Elements(FirstName){ Console.WriteLine(node);}

Voici le résultat :

Premier nœud enfant avec XContainer.Element()La méthode Element retourne le premier élément enfant de l’élément passé en argu-ment. Contrairement à la méthode précédende, c’est non pas une séquence qui estretournée, mais un élément unique (voir Listing 7.53).

Listing 7.53 : Accès au premier élément enfant d’un élément nommé avec la méthode Element.

XElement firstParticipant;

// Le document completXDocument xDocument = new XDocument( new XDeclaration("1.0", "UTF-8", "yes"), new XDocumentType("BookParticipants", null, "BookParticipants.dtd", null), new XProcessingInstruction("BookCataloger", "out-of-print"), // Utilisation de la référence au premier élément BookParticipant new XElement("BookParticipants", firstParticipant = new XElement("BookParticipant", new XComment("This is a new author."), new XProcessingInstruction("AuthorHandler", "new"), new XAttribute("type", "Author"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"),

<FirstName>Joe</FirstName>

Linq.book Page 230 Mercredi, 18. février 2009 7:58 07

Page 246: LINQ Language Integrated Query en C

Chapitre 7 L’API LINQ to XML 231

new XElement("LastName", "Buckingham"))));

Console.WriteLine(firstParticipant.Element("FirstName"));

Voici le résultat :

Ancêtres d’un nœud avec XNode.Ancestors()La propriété Parent permet d’obtenir l’ancêtre direct (le parent) d’un nœud. Si vousdésirez obtenir une séquence contenant tous les ancêtres d’un nœud, jusqu’au niveauhiérarchique le plus élevé, vous utiliserez la méthode Ancestors. Seuls les éléments (etnon tous les nœuds) ancêtres sont retournés.

Pour mieux illustrer cette méthode, nous allons ajouter plusieurs nœuds enfants àl’élément FirstName du premier participant. Par ailleurs, plutôt qu’énumérer les ancê-tres du premier participant, nous utiliserons la méthode Element pour nous déplacer dedeux niveaux hiérarchiques vers le bas afin d’atteindre l’élément NickName. Le nombred’ancêtres sera ainsi plus élevé, ce qui facilitera la compréhension de la méthodeAncestors (voir Listing 7.54).

Listing 7.54 : Ancêtres d’un objet XElement avec la méthode Ancestors.

XElement firstParticipant;

// Le document completXDocument xDocument = new XDocument( new XDeclaration("1.0", "UTF-8", "yes"), new XDocumentType("BookParticipants", null, "BookParticipants.dtd", null), new XProcessingInstruction("BookCataloger", "out-of-print"), // Utilisation de la référence au premier élément BookParticipant new XElement("BookParticipants", firstParticipant = new XElement("BookParticipant", new XComment("This is a new author."), new XProcessingInstruction("AuthorHandler", "new"), new XAttribute("type", "Author"), new XElement("FirstName", new XText("Joe"), new XElement("NickName", "Joey")), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham"))));

foreach (XElement element in firstParticipant. Element("FirstName").Element("NickName").Ancestors()) { Console.WriteLine(element.Name); }

Comme vous pouvez le voir, un objet XText initialisé à "Joe" et un XElement nomméNickName ont été ajoutés à l’élément FirstName. Le dernier bloc d’instructions recher-che les ancêtres de l’élément NickName. La boucle foreach est exécutée au niveau

<FirstName>Joe</FirstName>

Linq.book Page 231 Mercredi, 18. février 2009 7:58 07

Page 247: LINQ Language Integrated Query en C

232 LINQ to XML Partie III

XElement (et non XNode). Ainsi, l’instruction WriteLine peut accéder à la propriétéName des éléments retournés. Plutôt qu’afficher le code XML de chaque élément ancê-tre, nous nous contenterons d’afficher leur nom. Ceci uniquement dans un souci declarté.

Voici les résultats :

Ancêtres d’un nœud avec XElement.AncestorsAndSelf()Cette méthode est comparable à la méthode Ancestors, mais ses résultats incluentl’élément sur lequel s’effectue la recherche. Le Listing 7.55 est le même que le précé-dent, à ceci près que la méthode AncestorsAndSelf remplace la méthode Ancestors.

Listing 7.55 : Ancêtres d’un objet XElement avec la méthode AncestorsAndSelf.

XElement firstParticipant;

// Le document completXDocument xDocument = new XDocument( new XDeclaration("1.0", "UTF-8", "yes"), new XDocumentType("BookParticipants", null, "BookParticipants.dtd", null), new XProcessingInstruction("BookCataloger", "out-of-print"), // Utilisation de la référence au premier élément BookParticipant new XElement("BookParticipants", firstParticipant = new XElement("BookParticipant", new XComment("This is a new author."), new XProcessingInstruction("AuthorHandler", "new"), new XAttribute("type", "Author"), new XElement("FirstName", new XText("Joe"), new XElement("NickName", "Joey")), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham"))));

foreach (XElement element in firstParticipant. Element("FirstName").Element("NickName").AncestorsAndSelf()) { Console.WriteLine(element.Name); }

Les résultats sont identiques à ceux du listing précédent mais, cette fois, ils incluentl’élément NickName :

FirstNameBookParticipantBookParticipants

NickNameFirstNameBookParticipantBookParticipants

Linq.book Page 232 Mercredi, 18. février 2009 7:58 07

Page 248: LINQ Language Integrated Query en C

Chapitre 7 L’API LINQ to XML 233

Descendants d’un nœud avec XContainer.Descendants()Pour obtenir une séquence contenant tous les éléments descendant d’un nœud, vousutiliserez la méthode Descendants. Vous pouvez également utiliser la méthode Descen-dantNodes pour obtenir tous les nœuds descendant d’un autre nœud. Le Listing 7.56 estle même que le précédent mais, ici, c’est la méthode Descendants qui est appelée.

Listing 7.56 : Descendants d’un objet XElement avec la méthode Descendants.

XElement firstParticipant;

// Le document completXDocument xDocument = new XDocument( new XDeclaration("1.0", "UTF-8", "yes"), new XDocumentType("BookParticipants", null, "BookParticipants.dtd", null), new XProcessingInstruction("BookCataloger", "out-of-print"), // Utilisation de la référence au premier élément BookParticipant new XElement("BookParticipants", firstParticipant = new XElement("BookParticipant", new XComment("This is a new author."), new XProcessingInstruction("AuthorHandler", "new"), new XAttribute("type", "Author"), new XElement("FirstName", new XText("Joe"), new XElement("NickName", "Joey")), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham"))));

foreach (XElement element in firstParticipant.Descendants()) { Console.WriteLine(element.Name); }

Voici les résultats :

Tous les éléments qui descendent de l’élément firstParticipant, mais pas les autrestypes de nœuds, sont bien listés dans la console.

Descendants d’un nœud avec XElement.DescendantsAndSelf()DescendantsAndSelf est le pendant de AncestorsAndSelf. Cette méthode renvoie lesdescendants de l’élément sur lequel porte la requête, en y incluant cet élément. LeListing 7.57 est le même que le précédent, à ceci près que la méthode DescendantsAnd-Self remplace la méthode Descendants.

Listing 7.57 : Descendants d’un objet XElement avec la méthode DescendantsAndSelf.

XElement firstParticipant;

// Le document complet

FirstNameNickNameLastName

Linq.book Page 233 Mercredi, 18. février 2009 7:58 07

Page 249: LINQ Language Integrated Query en C

234 LINQ to XML Partie III

XDocument xDocument = new XDocument( new XDeclaration("1.0", "UTF-8", "yes"), new XDocumentType("BookParticipants", null, "BookParticipants.dtd", null), new XProcessingInstruction("BookCataloger", "out-of-print"), // Utilisation de la référence au premier élément BookParticipant new XElement("BookParticipants", firstParticipant = new XElement("BookParticipant", new XComment("This is a new author."), new XProcessingInstruction("AuthorHandler", "new"), new XAttribute("type", "Author"), new XElement("FirstName", new XText("Joe"), new XElement("NickName", "Joey")), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham"))));

foreach (XElement element in firstParticipant.DescendantsAndSelf()) { Console.WriteLine(element.Name); }

Les résultats incluent désormais le nom de l’élément firstParticipant :

Nœuds frères suivants avec XNode.NodesAfterSelf()Pour illustrer cet exemple, deux commentaires ont été ajoutés à l’élément BookParti-cipants. Les commentaires XComment étant des nœuds et non des éléments, les résultatsmettront en évidence que la méthode NodesAfterSelf retourne tous les types de nœudsfrères du nœud ciblé (voir Listing 7.58).

Listing 7.58 : Nœuds frères d’un objet XNode avec la méthode NodesAfterSelf.

XElement firstParticipant;

// Le document completXDocument xDocument = new XDocument( new XDeclaration("1.0", "UTF-8", "yes"), new XDocumentType("BookParticipants", null, "BookParticipants.dtd", null), new XProcessingInstruction("BookCataloger", "out-of-print"), // Utilisation de la référence au premier élément BookParticipant new XElement("BookParticipants", new XComment("Début de la liste"), firstParticipant = new XElement("BookParticipant", new XAttribute("type", "Author"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham")), new XComment("Fin de la liste")));

BookParticipantFirstNameNickNameLastName

Linq.book Page 234 Mercredi, 18. février 2009 7:58 07

Page 250: LINQ Language Integrated Query en C

Chapitre 7 L’API LINQ to XML 235

foreach (XNode node in firstParticipant.NodesAfterSelf()){ Console.WriteLine(node);}

Les nœuds ajoutés sont tous deux frères des deux éléments BookParticipant. Cettemodification du document XML concerne les exemples des méthodes NodesAfterSelf,ElementsAfterSelf, NodesBeforeSelf et ElementsBeforeSelf.

Tous les nœuds frères situés après le premier nœud BookParticipant sont énumérés.Voici le résultat :

Comme vous le voyez, le dernier commentaire est inclus dans le résultat. C’est en effetun nœud frère du nœud situé après le premier BookParticipant. Il se trouve au mêmeniveau hiérarchique que les éléments BookParticipant. Si les éléments FirstName etLastName sont affichés dans les résultats, c’est parce que la méthode ToString estappliquée au nœud BookParticipant.

Cette méthode ne se limite pas aux éléments. Elle retourne également les autres typesde nœuds. Si vous voulez filtrer les nœuds retournés à un certain type, utilisez l’opéra-teur TypeOf. Si ce ne sont que les éléments qui vous intéressent, utilisez la méthodeElementsAfterSelf (voir section suivante).

Éléments frères suivants avec XNode.ElementsAfterSelf()Nous utiliserons le même document XML que dans l’exemple précédent. Pour ne rete-nir que les éléments frères qui suivent le nœud référencé, la méthode ElementsAfter-Self est appelée (voir Listing 7.59).

Listing 7.59 : Éléments frères qui suivent le nœud référencé avec la méthode ElementsAfterSelf.

XElement firstParticipant;

// Le document completXDocument xDocument = new XDocument( new XDeclaration("1.0", "UTF-8", "yes"), new XDocumentType("BookParticipants", null, "BookParticipants.dtd", null), new XProcessingInstruction("BookCataloger", "out-of-print"), // Utilisation de la référence au premier élément BookParticipant new XElement("BookParticipants", new XComment("Début de la liste"), firstParticipant = new XElement("BookParticipant", new XAttribute("type", "Author"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"),

<BookParticipant type="Editor"> <FirstName>Ewan</FirstName> <LastName>Buckingham</LastName></BookParticipant><!—Fin de la liste-->

Linq.book Page 235 Mercredi, 18. février 2009 7:58 07

Page 251: LINQ Language Integrated Query en C

236 LINQ to XML Partie III

new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham")), new XComment("Fin de la liste")));

foreach (XNode node in firstParticipant.ElementsAfterSelf()){ Console.WriteLine(node);}

Voici le résultat :

Cette fois-ci, étant donné que le commentaire n’est pas un élément, il est exclu du résul-tat. Nous rappelons que les éléments FirstName et LastName sont affichés dans lesrésultats car la méthode ToString est appliquée au nœud BookParticipant.

Nœuds frères précédents avec XNode.NodesBeforeSelf()Cet exemple utilise le même document XML que le Listing 7.58. NodesBeforeSelf secomporte comme NodesAfterSelf, si ce n’est qu’elle retourne les nœuds frères quiprécèdent le nœud référencé. Dans cet exemple, nous invoquons la méthode NextNodeavant d’appeler NodesBeforeSelf pour que le résultat ne soit pas vide (voirListing 7.60).

Listing 7.60 : Nœuds frères qui précèdent le nœud référencé avec la méthode NodesBeforeSelf.

XElement firstParticipant;

// Le document completXDocument xDocument = new XDocument( new XDeclaration("1.0", "UTF-8", "yes"), new XDocumentType("BookParticipants", null, "BookParticipants.dtd", null), new XProcessingInstruction("BookCataloger", "out-of-print"), // Utilisation de la référence au premier élément BookParticipant new XElement("BookParticipants", new XComment("Début de la liste"), firstParticipant = new XElement("BookParticipant", new XAttribute("type", "Author"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham")), new XComment("Fin de la liste")));

foreach (XNode node in firstParticipant.NextNode.NodesBeforeSelf()){ Console.WriteLine(node);}

<BookParticipant type="Editor"> <FirstName>Ewan</FirstName> <LastName>Buckingham</LastName></BookParticipant>

Linq.book Page 236 Mercredi, 18. février 2009 7:58 07

Page 252: LINQ Language Integrated Query en C

Chapitre 7 L’API LINQ to XML 237

La méthode NextNode donne accès au deuxième participant. En lui appliquant laméthode ElementsBeforeSelf, les éléments frères qui précèdent le deuxième partici-pant sont listés. Ici, le premier participant. Voici le résultat :

Comme vous pouvez le voir, les nœuds frères sont listés dans l’ordre du document. Onaurait pu s’attendre à ce que les nœuds soient listés depuis le nœud courant vers le débutdu document. Nous aurions alors appelé l’opérateur Reverse ou InDocumentOrder (voirchapitre suivant) pour rétablir l’ordre adéquat. Mais il n’en est rien. Une fois encore, nesoyez pas perturbé si les éléments FirstName et LastName font partie des résultats. Ils nesont pas retournés par la méthode NodesBeforeSelf, mais proviennent de la méthodeToString, appliquée au nœud BookParticipant par la méthode Console.WriteLine.

Éléments frères précédents avec XNode.ElementsBeforeSelf()Cet exemple utilise le même document XML que le Listing 7.58. ElementsBeforeSelfse comporte comme ElementsAfterSelf, si ce n’est qu’elle retourne les élémentsfrères qui précèdent le nœud référencé. Dans cet exemple, nous invoquons la méthodeNextNode avant d’appeler NodesBeforeSelf pour que le résultat ne soit pas vide (voirListing 7.61).

Listing 7.61 : Éléments frères qui précèdent le nœud référencé avec la méthode ElementsBeforeSelf.

XElement firstParticipant;

// Le document completXDocument xDocument = new XDocument( new XDeclaration("1.0", "UTF-8", "yes"), new XDocumentType("BookParticipants", null, "BookParticipants.dtd", null), new XProcessingInstruction("BookCataloger", "out-of-print"), // Utilisation de la référence au premier élément BookParticipant new XElement("BookParticipants", new XComment("Début de la liste"), firstParticipant = new XElement("BookParticipant", new XAttribute("type", "Author"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham")), new XComment("Fin de la liste")));

foreach (XNode node in firstParticipant.NextNode.ElementsBeforeSelf()){ Console.WriteLine(node);}

<!--Début de la liste--><BookParticipant type="Author"> <FirstName>Joe</FirstName> <LastName>Ratz</LastName></BookParticipant>

Linq.book Page 237 Mercredi, 18. février 2009 7:58 07

Page 253: LINQ Language Integrated Query en C

238 LINQ to XML Partie III

La méthode NextNode donne accès au deuxième participant. En lui appliquant laméthode ElementsBeforeSelf, les éléments frères qui précèdent le deuxième partici-pant sont listés : ici, le premier participant. Bien entendu, le commentaire n’est pas affi-ché puisqu’il ne s’agit pas d’un élément :

Modification de données XML

Avec l’API LINQ to XML, la modification de données XML est un vrai jeu d’enfant : ilsuffit d’utiliser les méthodes dédiées pour ajouter, modifier ou supprimer les nœuds oules éléments de votre choix.

Comme il a été dit auparavant, LINQ to XML travaille essentiellement avec des objetsde type XElement. C’est la raison pour laquelle la plupart des exemples qui vont suivreconcerneront ce type d’objet. Nous nous intéresserons aux classes qui héritent deXNode, puis aux attributs.

Ajout de nœuds

Les différentes méthodes étudiées dans cette section utiliseront l’arbre défini dans leListing 7.62.

Listing 7.62 : L’arbre de base contient un seul participant.

// Un document incluant un seul participantXDocument xDocument = new XDocument( new XElement("BookParticipants", new XElement("BookParticipant", new XAttribute("type", "Author"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz"))));

Console.WriteLine(xDocument);

Ce code définit un arbre XML contenant un seul participant :

<BookParticipants> <BookParticipant type="Author"> <FirstName>Joe</FirstName> <LastName>Rattz</LastName> </BookParticipant></BookParticipants>

INFO

Tous les exemples de cette section sont également utilisables avec les classes LINQ to XMLqui héritent de la classe XNode.

<BookParticipant type="Author"> <FirstName>Joe</FirstName> <LastName>Ratz</LastName></BookParticipant>

Linq.book Page 238 Mercredi, 18. février 2009 7:58 07

Page 254: LINQ Language Integrated Query en C

Chapitre 7 L’API LINQ to XML 239

En complément des méthodes passées en revue dans cette section, vous pouvez égale-ment vous reporter à la section intitulée "XElement.SetElementValue() sur des objetsenfants de XElement", un peu plus loin dans ce chapitre.

XContainer.Add()

Pour ajouter des nœuds à un arbre XML, vous utiliserez essentiellement cette méthode.Elle ajoute un nœud après le dernier nœud enfant du nœud spécifié (voir Listing 7.63).

Listing 7.63 : Ajout d’un nœud après le dernier nœud enfant du nœud spécifié avec Add.

XDocument xDocument = new XDocument( new XElement("BookParticipants", new XElement("BookParticipant", new XAttribute("type", "Author"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")));

xDocument.Element("BookParticipants").Add( new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham")));

Console.WriteLine(xDocument);

Le code en gras a été ajouté au Listing 7.62 pour insérer un élément BookParticipantaux éléments BookParticipant déjà existants. Appliquée au document, la méthodeElement renvoie l’élément BookParticipants. Il suffit alors d’utiliser la méthode Addpour lui ajouter un élément BookParticipant. Voici le résultat :

La méthode Add a ajouté un nouvel élément BookParticipant à la fin des nœudsenfants de l’élément BookParticipants. Elle est aussi flexible que le constructeurXElement et autorise la construction fonctionnelle.

XContainer.AddFirst()

Pour ajouter un nœud en première position des nœuds enfants du nœud spécifié, vousutiliserez la méthode AddFirst. Le code utilisé est le même que dans l’exemple précédentmais, ici, la méthode appelée est AddFirst (voir Listing 7.64).

<BookParticipants> <BookParticipant type="Author"> <FirstName>Joe</FirstName> <LastName>Rattz</LastName> </BookParticipant> <BookParticipant type="Editor"> <FirstName>Ewan</FirstName> <LastName>Buckingham</LastName> </BookParticipant></BookParticipants>

Linq.book Page 239 Mercredi, 18. février 2009 7:58 07

Page 255: LINQ Language Integrated Query en C

240 LINQ to XML Partie III

Listing 7.64 : Ajout d’un nœud avant le nœud enfant du nœud spécifié avec AddFirst.

XDocument xDocument = new XDocument( new XElement("BookParticipants", new XElement("BookParticipant", new XAttribute("type", "Author"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz"))));

xDocument.Element("BookParticipants").AddFirst( new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham")));

Console.WriteLine(xDocument);

Comme on pouvait s’y attendre, le nouvel élément BookParticipant est ajouté devantles nœuds enfants de l’élément BookParticipants :

XNode.AddBeforeSelf()

Pour insérer un nœud à un emplacement bien défini dans une liste de nœuds enfants,vous devez obtenir la référence du nœud devant lequel ou après lequel doit se fairel’insertion, puis appeler la méthode AddBeforeSelf ou AddAfterSelf.

Nous utiliserons l’arbre XML du Listing 7.63 comme point de départ, et nous ajoute-rons un nouveau nœud entre les deux éléments BookParticipant existants. Pour cefaire, il est nécessaire d’obtenir la référence du deuxième élément BookParticipant,comme illustré dans le Listing 7.65.

Listing 7.65 : Ajout d’un nœud à l’emplacement spécifié avec AddBeforeSelf.

XDocument xDocument = new XDocument( new XElement("BookParticipants", new XElement("BookParticipant", new XAttribute("type", "Author"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz"))));

xDocument.Element("BookParticipants").Add( new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham")));

<BookParticipants> <BookParticipant type="Editor"> <FirstName>Ewan</FirstName> <LastName>Buckingham</LastName> </BookParticipant> <BookParticipant type="Author"> <FirstName>Joe</FirstName> <LastName>Rattz</LastName> </BookParticipant></BookParticipants>

Linq.book Page 240 Mercredi, 18. février 2009 7:58 07

Page 256: LINQ Language Integrated Query en C

Chapitre 7 L’API LINQ to XML 241

xDocument.Element("BookParticipants"). Elements("BookParticipant"). Where(e => ((string)e.Element("FirstName")) == "Ewan"). Single<XElement>().AddBeforeSelf( new XElement("BookParticipant", new XAttribute("type", "Technical Reviewer"), new XElement("FirstName", "Fabio"), new XElement("LastName", "Ferracchiati")));

Console.WriteLine(xDocument);

Nous allons définir la référence à l’élément BookParticipant en utilisant un opérateurLINQ. Cela nous permettra de faire un rappel sur les opérateurs de requête standardintroduits au Chapitre 2 et de les utiliser.

Dans la première ligne en gras, la méthode Element, appliquée à l’élément BookParti-cipants, permet d’accéder aux éléments qui la constituent. Les trois lignes suivantessélectionnent l’élément BookParticipant dont l’élément enfant FirstName vaut"Ewan". Un seul élément satisfaisant ce critère et étant donné que le nouvel élément doitêtre inséré avant l’élément courant, nous utilisons la méthode AddBeforeSelf. L’opéra-teur Single retourne l’objet XElement BookParticipant. C’est la référence utiliséepour insérer le nouveau XElement.

Dans l’opérateur Where, l’élément FirstName est converti en une chaîne. La fonctionna-lité d’extraction de valeur de LINQ sera ainsi mise à contribution pour comparer lavaleur de l’élément à la chaîne "Ewan".

Une fois la référence à l’élément BookParticipant obtenue, il ne reste plus qu’à appeler laméthode AddBeforeSelf pour effectuer l’insertion. Voici les résultats :

Le nouvel élément BookParticipant a bien été inséré avant l’élément BookPartici-pant dont l’élément FirstName vaut "Ewan".

XNode.AddAfterSelf()

Dans l’exemple précédent, nous utilisions toute une gymnastique pour accéder ausecond élément BookParticipant. Ici, nous nous contenterons d’obtenir une référence

<BookParticipants> <BookParticipant type="Author"> <FirstName>Joe</FirstName> <LastName>Rattz</LastName> </BookParticipant> <BookParticipant type="Technical Reviewer"> <FirstName>Fabio</FirstName> <LastName>Ferracchiati</LastName> </BookParticipant> <BookParticipant type="Editor"> <FirstName>Ewan</FirstName> <LastName>Buckingham</LastName> </BookParticipant></BookParticipants>

Linq.book Page 241 Mercredi, 18. février 2009 7:58 07

Page 257: LINQ Language Integrated Query en C

242 LINQ to XML Partie III

au premier élément BookParticipant en utilisant la méthode Element et de la fairesuivre d’un nouvel élément BookParticipant en utilisant la méthode AddAfterSelf.

Listing 7.66 : Ajout d’un nœud à l’emplacement spécifié avec AddBeforeSelf.

XDocument xDocument = new XDocument( new XElement("BookParticipants", new XElement("BookParticipant", new XAttribute("type", "Author"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz"))));

xDocument.Element("BookParticipants").Add( new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham")));

xDocument.Element("BookParticipants"). Element("BookParticipant").AddAfterSelf( new XElement("BookParticipant", new XAttribute("type", "Technical Reviewer"), new XElement("FirstName", "Fabio"), new XElement("LastName", "Ferracchiati")));

Console.WriteLine(xDocument);

Voici le résultat :

Suppression de nœuds

Deux méthodes permettent de supprimer des nœuds : Remove et RemoveAll.

En complément des méthodes passées en revue dans cette section, vous pouvez égale-ment vous reporter à la section "XElement.SetElementValue() sur des objets enfantsde XElement", un peu plus loin dans ce chapitre.

XNode.Remove()

Cette méthode permet de supprimer un nœud quelconque dans un arbre XML, ainsi que seséventuels nœuds enfants et attributs. Dans ce premier exemple, nous allons construireun arbre XML et mémoriser la référence au premier élément BookParticipant, en

<BookParticipants> <BookParticipant type="Author"> <FirstName>Joe</FirstName> <LastName>Rattz</LastName> </BookParticipant> <BookParticipant type="Technical Reviewer"> <FirstName>Fabio</FirstName> <LastName>Ferracchiati</LastName> </BookParticipant> <BookParticipant type="Editor"> <FirstName>Ewan</FirstName> <LastName>Buckingham</LastName> </BookParticipant></BookParticipants>

Linq.book Page 242 Mercredi, 18. février 2009 7:58 07

Page 258: LINQ Language Integrated Query en C

Chapitre 7 L’API LINQ to XML 243

utilisant la même technique que dans la section précédente. L’arbre XML sera affichéaprès la construction et avant toute suppression. Le premier élément BookParticipantsera alors supprimé et l’arbre XML, à nouveau affiché (voir Listing 7.67).

Listing 7.67 : Suppression d’un nœud avec la méthode Remove.

// L’objet firstParticipant sera utilisé pour mémoriser un élément dans l’arbre XMLXElement firstParticipant;

Console.WriteLine(System.Environment.NewLine + "Avant la suppression du nœud");

XDocument xDocument = new XDocument( new XElement("BookParticipants", firstParticipant = new XElement("BookParticipant", new XAttribute("type", "Author"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham"))));

Console.WriteLine(xDocument);

firstParticipant.Remove();Console.WriteLine(System.Environment.NewLine + " Après la suppression du nœud");Console.WriteLine(xDocument);

Voici le résultat :

Le premier élément BookParticipant a bien été supprimé.

IEnumerable<T>.Remove() où T est un XNodeDans l’exemple précédent, la méthode Remove a été appliquée à un seul nœud. Si néces-saire, il est également possible de l’appliquer à une séquence (IEnumerable<T>).Dans le Listing 7.68, la méthode Descendants est utilisée pour parcourir l’arbre XML.

Avant la suppression du nœud<BookParticipants> <BookParticipant type="Author"> <FirstName>Joe</FirstName> <LastName>Rattz</LastName> </BookParticipant> <BookParticipant type="Editor"> <FirstName>Ewan</FirstName> <LastName>Buckingham</LastName> </BookParticipant></BookParticipants>Après la suppression du nœud<BookParticipants> <BookParticipant type="Editor"> <FirstName>Ewan</FirstName> <LastName>Buckingham</LastName> </BookParticipant></BookParticipants>

Linq.book Page 243 Mercredi, 18. février 2009 7:58 07

Page 259: LINQ Language Integrated Query en C

244 LINQ to XML Partie III

Elle est combinée à un opérateur Where, et seuls sont retournés les éléments dont le nomest FirstName. La méthode Remove est enfin appelée sur cette séquence.

Listing 7.68 : Suppression d’une séquence de nœuds avec la méthode Remove.

XDocument xDocument = new XDocument( new XElement("BookParticipants", new XElement("BookParticipant", new XAttribute("type", "Author"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham"))));

xDocument.Descendants().Where(e => e.Name == "FirstName").Remove();

Console.WriteLine(xDocument);

La méthode XDocument.Descendants retourne les nœuds enfants d’une séquence.L’opérateur de requête standard Where est alors appelé pour filtrer les nœuds qui corres-pondent au critère de sélection (ici, le nom du nœud doit être FirstName). La séquenceretournée est alors passée à la méthode Remove pour supprimer les nœuds correspon-dants. Voici le résultat :

Comme vous pouvez le voir, tous les nœuds FirstName ont été supprimés.

XElement.RemoveAll()

Il est parfois nécessaire de supprimer le contenu d’un élément, mais pas l’élément lui-même. Vous utiliserez pour cela la méthode RemoveAll (voir Listing 7.69).

Listing 7.69 : Suppression du contenu d’un nœud avec RemoveAll.

XDocument xDocument = new XDocument( new XElement("BookParticipants", new XElement("BookParticipant", new XAttribute("type", "Author"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham"))));

Console.WriteLine(System.Environment.NewLine + "Avant la suppression du contenu");

<BookParticipants> <BookParticipant type="Author"> <LastName>Rattz</LastName> </BookParticipant> <BookParticipant type="Editor"> <LastName>Buckingham</LastName> </BookParticipant></BookParticipants>

Linq.book Page 244 Mercredi, 18. février 2009 7:58 07

Page 260: LINQ Language Integrated Query en C

Chapitre 7 L’API LINQ to XML 245

Console.WriteLine(xDocument);

xDocument.Element("BookParticipants").RemoveAll();

Console.WriteLine(System.Environment.NewLine + "Après la suppression du contenu");Console.WriteLine(xDocument);

Le document est affiché avant la suppression du contenu du nœud BookParticipants.Le contenu de ce nœud est alors supprimé puis le document est à nouveau affiché. Voiciles résultats :

Mise à jour de nœuds

Plusieurs des classes de XNode, comme XElement, XText et XComment, ont une propriétéValue qui peut être directement modifiée. D’autres, telles que XDocumentType et XPro-cessingInstruction, ont des propriétés spécifiques qui peuvent être modifiées. Lesméthodes XElement.SetElementValue et XContainer.ReplaceAll (voir un peu plus loindans ce chapitre) peuvent également être appelées pour modifier la valeur des éléments.

XElement.Value, XText.Value et XComment.Value Pour modifier la valeur d’un nœud XElement, XText et XComment, il suffit de modifier lapropriété Value des sous-classes de XNode correspondantes (voir Listing 7.70).

Listing 7.70 : Mise à jour de la valeur d’un nœud.

// Définition d’une référence sur un élément de l’arbre XMLXElement firstParticipant;

XDocument xDocument = new XDocument( new XElement("BookParticipants", firstParticipant = new XElement("BookParticipant", new XComment("Nouvel auteur"), new XAttribute("type", "Author"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz"))));

Console.WriteLine("Avant la modification des nœuds");Console.WriteLine(xDocument);

// Modification d’un élément, d’un commentaire et d’un nœud textefirstParticipant.Element("FirstName").Value = "Joey";

Avant la suppression du contenu<BookParticipants> <BookParticipant type="Author"> <FirstName>Joe</FirstName> <LastName>Rattz</LastName> </BookParticipant> <BookParticipant type="Editor"> <FirstName>Ewan</FirstName> <LastName>Buckingham</LastName> </BookParticipant></BookParticipants>

Après la suppression du contenu<BookParticipants />

Linq.book Page 245 Mercredi, 18. février 2009 7:58 07

Page 261: LINQ Language Integrated Query en C

246 LINQ to XML Partie III

firstParticipant.Nodes().OfType<XComment>().Single().Value = "Auteur du livre Pro LINQ en C# 2008";((XElement)firstParticipant.Element("FirstName").NextNode) .Nodes().OfType<XText>().Single().Value = "Rattz, Jr.";

Console.WriteLine("Après la modification des nœuds");Console.WriteLine(xDocument);

L’élément FirstName puis le commentaire sont modifiés en utilisant la propriété Valuecorrespondante. L’élément LastName est ensuite modifié par l’intermédiaire de lapropriété Value de son enfant XText. Cet exemple montre à quel point LINQ to XMLest flexible lorsqu’il s’agit d’accéder aux objets à modifier. Bien entendu, il n’est pasnécessaire de passer par l’enfant XText de l’élément LastName pour modifier sa valeur.Le chemin de traverse emprunté par ce code n’a qu’un but démonstratif. Voici le résultat :

Les valeurs des nœuds ont bien été mises à jour.

Les propriétés XDocumentType.Name, XDocumentType.PublicId, XDocumentType.SystemId et XDocumentType.InternalSubset Pour modifier les valeurs relatives à la définition de type de document (DTD), vousutiliserez quatre propriétés de la classe XDocumentType (voir Listing 7.71).

Listing 7.71 : Modification de la définition de type de document.

// Définition d’une référence sur le type de document pour un usage futurXDocumentType docType;

XDocument xDocument = new XDocument( docType = new XDocumentType("BookParticipants", null, "BookParticipants.dtd", null), new XElement("BookParticipants"));

Console.WriteLine("Avant la mise à jour du DTD");Console.WriteLine(xDocument);

docType.Name = "MyBookParticipants";docType.SystemId = "http://www.somewhere.com/DTDs/MyBookParticipants.DTD";docType.PublicId = "-//DTDs//TEXT Book Participants//EN";

Avant la mise à jour des nœuds<BookParticipants> <BookParticipant type="Author"> <!—Nouvel auteur--> <FirstName>Joe</FirstName> <LastName>Rattz</LastName> </BookParticipant></BookParticipants>

Après la mise à jour des nœuds<BookParticipants> <BookParticipant type="Author"> <!--Auteur de Pro LINQ en C# 2008--> <FirstName>Joey</FirstName> <LastName>Rattz, Jr.</LastName> </BookParticipant></BookParticipants>

Linq.book Page 246 Mercredi, 18. février 2009 7:58 07

Page 262: LINQ Language Integrated Query en C

Chapitre 7 L’API LINQ to XML 247

Console.WriteLine("Après la mise à jour du DTD");Console.WriteLine(xDocument);

Voici les résultats :

XProcessingInstruction.Target sur les objets XProcessingInstruction Objects et XProcessingInstruction.Data sur les objets XProcessingInstruction Pour modifier la valeur d’une instruction de traitement, il suffit de modifier les proprié-tés Target et Data de l’objet XProcessingInstruction (voir Listing 7.72).

Listing 7.72 : Mise à jour d’une instruction de traitement.

// Définition d’une référence pour un usage futurXProcessingInstruction procInst;

XDocument xDocument = new XDocument( new XElement("BookParticipants"), procInst = new XProcessingInstruction("BookCataloger", "out-of-print"));

Console.WriteLine("Avant la modification de l’instruction de traitement");Console.WriteLine(xDocument);

procInst.Target = "BookParticipantContactManager";procInst.Data = "update";

Console.WriteLine("Après la modification de l’instruction de traitement");Console.WriteLine(xDocument);

Voici le résultat de ce code :

XElement.ReplaceAll()

La méthode ReplaceAll permet de remplacer l’arbre XML relatif à un élément. Il estpossible de passer une simple valeur – une chaîne ou un nombre, par exemple – ou, siune méthode surchargée accepte des objets multiples via le mot-clé params, une portiond’arbre. La méthode ReplaceAll remplace également les attributs. Le Listing 7.73donne un exemple d’utilisation de cette méthode.

Avant la mise à jour du DTD<!DOCTYPE BookParticipants SYSTEM "BookParticipants.dtd"><BookParticipants />Après la mise à jour du DTD<!DOCTYPE MyBookParticipants PUBLIC "-//DTDs//TEXT Book Participants//EN""http://www.somewhere.com/DTDs/MyBookParticipants.DTD"><BookParticipants />

Avant la modification de l’instruction de traitement<BookParticipants /><?BookCataloger out-of-print?>Après la modification de l’instruction de traitement<BookParticipants /><?BookParticipantContactManager update?>

Linq.book Page 247 Mercredi, 18. février 2009 7:58 07

Page 263: LINQ Language Integrated Query en C

248 LINQ to XML Partie III

Listing 7.73 : Utilisation de la méthode ReplaceAll pour modifier l’arbre relatif à un élément.

// Définition d’une référence vers un des éléments de l’arbre XML pour un usage futurXElement firstParticipant;

XDocument xDocument = new XDocument( new XElement("BookParticipants", firstParticipant = new XElement("BookParticipant", new XAttribute("type", "Author"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz"))));

Console.WriteLine(System.Environment.NewLine + "Avant la modification");Console.WriteLine(xDocument);

firstParticipant.ReplaceAll( new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham"));

Console.WriteLine(System.Environment.NewLine + "Après la modification");Console.WriteLine(xDocument);

Les instructions en gras modifient l’arbre de l’élément firstParticipant. Commevous pouvez le voir, l’attribut type n’a pas été spécifié.

Voici le résultat :

Bien que les attributs ne soient pas des nœuds enfants des éléments, la méthode ReplaceAlla été en mesure de supprimer l’attribut type de l’arbre XML.

XElement.SetElementValue() sur des objets enfants de XElement

Cette méthode est très puissante. Elle permet d’ajouter, de modifier et de supprimer leséléments enfants de l’élément sur lequel elle est appelée.

Cette méthode admet deux paramètres : le nom de l’élément enfant à atteindre et lavaleur qui doit lui être affectée. Si un enfant portant ce nom est trouvé, et si la valeurpassée est différente de null, l’enfant est mis à jour. Si la valeur passée vaut null,l’enfant est supprimé. Si aucun enfant portant ce nom n’est trouvé, il est créé et lavaleur spécifiée lui est affectée.

Avant la modification<BookParticipants> <BookParticipant type="Author"> <FirstName>Joe</FirstName> <LastName>Rattz</LastName> </BookParticipant></BookParticipants>Après la modification<BookParticipants> <BookParticipant> <FirstName>Ewan</FirstName> <LastName>Buckingham</LastName> </BookParticipant></BookParticipants>

Linq.book Page 248 Mercredi, 18. février 2009 7:58 07

Page 264: LINQ Language Integrated Query en C

Chapitre 7 L’API LINQ to XML 249

La méthode SetElementValue n’affecte que le premier élément enfant portant le nomspécifié. Si un ou plusieurs autres éléments enfants portent le même nom, ils ne sont pasaffectés.

Le Listing 7.74 donne un exemple des trois possibilités de cette méthode.

Listing 7.74 : Utilisation de SetElementValue pour mettre à jour, ajouter et supprimer des éléments enfants.

// Définition d’une référence vers un des éléments de l’arbre XML pour un usage futurXElement firstParticipant;

XDocument xDocument = new XDocument( new XElement("BookParticipants", firstParticipant = new XElement("BookParticipant", new XAttribute("type", "Author"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz"))));

Console.WriteLine(System.Environment.NewLine + "Avant la mise à jour des éléments");Console.WriteLine(xDocument);

// Mise à jour de la valeur d’un élément// L’élément enfant FirstName étant trouvé, sa valeur sera initialisée à JosephfirstParticipant.SetElementValue("FirstName", "Joseph");

// Ajout d’un élément// L’élément enfant MiddleInitial n’étant pas trouvé, il est crééfirstParticipant.SetElementValue("MiddleInitial", "C");

// Suppression d’un élément// La valeur de l’élément étant initialisée à null, l’élément est suppriméfirstParticipant.SetElementValue("LastName", null);

Console.WriteLine(System.Environment.NewLine + "Après la mise à jour des éléments");Console.WriteLine(xDocument);

Dans un premier temps, la méthode SetElementValue est appelée sur l’élément enfantFirstName de l’élément firstParticipant. Comme un élément portant ce nom existe,sa valeur est mise à jour. Dans un deuxième temps, la méthode SetElementValue estappelée sur l’élément enfant MiddleInitial de l’élément firstParticipant. Commeaucun élément portant ce nom n’existe, il est créé. Enfin, dans un troisième temps, laméthode SetElementValue est appelée sur l’élément enfant LastName de l’élémentfirstParticipant. La valeur null étant passée dans le deuxième argument de laméthode, l’élément LastName est supprimé.

Voici les résultats :

Avant la mise à jour des éléments<BookParticipants> <BookParticipant type="Author"> <FirstName>Joe</FirstName> <LastName>Rattz</LastName> </BookParticipant></BookParticipants>Après la mise à jour des éléments

Linq.book Page 249 Mercredi, 18. février 2009 7:58 07

Page 265: LINQ Language Integrated Query en C

250 LINQ to XML Partie III

L’élément FirstName a été mis à jour, l’élément MiddleInitial a été créé et l’élémentLastName, supprimé.

ATTENTIONATTENTION

Lorsque la méthode SetElementValue est appelée avec un deuxième argument ayant pourvaleur null, elle supprime l’élément spécifié dans le premier argument. Que ceci ne vousfasse pas croire qu’il suffise d’initialiser un élément avec la valeur null pour le supprimerd’un arbre XML. Si vous tentez de le faire en agissant sur sa propriété Value, une exceptionsera levée.

Attributs XML

Lorsque l’on utilise l’API LINQ to XML, les attributs sont implémentés dans la classeXAttribute. Contrairement à ce qui avait cours dans l’API W3C XML DOM, ilsn’héritent pas d’un nœud. Ils n’ont donc aucune relation d’héritage avec les éléments.Et, pourtant, grâce à l’API LINQ to XML, ils sont tout aussi simples à utiliser.

Création d’un attribut

Les attributs sont créés de la même manière que les éléments et que la plupart des autresclasses LINQ to XML. Reportez-vous à la section "Création d’attributs avec XAttribute",au début de ce chapitre, pour en savoir plus à ce sujet.

Déplacements dans un attribut

Pour vous déplacer dans les attributs, vous utiliserez les propriétés XElement.First-Attribute, XElement.LastAttribute, XAttribute.NextAttribute et XAttribute.PreviousAttribute et les méthodes XElement.Attribute et XElement.Attributes. Vousen saurez plus à leur sujet dans les prochaines pages.

Premier attribut avec XElement.FirstAttribute Pour accéder au premier attribut d’un élément, vous pouvez utiliser la propriété FirstAt-tribute (voir Listing 7.75).

Listing 7.75 : Accès au premier attribut d’un élément avec la propriété FirstAttribute.

// Définition d’une référence vers un des éléments de l’arbre XML pour un usage futurXElement firstParticipant;

<BookParticipants> <BookParticipant type="Author"> <FirstName>Joseph</FirstName> <MiddleInitial>C</MiddleInitial> </BookParticipant></BookParticipants>

Linq.book Page 250 Mercredi, 18. février 2009 7:58 07

Page 266: LINQ Language Integrated Query en C

Chapitre 7 L’API LINQ to XML 251

XDocument xDocument = new XDocument( new XElement("BookParticipants", firstParticipant = new XElement("BookParticipant", new XAttribute("type", "Author"), new XAttribute("experience", "first-time"), new XAttribute("language", "English"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz"))));

Console.WriteLine(firstParticipant.FirstAttribute);

Ce code produit le résultat suivant dans la console :

Attribut suivant avec XAttribute.NextAttributePour accéder à l’attribut suivant, il suffit d’utiliser la propriété NextAttribute (voirListing 7.76).

Listing 7.76 : Accès à l’attribut suivant avec la propriété NextAttribute.

// Définition d’une référence vers un des éléments de l’arbre XML pour un usage futurXElement firstParticipant;

XDocument xDocument = new XDocument( new XElement("BookParticipants", firstParticipant = new XElement("BookParticipant", new XAttribute("type", "Author"), new XAttribute("experience", "first-time"), new XAttribute("language", "English"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz"))));

Console.WriteLine(firstParticipant.FirstAttribute.NextAttribute);

Avant d’utiliser la propriété NextAttribute, la propriété FirstAttribute a été appli-quée à l’élément firstParticipant pour obtenir une référence au premier attribut del’élément. Voici le résultat :

Si la propriété NextAttribute d’un attribut a pour valeur null, cela signifie qu’il s’agitdu dernier attribut de l’élément.

Attribut précédent avec XAttribute.PreviousAttributePour accéder à l’attribut précédent, il suffit d’utiliser la propriété PreviousAttribute(voir Listing 7.77).

Listing 7.77 : Accès à l’attribut précédent avec la propriété PreviousAttribute.

// Définition d’une référence vers un des éléments de l’arbre XML pour un usage futurXElement firstParticipant;

type="Author"

experience="first-time"

Linq.book Page 251 Mercredi, 18. février 2009 7:58 07

Page 267: LINQ Language Integrated Query en C

252 LINQ to XML Partie III

XDocument xDocument = new XDocument( new XElement("BookParticipants", firstParticipant = new XElement("BookParticipant", new XAttribute("type", "Author"), new XAttribute("experience", "first-time"), new XAttribute("language", "English"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz"))));

Console.WriteLine(firstParticipant.FirstAttribute.NextAttribute.PreviousAttribute);

Les propriétés FirstAttribute et NextAttribute ont été chaînées pour obtenir uneréférence au deuxième attribut de l’élément firstParticipant. En appliquant lapropriété PreviousAttribute, l’attribut pointé est donc le premier. Voici le résultat :

Si la propriété PreviousAttribute d’un attribut vaut null, cela signifie qu’il a étéappliqué au premier attribut de l’élément.

Dernier attribut avec XElement.LastAttributePour accéder au dernier attribut d’un élément, vous utiliserez la propriété LastAttri-bute (voir Listing 7.78).

Listing 7.78 : Accès au dernier attribut avec la propriété LastAttribute.

// Définition d’une référence vers un des éléments de l’arbre XML pour un usage ➥futurXElement firstParticipant;

XDocument xDocument = new XDocument( new XElement("BookParticipants", firstParticipant = new XElement("BookParticipant", new XAttribute("type", "Author"), new XAttribute("experience", "first-time"), new XAttribute("language", "English"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz"))));

Console.WriteLine(firstParticipant.LastAttribute);

L’instruction Writeln affiche le dernier attribut du XElement firstParticipant :

XElement.Attribute()S’il existe, cette méthode retourne le premier attribut dont le nom est passé en argument(voir Listing 7.79).

Listing 7.79 : Accès à un attribut avec la méthode Attribute.

// Définition d’une référence vers un des éléments de l’arbre XML pour un usage futurXElement firstParticipant;

type="Author"

language="English"

Linq.book Page 252 Mercredi, 18. février 2009 7:58 07

Page 268: LINQ Language Integrated Query en C

Chapitre 7 L’API LINQ to XML 253

XDocument xDocument = new XDocument( new XElement("BookParticipants", firstParticipant = new XElement("BookParticipant", new XAttribute("type", "Author"), new XAttribute("experience", "first-time"), new XAttribute("language", "English"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz"))));

Console.WriteLine(firstParticipant.Attribute("type").Value);

La méthode Attribute donne accès à l’attribut type. La valeur de cet attribut est alorsaffichée en utilisant la propriété Value. Voici le résultat :

À titre d’information, sachez que la valeur de l’attribut aurait également pu être obtenueen appliquant un casting de type string à l’attribut.

XElement.Attributes()

La méthode Attributes() retourne tous les attributs de l’élément sur lequel elle estappliquée. Les attributs sont retournés sous la forme d’une séquence d’objets XAttri-bute (voir Listing 7.80).

Listing 7.80 : Accès à tous les attributs d’un élément avec la méthode Attributes.

// Définition d’une référence vers un des éléments de l’arbre XML pour un usage futurXElement firstParticipant;

XDocument xDocument = new XDocument( new XElement("BookParticipants", firstParticipant = new XElement("BookParticipant", new XAttribute("type", "Author"), new XAttribute("experience", "first-time"), new XAttribute("language", "English"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz"))));

foreach(XAttribute attr in firstParticipant.Attributes()){ Console.WriteLine(attr);}

Voici le résultat :

Modification d’attributs

Comme il a été dit précédemment, les API W3C XML DOM et LINQ to XML manipu-lent les attributs d’une façon bien différente. Avec l’API W3C, les attributs sont desnœuds enfants du nœud dont ils sont l’attribut. Avec l’API LINQ to XML, les attributs

Author

type="Author"experience="first-time"

Linq.book Page 253 Mercredi, 18. février 2009 7:58 07

Page 269: LINQ Language Integrated Query en C

254 LINQ to XML Partie III

sont des paires nom/valeur. Ils sont accessibles via la méthode Attributes ou lapropriété FirstAttribute de l’élément. Il est important d’avoir cela en mémoire.

Les méthodes et propriétés des attributs sont très proches de celles qui ont déjà étéétudiées pour les éléments. Vous pouvez utiliser les méthodes suivantes pour ajouter unattribut à un élément :

m XElement.Add() ;

m XElement.AddFirst() ;

m XElement.AddBeforeThis() ;

m XElement.AddAfterThis().

Ces méthodes ont déjà été illustrées dans la section "Ajout de nœuds", un peu plus tôtdans ce chapitre. Reportez-vous aux exemples de cette section pour voir comment ajou-ter des attributs. Consultez également la section relative à la méthode XElement.Set-AttributeValue, un peu plus loin dans ce chapitre.

Suppression d’attributsPour supprimer un attribut, vous utiliserez la méthode XAttribute.Remove. Poursupprimer une séquence d’attributs, vous utiliserez la méthode IEnumerable<T>.Remove.

Vous consulterez également la section XElement.SetAttributeValue(), un peu plusloin dans ce chapitre.

XAttribute.Remove()

Vous vous rappelez certainement que la méthode Remove de la classe XNode permettaitde supprimer un nœud. Quant à elle, la méthode Remove de la classe XAttribute permetde supprimer un attribut (voir Listing 7.81).

Listing 7.81 : Suppression d’un attribut.

// Définition d’une référence vers un des éléments de l’arbre XML pour un usage futurXElement firstParticipant;

XDocument xDocument = new XDocument( new XElement("BookParticipants", firstParticipant = new XElement("BookParticipant", new XAttribute("type", "Author"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz"))));

Console.WriteLine(System.Environment.NewLine + "Avant la suppression de l’attribut");Console.WriteLine(xDocument);

firstParticipant.Attribute("type").Remove();

Console.WriteLine(System.Environment.NewLine + "Après la suppression de l’attribut");Console.WriteLine(xDocument);

Linq.book Page 254 Mercredi, 18. février 2009 7:58 07

Page 270: LINQ Language Integrated Query en C

Chapitre 7 L’API LINQ to XML 255

Dans cet exemple, nous utilisons la méthode Attribute pour obtenir la référence de l’attri-but à supprimer. La méthode Remove est alors appliquée à cette référence. Voici le résultat :

L’attribut type a bien été supprimé.

IEnumerable<T>.Remove() où T est un XNodeTout comme la méthode IEnumerable<T>.Remove() de la classe XNode permet desupprimer une séquence de nœuds, la méthode IEnumerable<T>.Remove() de la classeXAttribute permet de supprimer une séquence d’attributs (voir Listing 7.82).

Listing 7.82 : Suppression de tous les attributs d’un élément.

// Définition d’une référence vers un des éléments de l’arbre XML pour un usage futurXElement firstParticipant;

XDocument xDocument = new XDocument( new XElement("BookParticipants", firstParticipant = new XElement("BookParticipant", new XAttribute("type", "Author"), new XAttribute("experience", "first-time"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz"))));

Console.WriteLine(System.Environment.NewLine + "Avant la suppression des attributs");Console.WriteLine(xDocument);

firstParticipant.Attributes().Remove();

Console.WriteLine(System.Environment.NewLine + "Après la suppression des attributs");Console.WriteLine(xDocument);

La méthode Attributes() retourne la séquence des attributs du XElement firstParti-cipant. La méthode Remove supprime cette séquence. Voici les résultats :

Avant la suppression de l’attribut<BookParticipants> <BookParticipant type="Author"> <FirstName>Joe</FirstName> <LastName>Rattz</LastName> </BookParticipant></BookParticipants>

Après la suppression de l’attribut<BookParticipants> <BookParticipant> <FirstName>Joe</FirstName> <LastName>Rattz</LastName> </BookParticipant></BookParticipants>

Avant la suppression des attributs<BookParticipants> <BookParticipant type="Author" experience="first-time"> <FirstName>Joe</FirstName> <LastName>Rattz</LastName> </BookParticipant>

Linq.book Page 255 Mercredi, 18. février 2009 7:58 07

Page 271: LINQ Language Integrated Query en C

256 LINQ to XML Partie III

Modification de la valeur des attributsPour modifier la valeur d’un attribut, vous utiliserez la propriété XAttribute.Value(voir Listing 7.83).

INFO

Reportez-vous également à la section XElement.SetAttributeValue(), un peu plus loindans ce chapitre.

Listing 7.83 : Suppression de tous les attributs d’un élément.

// Définition d’une référence vers un des éléments de l’arbre XML pour un usage futurXElement firstParticipant;

XDocument xDocument = new XDocument( new XElement("BookParticipants", firstParticipant = new XElement("BookParticipant", new XAttribute("type", "Author"), new XAttribute("experience", "first-time"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz"))));

Console.WriteLine(System.Environment.NewLine + "Avant la modification de la valeur de ➥l’attribut");Console.WriteLine(xDocument);

firstParticipant.Attribute("experience").Value = "beginner";

Console.WriteLine(System.Environment.NewLine + "Après la modification de la valeur ➥de l’attribut");Console.WriteLine(xDocument);

La méthode Attribute a été utilisée pour obtenir une référence à l’attribut experience.La méthode Value a alors été appliquée à cette référence pour accéder à la valeur del’attribut. Voici le résultat :

</BookParticipants>

Après la suppression des attributs<BookParticipants> <BookParticipant> <FirstName>Joe</FirstName> <LastName>Rattz</LastName> </BookParticipant></BookParticipants>

Avant la modification de la valeur de l’attribut<BookParticipants> <BookParticipant type="Author" experience="first-time"> <FirstName>Joe</FirstName> <LastName>Rattz</LastName> </BookParticipant></BookParticipants>

Linq.book Page 256 Mercredi, 18. février 2009 7:58 07

Page 272: LINQ Language Integrated Query en C

Chapitre 7 L’API LINQ to XML 257

L’attribut experience avait pour valeur "first-time" avant l’exécution du code. Il adésormais pour valeur "beginner".

XElement.SetAttributeValue()

La méthode SetAttributeValue est le pendant pour les attributs de la méthode Set-ElementValue. Tout aussi complète, elle permet d’ajouter, de supprimer et de modifierla valeur d’un attribut.

Si un nom d’attribut inexistant lui est passé, cet attribut est ajouté à l’élément. Si unnom d’attribut existant ayant une valeur différente de null lui est passé, l’attribut estmis à jour avec la valeur passée. Enfin, si un nom d’attribut existant ayant la valeur nulllui est passé, il est supprimé (voir Listing 7.84).

Listing 7.84 : Suppression de tous les attributs d’un élément.

// Définition d’une référence vers un des éléments de l’arbre XML pour un usage futurXElement firstParticipant;

XDocument xDocument = new XDocument( new XElement("BookParticipants", firstParticipant = new XElement("BookParticipant", new XAttribute("type", "Author"), new XAttribute("experience", "first-time"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz"))));

Console.WriteLine(System.Environment.NewLine + "Avant la modification des attributs");Console.WriteLine(xDocument);

// L’attribut "type" existe et le deuxième argument est différent de "null".// L’attribut "type" est donc mis à jour.firstParticipant.SetAttributeValue("type", "beginner");

// L’attribut "language" n’existe pas. Il est donc ajouté à l’élément.firstParticipant.SetAttributeValue("language", "English");

// L’attribut "experience" existe et le deuxième argument a pour valeur "null"// L’attribut "experience" est donc supprimé.firstParticipant.SetAttributeValue("experience", null);

Console.WriteLine(System.Environment.NewLine + "Après la modification des attributs");Console.WriteLine(xDocument);

Après la modification de la valeur de l’attribut<BookParticipants> <BookParticipant type="Author" experience="beginner"> <FirstName>Joe</FirstName> <LastName>Rattz</LastName> </BookParticipant></BookParticipants>

Linq.book Page 257 Mercredi, 18. février 2009 7:58 07

Page 273: LINQ Language Integrated Query en C

258 LINQ to XML Partie III

Ce code met à jour la valeur d’un attribut, définit un nouvel attribut et supprime un attributexistant. Voici les résultats :

Annotations XML

En utilisant les annotations de l’API LINQ to XML, il est possible d’associer unedonnée utilisateur à une classe quelconque qui hérite de la classe XObject. Il est ainsipossible d’affecter une donnée quelconque (une clé supplémentaire, un objet qui parseles valeurs d’un élément) à un élément, à un document ou à un autre objet dont la classeest dérivée de XObject.

Ajout d’annotations avec XObject.AddAnnotation()

Voici le prototype de la méthode AddAnnotation() :

void XObject.AddAnnotation(object annotation);

Accès aux annotations avec XObject.Annotation() ou XObject.Annotations()

Voici les prototypes de ces deux méthodes :

object XObject.Annotation(Type type);T XObject.Annotation<T>();IEnumerable<object> XObject.Annotations(Type type);IEnumerable<T> XObject.Annotations<T>();

ATTENTIONATTENTION

Lorsque vous accédez à des annotations, veillez à passer le type actuel de l’objet, et non saclasse de base ou son interface. Sans quoi l’annotation ne serait pas trouvée.

Suppression d’annotations avec XObject.RemoveAnnotations()

Voici les deux prototypes de la méthode RemoveAnnotations() :

void XObject.RemoveAnnotations(Type type);void XObject.RemoveAnnotations<T>();

Avant la modification des attributs<BookParticipants> <BookParticipant type="Author" experience="first-time"> <FirstName>Joe</FirstName> <LastName>Rattz</LastName> </BookParticipant></BookParticipants>

Après la modification des attributs<BookParticipants> <BookParticipant type="beginner" language="English"> <FirstName>Joe</FirstName> <LastName>Rattz</LastName> </BookParticipant></BookParticipants>

Linq.book Page 258 Mercredi, 18. février 2009 7:58 07

Page 274: LINQ Language Integrated Query en C

Chapitre 7 L’API LINQ to XML 259

Exemples d’annotations

À titre d’exemple, nous allons définir un code qui ajoute, retrouve et supprime desannotations. Ici, nous utiliserons l’arbre XML désormais traditionnel BookPartici-pants. Nous allons associer un handler à chaque élément BookParticipant, en sebasant sur son attribut type. Dans cet exemple, le handler affichera l’élément dans unformat qui dépend de l’attribut type : un format pour les auteurs, un autre pour leséditeurs.

Voici les classes handler utilisées (une pour les auteurs et une pour les éditeurs) :

public class AuthorHandler{ public void Display(XElement element) { Console.WriteLine("BIOGRAPHIE DE L’AUTEUR"); Console.WriteLine("--------------------------"); Console.WriteLine("Nom : {0} {1}", (string)element.Element("FirstName"), (string)element.Element("LastName")); Console.WriteLine("Langue : {0}", (string)element.Attribute("language")); Console.WriteLine("Expérience : {0}", (string)element.Attribute("experience")); Console.WriteLine("==========================" + System.Environment.NewLine); }}

public class EditorHandler{ public void Display(XElement element) { Console.WriteLine("BIOGRAPHIE DE L’EDITEUR"); Console.WriteLine("--------------------------"); Console.WriteLine("Nom: {0}", (string)element.Element("FirstName")); Console.WriteLine(" {0}", (string)element.Element("LastName")); Console.WriteLine("==========================" + System.Environment.NewLine); }}

Ce code définit deux classes au comportement distinct. Dans cet exemple, les donnéesde l’élément sont affichées différemment. Bien entendu, le traitement pourrait être toutautre. Les annotations pourraient même ne pas être des handlers…

Cet exemple étant plus complexe que les précédents, nous avons divisé le code enplusieurs sections. Chacune d’entre elles sera suivie d’explications (voir Listing 7.85).

Listing 7.85 : Ajout, lecture et suppression d’annotations.

// Définition d’une référence vers un des éléments de l’arbre XML pour un usage futurXElement firstParticipant;

XDocument xDocument = new XDocument( new XElement("BookParticipants", firstParticipant = new XElement("BookParticipant", new XAttribute("type", "Author"), new XAttribute("experience", "first-time"), new XAttribute("language", "English"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")),

Linq.book Page 259 Mercredi, 18. février 2009 7:58 07

Page 275: LINQ Language Integrated Query en C

260 LINQ to XML Partie III

new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham"))));// Affichage du documentConsole.WriteLine(xDocument + System.Environment.NewLine);

Ces quelques lignes de code définissent le document XML et l’affichent. Le bloc decode suivant énumère les participants. Pour chacun d’entre eux, un handler est instanciéen fonction de l’attribut type et une annotation faisant référence à ce handler est ajoutéeà l’élément.

// Ajout d’annotations en fonction de la valeur de l’attribut typeforeach(XElement e in xDocument.Element("BookParticipants").Elements()){ if((string)e.Attribute("type") == "Author") { AuthorHandler aHandler = new AuthorHandler(); e.AddAnnotation(aHandler); } else if((string)e.Attribute("type") == "Editor") { EditorHandler eHandler = new EditorHandler(); e.AddAnnotation(eHandler); }}

Après l’exécution de ce code, chaque élément BookParticipant possède une annota-tion qui référence un handler dont le code dépend de la valeur de l’attribut type. Nousallons maintenant énumérer les éléments BookParticipant, retrouver les annotations etexécuter les handlers associés.

AuthorHandler aHandler2;EditorHandler eHandler2;foreach(XElement e in xDocument.Element("BookParticipants").Elements()){ if((string)e.Attribute("type") == "Author") { aHandler2 = e.GetAnnotation<AuthorHandler>(); if(aHandler2 != null) { aHandler2.Display(e); } } else if((string)e.Attribute("type") == "Editor") { eHandler2 = e.GetAnnotation<EditorHandler>(); if(eHandler2 != null) { eHandler2.Display(e); } }}

Ce code exécute la méthode Display du handler associé à chaque élément. Le bloc decode suivant va supprimer les annotations de chaque élément :

foreach(XElement e in xDocument.Element("BookParticipants").Elements()){

Linq.book Page 260 Mercredi, 18. février 2009 7:58 07

Page 276: LINQ Language Integrated Query en C

Chapitre 7 L’API LINQ to XML 261

if((string)e.Attribute("type") == "Author") { e.RemoveAnnotation<AuthorHandler>(); } else if((string)e.Attribute("type") == "Editor") { e.RemoveAnnotation<EditorHandler>(); }}

Ce code est plus long que les précédents. Il est composé de quatre sections principales.La première section crée l’arbre XML et l’affiche. Ceci n’a rien d’exceptionnel, puis-que nous l’avons déjà fait fréquemment dans les autres exemples de cet ouvrage. Ladeuxième section énumère les éléments BookParticipant et ajoute un handler en fonc-tion de la valeur de leur attribut type. La troisième section énumère les éléments Book-Participant. En fonction de la valeur de leur attribut type, la méthode Display duhandler correspondant est exécutée. Enfin, la quatrième section énumère les élémentsBookParticipant et supprime les annotations.

Dans les sections 2, 3 et 4 du code, l’accès aux attributs s’est fait via un casting auformat string. C’est ainsi qu’il a été possible de les comparer aux valeurs "Author" et"Editor".

Voici les résultats :

Ce qu’il faut remarquer dans ces résultats, c’est que les deux handlers sont appelés enfonction de l’attribut type et via les annotations. Retenez également que les annotationspeuvent être constituées d’objets quelconques, et pas seulement de handlers.

<BookParticipants> <BookParticipant type="Author" experience="first-time" language="English"> <FirstName>Joe</FirstName> <LastName>Rattz</LastName> </BookParticipant> <BookParticipant type="Editor"> <FirstName>Ewan</FirstName> <LastName>Buckingham</LastName> </BookParticipant></BookParticipants>

BIOGRAPHIE AUTEUR--------------------------Nom : Joe RattzLangue : EnglishExpérience: first-time==========================

BIOGRAPHIE EDITEUR--------------------------Nom : Ewan

Buckingham

==========================

Linq.book Page 261 Mercredi, 18. février 2009 7:58 07

Page 277: LINQ Language Integrated Query en C

262 LINQ to XML Partie III

Événements XML

Grâce à l’API LINQ to XML, vous pouvez demander à être informé à tout moment dela modification des objets qui héritent de la classe XObject.

Lorsque vous faites une telle demande auprès d’un objet, un événement sera levé si cetobjet, ou un de ses descendants, est modifié. Cela signifie que si, par exemple, vousvous abonnez à un événement situé au niveau du document, toutes les modificationseffectuées dans l’arbre provoqueront l’appel de la méthode à laquelle vous vous êtesabonné. C’est la raison pour laquelle vous ne devez faire aucune supposition sur le typede l’objet qui provoquera les événements. Lorsque la méthode de traitement est appe-lée, l’objet qui en est à l’origine est passé en tant qu’émetteur de l’événement. Son typeest object. Faites attention lorsque vous lui appliquerez un opérateur de casting, lors-que vous accéderez à ses propriétés ou appellerez ses méthodes. Il se peut que son typene corresponde pas à ce que vous attendez. Ceci sera illustré dans le Listing 7.86, oùl’objet sera de type XText alors que l’on attend un type XElement.

Sachez enfin que la construction d’un arbre XML ne génère aucun événement.Comment cela serait-il possible, puisque aucun événement ne peut être enregistré avantla construction de l’arbre ! Seule la modification ou la suppression d’un élément XMLpeut engendrer un événement, et seulement à condition que cet événement ait été enre-gistré.

XObject.Changing

Cet événement est levé lorsqu’un objet qui hérite de XObject est sur le point d’êtremodifié. Pour vous abonner à l’événement, vous devez ajouter un objet de type Even-tHandler à l’événement Changing de l’objet :

myobject.Changing += new EventHandler<XObjectChangeEventArgs>(MyHandler);

Le délégué doit avoir la signature suivante :

void MyHandler(object sender, XObjectChangeEventArgs cea)

L’objet sender est celui qui est sur le point d’être modifié et qui provoque la levée del’événement. La propriété ObjectChange de type XObjectChange de l’objet cea(Change Event Arguments) indique le type de changement qui est sur le point de surve-nir : XObjectChange.Add, XObjectChange.Name, XObjectChange.Remove ou XObject-Change.Value.

XObject.Changed

Cet événement est levé lorsqu’un objet qui hérite de XObject a été modifié. Pour vousabonner à l’événement, vous devez ajouter un objet de type EventHandler à l’événe-ment Changed de l’objet :

myobject.Changed += new EventHandler<XObjectChangeEventArgs>(MyHandler);

Linq.book Page 262 Mercredi, 18. février 2009 7:58 07

Page 278: LINQ Language Integrated Query en C

Chapitre 7 L’API LINQ to XML 263

Le délégué doit avoir la signature suivante :

void MyHandler(object sender, XObjectChangeEventArgs cea)

L’objet sender est celui qui a été modifié et qui provoque la levée de l’événement. Lapropriété ObjectChange de type XObjectChange de l’objet cea (Change Event Argu-ments) indique le type de changement qui est sur le point de survenir : XObject-Change.Add, XObjectChange.Name, XObjectChange.Remove ou XObjectChange.Value.

Quelques exemples d’événements

Un exemple va vous aider à bien comprendre toute la logique mise en œuvre pour gérerles événements XObject. Avant d’entrer dans le vif du sujet, nous allons présenter lesgestionnaires d’événements utilisés.

Cette méthode est exécutée lorsque l’événement Changing d’un élément est levé. Ellepermet d’être prévenu lorsqu’un élément est sur le point d’être modifié.

public static void MyChangingEventHandler(object sender, XObjectChangeEventArgs cea){Console.WriteLine("Type de l’objet qui va être modifié : {0}, Type du changement : {1}",sender.GetType().Name, cea.ObjectChange);}

Voici le gestionnaire utilisé pour générer un événement juste après qu’un élément a étémodifié. Elle permet d’être prévenu lorsqu’un élément a été modifié.

Cette méthode est exécutée lorsque l’événement Changed d’un élément est levé :

public static void MyChangedEventHandler(object sender, XObjectChangeEventArgs cea){Console.WriteLine("Type de l’objet qui a été modifié : {0}, Type du changement : {1}",sender.GetType().Name, cea.ObjectChange);}

Un peu plus tôt, j’ai indiqué qu’un événement serait levé si un descendant d’un objetauquel vous êtes abonné est modifié. Pour illustrer ce fait, nous allons définir une autreméthode que nous enregistrerons une fois le document modifié. Son unique but est demontrer que le document reçoit également un événement Changed, même s’il s’agitd’un descendant situé à plusieurs niveaux hiérarchiques de celui qui a été modifié.

Cette méthode est exécutée lorsque l’événement Changed du document XML est levé :

public static void DocumentChangedHandler(object sender, XObjectChangeEventArgs cea){ Console.WriteLine("Doc: Type de l’objet qui a été modifié : {0}, Type du changement : {1}{2}", sender.GetType().Name, cea.ObjectChange, System.Environment.NewLine);}

La seule différence entre les méthodes DocumentChangedHandler et MyChangedEvent-Handler se situe dans le début de l’affichage : l’affichage effectué dans DocumentChan-gedHandler débute par le terme "Doc:", afin de signaler que le gestionnaire est appelépar l’événement Changed du document, et non de l’élément.

Examinons le code du Listing 7.86.

Linq.book Page 263 Mercredi, 18. février 2009 7:58 07

Page 279: LINQ Language Integrated Query en C

264 LINQ to XML Partie III

Listing 7.86 : Le gestionnaire d’événements XObject.

XElement firstParticipant;

XDocument xDocument = new XDocument( new XElement("BookParticipants", firstParticipant = new XElement("BookParticipant", new XAttribute("type", "Author"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham"))));

Console.WriteLine("{0}{1}", xDocument, System.Environment.NewLine);

Rien de nouveau pour l’instant. Comme il a été fait à de nombreuses reprises dans lespages précédentes, un document XML a été créé en utilisant la construction fonction-nelle, puis affiché dans la console. Remarquez également qu’une référence au premierélément BookParticipant a été mémorisée. Les événements seront déclenchés parrapport à cet élément :

firstParticipant.Changing += newEventHandler<XObjectChangeEventArgs>(MyChangingEventHandler);firstParticipant.Changed += newEventHandler<XObjectChangeEventArgs>(MyChangedEventHandler);xDocument.Changed += newEventHandler<XObjectChangeEventArgs>(DocumentChangedHandler);

Après l’exécution de ces lignes de code, un événement sera généré :

m juste avant (Changing) le changement du premier élément BookParticipant ;

m juste après (Changed) le changement du premier élément BookParticipant ;

m juste après (Changed) la modification du document.

Le dernier type d’événement a été mis en place pour prouver que des événements sontgénérés lorsqu’un objet decendant est modifié. Il ne reste plus qu’à effectuer une modi-fication dans l’élément firstParticipant.

firstParticipant.Element("FirstName").Value = "Seph";

Console.WriteLine("{0}{1}", xDocument, System.Environment.NewLine);

La première ligne change la valeur de l’élément FirstName du premier élément Book-Participant. La deuxième ligne affiche le document XML résultant. Voici les résultats :

<BookParticipants> <BookParticipant type="Author"> <FirstName>Joe</FirstName> <LastName>Rattz</LastName> </BookParticipant> <BookParticipant type="Editor"> <FirstName>Ewan</FirstName> <LastName>Buckingham</LastName> </BookParticipant></BookParticipants>

Linq.book Page 264 Mercredi, 18. février 2009 7:58 07

Page 280: LINQ Language Integrated Query en C

Chapitre 7 L’API LINQ to XML 265

Cette sortie console montre le document avant et après l’utilisation du gestionnaired’événements. Comme vous pouvez le constater, l’élément FirstName du premierBookParticipant a été modifié. Les lignes situées entre les deux affichages de l’arbreXML correspondent aux messages affichés par les gestionnaires d’événements. L’objetmodifié est de type XText. Pour ma part, je m’attendais à ce qu’il soit de type XElement.Il est facile d’oublier que, lorsque vous affectez une chaîne à la valeur d’un élément, unobjet XText est automatiquement créé, de façon transparente.

En regardant d’un peu plus près le texte affiché par les gestionnaires d’événements, oncomprend mieux ce qu’il se passe lorsqu’un élément est modifié : dans le premier blocde trois lignes, la valeur XText est sur le point d’être supprimée, puis elle est supprimée.L’événement Changed du document est alors levé. Cela montre que les événements sepropagent du niveau le plus bas au niveau le plus haut.

Dans le deuxième bloc de trois lignes, la même suite d’événements est générée mais,ici, un objet XText est ajouté à l’arbre XML. Vous savez maintenant que, lorsque vousmodifiez la valeur d’un élément, un objet XText est supprimé puis restauré.

Dans cet exemple, nous avons utilisé des méthodes nommées. Cette démarche n’estnullement obligatoire : il est également possible d’utiliser des méthodes anonymes oudes expressions lambda. Le Listing 7.87 est identique au précédent mais, au lieu d’utili-ser les gestionnaires d’événements déjà implémentés, nous définissons des expressionslambda pour définir à la volée le code appelé par les événements.

Listing 7.87 : Gestion d’un événement XObject avec des expressions lambda.

XElement firstParticipant;

XDocument xDocument = new XDocument( new XElement("BookParticipants", firstParticipant = new XElement("BookParticipant", new XAttribute("type", "Author"),

Type de l’objet qui va être modifié : XText, Type du changement : SuppressionType de l’objet qui a été changé : XText, Type du changement : SuppressionDoc: Type de l’objet qui a été changé : XText, Type du changement : Suppression

Type de l’objet qui va être modifié : XText, Type du changement : AddType de l’objet qui a été modifié : XText, Type du changement : AddDoc: Type de l’objet qui a été modifié : XText, Type du changement : Add

<BookParticipants> <BookParticipant type="Author"> <FirstName>Seph</FirstName> <LastName>Rattz</LastName> </BookParticipant> <BookParticipant type="Editor"> <FirstName>Ewan</FirstName> <LastName>Buckingham</LastName> </BookParticipant></BookParticipants>

Linq.book Page 265 Mercredi, 18. février 2009 7:58 07

Page 281: LINQ Language Integrated Query en C

266 LINQ to XML Partie III

new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham"))));

Console.WriteLine("{0}{1}", xDocument, System.Environment.NewLine);

firstParticipant.Changing += new EventHandler<XObjectChangeEventArgs>( (object sender, XObjectChangeEventArgs cea) => Console.WriteLine("Type de l’objet qui va être modifié : {0}, Type of change: {1}", sender.GetType().Name, cea.ObjectChange));

firstParticipant.Changed += (object sender, XObjectChangeEventArgs cea) => Console.WriteLine("Type de l’objet qui a été modifié : {0}, Type du changement : {1}", sender.GetType().Name, cea.ObjectChange);

xDocument.Changed += (object sender, XObjectChangeEventArgs cea) => Console.WriteLine("Doc: Type de l’objet qui a été modifié : {0}, Type du changement : ➥{1}{2}", sender.GetType().Name, cea.ObjectChange, System.Environment.NewLine);

xDocument.Changed += new XObjectChangeEventHandler((sender, cea) => Console.WriteLine("Doc: Type de l’objet qui a été modifié : {0}, Type du changement : ➥{1}{2}", sender.GetType().Name, cea.ObjectChange, System.Environment.NewLine));

firstParticipant.Element("FirstName").Value = "Seph";

Console.WriteLine("{0}{1}", xDocument, System.Environment.NewLine);

Ce code se suffit à lui-même. Il ne dépend d’aucun des gestionnaires d’événementsprécédemment écrits. Voici les résultats :

<BookParticipants> <BookParticipant type="Author"> <FirstName>Joe</FirstName> <LastName>Rattz</LastName> </BookParticipant> <BookParticipant type="Editor"> <FirstName>Ewan</FirstName> <LastName>Buckingham</LastName> </BookParticipant></BookParticipants>

Type of object changing: XText, Type of change: RemoveType of object changed: XText, Type of change: RemoveDoc: Type of object changed: XText, Type of change: Remove

Type de l’objet qui va être modifié : XText, Type du changement : AddType de l’objet qui a été modifié : XText, Type du changement : AddDoc: Type de l’objet qui a été modifié : XText, Type du changement : Add

<BookParticipants> <BookParticipant type="Author"> <FirstName>Seph</FirstName> <LastName>Rattz</LastName> </BookParticipant> <BookParticipant type="Editor">

Linq.book Page 266 Mercredi, 18. février 2009 7:58 07

Page 282: LINQ Language Integrated Query en C

Chapitre 7 L’API LINQ to XML 267

Les résultats sont identiques à ceux du listing précédent. Avouez que les expressionslambda sont vraiment pratiques et efficaces. Les développeurs qui donnent leurspremières impressions sur LINQ disent souvent qu’ils n’apprécient pas les expressionslambda. Peut-être est-ce parce qu’elles sont nouvelles et très différentes. Mais avouezque cet exemple a de quoi les réconcilier avec ce nouvel outil.

Le bogue d’Halloween

Vous rappelez-vous du "bogue d’Halloween", introduit au début de ce chapitre ? Degrâce, résistez à l’envie qui vous poussera certainement à intervenir sur la portiond’arbre XML dans laquelle vous capturez des événements. Le contenu de l’arbre XMLet les événements générés pourraient en effet prendre une tournure incontrôlable.

Résumé

Dans ce chapitre, nous avons vu comment utiliser LINQ to XML pour créer, modifier etparcourir des documents XML, ainsi que pour interroger des objets XML à l’aide derequêtes. Vous avez pu voir que la nouvelle API apporte une grande flexibilité : ellepermet de créer un élément XML à la volée, de l’initialiser et de le placer dans un arbreXML en une seule instruction. L’API W3C DOM XML en est totalement incapable.C’est la raison pour laquelle l’API LINQ to XML a été conçue.

Ce chapitre vous a montré comment appliquer une requête LINQ sur un objet XMLunique. Les requêtes portaient par exemple sur les descendants ou les ancêtres d’unélément. À travers de nouveaux opérateurs XML, le chapitre suivant va vous montrercomment appliquer une requête LINQ sur une séquence d’éléments (les descendantsd’une séquence, par exemple).

<FirstName>Ewan</FirstName> <LastName>Buckingham</LastName> </BookParticipant></BookParticipants>

Linq.book Page 267 Mercredi, 18. février 2009 7:58 07

Page 283: LINQ Language Integrated Query en C

Linq.book Page 268 Mercredi, 18. février 2009 7:58 07

Page 284: LINQ Language Integrated Query en C

8

Les opérateurs LINQ to XML

Les requêtes prises en exemple au chapitre précédent se contentaient de retourner tousles éléments enfants ou tous les ancêtres d’un nœud. Vous rappelez-vous des exemplesqui faisaient appel à la méthode XContainer.Elements ? Dans l’affirmative, vous savezce qu’est une requête XML. C’est là une autre preuve de l’intégration parfaite desrequêtes LINQ dans le langage : il est parfois facile d’oublier que l’on est en traind’effectuer une requête.

Comme beaucoup des méthodes examinées jusqu’ici retournent une séquence d’objetsXML, c’est-à-dire des IEnumerable<T> (où T est une classe de l’API LINQ to XML), ilest possible d’appeler les opérateurs de requête standard sur la séquence retournée, cequi procure encore plus de puissance et de flexibilité.

Il est donc possible d’obtenir une séquence d’objets XML à partir d’un objet XMLunique (les descendants ou les ancêtres d’un objet, par exemple) mais, ce qui manque,ce sont des opérateurs qui pourraient s’appliquer sur chacun des éléments de cesséquences. À titre d’exemple, il n’existe aucune façon simple d’obtenir une séquenced’éléments et d’effectuer une autre opération XML spécifique sur chacun des élémentsde la séquence retournée, comme connaître les éléments enfants de chacun deséléments de la séquence. Pour dire les choses autrement, vous pouvez obtenir uneséquence des éléments enfants d’un élément en appelant la méthode Elements de cetélément, mais vous ne pouvez pas obtenir une séquence des éléments enfants deséléments enfants d’un élément. Ceci parce que la méthode Elements doit être appeléesur un XContainer (XElement ou XDocument, par exemple), mais pas sur une séquenced’objets XContainer. C’est à ce point précis que les opérateurs LINQ to XML vontvous venir en aide.

Linq.book Page 269 Mercredi, 18. février 2009 7:58 07

Page 285: LINQ Language Integrated Query en C

270 LINQ to XML Partie III

Introduction aux opérateurs LINQ to XML

L’API LINQ to XML étend les opérateurs de requête standard de LINQ to Objects en yajoutant des opérateurs spécifiques au XML. Ces opérateurs sont des méthodes d’exten-sion définies dans la classe System.Xml.Linq.Extensions, qui joue le rôle d’une classeconteneur.

Chacun de ces opérateurs est appelé sur une séquence d’un type de donnée LINQ toXML et effectue une action sur chacune des entrées de cette séquence. Il retourne parexemple les ancêtres ou les descendants des différentes entrées.

Virtuellement, chacun des opérateurs XML décrits dans ce chapitre a un équivalentdans le chapitre précédent. Cependant, les méthodes du chapitre précédent ne s’appli-quent qu’à un objet unique, alors que les opérateurs de ce chapitre s’appliquent à uneséquence d’objets. À titre d’exemple, au chapitre précédent, nous avons parlé de laméthode XContainer.Elements, dont voici le prototype :

IEnumerable<XElement> XContainer.Elements()

Dans ce chapitre, nous aborderons l’opérateur Extensions.Elements, dont voici leprototype :

IEnumerable<XElement> Elements<T> (this IEnumerable<T> source) where T : XContainer

Il existe une différence de taille entre ces deux méthodes : le premier prototype estappelé sur un objet unique dérivé de XContainer, alors que le second est appelé sur uneséquence d’objets dont chacun est dérivé de XContainer.

Pour bien différencier les méthodes du chapitre précédent des méthodes d’extensionsde ce chapitre, nous qualifierons les secondes du terme "opérateurs".

Et, maintenant, il est temps d’entrer dans le vif du sujet.

Opérateur Ancestors

L’opérateur Ancestors est appelé sur une séquence de nœuds. Il retourne une séquencequi contient les éléments ancêtres de chacun des nœuds sources.

Prototypes

L’opérateur Ancestors a deux prototypes.

Premier prototype

public static IEnumerable<XElement> Ancestors<T> ( this IEnumerable<T> source) where T : XNode

Cette version de l’opérateur peut être appelée sur une séquence de nœuds ou d’objetsdérivés de XNode. Elle retourne une séquence d’éléments contenant les ancêtres dechacun des nœuds de la séquence source.

Linq.book Page 270 Mercredi, 18. février 2009 7:58 07

Page 286: LINQ Language Integrated Query en C

Chapitre 8 Les opérateurs LINQ to XML 271

Second prototype

public static IEnumerable<XElement> Ancestors<T> ( this IEnumerable<T> source, XName name) where T : XNode

Ce prototype est identique au précédent mais, ici, un nom est passé dans les arguments.Seuls les ancêtres qui correspondent à ce nom sont retournés dans la séquence de sortie.

Exemples

Le Listing 8.1 donne un exemple d’appel du premier prototype.

Listing 8.1 : Un exemple d’appel du premier prototype de l’opérateur Ancestors.

XDocument xDocument = new XDocument( new XElement("BookParticipants", new XElement("BookParticipant", new XAttribute("type", "Author"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham"))));

IEnumerable<XElement> elements = xDocument.Element("BookParticipants").Descendants("FirstName");

// Affichage des éléments sourcesforeach (XElement element in elements){ Console.WriteLine("Element source : {0} : Valeur = {1}", element.Name, element.Value);}

// Affichage des éléments ancêtres des éléments sourcesforeach (XElement element in elements.Ancestors()){ Console.WriteLine("Elément ancêtre : {0}", element.Name);}

Les premières lignes de ce code définissent un document XML. Une séquenced’éléments FirstName est alors générée (rappelez-vous, la méthode Ancestors estappelée sur une séquence de nœuds, et non sur un nœud unique. Il est donc nécessairede créer une séquence). Pour faciliter l’identification des nœuds, nous allons afficherleurs noms. Étant donné que les éléments ont un nom, mais pas les nœuds, nous avonschoisi de définir une séquence d’éléments, et non de nœuds. Le dernier bloc de codeénumère les éléments retournés par la méthode Ancestors et les affiche. Voici les résultats :

Élément source : FirstName : valeur = JoeÉlément source : FirstName : valeur = EwanÉlément ancêtre : BookParticipantÉlément ancêtre : BookParticipantsÉlément ancêtre : BookParticipantÉlément ancêtre : BookParticipants

Linq.book Page 271 Mercredi, 18. février 2009 7:58 07

Page 287: LINQ Language Integrated Query en C

272 LINQ to XML Partie III

Comme vous pouvez le voir, ces résultats affichent les deux éléments de la séquencesource, puis les ancêtres de ces éléments.

L’opérateur Ancestors retourne tous les éléments ancêtres de chaque nœud sous laforme d’une séquence de nœuds. Dans cet exemple, la séquence utilisée est composéed’éléments, mais cela ne pose pas de problème, puisque les éléments sont dérivés deXNode. Assurez-vous que vous faites bien la différence entre l’opérateur Ancestors,appelé sur une séquence de nœuds, et la méthode Ancestors, étudiée au chapitreprécédent.

Cet exemple n’est pas aussi impressionnant qu’il peut le paraître. Le code a en effet étéétendu à des fins démonstratives. Nous avons ainsi utilisé quelques lignes de code pourénumérer les éléments de la séquence FirstName (appel à la méthode Descendants etbloc foreach suivant). La seconde boucle foreach appelle l’opérateur Ancestors etaffiche les ancêtres. Dans cette deuxième boucle, il aurait été possible d’appeler laméthode Ancestors du chapitre précédent sur chacun des éléments de la séquenced’éléments FirstName. Cette technique est illustrée dans le Listing 8.2.

Listing 8.2 : Même résultat que le listing précédent, mais sans appeler l’opérateur Ancestors.

XDocument xDocument = new XDocument( new XElement("BookParticipants", new XElement("BookParticipant", new XAttribute("type", "Author"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham"))));

IEnumerable<XElement> elements = xDocument.Element("BookParticipants").Descendants("FirstName");

// Affichage des éléments sourcesforeach (XElement element in elements){ Console.WriteLine("Elément source: {0} : valeur = {1}", element.Name, element.Value);}

foreach (XElement element in elements){ // Appel de la méthode Ancestors sur chaque élément foreach(XElement e in element.Ancestors()) // Affichage des ancêtres de chaque élément source Console.WriteLine("Elément ancêtre : {0}", e.Name);}

Cet exemple est différent du précédent : ici, au lieu d’appeler l’opérateur Ancestorssur les éléments de la séquence dans la boucle foreach, la boucle applique la méthode

Linq.book Page 272 Mercredi, 18. février 2009 7:58 07

Page 288: LINQ Language Integrated Query en C

Chapitre 8 Les opérateurs LINQ to XML 273

Ancestors du chapitre précédent à chacun des éléments de la séquence. Le résultat estle même que celui du listing précédent :

Grâce à l’opérateur Ancestors et à la concision de LINQ, cette requête peut être résumée àune déclaration bien plus réduite (voir Listing 8.3).

Listing 8.3 : Un exemple concis d’appel de l’opérateur Ancestors.

XDocument xDocument = new XDocument( new XElement("BookParticipants", new XElement("BookParticipant", new XAttribute("type", "Author"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham"))));

foreach (XElement element in xDocument.Element("BookParticipants").Descendants("FirstName").Ancestors()){ Console.WriteLine("Elément ancêtre : {0}", element.Name);}

Dans cet exemple, l’opérateur Ancestors est directement appelé sur la séquenced’éléments retournés par la méthode Descendants. Cette dernière retourne uneséquence d’éléments, et l’opérateur Ancestors retourne une autre séquence d’élémentsqui contient tous les ancêtres de chacun des éléments de la première séquence.

Contrairement aux deux listings précédents, les éléments FirstName ne sont pas affichés.Mais, bien évidemment, les ancêtres sont les mêmes :

En production, vous opterez certainement pour un code concis, semblable à celuiprésenté dans le Listing 8.3. Cependant, dans la suite de ce chapitre, nous utiliserons uncode plus verbeux, comparable à celui du Listing 8.1.

Pour illustrer le second prototype de l’opérateur Ancestors, nous utiliserons le mêmecode que dans le Listing 8.1, mais nous changerons l’appel à l’opérateur Ancestors, desorte qu’il limite la sortie aux ancêtres ayant pour valeur BookParticipant (voirListing 8.4).

Elément source : FirstName : valeur = JoeElément source : FirstName : valeur = EwanElément ancêtre : BookParticipantElément ancêtre : BookParticipantsElément ancêtre : BookParticipantElément ancêtre : BookParticipants

Elément ancêtre : BookParticipantElément ancêtre : BookParticipantsElément ancêtre : BookParticipantElément ancêtre : BookParticipants

Linq.book Page 273 Mercredi, 18. février 2009 7:58 07

Page 289: LINQ Language Integrated Query en C

274 LINQ to XML Partie III

Listing 8.4 : Appel du second prototype de l’opérateur Ancestors.

XDocument xDocument = new XDocument( new XElement("BookParticipants", new XElement("BookParticipant", new XAttribute("type", "Author"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham"))));

IEnumerable<XElement> elements = xDocument.Element("BookParticipants").Descendants("FirstName");

// Affichage des éléments sourcesforeach (XElement element in elements){ Console.WriteLine("Elément source : {0} : valeur = {1}", element.Name, element.Value);}

// Affichage des ancêtres de chaque élément source foreach (XElement element in elements.Ancestors("BookParticipant")){ Console.WriteLine("Elément ancêtre : {0}", element.Name);}

Les résultats sont semblables à ceux du Listing 8.1 mais, cette fois-ci, les ancêtresBookParticipants ne sont pas affichés :

Opérateur AncestorsAndSelf

L’opérateur AncestorsAndSelf est appelé sur une séquence d’éléments. Il retourne uneséquence qui contient les éléments ancêtres de chacun des éléments sources, ainsi quel’élément source. Cet opérateur est assez proche de l’opérateur Ancestors, si ce n’estqu’il ne peut être appelé que sur des éléments et qu’il inclut l’élément source dans laséquence de sortie.

Prototypes

L’opérateur AncestorsAndSelf a deux prototypes.

Premier prototype

public static IEnumerable<XElement> AncestorsAndSelf ( this IEnumerable<XElement> source)

Elément source : FirstName : valeur = JoeElément source : FirstName : valeur = EwanElément ancêtre : BookParticipantElément ancêtre : BookParticipant

Linq.book Page 274 Mercredi, 18. février 2009 7:58 07

Page 290: LINQ Language Integrated Query en C

Chapitre 8 Les opérateurs LINQ to XML 275

Ce prototype de l’opérateur AncestorsAndSelf est appelé sur une séquence d’éléments.Il retourne une séquence d’éléments composée des éléments sources et de leurséléments ancêtres.

Second prototype

public static IEnumerable<XElement> AncestorsAndSelf<T> ( this IEnumerable<XElement> source, XName name)

Ce prototype est identique au précédent mais, ici, un nom est passé dans les arguments.Seuls les éléments sources et les ancêtres qui correspondent à ce nom sont retournésdans la séquence de sortie.

Exemples

Pour illustrer le premier prototype de l’opérateur AncestorsAndSelf, nous utiliseronsle même exemple que dans le Listing 8.1 mais, ici, nous appellerons l’opérateur Ances-torsAndSelf et non l’opérateur Ancestors (voir Listing 8.5).

Listing 8.5 : Appel du premier prototype de l’opérateur AncestorsAndSelf.

XDocument xDocument = new XDocument( new XElement("BookParticipants", new XElement("BookParticipant", new XAttribute("type", "Author"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham"))));

IEnumerable<XElement> elements = xDocument.Element("BookParticipants").Descendants("FirstName");

// Affichage des éléments sourcesforeach (XElement element in elements){ Console.WriteLine("Elément source : {0} : valeur = {1}", element.Name, element.Value);}

// Affichage des éléments sources et de leurs ancêtresforeach (XElement element in elements.AncestorsAndSelf()){ Console.WriteLine("Elément ancêtre : {0}", element.Name);}

Le premier bloc de code crée le document XML. Une séquence d’éléments FirstNameest ensuite générée (la méthode AncestorsAndSelf étant appelée sur une séquenced’éléments, et non sur un élément unique, il est donc nécessaire de créer une séquence).Les éléments de la séquence source sont ensuite énumérés et affichés. Enfin, laséquence retournée par AncestorsAndSelf est énumérée et les éléments résultants, affichés.

Linq.book Page 275 Mercredi, 18. février 2009 7:58 07

Page 291: LINQ Language Integrated Query en C

276 LINQ to XML Partie III

Si tout fonctionne comme prévu, les résultats devraient être identiques à ceux affichéspar le premier exemple du prototype Ancestors mais, ici, les éléments de la séquenceFirstName devraient également être inclus.

Pour illustrer le second prototype de l’opérateur AncestorsAndSelf, nous utiliserons lemême code que dans l’exemple du second prototype de l’opérateur Ancestors. Maisici, bien entendu, nous utiliserons l’opérateur AncestorsAndSelf et non l’opérateurAncestors (voir Listing 8.6).

Listing 8.6 : Appel du second prototype de l’opérateur AncestorsAndSelf.

XDocument xDocument = new XDocument( new XElement("BookParticipants", new XElement("BookParticipant", new XAttribute("type", "Author"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham"))));

IEnumerable<XElement> elements = xDocument.Element("BookParticipants").Descendants("FirstName");

// Affichage des éléments sourcesforeach (XElement element in elements){ Console.WriteLine("Elément source: {0} : valeur = {1}", element.Name, element.Value);}

// Affichage des ancêtres de chaque élément source foreach (XElement element in elements.AncestorsAndSelf("BookParticipant")){ Console.WriteLine("Elément ancêtre: {0}", element.Name);}

Voici les résultats. Les ancêtres FirstName et BookParticipants ont été éliminés, carils ne correspondent pas au paramètre passé à l’opérateur AncestorsAndSelf :

Elément source : FirstName : valeur = JoeElément source : FirstName : valeur = EwanElément ancêtre : FirstNameElément ancêtre : BookParticipantElément ancêtre : BookParticipantsElément ancêtre : FirstNameElément ancêtre : BookParticipantElément ancêtre : BookParticipants

Elément source : FirstName : valeur = JoeElément source : FirstName : valeur = EwanElément ancêtre : BookParticipantElément ancêtre : BookParticipant

Linq.book Page 276 Mercredi, 18. février 2009 7:58 07

Page 292: LINQ Language Integrated Query en C

Chapitre 8 Les opérateurs LINQ to XML 277

Le second prototype de cet opérateur semble avoir peu d’intérêt. En effet, pensez-vousque deux niveaux d’éléments ou plus portant le même nom puissent cohabiter dans unarbre XML ?

Opérateur Attributes

L’opérateur Attributes est appelé sur une séquence d’éléments. Il retourne uneséquence contenant les attributs de chacun des éléments sources.

Prototypes

L’opérateur Attributes a deux prototypes.

Premier prototype

public static IEnumerable<XAttribute> Attributes ( this IEnumerable<XElement> source)

Ce premier prototype est appelé sur une séquence d’éléments. Il retourne une séquencecontenant tous les attributs des éléments sources.

Second prototype

public static IEnumerable<XAttribute> Attributes ( this IEnumerable<XElement> source, XName name)

Ce prototype est identique au précédent mais, ici, seuls les attributs qui correspondentau nom passé en argument sont retournés dans la séquence de sortie.

Exemples

Pour illustrer le premier prototype, nous allons ajouter des attributs à l’arbre XMLutilisé dans les exemples précédents. Nous travaillerons donc avec une séquenced’éléments BookParticipant (voir Listing 8.7).

Listing 8.7 : Appel du premier prototype Attributes.

XDocument xDocument = new XDocument( new XElement("BookParticipants", new XElement("BookParticipant", new XAttribute("type", "Author"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham"))));

IEnumerable<XElement> elements = xDocument.Element("BookParticipants").Elements("BookParticipant");

// Affichage des éléments sources

Linq.book Page 277 Mercredi, 18. février 2009 7:58 07

Page 293: LINQ Language Integrated Query en C

278 LINQ to XML Partie III

foreach (XElement element in elements){ Console.WriteLine("Elément source : {0} : valeur = {1}", element.Name, element.Value);}

// Affichage des attributs des éléments sourcesforeach (XAttribute attribute in elements.Attributes()){ Console.WriteLine("Attribut : {0} : valeur = {1}", attribute.Name, attribute.Value);}

La séquence d’éléments BookParticipant est générée puis affichée. L’opérateurAttributes est alors appelé sur cette séquence et les attributs des éléments sont affichésà l’aide d’une boucle foreach. Voici les résultats :

Pour illustrer le second prototype, nous utiliserons le même code que dans l’exempleprécédent, mais nous passerons un nom à l’opérateur Attributes. Seuls les attributsportant ce nom seront inclus dans la séquence de sortie (voir Listing 8.8).

Listing 8.8 : Appel du second prototype Attributes.

XDocument xDocument = new XDocument( new XElement("BookParticipants", new XElement("BookParticipant", new XAttribute("type", "Author"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham"))));

IEnumerable<XElement> elements = xDocument.Element("BookParticipants").Elements("BookParticipant");

// Affichage des éléments sourcesforeach (XElement element in elements){ Console.WriteLine("Elément source : {0} : valeur = {1}", element.Name, element.Value);}

// Affichage des attributs des éléments sourcesforeach (XAttribute attribute in elements.Attributes("type")){ Console.WriteLine("Attribut : {0} : valeur = {1}", attribute.Name, attribute.Value);}

Elément source : BookParticipant : valeur = JoeRattzElément source : BookParticipant : valeur = EwanBuckinghamAttribut : type : valeur = AuthorAttribut : type : valeur = Editor

Linq.book Page 278 Mercredi, 18. février 2009 7:58 07

Page 294: LINQ Language Integrated Query en C

Chapitre 8 Les opérateurs LINQ to XML 279

Seuls les attributs portant le nom "type" sont retournés dans la séquence de sortie. Voiciles résultats obtenus suite à l’appui sur Ctrl+F5 :

Si nous avions passé le paramètre "type" à l’opérateur Attributes, les deux attributsn’auraient pas été affichés. Cet opérateur est donc sensible à la casse, ce qui n’a rien desurprenant, puisque XML est un langage sensible à la casse.

Opérateur DescendantNodes

L’opérateur DescendantNodes est appelé sur une séquence d’éléments ou de docu-ments. Il retourne une séquence contenant les nœuds descendants de chacun deséléments ou documents sources.

Prototype

L’opérateur DescendantNodes a un seul prototype :

public static IEnumerable<XNode> DescendantNodes<T> ( this IEnumerable<T> source) where T : XContainer

Cet opérateur est différent de la méthode XContainer.DescendantNodes. Le premierest appelé sur une séquence d’éléments ou de documents, la deuxième, sur un élémentou un document unique.

Exemple

Nous utiliserons le même arbre XML que dans les exemples précédents mais, ici, nousajouterons un commentaire dans le premier élément BookParticipant. Ceci afin quel’opérateur DescendantNodes retourne au moins un nœud qui n’est pas un élément. Leséléments BookParticipant ayant plusieurs descendants, nous leur appliqueronsl’opérateur DescendantNodes (voir Listing 8.9).

Listing 8.9 : Appel du prototype de l’opérateur DescendantNodes.

XDocument xDocument = new XDocument( new XElement("BookParticipants", new XElement("BookParticipant", new XAttribute("type", "Author"), new XComment("Nouvel auteur"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham"))));

Elément source : BookParticipant : valeur = JoeRattzElément source : BookParticipant : valeur = EwanBuckinghamAttribut : type : valeur = AuthorAttribut : type : valeur = Editor

Linq.book Page 279 Mercredi, 18. février 2009 7:58 07

Page 295: LINQ Language Integrated Query en C

280 LINQ to XML Partie III

IEnumerable<XElement> elements = xDocument.Element("BookParticipants").Elements("BookParticipant");

// Affichage des éléments sourcesforeach (XElement element in elements){ Console.WriteLine("Elément source : {0} : valeur = {1}", element.Name, element.Value);}

// Affichage des nœuds descendants des éléments sourcesforeach (XNode node in elements.DescendantNodes()){ Console.WriteLine("Nœud descendant : {0}", node);}

Les premières lignes définissent l’arbre XML. Une séquence d’éléments BookPartici-pant est alors définie. Les éléments de cette séquence sont affichés, puis l’opérateurDescendantNodes lui est appliqué. Voici les résultats :

Comme vous pouvez le voir, l’opérateur DescendantNodes renvoie tous les nœudsdescendants de la séquence BookParticipant : les éléments, mais également lecommentaire. Remarquez aussi que chacun des éléments descendants donne lieu à deuxnœuds. Par exemple, <FirstName>Joe</FirstName> et Joe sont les deux nœudsdescendants relatifs à l’élément Joe. Le premier est l’élément lui-même et le deuxième,sa valeur XText. Je suis sûr que vous aviez oublié que des objets XText sont automati-quement créés pour chaque élément…

Opérateur DescendantNodesAndSelf

L’opérateur DescendantNodesAndSelf est appelé sur une séquence d’éléments. Ilretourne une séquence contenant les éléments sources et leurs nœuds descendants.

Prototype

L’opérateur DescendantNodesAndSelf a un seul prototype :

public static IEnumerable<XNode> DescendantNodesAndSelf ( this IEnumerable<XElement> source)

Elément source : BookParticipant : valeur = JoeRattzElément source : BookParticipant : valeur = EwanBuckinghamNoeud descendant : <!—Nouvel auteur-->Noeud descendant : <FirstName>Joe</FirstName>Noeud descendant : JoeNoeud descendant : <LastName>Rattz</LastName>Noeud descendant : RattzNoeud descendant : <FirstName>Ewan</FirstName>Noeud descendant : EwanNoeud descendant : <LastName>Buckingham</LastName>Noeud descendant : Buckingham

Linq.book Page 280 Mercredi, 18. février 2009 7:58 07

Page 296: LINQ Language Integrated Query en C

Chapitre 8 Les opérateurs LINQ to XML 281

Exemple

Nous utiliserons le même code que pour illustrer l’opérateur DescendantNodes mais,ici, nous appellerons l’opérateur DescendantNodesAndSelf (voir Listing 8.10).

Listing 8.10 : Appel du prototype de l’opérateur DescendantNodesAndSelf.

XDocument xDocument = new XDocument( new XElement("BookParticipants", new XElement("BookParticipant", new XAttribute("type", "Author"), new XComment("Nouvel auteur"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham"))));

IEnumerable<XElement> elements = xDocument.Element("BookParticipants").Elements("BookParticipant");

// Affichage des éléments sourcesforeach (XElement element in elements){ Console.WriteLine("Elément source : {0} : valeur = {1}", element.Name, element.Value);}

// Affichage des noeuds descendants des éléments sourcesforeach (XNode node in elements.DescendantNodesAndSelf()){ Console.WriteLine("Noeud descendant : {0}", node);}

Voici le résultat :

L’opérateur DescendantNodesAndSelf a retourné les éléments de la séquence d’entrée etleurs nœuds descendants, y compris le commentaire du premier élément BookParticipant.

Elément source : BookParticipant : valeur = JoeRattzElément source : BookParticipant : valeur = EwanBuckinghamNoeud descendant : <BookParticipant type="Author"><!—Nouvel auteur--><FirstName>Joe</FirstName><LastName>Rattz</LastName></BookParticipant>Noeud descendant : <!—Nouvel auteur-->Noeud descendant : <FirstName>Joe</FirstName>Noeud descendant : JoeNoeud descendant : <LastName>Rattz</LastName>Noeud descendant : RattzNoeud descendant : <BookParticipant type="Editor"><FirstName>Ewan</FirstName><LastName>Buckingham</LastName></BookParticipant>Noeud descendant : <FirstName>Ewan</FirstName>Noeud descendant : EwanNoeud descendant : <LastName>Buckingham</LastName>Noeud descendant : Buckingham

Linq.book Page 281 Mercredi, 18. février 2009 7:58 07

Page 297: LINQ Language Integrated Query en C

282 LINQ to XML Partie III

Comme vous avez pu le voir dans l’exemple précédent, l’opérateur DescendantNodes"oublie" le commentaire dans la séquence de sortie. Cette différence sera étudiée un peuplus loin dans ce chapitre.

Opérateur Descendants

L’opérateur Descendants peut être appelé sur une séquence d’éléments ou de docu-ments. Il retourne une séquence qui contient tous les éléments descendants deséléments ou documents sources.

Prototypes

L’opérateur Descendants a deux prototypes.

Premier prototype

public static IEnumerable<XElement> Descendants<T> ( this IEnumerable<T> source) where T : XContainer

Cet opérateur est différent de la méthode XContainer.Descendants. Le premier estappelé sur une séquence d’éléments ou de documents, la deuxième, sur un élément ouun document unique.

Second prototype

public static IEnumerable<XElement> Descendants<T> ( this IEnumerable<T> source, XName name) where T : XContainer

Ce prototype est identique au précédent mais, ici, seuls les descendants des élémentssources dont le nom correspond au paramètre sont retournés dans la séquence de sortie.

Exemples

Pour illustrer le premier prototype, nous allons utiliser le même code que pour l’opéra-teur DescendantNodes, mais nous allons appeler l’opérateur Descendants. Les résultatsdevraient être les mêmes, à ceci près que seuls les éléments devraient être retournésdans la séquence de sortie. Le Listing 8.11 représente le code utilisé pour illustrer ceprototype.

Listing 8.11 : Appel du premier prototype de l’opérateur Descendants.

XDocument xDocument = new XDocument( new XElement("BookParticipants", new XElement("BookParticipant", new XAttribute("type", "Author"), new XComment("Nouvel auteur"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"),

Linq.book Page 282 Mercredi, 18. février 2009 7:58 07

Page 298: LINQ Language Integrated Query en C

Chapitre 8 Les opérateurs LINQ to XML 283

new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham"))));

IEnumerable<XElement> elements = xDocument.Element("BookParticipants").Elements("BookParticipant");

// Affichage des éléments sourcesforeach (XElement element in elements){ Console.WriteLine("Elément source: {0} : valeur = {1}", element.Name, element.Value);}

// Affichage des nœuds descendants des éléments sourcesforeach (XNode node in elements.Descendants()){ Console.WriteLine("Nœud descendant : {0}", node);}

Seuls les éléments descendants des deux éléments BookParticipant sont inclus dans laséquence de sortie :

En comparant ces résultats à ceux de l’opérateur DescendantNodes, nous pouvons noterplusieurs différences :

m les descendants apparaissent en tant qu’éléments et non en tant que nœuds ;

m le commentaire n’est pas inclus dans la séquence de sortie ;

m les nœuds descendants (Joe et Ratz, par exemple) sont exclus de la séquence desortie, puisqu’ils sont de type XText et non XElement.

Nous illustrerons le second prototype avec le même code mais, ici, nous passerons unnom dans l’argument de l’opérateur. Seuls les descendants correspondants seront inclusdans la séquence de sortie (voir Listing 8.12).

Listing 8.12 : Appel du second prototype de l’opérateur Descendants.

XDocument xDocument = new XDocument( new XElement("BookParticipants", new XElement("BookParticipant", new XAttribute("type", "Author"), new XComment("Nouvel auteur"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham"))));

IEnumerable<XElement> elements =

Elément source : BookParticipant : valeur = JoeRattzElément source : BookParticipant : valeur = EwanBuckinghamElément descendant : <FirstName>Joe</FirstName>Elément descendant : <LastName>Rattz</LastName>Elément descendant : <FirstName>Ewan</FirstName>Elément descendant : <LastName>Buckingham</LastName>

Linq.book Page 283 Mercredi, 18. février 2009 7:58 07

Page 299: LINQ Language Integrated Query en C

284 LINQ to XML Partie III

xDocument.Element("BookParticipants").Elements("BookParticipant");

// Affichage des éléments sourcesforeach (XElement element in elements){ Console.WriteLine("Elément source : {0} : valeur = {1}", element.Name, element.Value);}

// Affichage des nœuds descendants des éléments sourcesforeach (XNode node in elements.Descendants("LastName")){ Console.WriteLine("Nœud descendant : {0}", node);}

Voici les résultats. Comme on pouvait s’y attendre, seul le descendant LastName estinclus dans la séquence de sortie :

Opérateur DescendantsAndSelf

L’opérateur DescendantsAndSelf est appelé sur une séquence d’éléments. Il retourneune séquence qui contient tous les éléments descendants des éléments sources.

Prototypes

L’opérateur DescendantsAndSelf a deux prototypes.

Premier prototype

public static IEnumerable<XElement> DescendantsAndSelf ( this IEnumerable<XElement> source)

Ce prototype est appelé sur une séquence d’éléments. Il retourne une séquence quicontient tous les éléments de la séquence et leurs descendants.

Second prototype

public static IEnumerable<XElement> DescendantsAndSelf ( this IEnumerable<XElement> source, XName name)

Le second prototype est semblable au premier, mais seuls les éléments qui correspon-dent au paramètre sont retournés dans la séquence de sortie.

Exemples

Pour illustrer le premier prototype, nous utiliserons le même code que dans le premierexemple de l’opérateur Descendants mais, ici, nous appellerons l’opérateur Descen-dantAndSelf (voir Listing 8.13).

Elément source : BookParticipant : valeur = JoeRattzElément source : BookParticipant : valeur = EwanBuckinghamElément descendant : <LastName>Rattz</LastName>Elément descendant : <LastName>Buckingham</LastName>

Linq.book Page 284 Mercredi, 18. février 2009 7:58 07

Page 300: LINQ Language Integrated Query en C

Chapitre 8 Les opérateurs LINQ to XML 285

Listing 8.13 : Appel du premier prototype de l’opérateur DescendantsAndSelf.

XDocument xDocument = new XDocument( new XElement("BookParticipants", new XElement("BookParticipant", new XAttribute("type", "Author"), new XComment("Nouvel auteur"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham"))));

IEnumerable<XElement> elements = xDocument.Element("BookParticipants").Elements("BookParticipant");

// Affichage des éléments sourcesforeach (XElement element in elements){ Console.WriteLine("Elément source : {0} : valeur = {1}", element.Name, element.Value);}

// Affichage des nœuds descendants des éléments sourcesforeach (XNode node in elements.DescendantsAndSelf()){ Console.WriteLine("Nœud descendant : {0}", node);}

Voici les résultats :

Les résultats sont identiques à ceux du premier prototype de l’opérateur Descendants, àceci près qu’ils incluent également les éléments sources eux-mêmes, c’est-à-dire leséléments BookParticipant.

Ne soyez pas trompé par la présence du commentaire dans les résultats. Cet objet estnon pas un résultat retourné par l’opérateur, mais bel et bien une partie de la séquence

Elément source : BookParticipant : valeur = JoeRattzElément source : BookParticipant : valeur = EwanBuckinghamElément descendant : <BookParticipant type="Author"><!—Nouvel auteur--><FirstName>Joe</FirstName><LastName>Rattz</LastName></BookParticipant>Elément descendant : <FirstName>Joe</FirstName>Elément descendant : <LastName>Rattz</LastName>Elément descendant : <BookParticipant type="Editor"><FirstName>Ewan</FirstName><LastName>Buckingham</LastName></BookParticipant>Elément descendant : <FirstName>Ewan</FirstName>Elément descendant : <LastName>Buckingham</LastName>

Linq.book Page 285 Mercredi, 18. février 2009 7:58 07

Page 301: LINQ Language Integrated Query en C

286 LINQ to XML Partie III

d’entrée incluse dans les résultats (c’est la partie Self de l’opérateur DescendantsAnd-Self).

Pour illustrer le second prototype, nous utiliserons le même code, mais nous passeronsun paramètre à l’opérateur pour limiter la sortie (voir Listing 8.14).

Listing 8.14 : Appel du second prototype de l’opérateur DescendantsAndSelf.

XDocument xDocument = new XDocument( new XElement("BookParticipants", new XElement("BookParticipant", new XAttribute("type", "Author"), new XComment("Nouvel auteur"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham"))));

IEnumerable<XElement> elements = xDocument.Element("BookParticipants").Elements("BookParticipant");

// Affichage des éléments sourcesforeach (XElement element in elements){ Console.WriteLine("Elément source : {0} : valeur = {1}", element.Name, element.Value);}

// Affichage des nœuds descendants des éléments sourcesforeach (XNode node in elements.DescendantsAndSelf("LastName")){ Console.WriteLine("Nœud descendant : {0}", node);}

Voici les résultats :

La sortie est bien plus limitée que dans l’exemple précédent. Il est même difficile defaire la différence entre les opérateurs Descendants et DescendantsAndSelf. Ceci vientdu fait que les éléments sources n’ont pas été retournés, car ils ne correspondaient pasau paramètre passé à l’opérateur.

Il est peu probable que vous ayez à utiliser la version "AndSelf" du second prototypede l’opérateur Descendants. En effet, les arbres XML que vous manipulerez n’ontque peu de chances d’avoir des éléments portant le même nom sur plusieurs niveauxhiérarchiques.

Elément source : BookParticipant : valeur = JoeRattzElément source : BookParticipant : valeur = EwanBuckinghamElément descendant : <LastName>Rattz</LastName>Elément descendant : <LastName>Buckingham</LastName>

Linq.book Page 286 Mercredi, 18. février 2009 7:58 07

Page 302: LINQ Language Integrated Query en C

Chapitre 8 Les opérateurs LINQ to XML 287

Opérateur Elements

L’opérateur Elements peut être appelé sur une séquence d’éléments ou de documents. Ilretourne une séquence d’éléments qui contient tous les éléments enfants des élémentsou documents sources.

Les opérateurs Elements et Descendants sont différents. En effet, l’opérateur Elementsne retourne que les éléments enfants de premier niveau, alors que l’opérateur Descen-dants retourne tous les enfants de la séquence d’entrée, en parcourant récursivementtous les niveaux hiérarchiques de l’arborescence.

Prototypes

L’opérateur Elements a deux prototypes.

Premier prototype

public static IEnumerable<XElement> Elements<T> ( this IEnumerable<T> source) where T : XContainer

Ce premier prototype est appelé sur une séquence d’éléments ou de documents. Ilretourne une séquence d’éléments qui contient tous les éléments enfants des élémentsou documents sources.

Cet opérateur est différent de la méthode XContainer.Elements. Le premier est appelésur une séquence d’éléments ou de documents, la deuxième, sur un élément ou undocument unique.

Second prototype

public static IEnumerable<XElement> Elements<T> ( this IEnumerable<T> source, XName name) where T : XContainer

Ce prototype est identique au premier mais, ici, seuls les éléments correspondant auparamètre passé à l’opérateur sont retournés dans la séquence de sortie.

Exemples

Nous utiliserons le même code que dans l’exemple du premier prototype de l’opérateurDescendantsAndSelf mais, ici, nous invoquerons l’opérateur Elements (voir Listing 8.15).

Listing 8.15 : Appel du premier prototype de l’opérateur Elements.

XDocument xDocument = new XDocument( new XElement("BookParticipants", new XElement("BookParticipant", new XAttribute("type", "Author"), new XComment("Nouvel auteur"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"),

Linq.book Page 287 Mercredi, 18. février 2009 7:58 07

Page 303: LINQ Language Integrated Query en C

288 LINQ to XML Partie III

new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham"))));

IEnumerable<XElement> elements = xDocument.Element("BookParticipants").Elements("BookParticipant");

// Affichage des éléments sourcesforeach (XElement element in elements){ Console.WriteLine("Elément source : {0} : valeur = {1}", element.Name, element.Value);}

// Affichage des nœuds descendants des éléments sourcesforeach (XElement element in elements.Elements()){ Console.WriteLine("Elément enfant : {0}", element);}

Voici les résultats :

Cet exemple retourne tous les éléments enfants de la séquence d’entrée. Pour limiter laséquence de sortie aux seuls éléments dont le nom est spécifié, nous utiliserons lesecond prototype de l’opérateur Elements (voir Listing 8.16).

Listing 8.16 : Appel du second prototype de l’opérateur Elements.

XDocument xDocument = new XDocument( new XElement("BookParticipants", new XElement("BookParticipant", new XAttribute("type", "Author"), new XComment("Nouvel auteur"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham"))));

IEnumerable<XElement> elements = xDocument.Element("BookParticipants").Elements("BookParticipant");

// Affichage des éléments sourcesforeach (XElement element in elements){ Console.WriteLine("Elément source : {0} : valeur = {1}", element.Name, element.Value);}

// Affichage des nœuds descendants des éléments sourcesforeach (XElement element in elements.Elements("LastName")){ Console.WriteLine("Elément enfant : {0}", element);}

Elément source : BookParticipant : valeur = JoeRattzElément source : BookParticipant : valeur = EwanBuckinghamElément enfant : <FirstName>Joe</FirstName>Elément enfant : <LastName>Rattz</LastName>Elément enfant : <FirstName>Ewan</FirstName>Elément enfant : <LastName>Buckingham</LastName>

Linq.book Page 288 Mercredi, 18. février 2009 7:58 07

Page 304: LINQ Language Integrated Query en C

Chapitre 8 Les opérateurs LINQ to XML 289

Voici les résultats :

Opérateur InDocumentOrder

L’opérateur InDocumentOrder est appelé sur une séquence de nœuds. Il retourne uneséquence composée des nœuds enfants des nœuds sources, dans l’ordre du document.

Prototype

L’opérateur InDocumentOrder a un seul prototype :

public static IEnumerable<T> InDocumentOrder<T> ( this IEnumerable<T> source) where T : XNode

Cet opérateur doit être appelé sur une séquence composée de nœuds ou d’objets déri-vés. Il retourne une séquence du même type composée des nœuds enfants des nœudssources, dans l’ordre du document.

Exemple

Pour illustrer cet opérateur, nous avons besoin d’une séquence de nœuds, éléments etnon éléments. Pour ce faire, nous utiliserons la séquence des nœuds enfants deséléments BookParticipant. L’un des nœuds est un commentaire, pas un élément.Nous verrons ainsi comment l’opérateur InDocumentOrder se comporte sur ce type denœud (voir Listing 8.17).

Listing 8.17 : Appel du prototype de l’opérateur InDocumentOrder.

XDocument xDocument = new XDocument( new XElement("BookParticipants", new XElement("BookParticipant", new XAttribute("type", "Author"), new XComment("Nouvel auteur"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham"))));

IEnumerable<XNode> nodes = xDocument.Element("BookParticipants").Elements("BookParticipant"). Nodes().Reverse();

Elément source : BookParticipant : valeur = JoeRattzElément source : BookParticipant : valeur = EwanBuckinghamElément enfant : <LastName>Rattz</LastName>Elément enfant : <LastName>Buckingham</LastName>

Linq.book Page 289 Mercredi, 18. février 2009 7:58 07

Page 305: LINQ Language Integrated Query en C

290 LINQ to XML Partie III

// Affichage des nœuds sourcesforeach (XNode node in nodes){ Console.WriteLine("Noeud source : {0}", node);}// Affichage des noeuds enfants des noeuds sourcesforeach (XNode node in nodes.InDocumentOrder()){ Console.WriteLine("Noeud ordonné : {0}", node);}

Après avoir construit l’arbre XML, les nœuds enfants des éléments BookParticipantssont obtenus en invoquant l’opérateur Nodes. L’opérateur Reverse est appliqué aurésultat de l’opérateur Nodes pour inverser l’ordre de la séquence (si nécessaire, repor-tez-vous à la section relative à l’opérateur LINQ to SQL Reverse, dans la deuxièmepartie de l’ouvrage, pour avoir des informations complémentaires).

La séquence utilisée en entrée de l’opérateur InDocumentOrder est donc composée desnœuds des éléments BookParticipant, disposés dans l’ordre inverse de celui du docu-ment. Voici le résultat :

Comme vous pouvez le voir, les nœuds sources sont dans l’ordre inverse des nœuds dela séquence de sortie.

Opérateur Nodes

L’opérateur Nodes peut être appelé sur une séquence d’éléments ou de documents. Ilretourne une séquence de nœuds composée des nœuds enfants des éléments/documentssources.

Cet opérateur est différent de l’opérateur DescendantNodes, car il ne retourne que leséléments enfants de premier niveau, alors que l’opérateur DescendantNodes retournetous les enfants de la séquence d’entrée, en parcourant récursivement tous les niveauxhiérarchiques de l’arborescence.

Prototype

L’opérateur Nodes n’a qu’un seul prototype :

public static IEnumerable<XNode> Nodes<T> ( this IEnumerable<T> source) where T : XContainer

Noeud source : <LastName>Buckingham</LastName>Noeud source : <FirstName>Ewan</FirstName>Noeud source : <LastName>Rattz</LastName>Noeud source : <FirstName>Joe</FirstName>Noeud source : <!—Nouvel auteur-->Noeud ordonné : <!--Nouvel auteur-->Noeud ordonné : <FirstName>Joe</FirstName>Noeud ordonné : <LastName>Rattz</LastName>Noeud ordonné : <FirstName>Ewan</FirstName>Noeud ordonné : <LastName>Buckingham</LastName>

Linq.book Page 290 Mercredi, 18. février 2009 7:58 07

Page 306: LINQ Language Integrated Query en C

Chapitre 8 Les opérateurs LINQ to XML 291

Cet opérateur est différent de la méthode XContainer.Nodes. Le premier est appelé surune séquence d’éléments ou de documents, la deuxième, sur un élément ou un documentunique.

Exemple

Nous allons définir un arbre XML, créer une séquence source d’éléments BookParti-cipant et lui appliquer l’opérateur Nodes. Comme toujours, nous afficherons leséléments sources et ceux retournés par l’opérateur (voir Listing 8.18).

Listing 8.18 : Appel du prototype de l’opérateur Nodes.

XDocument xDocument = new XDocument( new XElement("BookParticipants", new XElement("BookParticipant", new XAttribute("type", "Author"), new XComment("Nouvel auteur"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham"))));

IEnumerable<XElement> elements = xDocument.Element("BookParticipants").Elements("BookParticipant");

// Affichage des éléments sourcesforeach (XElement element in elements){ Console.WriteLine("Elément source : {0} : valeur = {1}", element.Name, element.Value);}

// Affichage des noeuds enfants des éléments sourcesforeach (XNode node in elements.Nodes()){ Console.WriteLine("Noeud enfant : {0}", node);}

L’opérateur Nodes retourne une séquence de nœuds (et non d’éléments) enfants de laséquence d’entrée. Le commentaire devrait donc être inclus dans la séquence de sortie.Voici les résultats :

Étant donné que seuls les nœuds enfants de premier niveau sont retournés par l’opéra-teur Nodes, les nœuds XText, enfants des éléments FirstName et LastName, ne sont pas

Elément source : BookParticipant : valeur = JoeRattzElément source : BookParticipant : valeur = EwanBuckinghamNoeud enfant : <!—Nouvel auteur-->Noeud enfant : <FirstName>Joe</FirstName>Noeud enfant : <LastName>Rattz</LastName>Noeud enfant : <FirstName>Ewan</FirstName>Noeud enfant : <LastName>Buckingham</LastName>

Linq.book Page 291 Mercredi, 18. février 2009 7:58 07

Page 307: LINQ Language Integrated Query en C

292 LINQ to XML Partie III

retournés. Si vous retournez quelques pages en arrière, vous verrez que l’opérateurDescendantNodes les incluait dans la séquence de sortie.

Opérateur Remove

L’opérateur Remove est appelé sur une séquence de nœuds ou d’attributs à supprimer.Pour éviter le bogue d’Halloween, introduit au chapitre précédent, les nœuds/attributssont mémorisés dans une liste.

Prototypes

L’opérateur Remove a deux prototypes.

Premier prototype

public static void Remove ( this IEnumerable<XAttribute> source)

Ce prototype est appelé sur une séquence d’attributs. Il supprime tous les attributs de laséquence d’entrée.

Second prototype

public static void Remove<T> ( this IEnumerable<T> source) where T : XNode

Ce prototype est appelé sur une séquence de nœuds (ou d’autres types qui en sont déri-vés). Il supprime tous les nœuds de la séquence d’entrée.

Exemples

Pour illustrer le premier prototype, nous avons besoin d’une séquence d’attributs. Nousallons donc utiliser notre arbre XML standard et travailler sur une séquence composéedes attributs des éléments BookParticipant. Nous allons afficher la séquence des attri-buts sources, appeler l’opérateur Remove sur cette séquence, puis afficher le documentXML dans sa totalité, pour nous assurer que l’opérateur Remove a bien fait son travail(voir Listing 8.19).

Listing 8.19 : Appel du premier prototype de l’opérateur Remove.

XDocument xDocument = new XDocument( new XElement("BookParticipants", new XElement("BookParticipant", new XAttribute("type", "Author"), new XComment("Nouvel auteur"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham"))));

Linq.book Page 292 Mercredi, 18. février 2009 7:58 07

Page 308: LINQ Language Integrated Query en C

Chapitre 8 Les opérateurs LINQ to XML 293

IEnumerable<XAttribute> attributes = xDocument.Element("BookParticipants").Elements("BookParticipant").Attributes();

// Affichage des attributs sourcesforeach (XAttribute attribute in attributes){ Console.WriteLine("Attribut source : {0} : valeur = {1}", attribute.Name, attribute.Value);}

attributes.Remove();

// Affichage du document XMLConsole.WriteLine(xDocument);

Voici les résultats :

Nous allons maintenant illustrer le second prototype. Plutôt que nous contenter d’obte-nir puis de supprimer une séquence de nœuds, nous allons envisager quelque chose deplus intéressant : extraire la séquence de commentaires de certains éléments et supprimeruniquement ces objets (voir Listing 8.20).

Listing 8.20 : Appel du second prototype.

XDocument xDocument = new XDocument( new XElement("BookParticipants", new XElement("BookParticipant", new XAttribute("type", "Author"), new XComment("Nouvel auteur"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham"))));

IEnumerable<XComment> comments = xDocument.Element("BookParticipants").Elements("BookParticipant"). Nodes().OfType<XComment>();

// Affichage des commentaires sourcesforeach (XComment comment in comments){ Console.WriteLine("Source comment: {0}", comment);}

Attribut source : type : valeur = AuthorAttribut source : type : valeur = Editor<BookParticipants> <BookParticipant> <!--Nouvel auteur--> <FirstName>Joe</FirstName> <LastName>Rattz</LastName> </BookParticipant> <BookParticipant> <FirstName>Ewan</FirstName> <LastName>Buckingham</LastName> </BookParticipant></BookParticipants>

Linq.book Page 293 Mercredi, 18. février 2009 7:58 07

Page 309: LINQ Language Integrated Query en C

294 LINQ to XML Partie III

comments.Remove();

// Affichage du document XMLConsole.WriteLine(xDocument);

Après avoir construit la séquence source, les nœuds enfants (Nodes) de type XComment(OfType<XComment>) sont placés dans la séquence comments. Reportez-vous si nécessaire àla deuxième partie de ce livre pour en savoir plus sur l’opérateur de requête standard OfType.

La méthode Remove est alors appliquée à la séquence comments. Après l’exécution decet opérateur, l’arbre XML est privé de tout commentaire dans les éléments BookParti-cipant. Voici le résultat :

L’opérateur OfType est très pratique et il s’intègre parfaitement dans une requête LINQto XML. Il pourrait se révéler très utile en situation réelle.

Résumé

Au chapitre précédent, nous avons introduit l’API LINQ to XML et montré commentl’utiliser pour créer, modifier, sauvegarder et lire des arbres XML. Nous avons intention-nellement utilisé le mot "arbre" et non le mot "document", car avec LINQ to XML il n’estplus nécessaire de manipuler des documents. Nous avons également montré commenteffectuer une requête sur un nœud/un élément pour atteindre les nœuds/éléments qui luisont hiérarchiquement liés. Dans ce chapitre, vous avez également appris à interroger desséquences de nœuds ou d’éléments en utilisant les opérateurs de LINQ to XML. Arrivé àce point dans la lecture du livre, vous devriez être en mesure d’effectuer des requêtesélémentaires sur des arbres XML en utilisant les opérateurs LINQ to XML. Cettenouvelle API devrait se révéler très utile pour interroger des données… en particulier sivous lui adjoignez des opérateurs de requête standard.

Vous connaissez maintenant toutes les techniques de base permettant de définir desrequêtes LINQ to SQL. Au chapitre suivant, nous aborderons des requêtes légèrementplus complexes et nous nous intéresserons à d’autres domaines d’action de LINQ toXML tels que la validation et la transformation.

Source comment: <!—Nouvel auteur--><BookParticipants> <BookParticipant type="Author"> <FirstName>Joe</FirstName> <LastName>Rattz</LastName> </BookParticipant> <BookParticipant type="Editor"> <FirstName>Ewan</FirstName> <LastName>Buckingham</LastName> </BookParticipant></BookParticipants>

Linq.book Page 294 Mercredi, 18. février 2009 7:58 07

Page 310: LINQ Language Integrated Query en C

9

Les autres possibilités de XML

Dans les deux chapitres précédents, vous avez appris à créer, à modifier et à parcourirdes données XML en utilisant l’API LINQ to XML. Nous avons également vucomment utiliser des blocs de construction pour créer des requêtes XML très puissan-tes. Je pense que, dès à présent, vous serez d’accord pour affirmer que LINQ to XMLpeut couvrir 90 % de vos besoins en matière de XML. Mais qu’en est-il des 10 %restants ? Voyons si nous pouvons diminuer ce pourcentage. Si Microsoft avait ajouté lavalidation de schéma, les transformations et les requêtes XPath, quel serait le pourcen-tage selon vous ?

Nous avons vu les bases de l’API LINQ to XML et comment effectuer les requêtesélémentaires. Nous allons maintenant nous intéresser à des requêtes plus complexes etaussi plus proches du monde réel. Dans ce chapitre, nous allons passer en revue quel-ques exemples qui, je l’espère, rendront à vos yeux les requêtes XML des plus trivialeslorsqu’elles seront effectuées via l’API LINQ to XML.

Pour décrire plus complètement cette API, nous aborderons des fonctionnalités complé-mentaires (essentiellement la transformation et la validation) et vous donnerons diversesinformations bonnes à connaître en LINQ to XML.

D’une façon plus spécifique, nous verrons comment effectuer des transformations avecet sans XSLT, comment valider un document XML par rapport à un schéma et donne-rons un exemple de requête utilisant le style XPath.

Espaces de noms référencés

Outre les espaces de noms LINQ et LINQ to XML désormais traditionnels,System.Linq et System.Xml.Linq, les exemples de ce chapitre utilisent également les espa-ces de noms System.Xml, System.Xml.Schema, System.Xml.Xsl et System.Xml.XPath.

Linq.book Page 295 Mercredi, 18. février 2009 7:58 07

Page 311: LINQ Language Integrated Query en C

296 LINQ to XML Partie III

À moins qu’elles ne soient déjà présentes dans votre code, vous devrez donc ajouter lesdirectives using suivantes :

using System.Linq;using System.Xml;using System.Xml.Linq;using System.Xml.Schema;using System.Xml.XPath;using System.Xml.Xsl;

Requêtes

Dans le chapitre précédent, nous avons vu les principes de base permettant d’exécuterdes requêtes XML via LINQ to XML. La plupart des exemples avaient pour but l’illus-tration d’un opérateur ou d’une propriété. Dans cette section, nous allons passer enrevue plusieurs exemples "orientés solution" et, donc, plus proches de la réalité.

La description du chemin n’est pas une obligation

Dans les chapitres précédents, la plupart des exemples "plongeaient" dans la hiérarchieXML pour obtenir une référence sur un élément particulier en utilisant les opérateursElement ou Elements de façon récursive, jusqu’à ce que l’élément visé soit atteint.

Ainsi, beaucoup d’exemples contenaient ce type d’instruction :

IEnumerable<XElement> elements = xDocument.Element("BookParticipants").Elements("BookParticipant");

Cet exemple accède à l’élément enfant BookParticipants du document, puis auxéléments enfants BookParticipant de l’élément BookParticipants. Cette techniquen’est pas toujours nécessaire. Vous pouvez en effet utiliser un code comparable auListing 9.1.

Listing 9.1 : Accès à des éléments sans décrire leur chemin.

XDocument xDocument = new XDocument( new XElement("BookParticipants", new XElement("BookParticipant", new XAttribute("type", "Author"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham"))));

IEnumerable<XElement> elements = xDocument.Descendants("BookParticipant");

foreach (XElement element in elements){ Console.WriteLine("Elément: {0} : valeur = {1}", element.Name, element.Value);}

Linq.book Page 296 Mercredi, 18. février 2009 7:58 07

Page 312: LINQ Language Integrated Query en C

Chapitre 9 Les autres possibilités de XML 297

Dans cet exemple, l’instruction en gras obtient les descendants BookParticipant dudocument. Étant donné que l’accès ne se fait pas dans une branche particulière del’arbre XML, il est nécessaire de connaître le schéma, car il serait possible d’accéderpar erreur à certaines branches de l’arbre. Cependant, cette technique fonctionne dansde nombreux cas. Voici les résultats :

Si tous les éléments BookParticipant ne sont pas utiles, vous pouvez restreindre larequête. Le Listing 9.2, par exemple, ne retourne que les éléments dont l’élément First-Name a pour valeur "Ewan".

Listing 9.2 : Accès restreint à des éléments sans décrire le chemin.

XDocument xDocument = new XDocument( new XElement("BookParticipants", new XElement("BookParticipant", new XAttribute("type", "Author"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham"))));

IEnumerable<XElement> elements = xDocument .Descendants("BookParticipant") .Where(e => ((string)e.Element("FirstName")) == "Ewan");

foreach (XElement element in elements){ Console.WriteLine("Elément: {0} : valeur = {1}", element.Name, element.Value);}

Cette fois-ci, nous avons appliqué l’opérateur Where en suffixe dans la définition del’objet elements. Remarquez l’utilisation de l’opérateur de casting (string) pourcomparer la valeur de l’élément avec la chaîne "Ewan". Voici les résultats :

Il est parfois nécessaire de contrôler l’ordre des résultats. Dans le Listing 9.3, nousallons modifier l’expression lambda de l’opérateur Where pour que deux élémentssoient retournés. La requête portera sur l’attribut type.

Listing 9.3 : Accès restreint à des éléments sans décrire le chemin, en définissant l’ordre et en utilisant la syntaxe d’interrogation des requêtes.

XDocument xDocument = new XDocument( new XElement("BookParticipants", new XElement("BookParticipant", new XAttribute("type", "Author"),

Elément: BookParticipant : valeur = JoeRattzElément: BookParticipant : valeur = EwanBuckingham

Elément: BookParticipant : valeur = EwanBuckingham

Linq.book Page 297 Mercredi, 18. février 2009 7:58 07

Page 313: LINQ Language Integrated Query en C

298 LINQ to XML Partie III

new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham"))));

IEnumerable<XElement> elements = from e in xDocument.Descendants("BookParticipant") where ((string)e.Attribute("type")) != "Illustrator" orderby ((string)e.Element("LastName")) select e;

foreach (XElement element in elements){ Console.WriteLine("Elément: {0} : valeur = {1}", element.Name, element.Value);}

La requête porte toujours sur les éléments BookParticipant du document mais, ici,seuls les éléments dont l’attribut type a une valeur différente de "Illustrator" sont sélec-tionnés. Dans cet arbre, cela correspond à tous les éléments BookParticipant. Lesrésultats sont alors classés par éléments LastName croissants. Remarquez l’utilisationd’opérateurs de casting pour obtenir la valeur de l’attribut type et de l’élément Last-Name. Voici les résultats :

Une requête complexe

Jusqu’ici, toutes les requêtes passées en revue étaient simplistes. Avant de passer à unautre sujet, nous allons étudier une requête complexe. Nous utiliserons des donnéesmises à disposition par le W3C à des fins de tests.

L’exemple du Listing 9.4 contient des données issues de trois documents XML diffé-rents. Ces documents sont obtenus en divisant une représentation texte des documentsXML suggérés par le W3C. Nous allons expliquer de façon détaillée chacune desparties du code.

La première étape va consister à définir les documents en utilisant du code XML.

Listing 9.4 : Une requête complexe qui effectue une jointure sur trois documents en utilisant la syntaxe d’expression de requête de LINQ.

XDocument users = XDocument.Parse( @"<users> <user_tuple> <userid>U01</userid> <name>Tom Jones</name> <rating>B</rating> </user_tuple> <user_tuple> <userid>U02</userid> <name>Mary Doe</name>

Elément: BookParticipant : valeur = EwanBuckinghamElément: BookParticipant : valeur = JoeRattz

Linq.book Page 298 Mercredi, 18. février 2009 7:58 07

Page 314: LINQ Language Integrated Query en C

Chapitre 9 Les autres possibilités de XML 299

<rating>A</rating> </user_tuple> <user_tuple> <userid>U03</userid> <name>Dee Linquent</name> <rating>D</rating> </user_tuple> <user_tuple> <userid>U04</userid> <name>Roger Smith</name> <rating>C</rating> </user_tuple> <user_tuple> <userid>U05</userid> <name>Jack Sprat</name> <rating>B</rating> </user_tuple> <user_tuple> <userid>U06</userid> <name>Rip Van Winkle</name> <rating>B</rating> </user_tuple> </users>");

XDocument items = XDocument.Parse( @"<items> <item_tuple> <itemno>1001</itemno> <description>Red Bicycle</description> <offered_by>U01</offered_by> <start_date>1999-01-05</start_date> <end_date>1999-01-20</end_date> <reserve_price>40</reserve_price> </item_tuple> <item_tuple> <itemno>1002</itemno> <description>Motorcycle</description> <offered_by>U02</offered_by> <start_date>1999-02-11</start_date> <end_date>1999-03-15</end_date> <reserve_price>500</reserve_price> </item_tuple> <item_tuple> <itemno>1003</itemno> <description>Old Bicycle</description> <offered_by>U02</offered_by> <start_date>1999-01-10</start_date> <end_date>1999-02-20</end_date> <reserve_price>25</reserve_price> </item_tuple> <item_tuple> <itemno>1004</itemno> <description>Tricycle</description> <offered_by>U01</offered_by> <start_date>1999-02-25</start_date> <end_date>1999-03-08</end_date> <reserve_price>15</reserve_price> </item_tuple> <item_tuple> <itemno>1005</itemno> <description>Tennis Racket</description> <offered_by>U03</offered_by> <start_date>1999-03-19</start_date> <end_date>1999-04-30</end_date>

Linq.book Page 299 Mercredi, 18. février 2009 7:58 07

Page 315: LINQ Language Integrated Query en C

300 LINQ to XML Partie III

<reserve_price>20</reserve_price> </item_tuple> <item_tuple> <itemno>1006</itemno> <description>Helicopter</description> <offered_by>U03</offered_by> <start_date>1999-05-05</start_date> <end_date>1999-05-25</end_date> <reserve_price>50000</reserve_price> </item_tuple> <item_tuple> <itemno>1007</itemno> <description>Racing Bicycle</description> <offered_by>U04</offered_by> <start_date>1999-01-20</start_date> <end_date>1999-02-20</end_date> <reserve_price>200</reserve_price> </item_tuple> <item_tuple> <itemno>1008</itemno> <description>Broken Bicycle</description> <offered_by>U01</offered_by> <start_date>1999-02-05</start_date> <end_date>1999-03-06</end_date> <reserve_price>25</reserve_price> </item_tuple> </items>");

XDocument bids = XDocument.Parse( @"<bids> <bid_tuple> <userid>U02</userid> <itemno>1001</itemno> <bid>35</bid> <bid_date>1999-01-07</bid_date> </bid_tuple> <bid_tuple> <userid>U04</userid> <itemno>1001</itemno> <bid>40</bid> <bid_date>1999-01-08</bid_date> </bid_tuple> <bid_tuple> <userid>U02</userid> <itemno>1001</itemno> <bid>45</bid> <bid_date>1999-01-11</bid_date> </bid_tuple> <bid_tuple> <userid>U04</userid> <itemno>1001</itemno> <bid>50</bid> <bid_date>1999-01-13</bid_date> </bid_tuple> <bid_tuple> <userid>U02</userid> <itemno>1001</itemno> <bid>55</bid> <bid_date>1999-01-15</bid_date> </bid_tuple> <bid_tuple> <userid>U01</userid> <itemno>1002</itemno> <bid>400</bid>

Linq.book Page 300 Mercredi, 18. février 2009 7:58 07

Page 316: LINQ Language Integrated Query en C

Chapitre 9 Les autres possibilités de XML 301

<bid_date>1999-02-14</bid_date> </bid_tuple> <bid_tuple> <userid>U02</userid> <itemno>1002</itemno> <bid>600</bid> <bid_date>1999-02-16</bid_date> </bid_tuple> <bid_tuple> <userid>U03</userid> <itemno>1002</itemno> <bid>800</bid> <bid_date>1999-02-17</bid_date> </bid_tuple> <bid_tuple> <userid>U04</userid> <itemno>1002</itemno> <bid>1000</bid> <bid_date>1999-02-25</bid_date> </bid_tuple> <bid_tuple> <userid>U02</userid> <itemno>1002</itemno> <bid>1200</bid> <bid_date>1999-03-02</bid_date> </bid_tuple> <bid_tuple> <userid>U04</userid> <itemno>1003</itemno> <bid>15</bid> <bid_date>1999-01-22</bid_date> </bid_tuple> <bid_tuple> <userid>U05</userid> <itemno>1003</itemno> <bid>20</bid> <bid_date>1999-02-03</bid_date> </bid_tuple> <bid_tuple> <userid>U01</userid> <itemno>1004</itemno> <bid>40</bid> <bid_date>1999-03-05</bid_date> </bid_tuple> <bid_tuple> <userid>U03</userid> <itemno>1007</itemno> <bid>175</bid> <bid_date>1999-01-25</bid_date> </bid_tuple> <bid_tuple> <userid>U05</userid> <itemno>1007</itemno> <bid>200</bid> <bid_date>1999-02-08</bid_date> </bid_tuple> <bid_tuple> <userid>U04</userid> <itemno>1007</itemno> <bid>225</bid> <bid_date>1999-02-12</bid_date> </bid_tuple> </bids>");

Linq.book Page 301 Mercredi, 18. février 2009 7:58 07

Page 317: LINQ Language Integrated Query en C

302 LINQ to XML Partie III

Ces trois documents représentent les données (utilisateurs, objets vendus et enchères)manipulées sur un site web de vente aux enchères. Ils ont été créés en appelant laméthode XDocument.Parse sur des représentations chaînes des données.

La requête va consister à extraire les enchères supérieures à 50 dollars. Les résultatsdoivent faire apparaître la date, le montant de l’enchère, le nom de la personne qui enest à l’origine, le numéro de l’objet et sa description. Voici la requête :

var biddata = from b in bids.Descendants("bid_tuple") where ((double)b.Element("bid")) > 50 join u in users.Descendants("user_tuple") on ((string)b.Element("userid")) equals ((string)u.Element("userid")) join i in items.Descendants("item_tuple") on ((string)b.Element("itemno")) equals ((string)i.Element("itemno")) select new {Item = ((string)b.Element("itemno")), Description = ((string)i.Element("description")), User = ((string)u.Element("name")), Date = ((string)b.Element("bid_date")), Price = ((double)b.Element("bid"))};

La requête est plus complexe que celles étudiées jusqu’ici.

La première ligne utilise la méthode Descendants pour accéder aux descendantsbid_tuple du document bids. La ligne suivante utilise l’opérateur Where pour neconserver que les enchères supérieures à 50 dollars. Il peut sembler inhabituel d’utiliserune clause Where si tôt dans la requête. Cette clause aurait tout aussi bien pu être spéci-fiée juste avant la clause select, mais cela aurait signifié que le Where aurait été appli-qué sur la jointure entre les utilisateurs et les objets, y compris pour les enchèresinférieures à 50 dollars. En ayant réduit le nombre de données avant la jointure, lacharge de travail a ainsi été allégée pour la suite de la requête et les performances,améliorées.

Une fois limitées aux seules enchères supérieures à 50 dollars, les données sont jointesau document XML users par l’intermédiaire de l’élément userid (lignes 3 à 5), afind’obtenir le nom de chaque utilisateur. Arrivés à ce point dans la requête, nous avonsjoint les documents bids et users et limité les données aux enchères supérieures à50 dollars.

Les trois prochaines lignes (6 à 8) effectuent une jointure sur le document XML itemspar l’intermédiaire du champ itemno afin d’obtenir la description de l’objet. À ce point,les documents bids, users et items sont joints.

Remarquez que différents opérateurs de casting ont été utilisés pour obtenir la valeurdes éléments dans le type souhaité. Ainsi, par exemple, le montant de l’enchère a étéobtenu avec un opérateur (double). Les enchères sont au format string mais, étantdonné que leur contenu peut être converti en une valeur double, l’opérateur de castinga fait son travail.

Linq.book Page 302 Mercredi, 18. février 2009 7:58 07

Page 318: LINQ Language Integrated Query en C

Chapitre 9 Les autres possibilités de XML 303

La prochaine étape va consister à sélectionner une classe anonyme qui contient leséléments enfants des éléments issus de cette double jointure.

Nous allons commencer par afficher un en-tête :

Console.WriteLine("{0,-12} {1,-12} {2,-6} {3,-14} {4,10}", "Date", "User", "Item", "Description", "Price");

Console.WriteLine("===================================================");

Les instructions suivantes énumèrent la séquence et affichent les valeurs correspondantes :

bid:foreach (var bd in biddata){ Console.WriteLine("{0,-12} {1,-12} {2,-6} {3,-14} {4,10:C}", bd.Date, bd.User, bd.Item, bd.Description, bd.Price);}

Cette portion de code est triviale. En fait, mis à part la requête elle-même, tout le restedu code est simplissime. Voici les résultats :

Quelques lignes de code ont suffi pour joindre trois documents XML ! Maintenant, jesuis sûr que vous vous rendez compte de la puissance de LINQ to XML. Mais attendezun peu, d’autres possibilités très intéressantes vous attendent dans les pages suivantes…

Transformations

LINQ to XML vous permet d’effectuer des transformations en utilisant deux approchesdiamétralement opposées. La première consiste à utiliser XSLT via les classes passerel-les XmlReader et XmlWriter. La seconde approche consiste à utiliser LINQ to XML enconstruisant fonctionnellement le document XML cible et en incluant une requêteLINQ to XML dans le document source XML.

Date User Item Description Price

===================================================================================

1999-01-151999-02-141999-02-161999-02-171999-02-251999-03-021999-01-251999-02-081999-02-12

Mary DoeTom JonesMary DoeDee LinquentRoger SmithMary DoeDee LinquentJack SpratRoger Smith

100110021002100210021002100710071007

Red BicycleMotorcycleMotorcycleMotorcycleMotorcycleMotorcycleRacing BicycleRacing BicycleRacing Bicycle

$55.00$400.00$600.00$800.00$1,000.00$1,200.00$175.00$200.00$225.00

Linq.book Page 303 Mercredi, 18. février 2009 7:58 07

Page 319: LINQ Language Integrated Query en C

304 LINQ to XML Partie III

XSLT est une technologie XML standard. Des outils permettant d’écrire, de débogueret de tester les transformations XSLT sont d’ores et déjà disponibles. Par ailleurs, il estpossible que vous disposiez déjà de documents XSLT. Si tel est le cas, vous pouvez lesutiliser dans vos nouvelles applications par l’intermédiaire de LINQ to XML. Denombreux documents XSLT sont disponibles. Vous n’avez qu’à choisir celui quis’adapte le mieux à vos souhaits. De plus, l’utilisation de XSLT pour vos transforma-tions se révèle plus dynamique. Contrairement à l’approche "construction fonction-nelle" de LINQ to XML, il n’est pas nécessaire de recompiler le code pour changer latransformation : le simple fait de modifier le document XSLT suffit pour changer latransformation à l’exécution. Enfin, la technologie XSLT est bien connue et bonnombre de développeurs experts dans ce domaine peuvent vous assister. Ce fait n’estbien entendu plus d’actualité si vous choisissez l’approche "construction fonction-nelle".

L’approche "construction fonctionnelle" ne vous demandera pas un gros investisse-ment. Les transformations XML seront en effet effectuées par l’intermédiaire de LINQto XML. Si vous ne connaissez pas XSLT, et si vos besoins en matière de transforma-tions sont modestes, cette approche peut vous convenir. Par ailleurs, bien que laconstruction fonctionnelle soit moins pratique que la modification d’un documentXSLT, la nécessité d’avoir à recompiler le code pour modifier une transformation peutêtre considérée comme une sécurité supplémentaire : un tiers ne peut ainsi modifier undocument externe pour changer le sens d’une transformation.

Transformations avec XSLT

Pour effectuer une transformation XML en utilisant XSLT, vous utiliserez les classespasserelles XmlWriter et XmlReader. Vous les obtiendrez à partir des méthodes Create-Writer et CreateReader des classes XDocument.

L’exemple du Listing 9.5 demande quelques explications. Nous les donnerons au fur età mesure, en séparant le code en plusieurs blocs fonctionnels.

Listing 9.5 : Transformation d’un document XML avec XSLT.

string xsl = @"<xsl:stylesheet version=’1.0’ xmlns:xsl=’http://www.w3.org/1999/XSL/Transform’> <xsl:template match=’//BookParticipants’> <html> <body> <h1>Book Participants</h1> <table> <tr align=’left’> <th>Role</th> <th>First Name</th> <th>Last Name</th> </tr> <xsl:apply-templates></xsl:apply-templates> </table> </body>

Linq.book Page 304 Mercredi, 18. février 2009 7:58 07

Page 320: LINQ Language Integrated Query en C

Chapitre 9 Les autres possibilités de XML 305

</html> </xsl:template> <xsl:template match=’BookParticipant’> <tr> <td><xsl:value-of select=’@type’/></td> <td><xsl:value-of select=’FirstName’/></td> <td><xsl:value-of select=’LastName’/></td> </tr> </xsl:template> </xsl:stylesheet>";

Ce code se contente de définir quelques instructions XSL qui vont créer du code HTMLafin d’afficher les données XML BookParticipant dans un tableau HTML. Laprochaine étape va consister à créer le document XML avec les participants :

XDocument xDocument = new XDocument( new XElement("BookParticipants", new XElement("BookParticipant", new XAttribute("type", "Author"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham"))));

Ce code a déjà été utilisé à de maintes reprises dans les pages précédentes. C’est à partirde maintenant que la magie va opérer. Nous allons créer un document XDocument pourla version transformée. À partir de ce document, nous définirons un XmlWriter, nousinstancierons un objet XslCompiledTransform, nous chargerons l’objet transforméavec la feuille de style de transformation et nous transformerons le document XMLd’entrée en la sortie XmlWriter :

XDocument transformedDoc = new XDocument();using (XmlWriter writer = transformedDoc.CreateWriter()){ XslCompiledTransform transform = new XslCompiledTransform(); transform.Load(XmlReader.Create(new StringReader(xsl))); transform.Transform(xDocument.CreateReader(), writer);}Console.WriteLine(transformedDoc);

Voici le résultat de la transformation. Comme vous pouvez le voir, nous utilisons lespasserelles XmlWriter et XmlReader pour effectuer la transformation :

<html> <body> <h1>Book Participants</h1> <table> <tr align="left"> <th>Role</th> <th>First Name</th> <th>Last Name</th> </tr> <tr> <td>Author</td> <td>Joe</td> <td>Rattz</td> </tr>

Linq.book Page 305 Mercredi, 18. février 2009 7:58 07

Page 321: LINQ Language Integrated Query en C

306 LINQ to XML Partie III

Transformations avec la construction fonctionnelle

Cette section va vous montrer comment effectuer des transformations XSLT en utilisantl’API LINQ to XML. Logiquement parlant, une transformation peut être aussi simpleque la combinaison d’un arbre XML défini par la construction fonctionnelle et d’unerequête XML incorporée dans cet arbre.

Nous allons expliquer les transformations XML à travers un exemple. Dans denombreux autres exemples des chapitres dédiés à LINQ to XML, nous avons utilisél’arbre XML suivant :

<BookParticipants> <BookParticipant type="Author"> <FirstName>Joe</FirstName> <LastName>Rattz</LastName> </BookParticipant> <BookParticipant type="Editor"> <FirstName>Ewan</FirstName> <LastName>Buckingham</LastName> </BookParticipant></BookParticipants>

Supposons que nous devions transformer cet arbre XML comme suit :

<MediaParticipants type="book"> <Participant Role="Author" Name="Joe Rattz" /> <Participant Role="Editor" Name="Ewan Buckingham" / ></MediaParticipants>

Pour accomplir cette transformation, nous allons utiliser la construction fonctionnelleen incluant une requête dans l’arbre. Cette approche va consister à construire unnouveau document dont l’allure correspond à l’arbre XML cible en appliquant unerequête LINQ to XML au document XML source pour y piocher les données. C’est lastructure de l’arbre XML cible qui va guider la construction fonctionnelle et la logiquede la requête.

Étant donné que cette tâche est légèrement plus complexe que la plupart des exemplesLINQ to XML précédents, nous donnerons des explications chaque fois que cela estnécessaire (voir Listing 9.6).

Listing 9.6 : Transformation d’un document XML.

XDocument xDocument = new XDocument( new XElement("BookParticipants", new XElement("BookParticipant", new XAttribute("type", "Author"), new XElement("FirstName", "Joe"),

<tr> <td>Editor</td> <td>Ewan</td> <td>Buckingham</td> </tr> </table> </body></html>

Linq.book Page 306 Mercredi, 18. février 2009 7:58 07

Page 322: LINQ Language Integrated Query en C

Chapitre 9 Les autres possibilités de XML 307

new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham"))));

Console.WriteLine("Document XML original :");Console.WriteLine("{0}{1}{1}", xDocument, System.Environment.NewLine);

Ce code définit le document XML source que nous allons transformer. La prochaineétape consiste à construire le nouveau document et l’élément racine :

XDocument xTransDocument = new XDocument( new XElement("MediaParticipants",

Rappelez-vous que la structure de l’arbre XML de sortie guide la construction fonction-nelle. Arrivés à ce point, nous avons un document et l’élément racine, Mediapartici-pants. Nous devons maintenant ajouter l’attribut type à l’élément racine :

new XAttribute("type", "book"),

L’attribut type et sa valeur n’existent pas dans le document XML source. Ils ont doncété définis dans le code.

Maintenant que l’attribut type est défini, nous allons générer un élément Participantpour chacun des éléments BookParticipant du document XML original. Pour ce faire,il va suffire d’exécuter la requête suivante :

xDocument.Element("BookParticipants") .Elements("BookParticipant")

Ces deux lignes de code fournissent une séquence d’éléments BookParticipant. Nousallons maintenant générer et initialiser un élément Participant pour chaque élémentBookParticipant. Pour ce faire, nous utiliserons l’opérateur de projection Select :

.Select(e => new XElement("Participant",

Nous allons maintenant construire les attributs Role et Name de l’élément Participanten piochant leurs valeurs dans l’élément BookParticipant :

new XAttribute("Role", (string)e.Attribute("type")),new XAttribute("Name", (string)e.Element("FirstName") + " " + (string)e.Element("LastName"))))));

Enfin, nous affichons le document XML transformé :

Console.WriteLine("Document XML transformé :");Console.WriteLine(xTransDocument);

Voici le résultat, tout à fait conforme aux attentes :

Document XML original:<BookParticipants> <BookParticipant type="Author"> <FirstName>Joe</FirstName> <LastName>Rattz</LastName> </BookParticipant> <BookParticipant type="Editor"> <FirstName>Ewan</FirstName> <LastName>Buckingham</LastName>

Linq.book Page 307 Mercredi, 18. février 2009 7:58 07

Page 323: LINQ Language Integrated Query en C

308 LINQ to XML Partie III

Astuces

Vous devez connaître quelques astuces si vous prévoyez d’effectuer des transformationsXML via l’API LINQ to XML.

Simplification de tâches complexes avec les méthodes HelperLa totalité du code responsable d’une transformation ou d’une requête n’est pas obligéede se trouver dans le code de transformation lui-même : vous pouvez créer des méthodesHelper pour effectuer des transformations plus complexes.

Dans cette section, vous trouverez du code qui vous montrera comment créerune méthode Helper pour diviser une tâche complexe en plusieurs tâches moinscomplexes.

Transformation d’un document XML avec une méthode Helper

static IEnumerable<XElement> Helper(){ XElement[] elements = new XElement[] { new XElement("Element", "A"), new XElement("Element", "B")};

return(elements);}

Le Listing 9.7 débute par la construction d’un arbre XML. Le nœud racine, RootEle-ment, est créé lors de l’appel du constructeur. Pour créer les nœuds enfants, la méthodeHelper est appelée. Il n’est pas important de savoir ce que fait cette méthode. Ce quiimporte, c’est qu’elle va nous aider à construire l’arbre XML et qu’elle peut être appeléedepuis la construction fonctionnelle de l’arbre XML.

Listing 9.7 : Utilisation d’une méthode Helper pour transformer un document XML.

XElement xElement = new XElement("RootElement", Helper());Console.WriteLine(xElement);

Voici les résultats :

</BookParticipant></BookParticipants>

Document XML transformé :<MediaParticipants type="book"> <Participant Role="Author" Name="Joe Rattz" /> <Participant Role="Ed</MediaParticipants>

<RootElement> <Element>A</Element> <Element>B</Element></RootElement>

Linq.book Page 308 Mercredi, 18. février 2009 7:58 07

Page 324: LINQ Language Integrated Query en C

Chapitre 9 Les autres possibilités de XML 309

Comme il a été dit au Chapitre 7, le constructeur XElement sait comment gérer lesIEnumerable<T>. C’est justement le type retourné par la méthode Helper. La vie estbelle, n’est-ce pas ?

Suppression de nœuds à la construction avec la valeur nullPour une raison ou une autre (donnée manquante ou valeurs inappropriées, par exem-ple), il se peut que vous désiriez supprimer certains nœuds de la construction. Dans lasection "Création d’éléments avec XElement" du Chapitre 7, j’avais indiqué qu’il étaitpossible de passer la valeur null à un élément et que cela pourrait être utile lorsque l’oneffectue une transformation. Cette technique est en effet très utile, puisqu’elle supprimela construction du nœud correspondant.

Nous allons raisonner sur un exemple. Nous allons créer une séquence d’éléments, puislancer la construction d’un arbre XML basé sur cette séquence. Si la valeur d’unélément est "A", nous n’allons pas l’inclure dans la séquence de sortie. Dans ce cas,nous passerons la valeur null au constructeur (voir Listing 9.8).

Listing 9.8 : Suppression de nœuds à la construction en transmettant la valeur null au constructeur.

IEnumerable<XElement> elements = new XElement[] { new XElement("Element", "A"), new XElement("Element", "B")};

XElement xElement = new XElement("RootElement", elements.Select(e => (string)e != "A" ? new XElement(e.Name, (string)e) : null));

Console.WriteLine(xElement);

Les quatre premières lignes définissent une séquence d’éléments. Les deux lignessuivantes construisent l’élément racine et énumèrent la séquence d’entrée. L’opérateurSelect agit de façon binaire : il renvoie un élément de la séquence d’entrée si sa valeurest différente de "A". Dans le cas contraire, il renvoie la valeur null. Dans le premiercas, le constructeur ajoute cet élément à l’arbre XML. Dans le second, il l’ignore, ce quirevient à dire qu’il l’exclut de l’arbre XML. À titre d’information, remarquez que, pouraccéder à la valeur de l’élément, nous avons utilisé l’opérateur de casting (string) dansl’expression lambda de l’opérateur Select.

Voici les résultats :

Comme on pouvait s’y attendre, l’élément "A" ne fait pas partie de l’arbre. D’autresapproches sont possibles pour exclure un nœud de l’arbre XML. Par exemple, nousaurions tout aussi bien pu utiliser l’opérateur Where pour filtrer les éléments dont la

<RootElement> <Element>B</Element></RootElement>

Linq.book Page 309 Mercredi, 18. février 2009 7:58 07

Page 325: LINQ Language Integrated Query en C

310 LINQ to XML Partie III

valeur est égale à "A". Mais, ici, le propos était de montrer le résultat obtenu lorsque lavaleur null est passée à un constructeur.

Il existe d’autres façons d’utiliser ce concept. Supposons que vous deviez générer unarbre XML dans lequel certaines instances peuvent contenir un élément vide et quevous préfériez que ces éléments ne fassent pas partie de l’arbre (voir Listing 9.9).

Listing 9.9 : Cet exemple génère un élément vide.

IEnumerable<XElement> elements = new XElement[] { new XElement("BookParticipant", new XElement("Name", "Joe Rattz"), new XElement("Book", "Pro LINQ: Language Integrated Query in C# 2008")), new XElement("BookParticipant", new XElement("Name", "John Q. Public"))};

XElement xElement = new XElement("BookParticipants", elements.Select(e => new XElement(e.Name, new XElement(e.Element("Name").Name, e.Element("Name").Value), new XElement("Books", e.Elements("Book")))));

Console.WriteLine(xElement);

Le premier bloc de ce code génère une séquence composée de deux éléments BookPar-ticipants. Le premier a un élément enfant nommé Book, mais pas le second.

Le deuxième bloc construit un arbre XML en utilisant la séquence d’éléments dupremier bloc. Un élément nommé BookParticipant est créé. Le nom du participantpuis la liste des livres du participant sont ajoutés à l’arbre en tant qu’élément enfant del’élément BookParticipant. Voici le résultat de ce code :

<BookParticipants> <BookParticipant> <Name>Joe Rattz</Name> <Books> <Book>Pro LINQ: Language Integrated Query in C# 2008</Book> </Books> </BookParticipant> <BookParticipant> <Name>John Q. Public</Name> <Books /> </BookParticipant></BookParticipants>

L’arbre XML est bien conforme aux attentes. Remarquez que l’élément Books dudeuxième participant est vide. Comment supprimer cet élément de l’arbre ? LeListing 9.10 vous montre comment transmettre la valeur null au constructeur si laséquence source ne contient aucun élément Book.

Listing 9.10 : Cet exemple supprime les éléments vides de l’arborescence.

IEnumerable<XElement> elements = new XElement[] { new XElement("BookParticipant",

Linq.book Page 310 Mercredi, 18. février 2009 7:58 07

Page 326: LINQ Language Integrated Query en C

Chapitre 9 Les autres possibilités de XML 311

new XElement("Name", "Joe Rattz"), new XElement("Book", "Pro LINQ: Language Integrated Query in C# 2008")), new XElement("BookParticipant", new XElement("Name", "John Q. Public"))};

XElement xElement = new XElement("BookParticipants", elements.Select(e => new XElement(e.Name, new XElement(e.Element("Name").Name, e.Element("Name").Value), e.Elements("Book").Any() ? new XElement("Books", e.Elements("Book")) : null)));

Console.WriteLine(xElement);

Les instructions en gras représentent les modifications par rapport au Listing 9.8. Plutôtque créer un élément Books et d’y définir tous les éléments enfants Book, nous utilisonsici l’opérateur de requête standard Any, combiné à l’opérateur ternaire (if ? then :else) pour créer l’élément Books si et seulement si au moins un élément Book estprésent dans la séquence d’entrée. Si aucun élément Book n’est trouvé, l’opérateurternaire renvoie la valeur null au constructeur, qui élimine la création de l’élémentBooks correspondant. Voici les résultats du Listing 9.10 :

Comme vous pouvez le voir, l’élément Books a disparu du deuxième élément BookPar-ticipant.

Gestion de nœuds multiples de même niveau dans une structure aplatieDans certaines situations, lorsque vous réalisez une transformation XML, vous savezexactement combien d’éléments de chaque type vous voulez. Que se passe-t-il si, enplus des éléments souhaités, il existe un certain nombre d’éléments "parasites" demême niveau pour chaque entrée de la source XML ? Supposons que vous disposiez ducode XML suivant :

L’allure souhaitée de la source XML

<BookParticipants> <BookParticipant type="Author"> <FirstName>Joe</FirstName> <LastName>Rattz</LastName> <Nickname>Joey</Nickname> <Nickname>Null Pointer</Nickname> </BookParticipant> <BookParticipant type="Editor">

<BookParticipants> <BookParticipant> <Name>Joe Rattz</Name> <Books> <Book>Pro LINQ: Language Integrated Query in C# 2008</Book> </Books> </BookParticipant> <BookParticipant> <Name>John Q. Public</Name> </BookParticipant></BookParticipants>

Linq.book Page 311 Mercredi, 18. février 2009 7:58 07

Page 327: LINQ Language Integrated Query en C

312 LINQ to XML Partie III

<FirstName>Ewan</FirstName> <LastName>Buckingham</LastName> </BookParticipant></BookParticipants>

Supposons que vous vouliez "aplatir" la structure de telle sorte que le nœud racineBookParticipants ne contienne que des ensembles d’éléments FirstName, LastNameet NickName et que ces éléments ne soient pas inclus dans l’élément BookParticipant.Le code XML cible devrait avoir l’allure suivante :

L’allure des données XML après transformation

<BookParticipants> <!— BookParticipant --> <FirstName>Joe</FirstName> <LastName>Rattz</LastName> <Nickname>Joey</Nickname> <Nickname>Null Pointer</Nickname> <!— BookParticipant --> <FirstName>Ewan</FirstName> <LastName>Buckingham</LastName></BookParticipants>

Les commentaires ne sont pas nécessaires, mais ils permettent de mieux cerner lesdonnées manipulées. Sans eux, il serait difficile de savoir si le prénom apparaît avant ouaprès le nom de famille. Une lecture rapide des données XML pourrait ainsi laisserpenser qu’un certain "Ewan Rattz" fait partie du jeu de données.

Cet exemple étant plus complexe, nous donnerons des explications chaque fois que celasera nécessaire (voir Listing 9.11).

Listing 9.11 : Gestion de nœuds multiples de même niveau dans une structure aplatie.

XDocument xDocument = new XDocument( new XElement("BookParticipants", new XElement("BookParticipant", new XAttribute("type", "Author"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz"), new XElement("Nickname", "Joey"), new XElement("Nickname", "Null Pointer")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham"))));

Console.WriteLine("Document XML original :");Console.WriteLine("{0}{1}{1}", xDocument, System.Environment.NewLine);

À ce point du code, l’arbre XML source a été construit et affiché. Il correspond bienentendu à l’arbre présenté au début de cette section. Il ne nous reste plus qu’à transfor-mer le code source XML :

XDocument xTransDocument = new XDocument( new XElement("BookParticipants", xDocument.Element("BookParticipants") .Elements("BookParticipant")

Linq.book Page 312 Mercredi, 18. février 2009 7:58 07

Page 328: LINQ Language Integrated Query en C

Chapitre 9 Les autres possibilités de XML 313

C’est ici que l’analyse est intéressante. Doit-on utiliser une projection via l’opérateurSelect pour créer un objet dans lequel seront placés les commentaires, le prénom, lenom et le ou les surnoms ? Dans ce cas, quel type d’objet doit-on créer ? Nous pour-rions créer un élément, puis ajouter les commentaires, prénoms, noms et surnomscomme éléments enfants. Mais cela ajouterait un niveau dans l’arbre XML. Nousdevons donc construire quelque chose qui n’ajoute aucun niveau à l’arbre XML. Untableau d’objets conviendrait. En effet, en C# 3.0, les tableaux implémentent l’interfaceIEnumerable<T>. Ils fonctionnent donc comme des séquences. Lorsqu’un IEnumerableest passé à un constructeur XElement en tant que contenu, la séquence est énumérée etchaque objet de la séquence est appliqué à l’élément en cours de construction (voirChapitre 7). Nous utiliserons la fonctionnalité d’initialisation des collections de C# 3.0pour remplir ce tableau avec les commentaires, prénoms, noms et surnoms.

.Select(e => new object[] { new XComment(" BookParticipant "), new XElement("FirstName", (string)e.Element("FirstName")), new XElement("LastName", (string)e.Element("LastName")), e.Elements("Nickname")})));

Console.WriteLine("Document XML transformé :");Console.WriteLine(xTransDocument);

À ce stade, un tableau contenant un commentaire, un prénom, un nom et autant desurnoms que présents dans le code XML a été projeté. Les deux dernières instructionsaffichent le document ainsi transformé.

Cet exemple est complexe. Remarquez que le tableau contient un objet XComment, deuxobjets XElement et un IEnumerable<XElement>. En le projetant en tant que valeurretournée par l’opérateur Select, une séquence de object[],IEnumerable<object[]>est insérée dans l’élément BookParticipants.

Dans ce cas, chacun des objets de cette séquence est un tableau d’objets et contient uncommentaire, les éléments FirstName et LastName et la séquence d’éléments NickName.Étant donné qu’un tableau d’objets ne crée pas un niveau supplémentaire dans l’arbreXML, les éléments du tableau sont simplement ajoutés dans l’élément BookParticipants.

Voici les résultats :

Document XML original :<BookParticipants> <BookParticipant type="Author"> <FirstName>Joe</FirstName> <LastName>Rattz</LastName> <Nickname>Joey</Nickname> <Nickname>Null Pointer</Nickname> </BookParticipant> <BookParticipant type="Editor"> <FirstName>Ewan</FirstName> <LastName>Buckingham</LastName> </BookParticipant></BookParticipants>

Linq.book Page 313 Mercredi, 18. février 2009 7:58 07

Page 329: LINQ Language Integrated Query en C

314 LINQ to XML Partie III

Le document transformé respecte exactement les spécifications. La partie la plus inté-ressante de ce code est la projection du tableau d’objets (une classe non XML) pourdéfinir des éléments XML en supprimant un niveau dans l’arbre XML.

Validation

L’API XML ne serait pas complète si elle n’était pas en mesure de valider le codeXML. Comme vous allez le voir, LINQ to XML sait valider un document XML parrapport à un schéma XML.

Les méthodes d’extension

La validation de données XML relève de la classe statique System.Xml.Schema.Exten-sions, qui met à disposition toutes les méthodes de validation nécessaires. Ces méthodessont implémentées en tant que méthodes d’extension.

Prototypes

Voici quelques-uns des prototypes des méthodes de validation de la classeSystem.Xml.Schema.Extensions :

System.Xml.Schema.Extensions class:

void Extensions.Validate(this XDocument source, XmlSchemaSet schemas, ValidationEventHandler validationEventHandler)

void Extensions.Validate(this XDocument source, XmlSchemaSet schemas, ValidationEventHandler validationEventHandler, bool addSchemaInfo)

void Extensions.Validate(this XElement source, XmlSchemaObject partialValidationType, XmlSchemaSet schemas, ValidationEventHandler validationEventHandler)

void Extensions.Validate(this XElement source, XmlSchemaObject partialValidationType, XmlSchemaSet schemas, ValidationEventHandler validationEventHandler, bool addSchemaInfo)

void Extensions.Validate(this XAttribute source, XmlSchemaObject partialValidationType, XmlSchemaSet schemas, ValidationEventHandler validationEventHandler)

Document XML transformé :<BookParticipants> <!-- BookParticipant --> <FirstName>Joe</FirstName> <LastName>Rattz</LastName> <Nickname>Joey</Nickname> <Nickname>Null Pointer</Nickname> <!-- BookParticipant --> <FirstName>Ewan</FirstName> <LastName>Buckingham</LastName></BookParticipants>

Linq.book Page 314 Mercredi, 18. février 2009 7:58 07

Page 330: LINQ Language Integrated Query en C

Chapitre 9 Les autres possibilités de XML 315

void Extensions.Validate(this XAttribute source, XmlSchemaObject partialValidationType, XmlSchemaSet schemas, ValidationEventHandler validationEventHandler, bool addSchemaInfo)

Chaque méthode admet deux prototypes. Les objets à valider peuvent être de typeXDocument, XElement ou XAttribute. Les seconds prototypes ajoutent un argumentbooléen indiquant si l’information de schéma doit être ajoutée au XElement et XAttri-bute après la validation. Les premiers prototypes (ceux sans l’argument bool) secomportent comme si la valeur false était affectée à l’argument addSchemaInfo desseconds prototypes. Si vous les utilisez, aucune information de schéma n’est donc ajoutéedans les objets LINQ to XML après la validation.

Pour obtenir l’information de schéma d’un objet XElement ou XAttribute, il suffitd’invoquer la méthode GetSchemaInfo sur cet objet. Si une information de schéman’est pas présente, cela signifie que le premier prototype ou que le second prototypeavec un argument addSchemaInfo initialisé à false a été appelé. Dans ce cas, laméthode GetSchemaInfo retourne la valeur null. Si une information de schéma esttrouvée, un objet qui implémente l’interface IXmlSchemaInfo est retourné. Cet objetcontient une propriété SchemaElement qui retournera un objet XmlSchemaElement etune autre SchemaAttribute qui retournera un objet XmlSchemaAttribute (à conditionque l’élément ou l’attribut soit valide). Ces objets pourront être utilisés pour obtenir desinformations complémentaires sur le schéma.

ATTENTIONATTENTION

L’information de schéma est accessible non pas pendant la validation, mais uniquement lors-que cette phase est terminée. Si vous appelez la méthode GetSchemaInfo dans le gestion-naire d’événements de la validation, la valeur null sera retournée.

Les prototypes des méthodes Validate dédiées aux XElement et XAttributes deman-dent un argument XmlSchemaObject. Cela signifie que le document doit avoir été validéavant de les appeler.

Par ailleurs, si vous passez la valeur null dans l’argument ValidationEventHandler,une exception de type XmlSchemaValidationException est levée. C’est l’approche laplus simple pour valider un document XML.

Obtention d’un schéma XML

Si vous vous intéressez à la validation de documents XML, il y a de grandes chancespour que vous sachiez ce qu’est un schéma XSD, voire comment le produire. Si vousn’avez aucune connaissance à ce sujet, rassurez-vous : nous allons vous montrercomment laisser l’environnement .NET gérer tout cela à votre place (voirListing 9.12).

Linq.book Page 315 Mercredi, 18. février 2009 7:58 07

Page 331: LINQ Language Integrated Query en C

316 LINQ to XML Partie III

Listing 9.12 : Création d’un document XSD à partir d’un document XML.

XDocument xDocument = new XDocument( new XElement("BookParticipants", new XElement("BookParticipant", new XAttribute("type", "Author"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham"))));

Console.WriteLine("Document XML source :");Console.WriteLine("{0}{1}{1}", xDocument, System.Environment.NewLine);

xDocument.Save("bookparticipants.xml");

XmlSchemaInference infer = new XmlSchemaInference();XmlSchemaSet schemaSet = infer.InferSchema(new XmlTextReader("bookparticipants.xml"));

XmlWriter w = XmlWriter.Create("bookparticipants.xsd");foreach (XmlSchema schema in schemaSet.Schemas()){ schema.Write(w);}w.Close();

XDocument newDocument = XDocument.Load("bookparticipants.xsd");Console.WriteLine("Schéma :");Console.WriteLine("{0}{1}{1}", newDocument, System.Environment.NewLine);

Les premières lignes créent un (désormais traditionnel) document XML et l’affichentdans la console. Ce document est alors sauvegardé sur le disque dur de l’ordinateur. Lebloc de code suivant instancie un objet XmlSchemaInference et crée un XmlSchemaSeten invoquant la méthode InferSchema sur l’objet XmlSchemaInference. Le bloc suivantcrée un objet XmlWriter, énumère l’ensemble des schémas et écrit chacun d’entre euxdans le fichier bookparticipants.xsd. Enfin, le dernier bloc de code ouvre le schémaXSD ainsi généré et affiche son contenu. Voici les résultats :

Document XML source :<BookParticipants> <BookParticipant type="Author"> <FirstName>Joe</FirstName> <LastName>Rattz</LastName> </BookParticipant> <BookParticipant type="Editor"> <FirstName>Ewan</FirstName> <LastName>Buckingham</LastName> </BookParticipant></BookParticipants>

Schéma :<xs:schema attributeFormDefault="unqualified" elementFormDefault="qualified" xmlns:xs="http://www.w3.org/2001/XMLSchema"> <xs:element name="BookParticipants"> <xs:complexType> <xs:sequence>

Linq.book Page 316 Mercredi, 18. février 2009 7:58 07

Page 332: LINQ Language Integrated Query en C

Chapitre 9 Les autres possibilités de XML 317

Nous utiliserons la classe XmlSchemaSet ainsi que ce schéma XSD (fichier bookpartici-pants.xsd) dans les exemples de validation qui vont suivre.

Exemples

Dans le premier exemple, nous allons vous montrer la façon la plus simple de valider undocument XML. Cette approche sera adoptée par de nombreux développeurs. Pour cefaire, nous allons passer la valeur null à l’argument ValidationEventHandler de laméthode Validate (voir Listing 9.13).

Listing 9.13 : Validation d’un document XML avec la méthode de validation par défaut.

XDocument xDocument = new XDocument( new XElement("BookParticipants", new XElement("BookParticipant", new XAttribute("type", "Author"), new XElement("FirstName", "Joe"), new XElement("MiddleInitial", "C"), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham"))));

Console.WriteLine("Document XML source :");Console.WriteLine("{0}{1}{1}", xDocument, System.Environment.NewLine);

XmlSchemaSet schemaSet = new XmlSchemaSet();schemaSet.Add(null, "bookparticipants.xsd");

try{ xDocument.Validate(schemaSet, null); Console.WriteLine("Document validated successfully.");}catch (XmlSchemaValidationException ex){ Console.WriteLine("Une exception a eu lieu : {0}", ex.Message); Console.WriteLine("Le document n’est pas valide.");}

Le traditionnel document XML est quelque peu modifié : l’élément MiddleInitial estajouté pour que le document soit intentionnellement invalide. Nous utiliserons le schémaqui a été inféré dans l’exemple précédent. Dans la seconde ligne en gras, remarquez que le

<xs:element maxOccurs="unbounded" name="BookParticipant"> <xs:complexType> <xs:sequence> <xs:element name="FirstName" type="xs:string" /> <xs:element name="LastName" type="xs:string" /> </xs:sequence> <xs:attribute name="type" type="xs:string" use="required" /> </xs:complexType> </xs:element> </xs:sequence> </xs:complexType> </xs:element></xs:schema>

Linq.book Page 317 Mercredi, 18. février 2009 7:58 07

Page 333: LINQ Language Integrated Query en C

318 LINQ to XML Partie III

deuxième argument de la méthode Validate a pour valeur null. Si une erreur est géné-rée pendant la validation, une exception de type XmlSchemaValidationException seraautomatiquement levée. Voici les résultats :

Dans l’exemple suivant, nous allons valider le document XML habituel (celui qui a étéutilisé pour inférer le schéma). Puisque le schéma a été obtenu à partir du documentXML que nous souhaitons valider, la validation ne va pas poser de problème.

Dans cet exemple, nous allons utiliser la méthode ValidationEventHandler ci-après :

static void MyValidationEventHandler(object o, ValidationEventArgs vea){ Console.WriteLine("A validation error occurred processing object type {0}.", o.GetType().Name);

Console.WriteLine(vea.Message); throw (new Exception(vea.Message));}

Ce gestionnaire est vraiment minimaliste. Il se contente d’afficher le message d’erreuret de lever une exception. La gestion des erreurs repose entièrement sur cette méthode.Il n’était pas nécessaire de lever une exception : nous aurions pu gérer les erreurs d’unefaçon moins grossière, par exemple en ignorant certaines erreurs spécifiques.

Examinons le Listing 9.14, qui utilise la méthode ValidationEventHandler.

Listing 9.14 : Validation réussie d’un document XML par un schéma XSD.

XDocument xDocument = new XDocument( new XElement("BookParticipants", new XElement("BookParticipant", new XAttribute("type", "Author"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham"))));

Document XML source :<BookParticipants> <BookParticipant type="Author"> <FirstName>Joe</FirstName> <MiddleInitial>C</MiddleInitial> <LastName>Rattz</LastName> </BookParticipant> <BookParticipant type="Editor"> <FirstName>Ewan</FirstName> <LastName>Buckingham</LastName> </BookParticipant></BookParticipants>

Une exception a eu lieu : L’élément ’BookParticipant’ a un élément enfant ’MiddleInitial’ invalide. Éléments attendus : ’LastName’.Le document n’est pas valide.

Linq.book Page 318 Mercredi, 18. février 2009 7:58 07

Page 334: LINQ Language Integrated Query en C

Chapitre 9 Les autres possibilités de XML 319

Console.WriteLine("Here is the source XML document:");Console.WriteLine("{0}{1}{1}", xDocument, System.Environment.NewLine);

XmlSchemaSet schemaSet = new XmlSchemaSet();schemaSet.Add(null, "bookparticipants.xsd");

try{ xDocument.Validate(schemaSet, MyValidationEventHandler); Console.WriteLine("Le document est valide.");}catch (Exception ex){ Console.WriteLine("Une exception a été générée : {0}", ex.Message); Console.WriteLine("Le document n’est pas valide.");}

Après avoir créé et affiché le document XML, ce code instancie un objet XmlSchemaSetet y ajoute le schéma inféré bookparticipants.xsd à l’aide de la méthode Add.

Le dernier bloc de données applique la méthode d’extension Validate au document enlui passant le schéma et le gestionnaire d’événements de la validation. Pour des raisonsde sécurité, l’appel à la méthode Validate a été réalisé à l’intérieur d’un bloc try/catch. Voici les résultats :

Nous allons maintenant donner un exemple de document non valide (voir Listing 9.15).

Listing 9.15 : Échec dans la validation d’un document XML par un schéma XSD.

XDocument xDocument = new XDocument( new XElement("BookParticipants", new XElement("BookParticipant", new XAttribute("type", "Author"), new XAttribute("language", "English"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham"))));

Console.WriteLine("Document source :");Console.WriteLine("{0}{1}{1}", xDocument, System.Environment.NewLine);

XmlSchemaSet schemaSet = new XmlSchemaSet();

Document source :<BookParticipants> <BookParticipant type="Author"> <FirstName>Joe</FirstName> <LastName>Rattz</LastName> </BookParticipant> <BookParticipant type="Editor"> <FirstName>Ewan</FirstName> <LastName>Buckingham</LastName> </BookParticipant></BookParticipants>

Le document est valide.

Linq.book Page 319 Mercredi, 18. février 2009 7:58 07

Page 335: LINQ Language Integrated Query en C

320 LINQ to XML Partie III

schemaSet.Add(null, "bookparticipants.xsd");

try{ xDocument.Validate(schemaSet, MyValidationEventHandler); Console.WriteLine("Le document est valide.");}

catch (Exception ex){ Console.WriteLine("Une exception a été levée : {0}", ex.Message); Console.WriteLine("Le document n’est pas valide.");}

Ce code est identique à celui de l’exemple précédent, à ceci près que nous avons ajoutél’attribut language dans le premier élément BookParticipant. Le schéma ne faisantpas référence à cet attribut, le document XML n’est pas valide. Voici les résultats :

Le document XML n’est pas valide. Dans les deux exemples précédents, nous avonscréé la méthode MyValidationEventHandler pour gérer les événements liés à la valida-tion. Rappelez-vous, C# 2.0 a introduit les méthodes anonymes et C# 3.0, lesexpressions lambda. Le Listing 9.16 est identique au précédent mais, ici, nous utilisonsune expression lambda à la place de la méthode nommée ValidationEventHandler.

Listing 9.16 : Échec dans la validation d’un document XML par un schéma XSD en utilisant une expression lambda.

XDocument xDocument = new XDocument(new XElement("BookParticipants",new XElement("BookParticipant",new XAttribute("type", "Author"),new XAttribute("language", "English"),new XElement("FirstName", "Joe"),new XElement("LastName", "Rattz")),new XElement("BookParticipant",new XAttribute("type", "Editor"),new XElement("FirstName", "Ewan"),new XElement("LastName", "Buckingham"))));

Console.WriteLine("Document source :");Console.WriteLine("{0}{1}{1}", xDocument, System.Environment.NewLine);

Document source :<BookParticipants> <BookParticipant type="Author" language="English"> <FirstName>Joe</FirstName> <LastName>Rattz</LastName> </BookParticipant> <BookParticipant type="Editor"> <FirstName>Ewan</FirstName> <LastName>Buckingham</LastName> </BookParticipant></BookParticipants>

Une erreur de validation s’est produite pendant le traitement d’un XAttribute.L’attribut ’language’ n’est pas déclaré.Une exception a été levée: L’attribut ’language’ n’est pas déclaré.Le document n’est pas valide.

Linq.book Page 320 Mercredi, 18. février 2009 7:58 07

Page 336: LINQ Language Integrated Query en C

Chapitre 9 Les autres possibilités de XML 321

XmlSchemaSet schemaSet = new XmlSchemaSet();schemaSet.Add(null, "bookparticipants.xsd");try{xDocument.Validate(schemaSet, (o, vea) =>{Console.WriteLine("Une erreur de validation s’est produite sur un objet de type {0}.",o.GetType().Name);Console.WriteLine(vea.Message);throw (new Exception(vea.Message));});Console.WriteLine("Document validated successfully.");}catch (Exception ex){Console.WriteLine("Exception occurred: {0}", ex.Message);Console.WriteLine("Document validated unsuccessfully.");}

Dans ce listing, l’expression lambda est, en fait…, une méthode complète. Voici lesrésultats :

Nous allons reprendre le code de l’exemple précédent mais, cette fois-ci, nous ajoute-rons l’information du schéma (voir Listing 9.17).

Listing 9.17 : Échec dans la validation d’un document XML par un schéma XSD en utilisant une expression lambda et en ajoutant l’information du schéma.

XDocument xDocument = new XDocument( new XElement("BookParticipants", new XElement("BookParticipant", new XAttribute("type", "Author"), new XElement("FirstName", "Joe"), new XElement("MiddleName", "Carson"), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham"))));

Console.WriteLine("Document XML source :");

Document source :<BookParticipants> <BookParticipant type="Author" language="English"> <FirstName>Joe</FirstName> <LastName>Rattz</LastName> </BookParticipant> <BookParticipant type="Editor"> <FirstName>Ewan</FirstName> <LastName>Buckingham</LastName> </BookParticipant></BookParticipants>

Une erreur de validation s’est produite sur un objet de type XAttribute.L’attribut ’language’ n’est pas déclaré.Une exception a été levée: L’attribut ’language’ n’est pas déclaré.Le document n’est pas valide.

Linq.book Page 321 Mercredi, 18. février 2009 7:58 07

Page 337: LINQ Language Integrated Query en C

322 LINQ to XML Partie III

Console.WriteLine("{0}{1}{1}", xDocument, System.Environment.NewLine);

XmlSchemaSet schemaSet = new XmlSchemaSet();schemaSet.Add(null, "bookparticipants.xsd");

xDocument.Validate(schemaSet, (o, vea) =>{ Console.WriteLine("Une exception s’est produite pendant le traitement d’un objet ➥de type {0}.", o.GetType().Name); Console.WriteLine("{0}{1}", vea.Message, System.Environment.NewLine);},true);

foreach(XElement element in xDocument.Descendants()){ Console.WriteLine("Element {0} est {1}", element.Name, element.GetSchemaInfo().Validity);

XmlSchemaElement se = element.GetSchemaInfo().SchemaElement; if (se != null) { Console.WriteLine( "L’élément du schéma {0} doit avoir MinOccurs = {1} et MaxOccurs = {2}{3}", se.Name, se.MinOccurs, se.MaxOccurs, System.Environment.NewLine); } else { // Les éléments non valides n’ont pas d’élément SchemaElement Console.WriteLine(); }}

Cet exemple commence comme le précédent. Il crée un document XML mais, cettefois-ci, l’élément MiddleName est ajouté au premier élément BookParticipant. Cetélément n’est pas valide, puisqu’il n’est pas spécifié dans le schéma XSD. Contraire-ment à l’exemple précédent, la méthode Validate ajoute les informations du schémadans l’objet schemaSet et ne lève aucune exception. Comme il a été dit précédemment,le processus de validation doit en effet être terminé pour que les informations duschéma soient accessibles. Le gestionnaire d’événements ne peut donc pas lever desexceptions durant cette phase. Pour les mêmes raisons, le bloc try catch a étésupprimé.

Lorsque la validation est terminée, tous les éléments du document sont énumérés, et,pour chacun d’entre eux, une information indiquant leur validité est affichée. L’objetSchemaElement est obtenu à partir des informations de schéma stockées dans l’objetschemaSet à l’étape précédente. Une instruction if teste la propriété XmlSchemaEle-ment se. Une valeur null signifie que l’élément n’est pas valide. Dans ce cas, aucuneinformation de schéma ne peut être affichée. Dans le cas contraire, le nom du XmlSche-maElement ainsi que les propriétés MinOccurs et MaxOccurs sont affichés.

La même technique pourrait être appliquée à la propriété SchemaAttribute pour détecterles attributs non valides.

Linq.book Page 322 Mercredi, 18. février 2009 7:58 07

Page 338: LINQ Language Integrated Query en C

Chapitre 9 Les autres possibilités de XML 323

Voici les résultats :

Ces informations n’ont rien de surprenant. Remarquez que la propriété MaxOccurs del’élément BookParticipant a une très grande valeur. Ceci est dû au fait que, dans leschéma, l’attribut maxOccurs a été initialisé à la valeur "unbounded".

Pour les deux derniers exemples de validation, nous utiliserons un des prototypes de laméthode Validate dédié à la validation des éléments. Vous remarquerez sans peine queces prototypes nécessitent un argument de type XmlSchemaObject. Cela signifie que ledocument doit avoir été validé au préalable. Ce scénario concerne les situations où lavalidation a été effectuée une première fois, mais où il est nécessaire de revalider unepartie de l’arbre XML.

Nous allons supposer qu’un document XML est chargé et validé et que, par la suite, unutilisateur modifie les données concernant un participant. Le document XML doit doncêtre mis à jour pour refléter ces modifications, et la portion d’arbre XML correspondante

Document XML source : <BookParticipants> <BookParticipant type="Author"> <FirstName>Joe</FirstName> <MiddleName>Carson</MiddleName> <LastName>Rattz</LastName> </BookParticipant> <BookParticipant type="Editor"> <FirstName>Ewan</FirstName> <LastName>Buckingham</LastName> </BookParticipant></BookParticipants>

Une exception s’est produite pendant le traitement d’un objet de type XElement.L’élément ’BookParticipant’ a un élément enfant non valide : ’MiddleName’. Éléments attendus possibles : ’LastName’.

L’élément BookParticipants n’est pas valideL’élément BookParticipants doit avoir MinOccurs = 1 et MaxOccurs = 1

L’élément BookParticipant n’est pas valideL’élément BookParticipants doit avoir MinOccurs = 1 et MaxOccurs =79228162514264337593543950335

L’élément FirstName est valideL’élément FirstName doit avoir MinOccurs = 1 et MaxOccurs = 1

L’élément MiddleName n’est pas valide

L’élément LastName n’est pas connu

L’élément BookParticipant est valideL’élément BookParticipant doit avoir MinOccurs = 1 et MaxOccurs =79228162514264337593543950335

L’élément FirstName est valideL’élément FirstName doit avoir MinOccurs = 1 et MaxOccurs = 1

L’élément LastName est valideL’élément LastName doit avoir MinOccurs = 1 et MaxOccurs = 1

Linq.book Page 323 Mercredi, 18. février 2009 7:58 07

Page 339: LINQ Language Integrated Query en C

324 LINQ to XML Partie III

doit être validée. C’est ici que les méthodes Validate réservées aux attributs et auxéléments se révèlent très pratiques.

Cet exemple (voir Listing 9.18) étant assez complexe, nous donnerons des explicationschaque fois que cela sera nécessaire. Plutôt que charger le schéma habituel depuis unfichier, nous allons définir un nouveau schéma, légèrement différent des précédents,afin de faciliter l’édition de l’arbre XML.

Listing 9.18 : Validation réussie d’un élément XML.

string schema = @"<?xml version=’1.0’ encoding=’utf-8’?> <xs:schema attributeFormDefault=’unqualified’ elementFormDefault=’qualified’ xmlns:xs=’http://www.w3.org/2001/XMLSchema’> <xs:element name=’BookParticipants’> <xs:complexType> <xs:sequence> <xs:element maxOccurs=’unbounded’ name=’BookParticipant’> <xs:complexType> <xs:sequence> <xs:element name=’FirstName’ type=’xs:string’ /> <xs:element minOccurs=’0’ name=’MiddleInitial’ type=’xs:string’ /> <xs:element name=’LastName’ type=’xs:string’ /> </xs:sequence> <xs:attribute name=’type’ type=’xs:string’ use=’required’ /> </xs:complexType> </xs:element> </xs:sequence> </xs:complexType> </xs:element> </xs:schema>";

XmlSchemaSet schemaSet = new XmlSchemaSet();schemaSet.Add("", XmlReader.Create(new StringReader(schema)));

Ce schéma est légèrement différent de celui utilisé dans les autres exemples. Ici, lesguillemets de délimitation sont remplacés par des apostrophes et l’élément MiddleIni-tial est ajouté, entre les éléments FirstName et LastName. Remarquez également quel’attribut minOccurs de l’élément MiddleInitial a été initialisé à "0". Cet élémentn’est donc pas obligatoire. Les deux dernières lignes créent un objet schemaSet en utili-sant les données du schéma. La prochaine étape va consister à créer un document XML.

XDocument xDocument = new XDocument( new XElement("BookParticipants", new XElement("BookParticipant", new XAttribute("type", "Author"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"), new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham"))));

Console.WriteLine("Le document XML source :");Console.WriteLine("{0}{1}{1}", xDocument, System.Environment.NewLine);

Linq.book Page 324 Mercredi, 18. février 2009 7:58 07

Page 340: LINQ Language Integrated Query en C

Chapitre 9 Les autres possibilités de XML 325

Rien de nouveau dans ce code : le document XML habituel est créé et affiché. Nousallons maintenant le valider :

bool valid = true;xDocument.Validate(schemaSet, (o, vea) => { Console.WriteLine("Une exception s’est produite pendant le traitement de l’objet {0}.", o.GetType().Name);

Console.WriteLine(vea.Message);

valid = false; }, true);

Console.WriteLine("Le document {0}.{1}", valid ? "est valide" : "n’est pas valide", System.Environment.NewLine);

La validation est légèrement différente de celle utilisée dans les exemples précédents.Une variable booléenne indiquant si le document est valide est initialisée à la valeurtrue. À l’intérieur du gestionnaire de validation, elle est initialisée à la valeur false.Ainsi, si une erreur de validation se produit, valid aura pour valeur false. La valeur dela variable est testée pour déterminer si le document est valide et un message correspondantest affiché. Arrivé à ce point dans l’exécution du code, le document est valide.

Imaginons maintenant que nous autorisons un utilisateur à éditer les éléments des diffé-rents participants. Ici, par exemple, l’utilisateur édite le participant dont le prénom est"Joe". Le code obtient une référence de cet élément, le met à jour et effectue une validationaprès la mise à jour.

XElement bookParticipant = xDocument.Descendants("BookParticipant"). Where(e => ((string)e.Element("FirstName")).Equals("Joe")).First();

bookParticipant.Element("FirstName"). AddAfterSelf(new XElement("MiddleInitial", "C"));

valid = true;bookParticipant.Validate(bookParticipant.GetSchemaInfo().SchemaElement, schemaSet, (o, vea) => { Console.WriteLine("An exception occurred processing object type {0}.", o.GetType().Name);

Console.WriteLine(vea.Message);

valid = false; }, true);

Console.WriteLine("L’élément {0}.{1}", valid ? "est valide" : "n’est pas valide", System.Environment.NewLine);

La variable valid est initialisée à true, puis la méthode Validate est appelée sur l’élémentBookParticipant (et non sur le document complet). À l’intérieur du gestionnaire

Linq.book Page 325 Mercredi, 18. février 2009 7:58 07

Page 341: LINQ Language Integrated Query en C

326 LINQ to XML Partie III

d’événement de validation, valid est initialisée à true. Après l’étape de validation duparticipant, la validité de l’élément est affichée. Voici les résultats :

Dans cet exemple, l’élément a été considéré comme valide.

Dans notre dernier exemple, nous allons utiliser le même code mais, ici, pendant la mise àjour de l’élément BookParticipant, nous allons créer un élément MiddleName, et nonMiddleInitial. L’élément sera donc considéré comme invalide (voir Listing 9.19).

Listing 9.19 : Échec de validation d’un élément XML.

string schema = @"<?xml version=’1.0’ encoding=’utf-8’?> <xs:schema attributeFormDefault=’unqualified’ elementFormDefault=’qualified’ xmlns:xs=’http://www.w3.org/2001/XMLSchema’> <xs:element name=’BookParticipants’> <xs:complexType> <xs:sequence> <xs:element maxOccurs=’unbounded’ name=’BookParticipant’> <xs:complexType> <xs:sequence> <xs:element name=’FirstName’ type=’xs:string’ /> <xs:element minOccurs=’0’ name=’MiddleInitial’ type=’xs:string’ /> <xs:element name=’LastName’ type=’xs:string’ /> </xs:sequence> <xs:attribute name=’type’ type=’xs:string’ use=’required’ /> </xs:complexType> </xs:element> </xs:sequence> </xs:complexType> </xs:element> </xs:schema>";

XmlSchemaSet schemaSet = new XmlSchemaSet();schemaSet.Add("", XmlReader.Create(new StringReader(schema)));

XDocument xDocument = new XDocument( new XElement("BookParticipants", new XElement("BookParticipant", new XAttribute("type", "Author"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"),

Le document XML source<BookParticipants> <BookParticipant type="Author"> <FirstName>Joe</FirstName> <LastName>Rattz</LastName> </BookParticipant> <BookParticipant type="Editor"> <FirstName>Ewan</FirstName> <LastName>Buckingham</LastName> </BookParticipant></BookParticipants>

Le document est valideL’élément est valide

Linq.book Page 326 Mercredi, 18. février 2009 7:58 07

Page 342: LINQ Language Integrated Query en C

Chapitre 9 Les autres possibilités de XML 327

new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham"))));

Console.WriteLine("Document XML source :");Console.WriteLine("{0}{1}{1}", xDocument, System.Environment.NewLine);

bool valid = true;xDocument.Validate(schemaSet, (o, vea) => { Console.WriteLine("Une exception s’est produite pendant la validation d’un objet ➥de type {0}.", o.GetType().Name);

Console.WriteLine(vea.Message);

valid = false; }, true);

Console.WriteLine("Le document {0}.{1}", valid ? "est valide" : "n’est pas valide", System.Environment.NewLine);

XElement bookParticipant = xDocument.Descendants("BookParticipant"). Where(e => ((string)e.Element("FirstName")).Equals("Joe")).First();

bookParticipant.Element("FirstName"). AddAfterSelf(new XElement("MiddleName", "Carson"));

valid = true;bookParticipant.Validate(bookParticipant.GetSchemaInfo().SchemaElement, schemaSet, (o, vea) => { Console.WriteLine("Une exception s’est produite pendant la validation d’un objet ➥de type {0}.", o.GetType().Name);

Console.WriteLine(vea.Message);

valid = false; }, true);

Console.WriteLine("L’élément {0}.{1}", valid ? "est valide" : "n’est pas valide", System.Environment.NewLine);

Ce code est identique au précédent mais, ici, au lieu d’ajouter un élément MiddleIni-tial, nous ajoutons un élément MiddleName. Voici les résultats :

Document XML source :<BookParticipants> <BookParticipant type="Author"> <FirstName>Joe</FirstName> <LastName>Rattz</LastName> </BookParticipant> <BookParticipant type="Editor"> <FirstName>Ewan</FirstName> <LastName>Buckingham</LastName> </BookParticipant></BookParticipants>

Le document est valide

Linq.book Page 327 Mercredi, 18. février 2009 7:58 07

Page 343: LINQ Language Integrated Query en C

328 LINQ to XML Partie III

Comme on s’y attendait, l’élément BookParticipant n’est pas valide. Cet exemple estquelque peu irréaliste. En effet, il est peu probable qu’un développeur définisse uneinterface pour que des utilisateurs puissent modifier un document XML. Mais imaginezque le document XML passe entre les mains d’un programmeur qui cherche personnel-lement à vous nuire (un hacker, par exemple). Dans ce cas, la revalidation des donnéesprend tout son sens…

XPath

Si vous utilisez couramment XPath, vous pouvez tirer avantage de la classeSystem.Xml.XPath.Extensions de l’espace de noms System.Xml.XPath.Extensions.Cette classe ajoute la possibilité de faire des recherches XPath par l’intermédiaire deméthodes d’extension.

Prototypes

Voici la liste des principaux prototypes des méthodes de la classeSystem.Xml.XPath.Extensions :

XPathNavigator Extensions.CreateNavigator(this XNode node);XPathNavigator Extensions.CreateNavigator(this XNode node, XmlNameTable nameTable);

object Extensions.XPathEvaluate(this XNode node, string expression);object Extensions.XPathEvaluate(this XNode node, string expression, IXmlNamespaceResolver resolver);

XElement Extensions.XPathSelectElement(this XNode node, string expression);XElement Extensions.XPathSelectElement(this XNode node, string expression, IXmlNamespaceResolver resolver);

IEnumerable<XElement> Extensions.XPathSelectElements(this XNode node, string expression);IEnumerable<XElement> Extensions.XPathSelectElements(this XNode node, string expression, IXmlNamespaceResolver resolver);

En utilisant ces méthodes d’extension, vous pouvez appliquer une requête sur un docu-ment LINQ to XML en utilisant les expressions de recherche XPath (voir Listing 9.20).

Listing 9.20 : Interrogation de données XML avec la syntaxe XPath.

XDocument xDocument = new XDocument( new XElement("BookParticipants", new XElement("BookParticipant", new XAttribute("type", "Author"), new XElement("FirstName", "Joe"), new XElement("LastName", "Rattz")), new XElement("BookParticipant", new XAttribute("type", "Editor"),

Une exception s’est produite pendant la validation d’un objet de type XElement.L’élément ’BookParticipant’ a un enfant non valide : ’MiddleName’. Éléments attendus : ’MiddleInitial, LastName’.

L’élément n’est pas valide

Linq.book Page 328 Mercredi, 18. février 2009 7:58 07

Page 344: LINQ Language Integrated Query en C

Chapitre 9 Les autres possibilités de XML 329

new XElement("FirstName", "Ewan"), new XElement("LastName", "Buckingham"))));

XElement bookParticipant = xDocument.XPathSelectElement("//BookParticipants/BookParticipant[FirstName=’Joe’]");

Console.WriteLine(bookParticipant);

Ces quelques lignes de code définissent le document XML conventionnel mais, contrai-rement à ce qui a été fait dans les exemples précédents, le document original n’est pasaffiché. La méthode XPathSelectElement est appelée sur le document. Une expressionde recherche XPath lui est passée en argument afin de trouver l’élément BookPartici-pant dont l’élément FirstName a pour valeur "Joe". Voici les résultats :

Les méthodes d’extension XPath donnent une référence sur un objetSystem.Xml.XPath.XPathNavigator. Par son intermédiaire, vous pouvez parcourir undocument XML, exécuter une requête XPath pour obtenir un élément ou une séquenced’éléments ou évaluer une expression de requête XPath.

Résumé

Arrivé à ce point dans la lecture de cet ouvrage, si vous n’aviez aucune expérience enXML, vous vous sentez peut-être dépassé. Si vous aviez une expérience en XML, maispas en LINQ to XML, j’espère que vous avez pu comprendre tout ce qui a été dit. Lapuissance et la flexibilité de l’API LINQ to XML est vraiment grisante !

Pendant l’écriture de ce chapitre et des exemples qui le ponctuent, je me suis trouvédans un tel état d’euphorie que je n’ai jamais eu envie de faire "machine arrière" etd’utiliser le langage XML traditionnel. Et ce malgré le fait que mon projet profession-nel n’était pas encore en mesure d’utiliser LINQ to XML. Bien des fois, j’ai pensé "siseulement je pouvais utiliser la construction fonctionnelle pour définir un fragmentXML", mais j’ai dû me replier sur la méthode String.Format de la librairie XML tradi-tionnelle.

Ne me jetez pas la pierre : comme je l’ai dit précédemment, un présentateur a utilisé lesmêmes méthodes que moi lors d’un séminaire Microsoft !

Après avoir écrit les exemples des chapitres relatifs à LINQ to XML, je peux vous direque je serais vraiment intéressé si je pouvais utiliser l’API LINQ to XML dans moncode de production. La création de documents XML est grandement facilitée, car elleest essentiellement basée sur les éléments (et non les documents) et qu’elle tire parti desénormes possibilités de la construction fonctionnelle. Le processus peut même se révé-ler amusant : combinez la facilité de création, le parcours et la modification intuitifs de

<BookParticipant type="Author"> <FirstName>Joe</FirstName> <LastName>Rattz</LastName></BookParticipant>

Linq.book Page 329 Mercredi, 18. février 2009 7:58 07

Page 345: LINQ Language Integrated Query en C

330 LINQ to XML Partie III

documents XML, et cela devient un vrai plaisir d’utiliser LINQ to XML… surtout sil’on considère les autres alternatives !

Ces facilités d’utilisation ainsi que la puissance et la flexibilité du langage d’interroga-tion font de LINQ to XML mon préféré dans le petit monde de LINQ. Si votre approchede XML est plutôt difficile, vous devriez vous intéresser à LINQ to XML. Il fera certai-nement sauter bien des barrières.

Linq.book Page 330 Mercredi, 18. février 2009 7:58 07

Page 346: LINQ Language Integrated Query en C

IV

LINQ to DataSet

Linq.book Page 331 Mercredi, 18. février 2009 7:58 07

Page 347: LINQ Language Integrated Query en C

Linq.book Page 332 Mercredi, 18. février 2009 7:58 07

Page 348: LINQ Language Integrated Query en C

10

LINQ to DataSet

Bien que LINQ to SQL n’ait pas encore été abordé dans cet ouvrage, je voudrais signa-ler que, pour utiliser LINQ to SQL sur une base de données, les classes de code sourcedoivent être générées et compilées spécifiquement pour cette base de données, ou qu’unfichier de mapping doit être créé. Cela signifie qu’il est impossible d’effectuer desrequêtes LINQ to SQL sur une base de données inconnue jusqu’à l’exécution. Maisalors que doit faire le développeur ?

Les opérateurs LINQ to DataSet permettent d’exécuter des requêtes LINQ sur des Data-Set. Étant donné qu’un DataSet peut être récupéré par une requête SQL ADO.NET, LINQto DataSet permet d’effectuer des requêtes sur toute base de données accessible viaADO.NET. Cela offre un dynamisme bien plus grand que si vous utilisiez LINQ to SQL.

Vous pouvez vous demander dans quelles circonstances la base de données pourrait nepas être connue jusqu’à l’exécution. Effectivement, dans les applications traditionnel-les, la base de données est connue pendant le développement, et LINQ to DataSet n’estpas un passage obligé. Mais qu’en est-il si vous développez un utilitaire pour bases dedonnées ? Considérons une application telle que SQL Server Enterprise Manager(l’interface graphique de SQL Server pour les tâches de création et d’administration desbases de données). Jusqu’à l’exécution, cette application ne connaît pas les bases dedonnées qui ont été installées. Cependant, elle vous permet de connaître leur nom ainsique celui des différentes tables accessibles dans chacune d’entre elles. Le développeurd’une telle application n’a aucun moyen de générer les classes LINQ to SQL nécessai-res à l’interfaçage des différentes bases de données à l’exécution. LINQ to DataSetdevient donc une nécessité.

Bien que ce chapitre soit intitulé "LINQ to DataSet", vous verrez que les opérateurspassés en revue sont essentiellement relatifs aux objets DataTable, DataRow et DataCo-lumn. Ne soyez pas surpris si ce chapitre ne fait pas souvent référence aux objets Data-Sets. Il est bien entendu qu’en circonstances réelles vos objets DataTable viendront

Linq.book Page 333 Mercredi, 18. février 2009 7:58 07

Page 349: LINQ Language Integrated Query en C

334 LINQ to DataSet Partie IV

essentiellement d’objets DataSets. Cependant, pour des raisons d’indépendance, deconcision et de clarté, la plupart des exemples de ce chapitre se basent sur de simplesobjets DataTable créés par programme. Les données traitées ne sont donc pas extraitesd’une base de données existante.

LINQ to DataSet donne accès à plusieurs opérateurs spécifiques issus de différentsassemblies et espaces de noms. Ces opérateurs permettent au développeur d’effectuerles actions suivantes :

m définitions de séquences d’objets DataRows ;

m recherche et modification de valeurs DataColumn ;

m obtention de séquences LINQ standard IEnumerable<T> à partir de DataTable afinde pouvoir leur appliquer des opérateurs de requête standard ;

m copie de séquences de DataRow modifiées dans un DataTable.

Outre ces opérateurs LINQ to DataSet, une fois l’opérateur AsEnumerable appelé, vouspouvez utiliser les opérateurs de requête standard de LINQ to Objects sur la séquenceDataRow retournée, ce qui ajoute encore plus de puissance et de flexibilité à LINQ toDataSet.

Référence des assemblies

Pour exécuter les exemples de ce chapitre, vous devrez (si elles ne sont pas déjà présentes)ajouter des références aux assemblies System.Data.dll et System.Data.DataSet-Extensions.dll.

Espaces de noms référencés

Pour être en mesure d’utiliser les opérateurs LINQ to DataSet, vous devez ajouter (sielles ne sont pas déjà présentes) les deux directives using suivantes en tête de votrecode :

using System.Data;using System.Linq;

Code commun utilisé dans les exemples

Tous les exemples de ce chapitre ont besoin d’un objet DataTable pour effectuer desrequêtes LINQ to DataSet. Dans un code de production réel, ces objets sont typique-ment obtenus en effectuant une requête sur une base de données. Dans plusieurs exemplesde ce chapitre, cette configuration est inconfortable, voire insuffisante. À titre d’exem-ple, nous aurons besoin de deux enregistrements identiques pour illustrer la méthodeDistinct. Plutôt que jongler avec une base de données pour obtenir les enregistrements

Linq.book Page 334 Mercredi, 18. février 2009 7:58 07

Page 350: LINQ Language Integrated Query en C

Chapitre 10 LINQ to DataSet 335

nécessaires, nous avons préféré créer par programme un objet DataTable qui contientles données nécessaires à chaque exemple.

Pour faciliter la définition de l’objet DataTable, nous utiliserons un tableau d’objetscontenu dans la classe prédéfinie Student.

Une classe simpliste avec deux membres publics

class Student{ public int Id; public string Name;}

Vous n’avez qu’à imaginer que nous interrogeons la table Students, composée de deuxcolonnes (Id et Name) et dans laquelle chaque enregistrement représente un étudiant.

Pour faciliter la création du DataTable et pour ne pas nuire aux détails de chaque exem-ple, nous utiliserons une méthode commune pour convertir un tableau d’objets Studenten un objet DataTable. Ceci nous permettra de faire varier simplement les donnéesd’un exemple à l’autre. Voici le code de cette méthode commune :

Conversion d’un tableau d’objets Student en un DataTable

static DataTable GetDataTable(Student[] students){ DataTable table = new DataTable();

table.Columns.Add("Id", typeof(Int32)); table.Columns.Add("Name", typeof(string));

foreach (Student student in students) { table.Rows.Add(student.Id, student.Name); }

return (table);}

Cette méthode n’a rien de bien compliqué. Elle se contente d’instancier un objet Data-Table, puis d’ajouter deux colonnes et une ligne pour chacun des éléments du tableaustudents passé en argument.

Pour plusieurs des exemples de ce chapitre, il est nécessaire d’afficher un objet DataTa-ble, pour s’assurer que les résultats sont conformes aux attentes. D’un exemple àl’autre, les données du DataTable peuvent varier, mais le code permettant d’afficher lecontenu du DataTable reste le même. Plutôt que répéter ce code dans tous les exemples,nous avons créé une méthode commune que nous appellerons chaque fois que cela seranécessaire :

La méthode OutputDataTableHeader

static void OutputDataTableHeader(DataTable dt, int columnWidth){ string format = string.Format("{0}0,-{1}{2}", "{", columnWidth, "}");

// Display the column headings.

Linq.book Page 335 Mercredi, 18. février 2009 7:58 07

Page 351: LINQ Language Integrated Query en C

336 LINQ to DataSet Partie IV

foreach(DataColumn column in dt.Columns) { Console.Write(format, column.ColumnName); } Console.WriteLine(); foreach(DataColumn column in dt.Columns) { for(int i = 0; i < columnWidth; i++) { Console.Write("="); } } Console.WriteLine();}

Cette méthode affiche l’en-tête d’un objet DataTable sous une forme tabulaire.

Opérateurs dédiés aux DataRow

Vous vous souvenez certainement que l’API LINQ to Objects contient un ensembled’opérateurs de requête standard très utiles lorsqu’il s’agit d’initialiser et/ou decomparer des séquences. Je fais référence aux opérateurs Distinct, Except, Inter-sect, Union et SequenceEqual, qui définissent une séquence en fonction d’uneautre.

Chacun de ces opérateurs doit être en mesure de tester l’égalité des éléments d’uneséquence pour effectuer l’opération pour laquelle il a été conçu. Le test d’égalité se faiten appliquant les méthodes GetHashCode et Equals aux éléments. En ce qui concerneles DataRow, ces deux méthodes provoquent la comparaison des références deséléments, ce qui n’est pas le comportement souhaité. Mais, rassurez-vous, ces opéra-teurs possèdent un autre prototype dont nous n’avons pas parlé dans les chapitresrelatifs à LINQ to Objects. Ce prototype permet de passer un argument complémen-taire : IEqualityComparer. Par commodité, un objet comparateur a été spécialementdéfini pour ces versions des opérateurs : System.Data.DataRowComparer.Default.Cette classe se trouve dans l’espace de noms System.Data et l’assemblySystem.Data.Entity.dll. L’égalité est déterminée en comparant le nombre de colon-nes et le type de donnée statique de chaque colonne et en utilisant l’interface ICompara-ble sur le type de donnée dynamique de la colonne si celui-ci l’implémente. Dans le cascontraire, la méthode System.Object Equals est appelée.

Ces prototypes sont définis dans la même classe statique que les autres :System.Linq.Enumerable.

Dans cette section, nous allons donner quelques exemples pour illustrer la mauvaise et,bien entendu, la bonne façon d’effectuer des comparaisons sur des objets DataSet.

Opérateur Distinct

L’opérateur Distinct supprime les lignes en double dans une séquence d’objets. Ilretourne un objet dont l’énumération renvoie la séquence source privée des doublons.

Linq.book Page 336 Mercredi, 18. février 2009 7:58 07

Page 352: LINQ Language Integrated Query en C

Chapitre 10 LINQ to DataSet 337

Cet opérateur devrait pouvoir déterminer l’égalité entre les différentes lignes en appelantles méthodes GetHashCode et Equals sur chacun des éléments. Cependant, pour desobjets de type DataRow, cette technique ne donne pas le résultat recherché.

Pour obtenir le résultat escompté, nous appellerons un nouveau prototype de cet opéra-teur et nous lui passerons le comparateur System.Data.DataRowComparer.Defaultdans son deuxième argument.

La comparaison est effectuée sur chacune des colonnes d’une ligne. Elle se base surle type de donnée statique de chaque colonne. L’interface IComparable est utiliséesur les colonnes qui implémentent cette interface. La méthode statiqueSystem.Object.Equals est utilisée sur les autres.

PrototypeUn seul prototype de l’opérateur Distinct sera étudié dans ce chapitre :

public static IEnumerable<T> Distinct<T> ( this IEnumerable<T> source, IEqualityComparer<T> comparer);

ExemplesDans le premier exemple, l’objet DataTable sera créé en appliquant la méthodecommune GetDataTable à un tableau d’objet Student. À dessein, ce tableau compren-dra deux fois la même ligne : celle dont le champ Id vaut 1. Pour mettre en évidence laligne en double dans le DataTable, le tableau sera affiché. La ligne en double seraensuite enlevée à l’aide de l’opérateur Distinct, et l’objet DataTable sera à nouveauaffiché, pour montrer que le doublon a été supprimé. Le code utilisé apparaît dans leListing 10.1.

Listing 10.1 : L’opérateur Distinct associé à un comparateur d’égalité.

Student[] students = { new Student { Id = 1, Name = "Joe Rattz" }, new Student { Id = 6, Name = "Ulyses Hutchens" }, new Student { Id = 19, Name = "Bob Tanko" }, new Student { Id = 45, Name = "Erin Doutensal" }, new Student { Id = 1, Name = "Joe Rattz" }, new Student { Id = 12, Name = "Bob Mapplethorpe" }, new Student { Id = 17, Name = "Anthony Adams" }, new Student { Id = 32, Name = "Dignan Stephens" }};

DataTable dt = GetDataTable(students);

Console.WriteLine("{0}Avant l’appel à Distinct(){0}", System.Environment.NewLine);

OutputDataTableHeader(dt, 15);

Linq.book Page 337 Mercredi, 18. février 2009 7:58 07

Page 353: LINQ Language Integrated Query en C

338 LINQ to DataSet Partie IV

foreach (DataRow dataRow in dt.Rows){ Console.WriteLine("{0,-15}{1,-15}", dataRow.Field<int>(0), dataRow.Field<string>(1));}

IEnumerable<DataRow> distinct =dt.AsEnumerable().Distinct(DataRowComparer.Default);

Console.WriteLine("{0}Après l’appel à Distinct(){0}", System.Environment.NewLine);

OutputDataTableHeader(dt, 15);

foreach (DataRow dataRow in distinct){ Console.WriteLine("{0,-15}{1,-15}", dataRow.Field<int>(0), dataRow.Field<string>(1));}

L’opérateur AsEnumerable a été utilisé pour obtenir la séquence d’objets DataRow àpartir du DataTable. Ceci afin d’assurer la compatibilité avec l’opérateur Distinct.Remarquez également que, dans le tableau students, la ligne dont le champ Id vaut "1"apparaît en double.

Vous avez sans doute noté que la méthode Field a été appelée sur l’objet DataRow. Pourl’instant, tout ce que vous devez en savoir, c’est qu’il s’agit d’une méthode qui facilitel’obtention des valeurs des objets DataColumn à partir d’un DataRow. L’opérateurField<T> sera étudié en détail un peu plus loin dans ce chapitre, dans la section "Opéra-teurs dédiés aux champs".

Voici les résultats :

Avant l’appel à Distinct()Id Name==============================1 Joe Rattz6 Ulyses Hutchens19 Bob Tanko45 Erin Doutensal1 Joe Rattz12 Bob Mapplethorpe17 Anthony Adams32 Dignan Stephens

Après l’appel à Distinct()Id Name==============================1 Joe Rattz6 aUlyses Hutchens19 Bob Tanko45 Erin Doutensal12 Bob Mapplethorpe17 Anthony Adams32 Dignan Stephens

Linq.book Page 338 Mercredi, 18. février 2009 7:58 07

Page 354: LINQ Language Integrated Query en C

Chapitre 10 LINQ to DataSet 339

Comme vous le voyez, la ligne dont le champ Id vaut 1 apparaît en double avant l’appelà l’opérateur Distinct. Elle n’apparaît plus qu’une seule fois lorsque cet opérateur aété appelé.

Dans notre deuxième exemple, nous allons voir ce qui se passerait si l’opérateurDistinct avait été appelé sans spécifier l’objet comparer (voir Listing 10.2).

Listing 10.2 : L’opérateur Distinct appelé sans comparateur d’égalité.

Student[] students = { new Student { Id = 1, Name = "Joe Rattz" }, new Student { Id = 6, Name = "Ulyses Hutchens" }, new Student { Id = 19, Name = "Bob Tanko" }, new Student { Id = 45, Name = "Erin Doutensal" }, new Student { Id = 1, Name = "Joe Rattz" }, new Student { Id = 12, Name = "Bob Mapplethorpe" }, new Student { Id = 17, Name = "Anthony Adams" }, new Student { Id = 32, Name = "Dignan Stephens" }};

DataTable dt = GetDataTable(students);

Console.WriteLine("{0}Avant l’appel à Distinct(){0}", System.Environment.NewLine);

OutputDataTableHeader(dt, 15);

foreach (DataRow dataRow in dt.Rows){ Console.WriteLine("{0,-15}{1,-15}", dataRow.Field<int>(0), dataRow.Field<string>(1));}

IEnumerable<DataRow> distinct = dt.AsEnumerable().Distinct();

Console.WriteLine("{0}Après l’appel à Distinct(){0}", System.Environment.NewLine);

OutputDataTableHeader(dt, 15);

foreach (DataRow dataRow in distinct){ Console.WriteLine("{0,-15}{1,-15}", dataRow.Field<int>(0), dataRow.Field<string>(1));}

La seule différence entre ce code et le précédent se situe au niveau de l’opérateurDistinct : dans le premier cas, on utilise un comparateur d’égalité, dans le second cas,non. Cette deuxième technique va-t-elle supprimer le doublon ? Jetons un œil aux résultats :

Avant l’appel à Distinct()Id Name==============================1 Joe Rattz6 Ulyses Hutchens19 Bob Tanko45 Erin Doutensal

Linq.book Page 339 Mercredi, 18. février 2009 7:58 07

Page 355: LINQ Language Integrated Query en C

340 LINQ to DataSet Partie IV

Ces résultats ne sont pas concluants. Comme vous le voyez, la deuxième technique decomparaison est inefficace.

Opérateur Except

L’opérateur Except renvoie une séquence composée des objets DataRow de la premièreséquence qui n’appartiennent pas à la seconde. Les éléments de la séquence de sortieapparaissent dans l’ordre original de la séquence d’entrée.

Pour déterminer quels éléments sont uniques, l’opérateur Except doit être en mesure dedéterminer si deux éléments sont égaux. Pour ce faire, il devrait suffire d’utiliser lesméthodes GetHashCode et Equals. Cependant, étant donné que les objets comparés sontdes DataRow, un prototype spécifique doit être utilisé. Le deuxième argument de ceprototype désigne le comparateur (System.Data.DataRowComparer.Default) à utiliser.

La comparaison est effectuée sur chacune des colonnes d’une ligne. Elle se base sur letype de donnée statique de chaque colonne. L’interface IComparable est utilisée sur lescolonnes qui l’implémentent. La méthode statique System.Object.Equals est utiliséesur les autres.

PrototypeNous nous intéresserons à un seul prototype de cet opérateur :

public static IEnumerable<T> Except<T> ( this IEnumerable<T> first, IEnumerable<T> second, IEqualityComparer<T> comparer);

ExempleDans cet exemple, nous appellerons l’opérateur Except à deux reprises. Dans lepremier appel, le comparateur passé sera System.Data.DataRowComparer.Default.Les résultats de la comparaison devraient donc être conformes aux attentes. Dans le

1 Joe Rattz12 Bob Mapplethorpe17 Anthony Adams32 Dignan Stephens

Après l’appel à Distinct()Id Name==============================1 Joe Rattz6 Ulyses Hutchens19 Bob Tanko45 Erin Doutensal1 Joe Rattz12 Bob Mapplethorpe17 Anthony Adams32 Dignan Stephens

Linq.book Page 340 Mercredi, 18. février 2009 7:58 07

Page 356: LINQ Language Integrated Query en C

Chapitre 10 LINQ to DataSet 341

second appel, aucun comparateur ne sera passé au prototype. Comme vous le verrez, lacomparaison ne fonctionnera pas (voir Listing 10.3).

Listing 10.3 : Appel de l’opérateur Except avec et sans comparateur.

Student[] students = { new Student { Id = 1, Name = "Joe Rattz" }, new Student { Id = 7, Name = "Anthony Adams" }, new Student { Id = 13, Name = "Stacy Sinclair" }, new Student { Id = 72, Name = "Dignan Stephens" }};

Student[] students2 = { new Student { Id = 5, Name = "Abe Henry" }, new Student { Id = 7, Name = "Anthony Adams" }, new Student { Id = 29, Name = "Future Man" }, new Student { Id = 72, Name = "Dignan Stephens" }};

DataTable dt1 = GetDataTable(students);IEnumerable<DataRow> seq1 = dt1.AsEnumerable();DataTable dt2 = GetDataTable(students2);IEnumerable<DataRow> seq2 = dt2.AsEnumerable();

IEnumerable<DataRow> except = seq1.Except(seq2, System.Data.DataRowComparer.Default);

Console.WriteLine("{0}Résultats de l’opérateur Except() avec le comparateur{0}", System.Environment.NewLine);

OutputDataTableHeader(dt1, 15);

foreach (DataRow dataRow in except){ Console.WriteLine("{0,-15}{1,-15}", dataRow.Field<int>(0), dataRow.Field<string>(1));}

except = seq1.Except(seq2);

Console.WriteLine("{0}Résultats de l’opérateur Except() sans le comparateur{0}", System.Environment.NewLine);

OutputDataTableHeader(dt1, 15);

foreach (DataRow dataRow in except){ Console.WriteLine("{0,-15}{1,-15}", dataRow.Field<int>(0), dataRow.Field<string>(1));}

Cet exemple crée deux objets DataTable et les remplit avec les données stockées dansles tableaux Student. La méthode AsEnumerable est alors appelée pour transformer lesdeux objets DataTable en séquences. Enfin, l’opérateur Except est appelé sur les deuxséquences et les résultats sont affichés. Comme vous pouvez le voir, le premier appel àl’opérateur Except transmet le comparateur System.Data.DataRowComparer.Defaultdans le deuxième argument. Le second appel ne transmet aucun comparateur.

Linq.book Page 341 Mercredi, 18. février 2009 7:58 07

Page 357: LINQ Language Integrated Query en C

342 LINQ to DataSet Partie IV

Voici les résultats affichés lors de l’appui sur Ctrl+F5 :

Comme vous pouvez le voir, seul le premier appel à l’opérateur Except a été en mesurede comparer de façon correcte les données des deux séquences.

Opérateur Intersect

L’opérateur Intersect renvoie une séquence d’objets DataRow qui représente l’inter-section des deux séquences DataRow passées en entrée. La séquence de sortie contientles éléments uniques des deux séquences d’entrée, listés dans leur ordre d’apparitionoriginal.

Pour déterminer quels éléments sont uniques, l’opérateur Intersect doit être enmesure de déterminer si deux éléments sont égaux. Pour ce faire, il devrait lui suffired’utiliser les méthodes GetHashCode et Equals. Cependant, étant donné que les objetscomparés sont de type DataRow, un prototype spécifique doit être utilisé. Le deuxièmeargument de ce prototype désigne le comparateur (System.Data.DataRowCompa-rer.Default) à utiliser.

La comparaison est effectuée sur chacune des colonnes d’une ligne. Elle se base sur letype de donnée statique de chaque colonne. L’interface IComparable est utilisée sur lescolonnes qui implémentent cette interface. La méthode statique System.Object.Equalsest utilisée sur les autres colonnes.

PrototypeNous nous intéresserons à un seul prototype de cet opérateur :

public static IEnumerable<T> Intersect<T> ( this IEnumerable<T> first, IEnumerable<T> second, IEqualityComparer<T> comparer);

ExempleNous utiliserons le même code que dans l’exemple de l’opérateur Except mais, ici,c’est l’opérateur Intersect qui sera appelé (voir Listing 10.4).

Résultats de l’opérateur Except() avec le comparateurId Name==============================1 Joe Rattz13 Stacy Sinclair

Résultats de l’opérateur Except() sans le comparateurId Name==============================1 Joe Rattz7 Anthony Adams13 Stacy Sinclair72 Dignan Stephens

Linq.book Page 342 Mercredi, 18. février 2009 7:58 07

Page 358: LINQ Language Integrated Query en C

Chapitre 10 LINQ to DataSet 343

Listing 10.4 : Appel de l’opérateur Intersect avec et sans comparateur.

Student[] students = { new Student { Id = 1, Name = "Joe Rattz" }, new Student { Id = 7, Name = "Anthony Adams" }, new Student { Id = 13, Name = "Stacy Sinclair" }, new Student { Id = 72, Name = "Dignan Stephens" }};

Student[] students2 = { new Student { Id = 5, Name = "Abe Henry" }, new Student { Id = 7, Name = "Anthony Adams" }, new Student { Id = 29, Name = "Future Man" }, new Student { Id = 72, Name = "Dignan Stephens" }};

DataTable dt1 = GetDataTable(students);IEnumerable<DataRow> seq1 = dt1.AsEnumerable();DataTable dt2 = GetDataTable(students2);IEnumerable<DataRow> seq2 = dt2.AsEnumerable();

IEnumerable<DataRow> intersect = seq1.Intersect(seq2, System.Data.DataRowComparer.Default);

Console.WriteLine("{0}Résultats de l’opérateur Intersect() avec le comparateur{0}", System.Environment.NewLine);

OutputDataTableHeader(dt1, 15);

foreach (DataRow dataRow in intersect){ Console.WriteLine("{0,-15}{1,-15}", dataRow.Field<int>(0), dataRow.Field<string>(1));}

intersect = seq1.Intersect(seq2);

Console.WriteLine("{0}Résultats de l’opérateur Intersect() sans le comparateur{0}", System.Environment.NewLine);

OutputDataTableHeader(dt1, 15);

foreach (DataRow dataRow in intersect){ Console.WriteLine("{0,-15}{1,-15}", dataRow.Field<int>(0), dataRow.Field<string>(1));}

Rien de nouveau dans ce code : deux objets DataTable sont créés et initialisés avec lesdonnées des tableaux Student. Ils sont ensuite convertis en séquences, puis l’opérateurIntersect leur est appliqué, avec puis sans comparateur. Les résultats sont affichésaprès chaque appel à l’opérateur Intersect.

Linq.book Page 343 Mercredi, 18. février 2009 7:58 07

Page 359: LINQ Language Integrated Query en C

344 LINQ to DataSet Partie IV

Voici les informations affichées suite à l’appui sur Ctrl+F5 :

Comme vous pouvez le voir, seul le premier appel à l’opérateur Intersect a été enmesure de comparer de façon correcte les données des deux séquences.

Opérateur Union

L’opérateur Union renvoie une séquence d’objets DataRow qui représente la réunion desdeux séquences DataRow passées en entrée. La séquence de sortie contient les élémentsde la première séquence suivis des éléments de la seconde séquence qui n’ont pas déjàété cités.

Pour déterminer quels éléments ont déjà été sélectionnés dans la première séquence,l’opérateur Union doit être en mesure de déterminer si deux éléments sont égaux. Pource faire, il devrait lui suffire d’utiliser les méthodes GetHashCode et Equals. Cependant,étant donné que les objets comparés sont de type DataRow, un prototype spécifique doitêtre utilisé. Le deuxième argument de ce prototype désigne le comparateur(System.Data.DataRowComparer.Default) à utiliser.

La comparaison est effectuée sur chacune des colonnes d’une ligne. Elle se base sur letype de donnée statique de chaque colonne. L’interface IComparable est utilisée sur lescolonnes qui implémentent cette interface. La méthode statique System.Object.Equalsest utilisée sur les autres colonnes.

PrototypeNous nous intéresserons à un seul prototype de cet opérateur :

public static IEnumerable<T> Union<T> ( this IEnumerable<T> first, IEnumerable<T> second, IEqualityComparer<T> comparer);

ExempleNous utiliserons le même code que dans l’exemple de l’opérateur Intersect mais, ici,c’est l’opérateur Union qui sera appelé (voir Listing 10.5).

Résultats de l’opérateur Intersect() avec le comparateurId Name==============================7 Anthony Adams72 Dignan Stephens

Résultats de l’opérateur Intersect() sans le comparateurId Name==============================

Linq.book Page 344 Mercredi, 18. février 2009 7:58 07

Page 360: LINQ Language Integrated Query en C

Chapitre 10 LINQ to DataSet 345

Listing 10.5 : Appel de l’opérateur Union avec et sans comparateur.

Student[] students = { new Student { Id = 1, Name = "Joe Rattz" }, new Student { Id = 7, Name = "Anthony Adams" }, new Student { Id = 13, Name = "Stacy Sinclair" }, new Student { Id = 72, Name = "Dignan Stephens" }};

Student[] students2 = { new Student { Id = 5, Name = "Abe Henry" }, new Student { Id = 7, Name = "Anthony Adams" }, new Student { Id = 29, Name = "Future Man" }, new Student { Id = 72, Name = "Dignan Stephens" }};

DataTable dt1 = GetDataTable(students);IEnumerable<DataRow> seq1 = dt1.AsEnumerable();DataTable dt2 = GetDataTable(students2);IEnumerable<DataRow> seq2 = dt2.AsEnumerable();

IEnumerable<DataRow> union = seq1.Union(seq2, System.Data.DataRowComparer.Default);

Console.WriteLine("{0}Résultats de l’opérateur Union() avec le comparateur{0}", System.Environment.NewLine);

OutputDataTableHeader(dt1, 15);

foreach (DataRow dataRow in union){ Console.WriteLine("{0,-15}{1,-15}", dataRow.Field<int>(0), dataRow.Field<string>(1));}

union = seq1.Union(seq2);

Console.WriteLine("{0}Résultats de l’opérateur Union() sans le comparateur{0}", System.Environment.NewLine);

OutputDataTableHeader(dt1, 15);

foreach (DataRow dataRow in union){ Console.WriteLine("{0,-15}{1,-15}", dataRow.Field<int>(0), dataRow.Field<string>(1));}

Ici encore, rien de nouveau dans ce code : deux objets DataTable sont créés et initiali-sés avec les données des tableaux Student. Ils sont ensuite convertis en séquences, puisl’opérateur Union leur est appliqué, avec puis sans comparateur. Les résultats sont affichésaprès chaque appel à l’opérateur Union.

Linq.book Page 345 Mercredi, 18. février 2009 7:58 07

Page 361: LINQ Language Integrated Query en C

346 LINQ to DataSet Partie IV

Voici les informations affichées suite à l’appui sur Ctrl+F5 :

Comme vous pouvez le voir, seul le premier appel à l’opérateur Union a donné les résul-tats escomptés.

Opérateur SequencialEqual

L’opérateur SequencialEqual compare deux séquences d’objets DataRow et détermineleur égalité. Pour ce faire, les deux séquences sources sont énumérées et leurs objetsDataRow, comparés. Si les deux séquences sources ont le même nombre de lignes, et sitous les objets DataRow sont égaux, l’opérateur retourne la valeur true. Dans le cascontraire, il retourne la valeur false.

Cet opérateur doit être en mesure de déterminer si deux éléments sont égaux. Pour cefaire, il devrait lui suffire d’utiliser les méthodes GetHashCode et Equals. Cependant,étant donné que les objets comparés sont de type DataRow, un prototype spécifique doitêtre utilisé. Le deuxième argument de ce prototype désigne le comparateur(System.Data.DataRowComparer.Default) à utiliser.

La comparaison est effectuée sur chacune des colonnes d’une ligne. Elle se base sur letype de donnée statique de chaque colonne. L’interface IComparable est utilisée sur lescolonnes qui l’implémentent. La méthode statique System.Object.Equals est utiliséesur les autres colonnes.

PrototypeNous nous intéresserons à un seul prototype de cet opérateur :

public static bool SequenceEqual<T> ( this IEnumerable<T> first, IEnumerable<T> second, IEqualityComparer<T> comparer);

Résultats de l’opérateur Union() avec le comparateurId Name==============================1 Joe Rattz7 Anthony Adams13 Stacy Sinclair72 Dignan Stephens5 Abe Henry29 Future Man

Résultats de l’opérateur Union() sans le comparateurId Name==============================1 Joe Rattz7 Anthony Adams13 Stacy Sinclair72 Dignan Stephens5 Abe Henry7 Anthony Adams29 Future Man72 Dignan Stephens

Linq.book Page 346 Mercredi, 18. février 2009 7:58 07

Page 362: LINQ Language Integrated Query en C

Chapitre 10 LINQ to DataSet 347

ExempleDans cet exemple, nous allons construire deux séquences identiques d’objets DataRowet les comparer avec l’opérateur SequencialEqual. Deux comparaisons seront effectuées.La première utilisera un comparateur et la seconde, non (voir Listing 10.6).

Listing 10.6 : Appel de l’opérateur SequenceEqual avec et sans comparateur.

Student[] students = { new Student { Id = 1, Name = "Joe Rattz" }, new Student { Id = 7, Name = "Anthony Adams" }, new Student { Id = 13, Name = "Stacy Sinclair" }, new Student { Id = 72, Name = "Dignan Stephens" }};

DataTable dt1 = GetDataTable(students);IEnumerable<DataRow> seq1 = dt1.AsEnumerable();DataTable dt2 = GetDataTable(students);IEnumerable<DataRow> seq2 = dt2.AsEnumerable();

bool equal = seq1.SequenceEqual(seq2, System.Data.DataRowComparer.Default);Console.WriteLine("Appel de SequenceEqual() avec comparateur : {0}", equal);equal = seq1.SequenceEqual(seq2);Console.WriteLine("Appel de SequenceEqual() sans le comparateur : {0}", equal);

Comme on pouvait s’y attendre, le premier appel à l’opérateur SequenceEqual indi-que que les deux séquences sont égales, alors que le second indique qu’elles sontdifférentes :

Opérateurs dédiés aux champs

Ces opérateurs viennent compléter ceux passés en revue dans la section précédente. Ilssont définis dans l’assembly System.Data.DataSetExtensions.dll, dans la classestatique System.Data.DataRowExtensions.

Vous avez certainement remarqué que, dans la plupart des exemples précédents, nousavons utilisé l’opérateur Field<T> pour extraire d’un DataRow la valeur d’un objetDataColumn. Cet opérateur a deux intérêts : grâce à lui, la comparaison de données estpossible, et il gère la valeur null.

La manipulation des objets DataRow présente un problème : les DataColumn, de type"valeur" (à opposer au type "référence"), ne peuvent pas être comparés correctement.En effet, ils peuvent contenir une donnée de type quelconque : un entier, une chaîne ouun autre type de donnée. Si, par exemple, un DataColumn contient une valeur de typeint, il doit être converti en une référence de type Object. Cette conversion est connuesous le nom "boxing" dans l’environnement de développement .NET. L’opérationinverse (la transformation d’un type référence en un type valeur) est appelée"unboxing". Le problème se situe au niveau du boxing.

Appel de SequenceEqual() avec comparateur : TrueAppel de SequenceEqual() sans comparateur : False

Linq.book Page 347 Mercredi, 18. février 2009 7:58 07

Page 363: LINQ Language Integrated Query en C

348 LINQ to DataSet Partie IV

Pour mieux comprendre ce problème, nous allons raisonner sur quelques exemples.Dans le Listing 10.7, nous comparons deux entiers littéraux de même valeur.

Listing 10.7 : Comparaison des valeurs 3 et 3.

Console.WriteLine("(3 == 3) vaut {0}.", (3 == 3));

Voici le résultat :

Aucune surprise dans ce résultat. Mais que devient la comparaison si les entiers àcomparer ont subi un boxing ? Examinons le code du Listing 10.8.

Listing 10.8 : Comparaison des valeurs 3 et 3 après leur casting dans des Object.

Console.WriteLine("((Object)3 == (Object)3) vaut {0}.", ((Object)3 == (Object)3));

Voici le résultat :

Que s’est-il passé ? L’opérateur de casting (Object) convertit chacune des valeurs enun objet de type Object. Dans cet exemple, la comparaison porte non pas sur la valeurde ces objets mais sur leur référence, c’est-à-dire leur adresse. Bien entendu, les deuxadresses ne sont pas identiques. Lorsque vous accédez aux objets DataColumn enindexant un objet DataRow, si une colonne a un type valeur, elle subit un boxing, et sacomparaison ne donne pas le résultat escompté.

Pour mettre en évidence ce problème, nous allons raisonner sur un exemple pluscomplexe qui utilise des objets DataColumn. Ici, nous utiliserons deux tableaux declasse différente. Le premier est le tableau Student, utilisé dans les exemples précé-dents. Le deuxième a pour classe designations. Il contient des données étrangères autableau Student. Voici la classe StudentClass :

Une classe élémentaire contenant deux propriétés publiques

class StudentClass{ public int Id; public string Class;}

Pour convertir un tableau de classe StudentClass en un objet DataTable, nous utilise-rons la méthode suivante :

static DataTable GetDataTable2(StudentClass[] studentClasses){ DataTable table = new DataTable();

table.Columns.Add("Id", typeof(Int32)); table.Columns.Add("Class", typeof(string)); foreach (StudentClass studentClass in studentClasses)

(3 == 3) vaut True.

((Object)3 == (Object)3) vaut False.

Linq.book Page 348 Mercredi, 18. février 2009 7:58 07

Page 364: LINQ Language Integrated Query en C

Chapitre 10 LINQ to DataSet 349

{ table.Rows.Add(studentClass.Id, studentClass.Class); }

return (table);}

Cette méthode est une copie de la méthode commune GetTableData, modifiée pourfonctionner avec des tableaux d’objets StudentClass. Si vous êtes amené à travailleravec des tableaux dans un code de production réel, vous ne définirez pas une méthodespécifique pour chacune des classes qui utilise des objets DataTable. Vous vous tourne-rez plutôt vers une méthode d’extension générique. Comme il a été dit il y a quelquespages, dans un environnement réel les données seront généralement obtenues en appli-quant des requêtes LINQ to DataSet à une base de données…

À titre d’exemple, nous convertirons les tableaux en séquences d’objets DataRow etnous tenterons d’effectuer une jointure sur le champ Id. Ce champ sera obtenu en utili-sant le nom des colonnes pour indexer les DataRow (voir Listing 10.9).

Listing 10.9 : Réalisation d’une jointure en indexant le DataRow.

Student[] students = { new Student { Id = 1, Name = "Joe Rattz" }, new Student { Id = 7, Name = "Anthony Adams" }, new Student { Id = 13, Name = "Stacy Sinclair" }, new Student { Id = 72, Name = "Dignan Stephens" }};

StudentClass[] classDesignations = { new StudentClass { Id = 1, Class = "Sophmore" }, new StudentClass { Id = 7, Class = "Freshman" }, new StudentClass { Id = 13, Class = "Graduate" }, new StudentClass { Id = 72, Class = "Senior" }};

DataTable dt1 = GetDataTable(students);IEnumerable<DataRow> seq1 = dt1.AsEnumerable();DataTable dt2 = GetDataTable2(classDesignations);IEnumerable<DataRow> seq2 = dt2.AsEnumerable();

string anthonysClass = (from s in seq1 where s.Field<string>("Name") == "Anthony Adams" from c in seq2 where c["Id"] == s["Id"] select (string)c["Class"]). SingleOrDefault<string>();

Console.WriteLine("La classe d’Anthony est : {0}", anthonysClass != null ? anthonysClass : "null");

Quelques éléments dans la requête précédente méritent des explications. La ligne engras réalise une jointure sur les deux tableaux. Pour ce faire, elle indexe les deux objetsDataRow afin d’accéder aux valeurs du champ Id. Ces valeurs étant de type string,elles subissent un boxing. Il sera donc impossible de déterminer leur égalité en utilisantdes moyens conventionnels. Deux lignes plus haut, nous utilisons l’opérateur Field<T>pour comparer la valeur du champ Name et la valeur littérale "Anthony Adams". Cet

Linq.book Page 349 Mercredi, 18. février 2009 7:58 07

Page 365: LINQ Language Integrated Query en C

350 LINQ to DataSet Partie IV

opérateur est appelé pour éliminer le problème de boxing qui va être mis en évidencesur le champ Id. Remarquez également que la requête mélange la syntaxe d’interrogationde requête de LINQ et la notation à point classique. Voici les résultats :

Ce problème vient du fait que la ligne en gras n’a pas été en mesure de réaliser la join-ture. Le boxing du champ Id en est évidemment la cause. Pour corriger ce problème,nous allons modifier la ligne :

where c["Id"] == s["Id"]

en :

where (int)c["Id"] == (int)s["Id"]

Le code devient donc celui du Listing 10.10.

Listing 10.10 : Utilisation d’un opérateur de casting pour pouvoir tester l’égalité des champs Id.

Student[] students = { new Student { Id = 1, Name = "Joe Rattz" }, new Student { Id = 7, Name = "Anthony Adams" }, new Student { Id = 13, Name = "Stacy Sinclair" }, new Student { Id = 72, Name = "Dignan Stephens" }};

StudentClass[] classDesignations = { new StudentClass { Id = 1, Class = "Sophmore" }, new StudentClass { Id = 7, Class = "Freshman" }, new StudentClass { Id = 13, Class = "Graduate" }, new StudentClass { Id = 72, Class = "Senior" }};

DataTable dt1 = GetDataTable(students);IEnumerable<DataRow> seq1 = dt1.AsEnumerable();DataTable dt2 = GetDataTable2(classDesignations);IEnumerable<DataRow> seq2 = dt2.AsEnumerable();

string anthonysClass = (from s in seq1 where s.Field<string>("Name") == "Anthony Adams" from c in seq2 where (int)c["Id"] == (int)s["Id"] select (string)c["Class"]). SingleOrDefault<string>();

Console.WriteLine("La classe d’Anthony est : {0}", anthonysClass != null ? anthonysClass : "null");

L’exécution de ce code produit le résultat suivant :

Le problème lié au boxing du champ Id a été évité. Cependant, un autre problème esttoujours présent : lorsque vous tentez d’obtenir une valeur dans une colonne enindexant un objet DataRow, l’objet retourné est de type Object. Pour le comparer à une

La classe d’Anthony est : Null

La classe d’Anthony est : Freshman

Linq.book Page 350 Mercredi, 18. février 2009 7:58 07

Page 366: LINQ Language Integrated Query en C

Chapitre 10 LINQ to DataSet 351

valeur littérale ou l’affecter à une variable, vous devrez utiliser un opérateur de casting.Dans cet exemple, nous utilisons l’opérateur (int). Étant donné que les objets DataSetutilisent la valeur DBNull.Value pour représenter une valeur null, si une colonnecontient la valeur null son casting au format int produira une exception.

Heureusement, les opérateurs LINQ to DataSet Field<T> et SetField<T> éliminent cesdeux problèmes. Le Listing 10.11 représente l’exemple précédent, dans lequel l’opéra-teur de casting (int) a été remplacé par un Field<T>.

Listing 10.11 : Utilisation de l’opérateur Field.

Student[] students = { new Student { Id = 1, Name = "Joe Rattz" }, new Student { Id = 7, Name = "Anthony Adams" }, new Student { Id = 13, Name = "Stacy Sinclair" }, new Student { Id = 72, Name = "Dignan Stephens" }};

StudentClass[] classDesignations = { new StudentClass { Id = 1, Class = "Sophmore" }, new StudentClass { Id = 7, Class = "Freshman" }, new StudentClass { Id = 13, Class = "Graduate" }, new StudentClass { Id = 72, Class = "Senior" }};

DataTable dt1 = GetDataTable(students);IEnumerable<DataRow> seq1 = dt1.AsEnumerable();DataTable dt2 = GetDataTable2(classDesignations);IEnumerable<DataRow> seq2 = dt2.AsEnumerable();

string anthonysClass = (from s in seq1 where s.Field<string>("Name") == "Anthony Adams" from c in seq2 where c.Field<int>("Id") == s.Field<int>("Id") select (string)c["Class"]). SingleOrDefault<string>();

Console.WriteLine("La classe d’Anthony est : {0}", anthonysClass != null ? anthonysClass : "null");

Ce code étant équivalent au précédent, à ceci près que l’opérateur Field<T> remplacel’opérateur de casting (int), il produit le même résultat :

Opérateur Field<T>

Comme nous venons de le montrer dans le Listing 10.11, l’opérateur Field<T> permetd’obtenir la valeur d’une colonne dans un objet DataRow. Par ailleurs, il évite les problèmesliés au boxing et aux valeurs null.

PrototypesSix prototypes de cet opérateur seront étudiés dans cette section.

La classe d’Anthony est : Freshman

Linq.book Page 351 Mercredi, 18. février 2009 7:58 07

Page 367: LINQ Language Integrated Query en C

352 LINQ to DataSet Partie IV

Le premier prototype retourne la valeur de la colonne pour le DataColumn et la versionspécifiés :

Le premier prototype

public static T Field ( this DataRow first, System.Data.DataColumn column, System.Data.DataRowVersion version);

Le deuxième prototype retourne la valeur de la colonne pour la colonne dont le nom etla version sont spécifiés :

Le deuxième prototype

public static T Field ( this DataRow first, string columnName, System.Data.DataRowVersion version);

Le troisième prototype retourne la valeur de la colonne pour la colonne dont l’ordinal etla version sont spécifiés :

Le troisième prototype

public static T Field ( this DataRow first, int ordinal, System.Data.DataRowVersion version);

Le quatrième prototype retourne la valeur de la colonne dont le DataColumn est spéci-fié :

Le quatrième prototype

public static T Field ( this DataRow first, System.Data.DataColumn column);

Le cinquième prototype retourne la valeur de la colonne pour la colonne dont le nom estspécifié :

Le cinquième prototype

public static T Field ( this DataRow first, string columnName);

Le sixième prototype retourne la valeur de la colonne pour la colonne dont l’ordinal estspécifié :

Le sixième prototype

public static T Field ( this DataRow first, int ordinal);

Vous l’avez certainement remarqué, les trois premiers prototypes permettent de choisirla version (DataRowVersion) de l’objet DataColumn à obtenir.

Linq.book Page 352 Mercredi, 18. février 2009 7:58 07

Page 368: LINQ Language Integrated Query en C

Chapitre 10 LINQ to DataSet 353

ExemplesLorsque vous êtes arrivé à ce point dans la lecture de l’ouvrage, plusieurs exemplesvous ont montré comment utiliser l’opérateur Field<T>. Le Listing 10.12 va aller plusloin en vous montrant les six facettes de cet opérateur.

Listing 10.12 : Un exemple des six prototypes de l’opérateur Field.

Student[] students = { new Student { Id = 1, Name = "Joe Rattz" }, new Student { Id = 7, Name = "Anthony Adams" }, new Student { Id = 13, Name = "Stacy Sinclair" }, new Student { Id = 72, Name = "Dignan Stephens" }};

DataTable dt1 = GetDataTable(students);IEnumerable<DataRow> seq1 = dt1.AsEnumerable();

int id;// Utilisation du premier prototypeid = (from s in seq1 where s.Field<string>("Name") == "Anthony Adams" select s.Field<int>(dt1.Columns[0], DataRowVersion.Current)). Single<int>();Console.WriteLine("Le champ Id d’Anthony, obtenu avec le premier prototype, a pour ➥valeur {0}", id);

// Utilisation du deuxième prototypeid = (from s in seq1 where s.Field<string>("Name") == "Anthony Adams" select s.Field<int>("Id", DataRowVersion.Current)). Single<int>();Console.WriteLine("Le champ Id d’Anthony, obtenu avec le deuxième prototype, a pour ➥valeur {0}", id);

// Utilisation du troisième prototypeid = (from s in seq1 where s.Field<string>("Name") == "Anthony Adams" select s.Field<int>(0, DataRowVersion.Current)). Single<int>();Console.WriteLine("Le champ Id d’Anthony, obtenu avec le troisième prototype, a pour ➥valeur {0}", id);

// Utilisation du quatrième prototypeid = (from s in seq1 where s.Field<string>("Name") == "Anthony Adams" select s.Field<int>(dt1.Columns[0])). Single<int>();Console.WriteLine("Le champ Id d’Anthony, obtenu avec le quatrième prototype, a pour ➥valeur {0}", id);

// Utilisation du cinquième prototypeid = (from s in seq1 where s.Field<string>("Name") == "Anthony Adams" select s.Field<int>("Id")). Single<int>();Console.WriteLine("Le champ Id d’Anthony, obtenu avec le cinquième prototype, a pour ➥valeur {0}", id);

Linq.book Page 353 Mercredi, 18. février 2009 7:58 07

Page 369: LINQ Language Integrated Query en C

354 LINQ to DataSet Partie IV

// Utilisation du sixième prototypeid = (from s in seq1 where s.Field<string>("Name") == "Anthony Adams" select s.Field<int>(0)). Single<int>();Console.WriteLine("Le champ Id d’Anthony, obtenu avec le sixième prototype, a pour ➥valeur {0}", id);

Les premières lignes du code définissent le tableau Students, initialisent un objetDataTable avec son contenu et le convertissent en une séquence. La suite du codeapplique tour à tour les six prototypes de l’opérateur Field<T> à la séquence pour obte-nir la valeur du champ Id. Notez que l’opérateur Field<T> est également utilisé dans lapartie Where de la requête. Voici les résultats :

Pour illustrer l’utilisation de l’argument DataRowVersion, nous avons modifié unevaleur DataColumn en utilisant l’opérateur SetField<T>. Cet opérateur n’a pas encoreété étudié. Pour l’instant, ignorez le code qui l’utilise. Vous en apprendrez plus à sonsujet dans la section suivante.

Ce chapitre étant consacré aux opérateurs LINQ to DataSet et non au fonctionnementdétaillé de la classe DataSet, nous n’aborderons ce sujet que très brièvement, à traversles deux méthodes DataSet utilisées dans l’exemple du Listing 10.13.

Listing 10.13 : Démonstration de l’argument DataRowVersion de l’opérateur Field.

Student[] students = { new Student { Id = 1, Name = "Joe Rattz" }, new Student { Id = 7, Name = "Anthony Adams" }, new Student { Id = 13, Name = "Stacy Sinclair" }, new Student { Id = 72, Name = "Dignan Stephens" }};

DataTable dt1 = GetDataTable(students);IEnumerable<DataRow> seq1 = dt1.AsEnumerable();

DataRow row = (from s in seq1 where s.Field<string>("Name") == "Anthony Adams" select s).Single<DataRow>();

row.AcceptChanges();row.SetField("Name", "George Oscar Bluth");

Console.WriteLine("Valeur originale = {0} : Valeur actuelle = {1}", row.Field<string>("Name", DataRowVersion.Original), row.Field<string>("Name", DataRowVersion.Current));

Le champ Id d’Anthony, obtenu avec le premier prototype, a pour valeur 7Le champ Id d’Anthony, obtenu avec le deuxième prototype, a pour valeur 7Le champ Id d’Anthony, obtenu avec le troisième prototype, a pour valeur 7Le champ Id d’Anthony, obtenu avec le quatrième prototype, a pour valeur 7Le champ Id d’Anthony, obtenu avec le cinquième prototype, a pour valeur 7Le champ Id d’Anthony, obtenu avec le sixième prototype, a pour valeur 7

Linq.book Page 354 Mercredi, 18. février 2009 7:58 07

Page 370: LINQ Language Integrated Query en C

Chapitre 10 LINQ to DataSet 355

row.AcceptChanges();Console.WriteLine("Valeur originale = {0} : Valeur actuelle = {1}", row.Field<string>("Name", DataRowVersion.Original), row.Field<string>("Name", DataRowVersion.Current));

Cet exemple définit une séquence à partir des données du tableau students. Unerequête est alors lancée pour obtenir un objet DataRow unique. Le premier code digned’intérêt est la méthode AcceptChanges, appelée juste après avoir obtenu l’objet Data-Row. Cette méthode est appelée pour que l’objet DataRow considère ses valeurs actuellescomme étant des valeurs originales. Si cette méthode n’avait pas été appelée, les valeursoriginales de l’objet DataRow ne seraient pas définies, et une exception se produirait sion essayait de les afficher. Une fois la méthode AcceptChanges exécutée, l’objet Data-Row est prêt pour pister les changements dans ses valeurs DataColumn.

Le premier appel de la méthode AcceptChanges est suivi de la modification du champ Nameavec l’opérateur SetField. Le bloc d’instructions suivant affiche la valeur originale("Anthony Adams") et la valeur actuelle ("George Oscar Bluth") du DataColumn Name.

La méthode AcceptChanges est appelée une deuxième fois, puis les valeurs originale etactuelle du DataColumn Name sont à nouveau affichées. Cette fois-ci, les deux valeursdevraient être identiques et égales à "George Oscar Bluth", puisque la méthode Accept-Changes a été appelée. Examinons les résultats :

Si vous ne deviez retenir qu’une chose de cet exemple, que ce soit l’utilisation de laméthode AcceptChanges. Cette méthode permet de mémoriser la valeur originale etd’affecter une autre valeur à un objet DataColumn.

Comme il a été dit précédemment, l’opérateur Field<T> sait également éviter leproblème lié aux champs vides (null). Dans le Listing 10.14, nous allons voir ce qui sepasse lorsqu’un nom d’étudiant n’est pas initialisé et que l’opérateur Field<T> n’est pasutilisé.

Listing 10.14 : Un exemple de champ null sans utiliser l’opérateur Field.

Student[] students = { new Student { Id = 1, Name = "Joe Rattz" }, new Student { Id = 7, Name = null }, new Student { Id = 13, Name = "Stacy Sinclair" }, new Student { Id = 72, Name = "Dignan Stephens" }};

DataTable dt1 = GetDataTable(students);IEnumerable<DataRow> seq1 = dt1.AsEnumerable();

string name = seq1.Where(student => student.Field<int>("Id") == 7) .Select(student => (string)student["Name"]) .Single();

Console.WriteLine("Student’s name is ’{0}’", name);

Valeur originale = Anthony Adams : Valeur actuelle = George Oscar BluthValeur originale = George Oscar Bluth : Valeur actuelle = George Oscar Bluth

Linq.book Page 355 Mercredi, 18. février 2009 7:58 07

Page 371: LINQ Language Integrated Query en C

356 LINQ to DataSet Partie IV

Deux passages apparaissent en gras pour attirer votre attention. Dans le premier, le nomde l’étudiant dont la colonne id vaut 7 est initialisé à null. Dans le second, l’opérateurField<T> est remplacé par un simple casting (string). Voici les résultats :

Que s’est-il passé ? La valeur de l’objet DataColumn dont la colonne id vaut 7 étantnull, il est impossible de lui appliquer un casting (string). Il existe des solutions plusverbeuses pour éviter ce problème, mais le plus simple consiste à utiliser l’opérateurField<T> (voir Listing 10.15).

Listing 10.15 : Un exemple de champ null avec l’opérateur Field.

Student[] students = { new Student { Id = 1, Name = "Joe Rattz" }, new Student { Id = 7, Name = null }, new Student { Id = 13, Name = "Stacy Sinclair" }, new Student { Id = 72, Name = "Dignan Stephens" }};

DataTable dt1 = GetDataTable(students);IEnumerable<DataRow> seq1 = dt1.AsEnumerable();

string name = seq1.Where(student => student.Field<int>("Id") == 7) .Select(student => student.Field<string>["Name"]) .Single();

Console.WriteLine("Le nom de l’étudiant est ’{0}’", name);

Ce code est identique au précédent mais, ici, le casting (string) est remplacé par unappel à l’opérateur Field<T>. Voici le résultat :

Opérateur SetField<T>

La valeur null affecte également l’initialisation des objets DataColumn. Pour éviter toutproblème, vous utiliserez l’opérateur SetField<T>. Par son intermédiaire, il est en effetpossible d’affecter à un DataColumn une donnée de type nullable dont la valeur estnull.

PrototypesNous nous intéresserons à trois prototypes de cet opérateur dans ce chapitre.

Le premier prototype vous permet de définir la valeur de la colonne spécifiée :

Le premier prototype

public static void SetField ( this DataRow first,

Une exception non gérée s’est produite : System.InvalidCastException : Impossible d’effectuer un cast d’un objet de type ’System.DBNull’ en type ’System.String’.…

Le nom de l’étudiant est ’’

Linq.book Page 356 Mercredi, 18. février 2009 7:58 07

Page 372: LINQ Language Integrated Query en C

Chapitre 10 LINQ to DataSet 357

System.Data.DataColumn column, T value);

Le deuxième prototype vous permet de définir la valeur de la colonne dont le nom estspécifié :

Le deuxième prototype

public static void SetField ( this DataRow first, string columnName, T value);

Le troisième prototype vous permet de définir la valeur de la colonne dont l’ordinal estspécifié :

Le troisième prototype

public static void SetField ( this DataRow first, int ordinal, T value);

ExemplesDans le Listing 10.16, nous définissons puis affichons la séquence de DataRow aveclaquelle nous allons travailler. Le DataRow d’un des étudiants est alors obtenu en effec-tuant une requête, puis le nom de l’étudiant est modifié avec l’opérateur SetField<T>.Enfin, la séquence de DataRow ainsi modifiée est à nouveau affichée. Ce processus estrépété pour chacun des prototypes de l’opérateur SetField.

Listing 10.16 : Un exemple d’utilisation des prototypes de l’opérateur SetField.

Student[] students = { new Student { Id = 1, Name = "Joe Rattz" }, new Student { Id = 7, Name = "Anthony Adams" }, new Student { Id = 13, Name = "Stacy Sinclair" }, new Student { Id = 72, Name = "Dignan Stephens" }};

DataTable dt1 = GetDataTable(students);IEnumerable<DataRow> seq1 = dt1.AsEnumerable();

Console.WriteLine("{0}Résultats avant d’appeler les prototypes :", System.Environment.NewLine);

foreach (DataRow dataRow in seq1){ Console.WriteLine("Student Id = {0} is {1}", dataRow.Field<int>("Id"), dataRow.Field<string>("Name"));}

// Utilisation du premier prototype(from s in seq1 where s.Field<string>("Name") == "Anthony Adams" select s).Single<DataRow>().SetField(dt1.Columns[1], "George Oscar Bluth");

Console.WriteLine("{0}Résultats après l’appel du premier prototype :", System.Environment.NewLine);

Linq.book Page 357 Mercredi, 18. février 2009 7:58 07

Page 373: LINQ Language Integrated Query en C

358 LINQ to DataSet Partie IV

foreach (DataRow dataRow in seq1){ Console.WriteLine("Student Id = {0} is {1}", dataRow.Field<int>("Id"), dataRow.Field<string>("Name"));}

// Utilisation du deuxième prototype (from s in seq1 where s.Field<string>("Name") == "George Oscar Bluth" select s).Single<DataRow>().SetField("Name", "Michael Bluth");

Console.WriteLine("{0}Résultats après l’appel du deuxième prototype :", System.Environment.NewLine);

foreach (DataRow dataRow in seq1){ Console.WriteLine("Student Id = {0} is {1}", dataRow.Field<int>("Id"), dataRow.Field<string>("Name"));}

// Utilisation du troisième prototype (from s in seq1 where s.Field<string>("Name") == "Michael Bluth" select s).Single<DataRow>().SetField("Name", "Tony Wonder");

Console.WriteLine("{0}Résultats après l’appel du troisième prototype :", System.Environment.NewLine);

foreach (DataRow dataRow in seq1){ Console.WriteLine("L’étudiant dont l’Id = {0} est {1}", dataRow.Field<int>("Id"), dataRow.Field<string>("Name"));}

Le code n’est pas aussi difficile qu’il en a l’air. Après avoir obtenu et affiché la séquenced’étudiants, un même bloc de code est répété à trois reprises (une pour chaque proto-type). Chacun des blocs contient une requête LINQ qui récupère le champ Name, modi-fie sa valeur et affiche dans la console une ligne d’en-tête suivie des éléments de laséquence.

Nous allons nous attarder sur plusieurs passages dans ce listing. Dans chacune desrequêtes LINQ portant sur le champ Name du DataRow, nous mélangeons la syntaxe derequête propre à LINQ et la traditionnelle syntaxe à point. Remarquez également quenous utilisons l’opérateur Field<T> pour accéder à l’enregistrement sur lequel nousallons appliquer l’opérateur SetField<T>. Chaque requête extrait un DataRow de laséquence par son champ Name (modifié à l’étape précédente, sauf pour le premier proto-type) et le modifie avec l’opérateur SetField. Par exemple, dans le code relatif aupremier prototype la requête extrait le DataRow dont le champ Name vaut "AnthonyAdams" et modifie ce nom en "George Oscar Bluth". Dans le code du deuxième proto-type, la requête extrait le DataRow dont le Name est "George Oscar Bluth" et le modifieen une valeur qui sera utilisée comme critère de sélection dans le troisième prototype.Pour chaque prototype, une boucle foreach affiche les éléments de la séquence ainsimodifiés, afin que vous puissiez vérifier que la modification a effectivement étéeffectuée.

Linq.book Page 358 Mercredi, 18. février 2009 7:58 07

Page 374: LINQ Language Integrated Query en C

Chapitre 10 LINQ to DataSet 359

Avez-vous remarqué que l’interrogation de la séquence et la mise à jour du DataRow sefont dans une seule et même instruction ? Il n’y a rien de magique là-dedans : nousutilisons simplement la puissance de LINQ.

Voici les résultats :

Opérateurs dédiés aux DataTable

Dans la classe DataRowExtensions, en complément des opérateurs dédiés aux DataRow,plusieurs opérateurs spécifiques DataTable ont été définis. Nous les avons regroupés danscette section. Ces opérateurs sont définis dans l’assembly System.Data.Entity.dll, dansla classe statique System.Data.DataTableExtensions.

Opérateur AsEnumerable

Vous devez certainement être surpris de trouver un opérateur AsEnumerable dédié à laclasse DataTable qui retourne une séquence d’objets DataRow. Si tel est le cas, celasignifie que vous ne vous êtes pas demandé pourquoi nous n’en avions pas déjà parlé…alors que nous l’avons utilisé dans pratiquement tous les exemples !

Si vous jetez un œil à la classe statique System.Data.DataTableExtensions, vous trou-verez effectivement un opérateur AsEnumerable. Cet opérateur retourne une séquencede type IEnumerable<DataRow> à partir d’un objet DataTable.

PrototypeUn seul prototype de cet opérateur sera traité dans cette section :

public static IEnumerable<DataRow> AsEnumerable ( this DataTable source );

Résultats avant d’appeler les prototypes :L’étudiant dont l’Id = 1 est Joe RattzL’étudiant dont l’Id = 7 est Anthony AdamsL’étudiant dont l’Id = 13 est Stacy SinclairL’étudiant dont l’Id = 72 est Dignan Stephens

Résultats après l’appel du premier prototype :L’étudiant dont l’Id = 1 est Joe RattzL’étudiant dont l’Id = 7 est George Oscar BluthL’étudiant dont l’Id = 13 est Stacy SinclairL’étudiant dont l’Id = 72 est Dignan Stephens

Résultats après l’appel du deuxième prototype :L’étudiant dont l’Id = 1 est Joe RattzL’étudiant dont l’Id = 7 est Michael BluthL’étudiant dont l’Id = 13 est Stacy SinclairL’étudiant dont l’Id = 72 est Dignan Stephens

Résultats après l’appel du troisième prototype :L’étudiant dont l’Id = 1 est Joe RattzL’étudiant dont l’Id = 7 est Tony WonderL’étudiant dont l’Id = 13 est Stacy SinclairL’étudiant dont l’Id = 72 est Dignan Stephens

Linq.book Page 359 Mercredi, 18. février 2009 7:58 07

Page 375: LINQ Language Integrated Query en C

360 LINQ to DataSet Partie IV

Cet opérateur est appliqué à un objet DataTable. Il retourne une séquence d’objetsDataRow. C’est traditionnellement la première étape lors de l’exécution d’une requêteLINQ to DataSet sur un DataTable d’un objet DataSet. Cet opérateur retourne uneséquence IEnumerable<T>, où T est un DataRow. Après son appel, vous pouvez doncutiliser les nombreux opérateurs de LINQ qui sont appelés sur une séquence de typeIEnumerable<T>.

ExemplesÉtant donné que l’opérateur AsEnumerable est la première étape permettant d’effectuerune requête LINQ to DataSet, la plupart des exemples de ce chapitre utilisent cet opérateur.

Opérateur CopyToDataTable<DataRow>

Vous savez maintenant comment effectuer une requête et modifier les valeurs Data-Column d’un DataRow. L’opérateur CopyToDataTable va vous permettre de placer cetteséquence d’objets DataRow ainsi modifiée dans un DataTable.

PrototypesDeux prototypes de cet opérateur seront examinés dans ce chapitre.

Le premier prototype est appelé sur un IEnumerable<DataRow> et il retourne un Data-Table. Vous l’utiliserez donc pour créer un nouvel objet DataTable à partir d’uneséquence d’objets DataRow :

Le premier prototype

public static DataTable CopyToDataTable<T> ( this IEnumerable<T> source) where T : DataRow;

Ce premier prototype crée automatiquement les versions originales de chaque champsans qu’il soit nécessaire d’appeler la méthode AcceptChange.

Le second prototype est appelé sur un IEnumerable<DataRow> d’une table source Data-Table. Il met à jour cette table en se basant sur la valeur LoadOption spécifiée dansl’argument.

Le second prototype

public static void CopyToDataTable<T> ( this IEnumerable<T> source, DataTable table, LoadOption options) where T : DataRow;

La valeur de l’argument LoadOption indique à l’opérateur si les valeurs originales et/oules valeurs actuelles des colonnes doivent être modifiées. Voici les valeurs possiblespour cet argument :

m OverwriteChanges. Les valeurs originale et actuelle de chaque colonne sont modi-fiées.

Linq.book Page 360 Mercredi, 18. février 2009 7:58 07

Page 376: LINQ Language Integrated Query en C

Chapitre 10 LINQ to DataSet 361

m PreserveChanges. Seule la valeur originale de chaque colonne est modifiée.

m Upsert. Seule la valeur actuelle de chaque colonne est modifiée.

L’argument LoadOption induit un nouveau problème : comment l’opérateur CopyToDa-taTable peut-il savoir quel enregistrement de la DataTable de destination correspond àl’enregistrement de la DataTable source ? L’enregistrement source doit-il être ajouté autableau de destination, ou un des enregistrements déjà présents doit-il être mis à jour ?Impossible de répondre à ces deux questions, à moins que l’opérateur n’utilise des clésprimaires.

Pour que le second prototype de l’opérateur CopyToDataTable fonctionne, l’objetDataTable de destination doit donc être pourvu de champs appropriés spécifiés en tantque clés primaires. Dans le cas contraire, les enregistrements sources seront ajoutés autableau de destination.

Une autre complication est inhérente au second prototype : les champs n’ont aucunevaleur originale, à moins que vous n’appeliez la méthode AcceptChanges pour les créer.Si vous essayez d’accéder à la version originale d’un champ qui en est dénué, uneexception se produit. Notez cependant que vous pouvez appeler la méthode HasVersionsur chacun des objets DataRow pour savoir s’ils possèdent une version originale, et ainsiéviter qu’une exception ne soit générée.

ExemplesPour illustrer le premier prototype, nous allons modifier un champ dans un DataTable,créer un nouveau DataTable à partir du DataTable modifié en invoquant l’opérateurCopyToDataTable et afficher le contenu du nouveau DataTable (voir Listing 10.17).

Listing 10.17 : Appel du premier prototype de l’opérateur CopyToDataTable.

Student[] students = { new Student { Id = 1, Name = "Joe Rattz" }, new Student { Id = 7, Name = "Anthony Adams" }, new Student { Id = 13, Name = "Stacy Sinclair" }, new Student { Id = 72, Name = "Dignan Stephens" }};

DataTable dt1 = GetDataTable(students);

Console.WriteLine("Le DataTable original :");foreach (DataRow dataRow in dt1.AsEnumerable()){ Console.WriteLine("Student Id = {0} is {1}", dataRow.Field<int>("Id"), dataRow.Field<string>("Name"));}

(from s in dt1.AsEnumerable() where s.Field<string>("Name") == "Anthony Adams" select s).Single<DataRow>().SetField("Name", "George Oscar Bluth");

DataTable newTable = dt1.AsEnumerable().CopyToDataTable();

Linq.book Page 361 Mercredi, 18. février 2009 7:58 07

Page 377: LINQ Language Integrated Query en C

362 LINQ to DataSet Partie IV

Console.WriteLine("{0}Le nouveau DataTable :", System.Environment.NewLine);foreach (DataRow dataRow in newTable.AsEnumerable()){ Console.WriteLine("L’étudiant d’Id = {0} est {1}", dataRow.Field<int>("Id"), dataRow.Field<string>("Name"));}

Les premières lignes définissent un objet DataTable à partir du tableau students. Lebloc d’instructions suivant affiche le contenu du DataTable dans la fenêtre console. Lebloc d’instructions suivant modifie le champ Name d’un des objets DataRow. Un nouveauDataTable est alors créé à partir des données modifiées en invoquant l’opérateur Copy-ToDataTable. Le dernier bloc d’instructions affiche le contenu du nouveau DataTable.

Voici le résultat :

Comme vous pouvez le voir, le nouveau DataTable contient la version modifiée duDataTable original.

Nous allons maintenant illustrer le deuxième prototype de l’opérateur CopyToDataTa-ble. Comme il a été dit précédemment, il est nécessaire de définir une clé primaire dansle DataSet de destination pour que l’argument LoadOption produise l’effet escompté. Àdes fins démonstratives, nous ne définirons aucune clé primaire (voir Listing 10.18).

Cet exemple étant plus complexe que le précédent, nous donnerons des explications àchaque fois que cela sera nécessaire.

Listing 10.18 : Appel du second prototype sans définir une clé primaire dans le DataSet de destination.

Student[] students = { new Student { Id = 1, Name = "Joe Rattz" }, new Student { Id = 7, Name = "Anthony Adams" }, new Student { Id = 13, Name = "Stacy Sinclair" }, new Student { Id = 72, Name = "Dignan Stephens" }};

DataTable dt1 = GetDataTable(students);DataTable newTable = dt1.AsEnumerable().CopyToDataTable();

Jusqu’ici, rien de nouveau : le DataTable source est créé à partir du tableau students etle DataTable de destination, en appelant l’opérateur CopyToDataTable sur l’objetDataTable source. Étant donné que nous avons utilisé le premier prototype de l’opérateur

Le DataTable original :L’étudiant d’Id = 1 est Joe RattzL’étudiant d’Id = 7 est Anthony AdamsL’étudiant d’Id = 13 est Stacy SinclairL’étudiant d’Id = 72 est Dignan Stephens

Le nouveau DataTable :L’étudiant d’Id = 1 est Joe RattzL’étudiant d’Id = 7 est George Oscar BluthL’étudiant d’Id = 13 est Stacy SinclairL’étudiant d’Id = 72 est Dignan Stephens

Linq.book Page 362 Mercredi, 18. février 2009 7:58 07

Page 378: LINQ Language Integrated Query en C

Chapitre 10 LINQ to DataSet 363

CopyToDataTable, il n’est pas nécessaire d’utiliser la méthode AcceptChanges sur leDataTable de destination. Il est important de le signaler car, dans le prochain bloc decode, la version originale du champ Name sera invoquée. Si la version originale de cetopérateur n’existait pas, une exception serait levée.

Console.WriteLine("Avant la mise à jour du DataTable :");foreach (DataRow dataRow in newTable.AsEnumerable()){ Console.WriteLine("Student Id = {0} : original {1} : current {2}", dataRow.Field<int>("Id"), dataRow.Field<string>("Name", DataRowVersion.Original), dataRow.Field<string>("Name", DataRowVersion.Current));}

Rien d’exceptionnel dans ce code, si ce n’est que la version originale du champ Name estutilisée. Aucune exception ne sera levée, puisqu’une version originale a été créée defaçon transparente par le premier prototype de l’opérateur CopyToDataTable.

(from s in dt1.AsEnumerable() where s.Field<string>("Name") == "Anthony Adams" select s).Single<DataRow>().SetField("Name", "George Oscar Bluth");

dt1.AsEnumerable().CopyToDataTable(newTable, LoadOption.Upsert);

Ce bloc de code est le plus intéressant de cet exemple. Comme vous pouvez le voir, lavaleur du champ Name d’un des enregistrements de l’objet DataTable source est modi-fiée avec l’opérateur SetField<T>. Cette modification effectuée, l’opérateur CopyToDa-taTable est appelé en spécifiant qu’une copie de type LoadOption.Upsert (limitée à lavaleur actuelle de chaque colonne) doit être effectuée. Ce deuxième opérateur CopyTo-DataTable pose un problème : la méthode AcceptChange n’ayant pas été appelée aupréalable, la valeur initiale des colonnes n’a pas été définie. Si nous essayons d’accéderà ces valeurs initiales, une exception sera générée. Pour éviter ce problème, il est néces-saire d’utiliser la méthode HasVersion. Comme aucune clé primaire n’a été définie,tous les enregistrements sources seront ajoutés dans le tableau de destination.

Console.WriteLine("{0}Après la mise à jour du DataTable:", System.Environment.NewLine);foreach (DataRow dataRow in newTable.AsEnumerable()){ Console.WriteLine("Etudiant d’Id = {0} : valeur originale {1}, valeur ➥actuelle{2}", dataRow.Field<int>("Id"), dataRow.HasVersion(DataRowVersion.Original) ? dataRow.Field<string>("Name", DataRowVersion.Original) : "-inexistante-", dataRow.Field<string>("Name", DataRowVersion.Current));}

Ce bloc de code se contente d’afficher le contenu de l’objet DataTable dans la console.Étant donné qu’aucune clé primaire n’a été définie dans le tableau de destination,aucune égalité ne sera établie entre les enregistrements lors de la copie du tableau. Tousles enregistrements sources seront donc ajoutés à la fin du DataTable de destination.

Linq.book Page 363 Mercredi, 18. février 2009 7:58 07

Page 379: LINQ Language Integrated Query en C

364 LINQ to DataSet Partie IV

Remarquez également que seuls les champs Name dont la valeur initiale existe, c’est-à-dire pour lesquels dataRow.HasVersion vaut true, sont affichés. Voici les résultats :

Comme vous pouvez le voir, plusieurs enregistrements apparaissent en double. Ceci estdû au fait qu’aucune clé primaire n’a été définie dans le DataTable de destination.L’enregistrement mis à jour apparaît également en double.

La méthode AcceptChanges n’ayant pas été automatiquement appelée par le secondprototype de l’opérateur CopyToDataTable, nous avons testé l’existence des valeursinitiales avec la méthode HasVersion. Vous vous demandez peut-être pourquoi nousn’avons pas simplement appelé la méthode AcceptChanges. Si nous l’avions fait, toutesles valeurs actuelles des champs seraient devenues des valeurs originales, et il aurait étéimpossible de déterminer quel enregistrement avait été modifié.

Pour solutionner ce problème, il suffit de définir une clé primaire dans le DataTable dedestination (voir Listing 10.19).

Listing 10.19 : Appel du second prototype en définissant une clé primaire dans le DataTable de destination.

Student[] students = { new Student { Id = 1, Name = "Joe Rattz" }, new Student { Id = 7, Name = "Anthony Adams" }, new Student { Id = 13, Name = "Stacy Sinclair" }, new Student { Id = 72, Name = "Dignan Stephens" }};

DataTable dt1 = GetDataTable(students);DataTable newTable = dt1.AsEnumerable().CopyToDataTable();newTable.PrimaryKey = new DataColumn[] { newTable.Columns[0] };

Console.WriteLine("Avant la mise à jour du DataTable :");

Avant la mise à jour du DataTable :L’étudiant d’Id = 1 : valeur originale Joe Rattz, valeur actuelle Joe RattzL’étudiant d’Id = 7 : valeur originale Anthony Adams, valeur actuelle Anthony AdamsL’étudiant d’Id = 13 : valeur originale Stacy Sinclair, valeur actuelle Stacy ➥SinclairL’étudiant d’Id = 72 : valeur originale Dignan Stephens, valeur actuelle Dignan ➥Stephens

Après la mise à jour du DataTable :L’étudiant d’Id = 1 : valeur originale Joe Rattz, valeur actuelle Joe RattzL’étudiant d’Id = 7 : valeur originale Anthony Adams, valeur actuelle Anthony AdamsL’étudiant d’Id = 13 : valeur originale Stacy Sinclair, valeur actuelle Stacy ➥SinclairL’étudiant d’Id = 72 : valeur originale Dignan Stephens, valeur actuelle Dignan ➥StephensL’étudiant d’Id = 1 : valeur originale -inexistante-, valeur actuelle Joe RattzL’étudiant d’Id = 7 : valeur originale -inexistante-, valeur actuelle George Oscar ➥BluthL’étudiant d’Id = 13 : valeur originale -inexistante-, valeur actuelle Stacy ➥SinclairL’étudiant d’Id = 72 : valeur originale -inexistante-, valeur actuelle Dignan ➥Stephens

Linq.book Page 364 Mercredi, 18. février 2009 7:58 07

Page 380: LINQ Language Integrated Query en C

Chapitre 10 LINQ to DataSet 365

foreach (DataRow dataRow in newTable.AsEnumerable()){ Console.WriteLine("Etudiant d’Id = {0} : valeur originale {1}, valeur actuelle ➥{2}", dataRow.Field<int>("Id"), dataRow.Field<string>("Name", DataRowVersion.Original), dataRow.Field<string>("Name", DataRowVersion.Current));}

(from s in dt1.AsEnumerable() where s.Field<string>("Name") == "Anthony Adams" select s).Single<DataRow>().SetField("Name", "George Oscar Bluth");

dt1.AsEnumerable().CopyToDataTable(newTable, LoadOption.Upsert);

Console.WriteLine("{0}Après la mise à jour du DataTable :", System.Environment.NewLine);foreach (DataRow dataRow in newTable.AsEnumerable()){ Console.WriteLine("Etudiant d’Id = {0} : valeur originale {1}, valeur actuelle ➥{2}", dataRow.Field<int>("Id"), dataRow.HasVersion(DataRowVersion.Original) ? dataRow.Field<string>("Name", DataRowVersion.Original) : "-does not exist-", dataRow.Field<string>("Name", DataRowVersion.Current));}

La seule différence entre cet exemple et le précédent réside dans la définition d’une cléprimaire dans le DataTable newTable. Voici les résultats :

Tout ceci est bien plus convenable : le champ Name de l’étudiant dont la colonne Id vaut7 avait pour valeur "Anthony Adams", mais il est maintenant égal à "George OscarBluth", et les enregistrements ne sont pas dupliqués.

Résumé

Ce chapitre vous a montré comment utiliser les opérateurs IEnumerable<T> pour initia-liser des objets DataRow et les opérateurs Field<T> et SetField<T> pour initialiser etlire les valeurs stockées dans des champs. Vous avez également vu qu’il était impératifd’utiliser les opérateurs spécifiques DataSet pour obtenir les résultats escomptés.

Avant la mise à jour du DataTable :L’étudiant d’Id = 1 : valeur originale Joe Rattz, valeur actuelle Joe RattzL’étudiant d’Id = 7 : valeur originale Anthony Adams, valeur actuelle Anthony AdamsL’étudiant d’Id = 13 : valeur originale Stacy Sinclair, valeur actuelle Stacy SinclairL’étudiant d’Id = 72 : valeur originale Dignan Stephens, valeur actuelle Dignan ➥Stephens

Après la mise à jour du DataTable :L’étudiant d’Id = 1 : valeur originale Joe Rattz, valeur actuelle Joe RattzL’étudiant d’Id = 7 : valeur originale Anthony Adams, valeur actuelle George Oscar ➥BluthL’étudiant d’Id = 13 : valeur originale Stacy Sinclair, valeur actuelle Stacy ➥SinclairL’étudiant d’Id = 72 : valeur originale Dignan Stephens, valeur actuelle Dignan ➥Stephens

Linq.book Page 365 Mercredi, 18. février 2009 7:58 07

Page 381: LINQ Language Integrated Query en C

366 LINQ to DataSet Partie IV

Enfin, vous avez vu qu’en les combinant avec les opérateurs de requête standard deLINQ to Objects vous pouviez définir des requêtes LINQ puissantes sur des objetsDataSet.

Dans le chapitre suivant, nous terminerons la partie dédiée à LINQ to DataSet enmontrant comment effectuer des requêtes sur des DataSet typés. Vous découvrirezégalement un exemple de requête LINQ to DataSet portant sur une base de donnéesréelle.

Linq.book Page 366 Mercredi, 18. février 2009 7:58 07

Page 382: LINQ Language Integrated Query en C

11

Possibilités complémentaires desDataSet

Le chapitre précédent a donné de nombreux exemples d’interrogation d’objets DataTa-ble. Dans un environnement de développement réel, ces objets proviendront de Data-Sets. Cependant, dans un souci de simplicité, ils ont été créés par programme, enutilisant des tableaux statiques. N’ayez crainte, comme vous le verrez dans ce chapitre,cette technique n’est nullement limitative.

Les exemples du chapitre précédent étaient tous basés sur des DataSets non typés. Il estparfois nécessaire d’exécuter une requête sur un DataSet typé en utilisant LINQ toDataSet.

Dans ce chapitre, nous examinerons ces nouvelles possibilités et vous montreronscomment tirer le meilleur de LINQ to DataSet. Nous commencerons par l’interrogationde DataSets typés. Et nous poursuivrons par l’interrogation d’une base de donnéesréelle.

Espaces de noms référencés

Les exemples de ce chapitre utilisent les classes des espaces de noms System.Data,System.Data.SqlClient et System.Linq. Si les directives using correspondantesn’existent pas dans votre code, vous devez les définir comme suit :

using System.Data;using System.Data.SqlClient;using System.Linq;

DataSets typés

LINQ est en mesure d’exécuter des requêtes sur des DataSets non typés et typés. Dansle second cas, le code d’interrogation sera très simple à écrire et à lire. Étant donné qu’il

Linq.book Page 367 Mercredi, 18. février 2009 7:58 07

Page 383: LINQ Language Integrated Query en C

368 LINQ to DataSet Partie IV

existe une classe dédiée aux DataSets, les requêtes peuvent accéder aux noms destables et aux colonnes en utilisant les propriétés de classe des objets DataSet typés.Cela est plus pratique qu’indexer la collection de Tables ou utiliser les opérateursField<T> et SetField<T>.

Plutôt qu’accéder à la table d’objets DataSet Students en utilisant cette instruction :

DataTable Students = dataSet.Tables["Students"];

vous utiliserez l’instruction suivante :

DataTable Students = dataSet.Students;

De la même manière, plutôt qu’obtenir la valeur d’un champ avec cette instruction :

dataRow.Field<string>("Name")

vous utiliserez l’instruction suivante :

dataRow.Name

Ces facilités d’écriture rendent le code bien plus facile à lire et à maintenir.

Avant de passer à la pratique, nous avons besoin de définir un DataSet typé. Voicicomment procéder :

1. Cliquez du bouton droit sur l’entrée correspondant au nom de votre projet dans lafenêtre Explorateur de solutions.

2. Sélectionnez Ajouter/Nouvel élément dans le menu contextuel.

3. Si nécessaire, développez l’arbre des catégories et sélectionnez Données dans laliste. Sous Modèles Visual Studio installés, sélectionnez DataSet. Donnez le nomStudentsDataSet.xsd au fichier DataSet et cliquez sur le bouton Ajouter.

4. Quelques instants plus tard, l’espace de travail affiche un concepteur de DataSet.Placez le pointeur sur la Boîte à outils, cliquez et glissez-déposez un DataTable surle concepteur de DataSet.

5. Cliquez du bouton droit sur la barre de titre du DataSet que vous venez d’ajouter etsélectionnez Propriétés dans le menu contextuel.

6. Dans la fenêtre Propriétés, donnez le nom Students au DataTable.

7. Cliquez du bouton droit sur la barre de titre du DataSet et sélectionnez Ajouter/Colonne dans le menu contextuel.

8. Affectez la valeur "Id" à la propriété Name et la valeur "System.Int32" à la propriétéDataType.

9. Cliquez du bouton droit sur la barre de titre du DataSet et sélectionnez Ajouter/Colonne dans le menu contextuel.

10. Affectez la valeur "Name" à la propriété Caption de ce DataColumn.

11. Sauvegardez le fichier.

Linq.book Page 368 Mercredi, 18. février 2009 7:58 07

Page 384: LINQ Language Integrated Query en C

Chapitre 11 Possibilités complémentaires des DataSet 369

Vous venez de créer le DataSet typé StudentsDataSet. Ce DataSet contient le DataTa-ble Student, qui contient lui-même deux colonnes de type DataColumn. La première apour nom Id et pour type Int32. La seconde a pour nom Name et pour type string.Nous allons utiliser ce DataSet pour effectuer des requêtes LINQ. Étant donné que ceDataSet est typé, nous pourrons accéder aux champs DataRow directement (voirListing 11.1).

Listing 11.1 : Un exemple de requête sur un DataSet typé.

StudentsDataSet studentsDataSet = new StudentsDataSet();studentsDataSet.Students.AddStudentsRow(1, "Joe Rattz");studentsDataSet.Students.AddStudentsRow(7, "Anthony Adams");studentsDataSet.Students.AddStudentsRow(13, "Stacy Sinclair");studentsDataSet.Students.AddStudentsRow(72, "Dignan Stephens");

string name = studentsDataSet.Students.Where(student => student.Id == 7).Single().Name;

Console.WriteLine(name);

Dans cet exemple, un objet StudentsDataSet est instancié et quatre enregistrementsStudents y sont ajoutés. Tout comme dans les exemples du chapitre précédent, chaqueenregistrement correspond à un étudiant. Dans la plupart des codes de production réels,cette étape ne sera pas nécessaire, car les données proviendront d’une base de données.

Une fois le DataSet typé initialisé, une requête LINQ lui est appliquée. Remarquezqu’on accède à la DataTable Students en tant que propriété de l’objet StudentsData-Set. Remarquez également que, dans l’expression lambda de la clause Where, onaccède à la propriété Id directement à partir de l’élément. Ici, il est inutile d’appeler lapropriété Field du DataRow. Cette facilité d’écriture vient du fait que le DataSet esttypé. Notez enfin qu’il est possible d’accéder à la propriété Name du résultat renvoyé parl’opérateur Single. Une fois encore, cette facilité d’écriture vient du fait que le DataSetest typé.

Voici le résultat :

Tout ceci est bien agréable : la manipulation de DataSets typés s’apparente au travailavec des objets et propriétés de classes.

Un exemple plus proche de la réalité

Les exemples du chapitre précédent ont été intentionnellement simplifiés pour faciliterl’apprentissage de l’API LINQ to DataSet. Nous avons fait en sorte qu’à travers lesdifférents exemples vous vous concentriez essentiellement sur LINQ. En particulier,nous avons évité de présenter le code nécessaire à la connexion sur une base dedonnées. Avant de terminer ce chapitre, je voudrais néanmoins vous donner un exemple

Anthony Adams

Linq.book Page 369 Mercredi, 18. février 2009 7:58 07

Page 385: LINQ Language Integrated Query en C

370 LINQ to DataSet Partie IV

plus complet et plus proche de la réalité, dans lequel le DataSet est défini à partir d’unebase de données.

Je dois avouer que la mise au point d’un exemple de taille raisonnable qui lit desdonnées dans une base de données et utilise l’API LINQ to DataSet pour les interrogerest un peu tiré par les cheveux. En effet, nous allons exécuter une requête SQL sur lesdonnées d’une base de données en utilisant ADO.NET pour obtenir un DataSet. Aprèsquoi nous interrogerons ce DataSet avec LINQ to DataSet pour obtenir les donnéesrecherchées. Pourquoi ne pas modifier la requête SQL pour obtenir directement lesinformations recherchées ? Eh bien tout simplement dans un but pédagogique !

Dans cet exemple, nous allons travailler avec la base de données de la sociétéNorthwind. Cette société utilise une application qui effectue des requêtes sur lescommandes. Cette application effectue différentes analyses sur les relations entreemployés et clients et sur les pays d’expédition des différentes commandes. Cette appli-cation place les employés, les clients et les pays de destination dans un DataSet. Notretâche va consister à effectuer une analyse complémentaire sur ces données. À titred’exemple, nous allons établir la liste de toutes les ventes à destination de l’Allemagneeffectuées par chacun des employés.

Dans cet exemple, nous instancions un SqlDataAdapter puis un DataSet et appelons laméthode Fill du SqlDataAdapter pour remplir le DataSet. Cette étape aurait déjà dûêtre faite par l’application dont nous venons de parler. Mais, étant donné que nous netravaillons pas dans un environnement réel, elle sera effectuée dans notre code. Une foisl’objet DataSet initialisé par la requête SQL, nous lancerons une requête LINQ to Data-Set et afficherons le résultat (voir Listing 11.2).

Listing 11.2 : Un exemple plus proche de la réalité.

string connectionString = @"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind;Integrated Security=SSPI;";

SqlDataAdapter dataAdapter = new SqlDataAdapter( @"SELECT O.EmployeeID, E.FirstName + ’ ’ + E.LastName as EmployeeName, O.CustomerID, C.CompanyName, O.ShipCountry FROM Orders O JOIN Employees E on O.EmployeeID = E.EmployeeID JOIN Customers C on O.CustomerID = C.CustomerID", connectionString);

DataSet dataSet = new DataSet();dataAdapter.Fill(dataSet, "EmpCustShip");

// Ici se termine le code hérité

var ordersQuery = dataSet.Tables["EmpCustShip"].AsEnumerable() .Where(r => r.Field<string>("ShipCountry").Equals("Germany")) .Distinct(System.Data.DataRowComparer.Default) .OrderBy(r => r.Field<string>("EmployeeName")) .ThenBy(r => r.Field<string>("CompanyName"));

foreach(var dataRow in ordersQuery)

Linq.book Page 370 Mercredi, 18. février 2009 7:58 07

Page 386: LINQ Language Integrated Query en C

Chapitre 11 Possibilités complémentaires des DataSet 371

{ Console.WriteLine("{0,-20} {1,-20}", dataRow.Field<string>("EmployeeName"), dataRow.Field<string>("CompanyName"));}

Les premières lignes établissent la connexion avec la base de données Northwind. Il sepeut que vous ayez à modifier les paramètres de la connexion pour qu’ils s’adaptent àvotre propre base de données.

Dans la requête LINQ, nous utilisons les opérateurs AsEnumerable, Distinct etField<T> (voir chapitre précédent) et les opérateurs Where, OrderBy et ThenBy de l’APILINQ to Objects pour créer la requête appropriée à nos besoins. J’espère que vousappréciez à sa juste valeur la facilité avec laquelle tous ces opérateurs dialoguent entreeux. Si la requête fonctionne, nous devrions obtenir la liste de tous les employés qui onteffectué au moins une vente à une société allemande. Cette liste devrait être classée parordre alphabétique sur les noms d’employés puis sur les sociétés et ne devrait compren-dre aucun doublon. Voici les résultats :

Vous pouvez remarquer que les résultats ne comprennent aucun doublon. Cela montreune fois de plus l’intérêt des opérateurs d’initialisation de l’API LINQ to DataSet. Àtitre d’information, si vous supprimez l’argument DataRowComparer.Default dansl’opérateur Distinct, vous verrez que plusieurs doublons apparaissent dans les résultats.

Le Listing 11.3 donne un exemple utilisant la syntaxe d’expression de requête.

Listing 11.3 : Un exemple plus proche de la réalité utilisant la syntaxe d’expression de requête.

string connectionString = @"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind;Integrated Security=SSPI;";

SqlDataAdapter dataAdapter = new SqlDataAdapter( @"SELECT O.EmployeeID, E.FirstName + ’ ’ + E.LastName as EmployeeName, O.CustomerID, C.CompanyName, O.ShipCountry FROM Orders O JOIN Employees E on O.EmployeeID = E.EmployeeID JOIN Customers C on O.CustomerID = C.CustomerID", connectionString);

Andrew FullerDie Wandernde KuhAndrew FullerKöniglich EssenAndrew FullerLehmanns MarktstandAndrew FullerMorgenstern GesundkostAndrew FullerOttilies KäseladenAndrew FullerQUICK-StopAndrew FullerToms SpezialitätenAnne DodsworthBlauer See DelikatessenAnne DodsworthKöniglich EssenAnne DodsworthLehmanns MarktstandAnne DodsworthQUICK-Stop…Steven BuchananFrankenversandSteven BuchananMorgenstern GesundkostSteven BuchananQUICK-Stop

Linq.book Page 371 Mercredi, 18. février 2009 7:58 07

Page 387: LINQ Language Integrated Query en C

372 LINQ to DataSet Partie IV

DataSet dataSet = new DataSet();dataAdapter.Fill(dataSet, "EmpCustShip");

// All code prior to this comment is legacy code.

var ordersQuery = (from r in dataSet.Tables["EmpCustShip"].AsEnumerable() where r.Field<string>("ShipCountry").Equals("Germany") orderby r.Field<string>("EmployeeName"), r.Field<string>("CompanyName") select r) .Distinct(System.Data.DataRowComparer.Default);*foreach (var dataRow in ordersQuery){ Console.WriteLine("{0,-20} {1,-20}", dataRow.Field<string>("EmployeeName"), dataRow.Field<string>("CompanyName"));}

Cette fois-ci, la requête utilise la syntaxe d’expression de requête. Nous avons essayé delui donner la même allure que dans l’exemple précédent, mais cela n’a pas été possible.Remarquez la position de l’opérateur Distinct, en fin de la requête. Rappelez-vous quele compilateur n’est en mesure de traduire que les opérateurs les plus courants d’unerequête exprimée avec la syntaxe d’expression de requête. Dans cet exemple, il ne saitpas comment traduire l’opérateur Distinct. C’est la raison pour laquelle cet opérateurne peut pas être utilisé dans la portion "syntaxe d’expression de requête" de la requêteet a été déporté à la fin de la requête.

Les résultats finaux des requêtes des Listings 11.2 et 11.3 sont identiques, mais ilsprésentent une différence au niveau des performances. Dans le Listing 11.2, l’opérateurDistinct est appelé juste après l’opérateur Where. Les doublons sont donc éliminésavant d’effectuer le classement. Dans le Listing 11.3, l’opérateur Distinct est appelé àla fin de la requête. Les doublons ont donc été pris en compte pendant l’étape de classe-ment. Cela engendre une charge supplémentaire qui se révèle toutefois indispensable sivous voulez utiliser la syntaxe d’expression de requête.

Résumé

Ce chapitre vous a montré que vous pouviez effectuer des requêtes LINQ to DataSet surdes DataSets typés. Sur ce type de DataSets, le code d’interrogation est plus simple àmaintenir et plus lisible. Vous avez également pu découvrir un exemple d’interrogationLINQ to DataSets plus réaliste, fondé sur la base de données Northwind.

L’API LINQ to DataSet ajoute un autre domaine d’utilisation aux requêtes LINQ. Parson intermédiaire, l’interrogation de DataSets n’a jamais été aussi simple, et lesnombreux codes qui utilisent ces objets ont tout intérêt à être remis au goût du jour.

L’API LINQ to DataSet a un avantage par rapport à l’API LINQ to SQL : aucun codede classe de base de données ne doit être généré et compilé avant de pouvoir effectuerdes requêtes. Ceci rend LINQ to DataSet plus dynamique et mieux adapté aux program-mes qui ne connaissent la base de données qu’ils vont utiliser qu’au moment del’exécution (les utilitaires de bases de données, par exemple).

Linq.book Page 372 Mercredi, 18. février 2009 7:58 07

Page 388: LINQ Language Integrated Query en C

Chapitre 11 Possibilités complémentaires des DataSet 373

Grâce à l’opérateur AsEnumerable, qui permet de créer des séquences à partir d’objetsDataTable, les opérateurs de requête standard LINQ to Objects viennent compléterl’arsenal de LINQ to DataSet, augmentant encore ses possibilités déjà immenses.

Des opérateurs ont été ajoutés dans les classes clés de LINQ to DataSet : DataTable,DataRow et DataColumn. N’oubliez pas que de nouveaux prototypes ont été ajoutés auxopérateurs Distinct, Union, Intersect, Except et SequentialEqual. Ils sont indispen-sables pour éliminer le problème lié à la comparaison des DataRows. Chaque fois quevous travaillerez avec des DataSets, DataTables et DataRows, utilisez les prototypesLINQ to DataSet des opérateurs d’initialisation Distinct, Union, Intersect, Except etSequentialEqual dans lesquels un comparateur d’égalité est spécifié en argument.

Lorsque vous travaillez avec des valeurs de colonnes, prenez le soin d’utiliser les opéra-teurs Field<T> et SetField<T> pour éviter les problèmes liés à la comparaison et auxvaleurs null.

En travaillant avec LINQ to DataSet, je me suis rendu compte que j’avais totalementsous-estimé la puissance et l’utilité des DataSets. Ils offrent des caractéristiques trèsintéressantes en matière de manipulation et de stockage de données. Leurs possibilitésde recherche quelque peu limitées disparaissent totalement lorsqu’ils sont épaulés parLINQ. Désormais, vous pourrez donc compter avec LINQ pour effectuer des requêtessur vos DataSets. Vous verrez, le codage sera bien plus simple qu’avant…

Linq.book Page 373 Mercredi, 18. février 2009 7:58 07

Page 389: LINQ Language Integrated Query en C

Linq.book Page 374 Mercredi, 18. février 2009 7:58 07

Page 390: LINQ Language Integrated Query en C

V

LINQ to SQL

Linq.book Page 375 Mercredi, 18. février 2009 7:58 07

Page 391: LINQ Language Integrated Query en C

Linq.book Page 376 Mercredi, 18. février 2009 7:58 07

Page 392: LINQ Language Integrated Query en C

12

Introduction à LINQ to SQL

Listing 12.1 : Un exemple élémentaire de mise à jour du champ ContactName d’un client dans la base de données Northwind.

// Création d’un DataContextNorthwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind");

// Accès au client LAZYKCustomer cust = (from c in db.Customers where c.CustomerID == "LAZYK" select c).Single<Customer>();

// Mise à jour du nom du contactcust.ContactName = "Ned Plimpton";

try{ // Sauvegarde des modifications db.SubmitChanges();}

// Détection des conflits d’accès concurrentielscatch (ChangeConflictException){ // Résolution des conflits db.ChangeConflicts.ResolveAll(RefreshMode.KeepChanges);}

INFO

Cet exemple nécessite la génération de classes d’entités. Vous trouverez tous les renseignementsnécessaires un peu plus loin dans ce chapitre.

Le Listing 12.1 travaille sur la table Customers de la base de données Northwind. Ilutilise une requête LINQ to SQL pour obtenir l’enregistrement dont le champ CustomerIDvaut "LAZYK" et pour retourner un objet Customer qui représente cet enregistrement.La propriété ContactName de l’objet Customer est alors mise à jour et l’enregistrement

Linq.book Page 377 Mercredi, 18. février 2009 7:58 07

Page 393: LINQ Language Integrated Query en C

378 LINQ to SQL Partie V

est sauvegardé dans la base de données par l’intermédiaire de la méthode SubmitChan-ges. Ce code n’est pas très long si l’on considère qu’il détecte les éventuels conflits et,le cas échéant, les résout.

Appuyez sur Ctrl+F5 pour exécuter ce code. Vous n’obtenez aucune sortie consolemais, si vous vérifiez le contenu de la base de données, vous verrez que le champContactName du client LAZYK vaut maintenant "Ned Plimpton".

INFO

Cet exemple modifie la base de données mais ne rétablit pas les données originales. Pourque les exemples donnés dans ce chapitre fonctionnent correctement, vous devez affecter lavaleur "John Steel" au champ ContactName du client LAZYK. Cette modification peut sefaire "à la main" ou en ajustant le code du Listing 12.1 en conséquence.

INFO

Cet ouvrage utilise une version étendue de la base de données Northwind. Reportez-vous àla section "Comment obtenir la version appropriée de la base de données Northwind" pouravoir toutes les informations nécessaires à ce sujet.

Introduction à LINQ to SQLArrivé à ce point dans la lecture de cet ouvrage, vous savez comment utiliser LINQ surdes collections de données et des tableaux en mémoire, des fichiers XML et des Data-Sets. Nous allons nous intéresser à ce que beaucoup considèrent comme la partie laplus importante de LINQ : LINQ to SQL. Je dis cela parce que la plupart des billetsrelatifs à LINQ dans le forum MSDN s’intéressent essentiellement à LINQ to XML. Jepense que beaucoup de développeurs ne sont pas pleinement conscients que le langagede requête LINQ peut "jouer dans plusieurs cours". J’espère que les chapitres précé-dents vous ont convaincu de son éclectisme.

L’API (Application Programming Interface) LINQ to SQL est faite pour interfacer lesbases de données SQL Server. Dans le monde des langages de programmation orientésobjets, l’interfaçage d’une base de données est souvent considéré comme le point leplus épineux. Lorsque nous écrivons une application, nous modelons les classes pourreprésenter des objets du monde réel : des clients, des comptes, des stocks, etc. Nousavons besoin de rendre ces objets persistants, de telle sorte que, lorsque l’application estlancée une nouvelle fois, ces objets et leurs valeurs ne sont pas perdus. La plupart desbases de données utilisées en production sont toujours relationnelles. Elles stockent lesdonnées dans des tables, en tant qu’enregistrements et non en tant qu’objets. Une classeclient peut ainsi contenir des adresses et des téléphones stockés dans des collections quisont des propriétés enfants de cette classe. Une fois rendues persistantes, ces donnéesseront certainement stockées dans différentes tables : une table de clients, une tabled’adresses et une table de téléphones.

Linq.book Page 378 Mercredi, 18. février 2009 7:58 07

Page 394: LINQ Language Integrated Query en C

Chapitre 12 Introduction à LINQ to SQL 379

Par ailleurs, les types de données supportés par le langage de programmation diffèrentsouvent de ceux de la base de données. Les développeurs doivent alors écrire du codequi sait comment initialiser un objet client à partir des tables appropriées et commentsauvegarder un objet Customer dans ces mêmes tables, en effectuant les conversions detypes nécessaires. Cette étape est souvent ennuyeuse et propice aux erreurs. Pourcontrer ce problème, lié au mappage des données relationnelles, de nombreux logicielsORM (Object-Relational Mapping) ont été écrits. LINQ to SQL est l’ORM d’entrée degamme compatible LINQ de Microsoft dédié aux bases de données SQL Server.

Les autres fabricants de bases de données sont déjà (ou vont se mettre) au travail pourimplémenter leur propre API LINQ. Personnellement, j’aimerais bien qu’une APILINQ to DB2 voie le jour. Je suis sûr que beaucoup d’entre vous apprécieraient desAPI LINQ to Oracle, LINQ to MySQL, LINQ to Sybase, etc.

INFO

LINQ to SQL ne peut être utilisé qu’avec SQL Server et SQL Express. Pour utiliser LINQ avecd’autres types de bases de données, vous devez utiliser des API additionnelles, mises aupoint par les différents fabricants des bases de données. Si ces API n’existent pas encore,vous pouvez toujours utiliser LINQ to DataSet.

Quelques lignes auparavant, j’ai dit que LINQ to SQL était une implémentation ORMd’entrée de gamme. Si sa puissance et/ou sa flexibilité ne vous suffisent pas, vouspouvez vous tourner vers LINQ to Entities. Cette partie de LINQ ne sera pas traitéedans cet ouvrage. Si elle procure plus de puissance et de flexibilité, elle complexifieégalement l’écriture. Par ailleurs, elle n’est pas aussi mature que LINQ to SQL…

La plupart des outils ORM limitent la manipulation des bases de données à celle desobjets métier (entités) correspondants. Cette limitation interdit l’utilisation de requêtesSQL, pourtant si importantes dans les bases de données relationnelles. LINQ to SQL sedifférencie de beaucoup de ses contemporains : il sait en effet tirer parti des objetsmappés à la base de données et offre un langage de requête semblable au SQL.

INFO

LINQ to SQL est un ORM d’entrée de gamme qui permet l’utilisation de requêtes SQL puis-santes.

Étant donné que les requêtes LINQ to SQL retournent des objets entité (et non desimples champs, des classes de nonentités nommées ou des classes anonymes), vousavez accès à toute la puissance de LINQ. Par ailleurs, LINQ to SQL vous permet égale-ment de rechercher les modifications effectuées sur les enregistrements et de mettre àjour la base de données, tout en détectant et en résolvant d’éventuels conflits d’accèsconcurrentiels et en assurant l’intégrité transactionnelle.

Linq.book Page 379 Mercredi, 18. février 2009 7:58 07

Page 395: LINQ Language Integrated Query en C

380 LINQ to SQL Partie V

Les premières lignes du Listing 12.1 ont défini une instance de la classe Northwind.Cette classe est dérivée de la classe DataContext (reportez-vous au Chapitre 16 pouravoir de plus amples informations). Pour l’instant, considérez cette instance comme uneconnexion surchargée à la base de données. La mise à jour de la base de données estégalement supportée via la méthode SubmitChanges. Quelques lignes plus bas, un desclients de la base de données Northwind a été placé dans un objet Customer. Cet objet aété obtenu en instanciant la classe d’entité Customer. Cette dernière doit être écrite ougénérée. Dans cet exemple, la classe Customer (tout comme la classe Northwind) a étégénérée par l’utilitaire SQLMetal. Après avoir récupéré le client, la propriété Contact-Name de l’objet Customer a été mise à jour, et la méthode SubmitChanges a été appeléepour stocker la modification dans la base de données, et ainsi la rendre persistante.L’appel à la méthode SubmitChanges a été placé dans un bloc try/catch et nous avonsécrit un code de traitement pour l’exception ChangeConflictException. Cette excep-tion se produit lorsqu’un conflit d’accès concurrentiel est détecté. Vous en saurez plus àce sujet en consultant le Chapitre 17.

Avant de pouvoir exécuter cet exemple ou un des autres de ce chapitre, vous devez créerdes classes d’entité pour la base de données Northwind. Reportez-vous à la sectionintitulée "Prérequis pour exécuter les exemples" de ce chapitre pour savoir commentprocéder.

LINQ to SQL est un sujet complexe. Pour mettre au point un exemple, de nombreuxéléments LINQ to SQL sont nécessaires. Dans le premier exemple, au début de cechapitre, nous utilisons une classe dérivée de DataContext (Northwind) et une classed’entités (Customer). La détection et la résolution de conflits de concurrence ainsi quela mise à jour de la base de données sont effectuées via la méthode SubmitChanges.Avant de pouvoir expliquer ces différents concepts, nous allons vous inculquer quel-ques connaissances de base qui vous permettront de comprendre les fondements deLINQ to SQL. Rassurez-vous, tous ces concepts seront traités en détail dans les chapitressuivants.

La classe DataContext

DataContext est la classe qui permet d’établir une connexion avec la base de données.Elle fournit également plusieurs services annexes, tels que le contrôle d’identité, ladétection de modifications et le processus de sauvegarde des modifications. Tout cecisera traité en détail au Chapitre 16. Pour l’instant, il vous suffit de savoir que c’est laclasse DataContext qui établit la connexion avec la base de données, qui contrôle lesmodifications et met à jour la base de données lorsque la méthode SubmitChanges estappelée.

L’utilisation d’une classe dérivée de DataContext est très classique en LINQ to SQL.Le nom de la classe dérivée est généralement le même que celui de la base de données

Linq.book Page 380 Mercredi, 18. février 2009 7:58 07

Page 396: LINQ Language Integrated Query en C

Chapitre 12 Introduction à LINQ to SQL 381

à laquelle elle est reliée. Nous y ferons parfois référence sous la forme [Your]DataContext,car son nom est lié à celui de la base de données pour laquelle elle a été créée.

Dans les exemples de ce chapitre, la classe dérivée de DataContext appelle"Northwind". Cette classe a en effet été générée avec l’outil en ligne de commandeSQLMetal, qui donne automatiquement le nom de la base de données à la classe Data-Context dérivée.

La classe [Your]DataContext, dérivée de DataContext, aura généralement unepropriété publique Table<T> pour chaque table mappée à la base de données (où T est letype de la classe d’entité instanciée pour chaque enregistrement obtenu à partir de cettetable, et Table<T> est une collection spécialisée). Par exemple, étant donné que la baseNorthwind contient une table Customers, la classe Northwind, dérivée de la classeDataContext, aura un Table<Customer> nommé Customers. Il est donc possibled’accéder aux enregistrements de la base de données Customers par l’intermédiaire despropriétés Customers de type Table<Customer> dans la classe Northwind. LeListing 12.1 en est un exemple : le raccourci d’écriture db.Customers donne accès auxenregistrements de la table Customers de la base de données Northwind.

Classes d’entités

LINQ to SQL utilise des classes d’entités. Chaque classe d’entité est généralement liéeà une seule table de la base de données. Cependant, il est possible, sous certainescirconstances spécifiques, de mapper toute la hiérarchie d’une classe dans une simpletable. Vous en apprendrez plus à ce sujet en vous reportant au Chapitre 18.

Nous avons donc des classes d’entités liées aux tables d’une base de données, et lespropriétés des classes d’entité liées aux colonnes des tables. Ces relations classe/table etpropriété/colonne sont l’essence même de LINQ to SQL.

INFO

Le fondement de LINQ to SQL consiste à relier les classes d’entité aux tables d’une base dedonnées et les propriétés des classes d’entité aux colonnes des tables de la base de données.

Ces liaisons peuvent se faire directement dans les fichiers de la classe source, en utili-sant les bons attributs, ou dans un fichier de mappage XML externe. En utilisant unfichier de mappage externe, les éléments spécifiques à LINQ to SQL peuvent être main-tenus à l’extérieur du code source. Ceci est très pratique si vous n’avez pas accès aucode source ou si vous voulez le séparer de LINQ to SQL. Dans la plupart des exemplesde ce chapitre, nous utiliserons des classes d’entité générées par l’outil en ligne decommande SQLMetal. Dans ces classes d’entité générées, le mappage LINQ to SQLest intégré dans le module source, sous la forme d’attributs et de propriétés.

Linq.book Page 381 Mercredi, 18. février 2009 7:58 07

Page 397: LINQ Language Integrated Query en C

382 LINQ to SQL Partie V

Vous détecterez sans peine les classes d’entité dans les exemples : vous verrez des clas-ses ou des objets dont le nom est le singulier d’un nom de table de la base de donnéesNorthwind. À titre d’exemple, dans le Listing 12.1 nous utilisons la classe Customer.Ce nom étant le singulier de Customers, et la base de données Northwind ayant unetable nommée Customers, nous pouvons en déduire que la classe Customer est uneclasse d’entité de la table Customers de la base de données Northwind.

L’option /pluralize de l’outil en ligne de commande SQLMetal est le responsable decette "singularisation" des classes d’entité. Si cette option n’avait pas été spécifiée lorsde la génération des classes d’entité, la classe d’entité de la table Customers aurait éténommée Customers (et non Customer). Cette distinction est importante, pour le cas oùvous vous sentiriez confus en lisant d’autres écrits relatifs à LINQ to SQL : en fonctionde la façon dont l’outil SQLMetal a été utilisé, les noms des classes d’entité peuventêtre au pluriel ou au singulier.

Associations

Le terme "association" désigne la relation entre une clé primaire et une clé étrangère,utilisées pour relier deux classes d’entité. Dans une relation un-à-plusieurs, par exem-ple, une association consiste en une classe parent dotée d’une clé primaire et une collec-tion de classes enfants contenant des clés étrangères. Cette collection est stockée dansune variable membre privée de type EntitySet<T>, où T est le type de la classe d’entitéenfant.

À titre d’exemple, dans la classe d’entité Customer, générée par l’outil en ligne decommande SQLMetal pour la base de données Northwind, le membre privé _Orders,de type EntitySet<Order>, contient tous les objets Order pour un objet Customerspécifique :

private EntitySet<Order> _Orders;

SQLMetal génère également la propriété publique Orders, afin d’accéder à la collectionprivée _Orders.

De l’autre côté de la relation, la classe enfant (celle dans laquelle se trouve la clé étran-gère) contient une référence vers la classe parent, puisqu’il s’agit d’une relation un-à-plusieurs. Cette référence est mémorisée dans une variable membre privée de typeEntityRef<T>, où T est le type de la classe parent.

La classe d’entité Order contient la variable membre privée _Customer de type Entity-Ref<Customer> :

private EntityRef<Customer> _Customer;

Ici encore, l’outil SQLMetal a généré la propriété Customer pour donner accès auparent.

Linq.book Page 382 Mercredi, 18. février 2009 7:58 07

Page 398: LINQ Language Integrated Query en C

Chapitre 12 Introduction à LINQ to SQL 383

L’association entre les clés primaire et étrangère ainsi que la direction de la relationsont définies par des attributs et des propriétés d’attributs dans le module source desclasses d’entité générées.

Cette association permet d’accéder aux classes enfants du parent – et donc aux enregis-trements de la base de données – aussi simplement que s’il s’agissait de propriétés de laclasse parent. De la même façon, l’accès à la classe parent d’un enfant est aussi simplequ’accéder à une propriété d’une classe enfant.

Détection de conflit d’accès concurrentiel

Un des services appréciables du DataContext est le traitement associé aux modifica-tions : lorsque vous essayez de mettre à jour votre base de données en appelant laméthode SubmitChanges de l’objet DataContext, une détection de conflit d’accèsconcurrentiels est automatiquement lancée.

Si un conflit est détecté, une exception ChangeConflictException est levée. Chaquefois que vous appelez la méthode SubmitChanges, vous devez donc l’inclure dans unbloc try/catch afin de traiter une éventuelle exception ChangeConflictException.

Reportez-vous au Listing 12.2 pour avoir un exemple de détection de conflit. Nousentrerons bien plus dans les détails sur la détection et la résolution des conflits auChapitre 17. Dans un but de concision et de clarté, la plupart des exemples des chapitresdédiés à LINQ to SQL n’incluront aucun code de détection et de résolution d’erreur.Cependant, dans un code de production réel, ce code devrait être systématiquement misen place…

Résolution de conflit d’accès concurrentiel

Une fois qu’un conflit a été détecté, vous devez le résoudre. Plusieurs techniquespeuvent être utilisées. Le Listing 12.1 utilise la technique la plus élémentaire. Ici, nousnous contentons d’appeler la méthode ResolveAll (collection ChangeConflicts de laclasse dérivée de DataContext) lorsqu’une exception ChangeConflictException estlevée.

Rappelons une fois encore que, dans un but de concision et de clarté, la plupart desexemples des chapitres dédiés à LINQ to SQL n’incluront aucun code de détection et derésolution d’erreur. Cependant, dans un code de production réel, ce code devrait êtresystématiquement mis en place. Le cas échéant, reportez-vous au Chapitre 17 pouravoir de plus amples détails sur la résolution de conflits.

Prérequis pour exécuter les exemples

La plupart des exemples des chapitres dédiés à LINQ to SQL utilisant la base dedonnées Northwind, fournie en exemple par Microsoft, nous avons besoin de classesd’entité et de fichiers de mappage pour cette base de données.

Linq.book Page 383 Mercredi, 18. février 2009 7:58 07

Page 399: LINQ Language Integrated Query en C

384 LINQ to SQL Partie V

Obtenir la version appropriée de la base de données Northwind

Plusieurs petites choses manquent dans la version originale de la base de donnéesNorthwind de Microsoft (les fonctions table-valued et scalar-valued, par exemple)pour que nous puissions montrer toutes les facettes de LINQ to SQL. Nous allons doncutiliser une version étendue de cette base de données.

Vous pouvez télécharger la version appropriée de la base de données Northwind dans lasection "Book Extras" de la page suivante, sur le site d’Apress :

http://www.apress.com/book/bookDisplay.html?bID=10241

Vous pouvez également vous rendre sur le site LINQDev.com et lancer le téléchargementdepuis la section "Obtain the Northwind Database" :

http://www.linqdev.com

Si vous téléchargez la base de données depuis LINQDev.com, assurez-vous que voustéléchargez la version étendue et non la version originale de la base de données.

Génération des classes d’entité de la base de données Northwind

La génération de classes d’entité n’a pas encore été étudiée. Je vais donc vous direcomment procéder, sans toutefois entrer dans les détails. Reportez-vous au Chapitre 13pour en savoir plus à ce sujet.

Pour commencer, assurez-vous que vous avez téléchargé la version étendue de la basede données Northwind.

Ouvrez une fenêtre Invite de commandes de Visual Studio. Pour ce faire, cliquezsuccessivement sur le bouton Démarrer, Tous les programmes, Microsoft Visual Studio2008, Visual Studio Tools puis Invite de commandes de Visual Studio 2008. Déplacez-vous dans le dossier où les classes d’entité et le fichier de mappage doivent être générés.Nous allons par exemple nous déplacer dans la racine du disque C en tapant :

cd \

Si vous voulez générer les classes d’entité de la base de données Northwind sans lesattacher au préalable à la base, utilisez la commande suivante :

sqlmetal /namespace:nwind /code:Northwind.cs /pluralize /functions /sprocs /views <chemin vers le fichier Northwind.mdf>

ATTENTIONATTENTION

Faites particulièrement attention au nom et à la casse du fichier MDF spécifié dans la lignede commande. Le nom et la casse de la classe générée par SQLMetal seront en effet identi-ques à ceux passés dans la ligne de commande. Si vous choisissez un autre nom que[chemin]\Northwind.mdf ([chemin]\northwind.mdf ou [chemin]\NorthWind.mdf,par exemple), aucun des exemples ne fonctionnera !

Linq.book Page 384 Mercredi, 18. février 2009 7:58 07

Page 400: LINQ Language Integrated Query en C

Chapitre 12 Introduction à LINQ to SQL 385

Pour créer des classes d’entité à partir du fichier Northwind.mdf, situé dans la racine dudisque C, entrez la commande suivante :

sqlmetal /namespace:nwind /code:Northwind.cs /pluralize /functions /sprocs /views "C:\Northwind.mdf"

L’exécution de cette commande fabriquera le module de classe d’entité Northwind.csdans le dossier courant.

Si vous voulez générer les classes d’entité de la base de données Northwind, déjà attachéeà SQL Server, utilisez la commande suivante :

sqlmetal /server:<server> /user:<user> /password:<password> /database:Northwind/namespace:nwind /code:Northwind.cs /pluralize /functions /sprocs /views

Pour créer les classes d’entité de la base de données Northwind attachée à SQLExpress,utilisez la commande suivante :

sqlmetal /server:.\SQLExpress /database:Northwind /namespace:nwind/code:Northwind.cs /pluralize /functions /sprocs /views

INFO

En fonction de votre environnement de travail, il se peut que vous deviez spécifier un nomd’utilisateur (option /user:[username]) et un mot de passe (optionpassword:[password]) dans la ligne de commande. Reportez-vous à la section intitulée"SQLMetal" du Chapitre 13 pour avoir plus de détails à ce sujet.

Après avoir tapé une de ces commandes, SQLMetal génère le code source dans ledossier courant, dans un fichier nommé Northwind.cs. Toutes les options de ceprogramme seront commentées au chapitre suivant. Insérez le fichier Northwind.csainsi généré dans votre projet en l’ajoutant en tant que "nouvel élément".

Vous pouvez maintenant vous servir de LINQ to SQL sur la base de données Northwinden utilisant les classes d’entité du fichier Northwind.cs.

ASTUCE

Vous pouvez faire des modifications dans le fichier d’entité, mais sachez qu’elles serontperdues si vous devez le générer une nouvelle fois. Vous pourriez par exemple vouloir ajou-ter une logique métier en définissant de nouvelles méthodes dans les classes d’entité. Mais,plutôt que modifier le fichier généré, pensez à tirer profit des classes partielles de C# 2.0 enplaçant les nouvelles propriétés et méthodes dans un module source annexe.

Génération du fichier de mappage XML de la base de données Northwind

Certains exemples ont également besoin d’un fichier de mappage. Ici encore, nousallons utiliser SQLMetal. Dans la même fenêtre Invite de commandes et à partir dumême dossier, exécutez la commande suivante :

sqlmetal /map:northwindmap.xml "C:\Northwind.mdf" /pluralize /functions /sprocs /views /namespace:nwind

Linq.book Page 385 Mercredi, 18. février 2009 7:58 07

Page 401: LINQ Language Integrated Query en C

386 LINQ to SQL Partie V

Comme précédemment, faites bien attention à la casse du fichier MDF. Cettecommande génère le fichier northwindmap.xml dans le dossier courant.

INFO

Cette commande affiche sur l’écran le code inséré dans le fichier de mappage XML. Toutesces lignes de code affichées sur votre écran sont donc tout à fait normales.

Utilisation de l’API LINQ to SQL

Pour pouvoir utiliser l’API LINQ to SQL, vous devez ajouter l’assemblySystem.Data.Linq.Dll dans votre projet, si elle ne s’y trouve pas déjà. De même, si lesdirectives using suivantes ne sont pas déjà présentes, vous devez les ajouter dans votremodule source :

using System.Data.Linq;using System.Linq;

Enfin, vous devez ajouter une clause using concernant l’espace de noms dans lequel lesclasses d’entité ont été générées :

using nwind;

IQueryable<T>

Dans la plupart des exemples des chapitres dédiés à LINQ to SQL, nous travailleronsavec des séquences de type IQueryable<T>, où T est le type d’une classe d’entité. Cesséquences sont généralement retournées par les requêtes LINQ to SQL. Elles fonction-nent souvent comme les séquences IEnumerable<T>, et…, cela n’est pas une coïnci-dence : l’interface IQueryable<T> implémente l’interface IEnumerable<T>. Voici ladéfinition de l’interface IQueryable<T> :

interface IQueryable<T> : IEnumerable<T>, IQueryable

Grâce à cet héritage, les séquences IQueryable<T> peuvent être traitées comme desséquences IEnumerable<T>.

Quelques méthodes communes

Un grand nombre d’exemples des chapitres dédiés à LINQ to SQL ont tendance à deve-nir rapidement complexes. Pour démontrer un conflit, il est nécessaire d’effectuer desmodifications dans la base de données en dehors de LINQ to SQL. Parfois, il est égale-ment nécessaire d’extraire des données sans utiliser LINQ to SQL. Pour mettre envaleur le code LINQ to SQL et ne pas être gêné par des détails annexes – sans pourautant s’écarter de la réalité –, nous avons défini quelques méthodes communes quiseront utilisées dans les exemples.

Linq.book Page 386 Mercredi, 18. février 2009 7:58 07

Page 402: LINQ Language Integrated Query en C

Chapitre 12 Introduction à LINQ to SQL 387

Assurez-vous que ces méthodes ont été ajoutées à vos modules sources lorsque voustesterez les exemples des chapitres LINQ to SQL.

La méthode GetStringFromDb()

Cette méthode se révélera bien pratique par la suite. Elle permet d’extraire une chaîned’une base de données en utilisant ADO.NET. Cela nous permettra d’examiner ce quise trouve dans la base de données et de le comparer à ce que LINQ to SQL affiche :

La méthode GetStringFromDb permet d’extraire une chaîne en utilisant ADO.NET

static private string GetStringFromDb( System.Data.SqlClient.SqlConnection sqlConnection, string sqlQuery) { if (sqlConnection.State != System.Data.ConnectionState.Open) { sqlConnection.Open(); }

System.Data.SqlClient.SqlCommand sqlCommand = new System.Data.SqlClient.SqlCommand(sqlQuery, sqlConnection);

System.Data.SqlClient.SqlDataReader sqlDataReader = sqlCommand.ExecuteReader(); string result = null;

try { if (!sqlDataReader.Read()) { throw (new Exception( String.Format("Exception inattendue pendant l’exécution de la requête [{0}].", sqlQuery))); } else { if (!sqlDataReader.IsDBNull(0)) { result = sqlDataReader.GetString(0); } } } finally { // Toujours appeler Close quand la lecture est faite sqlDataReader.Close(); }

return (result);}

La méthode GetStringFromDb demande deux arguments : un objet SqlConnection etune chaîne qui contient une requête SQL. La méthode vérifie que la connexion estouverte. Dans le cas contraire, elle l’ouvre.

Ensuite, un objet SqlCommand est créé en passant la requête et la connexion dans leconstructeur. Un objet SqlDataReader est alors obtenu en appelant la méthode Execute-Reader sur l’objet SqlCommand. Le SqlDataReader est lu en appelant la méthode Read.

Linq.book Page 387 Mercredi, 18. février 2009 7:58 07

Page 403: LINQ Language Integrated Query en C

388 LINQ to SQL Partie V

Si une donnée a été lue et si la première valeur de la colonne est différente de null,cette valeur est lue avec la méthode GetString.

Enfin, le SqlDataReader est fermé et la première valeur de la colonne est retournée àl’appelant.

La méthode ExecuteStatementInDb()

De temps à autre, il sera nécessaire d’exécuter des commandes insert, update etdelete en ADO.NET pour modifier l’état de la base de données sans utiliser LINQ toSQL. Pour ce faire, nous utiliserons la méthode ExecuteStatementInDb :

La méthode ExecuteStatementInDb exécute des commandes Insert, Update et Deleteen ADO.NET

static private void ExecuteStatementInDb(string cmd){ string connection = @"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind;Integrated Security=SSPI;";

System.Data.SqlClient.SqlConnection sqlConn = new System.Data.SqlClient.SqlConnection(connection);

System.Data.SqlClient.SqlCommand sqlComm = new System.Data.SqlClient.SqlCommand(cmd);

sqlComm.Connection = sqlConn; try { sqlConn.Open(); Console.WriteLine("Exécution de la commande SQL sur la base de données avec ➥ADO.NET ..."); sqlComm.ExecuteNonQuery(); Console.WriteLine("Base de données mise à jour"); } finally { // Fermeture de la connexion sqlComm.Connection.Close(); }}

La méthode ExecuteStatementInDb demande un argument : une chaîne contenant unecommande SQL. Un objet SqlConnection est créé, suivi par un objet SqlCommand. Lepremier est affecté au second. L’objet SqlConnection est alors ouvert et la commandeSQL, exécutée en appelant la méthode ExecuteNonQuery de l’objet SqlCommand. Enfin,l’objet SqlConnection est fermé.

Résumé

Ce chapitre constitue une introduction à LINQ to SQL et à un certain nombre de termesqui y sont relatifs. Par exemple, les objets DataContext, les classes d’entité, les associations,la détection et la résolution des conflits d’accès concurrentiel.

Linq.book Page 388 Mercredi, 18. février 2009 7:58 07

Page 404: LINQ Language Integrated Query en C

Chapitre 12 Introduction à LINQ to SQL 389

Vous y avez également appris à générer les classes d’entité et le fichier de mappagepour la version étendue de la base de données Northwind. Les classes d’entité serontabondamment utilisées dans les exemples LINQ to SQL.

Enfin, vous avez pu découvrir deux méthodes communes qui viendront en complémentdes instructions LINQ to SQL.

Au chapitre suivant, vous allez découvrir quelques astuces et voir comment utiliser desoutils dédiés à LINQ to SQL.

Linq.book Page 389 Mercredi, 18. février 2009 7:58 07

Page 405: LINQ Language Integrated Query en C

Linq.book Page 390 Mercredi, 18. février 2009 7:58 07

Page 406: LINQ Language Integrated Query en C

13

Astuces et outils pour LINQ to SQL

Le chapitre précédent a introduit LINQ to SQL et la terminologie qui lui est propre.Vous y avez appris à générer les classes d’entités nécessaires à la plupart des exemplesrelatifs à LINQ to SQL. Vous avez également découvert plusieurs méthodes communesqui seront utiles à de nombreux exemples des Chapitres 12 à 17.

Dans ce chapitre, vous allez découvrir des astuces qui, je l’espère, vous seront utileslorsque vous utiliserez LINQ to SQL. Vous ferez également connaissance avec quelques-uns des outils qui rendent LINQ to SQL si agréable à utiliser.

Introduction aux astuces et aux outils pour LINQ to SQL

Je tiens à rappeler ici que, pour pouvoir exécuter les exemples de ce chapitre, vousdevez au préalable satisfaire les conditions exposées dans la section "Prérequis pourexécuter les exemples" du chapitre précédent. En particulier, vous devez avoir télé-chargé la version étendue de la base de données Northwind et avoir généré les classesd’entité correspondantes.

Dans ce chapitre, étant donné que nous allons mettre en œuvre du code qui utilise lesclasses d’entité générées par SQLMetal et par le Concepteur Objet/Relationnel, nousn’indiquerons pas la directive using nwind dans le code des exemples. Cet espace denoms sera spécifié explicitement à chaque fois que cela se révélera nécessaire. Cettedémarche est nécessaire, car nous voulons contrôler quelle classe d’entité Customerest référencée dans chacun des exemples. Par défaut, le Concepteur Objet/Relationneldéfinit une classe qui porte le nom du projet. Étant donné que les exemples existentdéjà dans l’espace de noms du projet, il ne sera pas nécessaire de le spécifier ànouveau. En revanche, ceci n’est plus vrai lorsqu’un exemple utilise les classesd’entité générées par SQLMetal.

Linq.book Page 391 Mercredi, 18. février 2009 7:58 07

Page 407: LINQ Language Integrated Query en C

392 LINQ to SQL Partie V

INFO

Dans les exemples de ce chapitre, il ne sera pas nécessaire de déclarer une directive usingnwind;.

Astuces

Pour ne pas déroger à ce qui a été fait dans les chapitres précédents, nous allons vousprésenter quelques astuces qui mettent en œuvre des concepts qui n’ont pas encore étéabordés. Vous devez en effet connaître ces astuces avant d’en avoir besoin, et pas aprèsavoir décortiqué les théories qui les animent.

La propriété DataContext.Log

Nous allons rappeler quelques-unes des astuces relatives à LINQ to SQL présentéesau Chapitre 1. Une de ces astuces a été présentée dans la section "Utiliser le Log duDataContext". Elle vous a montré comment utiliser la propriété Log d’un objet Data-Context pour avoir un aperçu des requêtes traduites en SQL. Ceci peut être très utile,non seulement à des fins de débogage, mais également pour analyser les performan-ces. Vous pouvez par exemple découvrir que vos requêtes LINQ to SQL vont êtretraduites en des requêtes SQL peu efficaces. Ou encore qu’en raison du chargementdifféré des classes d’entité associées vous effectuez bien plus de requêtes SQL que lestrict nécessaire.

Le cas échéant, la propriété DataContext.Log vous révélera ce type d’information.

Pour pouvoir tirer parti de cette fonctionnalité, il vous suffit d’affecter la propriétéDataContext.Log à un objet System.IO.TextWriter : Console.Out, par exemple (voirListing 13.1).

Listing 13.1 : Un exemple d’utilisation de la propriété DataContext.Log.

nwind.Northwind db = new nwind.Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind");

db.Log = Console.Out;

var custs = from c in db.Customers where c.Region == "WA" select new { Id = c.CustomerID, Name = c.ContactName };

foreach (var cust in custs){ Console.WriteLine("{0} - {1}", cust.Id, cust.Name);}

Étant donné que nous utiliserons des classes d’entité fabriquées par SQLMetal et par leConcepteur Objet/Relationnel, nous aurons affaire à deux classes Customer différentes.Comme il a été dit précédemment, aucune directive using ne sera ajoutée dans lesexemples de ce chapitre, afin d’ôter toute ambiguïté en ce qui concerne les classes

Linq.book Page 392 Mercredi, 18. février 2009 7:58 07

Page 408: LINQ Language Integrated Query en C

Chapitre 13 Astuces et outils pour LINQ to SQL 393

d’entité utilisées. Dans le cas du Listing 13.1, nous devons cependant spécifier l’espacede noms nwind de la classe Northwind, car nous utiliserons le code de la classe d’entitégénérée par SQLMetal.

Comme vous avez pu le voir, le Listing 13.1 se contente d’affecter l’objet Console.Outà la propriété Log de l’objet NorthwindDataContext. Voici les résultats de ce code :

Ces résultats contiennent le code SQL généré par la requête LINQ to SQL. Remarquezque ce code utilise des paramètres. En utilisant LINQ to SQL, vous êtes donc automati-quement protégé des attaques de type "injection de commandes SQL".

ATTENTIONATTENTION

Si vos résultats laissent apparaître que le nom associé au client LAZYK est "Ned Plimpton" etnon "John Steel", vous avez certainement exécuté le code du Listing 12.1 sans restaurer ladonnée qui a été affectée. Vous devriez régler ce problème avant d’exécuter les exemplessuivants. Reportez-vous si nécessaire au Chapitre 12 pour savoir comment procéder.

Dans les chapitres suivants, vous verrez comment utiliser le Log du DataContext pourdétecter et résoudre des problèmes de performances potentiels.

La méthode GetChangeSet()

La méthode GetChangeSet de l’objet DataContext permet de connaître tous les objetsentité qui ont été modifiés et qui doivent être mémorisés dans la base de données lors-que la méthode SubmitChanges est appelée. Cette méthode est utile en ce qui concernele Log du DataContext et le débogage. Vous en saurez plus à son sujet en vous reportantau Chapitre 16.

Utilisation de classes partielles ou de fichiers de mappage

Lors de l’utilisation d’un outil ORM, une des principales difficultés consiste en lagestion des modifications dans la base de données. Si vous conservez la logique de vosclasses métier et de LINQ to SQL dans les mêmes modules, vous aurez beaucoup demal à les maintenir lorsque la base de données est modifiée. Pensez à placer votre logi-que métier dans un module différent de celui des classes d’entité. En utilisant des clas-ses partielles pour séparer vos attributs de base de données LINQ to SQL de votre

SELECT [t0].[CustomerID], [t0].[ContactName]FROM [dbo].[Customers] AS [t0]WHERE [t0].[Region] = @p0-- @p0: Input String (Size = 2; Prec = 0; Scale = 0) [WA]-- Context: SqlProvider(Sql2005) Model: AttributedMetaModel Build: 3.5.20706.1

LAZYK - John SteelTRAIH - Helvetius NagyWHITC - Karl Jablonski

Linq.book Page 393 Mercredi, 18. février 2009 7:58 07

Page 409: LINQ Language Integrated Query en C

394 LINQ to SQL Partie V

logique métier, vous minimiserez la nécessité d’ajouter du code dans les classesd’entité.

Une autre solution consisterait à utiliser des fichiers de mappage XML externes pourdécoupler les classes métier et le mappage LINQ to SQL. Ce fichier XML relierait lesobjets métier à la base de données sans compter sur les attributs LINQ to SQL. Vous ensaurez plus au sujet des fichiers de mappage dans la section intitulée "Schéma de fichierde mappage externe XML" du Chapitre 15 et dans la section "La classe DataContext"du Chapitre 16.

Utilisation de méthodes partielles

Si les méthodes partielles sont apparues assez tardivement dans le langage C#, vous nedevez pas pour autant les ignorer. Vous les utiliserez pour traiter certains événementsqui ont lieu dans les classes d’entité. Si vous n’implémentez aucune méthode partielle(et c’est là toute leur "beauté"), le compilateur n’émet aucun code pour les activer.

Reportez-vous à la section "Appel des méthodes partielles appropriées" du Chapitre 15pour en savoir plus sur l’utilisation des méthodes partielles dans les classes d’entité.

Outils

Cette section va vous présenter plusieurs outils qui vous faciliteront la vie et accélére-ront votre adoption de LINQ to SQL. Bien qu’un peu prématurée, cette étape mesemble nécessaire, tout au moins pour que vous sachiez que ces outils existent, même sivous ne les utilisez pas encore.

SQLMetal

Si vous n’avez pas encore de classes métier, la façon la plus simple de créer les classesd’entité d’une base de données consiste à utiliser l’outil SQLMetal. Vous le trouverezdans le dossier %windir%\Microsoft.NET\Framework\v3.5. Il suffit d’indiquer le nomd’une base de données à SQLMetal pour qu’il génère toutes les classes d’entité néces-saires à LINQ to SQL. SQLMetal fonctionne en ligne de commande et ne disposed’aucune interface utilisateur.

Pour avoir une idée des options utilisables, commencez par ouvrir une fenêtre Invite decommandes Visual Studio. Pour ce faire, cliquez successivement sur Démarrer, Tous lesprogrammes, Microsoft Visual Studio 2008, Visual Studio Tools puis Invite decommandes de Visual Studio 2008.

Dans la fenêtre Invite de commandes, tapez sqlmetal et appuyez sur la touche Entréedu clavier :

sqlmetal

Linq.book Page 394 Mercredi, 18. février 2009 7:58 07

Page 410: LINQ Language Integrated Query en C

Chapitre 13 Astuces et outils pour LINQ to SQL 395

Cette commande provoque l’affichage suivant dans la fenêtre Invite de commandes :

Microsoft (R) Database Mappage Generator 2008 version 1.00.21022pour Microsoft (R) .NET Framework version 3.5Copyright (C) Microsoft Corporation.Tous droits réservés.

SqlMetal [options] [<fichier_entrée>]

Génère un code et un mappage pour le composant LINQ to SQL du .NET Framework.SqlMetal peut effectuer les opérations suivantes : – Générer des attributs de code source et de mappage ou un fichier de mappage à partir

d’une base de données. – Générer un fichier dbml intermédiaire pour le personnaliser à partir de la base de données. – Générer des attributs de code et de mappage ou un fichier de mappage à partir d’un

fichier dbml.

Options : /server:<name> Nom du serveur de base de données. /database:<name> Catalogue de bases de données sur le serveur. /user:<name> ID utilisateur de connexion (valeur par défaut :

utilisation de l’authentification Windows). /password:<password> Mot de passe de connexion (par défaut : utilisation de

l’authentification Windows). /conn:<connection string> Chaîne de connexion de base de données. Ne peut pas

être utilisée avec les options /server, /database, /user et /password.

/timeout:<seconds> Valeur de délai d’attente à utiliser lorsque SqlMetal accède à la base de données (valeur par défaut : 0, soit à l’infini).

/views Extraire des vues de base de données. /functions Extraire des fonctions de base de données. /sprocs Extraire des procédures stockées.

/dbml[:file] Sortie en dbml. Ne peut être utilisé avec l’option /map.

/code[:file] Sortie en tant que code source. Ne peut être utilisée avec l’option /dbml.

/map[:file] Générer un fichier de mappage mais pas des attributs. Ne peut être utilisé avec l’option /dbml.

/language:<language> Langage du code source : VB ou C# (provient par défaut de l’extension du nom de fichier du code).

/namespace:<name> Espace de noms du code généré (valeur par défaut : aucun espace de noms).

/context:<type> Nom de la classe du contexte de données (provient par défaut du nom de la base de données).

/entitybase:<type> Classe de base des classes d’entité dans le code généré (valeur par défaut : les entités n’ont aucune classe de base).

/pluralize Mettez automatiquement au pluriel ou au singulier des noms de membres et de classes d’après des règles de langue anglaise.

/serialization:<option> Générer des classes sérialisables : None ou Unidirectional (valeur par défaut : None).

/provider:<type> Type de fournisseur : SQLCompact, SQL2000 ou SQL2005. (valeur par défaut : le fournisseur est déterminé au moment de l’exécution).

<fichier_entrée> Peut être un fichier mdf SqlExpress, un fichier sdf SqlCE ou un fichier dbml intermédiaire.

Créer du code à partir de SqlServer : SqlMetal /server:myserver /database:northwind /code:nwind.cs /namespace:nwind

Linq.book Page 395 Mercredi, 18. février 2009 7:58 07

Page 411: LINQ Language Integrated Query en C

396 LINQ to SQL Partie V

Comme vous pouvez le voir, plusieurs exemples sont donnés dans la fenêtre Invite decommandes. La plupart des options se comprennent d’elles-mêmes. Le Tableau 13.1donne quelques explications complémentaires pour les options les plus complexes.

Générer un fichier dbml intermédiaire à partir de SqlServer : SqlMetal /server:myserver /database:northwind /dbml:northwind.dbml /namespace:nwind

Générer du code avec du mappage externe à partir d’un fichier dbml : SqlMetal /code:nwind.cs /map:nwind.map northwind.dbml

Générer un fichier dbml à partir d’un fichier sdf SqlCE : SqlMetal /dbml:northwind.dbml northwind.sdf

Générer un fichier dbml à partir d’un serveur local SqlExpress : SqlMetal /server:.\sqlexpress /database:northwind /dbml:northwind.dbml

Générer un fichier dbml à l’aide d’une chaîne de connexion dans la ligne de commande : SqlMetal /conn:"server=’myserver’; database=’northwind’" /dbml:northwind.dbml

Tableau 13.1 : Les options de l’outil SQLMetal

Option/Exemple Description

/server:<name>

/server:.\SQLExpress

Nom du serveur sur lequel se trouve la base de données à utiliser. Si cette option n’est pas présente, SQLMetal utilise la valeur localhost/sqlexpress par défaut.

Pour que SQLMetal génère les classes d’entité à partir d’un fichier MDF, omettez cette option ainsi que /database, et spécifiez le nom complet du fichier MDF à la fin de la commande.

/database:<name>

/database:Northwind

Nom de la base de données pour lequel vous voulez générer les classes d’entité.

Pour que SQLMetal génère les classes d’entité à partir d’un fichier MDF, omettez cette option ainsi que /server, et spécifiez le nom complet du fichier MDF à la fin de la commande.

/user:<name>

/user:sa

Nom d’utilisateur permettant de se connecter à la base de données.

/password:<password>

/password:1590597893

Mot de passe permettant de se connecter à la base de données.

/conn:<chaine de connexion>

/conn:"Data

Source=.\SQLEXPRESS;Initial

Catalog=Northwind;Integrated

Security=SSPI;"

Chaîne de connexion à la base de données. Vous pouvez utiliser cette option pour regrouper le nom du serveur, le nom de la base de données, le nom d’utilisateur et le mot de passe.

Linq.book Page 396 Mercredi, 18. février 2009 7:58 07

Page 412: LINQ Language Integrated Query en C

Chapitre 13 Astuces et outils pour LINQ to SQL 397

/timeout:<seconds>

/timeout:120

Délai d’attente en secondes pour accéder à la base de données. Si cette option n’est pas spécifiée, aucun délai d’attente n’est utilisé.

Alors que nous écrivons ces lignes, cette option n’est pas encore supportée. Quoi qu’il en soit, le code généré ne dépend aucunement de cette option. Vous pouvez vérifier si elle fonctionne dans votre Visual Studio 2008. Dans la négative, vous pouvez toujours utiliser la propriété CommandTimeout de la classe DataContext, ou encore appeler la méthode DataContext.GetCommand et définir un délai d’attente pour une requête particulière. Consultez le Listing 16.29, au Chapitre 16, pour en savoir plus à ce sujet.

/views

/views

Spécifiez cette option si vous voulez que SQLMetal génère le code nécessaire afin que les propriétés Table<T> et les classes d’entité supportent les vues de la base de données.

/functions

/functions

Spécifiez cette option pour que SQLMetal génère des méthodes qui permettront d’appeler les fonctions de base de données définies par l’utilisateur.

/sprocs

/sprocs

Spécifiez cette option pour que SQLMetal génère des méthodes qui permettront d’appeler les procédures stockées.

/dbml[:file]

/dbml:Northwind.dbml

Cette option spécifie le nom d’un fichier intermédiaire DBML. Par son intermédiaire, vous pourrez contrôler les noms des classes et des propriétés des classes d’entité générées.

Si vous utilisez cette option, éditez le fichier DBML généré, faites les modifications nécessaires et demandez la définition du module de code source en appelant SQLMetal sur le fichier DBML modifié et en spécifiant l’option /code.

Vous pouvez également ouvrir le fichier DBML généré dans le Concepteur Objet/Relationnel, faire les modifications nécessaires par son intermédiaire et demander au Concepteur de générer le code source correspondant.

Cette option ne doit pas être utilisée conjointement avec /map.

Tableau 13.1 : Les options de l’outil SQLMetal (suite)

Option/Exemple Description

Linq.book Page 397 Mercredi, 18. février 2009 7:58 07

Page 413: LINQ Language Integrated Query en C

398 LINQ to SQL Partie V

/code[:file]

/code:Northwind.cs

Nom du fichier créé par SQLMetal et contenant le DataContext dérivé et les classes d’entité dans le langage de programmation spécifié.

Cette option ne peut être utilisée conjointement à /dbml.

Si vous spécifiez les options /code et /map dans une même invocation à SQLMetal, le code généré ne contiendra pas les attributs LINQ to SQL. Bien entendu, vous utiliserez le fichier de mappage et le fichier de code générés pour être en mesure d’utiliser LINQ to SQL.

/map[:file]

/map:northwindmap.xml

Demande la génération d’un fichier de mappage XML (à opposer à l’option /code, qui demande la création d’un fichier de code).

Ce fichier de mappage XML externe peut être chargé lors de l’instanciation du DataContext. Cela permet d’utiliser LINQ to SQL sans qu’aucun code source LINQ to SQL ne doive être compilé avec votre code.

Si vous spécifiez les options /code et /map dans une même invocation à SQLMetal, le code généré ne contiendra pas les attributs LINQ to SQL. Bien entendu, vous utiliserez le fichier de mappage et le fichier de code générés pour être en mesure d’utiliser LINQ to SQL.

/language:<langage>

/language:C#

Cette option définit le langage dans lequel SQLMetal doit écrire le code source. Les valeurs possibles sont csharp, C# et VB.

Si vous omettez cette option, SQLMetal déduira le langage de l’extension du fichier de code source.

/namespace:<nom>

/namespace:nwind

Indique l’espace de noms duquel la classe dérivée de DataContext et les classes d’entité dépendront.

/context:<type>

/context:Northwind

Nom de la classe générée, dérivée de la classe DataContext. Si cette option est omise, le nom de la classe sera le même que celui de la base de données.

/entitybase:<type>

/entitybase:MyEntityClassBase

Nom de la classe de base pour toutes les classes d’entité générées. Si cette option est omise, les classes d’entité n’auront aucune classe de base.

Tableau 13.1 : Les options de l’outil SQLMetal (suite)

Option/Exemple Description

Linq.book Page 398 Mercredi, 18. février 2009 7:58 07

Page 414: LINQ Language Integrated Query en C

Chapitre 13 Astuces et outils pour LINQ to SQL 399

À titre d’information, sachez que les options /dbml, /code et /map peuvent être spéci-fiées sans aucun nom de fichier. Dans ce cas, le code ou XML généré sera affiché dansla console.

Fichier de mappage XML ou fichier intermédiaire DBML ?SQLMetal vous permet de spécifier deux différents types de fichiers XML…, ce quipeut se révéler assez déroutant. Le premier correspond à l’option /map et le second, àl’option /dbml.

L’option /map crée un fichier de mappage externe XML destiné à être chargé à l’instan-ciation du DataContext. Cette option est une alternative à la génération ou à l’écrituremanuelle d’un module source qui contient les attributs LINQ to SQL à compiler. Aveccette approche, le code source ne comprend et ne fait référence à aucun code LINQ toSQL spécifique à la base de données. Cela autorise une consommation "quelque peudynamique" de la base de données, puisque vous n’avez besoin d’aucun code prégénéré

/pluralize

/pluralize

Lorsque cette option est spécifiée, SQLMetal met le nom des tables au pluriel et le nom des classes d’entité mappées au singulier. Par exemple, pour une table Customers, la classe d’entité générée aura pour nom Customer, et un Table<Customer> nommé Customers sera généré. Ainsi, la table Customers contiendra des objets Customer, ce qui, grammaticalement parlant, est tout à fait correct.

Si cette option n’est pas spécifiée, la classe d’entité et le Table<Customers> porteront le nom Customers. La table Customers contiendra des objets Customers, ce qui, grammaticalement parlant, est incorrect.

/serialization:<option>

/serialization:none

Indique si les attributs de sérialisation doivent être générés dans les classes. Les valeurs possibles sont None et Unidirectional. Si cette option n’est pas spécifiée, les attributs de sérialisation ne seront pas inclus.

/provider:<type>

/provider:SQL2005

Indique la classe du fournisseur de la base de données. Les valeurs possibles sont SQLCompact, SQL2000 et SQL2005. SQLMetal génère un attribut Provider qui indique la valeur spécifiée dans cette option.

Les classes des fournisseurs se trouvent dans l’espace de noms System.Data.Linq.SqlClient. N’oubliez pas de spécifier cet espace de noms si vous utilisez cette option.

Tableau 13.1 : Les options de l’outil SQLMetal (suite)

Option/Exemple Description

Linq.book Page 399 Mercredi, 18. février 2009 7:58 07

Page 415: LINQ Language Integrated Query en C

400 LINQ to SQL Partie V

et compilé. J’ai bien dit "quelque peu dynamique". En effet, le code doit connaître lenom des tables et des champs, sans quoi il ne serait pas en mesure d’effectuer desrequêtes. Le fichier de mappage externe indique à LINQ to SQL le nom des tables,colonnes et procédures stockées aves lesquelles il peut interagir, ainsi que le nom desclasses, des propriétés et des méthodes auxquelles elles sont mappées.

L’option /dbml crée un fichier intermédiaire DBML (XML). Vous pouvez éditer cefichier afin de choisir le nom des classes et propriétés pour les classes d’entité à générer.Vous devez alors exécuter une nouvelle fois SQLMetal en lui indiquant non pas le nomde la base de données, mais le nom du fichier DBML, et en utilisant l’option /code.Vous pouvez également ouvrir le fichier DBML généré dans le Concepteur Objet/Rela-tionnel, faire les modifications nécessaires par son intermédiaire et demander auConcepteur de générer le code source correspondant.

Pour ajouter à la confusion, les schémas des deux types de fichiers XML générés parSQLMetal sont assez proches. Si nécessaire, reportez-vous au Chapitre 15 pour avoirdes informations complémentaires sur les fichiers de mappage XML.

Travailler avec des fichiers intermédiaires DBMLComme il a été dit dans la section précédente, le fichier intermédiaire DBML permet decontrôler le nom des classes et des propriétés, en intervenant manuellement entrel’extraction du schéma de la base de données et la génération de la classe d’entité. Sivous n’avez que faire de cette possibilité, les fichiers DBML ne sont pas pour vous.Dans la suite, nous allons supposer que vous avez besoin de choisir le nom des classeset des propriétés des classes d’entité.

Supposons que vous ayez attaché la base de données étendue Northwind à votre serveurSQL. Vous définirez le fichier intermédiaire DBML avec la commande suivante :

sqlmetal /server:.\SQLExpress /database:Northwind /pluralize /sprocs /functions /views /dbml:Northwind.dbml

INFO

L’utilisation des options /server et /database dans la commande sqlmetal nécessite quela base de données soit attachée au serveur SQL.

Il se peut également que vous ayez à spécifier les options /user et /password pour queSQLMetal soit en mesure de se connecter à la base de données.

Si vous préférez, le fichier intermédiaire DBML peut être généré à partir d’un fichierMDF :

sqlmetal /pluralize /sprocs /functions /views /dbml:Northwind.dbml "C:\Northwind.mdf"

Linq.book Page 400 Mercredi, 18. février 2009 7:58 07

Page 416: LINQ Language Integrated Query en C

Chapitre 13 Astuces et outils pour LINQ to SQL 401

INFO

La génération du fichier intermédiaire DBMF à partir d’un fichier MDF peut engendrerl’attachement du fichier MDF au serveur SQL sous le nom C:\NORTHWIND.MDF, ou quelquechose de similaire. Le cas échéant, vous devriez donner le nom Northwind à la base dedonnées dans SQL Server Enterprise Manager ou SQL Server Management Studio pour queles exemples de ce chapitre soient en mesure de fonctionner.

Ces deux approches devraient produire le même fichier DBML. Dans les deux lignes decommande précédentes, seules les options nécessaires à la lecture de la base de donnéeset à la création du fichier DBML ont été spécifiées. Des options telles que /language et/code n’ont d’intérêt que pour la génération d’un module de code source.

Une fois le fichier XML intermédiaire modifié, vous obtiendrez le module de codesource en exécutant cette commande :

sqlmetal /namespace:nwind /code:Northwind.cs Northwind.dbml

Les options spécifiées dans cette commande sont appropriées à la génération de codesource.

Schéma de fichier intermédiaire DBMLSi vous choisissez de créer un fichier intermédiaire DBML, de l’éditer et de générer desclasses d’entité par cet intermédiaire, vous devez savoir ce qu’est un schéma et ce quesignifient les noms d’éléments et d’attributs.

Le fonctionnement des schémas étant sujet à modifications, consultez la documentationMicrosoft pour prendre connaissance des dernières informations à leur sujet. Une foisle concept de schéma compris, vous pourrez choisir d’éditer manuellement le fichierDBML pour contrôler les noms des classes d’entité et des propriétés, puis générer lecode source de la classe d’entité en indiquant à SQLMetal le nom du fichier DBMLmodifié.

Encore mieux, vous pourrez ouvrir le fichier DBML généré dans le Concepteur Objet/Relationnel et y faire les modifications nécessaires. En utilisant son interface graphi-que, et même si vous ne connaissez ni ne comprenez ce qu’est un schéma, vous pourrezmodifier le mappage relationnel.

Le Concepteur Objet/Relationnel

Outre SQLMetal, il existe également un outil graphique permettant de générer desclasses d’entité. Cet outil fait partie intégrante de Visual Studio. Il est connu sous lesnoms "Concepteur Objet/Relationnel", "Concepteur LINQ to SQL", "Concepteur O/R"(Object-to-Relational) ou encore "Concepteur DLinq". L’outil en ligne de commandeSQLMetal a été conçu pour fabriquer des classes d’entité pour toutes les tables dela base de données. Vous avez cependant la possibilité de limiter son action à certai-nes tables de la base en créant un fichier DBML, en le modifiant et en générant des

Linq.book Page 401 Mercredi, 18. février 2009 7:58 07

Page 417: LINQ Language Integrated Query en C

402 LINQ to SQL Partie V

classes d’entité par son intermédiaire. Le Concepteur Objet/Relationnel permetune approche plus sélective et… entièrement graphique. Dans la suite de ce chapi-tre, nous utiliserons le terme "Concepteur" pour désigner le Concepteur Objet/Rela-tionnel.

Le Concepteur permet de modifier la classe d’entité par de simples glisser-déposer.N’ayez crainte, le Concepteur fait toute la partie ingrate du travail. Votre part neconsiste qu’à sélectionner les tables à modeler et, si vous le souhaitez, à modifier lesnoms et les propriétés des classes d’entité. Il est bien entendu toujours possible de créerle modèle à la main dans le Concepteur pour avoir un contrôle total…

Création du fichier des classes LINQ to SQLLa première chose à faire concernant le Concepteur consiste à créer les classes LINQ toSQL en cliquant du bouton droit sur le projet et en sélectionnant Ajouter/Nouvelélément dans le menu contextuel. Cette action provoque l’affichage de la boîte de dialo-gue Ajouter un nouvel élément. Sélectionnez le modèle Classes LINQ to SQL dans laliste. Choisissez un nom pour la nouvelle classe : le nom de la base de données est unbon choix, et l’extension est .dbml. Dans cet exemple, nous utiliserons le nomNorthwind.dbml.

ATTENTIONATTENTION

Si vous avez déjà défini un fichier Northwind.dbml dans un autre projet créé à partir desexemples de cet ouvrage, faites attention à ne pas écraser le code existant.

Cliquez sur Ajouter. Après quelques instants, un espace blanc occupe le centre de lafenêtre. Il s’agit de l’espace de travail (aussi appelé canevas) du Concepteur. LaFigure 13.1 donne un aperçu du canevas.

Figure 13.1 :

Le canevas du Concepteur Objet/Relation-nel.

Linq.book Page 402 Mercredi, 18. février 2009 7:58 07

Page 418: LINQ Language Integrated Query en C

Chapitre 13 Astuces et outils pour LINQ to SQL 403

Cliquez du bouton droit sur le canevas et sélectionnez Propriétés dans le menu. Lespropriétés apparaissent dans la partie droite de la fenêtre. La propriété Name représentele nom de la classe DataContext générée. Le fichier de classes LINQ to SQL ayant pournom "Northwind.dbml", le nom par défaut de la classe DataContext sera NorthwindData-Context. Ce nom peut être modifié mais, ici, nous allons le laisser intact.

Examinez l’Explorateur de solutions. Vous verrez que le fichier Northwind.designer.csa été inséré dans le dossier Northwind.dbml. Si vous ouvrez ce fichier, vous verrez qu’ilne contient que très peu de code à ce niveau. En fait, il contient les constructeurs de lanouvelle classe DataContext NorthwindDataContext dont il est dérivé.

Connexion du DataContext à la base de donnéesLa prochaine étape va consister à ajouter (si elle n’existe pas déjà) une connexion auserveur de base de données contenant la base de données Northwind dans la fenêtreExplorateur de serveurs.

ASTUCE

Si la fenêtre Explorateur de serveurs n’est pas accessible, lancez la commande Explorateur deserveurs dans le menu Affichage de Visual Studio.

Pour ajouter une connexion à la base de données, cliquez du bouton droit sur l’icôneConnexion de données dans la fenêtre Explorateur de serveurs et choisissez Ajouter uneconnexion dans le menu contextuel. Cette action ouvre la boîte de dialogue Ajouter uneconnexion (voir Figure 13.2). La zone de texte Source de données devrait laisser appa-raître Microsoft SQL Server (SqlClient). Si ce n’est pas le cas, cliquez sur Modifier etchoisissez l’entrée correspondante.

Figure 13.2 :

La boîte de dialogue Ajouter une connexion.

Linq.book Page 403 Mercredi, 18. février 2009 7:58 07

Page 419: LINQ Language Integrated Query en C

404 LINQ to SQL Partie V

Configurez les paramètres nécessaires à l’accès de la base de données Northwind dansla boîte de dialogue Ajouter une connexion. Pour vous assurer que la connexion est bienconfigurée, cliquez sur Tester la connexion.

Une fois que la connexion est configurée, cliquez sur OK. Vous devriez avoir une entréequi représente la connexion avec la base de données Northwind sous le libelléConnexions de données, dans l’Explorateur de serveurs. Vous êtes maintenant enmesure d’accéder à la base de données Northwind dans le Concepteur.

Avant de commencer, assurez-vous que vous êtes en train de visualiser le fichierNorthwind.dbml dans l’éditeur de Visual Studio.

Ajout d’une classe d’entitéDans la fenêtre Explorateur de serveurs, sous Connexions de données, cliquez sur lesigne "+" qui précède l’entrée Northwind.mdf, puis sur le signe "+" qui précède l’entrée"Tables" pour afficher la liste des tables de la base de données Northwind. Les classesd’entité sont créées en déposant des tables de la fenêtre Explorateur de serveurs sur lecanevas du Concepteur.

Déposez la table Customers sur le canevas du Concepteur. Cette simple opérationdemande au Concepteur de créer la classe d’entité Customer pour la table Customers.Le canevas devrait maintenant ressembler à la Figure 13.3.

Il se peut que vous ayez à opérer plusieurs redimensionnements pour que tout ce qui estaffiché dans Visual Studio apparaisse clairement. En déposant la table Customers sur lecanevas du Concepteur, le code source de l’entité Customer est ajouté au fichier source

Figure 13.3 :

Le Concepteur, après avoir déposé la table Customers sur le canevas.

Linq.book Page 404 Mercredi, 18. février 2009 7:58 07

Page 420: LINQ Language Integrated Query en C

Chapitre 13 Astuces et outils pour LINQ to SQL 405

Northwind.designer.cs. Une fois que vous aurez construit votre projet, vous pourrezcommencer à utiliser la classe d’entité Customer pour accéder aux données et les mettreà jour dans la base de données Northwind. C’est aussi simple que cela !

Avant de construire le projet et d’écrire le code qui utilise les classes d’entité générées,nous allons créer d’autres petites choses nécessaires pour tirer parti de LINQ to SQL.Déposez la table Orders sur le canevas (après l’avoir déposée, il se peut que vous ayezà la déplacer pour lui donner une meilleure position). Ce faisant, vous avez demandé auConcepteur de créer la classe d’entité Order pour la table Orders. Votre canevas devraitmaintenant avoir l’allure de la Figure 13.4.

Vous avez peut-être remarqué que le volet qui apparaissait à droite du canevas à laFigure 13.3 a maintenant disparu. Il s’agit du volet Méthodes. Pour le fermer, il suffit dele pointer, de cliquer à droite et de sélectionner Masquer le volet Méthodes dans lemenu contextuel. Pour l’ouvrir à nouveau, cliquez du bouton droit sur le canevas etsélectionnez Afficher le volet Méthodes. Nous avons fermé ce volet pour laisser un plusgrand espace dans le canevas.

En observant le canevas, vous pouvez voir une ligne pointillée qui relie la classe Custo-mer à la classe Order. Cette ligne représente la relation (connue sous le nom "associa-tion" dans LINQ to SQL) entre les tables Customers et Orders, telle qu’elle a étédéfinie par la clé étrangère FK_Orders_Customers définie dans la base de donnéesNorthwind. Cette ligne pointillée indique également que le Concepteur définira uneassociation entre les classes d’entité pour supporter la relation qui lie les deux tables.

Figure 13.4 :

Le Concepteur, après avoir déposé la table Orders sur le canevas.

Linq.book Page 405 Mercredi, 18. février 2009 7:58 07

Page 421: LINQ Language Integrated Query en C

406 LINQ to SQL Partie V

C’est par cette association que vous pourrez obtenir une référence vers une collectionde commandes des clients en référençant une propriété d’un objet Customer. De même,vous pourrez obtenir une référence vers une commande d’un client en référençant unepropriété dans l’objet Order.

Si vous ne voulez pas conserver l’association, il vous suffit de cliquer sur la ligne poin-tillée qui relie les deux tables et d’appuyer sur la touche Suppr du clavier. Vous pouvezégalement cliquer du bouton droit sur la ligne pointillée et sélectionner Supprimer dansle menu contextuel.

Utilisation des classes d’entité générées par le ConcepteurVous êtes maintenant prêt pour utiliser les classes d’entité générées par le Concepteur.À titre d’exemple, le Listing 13.2 effectue une requête sur la base de donnéesNorthwind pour connaître les clients qui habitent à Londres.

Listing 13.2 : Un exemple d’utilisation des classes d’entité générées par le Concepteur.

NorthwindDataContext db = new NorthwindDataContext();

IQueryable<Customer> custs = from c in db.Customers where c.City == "London" select c;

foreach(Customer c in custs){ Console.WriteLine("{0} a passé {1} commandes.", c.CompanyName, c.Orders.Count);}

Ce code est assez proche de celui des autres exemples. Mais remarquez qu’aucuneinformation de connexion n’a été spécifiée lors de l’instanciation de l’objet Northwind-DataContext. Ceci vient du fait que le Concepteur a généré la classe NorthwindData-Context avec un constructeur qui n’a pas besoin de paramètres, car les informations deconnexion proviennent du fichier de configuration du projet : app.config. Ce fichiercontient le code suivant :

Le constructeur DataContext généré par le Concepteur

public NorthwindDataContext() : base(global::LINQChapter13.Properties.Settings.Default.NorthwindConnectionString, mappingSource){ OnCreated();}

ATTENTIONATTENTION

Si vous avez téléchargé le code source d’accompagnement de cet ouvrage, assurez-vous quevous avez mis à jour la chaîne de connexion connectionString dans le fichier app.config.En particulier, le code source doit contenir le nom de votre ordinateur, qui n’a que peu dechances d’être identique au mien.

Linq.book Page 406 Mercredi, 18. février 2009 7:58 07

Page 422: LINQ Language Integrated Query en C

Chapitre 13 Astuces et outils pour LINQ to SQL 407

Dans le code précédent, remarquez qu’il nous a été possible d’accéder aux commandesdes clients en faisant référence à la propriété Orders d’un objet Customer. Ceci vient dufait que le Concepteur a automatiquement défini une association entre ces deux tables.Voici les résultats de cette requête :

Édition du modèle de classe d’entitéIl se peut que vous désiriez modifier les noms des classes d’entité, les propriétés desclasses d’entité (dans la fenêtre des propriétés), le nom des propriétés (des membres)des classes d’entité, les propriétés d’une propriété d’une classe d’entité (d’un membred’une classe d’entité). Merci Microsoft ! Vous auriez certainement pu mieux faire auniveau du choix des termes. Pourquoi les membres des classes ont-ils été appelés"propriétés", alors que Visual Studio utilise également ce terme pour parler des différentsréglages accessibles dans la fenêtre des propriétés ?

Si le Concepteur est si intéressant, c’est certainement à cause de sa flexibilité et de lafacilité avec laquelle il est possible de contrôler le nom des classes d’entité et de leurspropriétés : de simples glisser-déposer, pointer et cliquer suffisent !

Modification du nom d’une classe d’entitéPour modifier le nom d’une classe d’entité, il suffit de double-cliquer sur ce nom dansle canevas. Vous pouvez également cliquer sur la classe d’entité dans le canevas etmodifier la propriété Name dans la fenêtre des propriétés.

Modification des propriétés d’une classe d’entitéPour modifier les propriétés d’une classe d’entité, il suffit de cliquer sur cette classedans le canevas et de faire la modification souhaitée dans la fenêtre des propriétés.Vous pouvez ainsi modifier le nom de la table dans laquelle les entités sont stockées,le nom des méthodes surchargées Delete, Insert et Update, ainsi que d’autrespropriétés.

Modification du nom d’une propriété d’une classe d’entitéPour modifier le nom d’une propriété d’une classe d’entité, il suffit de triple-cliquer dessus dans le canevas. Vous pouvez également sélectionner la propriétéen cliquant dessus dans le canevas et modifier la propriété Name dans la fenêtre despropriétés.

Around the Horn a passé 13 commandes.B’s Beverages a passé 10 commandes.Consolidated Holdings a passé 3 commandes.Eastern Connection a passé 8 commandes.North/South a passé 3 commandes.Seven Seas Imports a passé 9 commandes.

Linq.book Page 407 Mercredi, 18. février 2009 7:58 07

Page 423: LINQ Language Integrated Query en C

408 LINQ to SQL Partie V

Modification des propriétés d’une propriété d’une classe d’entitéPour modifier les propriétés d’une propriété d’une classe d’entité, sélectionnez lapropriété dans le canevas et modifiez les propriétés souhaitées dans la fenêtre despropriétés : Name et UpdateCheck, par exemple. Nous discuterons en détail des attributsdes classes d’entité au Chapitre 15.

Ajout d’objets dans le modèle de classe d’entitéGlisser-déposer une classe dans le canevas est une technique simple… à conditionque la table correspondante se trouve dans la fenêtre Explorateur de serveurs. Ilexiste plusieurs cas dans lesquels ce luxe ne vous sera pas permis. Par exemple, sivous définissez la classe d’entité en premier et prévoyez de générer la base dedonnées en appelant la méthode CreateDatabase sur le DataContext. Ou encore sivous voulez vous servir de l’héritage d’une classe d’entité et qu’il n’existe aucunetable pour la mapper.

Ajout de nouvelles classes d’entitéPour ajouter de nouvelles classes d’entité dans le modèle de classes d’entité, une solu-tion consiste à les déplacer depuis les tables de la base de données (fenêtre Explorateurde serveurs) sur le canevas du Concepteur, comme cela a été fait dans la section précé-dente. Une autre technique consiste à déplacer l’objet Classe du Concepteur Objet/Relationnel de la Boîte à outils sur le canevas. Modifiez le nom et les propriétés de laclasse d’entité comme il a été exposé dans la section précédente.

Ajout de nouvelles propriétés (membres) dans une classe d’entitéPour ajouter une nouvelle propriété dans une classe d’entité, cliquez du bouton droit surla classe d’entité, dans le Concepteur, pointez Ajouter et sélectionnez Propriété dans lemenu contextuel. Lorsque la propriété a été ajoutée, procédez comme indiqué dans lasection "Modification des propriétés d’une propriété d’une classe d’entité" précédentepour modifier les propriétés de cette propriété.

Ajout d’une nouvelle associationPlutôt qu’utiliser un glisser-déposer pour créer une association, cliquez sur l’objetAssociation dans la Boîte à outils, sur la classe d’entité parent (le côté "un" d’une rela-tion "un-à-plusieurs"), puis sur la classe d’entité enfant (le côté "plusieurs" d’une rela-tion "un-à-plusieurs"). Avant de pouvoir définir une association, chacune des deuxclasses doit posséder une propriété appropriée, de telle sorte que vous puissiez définir laclé primaire du côté "un" et la clé étrangère du côté "plusieurs". Après avoir cliqué surla deuxième classe (celle qui correspond au côté "plusieurs" de l’association), la boîtede dialogue Editeur d’associations est affichée. Il ne vous reste plus qu’à l’utiliser pourrelier une propriété du côté "un" à une autre du côté "plusieurs".

Linq.book Page 408 Mercredi, 18. février 2009 7:58 07

Page 424: LINQ Language Integrated Query en C

Chapitre 13 Astuces et outils pour LINQ to SQL 409

Cette étape terminée, cliquez sur OK. Une flèche pointillée reliera la classe d’entitéparent à la classe d’entité enfant.

Sélectionnez l’association en cliquant sur la ligne pointillée et définissez les propriétésde l’association dans la fenêtre des propriétés. Si nécessaire, reportez-vous au Chapi-tre 15 pour avoir plus d’informations sur les propriétés d’une association.

Ajout d’un nouvel héritageVous pouvez également utiliser le Concepteur Objet/Relationnel pour modeler les rela-tions d’héritage : ajouter une relation d’héritage revient à ajouter une nouvelle associa-tion. Sélectionnez l’objet Héritage dans la Boîte à outils de Visual Studio, cliquez sur laclasse d’entité qui sera la classe dérivée, puis sur la classe d’entité qui sera la classe debase. Assurez-vous que vous avez défini correctement les propriétés de la classed’entité, comme spécifié dans les attributs de classe d’entité InheritanceMapping etColumn. Si nécessaire, reportez-vous au Chapitre 15 pour en savoir plus à ce sujet.

Ajout de procédures stockées et de fonctions définies par l’utilisateurAu Chapitre 14, vous apprendrez à surcharger les méthodes insert, update et deleteutilisées par LINQ to SQL suite à des modifications effectuées dans une classe d’entité.Pour surcharger les méthodes par défaut, il suffit d’ajouter des méthodes spécifiquesdans une classe d’entité. Si vous adoptez cette approche, assurez-vous que vous utilisezdes classes partielles. Ainsi, vous ne modifierez aucun code généré. Nous étudieronscette technique en détail au Chapitre 14.

Sachez cependant que les méthodes insert, update et delete peuvent être facilementsurchargées en utilisant le Concepteur. Supposons que nous disposions de la procédurestockée InsertCustomer, qui insère un enregistrement d’un nouveau client dans la tableCustomer de la base de données Northwind. Voici le code de la procédure stockée :

CREATE PROCEDURE dbo.InsertCustomer ( @CustomerID nchar(5), @CompanyName nvarchar(40), @ContactName nvarchar(30), @ContactTitle nvarchar(30), @Address nvarchar(60), @City nvarchar(15), @Region nvarchar(15), @PostalCode nvarchar(10), @Country nvarchar(15), @Phone nvarchar(24), @Fax nvarchar(24) )AS INSERT INTO Customers ( CustomerID, CompanyName, ContactName, ContactTitle, Address,

Linq.book Page 409 Mercredi, 18. février 2009 7:58 07

Page 425: LINQ Language Integrated Query en C

410 LINQ to SQL Partie V

City, Region, PostalCode, Country, Phone, Fax )VALUES( @CustomerID, @CompanyName, @ContactName, @ContactTitle, @Address, @City, @Region, @PostalCode, @Country, @Phone, @Fax)

INFO

La procédure stockée InsertCustomer ne fait pas partie de la version étendue de la basede données Northwind. Elle a été ajoutée manuellement pour cette démonstration.

Pour surcharger la méthode insert de la classe d’entité Customer, assurez-vous que levolet Méthodes est visible. Dans le cas contraire, cliquez du bouton droit sur le canevaset sélectionnez Ajouter le volet Méthodes dans le menu contextuel. Si la fenêtre Explo-rateur de serveurs n’est pas déjà ouverte, ouvrez-la. Développez le nœud Procéduresstockées. La fenêtre de Visual Studio doit maintenant ressembler à la Figure 13.5.

Figure 13.5 :

La procédure stockée Insert-Customer.

m

Linq.book Page 410 Mercredi, 18. février 2009 7:58 07

Page 426: LINQ Language Integrated Query en C

Chapitre 13 Astuces et outils pour LINQ to SQL 411

Faites glisser la procédure stockée InsertCustomer de l’Explorateur de serveurs dansle volet Méthodes. La fenêtre de Visual Studio devrait maintenant ressembler à laFigure 13.6.

Le simple fait de déposer une méthode stockée depuis l’Explorateur de serveurs dans levolet Méthodes demande au Concepteur de générer le code qui permettra d’appeler laprocédure stockée depuis LINQ to SQL. Vous utiliserez la même méthode pour générerle code d’une fonction définie par l’utilisateur.

Pour qu’une opération Delete, Insert ou Update appelle une procédure stockée (et nonla méthode par défaut), la procédure doit être accessible depuis LINQ to SQL. Cettepremière étape effectuée, il suffit alors de surcharger l’opération.

Cliquez sur la classe Customers dans le Concepteur et observez la fenêtre des proprié-tés. Les dernières lignes listent les méthodes utilisées par défaut pour les opérationsDelete, Insert et Update. Cliquez sur l’entrée Insert. Un bouton contenant des pointsde suspension apparaît dans la partie droite de l’entrée (voir Figure 13.7).

Cliquez sur le bouton pour afficher la boîte de dialogue Configurer le comportement.Sélectionnez l’option Personnaliser et la procédure stockée InsertCustomer dans laliste déroulante. Reliez les arguments de méthode (à gauche) aux propriétés de classe (àdroite), comme illustré à la Figure 13.8.

Comme vous pouvez le constater, tous les arguments de méthode sont déjà correcte-ment reliés aux propriétés de classe. Cliquez sur OK pour fermer la boîte de dialogueConfigurer le comportement. Vous êtes maintenant prêt à insérer des enregistrementsCustomer en utilisant la procédure stockée InsertCustomer (voir Listing 13.3).

Figure 13.6 :

Glisser-déposer de la procédure stockée dans le volet Méthodes.

Linq.book Page 411 Mercredi, 18. février 2009 7:58 07

Page 427: LINQ Language Integrated Query en C

412 LINQ to SQL Partie V

Listing 13.3 : Création d’un enregistrement Customer avec la surcharge de la méthode par défaut Insert.

NorthwindDataContext db = new NorthwindDataContext();

db.Log = Console.Out;

Customer cust = new Customer {

Figure 13.7 :

Sélection de la méthode Insert dans la catégorie Méthodes par défaut de la fenêtre des propriétés.

Figure 13.8 :

Liaison des argu-ments de méthode aux pro-priétés de classe.

Linq.book Page 412 Mercredi, 18. février 2009 7:58 07

Page 428: LINQ Language Integrated Query en C

Chapitre 13 Astuces et outils pour LINQ to SQL 413

CustomerID = "EWICH", CompanyName = "Every ’Wich Way", ContactName = "Vickey Rattz", ContactTitle = "Owner", Address = "105 Chip Morrow Dr.", City = "Alligator Point", Region = "FL", PostalCode = "32346", Country = "USA", Phone = "(800) EAT-WICH", Fax = "(800) FAX-WICH" };

db.Customers.InsertOnSubmit(cust);

db.SubmitChanges();

Customer customer = db.Customers.Where(c => c.CustomerID == "EWICH").First();Console.WriteLine("{0} - {1}", customer.CompanyName, customer.ContactName);

// Restauration de la base de donnéesdb.Customers.DeleteOnSubmit(cust);db.SubmitChanges();

Comme vous pouvez le voir, aucun espace de noms n’est spécifié dans la classe Custo-mer référencée. Nous utiliserons en effet la classe Customer générée par le Concepteur.Cette classe se trouve dans le même espace de noms que le projet.

Le Listing 13.3 est assez simple à comprendre. Après avoir instancié le DataContextNorthwindDataContext, généré par le Concepteur, un objet Customer est créé etinitialisé. Cet objet est inséré dans la propriété Customers Table<T>. La méthodeSubmitChanges est alors appelée pour reporter la modification dans la base dedonnées. Une requête retrouve cet enregistrement dans la base de données, puis uneinstruction Console.WriteLine l’affiche dans la console, pour mettre en évidenceque l’enregistrement a bien été inséré dans la base de données. Enfin, l’enregistre-ment est supprimé de la base de données avec la méthode DeleteOnSubmit, et lasuppression est rendue permanente avec la méthode SubmitChanges. La base dedonnées se trouve donc dans le même état après l’exécution du code (voirListing 13.3). Les exemples suivants ne seront donc pas affectés par ce code, et cequ’il soit exécuté une ou plusieurs fois.

EXEC @RETURN_VALUE = [dbo].[InsertCustomer] @CustomerID = @p0, @CompanyName = @p1,@ContactName = @p2, @ContactTitle = @p3, @Address = @p4, @City = @p5, @Region = @p6,@PostalCode = @p7, @Country = @p8, @Phone = @p9, @Fax = @p10-- @p0: Input StringFixedLength (Size = 5; Prec = 0; Scale = 0) [EWICH]-- @p1: Input String (Size = 15; Prec = 0; Scale = 0) [Every ’Wich Way]-- @p2: Input String (Size = 12; Prec = 0; Scale = 0) [Vickey Rattz]-- @p3: Input String (Size = 5; Prec = 0; Scale = 0) [Owner]-- @p4: Input String (Size = 19; Prec = 0; Scale = 0) [105 Chip Morrow Dr.]-- @p5: Input String (Size = 15; Prec = 0; Scale = 0) [Alligator Point]-- @p6: Input String (Size = 2; Prec = 0; Scale = 0) [FL]-- @p7: Input String (Size = 5; Prec = 0; Scale = 0) [32346]-- @p8: Input String (Size = 3; Prec = 0; Scale = 0) [USA]-- @p9: Input String (Size = 14; Prec = 0; Scale = 0) [(800) EAT-WICH]-- @p10: Input String (Size = 14; Prec = 0; Scale = 0) [(800) FAX-WICH]-- @RETURN_VALUE: Output Int32 (Size = 0; Prec = 0; Scale = 0) []-- Context: SqlProvider(Sql2005) Model: AttributedMetaModel Build: 3.5.20706.1

Linq.book Page 413 Mercredi, 18. février 2009 7:58 07

Page 429: LINQ Language Integrated Query en C

414 LINQ to SQL Partie V

SELECT TOP 1 [t0].[CustomerID], [t0].[CompanyName], [t0].[ContactName],[t0].[ContactTitle], [t0].[Address], [t0].[City], [t0].[Region], [t0].[PostalCode],[t0].[Country], [t0].[Phone], [t0].[Fax]FROM [dbo].[Customers] AS [t0]WHERE [t0].[CustomerID] = @p0-- @p0: Input String (Size = 5; Prec = 0; Scale = 0) [EWICH]-- Context: SqlProvider(Sql2005) Model: AttributedMetaModel Build: 3.5.20706.1

Every ’Wich Way - Vickey RattzDELETE FROM [dbo].[Customers] WHERE ([CustomerID] = @p0) AND ([CompanyName] = @p1)AND ([ContactName] = @p2) AND ([ContactTitle] = @p3) AND ([Address] = @p4) AND([City] = @p5) AND ([Region] = @p6) AND ([PostalCode] = @p7) AND ([Country] = @p8)AND ([Phone] = @p9) AND ([Fax] = @p10)-- @p0: Input StringFixedLength (Size = 5; Prec = 0; Scale = 0) [EWICH]-- @p1: Input String (Size = 15; Prec = 0; Scale = 0) [Every ’Wich Way]-- @p2: Input String (Size = 12; Prec = 0; Scale = 0) [Vickey Rattz]-- @p3: Input String (Size = 5; Prec = 0; Scale = 0) [Owner]-- @p4: Input String (Size = 19; Prec = 0; Scale = 0) [105 Chip Morrow Dr.]-- @p5: Input String (Size = 15; Prec = 0; Scale = 0) [Alligator Point]-- @p6: Input String (Size = 2; Prec = 0; Scale = 0) [FL]-- @p7: Input String (Size = 5; Prec = 0; Scale = 0) [32346]-- @p8: Input String (Size = 3; Prec = 0; Scale = 0) [USA]-- @p9: Input String (Size = 14; Prec = 0; Scale = 0) [(800) EAT-WICH]-- @p10: Input String (Size = 14; Prec = 0; Scale = 0) [(800) FAX-WICH]-- Context: SqlProvider(Sql2005) Model: AttributedMetaModel Build: 3.5.20706.1

Comme vous pouvez le voir, aucune déclaration SQL insert n’a été créée. En revanche,la procédure stockée InsertCustomer a été appelée (code en gras). Le Concepteur faci-lite grandement la surcharge des méthodes Insert, Update et Delete d’une classed’entité.

Utiliser SQLMetal et le Concepteur O/R

Étant donné que le fichier intermédiaire DBML issu de SQLMetal utilise le mêmeschéma que le Concepteur Objet/Relationnel, il est tout à fait possible d’utiliser cesdeux outils conjointement.

Par exemple, vous pourriez très bien générer un fichier DBML intermédiaire en utili-sant SQLMetal, puis ouvrir ce fichier dans le Concepteur afin de modifier le nom d’uneclasse d’entité ou d’une propriété d’une classe d’entité. Cette technique vous permet decréer simplement les classes d’entité d’une base de données et de modifier le ou lesnoms souhaités.

La surcharge des opérations insert, delete et update est un autre exemple pour lequelcette interchangeabilité peut être mise à profit : vous pourriez générer un fichier inter-médiaire DBML avec SQLMetal, le charger dans le Concepteur et modifier les métho-des insert, delete et update, comme indiqué dans la section "Le Concepteur Objet/Relationnel" de ce chapitre.

Linq.book Page 414 Mercredi, 18. février 2009 7:58 07

Page 430: LINQ Language Integrated Query en C

Chapitre 13 Astuces et outils pour LINQ to SQL 415

Résumé

Les classes d’entité et la classe DataContext n’ayant pas encore été étudiées en détail,la plupart des informations de ce chapitre peuvent vous sembler prématurées. Cepen-dant, je me devais d’introduire les astuces et outils disponibles pour les développementsLINQ to SQL. N’hésitez pas à revenir vers ce chapitre lorsque vous aurez toutes lesconnaissances nécessaires pour bien l’appréhender.

Rappelez-vous que vous disposez de deux outils pour générer les classes d’entité d’unebase de données : SQLMetal et le Concepteur Objet/Relationnel. SQLMetal est un outilqui fonctionne en ligne de commande. Il est approprié lorsqu’il s’agit de générer toutesles classes d’entité d’une base de données. Le Concepteur O/R (aussi connu sous lenom Concepteur LINQ to SQL) fait partie intégrante de Visual Studio. Il utilise la tech-nique du glisser-déposer de Windows pour modéliser les classes d’entité. Très interac-tif, il s’adapte parfaitement aux nouveaux développements. Notez cependant que cesdeux outils peuvent être utilisés conjointement. La meilleure technique consiste certai-nement à générer toutes les classes d’entité avec SQLMetal et à assurer leur mainte-nance avec le Concepteur O/R.

Vous connaissez maintenant les outils LINQ to SQL et quelques astuces de développe-ment, et vous avez appris à créer vos classes d’entité. Le chapitre suivant va vousmontrer comment effectuer les opérations basiques de base de données, courammentutilisées lors de développements LINQ to SQL.

Linq.book Page 415 Mercredi, 18. février 2009 7:58 07

Page 431: LINQ Language Integrated Query en C

Linq.book Page 416 Mercredi, 18. février 2009 7:58 07

Page 432: LINQ Language Integrated Query en C

14

Opérations standardsur les bases de données

Dans ce chapitre, vous allez apprendre à utiliser LINQ to SQL pour effectuer les opéra-tions standard sur des bases de données. Nous nous intéresserons en particulier auxopérations suivantes :

m insertion d’enregistrements ;

m requêtes ;

m mises à jour ;

m suppression d’enregistrements.

Après avoir introduit les opérations de base de données standard, vous verrez commentsurcharger les méthodes insert, update et delete utilisées pour enregistrer des modi-fications dans une base de données.

Le sujet suivant sera consacré à la traduction automatique des requêtes LINQ to SQL.Vous y apprendrez en outre comment "bien écrire" vos requêtes.

Pour pouvoir discuter des opérations de base de données standard, nous devrons nousréférer au DataContext et aux classes afférentes. Pour l’instant, nous n’avons donnéque peu de détails sur leur fonctionnement. Vous en apprendrez plus à leur sujet dansles chapitres (respectivement) 16 et 15. Pour l’instant, il vous suffit de savoir que leDataContext gère la connexion à la base de données et les objets des classes d’entité(une classe d’entité représente la forme objet d’un enregistrement de base de données).

Prérequis pour exécuter les exemples

Pour pouvoir exécuter les exemples de ce chapitre, vous devez avoir téléchargé laversion étendue de la base de données exemple de Microsoft Northwind et avoir généré

Linq.book Page 417 Mercredi, 18. février 2009 7:58 07

Page 433: LINQ Language Integrated Query en C

418 LINQ to SQL Partie V

les classes d’entité correspondantes. Reportez-vous à la section "Prérequis pour exécu-ter les exemples" du Chapitre 12 pour en savoir plus à ce sujet.

Méthodes communes

Vous aurez également besoin de quelques méthodes communes. Reportez-vous à lasection "Méthodes communes" du Chapitre 12 pour en savoir plus à ce sujet.

Utilisation de l’API LINQ to SQL

Pour exécuter les exemples de ce chapitre, vous aurez peut-être besoin d’ajouter lesréférences et directives using appropriées dans votre projet. Reportez-vous à la section"Utilisation de l’API LINQ to SQL" du Chapitre 12 pour en savoir plus à ce sujet.

Opérations standard de bases de données

Les requêtes LINQ to SQL seront traitées en détail dans les prochains chapitres. Ici,nous nous contenterons de vous montrer comment effectuer les opérations de base. Lesexemples de ce chapitre étant volontairement limités à des fins pédagogiques, nous n’yinclurons aucun code de recherche d’erreur ni de gestion des exceptions.

Normalement, les opérations qui effectuent des modifications dans la base de donnéesdevraient inclure un code de détection et de traitement des conflits d’accès concurren-tiel. Mais, dans un souci de simplification, cette portion de code sera omise. Si néces-saire, reportez-vous au Chapitre 17 pour savoir comment détecter et résoudre lesconflits d’accès concurrentiels.

Insertions

Il ne suffit pas d’instancier une classe d’entité (la classe Customer, par exemple) pourinsérer un enregistrement dans la base de données. Vous devez également insérer unobjet entité dans une collection de tables de type Table<T> (où T est le type de la classed’entité stocké dans la table) ou l’ajouter à un EntitySet<T> (où T est le type de laclasse d’entité) sur un objet d’entité déjà présent dans le DataContext.

Pour insérer un enregistrement dans la base de données, respectez les quatre étapessuivantes :

1. Définissez un DataContext (c’est également la première étape pour toute requêteLINQ to SQL).

2. Instanciez un objet entité à partir de la classe d’entité.

3. Insérez l’objet entité dans la collection de table.

4. Appelez la méthode SubmitChanges sur le DataContext.

Le Listing 14.1 donne un exemple d’insertion d’un enregistrement dans une base dedonnées.

Linq.book Page 418 Mercredi, 18. février 2009 7:58 07

Page 434: LINQ Language Integrated Query en C

Chapitre 14 Opérations standard sur les bases de données 419

Listing 14.1 : Insertion d’un enregistrement en insérant un objet entité dans un Table<T>.

// 1. Création du DataContextNorthwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind");

// 2. Instanciation d’un objet entitéCustomer cust =new Customer {CustomerID = "LAWN",CompanyName = "Lawn Wranglers",ContactName = "Mr. Abe Henry",ContactTitle = "Owner",Address = "1017 Maple Leaf Way",City = "Ft. Worth",Region = "TX",PostalCode = "76104",Country = "USA",Phone = "(800) MOW-LAWN",Fax = "(800) MOW-LAWO" };

// 3. Ajout de l’objet entité à la table Customersdb.Customers.InsertOnSubmit(cust);

// 4. Appel de la méthode SubmitChangesdb.SubmitChanges();

// 5. Requête sur l’enregistrementCustomer customer = db.Customers.Where(c => c.CustomerID == "LAWN").First();Console.WriteLine("{0} - {1}", customer.CompanyName, customer.ContactName);// Cette portion de code restaure la base de données dans son// état initial. Le code peut donc être exécuté à plusieurs reprises.Console.WriteLine("Suppression du client LAWN.");db.Customers.DeleteOnSubmit(cust);db.SubmitChanges();

Cet exemple est très simple. Le premier bloc instancie l’objet Northwind afin d’obtenir unobjet DataContext pour la base de données Northwind. Le deuxième bloc de code instan-cie l’objet Customer et l’initialise en utilisant la technique d’initialisation d’objet apparuedans C# 3.0. Le troisième bloc insère l’objet Customer de type Table<Customer>dans latable Customers. Le quatrième bloc appelle la méthode SubmitChanges pour enregistrerl’objet Customer dans la base de données. Enfin, le cinquième bloc effectue une requêtesur la base de données, afin de montrer que l’enregistrement a bien été inséré.

INFO

Si vous exécutez cet exemple, un nouvel enregistrement correspondant au client LAWN esttemporairement ajouté à la table Customers de la base de données Northwind. Après avoireffectué une requête sur ce nouvel enregistrement et affiché les données correspondantes,les dernières lignes du code le suppriment. Cette précaution est nécessaire car, ainsi, l’exem-ple pourra être exécuté autant de fois que vous le souhaiterez sans que la base de donnéesne soit modifiée pour les exemples suivants. Cette portion de code sera utilisée pour tous lesexemples qui modifient la base de données. Si, pour une raison ou pour une autre, le codene s’exécute pas entièrement, vous devrez restaurer manuellement la base de données à sonétat initial.

Linq.book Page 419 Mercredi, 18. février 2009 7:58 07

Page 435: LINQ Language Integrated Query en C

420 LINQ to SQL Partie V

Voici les résultats du Listing 14.1 :

Comme vous pouvez le constater, le nouvel enregistrement a bien été trouvé dans labase de données.

Pour insérer un nouvel enregistrement dans une base de données, vous pouvez égale-ment ajouter une nouvelle instance d’une classe d’entité à un objet entité déjà référencédans l’objet DataContext (voir Listing 14.2).

Listing 14.2 : Insertion d’un enregistrement dans la base de données Northwind en intervenant sur EntitySet<T>.

Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind");// Requête pour retrouver l’enregistrement déjà référencé.Customer cust = (from c in db.Customerswhere c.CustomerID == "LONEP"select c).Single<Customer>();

// Définition du nouvel enregistrement.DateTime now = DateTime.Now;Order order = new Order{CustomerID = cust.CustomerID,EmployeeID = 4,OrderDate = now,RequiredDate = DateTime.Now.AddDays(7),ShipVia = 3,Freight = new Decimal(24.66),ShipName = cust.CompanyName,ShipAddress = cust.Address,ShipCity = cust.City,ShipRegion = cust.Region,ShipPostalCode = cust.PostalCode,ShipCountry = cust.Country};

cust.Orders.Add(order);db.SubmitChanges();IEnumerable<Order> orders =db.Orders.Where(o => o.CustomerID == "LONEP" && o.OrderDate.Value == now);foreach (Order o in orders){Console.WriteLine("{0} {1}", o.OrderDate, o.ShipName);}

// Cette portion de code restaure la base de données dans son// état initial. Le code peut donc être exécuté à plusieurs reprises.db.Orders.DeleteOnSubmit(order);db.SubmitChanges();

INFO

La méthode InsertOnSubmit est appelée dans le Listing 14.1, alors que c’est la méthodeAdd qui est appelée dans le Listing 14.2. Ces méthodes sont différentes, car elles sont appe-

Lawn Wranglers - Mr. Abe HenrySuppression du client LAWN.

Linq.book Page 420 Mercredi, 18. février 2009 7:58 07

Page 436: LINQ Language Integrated Query en C

Chapitre 14 Opérations standard sur les bases de données 421

lées sur des objets différents. Dans le Listing 14.1, la méthode InsertOnSubmit est appeléesur un objet Table<T>. Dans le Listing 14.2, la méthode Add est appelée sur un objet Enti-tySet<T>.

Dans le Listing 14.2, nous avons créé le DataContext Northwind, effectué une requêtesur le client LONEP et ajouté l’objet entité order fraîchement construit à l’Entity-Set<Order> Orders de l’objet entité Customer. Le nouvel enregistrement a alors étérécupéré à l’aide d’une requête et affiché dans la console.

INFO

Les dernières lignes du code utilisent la méthode DeleteOnSubmit pour supprimer l’enre-gistrement order, précédemment ajouté à la base de données. Si le code ne s’exécute pasentièrement, vous devrez supprimer manuellement l’enregistrement pour que la base dedonnées ne soit pas affectée pour les autres exemples.

Si vous pensez que cet exemple est comparable au précédent, sachez que, dans leListing 14.1, un objet Customer a été inséré dans une variable de type Table<Custo-mer>, alors que, dans le Listing 14.2, l’objet Order a été ajouté à une variable de typeEntitySet<Order>.

Voici les résultats du Listing 14.2 :

Insertion d’objets entité liésLe DataContext détecte les objets de classe d’entité "liés". Ces objets seront égalementenregistrés lors de l’appel de la méthode SubmitChanges. Le terme "lié" se rapporte auxobjets de classe d’entité qui possèdent une clé étrangère vers l’objet de classe entitéinséré. Pour clarifier les choses, considérez l’exemple du Listing 14.3.

Listing 14.3 : Ajout d’enregistrements liés.

Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind");Customer cust =new Customer {CustomerID = "LAWN",CompanyName = "Lawn Wranglers",ContactName = "Mr. Abe Henry",ContactTitle = "Owner",Address = "1017 Maple Leaf Way",City = "Ft. Worth",Region = "TX",PostalCode = "76104",Country = "USA",Phone = "(800) MOW-LAWN",Fax = "(800) MOW-LAWO",Orders = {new Order {

9/2/2007 6:02:16 PM Lonesome Pine Restaurant

Linq.book Page 421 Mercredi, 18. février 2009 7:58 07

Page 437: LINQ Language Integrated Query en C

422 LINQ to SQL Partie V

CustomerID = "LAWN",EmployeeID = 4,OrderDate = DateTime.Now,RequiredDate = DateTime.Now.AddDays(7),ShipVia = 3,Freight = new Decimal(24.66),ShipName = "Lawn Wranglers",ShipAddress = "1017 Maple Leaf Way",ShipCity = "Ft. Worth",ShipRegion = "TX",ShipPostalCode = "76104",ShipCountry = "USA" } } };

db.Customers.InsertOnSubmit(cust);db.SubmitChanges();

Customer customer = db.Customers.Where(c => c.CustomerID == "LAWN").First();Console.WriteLine("{0} - {1}", customer.CompanyName, customer.ContactName);foreach (Order order in customer.Orders){Console.WriteLine("{0} - {1}", order.CustomerID, order.OrderDate);}

// Cette portion de code restaure la base de données dans son// état initial. Le code peut donc être exécuté à plusieurs reprises.db.Orders.DeleteOnSubmit(cust.Orders.First());db.Customers.DeleteObSubmit(cust);db.SubmitChanges();

Dans le Listing 14.3, un objet Customer a été créé puis initialisé avec des donnéescontenant, entre autres, une collection Orders composée d’un objet Order. L’objetOrder étant lié à l’objet Customer, il est automatiquement sauvegardé dans la base dedonnées lors de l’appel de la méthode SubmitChanges.

Nous allons consacrer quelques instants à un autre détail de ce listing. Le dernier blocd’instructions restaure la base de données dans son état initial. Pour ce faire, il invoquela méthode DeleteOnSubmit à deux reprises : une pour l’enregistrement Order, uneautre pour l’enregistrement Customer. Dans cet exemple, le premier enregistrementOrder est supprimé. L’enregistrement Customer étant nouveau, nous pouvons être sûrsque l’enregistrement Order est unique. Cette double suppression est utile car, si l’inser-tion d’un enregistrement contenant des données liées provoque la mise à jour "encascade" des tables correspondantes, il n’en est rien de la suppression : la suppressiond’un objet entité parent n’entraîne pas la suppression automatique des objets entitéenfant liés. Si l’enregistrement Order lié à l’enregistrement Customer n’avait pas étésupprimé, une exception aurait été levée. Nous reparlerons de tout cela en détail dans lasection "Suppressions" de ce chapitre.

Voici le résultat affiché dans la console lorsque vous appuyez sur Ctrl+F5 :

Lawn Wranglers - Mr. Abe HenryLAWN - 9/2/2007 6:05:07 PM

Linq.book Page 422 Mercredi, 18. février 2009 7:58 07

Page 438: LINQ Language Integrated Query en C

Chapitre 14 Opérations standard sur les bases de données 423

Le nouveau client a bel et bien été inséré dans la base de données. Du moins temporai-rement, puisque les dernières lignes du code en ont effacé toute trace.

Requêtes

L’exécution de requêtes LINQ to SQL s’apparente à l’exécution d’autres requêtesLINQ, à quelques petits détails près. Nous allons parler brièvement de ces détails danscette section.

Pour exécuter une requête LINQ to SQL, il suffit de créer un objet DataContext, puisd’appliquer la requête sur cet objet (voir Listing 14.4).

Listing 14.4 : Une requête LINQ to SQL élémentaire sur la base de données Northwind.

Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind");Customer cust = (from c in db.Customerswhere c.CustomerID == "LONEP"select c).Single<Customer>();

Ce code stocke l’enregistrement du client dont le champ CustomerId vaut "LONEP"dans l’objet Customer cust. Comme il a été mentionné au Chapitre 5, si ce clientn’existe pas, l’opérateur Single provoque une exception. Vous devez donc vous assurerde l’existence du client avant d’exécuter ce code. En réalité, vous pourriez utiliserl’opérateur SingleOrDefault à la place de l’opérateur Single, pour éviter la levéed’une exception.

Plusieurs autres détails méritent quelques explications. Remarquez que la requêteutilise la syntaxe C# pour tester la valeur du champ CustomerId. Ceci est facile àdéduire, car la valeur LONEP est entourée de guillemets, et non d’apostrophes, commel’exige la syntaxe de SQL. Un autre indice : l’opérateur "==" est utilisé à la place del’opérateur SQL "=". La requête est incluse dans le langage. Heureusement, puisqu’elleest exprimée en LINQ et que cette abréviation signifie "langage d’interrogation inté-gré". Remarquez également que nous mélangeons la syntaxe d’expression de requête etla notation standard "à point" dans cette requête. La portion syntaxe d’expression derequête se trouve à l’intérieur des parenthèses. L’opérateur Single, quant à lui, estappliqué au résultat de la requête en utilisant la notation standard de C#.

Dans les chapitres précédents, nous avons souvent parlé des opérateurs de requête diffé-rés. Pensez-vous que la requête du Listing 14.4 exécutera immédiatement la requête ?La réponse est oui. En effet, l’opérateur de requête standard Single provoqueral’exécution immédiate de la requête. Si cet opérateur avait été omis, la requête n’auraitpas été exécutée.

Le Listing 14.4 ne provoque aucune sortie écran. Pour vérifier que le code extrait le bonclient de la base de données, nous allons lui ajouter une instruction qui affichera le nomde la société et le nom du contact dans la console (voir Listing 14.5).

Linq.book Page 423 Mercredi, 18. février 2009 7:58 07

Page 439: LINQ Language Integrated Query en C

424 LINQ to SQL Partie V

Listing 14.5 : La même requête avec une sortie dans la console.

Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind");Customer cust = (from c in db.Customerswhere c.CustomerID == "LONEP"select c).Single<Customer>();Console.WriteLine("{0} - {1}", cust.CompanyName, cust.ContactName);

Voici la sortie console de ce listing :

Quelques exceptionsUn peu plus tôt, nous avons signalé que les requêtes LINQ to SQL se comportaientcomme des requêtes LINQ, à quelques exceptions près. Cette section passe en revue cesexceptions.

Si une requête LINQ to SQL retourne un IQueryable<T>Alors que les requêtes LINQ appliquées à des tableaux et à des collections retournentdes séquences de type IEnumerable<T>, la séquence retournée par une requête LINQ toSQL (le cas échéant) est de type IQueryable<T>. Le Listing 14.6 donne un exempled’une requête qui retourne une séquence de ce type.

Listing 14.6 : Une requête LINQ to SQL basique qui retourne une séquence IQueryable<T>.

Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind");IQueryable<Customer> custs = from c in db.Customerswhere c.City == "London"select c;foreach(Customer cust in custs){Console.WriteLine("Client : {0}", cust.CompanyName);}

Comme vous pouvez le voir, le type retourné par cette requête est IQueryable<Custo-mer>. Voici les résultats affichés dans la console :

Comme il a été dit dans le Chapitre 12, l’interface IQueryable<T> étend l’interfaceIEnumerable<T>. Vous pouvez donc traiter une séquence de type IQueryable<T>comme s’il s’agissait d’une séquence IEnumerable<T>. Si cela vous pose des difficul-tés, rappelez-vous l’opérateur AsEnumerable…

Lonesome Pine Restaurant - Fran Wilson

Client : Around the HornClient : B’s BeveragesClient : Consolidated HoldingsClient : Eastern ConnectionClient : North/SouthClient : Seven Seas Imports

Linq.book Page 424 Mercredi, 18. février 2009 7:58 07

Page 440: LINQ Language Integrated Query en C

Chapitre 14 Opérations standard sur les bases de données 425

Si une requête LINQ to SQL est appliquée à un objet Table<T>La plupart des requêtes LINQ traditionnelles sont appliquées à des tableaux ou à descollections qui implémentent l’interface IEnumerable<T> ou IEnumerable. Les requêtesLINQ to SQL, en revanche, sont appliquées sur des classes qui implémentent IQuerya-ble<T> ; par exemple la classe Table<T>.

Cela signifie que les requêtes LINQ to SQL disposent d’opérateurs additionnels etqu’elles peuvent utiliser les opérateurs de requête standard, puisque IQueryable<T>étend IEnumerable<T>.

Les requêtes LINQ to SQL sont traduites en SQLComme indiqué au Chapitre 2, étant donné que les requêtes LINQ to SQL retournentdes séquences de type IQueryable<T>, elles ne sont pas compilées en langage intermé-diaire .NET, contrairement aux requêtes LINQ traditionnelles : elles sont converties enarbres d’expression. Ainsi, elles sont traduites en requêtes SQL optimisées. Reportez-vous à la section "Traduction SQL", à la fin de ce chapitre, pour en savoir plus au sujetde la traduction appliquée aux requêtes LINQ to SQL.

Les requêtes LINQ to SQL sont exécutées dans la base de donnéesContrairement aux requêtes LINQ traditionnelles, qui sont exécutées dans la mémoirede l’ordinateur local, les requêtes LINQ to SQL sont traduites en appels SQL et exécu-tées sur l’ordinateur qui héberge la base de données. En corollaire, la façon dont lesprojections sont gérées implique que les traitements ne peuvent pas se faire dans la basede données : en effet, cette dernière ignore tout des classes d’entité ou des autres classesde ce type.

Par ailleurs, étant donné que les requêtes s’exécutent dans la base de données et quecette dernière n’a pas accès au code de l’application, les actions effectuées dans unerequête doivent être traduites, en respectant les possibilités du traducteur. Par exemple,si vous effectuez un appel à une méthode qui a été spécifiée dans une expressionlambda, SQL Server sera incapable de traiter cet appel. C’est la raison pour laquelle ilest bon de savoir ce qui peut être traduit, le résultat de la traduction et ce qui se passerasi une expression ne peut pas être traduite.

AssociationsAvec LINQ to SQL, l’interrogation d’une classe associée est aussi simple quel’accès à une variable membre d’une classe d’entité. Ceci est dû au fait qu’une classeassociée est une variable membre de la classe d’entité liée ou qu’elle est stockéedans une collection de classes d’entité et que la collection est une variable membrede la classe d’entité liée. Si la classe associée se trouve du côté "plusieurs" (enfant)d’une relation "un-à-plusieurs", la classe "plusieurs" sera stockée dans une collec-tion de classes "plusieurs". Le type de la collection sera EntitySet<T>, où T est letype de la classe d’entité "plusieurs". Cette collection sera une variable membre de

Linq.book Page 425 Mercredi, 18. février 2009 7:58 07

Page 441: LINQ Language Integrated Query en C

426 LINQ to SQL Partie V

la classe "un". Si la classe associée se trouve du côté "un" (parent) d’une relation"un-à-plusieurs", une référence à la classe "un" sera stockée dans une variable detype EntityRef<T>, où T est le type de la classe "un". Cette référence sera une variablemembre de la classe "plusieurs".

À titre d’exemple, considérez le cas des classes d’entité Customer et Order, générées àpartir de la base de données Northwind. Un client peut avoir passé plusieurs comman-des, mais une commande ne peut être associée qu’à un seul client. Dans cet exemple, laclasse Customer est le côté "un" de la relation "un-à-plusieurs" entre les classes d’entitéCustomer et Order. La classe Order est le côté "plusieurs" de cette relation. Par consé-quent, les commandes d’un objet Customer peuvent être référencées par une variablemembre, généralement appelée Orders, de type EntitySet<Order> dans la classeCustomer. Un client d’un objet Order peut être référencé avec une variable membre,généralement nommée Customer, de type EntityRef<Customer> dans la classe Order(voir Figure 14.1).

Si vous avez du mal à vous remémorer quel côté de la relation est stocké dans quel typede variable, rappelez-vous qu’un enfant a un parent et que, par conséquent, il est stockédans une référence "un". L’enfant stocke le parent associé dans une variable de typeEntityRef<T>. Comme un parent peut avoir plusieurs enfants, il doit stocker les réfé-rences à ses enfants dans une collection. Le parent stocke donc les références à sesenfants dans une variable de type EntitySet<T>.

Les classes sont associées en définissant leur attribut Association dans la propriété declasse qui contient la référence à la classe associée dans la définition de classe d’entité.Étant donné que le parent a une propriété de classe qui référence les enfants et inverse-ment, l’attribut Association doit être spécifié dans les classes d’entité parent et enfant.Nous reviendrons plus en détail sur l’attribut Association au Chapitre 15.

Le Listing 14.7 recherche certains clients, les affiche ainsi que chacune des commandesqu’ils ont passées.

Figure 14.1 :

Une relation entre classes d’entité parent et enfant.

Customer (parent)

EntitySet<Order>Orders EntitySet<Order>

Order (enfant)

EntityRef<Customer>Customer

Order (enfant)

EntityRef<Customer>Customer

Order (enfant)

EntityRef<Customer>Customer

Linq.book Page 426 Mercredi, 18. février 2009 7:58 07

Page 442: LINQ Language Integrated Query en C

Chapitre 14 Opérations standard sur les bases de données 427

Listing 14.7 : Utilisation d’une association pour accéder à des données liées.

Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind");IQueryable<Customer> custs = from c in db.Customerswhere c.Country == "UK" &&c.City == "London"orderby c.CustomerIDselect c;foreach (Customer cust in custs){Console.WriteLine("{0} - {1}", cust.CompanyName, cust.ContactName);foreach (Order order in cust.Orders) {Console.WriteLine(" {0} {1}", order.OrderID, order.OrderDate); }}

Ce code sélectionne les clients dont le champ Country vaut "UK" et le champ Cityvaut "London". Ces clients sont affichés, ainsi que chacune des commandes qu’ilsont passées. Comme vous pouvez le voir, il n’a jamais été fait référence à la tableOrders dans la requête. Voici une partie des résultats renvoyés par le code duListing 14.7 :

Tout ceci fonctionne à la perfection. Les commandes sont bien listées, même si aucunerequête n’a été explicitement définie pour les obtenir. Vous êtes donc en droit de vousdemander si ce code n’est pas inefficace si l’on n’accède jamais aux commandes desclients.

La réponse est non. En effet, les commandes ne sont pas extraites tant qu’elles ne sontpas référencées. Si la deuxième boucle foreach n’avait pas été écrite, les commandesn’auraient jamais été extraites de la base de données. Ce principe est connu sous le nomde "chargement différé". Ne le confondez pas avec les requêtes différées, qui ont étéprésentées dans les chapitres précédents.

Around the Horn - Thomas Hardy10355 11/15/1996 12:00:00 AM10383 12/16/1996 12:00:00 AM10453 2/21/1997 12:00:00 AM10558 6/4/1997 12:00:00 AM10707 10/16/1997 12:00:00 AM10741 11/14/1997 12:00:00 AM10743 11/17/1997 12:00:00 AM10768 12/8/1997 12:00:00 AM10793 12/24/1997 12:00:00 AM10864 2/2/1998 12:00:00 AM10920 3/3/1998 12:00:00 AM10953 3/16/1998 12:00:00 AM11016 4/10/1998 12:00:00 AM…Consolidated Holdings - Elizabeth Brown10435 2/4/1997 12:00:00 AM10462 3/3/1997 12:00:00 AM10848 1/23/1998 12:00:00 AM…

Linq.book Page 427 Mercredi, 18. février 2009 7:58 07

Page 443: LINQ Language Integrated Query en C

428 LINQ to SQL Partie V

Chargement différéOn dit qu’il y a chargement différé quand les enregistrements ne sont chargés dans labase de données que lorsque cela est absolument nécessaire. C’est-à-dire à leur premierréférencement.

Dans le Listing 14.7, la variable membre Orders n’est jamais référencée. Les enregis-trements correspondants ne sont donc jamais chargés depuis la base de données. Dansla plupart des situations, le chargement différé est une bonne chose. Il empêche lesrequêtes d’extraire des données dont elles n’ont pas besoin et limite la bande passanteconsommée sur le réseau.

Cependant, le chargement différé peut provoquer certains problèmes. Le Listing 14.8est identique au Listing 14.7, à ceci près que le Log du DataContext a été activé pourmettre le problème en évidence.

Listing 14.8 : Illustration du chargement différé.

Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind");IQueryable<Customer> custs = from c in db.Customerswhere c.Country == "UK" &&c.City == "London"orderby c.CustomerIDselect c;// Activation du Loggingdb.Log = Console.Out;foreach (Customer cust in custs){Console.WriteLine("{0} - {1}", cust.CompanyName, cust.ContactName);foreach (Order order in cust.Orders) {Console.WriteLine(" {0} {1}", order.OrderID, order.OrderDate); }}

Voici une partie des résultats affichée dans la console lors de l’appui sur Ctrl+F5 :

SELECT [t0].[CustomerID], [t0].[CompanyName], [t0].[ContactName],[t0].[ContactTitle], [t0].[Address], [t0].[City], [t0].[Region], [t0].[PostalCode],[t0].[Country], [t0].[Phone], [t0].[Fax]FROM [dbo].[Customers] AS [t0]WHERE ([t0].[Country] = @p0) AND ([t0].[City] = @p1)ORDER BY [t0].[CustomerID]•@p0: Input String (Size = 2; Prec = 0; Scale = 0) [UK]•@p1: Input String (Size = 6; Prec = 0; Scale = 0) [London]•Context: SqlProvider(Sql2005) Model: AttributedMetaModel Build: 3.5.20706.1Around the Horn - Thomas Hardy

SELECT [t0].[OrderID], [t0].[CustomerID], [t0].[EmployeeID], [t0].[OrderDate],[t0].[RequiredDate], [t0].[ShippedDate], [t0].[ShipVia], [t0].[Freight],[t0].[ShipName], [t0].[ShipAddress], [t0].[ShipCity], [t0].[ShipRegion],[t0].[ShipPostalCode], [t0].[ShipCountry]FROM [dbo].[Orders] AS [t0]WHERE [t0].[CustomerID] = @p0•@p0: Input String (Size = 5; Prec = 0; Scale = 0) [AROUT]•Context: SqlProvider(Sql2005) Model: AttributedMetaModel Build: 3.5.20706.1

Linq.book Page 428 Mercredi, 18. février 2009 7:58 07

Page 444: LINQ Language Integrated Query en C

Chapitre 14 Opérations standard sur les bases de données 429

Les requêtes SQL apparaissent en gras, pour les différencier des données issues destables Customer et Order. La première requête SQL interroge la table Customers. Ellene fait aucunement référence à la table Orders. Après avoir affiché le nom de la société(du client) et le nom du contact de la première société, une deuxième requête est exécu-tée. Cette dernière interroge la table Orders. La clause where limite les réponses auxenregistrements dont le champ CustomerID est égal au client sélectionné dans lapremière requête. Les lignes suivantes affichent la liste des commandes de ce client et lenom du client suivant. La requête SQL suivante concerne une commande client spécifique.

Comme vous le voyez, une requête différente est exécutée pour obtenir les commandesde chaque client. La table Orders n’est pas interrogée, et donc non chargée, jusqu’à ceque la variable EntityRef<T> Orders ne soit référencée dans la deuxième boucleforeach ; c’est-à-dire juste après l’affichage des informations relatives au client. Lescommandes n’étant pas chargées jusqu’à leur référencement, on dit que le chargementest différé.

10355 11/15/1996 12:00:00 AM10383 12/16/1996 12:00:00 AM10453 2/21/1997 12:00:00 AM10558 6/4/1997 12:00:00 AM10707 10/16/1997 12:00:00 AM10741 11/14/1997 12:00:00 AM10743 11/17/1997 12:00:00 AM10768 12/8/1997 12:00:00 AM10793 12/24/1997 12:00:00 AM10864 2/2/1998 12:00:00 AM10920 3/3/1998 12:00:00 AM10953 3/16/1998 12:00:00 AM11016 4/10/1998 12:00:00 AMB’s Beverages - Victoria Ashworth

SELECT [t0].[OrderID], [t0].[CustomerID], [t0].[EmployeeID], [t0].[OrderDate],[t0].[RequiredDate], [t0].[ShippedDate], [t0].[ShipVia], [t0].[Freight],[t0].[ShipName], [t0].[ShipAddress], [t0].[ShipCity], [t0].[ShipRegion],[t0].[ShipPostalCode], [t0].[ShipCountry]FROM [dbo].[Orders] AS [t0]WHERE [t0].[CustomerID] = @p0•@p0: Input String (Size = 5; Prec = 0; Scale = 0) [BSBEV]•Context: SqlProvider(Sql2005) Model: AttributedMetaModel Build: 3.5.20706.1

10289 8/26/1996 12:00:00 AM10471 3/11/1997 12:00:00 AM10484 3/24/1997 12:00:00 AM10538 5/15/1997 12:00:00 AM10539 5/16/1997 12:00:00 AM10578 6/24/1997 12:00:00 AM10599 7/15/1997 12:00:00 AM10943 3/11/1998 12:00:00 AM10947 3/13/1998 12:00:00 AM11023 4/14/1998 12:00:00 AMConsolidated Holdings - Elizabeth Brown…

Linq.book Page 429 Mercredi, 18. février 2009 7:58 07

Page 445: LINQ Language Integrated Query en C

430 LINQ to SQL Partie V

Une requête est définie pour chaque client. Cela fait beaucoup d’allers-retours entre leprogramme et la base de données, et les performances ne sont pas optimales.

Pour améliorer les choses, il faudrait que les commandes soient obtenues en mêmetemps que les clients. Dans ce cas, on dit que le chargement est immédiat.

Chargement immédiat avec la classe DataLoadOptionsPar défaut, les classes associées utilisent un chargement différé. Si vous le souhaitez,vous pouvez forcer le chargement immédiat. Dans ce cas, les classes associées serontchargées avant d’être référencées. Dans certains cas, les performances en serontaméliorées. Pour ce faire, vous utiliserez l’opérateur LoadWith<T> de la classe Data-LoadOptions pour demander au DataContext de charger immédiatement la classe asso-ciée, spécifiée dans l’expression lambda de l’opérateur LoadWith<T>. Ainsi, àl’exécution de la requête, la classe primaire et ses classes associées seront chargées.

Le Listing 14.9 utilise le même code que le Listing 14.8, mais, ici :

m Un objet DataLoadOptions est instancié.

m L’opérateur LoadWith<T> est appelé sur cet objet, en passant le membre Orderspour provoquer le chargement immédiat des commandes lorsqu’un objet Customerest chargé.

m L’objet DataLoadOptions est affecté au DataContext Northwind.

m Pour s’assurer que les classes associées (les commandes) sont chargées avant d’êtreréférencées, le code d’énumération des commandes clients est omis.

Listing 14.9 : Illustration du chargement immédiat avec la classe DataLoadOptions.

Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind");DataLoadOptions dlo = new DataLoadOptions();dlo.LoadWith<Customer>(c => c.Orders);db.LoadOptions = dlo;

IQueryable<Customer> custs = (from c in db.Customerswhere c.Country == "UK" &&c.City == "London"orderby c.CustomerIDselect c);// Activation du Logdb.Log = Console.Out;

foreach (Customer cust in custs){Console.WriteLine("{0} - {1}", cust.CompanyName, cust.ContactName);}

Rappelons que les différences avec le listing précédent portent sur :

m l’instanciation d’un objet DataLoadOptions ;

m l’appel de l’opérateur LoadWith<T> ;

Linq.book Page 430 Mercredi, 18. février 2009 7:58 07

Page 446: LINQ Language Integrated Query en C

Chapitre 14 Opérations standard sur les bases de données 431

m l’affectation de l’objet DataLoadOptions au DataContext Northwind ;

m la suppression de toute référence aux commandes des utilisateurs.

Dans l’appel à l’opérateur LoadWith<T>, nous demandons au DataLoadOptions decharger immédiatement les commandes chaque fois qu’un objet Customer est chargé.Voici les résultats de ce listing :

Tout comme dans les résultats du Listing 14.8, les requêtes SQL apparaissent en gras.Les données des clients importent peu : ici, nous ne nous intéressons qu’aux requêtesSQL.

Comme vous le voyez, une seule requête suffit pour retrouver les clients qui répondentaux conditions de la clause where. Vous constatez également que, même si les comman-des des clients n’ont jamais été référencées dans le code, la requête a permis d’extraireles données des clients et les commandes correspondantes. Les commandes ont étéobtenues par un chargement immédiat. Par ailleurs, il n’est plus nécessaire d’avoir unerequête pour la table des clients et autant de requêtes que de commandes : une seule etunique requête suffit. Si le nombre de clients est élevé, cela peut faire une grande diffé-rence.

Avec la classe DataLoadOptions, vous n’êtes pas limité au chargement immédiat d’uneseule classe associée ou d’une seule hiérarchie de classes associées. Dans tous les cas,quel que soit le nombre de classes associées, le chargement immédiat fonctionne selonle même principe.

SELECT [t0].[CustomerID], [t0].[CompanyName], [t0].[ContactName],[t0].[ContactTitle], [t0].[Address], [t0].[City], [t0].[Region], [t0].[PostalCode],[t0].[Country], [t0].[Phone], [t0].[Fax], [t1].[OrderID], [t1].[CustomerID] AS[CustomerID2], [t1].[EmployeeID], [t1].[OrderDate], [t1].[RequiredDate],[t1].[ShippedDate], [t1].[ShipVia], [t1].[Freight], [t1].[ShipName],[t1].[ShipAddress], [t1].[ShipCity], [t1].[ShipRegion], [t1].[ShipPostalCode],[t1].[ShipCountry], (SELECT COUNT(*)FROM [dbo].[Orders] AS [t2]WHERE [t2].[CustomerID] = [t0].[CustomerID]) AS [count]FROM [dbo].[Customers] AS [t0]LEFT OUTER JOIN [dbo].[Orders] AS [t1] ON [t1].[CustomerID] = [t0].[CustomerID]WHERE ([t0].[Country] = @p0) AND ([t0].[City] = @p1)ORDER BY [t0].[CustomerID], [t1].[OrderID]•@p0: Input String (Size = 2; Prec = 0; Scale = 0) [UK]•@p1: Input String (Size = 6; Prec = 0; Scale = 0) [London]•Context: SqlProvider(Sql2005) Model: AttributedMetaModel Build: 3.5.20706.1

Around the Horn - Thomas HardyB’s Beverages - Victoria AshworthConsolidated Holdings - Elizabeth BrownEastern Connection - Ann DevonNorth/South - Simon CrowtherSeven Seas Imports - Hari Kumar

Linq.book Page 431 Mercredi, 18. février 2009 7:58 07

Page 447: LINQ Language Integrated Query en C

432 LINQ to SQL Partie V

Lorsque le chargement immédiat… n’est pas si immédiatLorsque les classes ne sont pas chargées avant leur référencement, on parle de charge-ment différé. Lorsqu’elles sont chargées avant leur référencement, on parle de chargementimmédiat. Dans certains cas, le chargement immédiat peut se révéler non aussi immédiatqu’il le devrait.

Dans le code du Listing 14.9, nous avons vu qu’en spécifiant une classe associée enargument de la méthode LoadWith<T> de la classe DataLoadOptions les commandesétaient chargées en même temps que les clients. Si la méthode LoadWith<T> est appeléeplusieurs fois pour que plusieurs classes soient chargées immédiatement, une seuled’entre elles sera liée à la classe d’origine et chargée en même temps qu’elle. Les autresne seront chargées qu’au référencement de la classe d’origine. Lorsque cette situationse produit, étant donné que les classes non liées à la classe d’origine sont chargées avantd’être référencées, on parle toujours de chargement immédiat, mais une requêtecomplémentaire est toujours nécessaire lorsque vous référencez chacune des classesd’origine. Le chargement des classes non liées est donc moins immédiat que celui desclasses liées.

C’est LINQ to SQL (et non la base de données) qui choisit quelles classes associéesdoivent être liées et quelles classes associées ne doivent pas l’être. Sa décision estfondée sur des principes généraux appliqués au modèle de classe d’entité. La liaison esteffectuée sur l’association qui a le plus bas niveau hiérarchique dans les classes char-gées immédiatement. Ceci sera plus facile à comprendre lorsque nous aborderons lasection qui montre comment charger une hiérarchie de classes associées.

Nous allons nous intéresser à deux approches : le chargement de plusieurs classes asso-ciées à la classe d’entité originale, et le chargement d’une hiérarchie de classes asso-ciées.

Chargement immédiat de plusieurs classes associéesEn utilisant la classe DataLoadOptions, vous pouvez demander le chargement immédiat deplusieurs classes associées d’une classe d’entité.

Dans le Listing 14.9, la requête SQL générée ne faisait aucune référence aux donnéesdémographiques des clients de la table associée Customers. Si nous avions fait réfé-rence à ces données, une nouvelle requête SQL aurait été exécutée pour chaque clientdont les données démographiques étaient référencées.

Dans le Listing 14.10, nous demandons au DataLoadOptions de charger immédiate-ment les données provenant de la table CustomerCustomerDemos ainsi que celles de latable Commandes.

Listing 14.10 : Chargement immédiat de plusieurs EntitySets.

Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind");DataLoadOptions dlo = new DataLoadOptions();

Linq.book Page 432 Mercredi, 18. février 2009 7:58 07

Page 448: LINQ Language Integrated Query en C

Chapitre 14 Opérations standard sur les bases de données 433

dlo.LoadWith<Customer>(c => c.Orders);dlo.LoadWith<Customer>(c => c.CustomerCustomerDemos);db.LoadOptions = dlo;IQueryable<Customer> custs = (from c in db.Customers

where c.Country == "UK" &&c.City == "London"orderby c.CustomerIDselect c);

// Activation du loggingdb.Log = Console.Out;

foreach (Customer cust in custs){Console.WriteLine("{0} - {1}", cust.CompanyName, cust.ContactName);}

Ce listing demande au DataLoadOptions de charger immédiatement des donnéesprovenant de deux tables. Comme vous pouvez le voir, aucune d’elles n’est référencéeen dehors de la requête. Le chargement est donc immédiat, et non différé. Ce qui vanous intéresser dans les résultats du code, ce sont non pas les données extraites mais belet bien les requêtes SQL émises. Voici un sous-ensemble des résultats :

SELECT [t0].[CustomerID], [t0].[CompanyName], [t0].[ContactName],[t0].[ContactTitle], [t0].[Address], [t0].[City], [t0].[Region], [t0].[PostalCode],[t0].[Country], [t0].[Phone], [t0].[Fax], [t1].[CustomerID] AS [CustomerID2],[t1].[CustomerTypeID], (SELECT COUNT(*)FROM [dbo].[CustomerCustomerDemo] AS [t2]WHERE [t2].[CustomerID] = [t0].[CustomerID]) AS [count]FROM [dbo].[Customers] AS [t0]LEFT OUTER JOIN [dbo].[CustomerCustomerDemo] AS [t1] ON [t1].[CustomerID] =[t0].[CustomerID]WHERE ([t0].[Country] = @p0) AND ([t0].[City] = @p1)ORDER BY [t0].[CustomerID], [t1].[CustomerTypeID]•@p0: Input String (Size = 2; Prec = 0; Scale = 0) [UK]•@p1: Input String (Size = 6; Prec = 0; Scale = 0) [London]•Context: SqlProvider(Sql2005) Model: AttributedMetaModel Build: 3.5.20706.1

SELECT [t0].[OrderID], [t0].[CustomerID], [t0].[EmployeeID], [t0].[OrderDate],[t0].[RequiredDate], [t0].[ShippedDate], [t0].[ShipVia], [t0].[Freight],[t0].[ShipName], [t0].[ShipAddress], [t0].[ShipCity], [t0].[ShipRegion],[t0].[ShipPostalCode], [t0].[ShipCountry]FROM [dbo].[Orders] AS [t0]WHERE [t0].[CustomerID] = @x1•@x1: Input StringFixedLength (Size = 5; Prec = 0; Scale = 0) [AROUT]•Context: SqlProvider(Sql2005) Model: AttributedMetaModel Build: 3.5.20706.1

Around the Horn - Thomas HardySELECT [t0].[OrderID], [t0].[CustomerID], [t0].[EmployeeID], [t0].[OrderDate],[t0].[RequiredDate], [t0].[ShippedDate], [t0].[ShipVia], [t0].[Freight],[t0].[ShipName], [t0].[ShipAddress], [t0].[ShipCity], [t0].[ShipRegion],[t0].[ShipPostalCode], [t0].[ShipCountry]FROM [dbo].[Orders] AS [t0]WHERE [t0].[CustomerID] = @x1•@x1: Input StringFixedLength (Size = 5; Prec = 0; Scale = 0) [BSBEV]•Context: SqlProvider(Sql2005) Model: AttributedMetaModel Build: 3.5.20706.1

B’s Beverages - Victoria Ashworth…

Linq.book Page 433 Mercredi, 18. février 2009 7:58 07

Page 449: LINQ Language Integrated Query en C

434 LINQ to SQL Partie V

Comme vous pouvez le voir, la table des données démographiques a été jointe avec latable des clients dans la première requête. En revanche, une requête a été émise pourcharger les commandes de chaque client. Les requêtes concernant les commandes sontexécutées au référencement de chaque client, à l’intérieur de la boucle foreach. Dansles résultats, remarquez que la requête permettant de retrouver les commandes d’unclient est affichée avant que les informations du client ne soient affichées dans laconsole.

Étant donné que les tables des données démographiques et des commandes n’ont pasété référencées dans le code, en dehors de l’appel à la méthode LoadWith<T>, le charge-ment est donc immédiat et non différé. Cependant, le chargement des données démo-graphiques est "un peu plus immédiat" que celui des commandes.

Chargement immédiat d’une hiérarchie de classes associéesLa section précédente vous a montré comment charger immédiatement plusieurs clas-ses d’entité associées. Dans cette section, vous allez apprendre à charger immédiate-ment une hiérarchie de classes d’entité associées. Dans le Listing 14.11, la requête nese contente pas de charger immédiatement les différentes commandes : elle fait demême en ce qui concerne le détail des différentes commandes.

Listing 14.11 : Chargement immédiat d’une hiérarchie de classes d’entité.

Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind");

DataLoadOptions dlo = new DataLoadOptions();

dlo.LoadWith<Customer>(c => c.Orders);

dlo.LoadWith<Order>(o => o.OrderDetails);

db.LoadOptions = dlo;

IQueryable<Customer> custs = (from c in db.Customers

where c.Country == "UK" &&

c.City == "London"

orderby c.CustomerID

select c);

// Activation du logging

db.Log = Console.Out;

foreach (Customer cust in custs)

{

Console.WriteLine("{0} - {1}", cust.CompanyName, cust.ContactName);

foreach (Order order in cust.Orders)

{

Console.WriteLine(" {0} {1}", order.OrderID, order.OrderDate);

}

}

Dans ce listing, les commandes et le détail des commandes sont chargés immédia-tement.

Linq.book Page 434 Mercredi, 18. février 2009 7:58 07

Page 450: LINQ Language Integrated Query en C

Chapitre 14 Opérations standard sur les bases de données 435

Voici un extrait des informations affichées dans la console :

Ici encore, ce sont non pas les données mais les requêtes SQL qui sont intéressantes.Cette fois-ci, la requête sur les clients n’est pas liée à la table des commandes ni à latable des détails des commandes. Lors du référencement de chaque client, une requêteSQL complémentaire est émise pour lier la table des commandes à la table des détailsdes commandes. Aucune de ces deux tables n’étant référencée, leur chargement estconsidéré comme immédiat.

En observant le fonctionnement de cet exemple, vous voyez que LINQ to SQL a choisid’effectuer un lien sur la table dont le niveau hiérarchique est le plus bas ; ici, la tabledes détails des commandes.

SELECT [t0].[CustomerID], [t0].[CompanyName], [t0].[ContactName],[t0].[ContactTitle], [t0].[Address], [t0].[City], [t0].[Region], [t0].[PostalCode],[t0].[Country], [t0].[Phone], [t0].[Fax]FROM [dbo].[Customers] AS [t0]WHERE ([t0].[Country] = @p0) AND ([t0].[City] = @p1)ORDER BY [t0].[CustomerID]•@p0: Input String (Size = 2; Prec = 0; Scale = 0) [UK]•@p1: Input String (Size = 6; Prec = 0; Scale = 0) [London]•Context: SqlProvider(Sql2005) Model: AttributedMetaModel Build: 3.5.20706.1

SELECT [t0].[OrderID], [t0].[CustomerID], [t0].[EmployeeID], [t0].[OrderDate],[t0].[RequiredDate], [t0].[ShippedDate], [t0].[ShipVia], [t0].[Freight],[t0].[ShipName], [t0].[ShipAddress], [t0].[ShipCity], [t0].[ShipRegion],[t0].[ShipPostalCode], [t0].[ShipCountry], [t1].[OrderID] AS [OrderID2],[t1].[ProductID], [t1].[UnitPrice], [t1].[Quantity], [t1].[Discount], (SELECT COUNT(*)FROM [dbo].[Order Details] AS [t2]WHERE [t2].[OrderID] = [t0].[OrderID]) AS [count]FROM [dbo].[Orders] AS [t0]LEFT OUTER JOIN [dbo].[Order Details] AS [t1] ON [t1].[OrderID] = [t0].[OrderID]WHERE [t0].[CustomerID] = @x1ORDER BY [t0].[OrderID], [t1].[ProductID]•@x1: Input StringFixedLength (Size = 5; Prec = 0; Scale = 0) [AROUT]•Context: SqlProvider(Sql2005) Model: AttributedMetaModel Build: 3.5.20706.1

Around the Horn - Thomas HardySELECT [t0].[OrderID], [t0].[CustomerID], [t0].[EmployeeID], [t0].[OrderDate],[t0].[RequiredDate], [t0].[ShippedDate], [t0].[ShipVia], [t0].[Freight],[t0].[ShipName], [t0].[ShipAddress], [t0].[ShipCity], [t0].[ShipRegion],[t0].[ShipPostalCode], [t0].[ShipCountry], [t1].[OrderID] AS [OrderID2],[t1].[ProductID], [t1].[UnitPrice], [t1].[Quantity], [t1].[Discount], (SELECT COUNT(*)FROM [dbo].[Order Details] AS [t2]WHERE [t2].[OrderID] = [t0].[OrderID]) AS [count]FROM [dbo].[Orders] AS [t0]LEFT OUTER JOIN [dbo].[Order Details] AS [t1] ON [t1].[OrderID] = [t0].[OrderID]WHERE [t0].[CustomerID] = @x1ORDER BY [t0].[OrderID], [t1].[ProductID]•@x1: Input StringFixedLength (Size = 5; Prec = 0; Scale = 0) [BSBEV]•Context: SqlProvider(Sql2005) Model: AttributedMetaModel Build: 3.5.20706.1

B’s Beverages - Victoria Ashworth...

Linq.book Page 435 Mercredi, 18. février 2009 7:58 07

Page 451: LINQ Language Integrated Query en C

436 LINQ to SQL Partie V

Filtrage et classementTant que nous en sommes à discuter de la classe DataLoadOptions, nous allons prendrequelques instants pour introduire la méthode AssociateWith, qui permet de filtrer et declasser des objets enfant associés.

Dans le Listing 14.8, plusieurs clients sont extraits de la base de données, puis énumé-rés en affichant les noms des clients et les commandes qu’ils ont passées. Si vous vousreportez aux résultats, vous verrez que les dates des commandes apparaissent dans unordre chronologique inverse. Le Listing 14.12 montre comment utiliser l’opérateurAssociateWith pour filtrer et classer les classes associées.

Listing 14.12 : Utilisation de la classe DataLoadOptions pour filtrer et classer des enregistrements.

Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind");DataLoadOptions dlo = new DataLoadOptions();dlo.AssociateWith<Customer>(c => from o in c.Orderswhere o.OrderID < 10700orderby o.OrderDate descendingselect o);db.LoadOptions = dlo;IQueryable<Customer> custs = from c in db.Customerswhere c.Country == "UK" &&c.City == "London"orderby c.CustomerIDselect c;foreach (Customer cust in custs){Console.WriteLine("{0} - {1}", cust.CompanyName, cust.ContactName);foreach (Order order in cust.Orders) {Console.WriteLine(" {0} {1}", order.OrderID, order.OrderDate); }}

L’argument de la méthode AssociateWith utilise une expression lambda pour ne retenirque les enregistrements dont le champ OrderId est inférieur à 10 700 et pour classer lesrésultats par ordre décroissant sur le champ OrderDate. Voici les résultats :

Around the Horn - Thomas Hardy10558 6/4/1997 12:00:00 AM10453 2/21/1997 12:00:00 AM10383 12/16/1996 12:00:00 AM10355 11/15/1996 12:00:00 AM

B’s Beverages - Victoria Ashworth10599 7/15/1997 12:00:00 AM10578 6/24/1997 12:00:00 AM10539 5/16/1997 12:00:00 AM10538 5/15/1997 12:00:00 AM10484 3/24/1997 12:00:00 AM10471 3/11/1997 12:00:00 AM10289 8/26/1996 12:00:00 AM

Consolidated Holdings - Elizabeth Brown10462 3/3/1997 12:00:00 AM10435 2/4/1997 12:00:00 AM

Linq.book Page 436 Mercredi, 18. février 2009 7:58 07

Page 452: LINQ Language Integrated Query en C

Chapitre 14 Opérations standard sur les bases de données 437

Seuls les enregistrements dont le champ OrderId est inférieur à 10 700 sont retenus. Parailleurs, les résultats sont classés par ordre décroissant sur le champ OrderDate.

Les jointures automatiques sont une coïncidenceLes associations ont un avantage : elles effectuent des jointures automatiques. À titred’exemple, lorsqu’une requête est appliquée sur la table des clients de la base dedonnées Northwind, chaque client possède une collection de commandes accessible viala propriété Orders de l’objet Customer. L’obtention des commandes pour les différentsclients est donc automatique. La réciproque est également vraie. Dans la classe Order,chacune des commandes a une propriété Customer qui fait référence au client appro-prié. Pour accéder aux données de deux tables liées, la technique conventionnelle auraitconsisté à créer une jointure.

Lorsqu’un objet enfant a une relation avec un objet parent, il est logique de pouvoiraccéder au parent via une référence dans l’objet enfant. À titre d’exemple, lorsque l’ontravaille avec le langage XML, il paraît logique de pouvoir accéder au nœud parent d’unnœud enfant via une variable membre du nœud enfant. Qui s’attendrait à devoir effec-tuer une requête sur la structure XML et à fournir le nœud enfant comme argument dela recherche ? De même, il est logique de pouvoir accéder aux enfants d’un nœud enutilisant une référence dans le nœud parent.

La jointure automatique est certes pratique. Cependant, son implémentation est dictéepar la nature des relations entre les objets et les attentes comportementales du program-meur. De ce point de vue, le côté automatique des jointures n’est donc qu’une purecoïncidence…

JointuresNous venons de voir que bon nombre des relations d’une base de données sont défi-nies en tant qu’associations et que les objets associés sont accessibles via un membrede classe. Sachez cependant que seules les relations qui utilisent des clés étrangèresauront ce comportement. Étant donné que toutes les relations n’utilisent pas des clésétrangères, vous aurez parfois besoin d’effectuer des jointures explicites entre deuxtables.

Eastern Connection - Ann Devon10532 5/9/1997 12:00:00 AM10400 1/1/1997 12:00:00 AM10364 11/26/1996 12:00:00 AM

North/South - Simon Crowther10517 4/24/1997 12:00:00 AM

Seven Seas Imports - Hari Kumar10547 5/23/1997 12:00:00 AM10523 5/1/1997 12:00:00 AM10472 3/12/1997 12:00:00 AM10388 12/19/1996 12:00:00 AM10377 12/9/1996 12:00:00 AM10359 11/21/1996 12:00:00 AM

Linq.book Page 437 Mercredi, 18. février 2009 7:58 07

Page 453: LINQ Language Integrated Query en C

438 LINQ to SQL Partie V

Jointures internesL’opérateur join permet d’effectuer une équijointure interne. Comme il est de coutumedans ce type de jointure, les éventuels enregistrements de la table externe sont omis s’ilsne contiennent pas un enregistrement correspondant dans la table interne (voirListing 14.13).

Listing 14.13 : Jointure interne.

Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind");var entities = from s in db.Suppliersjoin c in db.Customers on s.City equals c.Cityselect new {SupplierName = s.CompanyName,CustomerName = c.CompanyName,City = c.City };

foreach (var e in entities){Console.WriteLine("{0}: {1} - {2}", e.City, e.SupplierName, e.CustomerName);}

Ce code effectue une jointure interne sur les fournisseurs et les clients. Pour un clientdonné, si aucun enregistrement fournisseur possédant la même valeur du même champcity n’est trouvé, l’enregistrement fournisseur est omis des résultats.

Comme vous pouvez le voir, certains fournisseurs apparaissent à plusieurs reprises, etd’autres n’apparaissent pas du tout. Les fournisseurs omis sont ceux pour lesquelsaucun client ne se trouve dans la même ville. Pour afficher tous les fournisseurs, sanstenir compte de l’existence d’un client dans la même ville, nous devons utiliser unejointure externe.

Jointures externesAu Chapitre 4, nous avons étudié l’opérateur de requête standard DefaultIfEmpty etindiqué qu’il pouvait être utilisé pour effectuer une jointure externe. Dans le

London: Exotic Liquids - Around the HornLondon: Exotic Liquids - B’s BeveragesLondon: Exotic Liquids - Consolidated HoldingsLondon: Exotic Liquids - Eastern ConnectionLondon: Exotic Liquids - North/SouthLondon: Exotic Liquids - Seven Seas ImportsSao Paulo: Refrescos Americanas LTDA - Comércio MineiroSao Paulo: Refrescos Americanas LTDA - Familia ArquibaldoSao Paulo: Refrescos Americanas LTDA - Queen CozinhaSao Paulo: Refrescos Americanas LTDA - Tradiçao HipermercadosBerlin: Heli Süßwaren GmbH & Co. KG - Alfred FutterkisteParis: Aux joyeux ecclésiastiques - Paris spécialitésParis: Aux joyeux ecclésiastiques - Spécialités du mondeMontréal: Ma Maison - Mère Paillarde

Linq.book Page 438 Mercredi, 18. février 2009 7:58 07

Page 454: LINQ Language Integrated Query en C

Chapitre 14 Opérations standard sur les bases de données 439

Listing 14.14, nous allons utiliser la clause into pour placer les résultats de la jointuredans une séquence temporaire sur laquelle nous appellerons l’opérateur DefaultI-fEmpty. De la sorte, si l’enregistrement ne se trouve pas dans les résultats joints, il seraremplacé par une valeur par défaut. Nous allons utiliser la fonctionnalité de log duDataContext afin d’afficher la déclaration SQL correspondante.

Listing 14.14 : Une jointure externe.

Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind");db.Log = Console.Out;var entities =from s in db.Suppliersjoin c in db.Customers on s.City equals c.City into tempfrom t in temp.DefaultIfEmpty()select new {SupplierName = s.CompanyName,CustomerName = t.CompanyName,City = s.City };foreach (var e in entities){Console.WriteLine("{0}: {1} - {2}", e.City, e.SupplierName, e.CustomerName);}

Les résultats de la clause join sont redirigés temporairement dans la séquence temp (lenom de la séquence peut être tout autre, à condition qu’il n’entre pas en conflit avec unautre nom de variable ou un mot-clé). L’opérateur DefaultIfEmpty est appliqué aurésultat de la jointure. Cet opérateur n’a pas encore été étudié. Mais sachez qu’il diffèrede l’opérateur de même nom étudié au Chapitre 4 de cet ouvrage. Comme vous leverrez dans quelques lignes, les requêtes LINQ to SQL sont transformées en déclara-tions SQL, elles-mêmes exécutées dans la base de données. SQLServer ne disposantd’aucun moyen pour appeler l’opérateur de requête standard DefaultIfEmpty, cedernier est transformé en une déclaration SQL équivalente. C’est pour mettre enévidence cette transformation que nous avons affecté la valeur Console.out à lapropriété Log du DataContext.

Une nouvelle requête est appliquée au résultat de l’opérateur DefaultIfEmpty. Remar-quez que le nom de la ville provient de la table Suppliers, et non de la collection temp.Ceci parce que nous savons qu’il y a toujours un enregistrement correspondant dans latable Suppliers. En revanche, dans le cas d’un fournisseur pour lequel aucun client n’aun champ City identique, le champ City est absent de la collection temp. Cet exemplediffère du précédent, pour lequel le champ City était obtenu à partir de la jointure. Danscet exemple, la table depuis laquelle le champ City était obtenu importait peu car, danstous les cas, si aucun client ne correspondait à un fournisseur, il n’aurait pas été inclusdans les résultats, puisque la jointure était interne.

Linq.book Page 439 Mercredi, 18. février 2009 7:58 07

Page 455: LINQ Language Integrated Query en C

440 LINQ to SQL Partie V

Voici les résultats du Listing 14.14 :

Comme vous pouvez le voir, chaque fournisseur a au moins un enregistrement, maiscertains fournisseurs ne sont associés à aucun client. Ceci prouve que la jointure estexterne. Si vous en doutez encore, jetez un œil à la déclaration SQL…

Aplatir ou ne pas aplatir ?Dans les Listings 14.13 et 14.14, les résultats des requêtes ont été projetés sur unestructure "aplatie". En d’autres termes, nous avons créé un objet en utilisant une classeanonyme dans laquelle chacun des champs demandés est un membre de cette classeanonyme. Une autre approche aurait consisté à créer une classe anonyme composéed’objets Supplier et des objets Customer correspondants.

SELECT [t0].[CompanyName], [t1].[CompanyName] AS [value], [t0].[City]FROM [dbo].[Suppliers] AS [t0]LEFT OUTER JOIN [dbo].[Customers] AS [t1] ON [t0].[City] = [t1].[City]•Context: SqlProvider(Sql2005) Model: AttributedMetaModel Build: 3.5.20706.1London: Exotic Liquids - Around the HornLondon: Exotic Liquids - B’s BeveragesLondon: Exotic Liquids - Consolidated HoldingsLondon: Exotic Liquids - Eastern ConnectionLondon: Exotic Liquids - North/SouthLondon: Exotic Liquids - Seven Seas ImportsNew Orleans: New Orleans Cajun Delights -Ann Arbor: Grandma Kelly’s Homestead -Tokyo: Tokyo Traders -Oviedo: Cooperativa de Quesos ’Las Cabras’ -Osaka: Mayumi’s -Melbourne: Pavlova, Ltd. -Manchester: Specialty Biscuits, Ltd. -Göteborg: PB Knäckebröd AB -Sao Paulo: Refrescos Americanas LTDA - Comércio MineiroSao Paulo: Refrescos Americanas LTDA - Familia ArquibaldoSao Paulo: Refrescos Americanas LTDA - Queen CozinhaSao Paulo: Refrescos Americanas LTDA - Tradiçao HipermercadosBerlin: Heli Süßwaren GmbH & Co. KG - Alfreds FutterkisteFrankfurt: Plutzer Lebensmittelgroßmärkte AG -Cuxhaven: Nord-Ost-Fisch Handelsgesellschaft mbH -Ravenna: Formaggi Fortini s.r.l. -Sandvika: Norske Meierier -Bend: Bigfoot Breweries -Stockholm: Svensk Sjöföda AB -Paris: Aux joyeux ecclésiastiques - Paris spécialitésParis: Aux joyeux ecclésiastiques - Spécialités du mondeBoston: New England Seafood Cannery -Singapore: Leka Trading -Lyngby: Lyngbysild -Zaandam: Zaanse Snoepfabriek -Lappeenranta: Karkki Oy -Sydney: G’day, Mate -Montréal: Ma Maison - Mère PaillardeSalerno: Pasta Buttini s.r.l. -Montceau: Escargots Nouveaux -Annecy: Gai pâturage -Ste-Hyacinthe: Forêts d’érables -

Linq.book Page 440 Mercredi, 18. février 2009 7:58 07

Page 456: LINQ Language Integrated Query en C

Chapitre 14 Opérations standard sur les bases de données 441

Si nous adoptions l’approche "aplatie", comme dans les deux exemples précédents,étant donné que la classe de sortie n’est pas une classe d’entité, il ne serait pas possiblede modifier les objets et de demander à l’objet DataContext de sauvegarder ces modifi-cations dans la base de données. L’approche "aplatie" ne convient que si les données nesont pas modifiées.

Parfois, il peut être nécessaire de modifier les objets obtenus en sortie. Dans ce cas,vous devrez adopter une approche "non aplatie". Si vous modifiez les objets obtenus,l’objet DataContext pourra sauvegarder les modifications dans la base de données. LeListing 14.15 donne un exemple de résultats non aplatis. Pour avoir des informationscomplémentaires sur les projections non aplaties, consultez le Chapitre 16.

Listing 14.15 : Les résultats n’étant pas aplatis, ils peuvent donc être rendus persistants via le DataContext.

Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind");var entities = from s in db.Suppliersjoin c in db.Customers on s.City equals c.City into tempfrom t in temp.DefaultIfEmpty()select new { s, t };foreach (var e in entities){Console.WriteLine("{0}: {1} - {2}", e.s.City,e.s.CompanyName,e.t != null ? e.t.CompanyName : "");}

Dans cet exemple, les résultats de la requête ne sont pas retournés dans un objet anonymeaplati contenant un membre pour chaque champ : ils sont placés dans un objet ano-nyme composé des objets d’entité Supplier, et éventuellement Customer.

Dans la dernière ligne de l’instruction Console.WriteLine, un test est effectué poursavoir si la valeur temporaire du champ CompanyName est nulle. Dans ce cas, aucunclient ne correspond au fournisseur et rien n’est affiché. Voici les résultats :

London: Exotic Liquids - Around the HornLondon: Exotic Liquids - B’s BeveragesLondon: Exotic Liquids - Consolidated HoldingsLondon: Exotic Liquids - Eastern ConnectionLondon: Exotic Liquids - North/SouthLondon: Exotic Liquids - Seven Seas ImportsNew Orleans: New Orleans Cajun Delights -Ann Arbor: Grandma Kelly’s Homestead -Tokyo: Tokyo Traders -Oviedo: Cooperativa de Quesos ’Las Cabras’ -Osaka: Mayumi’s -Melbourne: Pavlova, Ltd. -Manchester: Specialty Biscuits, Ltd. -Göteborg: PB Knäckebröd AB -Sao Paulo: Refrescos Americanas LTDA - Comércio MineiroSao Paulo: Refrescos Americanas LTDA - Familia ArquibaldoSao Paulo: Refrescos Americanas LTDA - Queen CozinhaSao Paulo: Refrescos Americanas LTDA - Tradiçao HipermercadosBerlin: Heli Süßwaren GmbH & Co. KG - Alfreds Futterkiste

Linq.book Page 441 Mercredi, 18. février 2009 7:58 07

Page 457: LINQ Language Integrated Query en C

442 LINQ to SQL Partie V

En observant ces résultats, vous voyez que certains fournisseurs n’ont aucun client dansleur ville. Contrairement à la séquence d’objets anonymes retournée par la requête duListing 14.14, les objets anonymes retournés par la requête du Listing 14.15 contien-nent des objets entité de type Supplier et Customer. Il est donc possible de tirer partides services afférents. En particulier, ces objets peuvent être modifiés et les modifications,rendues persistantes via le DataContext.

Exécution de requêtes différéesUn petit rappel pour ceux de nos lecteurs qui n’auraient pas lu assez attentivement lechapitre dédié aux requêtes différées.

Une requête LINQ to SQL, LINQ to XML ou LINQ to Objects différée ne s’exécutepas au moment où elle est définie. Prenons l’exemple de la requête suivante :

IQueryable<Customer> custs = from c in db.Customerswhere c.Country == "UK"select c;

Cette requête n’est pas exécutée lorsque la déclaration est exécutée. Elle est juste affec-tée à la variable custs. La requête sera exécutée à l’énumération de la séquence.Plusieurs conséquences en découlent.

Conséquences de l’exécution différée des requêtesPremière conséquence. Si une requête différée contient des erreurs, celles-ci serontdétectées lors de son exécution (et non de sa définition) et produiront des exceptions.Ceci peut se révéler trompeur, en particulier si le débogueur ne détecte aucune erreurdans la requête et que l’exception se produit bien plus loin dans le code, lors del’énumération de la séquence ou lorsque vous appliquez un opérateur à la séquence quiprovoque son énumération.

Frankfurt: Plutzer Lebensmittelgroßmärkte AG -Cuxhaven: Nord-Ost-Fisch Handelsgesellschaft mbH -Ravenna: Formaggi Fortini s.r.l. -Sandvika: Norske Meierier -Bend: Bigfoot Breweries -Stockholm: Svensk Sjöföda AB -Paris: Aux joyeux ecclésiastiques - Paris spécialitésParis: Aux joyeux ecclésiastiques - Spécialités du mondeBoston: New England Seafood Cannery -Singapore: Leka Trading -Lyngby: Lyngbysild -Zaandam: Zaanse Snoepfabriek -Lappeenranta: Karkki Oy -Sydney: G’day, Mate -Montréal: Ma Maison - Mère PaillardeSalerno: Pasta Buttini s.r.l. -Montceau: Escargots Nouveaux -Annecy: Gai pâturage -Ste-Hyacinthe: Forêts d’érables –

Linq.book Page 442 Mercredi, 18. février 2009 7:58 07

Page 458: LINQ Language Integrated Query en C

Chapitre 14 Opérations standard sur les bases de données 443

Seconde conséquence. Une requête SQL différée étant exécutée lors de l’énumérationde la séquence, plusieurs énumérations provoqueront plusieurs exécutions de larequête. Ceci peut être désastreux en termes de performances. Pour prévenir ceproblème, il suffit d’appliquer un opérateur de requête standard de conversion (tel queToArray<T>, ToList<T>, ToDictionary<T, K> ou ToLookup<T, K>) à la séquence. Cesopérateurs convertissent la séquence en une structure du type spécifié et la placent dansune "mémoire tampon" qui pourra être énumérée autant de fois que nécessaire sans quela requête SQL doive être exécutée à plusieurs reprises.

Tirer avantage de l’exécution différée des requêtesPremier avantage. Étant donné qu’une requête différée est exécutée à chaque fois quela séquence est énumérée, vous pouvez la définir une fois et l’exécuter autant de foisque nécessaire, lorsque la situation le justifie. Si le code n’examine pas les résultats dela requête, la requête SQL n’est pas exécutée. Les performances sont donc améliorées.

Second avantage. Étant donné que la requête n’est pas exécutée lors de sa définition,vous pouvez, si cela se révèle nécessaire, lui ajouter des opérateurs a fortiori. Imaginezune application qui permette d’effectuer des requêtes sur une table Customers. Suppo-sons que l’utilisateur puisse filtrer les clients issus de la requête. Imaginez une interfacede filtrage qui inclut une liste déroulante pour chaque colonne de la table Customer.Ainsi, vous pourriez disposer d’une liste déroulante pour la colonne City et d’une autrepour la colonne Country. La première donnerait accès aux villes de tous les clients de labase de données et la seconde, aux pays de tous les clients de la base de données. Lapremière option de chaque liste déroulante aurait pour valeur [TOUS] et serait sélection-née par défaut. Si l’utilisateur ne change pas les réglages par défaut des listes déroulan-tes City et Country, aucune clause where n’est ajoutée à la requête. Le Listing 14.16montre comment fabriquer une requête à partir d’une telle interface.

Listing 14.16 : Création d’une requête par programmation.

Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind");// Activation du loggingdb.Log = Console.Out;

// Simulation de valeurs sélectionnées dans des listes déroulantesstring dropdownListCityValue = "Cowes";string dropdownListCountryValue = "UK";IQueryable<Customer> custs = (from c in db.Customersselect c);if (!dropdownListCityValue.Equals("[TOUS]")){custs = from c in custswhere c.City == dropdownListCityValueselect c;}

if (!dropdownListCountryValue.Equals("[TOUS]")){custs = from c in custswhere c.Country == dropdownListCountryValue

Linq.book Page 443 Mercredi, 18. février 2009 7:58 07

Page 459: LINQ Language Integrated Query en C

444 LINQ to SQL Partie V

select c;}

foreach (Customer cust in custs){Console.WriteLine("{0} - {1} - {2}", cust.CompanyName, cust.City, cust.Country);}

Dans ce listing, nous simulons les listes déroulantes City et Country. Si l’une d’entreelles n’a pas la valeur [TOUS], un opérateur where est ajouté à la fin de la requête. Étantdonné que la requête n’est pas exécutée jusqu’à l’énumération de la séquence, il estpossible de la construire en plusieurs étapes.

Voici les résultats de ce code :

Étant donné que la liste déroulante dropdownListCityValue a pour valeur "Cowes" etque la liste déroulante dropdownListCountryValue a pour valeur "UK", nous obtenonsles enregistrements des clients qui résident à Cowes, au Royaume-Uni. Remarquezqu’une seule requête SQL est utilisée. Étant donné que l’exécution de la requête estdifférée jusqu’à l’énumération de la séquence, il est possible de lui ajouter des clausesrestrictives ou de tri sans pour autant devoir mettre en place plusieurs requêtes SQL.

Vous pouvez voir que les deux critères du filtre (la ville et le pays) apparaissent dans laclause where de la requête SQL exécutée.

Nous allons donner un autre exemple. Dans le Listing 14.17, nous allons affecter lavaleur "[TOUS]" à la variable dropdownListCityValue, afin de voir l’allure de la décla-ration SQL et des résultats qui en découlent. Étant donné que la valeur par défaut"[TOUS]" est spécifiée, aucune restriction ne devrait être opérée sur la ville dans larequête SQL.

Listing 14.17 : Construction d’une autre requête par programme.

Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind");// Activation du loggingdb.Log = Console.Out;

// Simulation de valeurs sélectionnées dans des listes déroulantesstring dropdownListCityValue = "[ALL]";string dropdownListCountryValue = "UK";IQueryable<Customer> custs = (from c in db.Customersselect c);if (!dropdownListCityValue.Equals("[ALL]"))

SELECT [t0].[CustomerID], [t0].[CompanyName], [t0].[ContactName],[t0].[ContactTitle], [t0].[Address], [t0].[City], [t0].[Region], [t0].[PostalCode],[t0].[Country], [t0].[Phone], [t0].[Fax]FROM [dbo].[Customers] AS [t0]WHERE ([t0].[Country] = @p0) AND ([t0].[City] = @p1)•@p0: Input String (Size = 2; Prec = 0; Scale = 0) [UK]•@p1: Input String (Size = 5; Prec = 0; Scale = 0) [Cowes]•Context: SqlProvider(Sql2005) Model: AttributedMetaModel Build: 3.5.20706.1

Island Trading - Cowes - UK

Linq.book Page 444 Mercredi, 18. février 2009 7:58 07

Page 460: LINQ Language Integrated Query en C

Chapitre 14 Opérations standard sur les bases de données 445

{custs = from c in custswhere c.City == dropdownListCityValueselect c;}

if (!dropdownListCountryValue.Equals("[ALL]")){custs = from c in custswhere c.Country == dropdownListCountryValueselect c;}

foreach (Customer cust in custs){Console.WriteLine("{0} - {1} - {2}", cust.CompanyName, cust.City, cust.Country);}

Voici les résultats :

Comme vous le voyez, la clause where de la déclaration SQL ne spécifie plus la condi-tion restrictive portant sur le champ City. En conséquence, les clients affichés habitentdifférentes villes britanniques.

Bien entendu, il est toujours possible d’ajouter un appel à l’opérateur de requête stan-dard ToArray<T>, ToList<T>, ToDictionary<T, K> ou ToLookup<T, K> pour forcerl’exécution immédiate de la requête.

J’espère que vous êtes maintenant convaincu que les requêtes différées peuvent être trèsutiles et que le Log du DataContext est une précieuse source de renseignements.

L’opérateur Contains en lieu et place de la déclaration SQL INLes premières versions de LINQ to SQL n’implémentaient pas la déclaration SQL IN.Voici un exemple d’une telle déclaration :

SELECT *FROM CustomersWHERE (City IN (’London’, ’Madrid’))

Pour combler ce manque, Microsoft a défini l’opérateur Contains. Cet opérateur estquelque peu différent de la déclaration SQL IN. Par son intermédiaire, il aurait été

SELECT [t0].[CustomerID], [t0].[CompanyName], [t0].[ContactName],[t0].[ContactTitle], [t0].[Address], [t0].[City], [t0].[Region], [t0].[PostalCode],[t0].[Country], [t0].[Phone], [t0].[Fax]FROM [dbo].[Customers] AS [t0]WHERE [t0].[Country] = @p0•@p0: Input String (Size = 2; Prec = 0; Scale = 0) [UK]•Context: SqlProvider(Sql2005) Model: AttributedMetaModel Build: 3.5.20706.1

Around the Horn - London - UKB’s Beverages - London - UKConsolidated Holdings - London - UKEastern Connection - London - UKIsland Trading - Cowes - UKNorth/South - London - UKSeven Seas Imports - London - UK

Linq.book Page 445 Mercredi, 18. février 2009 7:58 07

Page 461: LINQ Language Integrated Query en C

446 LINQ to SQL Partie V

logique de pouvoir énoncer qu’un membre d’une classe d’entité doit se trouver dans unensemble de valeurs. Cependant, comme vous pourrez le constater dans leListing 14.18, cet opérateur fonctionne d’une façon diamétralement opposée.

Listing 14.18 : L’opérateur Contains.

Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind");db.Log = Console.Out;string[] cities = { "London", "Madrid" };IQueryable<Customer> custs = db.Customers.Where(c => cities.Contains(c.City));foreach (Customer cust in custs){Console.WriteLine("{0} - {1}", cust.CustomerID, cust.City);}

Plutôt que d’indiquer que le champ City de la table Customer doit se trouver dans unensemble de valeurs, l’instruction en gras de cette requête indique qu’un ensemble devaleurs doit contenir le champ City. Dans cet exemple, nous définissons un tableau devilles nommé cities. La requête appelle l’opérateur Contains sur le tableau cities enlui passant le champ City de la table Customer. Si le tableau cities contient la ville duclient, la valeur true est retournée à l’opérateur Where, et l’objet Customer est inclusdans la séquence de sortie.

Voici les résultats de ce listing :

En observant la déclaration SQL, vous voyez que l’opérateur Contains a été transforméen une déclaration SQL IN.

Mises à jour

Avec LINQ to SQL, la mise à jour d’une base de données revient à modifier les proprié-tés d’un objet, à appeler la méthode SubmitChanges de l’objet DataContext et à gérerd’éventuels conflits d’accès concurrentiel. Que ce dernier point ne vous intimide pas

SELECT [t0].[CustomerID], [t0].[CompanyName], [t0].[ContactName],[t0].[ContactTitle], [t0].[Address], [t0].[City], [t0].[Region], [t0].[PostalCode],[t0].[Country], [t0].[Phone], [t0].[Fax]FROM [dbo].[Customers] AS [t0]WHERE [t0].[City] IN (@p0, @p1)•@p0: Input String (Size = 6; Prec = 0; Scale = 0) [London]•@p1: Input String (Size = 6; Prec = 0; Scale = 0) [Madrid]•Context: SqlProvider(Sql2005) Model: AttributedMetaModel Build: 3.5.20706.1

AROUT - LondonBOLID - MadridBSBEV - LondonCONSH - LondonEASTC - LondonFISSA - MadridNORTS - LondonROMEY - MadridSEVES – London

Linq.book Page 446 Mercredi, 18. février 2009 7:58 07

Page 462: LINQ Language Integrated Query en C

Chapitre 14 Opérations standard sur les bases de données 447

outre mesure : les différentes options qui vous sont offertes sont loin d’être hors deportée. Nous y reviendrons en détail au Chapitre 17.

Pour gérer facilement les conflits d’accès concurrentiel, les classes d’entité doivent êtreconvenablement mappées à la base de données et le graphe correspondant doit être cohé-rent. Pour avoir plus d’informations sur le mappage des classes d’entité à la base dedonnées, consultez la section "Attributs des classes d’entité et propriétés des attributs" duChapitre 15. Pour avoir des informations complémentaires sur la cohérence du graphe,reportez-vous à la section "Cohérence du graphe" du Chapitre 15. SQLMetal et le Concep-teur Objet/Relationnel vous fourniront tout ce dont vous avez besoin pour que ces deuxconditions soient respectées : il vous suffit de les laisser créer vos classes d’entité.

Mise à jour d’une référence parent d’un enfantÀ titre d’exemple, le Listing 14.19 vous montre comment modifier l’employé à l’origined’une commande dans la base de données Northwind. Cet exemple est assez complexe,c’est pourquoi nous donnerons des explications à chaque fois que cela sera nécessaire.

Listing 14.19 : Modification d’une relation en affectant un nouveau parent.

Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind");Order order = (from o in db.Orders

where o.EmployeeID == 5orderby o.OrderDate descendingselect o).First<Order>();

// Mémorisation de l’employé pour pouvoir le restaurer à la fin du listingEmployee origEmployee = order.Employee;

La première ligne obtient le DataContext de la base de données. Le bloc d’instructionssuivant obtient la commande la plus récente de l’employé dont le champ EmployeeIDvaut 5. Pour ce faire, les commandes sont classées par dates décroissantes et l’opérateurFirst est appelé. La dernière instruction sauvegarde la référence de l’employé dans lavariable origEmployee. Cette référence pourra ainsi être restaurée à la fin du listing.

Console.WriteLine("Avant la modification de l’employé");Console.WriteLine("OrderID = {0} : OrderDate = {1} : EmployeeID = {2}",order.OrderID, order.OrderDate, order.Employee.EmployeeID);

La première ligne affiche un message dans la console indiquant que l’employé n’a pasencore été modifié. Les deux lignes suivantes affichent le numéro de la commande, ladate et l’identifiant de l’employé qui en est à l’origine.

Employee emp = (from e in db.Employeeswhere e.EmployeeID == 9select e).Single<Employee>();

// Affectation d’un nouvel employé à la commandeorder.Employee = emp;db.SubmitChanges();

Le premier bloc d’instructions extrait l’employé dont le champ EmployeeID vaut 9. Cetemployé est alors affecté à la commande obtenue par la première requête et la modificationest mémorisée dans la base de données avec la méthode SubmitChanges.

Linq.book Page 447 Mercredi, 18. février 2009 7:58 07

Page 463: LINQ Language Integrated Query en C

448 LINQ to SQL Partie V

Pour prouver que la modification a été effectuée des deux côtés de la relation, nouspourrions afficher la valeur de la propriété EmployeeID de l’objet order. Mais celan’aurait pas beaucoup de sens puisque, deux lignes plus haut, l’objet order a été modi-fié dans ce sens. Par ailleurs, cela ne prouverait pas que la modification a été répercutéedans la table Employees. Nous allons donc récupérer la commande qui vient d’êtremodifiée dans la collection de commandes de l’employé en utilisant une nouvellerequête.

Order order2 = (from o in emp.Orderswhere o.OrderID == order.OrderIDselect o).First<Order>();

Cette requête retrouve la commande par son numéro (order.OrderID). Si cette commandeexiste, cela prouvera que la mise à jour a bien été répercutée sur la table Employees.

Console.WriteLine("{0}Après la modification de l’employé", System.Environment.NewLine);Console.WriteLine("OrderID = {0} : OrderDate = {1} : EmployeeID = {2}",order2.OrderID, order2.OrderDate, order2.Employee.EmployeeID);

La première ligne affiche un message indiquant que l’employé a été modifié. Les deuxlignes suivantes affichent le numéro de la commande, la date et l’identifiant del’employé qui en est à l’origine. Si tout a bien fonctionné, l’identifiant de l’employédevrait être égal à 9 (il valait 5 avant la modification).

// Annulation de la modification pour permettre l’exécution multiple du programmeorder.Employee = origEmployee;db.SubmitChanges();

Ces dernières lignes restaurent les données originales dans la base de données. Ainsi, leprogramme pourra s’exécuter plusieurs fois en donnant toujours les mêmes résultats :

Tout a fonctionné comme prévu : le champ EmployeeID de la table Orders est passé de5 à 9. Après la modification, le code ne s’est pas contenté d’afficher les résultats à partirde la variable order : la commande modifiée a été retrouvée dans la base de données, àpartir de l’employé dont le champ EmployeeID avait pour valeur 9. Ceci prouve que lamodification a bien eu lieu dans la table Employee.

Dans cet exemple, nous avons mis à jour la référence parent (employé) d’un objetenfant (commande). Une approche diamétralement opposée aurait permis d’arriver aumême résultat : nous aurions pu mettre à jour la référence enfant (commande) d’unobjet parent (employé).

Mise à jour d’une référence enfant d’un parentPour changer la relation entre deux objets, une autre approche consiste à enlever l’objetenfant de la collection EntitySet<T> de l’objet parent, puis de l’ajouter dans une

Avant la modification de l’employéOrderID = 11043 : OrderDate = 4/22/1998 12:00:00 AM : EmployeeID = 5Après la modification de l’employéOrderID = 11043 : OrderDate = 4/22/1998 12:00:00 AM : EmployeeID = 9

Linq.book Page 448 Mercredi, 18. février 2009 7:58 07

Page 464: LINQ Language Integrated Query en C

Chapitre 14 Opérations standard sur les bases de données 449

collection EntitySet<T> différente de l’objet parent. Dans le Listing 14.20, noussupprimons une commande dans la collection de commandes d’un employé. Cet exem-ple étant semblable à celui du Listing 14.19, nous serons plus avares en explications.Les principales modifications entre les deux listings apparaîtront en gras.

Listing 14.20 : Modification d’une relation en supprimant puis en ajoutant un enfant à une collection EntitySet du parent.

Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind");

Order order = (from o in db.Orderswhere o.EmployeeID == 5orderby o.OrderDate descendingselect o).First<Order>();

// Mémorisation de l’employé pour pouvoir le restaurer à la fin du listingEmployee origEmployee = order.Employee;

Console.WriteLine("Avant la modification de l’employé");Console.WriteLine("OrderID = {0} : OrderDate = {1} : EmployeeID = {2}",order.OrderID, order.OrderDate, order.Employee.EmployeeID);

Employee emp = (from e in db.Employeeswhere e.EmployeeID == 9select e).Single<Employee>();

// Suppression de la commande dans les commandes de l’employé originalorigEmployee.Orders.Remove(order);

// Ajout de la commande dans les commandes du nouvel employéemp.Orders.Add(order);

db.SubmitChanges();

Console.WriteLine("{0}Après la modification de l’employé", ➥System.Environment.NewLine);Console.WriteLine("OrderID = {0} : OrderDate = {1} : EmployeeID = {2}",order.OrderID, order.OrderDate, order.Employee.EmployeeID);

// Annulation de la modification pour permettre l’exécution multiple du programmeorder.Employee = origEmployee;db.SubmitChanges();

Après avoir retrouvé la commande la plus récente de l’employé dont la propriétéEmployeeID vaut 5, nous la sauvegardons dans l’objet origEmployee pour pouvoir larestaurer à la fin du listing. Cette commande est alors affichée, avant la modification del’employé. L’employé dont la propriété EmployeeID vaut 9 est alors récupéré, puis saréférence est mémorisée dans la variable emp. À ce point précis, le code est identique àcelui du Listing 14.19.

La commande est supprimée de la collection de commandes de l’employé numéro 5puis ajoutée à la collection de commandes de l’employé numéro 9. La méthodeSubmitChanges est alors appelée pour sauvegarder les modifications dans la base dedonnées. Une fois la modification effectuée, la commande est affichée dans la console,puis l’état original de la base de données est restauré, de telle sorte que le programmepuisse être exécuté plusieurs fois.

Linq.book Page 449 Mercredi, 18. février 2009 7:58 07

Page 465: LINQ Language Integrated Query en C

450 LINQ to SQL Partie V

Voici les résultats affichés dans la console :

Suppressions

Pour supprimer un enregistrement dans une base de données en utilisant LINQ to SQL,vous devez supprimer l’objet entité du Table<T> dont il est membre en utilisant laméthode DeleteOnSubmit de l’objet Table<T>. Ensuite, vous devez appeler la méthodeSubmitChanges (voir Listing 14.21).

ATTENTIONATTENTION

Contrairement aux deux autres exemples de ce chapitre, l’état initial de la base de donnéesne sera pas restauré. Ceci parce qu’une des tables utilisées contient une colonne d’identitéet que cette colonne ne peut pas être facilement restaurée par programme. Avant d’exécu-ter cet exemple, assurez-vous que vous avez effectué une sauvegarde de votre base dedonnées. Vous pourrez ainsi facilement la restaurer. Si vous avez téléchargé la version ZIP dela base de données étendue Northwind, vous pouvez aussi utiliser le contenu du fichiercompressé pour restaurer la base de données dans son état original.

Listing 14.21 : Suppression d’un enregistrement en agissant sur le Table<T> dont il est membre.

Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind");

// Récupération du client à supprimerCustomer customer = (from c in db.Customerswhere c.CompanyName == "Alfreds Futterkiste"select c).Single<Customer>();

db.OrderDetails.DeleteAllOnSubmit(customer.Orders.SelectMany(o => o.OrderDetails));db.Orders.DeleteAllOnSubmit(customer.Orders);db.Customers.DeleteOnSubmit(customer);

db.SubmitChanges();

Customer customer2 = (from c in db.Customers where c.CompanyName == "Alfreds Futterkiste" select c).SingleOrDefault<Customer>();

Console.WriteLine("Le client {0} trouvé.", customer2 != null ? "a été" : "n’a pas été");

Cet exemple est assez simple, mais nous allons nous arrêter sur quelques points dedétail intéressants.

La table Order contenant une clé étrangère qui la lie à la table Customer, il n’est paspossible de supprimer un client sans avoir supprimé au préalable ses commandes.La table OrderDetails contenant une clé étrangère qui la lie à la table Orders, il n’est paspossible de supprimer une commande sans avoir au préalable supprimé les enregistrements

Avant la modification de l’employéOrderID = 11043 : OrderDate = 4/22/1998 12:00:00 AM : EmployeeID = 5Après la modification de l’employéOrderID = 11043 : OrderDate = 4/22/1998 12:00:00 AM : EmployeeID = 9

Linq.book Page 450 Mercredi, 18. février 2009 7:58 07

Page 466: LINQ Language Integrated Query en C

Chapitre 14 Opérations standard sur les bases de données 451

correspondants dans la table OrderDetails. Pour pouvoir supprimer un client, il fautdonc supprimer les enregistrements de la table OrderDetails puis les enregistrementsde la table Orders qui correspondent à toutes ses commandes.

Grâce à l’opérateur DeleteAllOnSubmit (qui peut supprimer une séquence de comman-des), la suppression des commandes est élémentaire. La suppression des commandesdétaillées est un peu plus complexe. Bien entendu, il serait possible d’énumérer lesdifférentes commandes et d’appeler l’opérateur DeleteAllOnSubmit sur les séquencescorrespondantes dans la table OrderDetail. Mais, ici, nous allons utiliser l’opérateurSelectMany pour obtenir une séquence composée de séquences de commandesdétaillées. Cette séquence sera alors passée à l’opérateur DeleteAllOnSubmit. Avouezque LINQ permet bien des prouesses !

Après avoir supprimé les commandes détaillées, les commandes et le client, la méthodeSubmitChanges est appelée pour sauvegarder les modifications dans la base de données.

Voici le résultat affiché dans la console :

Cette sortie écran prouve que le client a bel et bien été supprimé. Bien qu’initialementconçu pour montrer comment supprimer un objet entité, cet exemple a également révéléla puissance de l’opérateur SelectMany.

INFO

Rappelez-vous que cet exemple ne restaure pas l’état original de la base de données. Larestauration devra se faire manuellement.

Suppression d’objets entité attachésAlors que l’insertion d’un objet parent attaché à un objet entité dépendant provoquel’insertion automatique de ce dernier dans la base de données (voir Listing 14.3), cetautomatisme n’est plus d’actualité en ce qui concerne la suppression d’un objet parent(ici, le mot "dépendant" se réfère aux objets entité qui contiennent une clé étrangère).Ceci a été illustré dans le Listing 14.21, où les enregistrements de la table Orderde-tails devaient être supprimés avant les enregistrements de la table Orders, et les enre-gistrements de la table Orders avant ceux de la table Customers.

Dans la base de données Northwind, si vous tentez de supprimer une commande, lesenregistrements correspondants dans la table OrderDetails ne seront pas automatique-ment supprimés. De plus, vous provoquerez une violation de clé étrangère. Veillez doncà supprimer tous les objets entité enfant associés avant de supprimer un objet entité.

Les Listings 14.21 et 14.3 illustrent ce principe. Dans chacun d’entre eux, les objetsentité attachés ont été préalablement supprimés afin que leurs parents ne puissent l’être.

Le client n’a pas été trouvé.

Linq.book Page 451 Mercredi, 18. février 2009 7:58 07

Page 467: LINQ Language Integrated Query en C

452 LINQ to SQL Partie V

Suppression de relationsPour supprimer une relation entre deux objets entité dans LINQ to SQL, il suffit de réaf-fecter cette relation à un autre objet ou de lui affecter la valeur null. Dans le second cas,la relation entre les deux objets entité est perdue. Cela ne signifie aucunement quel’enregistrement est supprimé. Si c’est ce que vous souhaitez faire, vous devez suppri-mer les objets entité correspondants dans l’objet Table<T> approprié. Le Listing 14.22donne un exemple de suppression d’une relation.

Listing 14.22 : Suppression d’une relation entre deux objets entité.

Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind");

// Obtention de la commande pour laquelle la relation doit être suppriméeOrder order = (from o in db.Orders

where o.OrderID == 11043select o).Single<Order>();

// Sauvegarde du client pour pouvoir le restaurer à la fin du listingCustomer c = order.Customer;Console.WriteLine("Les commandes avant la suppression de la relation :");foreach (Order ord in c.Orders){Console.WriteLine("OrderID = {0}", ord.OrderID);}

// Suppression de la relation avec le clientorder.Customer = null;db.SubmitChanges();Console.WriteLine("{0}Les commandes après la suppression de la relation :",System.Environment.NewLine);foreach (Order ord in c.Orders){Console.WriteLine("OrderID = {0}", ord.OrderID);}

// Restauration de la base de données à son état originalorder.Customer = c;db.SubmitChanges();

Après avoir obtenu la commande dont le champ OrderID vaut 11043, le client corres-pondant à la commande est sauvegardé en vue d’une restauration en fin de listing. Lesdifférentes commandes de ce client sont alors affichées dans la console. La valeur nullest alors affectée au client de la commande 11043 et la méthode SubmitChanges estappelée pour mémoriser les modifications dans la base de données. Les commandes duclient sont à nouveau affichées. Comme vous pouvez le voir, la commande 11043 nefait plus partie de la liste.

Voici les résultats affichés dans la console.

Les commandes avant la suppression de la relation :OrderID = 10738OrderID = 10907OrderID = 10964OrderID = 11043

Linq.book Page 452 Mercredi, 18. février 2009 7:58 07

Page 468: LINQ Language Integrated Query en C

Chapitre 14 Opérations standard sur les bases de données 453

Comme vous pouvez le constater, après avoir supprimé la relation entre la commande11043 et le client, cette commande ne fait plus partie de la collection de commandes duclient.

Surcharger les méthodes de mise à jour des bases de données

Si vous pensez ne pas pouvoir utiliser LINQ to SQL dans votre environnement, peut-être parce que vous devez utiliser des procédures stockées pour toutes vos modificationsdans la base de données, vous serez intéressé(e) de savoir que le code appelé pour effectuerdes mises à jour peut être surchargé.

Pour surcharger le code appelé pour insérer, mettre à jour et supprimer des données, ilsuffit de définir une méthode partielle nommée avec la signature appropriée. Le Data-Context appelle automatiquement les méthodes surchargées à la place des méthodeshabituelles. Microsoft propose une autre méthode qui tire parti des méthodes partielles :vous pouvez ajouter des instructions dans le code sans surcharger les méthodes, si vousle souhaitez.

Mais faites bien attention, si vous adoptez cette approche, vous devrez mettre en placeune détection de conflits d’accès concurrentiel. Prenez le temps de consulter le Chapi-tre 17 avant de prendre cette décision.

Si vous décidez de surcharger les méthodes de mise à jour, c’est le nom de la méthodepartielle et le type d’entité de ses paramètres qui entraînent le DataContext à appelerles méthodes surchargées.

Jetons un œil aux prototypes à mettre en place pour surcharger les méthodes insert,update et delete.

Surcharge de la méthode Insert

Vous pouvez surcharger la méthode appelée pour insérer un enregistrement dans la basede données en implémentant une méthode partielle prototypée comme suit :

partial void Insert[EntityClassName](T instance)

où [EntityClassName] est le nom de la classe d’entité à partir de laquelle la méthodeinsert est surchargée et T est le type de la classe d’entité.

Par exemple, pour surcharger la méthode insert avec la classe d’entité Shipper, vousutiliserez le code suivant :

partial void InsertShipper(Shipper instance)

Les commandes après la suppression de la relation :OrderID = 10738OrderID = 10907OrderID = 10964

Linq.book Page 453 Mercredi, 18. février 2009 7:58 07

Page 469: LINQ Language Integrated Query en C

454 LINQ to SQL Partie V

Surcharge de la méthode Update

Vous pouvez surcharger la méthode appelée pour mettre à jour un enregistrement dansla base de données en implémentant une méthode partielle prototypée comme suit :

partial void Update[EntityClassName](T instance)

où [EntityClassName] est le nom de la classe d’entité à partir de laquelle la méthodeupdate est surchargée et T est le type de la classe d’entité.

Par exemple, pour surcharger la méthode update avec la classe d’entité Shipper, vousutiliserez le code suivant :

partial void UpdateShipper(Shipper instance)

Surcharge de la méthode Delete

Vous pouvez surcharger la méthode appelée pour supprimer un enregistrement dans labase de données en implémentant une méthode partielle prototypée comme suit :

partial void Delete[EntityClassName](T instance)

où [EntityClassName] est le nom de la classe d’entité à partir de laquelle la méthodedelete est surchargée et T est le type de la classe d’entité.

Par exemple, pour surcharger la méthode delete avec la classe d’entité Shipper, vousutiliserez le code suivant :

partial void DeleteShipper(Shipper instance)

Exemple

Pour illustrer la surcharge des méthodes insert, update et delete, nous n’allons pasmodifier le fichier de classe d’entité généré. Nous allons plutôt créer un nouveaufichier. Ainsi, s’il est nécessaire de restaurer le fichier de classe d’entité, les méthodespartielles surchargées ne seront pas perdues. Le nouveau fichier a pour nomNorthwindExtended.cs :

using System;using System.Data.Linq;

namespace nwind{public partial class Northwind : DataContext {partial void InsertShipper(Shipper instance) {Console.WriteLine("La méthode surchargée Insert a été appelée pour l’affréteur {0}.",instance.CompanyName); }

partial void UpdateShipper(Shipper instance) {Console.WriteLine("La méthode surchargée Update a été appelée pour l’affréteur {0}.",instance.CompanyName); }

Linq.book Page 454 Mercredi, 18. février 2009 7:58 07

Page 470: LINQ Language Integrated Query en C

Chapitre 14 Opérations standard sur les bases de données 455

partial void DeleteShipper(Shipper instance) {Console.WriteLine("La méthode surchargée Delete a été appelée pour l’affréteur {0}.",instance.CompanyName); } }}

INFO

Le fichier NorthwindExtended.cs doit être ajouté au projet Visual Studio.

Les méthodes surchargées sont des méthodes partielles définies au niveau DataContext, etnon dans la classe d’entité relative.

Comme vous pouvez le voir, ces méthodes se contentent d’afficher un message indi-quant qu’elles ont été appelées. Cependant, dans la plupart des situations réelles, lasurcharge servira à appeler une procédure stockée…

Le Listing 14.23 contient le code qui appelle les méthodes surchargées.

Listing 14.23 : Un exemple d’utilisation des méthodes surchargées Update, Insert et Delete.

Northwind db = new Northwind(@ "Data Source=.\SQLEXPRESS;Initial ➥Catalog=Northwind ");

Shipper ship = (from s in db.Shipperswhere s.ShipperID == 1select s).Single<Shipper>();

ship.CompanyName = "Jiffy Shipping";

Shipper newShip =new Shipper {ShipperID = 4,CompanyName = "Vickey Rattz Shipping" ,Phone ="(800) SHIP-NOW" };

db.Shippers.InsertOnSubmit(newShip);Shipper deletedShip = (from s in db.Shippers

where s.ShipperID == 3select s).Single<Shipper>();

db.Shippers.DeleteOnSubmit(deletedShip);db.SubmitChanges();

La note n’est plus d’actualité

Ce code est composé de trois grandes parties. Dans la première, on accède à l’affréteurdont le champ ShipperID vaut 1 et son champ CompanyName est modifié. Dans ladeuxième, un nouvel affréteur (Vickey Rattz Shipping) est défini. Enfin, dans la troi-sième, l’affréteur dont le champ ShipperID vaut 3 est supprimé. Bien entendu, étantdonné que les méthodes surchargées se contentent d’afficher un message dans la

Linq.book Page 455 Mercredi, 18. février 2009 7:58 07

Page 471: LINQ Language Integrated Query en C

456 LINQ to SQL Partie V

console, la base de données reste vierge de toute modification. Voici les résultats affi-chés dans la console :

Les trois méthodes surchargées ont bien été appelées.

Supposons maintenant que vous vouliez surcharger les méthodes Update, Insert etDelete tout en maintenant les actions effectuées par défaut par ces méthodes. La techniqueà utiliser est très simple. Elle consiste à appeler les méthodes DataContext.ExecuteDy-namicUpdate, DataContext.ExecuteDynamicInsert et DataContext.ExecuteDynami-cDelete pour (respectivement) maintenir le comportement initial des méthodes Update,Insert et Delete.

À titre d’exemple, voici la transformation à appliquer dans les méthodes partielles ducode précédent pour continuer à afficher les messages dans la console, mais cette fois-ci en effectuant les modifications dans la base de données :

namespace nwind{public partial class Northwind : DataContext {partial void InsertShipper(Shipper instance) {Console.WriteLine("La méthode surchargée Insert a été appelée pour l’affréteur {0}.",instance.CompanyName);this.ExecuteDynamicInsert(instance); }

partial void UpdateShipper(Shipper instance) {Console.WriteLine("La méthode surchargée Update a été appelée pour l’affréteur {0}.",instance.CompanyName);this.ExecuteDynamicUpdate(instance); }

partial void DeleteShipper(Shipper instance) {Console.WriteLine("La méthode surchargée Delete a été appelée pour l’affréteur {0}.",instance.CompanyName);this.ExecuteDynamicDelete(instance); } }}

Comme vous pouvez le voir, une instruction en gras a été ajoutée dans chacune desméthodes partielles pour appeler la méthode ExecuteDynamic appropriée. Vous voyezdonc qu’il est facile d’étendre ou de modifier le comportement d’une classe d’entité.LINQ to SQL est vraiment très flexible !

La méthode surchargée Update a été appelée pour l’affréteur Jiffy Shipping.La méthode surchargée Insert a été appelée pour l’affréteur Vickey Rattz Shipping.La méthode surchargée Delete a été appelée pour l’affréteur Federal Shipping.

Linq.book Page 456 Mercredi, 18. février 2009 7:58 07

Page 472: LINQ Language Integrated Query en C

Chapitre 14 Opérations standard sur les bases de données 457

Surcharge dans le Concepteur Objet/Relationnel

Comme il a été dit au Chapitre 13, la surcharge des méthodes insert, update et deletepeut également être effectuée dans le Concepteur Objet/Relationnel.

Considérations

Lorsque vous surchargez les méthodes insert, update et delete, vous avez la respon-sabilité de mettre en place une détection de conflits d’accès concurrentiel. Pour ce faire,vous devez être familier avec le fonctionnement de la détection de conflits utilisée pardéfaut. Ainsi, par exemple, Microsoft spécifie tous les champs impliqués dans des véri-fications de mises à jour dans la clause where de l’instruction update. La logique testealors le nombre d’enregistrements mis à jour par l’instruction update. Si deux enregis-trements ou plus ont été mis à jour, il y a eu un conflit d’accès concurrentiel. Vousdevriez suivre une démarche similaire et lever l’exception ChangeConflictExceptionsi un conflit d’accès concurrentiel se produit. Si vous décidez de surcharger les métho-des insert, update et/ou delete, n’hésitez pas à vous reporter au Chapitre 17 pour ensavoir plus à ce sujet.

Traduction SQL

Lorsque vous écrivez des requêtes LINQ to SQL, vous avez sans doute remarqué queles clauses where et les autres expressions du même type sont spécifiées en utilisant lesinstructions du langage natif (C# dans cet ouvrage), et non en SQL.

À titre d’exemple, voici la requête utilisée dans le Listing 14.2 :

Un exemple de requête LINQ to SQL

Customer cust = (from c in db.Customerswhere c.CustomerID =="LONEP"select c).Single<Customer>();

Comme vous pouvez le voir, la requête est exprimée en C#. Si elle était écrite en SQL,elle aurait l’allure suivante :

Un exemple invalide de requête LINQ to SQL

Customer cust = (from c in db.Customerswhere c.CustomerID = ’LONEP’select c).Single<Customer>();

Ici, ce n’est pas l’opérateur d’égalité C# "==", mais c’est celui d’égalité SQL "="qui estutilisé. Par ailleurs, la chaîne littérale LONEP est entourée non pas de guillemets maisd’apostrophes. LINQ a un gros avantage : il permet au programmeur d’utiliser sonlangage de programmation pour effectuer des requêtes. Quoi de plus normal, puisqueLINQ signifie Language Integrated Query (langage de requête intégré). Cependant, labase de données n’étant pas en mesure d’exécuter des instructions C#, ces dernièresdoivent être traduites en instructions SQL.

Linq.book Page 457 Mercredi, 18. février 2009 7:58 07

Page 473: LINQ Language Integrated Query en C

458 LINQ to SQL Partie V

En général, la traduction est de très bonne qualité. Plutôt que redéfinir une référencecomparable à MSDN, en indiquant ce qui peut et ce qui ne peut pas être traduit, je vaisvous montrer ce qui va se passer quand une requête LINQ to SQL ne peut pas êtretraduite.

Sachez que le code en question peut passer l’étape de la compilation et que le problèmepeut n’être mis en évidence qu’à l’exécution. Étant donné que l’exécution des requêtespeut être différée, la ligne de code contenant la requête peut également s’exécuter sansproblème. Ce n’est que lors de l’exécution réelle de la requête qu’un message d’erreurdu type suivant peut s’afficher.

Ce message est très clair. Le Listing 14.24 représente le code qui en est à l’origine.

Listing 14.24 : Une requête LINQ to SQL qui ne peut pas être traduite.

Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind");IQueryable<Customer> custs = from c in db.Customerswhere c.CustomerID.TrimEnd(’K’) =="LAZY"select c;foreach (Customer c in custs){Console.WriteLine("{0}", c.CompanyName);}

La méthode TrimEnd, à l’origine de l’exception, est appelée sur un champ de la base dedonnées, et non sur la chaîne littérale"LAZY". Dans le Listing 14.25, nous allons modi-fier la position de la méthode TrimEnd.

Listing 14.25 : Une requête LINQ to SQL qui peut être traduite.

Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind");IQueryable<Customer> custs = from c in db.Customerswhere c.CustomerID =="LAZY".TrimEnd(’K’)select c;foreach (Customer c in custs){Console.WriteLine("{0}", c.CompanyName);}

L’exécution de ce code ne produit aucun message d’erreur en rapport avec la traductionSQL.

L’appel d’une méthode non supportée sur une colonne d’une base de données produitdonc une exception, alors que l’appel de cette même méthode sur un paramètre estsupporté. Ceci peut se comprendre : LINQ to SQL peut en effet appeler la méthodeTrimEnd sur un paramètre, car cette opération s’effectue sur l’ordinateur qui exécute leprogramme. Si nous appelons la méthode TrimEnd sur une colonne de la base dedonnées, elle est exécutée sur l’ordinateur qui héberge la base de données. Elle doit

Exception non gérée : System.NotSupportedException : La méthode ’TrimEnd’ n’a pas pu ➥être traduite en SQL....

Linq.book Page 458 Mercredi, 18. février 2009 7:58 07

Page 474: LINQ Language Integrated Query en C

Chapitre 14 Opérations standard sur les bases de données 459

donc être traduite en SQL, passée à la base de données et exécutée. La méthodeTrimEnd n’étant pas traduisible, une exception est levée.

Si vous êtes amené à appeler une méthode non supportée sur une colonne d’une base dedonnées, peut-être pourriez-vous utiliser une autre méthode qui a l’effet contraire sur leparamètre. Supposons par exemple que vous vouliez appeler la méthode ToUpper surune colonne d’une base de données et que cette méthode ne soit pas supportée. Si vousappelez la méthode ToLower sur le paramètre, elle sera supportée. Vous devez égale-ment vous assurer que la méthode appelée a l’effet souhaité. Dans notre exemple, lacolonne de la base de données pourrait avoir des casses différentes. L’appel de laméthode ToLower ne produirait donc pas l’effet escompté. Si la colonne de la base dedonnées a pour valeur "Smith" et le paramètre a pour valeur "SMITH", la méthodeToUpper appliquée à la colonne de la base de données donnerait l’effet recherché.Malheureusement, cette méthode n’est pas supportée, et appliquer la méthode opposée,ToLower, au paramètre ne résout en rien le problème.

Vous vous demandez certainement comment savoir que la méthode TrimEnd n’est pastraduisible en SQL. Les types et méthodes primitifs supportés étant dynamiques etsujets à des changements, ce livre ne saurait les couvrir d’une façon exhaustive. S’ilexiste de nombreuses restrictions liées à la traduction, Microsoft fait des efforts conti-nuels pour améliorer les choses. Pour avoir un aperçu des méthodes qui peuvent êtretraduites en SQL, le mieux est de vous reporter à la documentation MSDN intitulée"NET Framework Function Translation", dans la section "LINQ to SQL". Quoi qu’il ensoit, comme vous avez pu le constater au travers des exemples précédents, il reste trèssimple de déterminer si une méthode est supportée ou non.

Résumé

Ce chapitre vous a montré comment utiliser LINQ to SQL pour réaliser des opérationscourantes sur les bases de données. Essentiellement, insertion, interrogation, mise àjour et suppression d’enregistrements. Vous avez également vu que les requêtes LINQto SQL étaient différentes des requêtes LINQ to Object.

Ayez bien à l’esprit que, si un code LINQ to SQL modifie le contenu d’une base dedonnées, il doit également se charger de la détection des conflits d’accès concurrentiel.Par souci de clarté, aucun exemple de ce chapitre n’a implémenté le code correspon-dant. Le cas échéant, reportez-vous au Chapitre 17 pour avoir toutes les informationsnécessaires à ce sujet.

Il ne suffit pas de savoir comment effectuer des opérations de base sur des objets entité.Vous devez également comprendre comment ces opérations affectent le contenu de labase de données. En particulier, lorsque vous ajoutez un objet dans la base de données,les objets attachés sont automatiquement ajoutés sans qu’aucune action de votre part nesoit nécessaire. Mais, attention, cet automatisme ne s’applique pas aux suppressions :

Linq.book Page 459 Mercredi, 18. février 2009 7:58 07

Page 475: LINQ Language Integrated Query en C

460 LINQ to SQL Partie V

pour supprimer un objet entité parent dans une association, vous devez au préalablesupprimer les objets entité enfants afin d’éviter qu’une exception ne soit levée !

Dans ce chapitre, vous avez également appris à surcharger les méthodes utilisées pardéfaut pour modifier les objets entité correspondant aux enregistrements de la base dedonnées. Ceci permet au développeur de contrôler les modifications effectuées dans labase de données et facilite l’utilisation des procédures stockées.

Enfin, vous avez appris que les requêtes LINQ to SQL devaient être traduites en décla-rations SQL. Il faut toujours garder à l’esprit qu’une telle traduction doit être opérée etque, parfois, certaines requêtes LINQ to SQL ne sont pas traduisibles.

Jusqu’ici, les classes d’entité ont souvent été citées, mais jamais étudiées en détail. LeChapitre 15 vous donnera toutes les informations nécessaires pour les connaître commele fond de votre poche.

Linq.book Page 460 Mercredi, 18. février 2009 7:58 07

Page 476: LINQ Language Integrated Query en C

15

Les classes d’entité LINQ to SQL

Le chapitre précédent a utilisé à de nombreuses reprises des classes d’entité. Dans cechapitre, nous allons les décrire en détail. Vous apprendrez ce qu’elles sont et les diffé-rentes techniques qui permettent de les créer. Vous apprendrez également vers quellesdifficultés vous vous acheminez si vous décidez de créer manuellement vos classesd’entité.

Mais, avant d’entrer dans le vif du sujet, voyons quelques prérequis nécessaires àl’exécution des exemples de ce chapitre.

Prérequis pour exécuter les exemples

Pour exécuter les exemples de ce chapitre, vous devez être en possession de la versionétendue de la base de données Northwind et avoir généré les classes d’entité correspon-dantes. Si nécessaire, reportez-vous à la section "Prérequis pour exécuter les exemples"du Chapitre 12 pour savoir comment résoudre ces deux points.

Les classes d’entité

Les classes mappées à une base de données SQL Server par l’intermédiaire de LINQ toSQL sont appelées "classes d’entité". Un objet instancié à partir d’une classe d’entitéest appelé "objet entité". Les classes d’entité sont de traditionnelles classes C# pourlesquelles des attributs LINQ to SQL additionnels sont spécifiés. Elles peuvent égale-ment être définies en fournissant un fichier de mappage XML lors de l’instanciation del’objet DataContext. Les attributs ou le fichier de mappage définissent le mappageentre les classes d’entité et la base de données SQL Server au travers de LINQ to SQL.

C’est en utilisant ces classes d’entité que LINQ to SQL va vous permettre de définir desrequêtes et de modifier une base de données.

Linq.book Page 461 Mercredi, 18. février 2009 7:58 07

Page 477: LINQ Language Integrated Query en C

462 LINQ to SQL Partie V

Création de classes d’entité

Les classes d’entité sont l’élément de base permettant de définir des requêtes LINQ toSQL. Il est donc nécessaire de les fabriquer en premier lieu. Deux techniques sont utili-sables. Vous pouvez les générer en utilisant les modes opératoires décrits aux Chapi-tres 12 et 13, mais également les rédiger "à la main" ou, pourquoi pas, utiliser unecombinaison de ces deux techniques.

Si vous n’avez pas encore de classes métier pour les entités stockées dans la base dedonnées, la génération des classes d’entité est certainement la meilleure approche. Sivous avez déjà un modèle objet, c’est l’écriture manuelle des classes d’entité qui estsans doute la meilleure approche.

Si vous commencez un nouveau projet, je vous recommande de définir votre base dedonnées, puis de générer les classes d’entités à partir de la base de données. Cela vouspermettra d’avoir une prise sur les classes d’entité générées et vous évitera d’avoir à lesécrire, ce qui, comme vous le verrez par la suite, n’est pas toujours chose évidente.

Génération de classes d’entitéJusqu’ici, la seule façon envisagée pour créer des classes d’entité consistait à les géné-rer. Au Chapitre 12, vous avez vu comment générer les classes d’entité de la base dedonnées Northwind. Par leur intermédiaire, vous êtes en mesure d’exécuter les exem-ples des chapitres dédiés à LINQ to SQL. Au Chapitre 13, vous avez appris à utiliserl’outil en ligne de commande SQLMetal ou le Concepteur Objet/Relationnel pourgénérer des classes d’entité.

SQLMetal est très simple à utiliser, mais il ne permet pas de choisir le nom des classesd’entité générées. Il produit un fichier XML intermédiaire, permet de l’éditer et fabri-que les classes d’entité à partir de ce fichier. Les classes d’entité sont définies pour tousles champs de toutes les tables de la base de données, en utilisant la procédure forte-ment consommatrice en ressources décrite précédemment. Cela ne vous laisse que peude contrôle sur le nom des classes d’entité et sur leurs propriétés. Le Concepteur Objet/Relationnel peut prendre plus de temps pour créer un modèle objet complet, mais ilvous permet de spécifier précisément pour quelles tables et quels champs vous voulezgénérer des classes d’entité. Vous pouvez également choisir le nom des classes d’entitéet de leurs propriétés. Si nécessaire, reportez-vous au Chapitre 13 pour avoir de plusamples renseignements sur ces deux outils.

Rien ne vous oblige à générer les classes d’entité de toutes les tables d’une base dedonnées. Par ailleurs, vous pouvez, si nécessaire, ajouter des fonctionnalités métier auxclasses d’entité générées. À titre d’exemple, la classe Customer a été générée avecSQLMetal au Chapitre 12. Il est possible d’ajouter des méthodes métier ou desmembres de classes non persistants à cette classe. Si vous le faites, assurez-vous quevous ne modifiez pas le code de la classe d’entité générée. Pour ce faire, la meilleure

Linq.book Page 462 Mercredi, 18. février 2009 7:58 07

Page 478: LINQ Language Integrated Query en C

Chapitre 15 Les classes d’entité LINQ to SQL 463

méthode consiste à créer un autre module de classe Customer et à profiter du fait que lesclasses d’entité sont générées en tant que classes partielles. Cette nouveauté intéres-sante de C# permet de séparer très simplement des fonctionnalités dans différentsmodules. Ainsi, si pour une raison quelconque la classe d’entité doit à nouveau êtregénérée, vous ne perdrez pas les méthodes et/ou membres qui y ont été ajoutés.

Écriture manuelle des classes d’entitéL’écriture manuelle des classes d’entité est l’approche la plus difficile. Elle nécessiteune bonne compréhension des attributs LINQ to SQL et du schéma de mappageexterne. Notez cependant que l’écriture manuelle de classes d’entité est une bonnefaçon d’apprendre le langage LINQ to SQL.

L’écriture manuelle de classes peut se révéler intéressante pour des classes déjà existan-tes. Vous pouvez avoir une application existante avec un modèle objet déjà implémenté.Il ne serait pas très astucieux de générer les classes d’entité à partir de la base dedonnées, puisque vous avez déjà un modèle objet utilisé par l’application.

La solution consiste à ajouter les attributs nécessaires au modèle objet existant ou àcréer un fichier de mappage. Grâce à la flexibilité de LINQ to SQL, il n’est pas néces-saire que les classes aient le nom de la table correspondante ni que les noms despropriétés de la classe correspondent aux noms des colonnes dans la table.

Pour créer des classes d’entité manuellement, vous devez ajouter des attributs dans vosclasses, qu’il s’agisse de classes métier existantes ou de nouvelles classes créées spéci-fiquement en tant que classes entité. Reportez-vous à la section intitulée "Attributs declasses d’entité et propriétés d’attributs" dans ce chapitre pour prendre connaissancedes attributs et propriétés disponibles.

Pour créer des classes d’entité en utilisant un fichier de mappage externe, vous devezcréer un fichier XML qui se conforme au schéma présenté dans la section "Schéma defichier de mappage externe XML", un peu plus loin dans ce chapitre. Une fois ce fichiercréé, vous utiliserez le constructeur approprié lors de l’instanciation du DataContextpour charger le fichier de mappage. Deux constructions vous permettent de spécifier unfichier de mappage externe.

Responsabilités annexes des classes d’entitéMalheureusement, il ne suffit pas de comprendre comment fonctionnent les attributs etles propriétés des attributs pour être en mesure d’écrire des classes d’entité à la main.Vous devez également avoir des connaissances sur les responsabilités annexes des clas-ses d’entité.

Les classes d’entité doivent entre autres être attentives aux notifications de changementet assurer la cohérence entre les classes parents et enfants. Ces responsabilités annexessont toutes gérées de façon transparente lorsque vous utilisez SQLMetal ou le Concepteur

Linq.book Page 463 Mercredi, 18. février 2009 7:58 07

Page 479: LINQ Language Integrated Query en C

464 LINQ to SQL Partie V

Objet/Relationnel. Si vous écrivez les classes d’entité manuellement, vous devrezimplémenter le code correspondant par vous-même.

Notifications de changementUn peu plus loin, au Chapitre 16, nous nous intéresserons à la détection de changement.Cette dernière n’est pas très élégante ni efficace sans l’assistance des classes d’entité. Sivos classes d’entité ont été générées par SQLMetal ou le Concepteur Objet/Relationnel,détendez-vous : ces deux outils fabriquent automatiquement le code nécessaire lors dela génération des classes d’entité. Si vous écrivez vos classes d’entité manuellement,vous devez bien appréhender les notifications de changement et implémenter du codepour les gérer.

Les classes d’entité peuvent participer ou ne pas participer aux notifications de change-ment. Dans le second cas, le DataContext fournit une trace des changements en conser-vant deux copies de chaque objet entité : une avec la valeur originale et une avec lavaleur actuelle. La première copie est créée à la première lecture d’une entité dans labase de données. C’est alors que le traçage des changements se met en branle. Vouspouvez améliorer l’efficacité du processus en implémentant les interfaces de notifica-tions de changement System.ComponentModel.INotifyPropertyChanging etSystem.ComponentModel.INotifyPropertyChanged dans les classes d’entité que vousavez écrites manuellement.

Comme fréquemment dans les chapitres relatifs à LINQ to SQL, je vais me référer aucode généré par SQLMetal pour vous montrer la meilleure façon de gérer certainessituations. Ici, je vais utiliser ce code pour vous montrer comment gérer les notificationsde changement. Pour implémenter les interfaces System.ComponentModel.INotify-PropertyChanging et System.ComponentModel.INotifyPropertyChanged, quatreétapes doivent être accomplies.

Première étape. Nous devons définir la classe d’entité de telle sorte qu’elle implé-mente les interfaces System.ComponentModel.INotifyPropertyChanging etSystem.ComponentModel.INotifyPropertyChanged :

Dans la classe d’entité générée Customer

[Table(Name="dbo.Customers")]public partial class Customer : INotifyPropertyChanging, INotifyPropertyChanged{ … }

Comme la classe d’entité implémente ces deux interfaces, le DataContext définit deuxgestionnaires d’événements pour ces événements (voir un peu plus loin dans cettesection).

Comme vous le voyez, le code précédent utilise l’attribut Table. Pour des raisons decontexte, les attributs sont spécifiés dans cette section, sans toutefois les expliquer endétail. Vous en saurez plus à leur sujet un peu plus loin dans ce chapitre.

Linq.book Page 464 Mercredi, 18. février 2009 7:58 07

Page 480: LINQ Language Integrated Query en C

Chapitre 15 Les classes d’entité LINQ to SQL 465

Deuxième étape. Nous devons déclarer une classe private static de type Property-ChangingEventsArgs et passer String.Empty à son constructeur.

Dans la classe d’entité générée Customer

[Table(Name="dbo.Customers")]public partial class Customer : INotifyPropertyChanging, INotifyPropertyChanged{ private static PropertyChangingEventArgs emptyChangingEventArgs = new PropertyChangingEventArgs(String.Empty); ...}

L’objet emptyChangingEventArgs sera passé à un des gestionnaires d’événementsmentionnés précédemment lorsqu’un événement approprié sera levé.

Troisième étape. Deux membres public event (PropertyChanging, de typeSystem.ComponentModel.PropertyChangingEventHandler et PropertyChanged, detype System.ComponentModel.PropertyChangedEventHandler) doivent être ajoutés.

Dans la classe d’entité générée Customer

[Table(Name="dbo.Customers")]public partial class Customer : INotifyPropertyChanging, INotifyPropertyChanged{ private static PropertyChangingEventArgs emptyChangingEventArgs = new PropertyChangingEventArgs(String.Empty); ... public event PropertyChangingEventHandler PropertyChanging;

public event PropertyChangedEventHandler PropertyChanged; ...}

Lorsque l’objet DataContext initie une détection de changement pour un objet entité,deux cas peuvent se produire. Si la classe d’entité implémente les deux interfaces denotification de changement, les gestionnaires d’événements correspondants sont mis enplace. Dans le cas contraire, une copie de l’objet entité est effectuée, comme mentionnéprécédemment.

Quatrième étape. Chaque fois qu’une propriété d’une entité mappée est modifiée,l’événement PropertyChanging doit être levé avant la modification et l’événementPropertyChanged, après la modification.

SQLMetal génère les méthodes SendPropertyChanging et SendPropertyChanged àvotre place (la gestion des événements ne doit pas forcément être implémentée de cettefaçon).

Dans la classe d’entité générée Customer

protected virtual void SendPropertyChanging(){ if ((this.PropertyChanging != null)) { this.PropertyChanging(this, emptyChangingEventArgs); }}

Linq.book Page 465 Mercredi, 18. février 2009 7:58 07

Page 481: LINQ Language Integrated Query en C

466 LINQ to SQL Partie V

protected virtual void SendPropertyChanged(String propertyName){ if ((this.PropertyChanged != null)) { this.PropertyChanged(this, new PropertyChangedEventArgs(propertyName)); }}

Lorsque l’événement PropertyChanged est levé, un nouvel objet PropertyChangedE-ventArgs est créé et le nom de la propriété qui a été modifiée lui est passé. De cettefaçon, l’objet DataContext sait quelle propriété a été modifiée. Lorsque la méthodeSendPropertyChanging est appelée, elle lève l’événement PropertyChanging, ce quiprovoque l’appel du gestionnaire d’événements enregistré par l’objet DataContext. Lemême principe s’applique à la méthode SendPropertyChanged et à l’événementPropertyChanged.

Bien entendu, vous pouvez choisir d’inclure manuellement cette logique dans votrecode plutôt que créer des méthodes toutes faites mais, croyez-moi, cela est plutôt péni-ble et le code à maintenir est assez important.

Dans la méthode set de chaque propriété, il faut appeler les méthodes SendPropertyChan-ging et SendPropertyChanged juste avant, puis juste après la modification de la propriété.

Dans la classe d’entité générée Customer

[Column(Storage="_ContactName", DbType="NVarChar(30)")]public string ContactName{ get { return this._ContactName; } set { if ((this._ContactName != value)) { this.OnContactNameChanging(value); this.SendPropertyChanging(); this._ContactName = value; this.SendPropertyChanged("ContactName"); this.OnContactNameChanged(); } }}

Le nom de la propriété (ici ContactName) est passé dans l’appel à la méthode Send-PropertyChanged. Lorsque la méthode SendPropertyChanged est appelée, l’objet Data-Context sait ainsi que la propriété ContactName de cet objet entité a été modifiée.

Les mêmes événements doivent également être levés dans les méthodes set despropriétés qui représentent l’association entre les tables Order et Customer. Du côté"plusieurs" de l’association "un-à-plusieurs", le code en gras doit être ajouté :

Dans la classe Order, puisque Customer n’a pas de propriété EntityRef<T>

[Association(Name="FK_Orders_Customers", Storage="_Customer",ThisKey="CustomerID", IsForeignKey=true)]

Linq.book Page 466 Mercredi, 18. février 2009 7:58 07

Page 482: LINQ Language Integrated Query en C

Chapitre 15 Les classes d’entité LINQ to SQL 467

public Customer Customer{ get { return this._Customer.Entity; } set { Customer previousValue = this._Customer.Entity; if (((previousValue != value) || (this._Customer.HasLoadedOrAssignedValue == false))) { this.SendPropertyChanging(); if ((previousValue != null)) { this._Customer.Entity = null; previousValue.Orders.Remove(this); } this._Customer.Entity = value; if ((value != null)) { value.Orders.Add(this); this._CustomerID = value.CustomerID; } else { this._CustomerID = default(string); } this.SendPropertyChanged("Customer"); } }}

Du côté "un" de l’association "un-à-plusieurs", le code en gras doit être ajouté :

Dans la classe d’entité générée Customer

public Customer(){ ... this._Orders = new EntitySet<Order>(new Action<Order>(this.attach_Orders), new Action<Order>(this.detach_Orders));}...private void attach_Orders(Order entity){ this.SendPropertyChanging(); entity.Customer = this; this.SendPropertyChanged("Orders");}

private void detach_Orders(Order entity){ this.SendPropertyChanging(); entity.Customer = null; this.SendPropertyChanged("Orders");}

Si le délégué générique Action utilisé dans le code précédent ne vous est pas familier,sachez qu’il se trouve dans l’espace de noms System et qu’il a été ajouté au framework.NET 3.0. Le code précédent instancie un objet délégué Action pour la classe d’entitéOrder et le passe en tant que délégué à la méthode attach_Orders. LINQ to SQL utilisera

Linq.book Page 467 Mercredi, 18. février 2009 7:58 07

Page 483: LINQ Language Integrated Query en C

468 LINQ to SQL Partie V

ce délégué un peu plus tard pour relier un client et une commande. De la mêmemanière, un autre objet délégué Action est instancié et un délégué vers la méthodedetach_Orders lui est passé. LINQ to SQL utilisera ce délégué un peu plus tard poursupprimer le lien entre un client et une commande.

En implémentant la notification de changement comme il vient d’être décrit, les tracesdes modifications sont plus efficaces. De cette façon, l’objet DataContext sait quand etquelle propriété d’une classe d’entité a été modifiée.

Lorsque nous appelons la méthode SubmitChanges, l’objet DataContext oublie lavaleur originale des propriétés, la valeur actuelle devient la valeur originale et le traçagedes modifications est réinitialisé. Pour en savoir plus à ce sujet, reportez-vous au Chapi-tre 16.

Bien entendu, comme il a été dit précédemment, si vous définissez vos classes d’entitévia SQLMetal ou le Concepteur Objet/Relationnel, tout le code dont nous venons deparler est généré automatiquement. Vous n’aurez à implémenter la notification de chan-gement que dans le cas où vous décidez d’écrire les classes d’entité à la main.

Cohérence du grapheEn mathématiques, lorsque des nœuds sont connectés l’un à l’autre, le réseau constituépar les connexions est appelé "graphe". De la même manière, le réseau qui représenteles connexions créées par des classes qui référencent d’autres classes est appelé"graphe". Lorsque deux classes d’entité partagent une relation (une association),étant donné que chacune d’entre elles référence l’autre, il existe un graphe pour lesreprésenter.

Lorsque vous modifiez une relation entre deux objets entité (un client et unecommande, par exemple), la référence de chaque côté de la relation doit être correcte-ment mise à jour, afin que chaque objet entité référence correctement (ou ne référenceplus) l’autre. Ceci reste valable que vous définissiez ou supprimiez une relation. AvecLINQ to SQL, le programmeur qui utilise des classes d’entité ne doit modifier qu’unseul côté d’une relation : l’autre côté est modifié de façon transparente, mais pas parLINQ to SQL…

C’est la responsabilité de la classe d’entité que mettre à jour l’autre côté de la relation.Si vos classes d’entité ont été générées via SQLMetal ou le Concepteur Objet/Relation-nel, cette étape est automatique. Vous n’aurez à implémenter la mise à jour de l’autrecôté de la relation que dans le cas où vous décidez d’écrire les classes d’entité à la main.

Le graphe est dit "cohérent" si les deux côtés de la relation sont correctement mis à jour.Dans le cas contraire, il est dit "incohérent" et… le chaos n’est pas loin : un client pour-rait ainsi être relié à une commande et cette commande, reliée à un autre client ou àaucun client ! Cela rend toute navigation impossible dans la base de données et la situationest inacceptable.

Linq.book Page 468 Mercredi, 18. février 2009 7:58 07

Page 484: LINQ Language Integrated Query en C

Chapitre 15 Les classes d’entité LINQ to SQL 469

Heureusement, Microsoft fournit un modèle qui permet de s’assurer que les graphes desclasses d’entité sont cohérents. Voyons leur implémentation, générée par SQLMetalpour la base de données Northwind :

Dans la classe d’entité générée Customer

public Customer(){ ... this._Orders = new EntitySet<Order>(new Action<Order>(this.attach_Orders), new Action<Order>(this.detach_Orders));}...private void attach_Orders(Order entity){ this.SendPropertyChanging(); entity.Customer = this; this.SendPropertyChanged("Orders");}private void detach_Orders(Order entity){ this.SendPropertyChanging(); entity.Customer = null; this.SendPropertyChanged("Orders");}

Dans cet exemple, la classe Customer est la classe parent (le côté "un" de la relation"un-à-plusieurs"). La classe Order est la classe enfant (le côté "plusieurs" de la relation"un-à-plusieurs").

Dans le code précédent, deux objets délégués Action<T> sont passés au constructeur dela classe parent Customer lors de l’initialisation de la collection de classes enfant_Orders.

Le délégué passé en argument au premier Action<T> représente la méthode chargée del’affectation de l’objet Customer courant à l’objet Order. Dans ce délégué, le mot-cléthis fait référence au Customer de l’objet Order passé à la méthode attach_Orders.

Le délégué passé en argument au deuxième Action<T> représente la méthode chargéede la désaffectation de l’objet Customer courant à l’objet Order. Dans ce délégué, lemot-clé this fait référence au Customer de l’objet Order passé à la méthodedetach_Orders.

Bien que le code précédent se trouve dans la classe parent Customer, l’affectation de laclasse enfant Order au client est gérée par la propriété Customer de l’objet Order. Vouspouvez voir que les méthodes attach_Orders et detach_Orders se contentent de modi-fier la propriété Customer de l’objet Order : la propriété entity.Customer est initiali-sée à this (attach_Orders)/à null (detach_Orders) pour (respectivement) attacher/détacher le Customer à/de l’objet Order. C’est dans les méthodes get et set de la classeenfant Order que se fera tout le travail de fond qui assurera la cohérence du graphe. Laclasse parent, quant à elle, "se contente" de maintenir la cohérence du graphe.

Linq.book Page 469 Mercredi, 18. février 2009 7:58 07

Page 485: LINQ Language Integrated Query en C

470 LINQ to SQL Partie V

Avant de continuer, remarquez que les notifications de changement sont résolues enappelant les méthodes SendPropertyChanging et SetPropertyChanged dans les métho-des attach_Orders et detach_Orders.

Voyons maintenant ce qui doit être fait dans la classe enfant d’une relation "parent-vers-enfant" pour maintenir la cohérence du graphe.

Dans la classe d’entité générée Order

[Association(Name="FK_Orders_Customers", Storage="_Customer", ThisKey="CustomerID", IsForeignKey=true)]public Customer Customer{ get { return this._Customer.Entity; } set { Customer previousValue = this._Customer.Entity; if (((previousValue != value) || (this._Customer.HasLoadedOrAssignedValue == false))) { this.SendPropertyChanging(); if ((previousValue != null)) { this._Customer.Entity = null; previousValue.Orders.Remove(this); } this._Customer.Entity = value; if ((value != null)) { value.Orders.Add(this); this._CustomerID = value.CustomerID; } else { this._CustomerID = default(string); } this.SendPropertyChanged("Customer"); } }}

Ce code correspond à la méthode set de la propriété Customer : le côté "parent" de larelation lui a donné la charge de maintenir la cohérence du graphe. Cette méthode étantassez complexe, nous allons décrire son fonctionnement pas à pas.

set{ Customer previousValue = this._Customer.Entity;

La première ligne de la méthode set copie le client original affecté à la commande dansla variable previousValue. Que la référence this._Customer.Entity ne voussurprenne pas : la variable membre _Customer est de type Entity<Customer> et non detype Customer. Pour obtenir l’objet Customer, le code doit faire référence à la propriétéEntity de l’objet EntityRef<T>. Le type EntityRef<T> étant apparenté à un Customer,

Linq.book Page 470 Mercredi, 18. février 2009 7:58 07

Page 486: LINQ Language Integrated Query en C

Chapitre 15 Les classes d’entité LINQ to SQL 471

le type de Entity sera Customer. Aucun casting n’est donc nécessaire. Avouez que lesgénériques, apparus dans C# 2.0, sont vraiment pratiques.

if (((previousValue != value) || (this._Customer.HasLoadedOrAssignedValue == false))){

Ce bloc de code teste si l’objet Customer actuel associé à l’objet Order via le paramètrevalue n’est pas le même que celui qui est déjà associé à l’objet Order. Dans ce cas,aucune action n’est nécessaire, sauf si l’objet Customer n’est pas encore initialisé. Étantdonné la nature récursive de ce code, cette ligne est très importante, car c’est elle qui vaarrêter l’itération.

this.SendPropertyChanging();

La méthode SendPropertyChanging est appelée pour lever la notification de l’événementde changement.

if ((previousValue != null)){

Le code détermine alors si un objet Customer (parent) est déjà affecté à l’objet Order(enfant) en testant si l’objet previousValue a pour valeur null.

Si un objet Customer est associé à l’objet Order (en d’autres termes si previousValuen’est pas nul), la valeur null doit être affectée à la propriété Entity de l’objet Entity-Ref<T> Customer de l’objet Order.

this._Customer.Entity = null;

La propriété Entity est initialisée à null dans la ligne précédente pour arrêter leparcours récursif qui sera mis en branle sur la ligne suivante. Étant donné que cettepropriété est nulle, qu’elle ne représente pas l’objet Customer actuel et que la propriétéOrders de l’objet Customer contient toujours cette commande, le graphe est incohérent.

Dans la ligne suivante, la méthode Remove est appelée sur la propriété Orders de l’objetCustomer et l’objet Order courant est passé en argument pour qu’il soit supprimé.

previousValue.Orders.Remove(this);}

L’appel de la méthode Remove provoque l’appel de la méthode detach_Orders de laclasse Customer en lui passant l’objet Order à désaffecter. Dans la méthodedetach_Orders, la propriété Customer de l’objet Order est initialisée à null. Pour vousrafraîchir la mémoire, voici à quoi ressemble la méthode detach_Orders :

La méthode detach_Orders, juste pour vous rafraîchir la mémoire

private void detach_Orders(Order entity){ this.SendPropertyChanging(); entity.Customer = null; this.SendPropertyChanged("Orders");}

Lors de l’appel de la méthode detach_Orders, la propriété Customer de l’objet Orderpassé en argument est initialisée à null. Ceci provoque l’appel de la méthode set de la

Linq.book Page 471 Mercredi, 18. février 2009 7:58 07

Page 487: LINQ Language Integrated Query en C

472 LINQ to SQL Partie V

propriété Customer de l’objet Order, celle-là même qui est à l’origine de l’invoca-tion de la méthode detach_Orders. La méthode qui a lancé le processus de désaf-fectation de l’objet Order est donc appelée récursivement et la valeur null est passée àla méthode set. L’exécution consiste donc en un appel récursif à la méthode set del’objet Customer.

La méthode detach_Orders provoque l’appel récursif de la méthode set

set{ Customer previousValue = this._Customer.Entity; if (((previousValue != value) || (this._Customer.HasLoadedOrAssignedValue == false))) {

La quatrième ligne de la méthode set teste l’objet value. S’il est égal à la propriétéEntity de la propriété Customer actuellement affectée, l’appel récursif à la méthodeset n’effectue aucune action. Dans la ligne de code précédente, le premier appel nonrécursif de la méthode set qui appelle la propriété Entity de la propriété Customeravait pour valeur null. Étant donné que la valeur null est passée dans l’objet value dela méthode detach_Orders, ces deux valeurs sont égales. L’invocation récursive de laméthode set se termine sans qu’aucune action n’ait été accomplie, et le flux de contrôleretourne à la première invocation de la méthode set. C’est ce que nous voulions direquelques lignes plus haut lorsque nous indiquions que la propriété Entity était initiali-sée à la valeur null pour arrêter la boucle récursive. Il est temps de marquer une pause,car le besoin d’aspirine est de plus en plus pressant…

Lorsque l’appel récursif à la méthode set prend fin, le flux retourne à la ligne qui suitl’invocation initiale de la méthode set.

Par commodité, cette ligne de code peut être réécrite en utilisant un snippet

previousValue.Orders.Remove(this);}

Une fois l’exécution de la méthode Orders.Remove terminée, la propriété Orders del’objet Customer ne contient plus aucune référence à cette commande. Le graphe estdonc à nouveau cohérent.

Si vous prévoyez d’écrire vos classes d’entité, vous devez vous attendre à passer un peude temps dans le débogueur sur ce que nous venons de présenter. Placez des pointsd’arrêt dans les méthodes detach_Orders et set, et observez ce qui se passe.

Le nouvel objet Customer, qui était passé à la méthode set dans le paramètre value, estalors stocké dans la propriété Entity de l’objet Customer.

this._Customer.Entity = value;

Après tout, il s’agit de la méthode set de la propriété Customer. Nous voulions affecterl’objet Order à un nouvel objet Customer. À nouveau, à ce point du code, l’objet Ordera une référence vers l’objet Customer nouvellement assigné, mais ce dernier n’a pas deréférence vers l’objet Order. Le graphe n’est donc plus cohérent.

Linq.book Page 472 Mercredi, 18. février 2009 7:58 07

Page 488: LINQ Language Integrated Query en C

Chapitre 15 Les classes d’entité LINQ to SQL 473

Le code teste la valeur de l’objet Customer. Si elle est différente de null, l’objet Customerdoit être affecté à l’objet Order.

if ((value != null)){

Si l’objet Customer passé dans le paramètre value n’est pas nul, l’objet Order actuel estajouté à la collection d’objets Order de l’objet Customer.

value.Orders.Add(this);

Dans cette ligne, le délégué qui avait été passé à la méthode du constructeur Entity-Set<T> de l’objet Customer est appelé. L’affectation provoquera donc l’appel de laméthode attach_Orders de l’objet Customer.

Ceci, tour à tour, affectera l’objet Customer courant de l’objet Order à l’objet Customerpassé. Ce qui provoquera l’appel de la méthode set de la propriété Customer de l’objetOrder. Le code effectuera un appel récursif dans la méthode set, comme il a été indiquéun peu plus tôt. Cependant, deux instructions plus haut, et avant de commencer la récur-sion, la propriété Entity de la propriété Customer de l’objet Order a été initialisée avecun nouvel objet Customer, celui-là même qui a été passé à la méthode set via laméthode attach_Orders. Ici encore, la méthode set est appelée récursivement, puis ladeuxième ligne de code est appelée.

Cette ligne de code est issue d’une autre invocation de la méthode set

if (((previousValue != value) || (this._Customer.HasLoadedOrAssignedValue == false)))

Étant donné que l’objet Customer courant de l’objet Order (actuellement stocké dansl’objet previousValue) et le paramètre value sont identiques, la méthode set ne faitaucune action et la récursivité prend fin.

Dans la ligne suivante, le membre CustomerID courant de l’objet Order est initialiséavec le CustomerID du nouvel objet Customer.

this._CustomerID = value.CustomerID;}

Si l’objet Customer qui vient d’être assigné est nul, le code se contente d’affecter lavaleur par défaut du type du membre (ici, String) au membre CustomerID de l’objetOrder.

else{ this._CustomerID = default(string);}

Si le membre CustomerID avait été de type int, le code aurait réalisé une affectation detype default(int).

Dans la dernière ligne de code, le nom de la propriété modifiée est passé à la méthodeSendPropertyChanged pour lever une notification de changement.

this.SendPropertyChanged("Customer");}

Linq.book Page 473 Mercredi, 18. février 2009 7:58 07

Page 489: LINQ Language Integrated Query en C

474 LINQ to SQL Partie V

Cette technique fonctionne pour les relations "un-à-plusieurs". Dans une relation "un-à-un", chaque côté de la relation correspond, à quelques détails près, au côté "enfant" decet exemple. Dans une relation "un-à-un", la notion de parent et d’enfant n’existe pas.Supposons que la relation entre les clients et les commandes est de type "un-à-un".

Si vous écrivez les classes d’entité à la main et que la relation entre les classes Customeret Order soit de type "un-à-un", sachez que chacune de ces classes contient unepropriété de type EntityRef<T>, où T est l’autre classe d’entité. Ainsi, la classe Custo-mer contient un EntityRef<Order> et la classe Order, un EntityRef<Customer>.Comme aucune des deux classes ne contient un EntitySet<T>, les méthodes Add etRemove des relations "un-à-plusieurs" ne sont pas appelées.

Si nous supposons qu’il existe une relation "un-à-un" entre les commandes et lesclients, la méthode set de la propriété Customer de la classe Order aura la même allurequ’auparavant, mis à part que la commande courante ne sera plus affectée au clientoriginal. Le client original ne pouvant passer qu’une commande, nous ne supprimeronspas la commande courante d’une collection d’objets Order : il suffira d’affecter lavaleur null à la propriété Order de l’objet Customer.

La ligne de code suivante :

previousValue.Orders.Remove(this);

sera remplacée par :

previousValue.Order = null;

Comme vous pouvez le voir, le maintien de la cohérence dans le graphe est loin d’êtretrivial, et il est facile de s’y perdre. Heureusement, deux outils gèrent cette tâche pourvous : SQLMetal et le Concepteur Objet/Relationnel. Pour assurer la cohérence dugraphe et implémenter les notifications de changement, ils n’ont pas leur pareil ! L’outilen ligne de commande aurait pu s’appeler SQLGold tant il est pratique, mais je supposeque la partie "metal" de son nom vient du mot "metalangage"…

Appel des méthodes partielles appropriéesLorsque Microsoft a mis au point les méthodes partielles pour permettre l’extension ducode généré (des classes d’entité, par exemple), il a encore alourdi votre travail si vousdécidez d’implémenter manuellement vos classes d’entité.

Ainsi, vous devez déclarer plusieurs méthodes partielles :

partial void OnLoaded();partial void OnValidate(ChangeAction action);partial void OnCreated();partial void On[Property]Changing(int value);partial void On[Property]Changed();

Vous devriez également définir les méthodes On[Property]Changing etOn[Property]Changed pour chacune des propriétés des classes d’entité.

Linq.book Page 474 Mercredi, 18. février 2009 7:58 07

Page 490: LINQ Language Integrated Query en C

Chapitre 15 Les classes d’entité LINQ to SQL 475

Les méthodes OnLoaded et OnValidate ne nécessitent aucun appel complémentairedans la classe d’entité : elles seront automatiquement appelées par l’objet DataContext.

En revanche, vous devez ajouter du code pour appeler la méthode OnCreated dans leconstructeur de la classe d’entité.

Appel de la méthode partielle OnCreated

public Customer(){ OnCreated(); ...}

Puis, pour chacune des propriétés de classe d’entité mappée, vous devez ajouter unappel aux méthodes On[Property]Changing et On[Property]Changed, juste avant etjuste après la modification d’une propriété d’une classe d’entité.

Appel des méthodes On[Property]Changing et On[Property]Changed dans la méthodeset d’une classe d’entité

public string CompanyName{ get { return this._CompanyName; } set { if ((this._CompanyName != value)) { this.OnCompanyNameChanging(value); this.SendPropertyChanging(); this._CompanyName = value; this.SendPropertyChanged("CompanyName"); this.OnCompanyNameChanged(); } }}

La méthode On[Property]Changing est appelée avant la méthode SendPropertyChanginget la méthode On[Property]Changed, après la méthode SendPropertyChanged.

En déclarant et en appelant ces méthodes partielles, vous donnez aux autres déve-loppeurs la capacité d’étendre les possibilités de base, sans pour autant que cela influesur les performances, et ce qu’ils en tirent parti ou non. C’est là toute la beauté desméthodes partielles…

Problèmes relatifs à EntityRef<T>Alors que les membres d’une classe privée associée ont pour type EntityRef<T>, leurspropriétés publiques doivent être du type de la classe d’entité, et non EntityRef<T>.

Voyons comment SQLMetal génère une propriété pour un membre privé Entity-Ref<T>.

Linq.book Page 475 Mercredi, 18. février 2009 7:58 07

Page 491: LINQ Language Integrated Query en C

476 LINQ to SQL Partie V

La propriété publique d’un membre de classe retourne le type de la classe et non Enti-tyRef<T>

[Table(Name="dbo.Orders")]public partial class Order : INotifyPropertyChanging, INotifyPropertyChanged{ ... private EntityRef<Customer> _Customer; ... [Association(Name="FK_Orders_Customers", Storage="_Customer", ThisKey="CustomerID", IsForeignKey=true)] public Customer Customer { get { return this._Customer.Entity; } set { ... } } ...}

Comme vous pouvez le voir, même si le membre de classe privée _Customer est de typeEntityRef<Customer>, la propriété Customer est de type Customer, et non Entity-Ref<Customer>. Ceci est important car, dans une requête, une référence à un type Enti-tyRef<T> ne pourra pas être traduite en SQL.

Problèmes relatifs à EntitySet<T>Si les propriétés publiques des membres de classes privés de type EntityRef<T>devraient être de type T (et non EntityRef<T>), ceci n’est plus vrai en ce qui concerneles propriétés publiques des membres de classes privés de type EntitySet<T>. Exami-nons le code généré par SQLMetal pour un membre de classe privé de type Entity-Set<T>.

Un membre de classe privé EntitySet<T> et ses propriétés

[Table(Name="dbo.Customers")]public partial class Customer : INotifyPropertyChanging, INotifyPropertyChanged{ ... private EntitySet<Order> _Orders; ... [Association(Name="FK_Orders_Customers", Storage="_Orders", OtherKey="CustomerID", DeleteRule="NO ACTION")] public EntitySet<Order> Orders { get { return this._Orders; } set { this._Orders.Assign(value); } }

Linq.book Page 476 Mercredi, 18. février 2009 7:58 07

Page 492: LINQ Language Integrated Query en C

Chapitre 15 Les classes d’entité LINQ to SQL 477

...}

Comme vous le voyez, le type de la propriété retournée est EntitySet<Order>, c’est-à-dire le même type que le membre de la classe privée. Comme EntitySet<T> implé-mente l’interface ICollection<T>, la propriété retournée peut être au format Entity-Set<T> si vous voulez cacher les détails de l’implémentation.

Si vous écrivez vos propres classes d’entité, vous devez également savoir que, lorsquevous utilisez une instruction d’affectation publique pour une propriété EntitySet<T>,vous devez passer par la méthode Assign, et non affecter la valeur au membre de laclasse EntitySet<T>. Cela permet à l’objet entité de continuer à utiliser la collectionoriginale d’objets entité associés, puisque cette dernière est peut-être déjà surveillée parle code de pistage de changements de l’objet DataContext.

Observez à nouveau le listing précédent. Vous verrez qu’au lieu d’affecter la variablemembre this._Orders à la valeur de la variable value la méthode Assign est utilisée.

Attributs des classes d’entité et propriétés des attributsLes classes d’entité sont définies par les attributs et les propriétés des attributs quimappent la totalité de la classe à une table d’une base de données et les propriétés de laclasse d’entité aux colonnes d’une table de la base de données. Les attributs définissentl’existence d’un mappage et les propriétés des attributs définissent comment se mapper.À titre d’exemple, c’est l’attribut Table qui définit qu’une classe est mappée sur unetable, mais c’est la propriété Name qui spécifie le nom de la table à laquelle la classe estmappée.

La meilleure façon de comprendre comment fonctionnent les attributs et les propriétésdes attributs consiste à examiner les attributs générés par un outil dédié à cette tâche. Àtitre d’exemple, nous allons examiner l’objet entité Customer généré par SQLMetal.

Voici une partie de la classe d’entité Customer :

[Table(Name="dbo.Customers")]public partial class Customer : INotifyPropertyChanging, INotifyPropertyChanged{ ... [Column(Storage="_CustomerID", DbType="NChar(5) NOT NULL", CanBeNull=false, IsPrimaryKey=true)] public string CustomerID { get { return this._CustomerID; } set { if ((this._CustomerID != value)) { this.OnCustomerIDChanging(value); this.SendPropertyChanging(); this._CustomerID = value; this.SendPropertyChanged("CustomerID");

Linq.book Page 477 Mercredi, 18. février 2009 7:58 07

Page 493: LINQ Language Integrated Query en C

478 LINQ to SQL Partie V

this.OnCustomerIDChanged(); } } }

...

[Association(Name="FK_Orders_Customers", Storage="_Orders", OtherKey="CustomerID", DeleteRule="NO ACTION")] public EntitySet<Order> Orders { get { return this._Orders; } set { this._Orders.Assign(value); } }

...

}}

Dans un souci de brièveté, nous n’avons conservé que les lignes qui contiennent desattributs LINQ to SQL (les attributs redondants ont également été supprimés).

Le bloc de code ci-après correspond à une procédure stockée et à une fonction définiepar l’utilisateur.

[Function(Name="dbo.Get Customer And Orders")][ResultType(typeof(GetCustomerAndOrdersResult1))][ResultType(typeof(GetCustomerAndOrdersResult2))]public IMultipleResults GetCustomerAndOrders([Parameter(Name="CustomerID", DbType="NChar(5)")] string customerID){ ...}

[Function(Name="dbo.MinUnitPriceByCategory", IsComposable=true)][return: Parameter(DbType="Money")]public System.Nullable<decimal> MinUnitPriceByCategory([Parameter(DbType="Int")] System.Nullable<int> categoryID){ ...}

Les attributs apparaissent en gras dans les fragments de code précédents. Ces fragmentsont été spécifiquement choisis pour alimenter notre discussion sur les attributs.

L’attribut DatabaseDans une classe dérivée de DataContext, l’attribut Database spécifie le nom par défautde la base de données mappée, si ce nom n’est pas spécifié dans les informations deconnexion lors de l’instanciation du DataContext. Si aucune de ces deux alternativesn’est définie, le nom de la classe dérivée de DataContext est supposé être celui de labase de données.

Linq.book Page 478 Mercredi, 18. février 2009 7:58 07

Page 494: LINQ Language Integrated Query en C

Chapitre 15 Les classes d’entité LINQ to SQL 479

Afin de clarifier les choses, voici l’ordre de précédence (du plus grand au plus petit) desdifférents endroits d’où peut provenir le nom de la base de données :

1. Les informations de connexion fournies lors de l’instanciation de la classe DataContext.

2. Le nom de la base de données spécifié dans l’attribut Database.

3. Le nom de la classe dérivée de DataContext.

Voici la portion de code correspondante de la classe Northwind dérivée de DataContextet générée par SQLMetal :

public partial class Northwind : System.Data.Linq.DataContext{

Comme vous pouvez le voir, l’attribut Database n’est pas spécifié dans la classeNorthwind générée, dérivée de la classe DataContext. Étant donné que cette classe aété générée par un programme Microsoft, nous pouvons supposer que ceci est inten-tionnel. Pour spécifier implicitement la base de données NorthwindTest via l’attributDatabase, vous devriez utiliser le code ci-après :

L’attribut Database

[Database(Name="NorthwindTest")]public partial class Northwind : System.Data.Linq.DataContext{

Je ne vois aucune raison d’omettre l’attribut Database. Peut-être est-ce parce que, si lenom de la base de données était indiqué dans les informations de connexion, il seraitprépondérant par rapport au nom de la classe dérivée de DataContext et à l’attributDatabase. Peut-être Microsoft a-t-il pensé que, si le nom de la base de données n’étaitpas spécifié dans les informations de connexion, ce serait celui de la classe dérivée deDataContext qui serait utilisé à la place.

Après avoir réfléchi à tout ceci, je n’aime pas beaucoup l’idée d’une classe générée,dérivée de DataContext, qui se connecterait à une base de données par défaut. La possi-bilité d’exécuter une application – peut-être non intentionnellement – qui n’a pasencore été configurée et qui accéderait à une base de données par défaut me rend parti-culièrement mal à l’aise. C’est pourquoi je vous recommande de spécifier un attributDatabase avec un nom intentionnellement ridicule dans le but d’éviter toute connexionà une base de données par défaut. Peut-être quelque chose comme ceci :

Une classe dérivée de DataContext qui n’a pratiquement aucune chance de se connec-ter à une base de données par défaut

[Database(Name="goopeygobezileywag")]public partial class Northwind : System.Data.Linq.DataContext{

Ce code empêchera la connexion à une base de données par défaut, à moins que sonnom n’ait été spécifié dans les informations de connexion lors de l’instanciation duDataContext.

Linq.book Page 479 Mercredi, 18. février 2009 7:58 07

Page 495: LINQ Language Integrated Query en C

480 LINQ to SQL Partie V

Name (string)

La propriété Name d’un attribut est un objet de type string qui spécifie le nom de labase de données à laquelle se connecter si aucun nom n’a été indiqué dans les informa-tions de connexion lors de l’instanciation de la classe dérivée de DataContext. Si lapropriété Name n’est pas spécifiée et que le nom de la base de données n’est pas indiquédans les informations de connexion, le nom de la classe dérivée de DataContext estsupposé être celui de la base de données.

Table

L’attribut Table indique dans quelle table de la base de données la classe d’entité doitêtre enregistrée (le nom de la classe d’entité n’est pas forcément le même que celui dela table). Voici la portion de code correspondante dans la classe d’entité :

L’attribut Table

[Table(Name="dbo.Customers")]public partial class Customer : INotifyPropertyChanging, INotifyPropertyChanged{

L’attribut Table spécifie le nom de la table de la base de données via la propriété Name.Si le nom de la classe d’entité est le même que celui de la table, la propriété Name peutêtre omise : par défaut, le nom de la classe sera alors celui de la table sur laquelle elleest mappée.

Dans cet exemple, l’option "pluriel" ayant été spécifiée à SQLMetal lors de la généra-tion des classes d’entité de la base de données Northwind par défaut, le nom de la classeest la version au singulier (Customer) du nom de la table de la base de données (Custo-mers). Le nom de la classe étant différent du nom de la table, la propriété Name doit êtrespécifiée.

Name (string)

La propriété Name d’un attribut est un objet de type string qui spécifie le nom de latable sur laquelle la classe d’entité doit être mappée. Si la propriété Name n’est passpécifiée, le nom de la classe d’entité sera mappé par défaut à une table de mêmenom.

Column

L’attribut Column met en relation une propriété d’une classe d’entité et une colonnedans une table d’une base de données. Voici la portion de code correspondante dans laclasse d’entité :

L’attribut Column

Private string _CustomerID;...[Column(Storage="_CustomerID", DbType="NChar(5) NOT NULL", CanBeNull=false, IsPrimaryKey=true)]public string CustomerID{

Linq.book Page 480 Mercredi, 18. février 2009 7:58 07

Page 496: LINQ Language Integrated Query en C

Chapitre 15 Les classes d’entité LINQ to SQL 481

Dans cet exemple, la propriété Storage de l’attribut Column est spécifiée. LINQ to SQLpeut donc accéder directement à la variable membre privée _CustomerID, en évitant depasser par l’accesseur de propriété publique CustomerID. Si la propriété Storage del’attribut n’était pas spécifiée, l’accesseur public serait utilisé. Ce détail peut se révélerutile si vous voulez éviter d’exécuter le code qui se trouve dans les accesseurs depropriétés publiques.

Le type de la base de données est spécifié dans l’attribut DbType. Ici, Nchar, de longueur5 caractères. L’attribut CanBeNull étant initialisé à la valeur false, le champ Custo-mer-ID ne peut avoir la valeur Null dans la base de données. Comme l’attributIsPrimaryKey est spécifié et a la valeur true, cette colonne est un identifiant.

Toutes les propriétés d’une classe d’entité ne sont pas nécessairement mappées à la basede données. Certaines propriétés n’ont une signification qu’à l’exécution et ne doiventpas être sauvegardées dans la base de données. Pour ces propriétés, vous omettrezl’attribut Column.

Certaines colonnes sauvegardées dans la base de données peuvent n’être accessiblesqu’en lecture seulement. Pour ce faire, il suffit de mapper la colonne, de spécifier lapropriété Storage de l’attribut pour référencer la variable membre privée, mais de nepas implémenter la méthode set de la propriété. Le DataContext peut toujours accéderau membre privé, mais, étant donné que la méthode set n’est pas définie, sa valeur nepeut être modifiée.

AutoSync (AutoSync enum)La propriété Autosync d’un attribut est de type Autosync (enum). À l’exécution, elledemande de récupérer la valeur de la colonne mappée après une opération insert ouupdate dans la base de données. Les valeurs possibles sont Default, Always, Never,OnInsert et OnUpdate. Quelle est d’après vous la valeur utilisée par défaut ? Si l’on seréfère à la documentation Microsoft, il s’agit de la valeur Never.

Cette propriété est surchargée lorsque la propriété IsDbGenerated ou IsVersion estinitialisée à true.

CanBeNull (bool)La propriété CanBeNull d’un attribut est un booléen qui indique si une valeur decolonne dans la base de données mappée peut être nulle. La valeur par défaut esttrue.

DbType (string)La propriété DbType d’un attribut est de type string. Elle indique le type de la colonneà laquelle cette propriété de classe d’entité est mappée. Si la propriété DbType n’est pas

Linq.book Page 481 Mercredi, 18. février 2009 7:58 07

Page 497: LINQ Language Integrated Query en C

482 LINQ to SQL Partie V

spécifiée, le type de la colonne sera inféré à partir du type de donnée de la propriété dela classe d’entité. Cette propriété n’est utilisée que dans l’appel à la méthode Create-Database.

Expression (string)La propriété Expression d’un attribut est de type string. Elle définit une colonnecalculée dans la base de données. Elle n’est utilisée que conjointement à la méthodeCreateDatabase. La valeur par défaut est String.Empty.

IsDbGenerated (bool)La propriété IsDbGenerated d’un attribut est de type bool. Elle indique si la colonne dela table de la base de données sur laquelle est mappée la propriété de classe est automa-tiquement générée par la base de données. Si une clé primaire est spécifiée avec unepropriété d’attribut IsDbGenerated initialisée à true, la propriété DbType de l’attributde la propriété de classe doit être initialisée à IDENTITY.

Une propriété de classe dont la propriété IsDbGenerated de l’attribut est initialisée àtrue est immédiatement synchronisée après qu’un enregistrement eut été inséré dans labase de données, et ce quel que soit la valeur de la propriété de l’attribut AutoSync. Parailleurs, la valeur synchronisée de la propriété de classe sera visible dans la propriété declasse dès que la méthode SubmitChanges aura été totalement exécutée. La valeur pardéfaut de cette propriété est false.

IsDiscriminator (bool)La propriété IsDiscriminator d’un attribut est de type bool. Elle indique si lapropriété de la classe d’entité mappée contient une valeur de discriminant pour unehiérarchie d’héritage. La valeur par défaut de cette propriété est false. Reportez-vous àla section relative à l’attribut InheritanceMapping, un peu plus loin dans ce chapitre, età la section "Héritage des classes d’entité", au Chapitre 18, pour avoir des renseignementscomplémentaires.

IsPrimaryKey (bool)La propriété IsPrimaryKey d’un attribut est de type bool. Elle indique si la colonnede la table à laquelle est mappée cette propriété de classe d’entité est une cléprimaire. Plusieurs propriétés de la classe peuvent être spécifiées comme clésprimaires. Dans ce cas, toutes les colonnes de la base de données mappée se compor-tent comme une clé primaire composite. Pour qu’un objet entité puisse être mis àjour, au moins une des propriétés des classes d’entité doit avoir une propriété d’attri-but IsPrimaryKey initialisée à true. Dans le cas contraire, les objets entité mappés àcette table ne seront accessibles qu’en lecture seule. La valeur par défaut de cettepropriété est false.

Linq.book Page 482 Mercredi, 18. février 2009 7:58 07

Page 498: LINQ Language Integrated Query en C

Chapitre 15 Les classes d’entité LINQ to SQL 483

IsVersion (bool)La propriété IsVersion d’un attribut est de type bool. Elle indique si la colonne de labase de données mappée est un numéro de version ou un timestamp (compteur numéri-que servant de référence temporelle) qui représente une information de version pourl’enregistrement. Si la propriété IsVersion est spécifiée et initialisée à true, la colonnede la base de données mappée est incrémentée (s’il s’agit d’un numéro de version) oumise à jour (s’il s’agit d’un timestamp) chaque fois que l’enregistrement de la table dela base de données est mis à jour.

Une propriété de classe dont la propriété IsVersion d’un attribut vaut true est immé-diatement synchronisée après l’insertion ou la mise à jour d’un enregistrement dans labase de données, et ce quelle que soit la valeur de la propriété Autosync. Par ailleurs, lavaleur synchronisée de la propriété de la classe sera visible dans la propriété de la classelorsque la méthode SubmitChanges aura été exécutée. La valeur par défaut de cettepropriété est false.

Name (string)La propriété d’un attribut Name est de type string. Elle spécifie le nom de la colonne surlaquelle cette propriété de classe est mappée. Si la propriété Name n’est pas spécifiée, lapropriété de classe sera mappée par défaut sur une colonne de même nom.

Storage (string) La propriété Storage d’un attribut est de type string. Elle indique la variable membreprivée dans laquelle est mémorisée la valeur de la propriété de la classe d’entité. Cettepropriété permet à LINQ to SQL de ne pas utiliser les accesseurs publics des propriétéset la logique métier qu’ils contiennent et d’accéder directement à la variable membreprivée. Si la propriété Storage n’est pas spécifiée, les accesseurs publics de propriétéssont utilisés par défaut.

UpdateCheck (UpdateCheck enum)

La propriété UpdateCheck est de type UpdateCheck (enum). Elle contrôle le comporte-ment de la détection de conflits concurrentiels, dans le cas où aucune propriété mappéen’a une propriété d’attribut IsVersion initialisée à true. Les trois valeurs possiblessont UpdateCheck.Always, UpdateCheck.WhenChanged et UpdateCheck.Never. Siaucune propriété d’une classe d’entité n’a une propriété IsVersion initialisée à true, lavaleur de la propriété d’attribut UpdateCheck aura la valeur par défaut Always. Repor-tez-vous au Chapitre 17 pour avoir plus d’informations sur cette propriété et sur seseffets.

Association

L’attribut Association est utilisé pour définir des relations entre deux tables. Par exem-ple une relation entre une clé primaire et une clé étrangère. Dans l’exemple qui suit,l’entité dont la table mappée contient la clé primaire est appelée "parent" et l’entité dont

Linq.book Page 483 Mercredi, 18. février 2009 7:58 07

Page 499: LINQ Language Integrated Query en C

484 LINQ to SQL Partie V

la table mappée contient la clé étrangère est appelée "enfant". Voici les portions de codecorrespondantes extraites de deux classes d’entité liées par une association :

L’association de la classe d’entité parent (Customer)

[Association(Name="FK_Orders_Customers", Storage="_Orders", OtherKey="CustomerID",DeleteRule="NO ACTION")]public EntitySet<Order> Orders{

L’association de la classe d’entité enfant (Order)

[Association(Name="FK_Orders_Customers", Storage="_Customer", ThisKey="CustomerID",IsForeignKey=true)]public Customer Customer{

Dans cet exemple, nous utilisons la classe d’entité parent Customer et la classe d’entitéenfant Order. Les attributs Association qui existent dans les deux classes d’entité ontété initialisés en conséquence.

Certaines propriétés de l’attribut Association se rapportent à la classe dans laquellel’attribut Association existe. D’autres seront relatives à la classe d’entité associée.Dans ce contexte, la classe dans laquelle l’attribut Association existe sera appelée"classe source", et la classe associée sera appelée "classe cible". Donc, si nous discu-tons des propriétés de l’attribut Association spécifié dans la classe d’entité Customer,cette dernière sera la classe source, et la classe d’entité Order sera la classe cible. Sinous parlons des propriétés de l’attribut Association spécifié dans la classe d’entitéOrder, cette dernière sera la classe source, et la classe d’entité Customer sera la classecible.

L’attribut Association indique que la classe d’entité source (Customer) a une relationavec la classe d’entité cible (Order).

Dans les exemples précédents, la propriété Name est spécifiée pour donner un nom à larelation. La valeur de cette propriété correspond au nom de la clé étrangère de la basede données. Elle sera utilisée pour créer une restriction de clé étrangère si la méthodeCreateDatabase est appelée. La propriété Storage est également spécifiée. Par sonintermédiaire, LINQ to SQL peut contourner les accesseurs publics pour accéder direc-tement à la valeur de la propriété de la classe d’entité.

Une classe d’entité parent mémorise la référence à la classe ou aux classes d’entitéenfant dans une collection EntitySet<T>. En effet, il se peut qu’il y ait plusieursenfants. En revanche, une classe d’entité enfant mémorise la référence à la classed’entité parent dans un EntityRef<T>. En effet, il ne peut y avoir qu’un seul parent.Reportez-vous aux sections intitulées "EntitySet<T>" et "EntityRef<T>", au Chapi-tre 14, pour avoir de plus amples informations sur les associations et leurs caractéris-tiques.

Linq.book Page 484 Mercredi, 18. février 2009 7:58 07

Page 500: LINQ Language Integrated Query en C

Chapitre 15 Les classes d’entité LINQ to SQL 485

DeleteOnNull (bool)La propriété DeleteOnNull d’un attribut est de type bool. Elle indique si un objet entitésitué du côté enfant d’une association doit être supprimé lorsque la référence à sonparent a pour valeur null.

La valeur de cette propriété est déduite par SQLMetal si une règle Delete "en cascade"est spécifiée pour définir une contrainte sur la clé étrangère et si la colonne de la cléétrangère n’autorise pas la valeur null.

DeleteRule (string)La propriété DeleteRule d’un attribut est de type string. Elle ajoute le comportement desuppression à une association. Elle est seulement utilisée par LINQ to SQL lorsqu’unecontrainte est créée dans la base de données par la méthode CreateDatabase.

Les valeurs possibles sont "NO ACTION", "CASCADE", "SET NULL" et "SET DEFAULT". Sinécessaire, reportez-vous à votre documentation SQL Server pour prendre connais-sance de la signification de ces valeurs.

IsForeignKey (bool)La propriété IsForeignKey d’un attribut est de type bool. Si elle est initialisée à lavaleur true, elle indique que la classe d’entité source est le côté de la relation quicontient la clé étrangère. C’est donc le côté enfant de la relation. La valeur par défaut decette propriété est false.

Dans les exemples de l’attribut Association précédents, mettant en jeu les classesd’entité Customer et Order, la propriété IsForeignKey de l’attribut Association estinitialisé à true dans la classe d’entité Order. Cette dernière joue donc le rôle del’enfant dans la relation.

IsUnique (bool)La propriété IsUnique d’un attribut est de type bool. Si elle vaut true, elle indique quela clé étrangère est unique. Il s’agit donc d’une relation "un-à-un" entre les deux classesd’entité. La valeur par défaut de cette propriété est false.

Name (string)La propriété Name d’un attribut est une chaîne qui spécifie le nom de la contrainte appli-quée à la clé étrangère. La contrainte est définie lorsque la méthode CreateDatabase estappelée. Cette propriété permet également de différencier des relations multiples entredeux entités. Dans ce cas, si les côtés parent et enfant de la relation spécifient tous deuxun nom, il doit être identique.

S’il n’existe aucune relation multiple entre deux classes d’entité et que la méthodeCreateDatabase ne soit pas appelée, cette propriété n’a aucun intérêt.

Cette propriété n’a aucune valeur par défaut.

Linq.book Page 485 Mercredi, 18. février 2009 7:58 07

Page 501: LINQ Language Integrated Query en C

486 LINQ to SQL Partie V

OtherKey (string)La propriété OtherKey d’un attribut est une chaîne délimitée par des virgules. Elle listetoutes les propriétés de la classe d’entité cible qui constituent la clé (primaire ou étran-gère, en fonction du côté de la relation dans lequel se trouve l’entité cible). Si cettepropriété n’est pas spécifiée, les membres clé primaire de la classe d’entité cible sontutilisés par défaut.

Il est important de réaliser que l’attribut Association, spécifié de chaque côté del’association (Customer et Order), indique où se trouvent les clés des deux côtés de larelation. L’attribut Association spécifié dans la classe d’entité Customer spécifiequelle propriété de la classe d’entité Customer contient la clé pour la relation et quellepropriété de la classe d’entité Order contient la clé pour la relation.

Les deux côtés d’une relation ne spécifient pas toujours l’emplacement des clés desdeux côtés de la relation. Ceci parce que, généralement, du côté parent de la relation laclé primaire de la table est la clé utilisée. La propriété ThisKey n’a pas besoin d’êtrespécifiée, puisque la clé primaire est la valeur par défaut. Du côté enfant, la propriétéOtherKey n’a pas besoin d’être spécifiée, car la clé primaire du parent est la valeur pardéfaut. Par conséquent, il est courant de voir la propriété OtherKey spécifiée unique-ment du côté parent et la propriété ThisKey, uniquement du côté enfant. Grâce auxvaleurs par défaut, chaque côté de la relation connaît la clé de l’autre.

Storage (string)La propriété Storage d’un attribut est de type string. Elle spécifie la variable membreprivée dans laquelle est stockée la valeur provenant de la base de données. Par son inter-médiaire, LINQ to SQL peut accéder directement à la variable membre privée, sanspasser par les accesseurs publics des propriétés des classes d’entité. Ceci permet de nepas dépendre d’une éventuelle logique métier qui se trouverait dans les accesseurs. Si lapropriété Storage n’est pas spécifiée, les accesseurs publics de la propriété sont utiliséspar défaut.

Microsoft recommande que les deux membres d’une association soient des propriétésde classe d’entité, qu’ils stockent les données dans des variables membres de classesd’entité séparées et qu’enfin la propriété Storage soit spécifiée.

ThisKey (string)La propriété ThisKey de l’attribut est une chaîne contenant des données délimitées pardes virgules. Elle liste toutes les propriétés de la classe d’entité source qui constituent laclé (primaire ou étrangère, en fonction du côté de la relation dans lequel se trouvel’entité cible). Si cette propriété n’est pas spécifiée, les membres clé primaire de laclasse d’entité source sont utilisés par défaut.

Quelques pages plus tôt, l’attribut Association pris en exemple pour la classe d’entitéCustomer ne contenait pas la propriété IsForeignKey. Nous savons donc que la classe

Linq.book Page 486 Mercredi, 18. février 2009 7:58 07

Page 502: LINQ Language Integrated Query en C

Chapitre 15 Les classes d’entité LINQ to SQL 487

d’entité Customer est le côté parent de la relation, c’est-à-dire le côté qui contient la cléprimaire. Comme l’attribut Association ne spécifiait pas la propriété d’attribut This-Key, nous savons que la valeur de la clé primaire de la table Customer est la clé étran-gère de la table associée, Orders.

Comme l’attribut Association de la classe d’entité Order prise en exemple précédem-ment spécifie l’attribut IsForeignKey et lui affecte la valeur true, nous savons que latable Orders est le côté de l’association qui contient la clé étrangère. Par ailleurs,comme l’attribut Association spécifie la propriété ThisKey et lui affecte la valeurCustomerID, nous savons que la clé étrangère sera stockée dans la colonne CustomerIDde la table Orders.

Il est important de réaliser que l’attribut Association spécifié de chaque côté de l’asso-ciation (Customer et Order) indique où les clés sont situées des deux côtés de l’associa-tion. L’attribut Association spécifié dans la classe d’entité Customer spécifie quellepropriété de la classe d’entité Customer contient la clé pour la relation et quellepropriété de la classe d’entité Order contient la clé pour la relation. De la même façon,l’attribut Association spécifié dans la classe d’entité Order indique quelle propriété declasse d’entité Order contient la clé pour la relation et quelle propriété de la classed’entité Customer contient la clé pour la relation.

Les deux côtés d’une relation ne spécifient pas toujours l’emplacement des clés desdeux côtés de la relation. Ceci parce que, généralement, du côté parent de la relation laclé primaire de la table est la clé utilisée. La propriété ThisKey n’a pas besoin d’êtrespécifiée, puisque la clé primaire est la valeur par défaut. Du côté enfant, la propriétéOtherKey n’a pas besoin d’être spécifiée, car la clé primaire du parent est la valeur pardéfaut. Par conséquent, il est courant de voir la propriété OtherKey spécifiée unique-ment du côté parent et la propriété ThisKey, uniquement du côté enfant. Grâce auxvaleurs par défaut, chaque côté de la relation connaît la clé de l’autre.

Function

L’attribut Function définit la procédure stockée ou la fonction définie par l’utilisateur(valeur scalaire ou table) appelée lors de l’appel d’une méthode de classe. Voici laportion de code dérivée de la classe DataContext pour une procédure stockée.

Une fonction Attribute qui mappe une méthode vers une procédure stockée dans labase de données Northwind

[Function(Name="dbo.Get Customer And Orders")][ResultType(typeof(GetCustomerAndOrdersResult1))][ResultType(typeof(GetCustomerAndOrdersResult2))]public IMultipleResults GetCustomerAndOrders( [Parameter(Name="CustomerID", DbType="NChar(5)")] string customerID){ ...}

Linq.book Page 487 Mercredi, 18. février 2009 7:58 07

Page 503: LINQ Language Integrated Query en C

488 LINQ to SQL Partie V

Dans ce code, la méthode GetCustomerAndOrders appelle la procédure stockée "GetCustomers And Orders". Nous savons que la méthode sera mappée à une procédurestockée, et non à une fonction définie par l’utilisateur. En effet, la propriété IsComposa-ble n’étant pas spécifiée, sa valeur par défaut est false et le mappage se fait vers uneprocédure stockée. Nous pouvons également voir que la fonction retourne plusieursformes de résultats, car deux attributs ResultType sont spécifiés.

L’écriture d’une classe dérivée de DataContext qui est capable d’appeler une procédurestockée n’est pas aussi simple que le mappage d’une classe d’entité à une table. En plusde spécifier les bons attributs, vous devez également appeler la bonne version de laméthode ExecuteMethodCall de la classe DataContext. Vous en apprendrez plus à cesujet au Chapitre 16.

Bien entendu, ceci n’est nécessaire que si vous écrivez votre classe DataContext à lamain. Dans le cas contraire, SQLMetal ou le Concepteur Objet/Relationnel feront letravail à votre place.

Voici la portion de code dérivée de la classe DataContext pour une fonction définie parl’utilisateur.

Une fonction Attribute qui mappe une méthode vers une fonction définie par l’utilisa-teur dans la base de données Northwind

[Function(Name="dbo.MinUnitPriceByCategory", IsComposable=true)][return: Parameter(DbType="Money")]public System.Nullable<decimal> MinUnitPriceByCategory([Parameter(DbType="Int")] System.Nullable<int> categoryID){...}

Dans ce code, la méthode MinUnitPriceByCategory appelle la fonction définie parl’utilisateur "MinUnitPriceByCategory". Nous savons que la méthode sera mappée àune fonction définie par l’utilisateur, et non à une procédure stockée. En effet, lapropriété IsComposable est initialisée à true. Nous pouvons également voir dansl’attribut return que la fonction définie par l’utilisateur retournera une valeur de typeMoney.

L’écriture d’une classe dérivée de DataContext qui est capable d’appeler une fonctiondéfinie par l’utilisateur n’est pas aussi simple que le mappage d’une classe d’entité àune table. En plus de spécifier les bons attributs, vous devez également appeler la bonneversion de la méthode ExecuteMethodCall (pour les fonctions qui renvoient une valeurde type scalaire) ou de la méthode CreateMethodCallQuery (pour les fonctions quirenvoient une valeur de type table). Vous en apprendrez plus à ce sujet au Chapitre 16.

Bien entendu, ceci n’est nécessaire que si vous écrivez votre classe DataContext à lamain. Dans le cas contraire, SQLMetal ou le Concepteur Objet/Relationnel feront letravail à votre place.

Linq.book Page 488 Mercredi, 18. février 2009 7:58 07

Page 504: LINQ Language Integrated Query en C

Chapitre 15 Les classes d’entité LINQ to SQL 489

IsComposable (bool)La propriété IsComposable d’un attribut est de type bool. Elle indique si la fonctionmappée appelle une procédure stockée (false) ou une fonction définie par l’utilisateur(true). Lorsqu’elle n’est pas spécifiée, la valeur par défaut de cette propriété est false.Une méthode mappée avec l’attribut Function appellera donc une procédure stockée sila propriété d’attribut IsComposable n’est pas spécifiée.

Name (string)La propriété Name d’un attribut est un string. Elle indique le nom actuel de la procé-dure stockée ou de la fonction définie par l’utilisateur dans la base de données. Si lapropriété Name n’est pas spécifiée, le nom de la procédure stockée ou de la fonction défi-nie par l’utilisateur est supposé être le même que celui de la méthode.

return

L’attribut return est utilisé pour spécifier la donnée retournée par une procédure stockéeou une fonction définie par l’utilisateur. Il contient généralement un attribut Parameter.

Un attribut return pour la classe Northwind

[Function(Name="dbo.MinUnitPriceByCategory", IsComposable=true)][return: Parameter(DbType="Money")]public System.Nullable<decimal> MinUnitPriceByCategory( [Parameter(DbType="Int")] System.Nullable<int> categoryID){ ...}

Dans ce code, la ligne en gras nous montre que la fonction définie par l’utilisateurretournera une valeur de type Money (voir attribut return et propriété dbType de l’attri-but Parameter).

ResultType

L’attribut ResultType indique le ou les types de données qui peuvent être retournés parune procédure stockée. Si une procédure stockée retourne plusieurs formes, plusieursattributs ResultType doivent être spécifiés, dans l’ordre de leur retour.

Attributs ResultType issus de la classe Northwind

[Function(Name="dbo.Get Customer And Orders")][ResultType(typeof(GetCustomerAndOrdersResult1))][ResultType(typeof(GetCustomerAndOrdersResult2))]public IMultipleResults GetCustomerAndOrders( [Parameter(Name="CustomerID", DbType="NChar(5)")] string customerID){ ...}

En examinant le code précédent, nous voyons que la procédure stockée à laquelle cetteméthode est mappée retournera une forme de type GetCustomerAndOrdersResult1,puis une forme de type GetCustomerAndOrdersResult2. SQLMetal est assez prévenant

Linq.book Page 489 Mercredi, 18. février 2009 7:58 07

Page 505: LINQ Language Integrated Query en C

490 LINQ to SQL Partie V

pour générer automatiquement des classes d’entité pour GetCustomerAndOrders-Result1 et GetCustomerAndOrdersResult2.

Parameter

L’attribut Parameter mappe un paramètre d’une méthode à un paramètre d’une procé-dure stockée/d’une fonction définie par l’utilisateur. Voici une portion de code corres-pondante dans la classe dérivée de DataContext pour la base de données Northwind :

Un attribut Parameter issu de la classe Northwind

[Function(Name="dbo.Get Customer And Orders")][ResultType(typeof(GetCustomerAndOrdersResult1))][ResultType(typeof(GetCustomerAndOrdersResult2))]public IMultipleResults GetCustomerAndOrders( [Parameter(Name="CustomerID", DbType="NChar(5)")] string customerID){ ...}

En examinant ce code, nous voyons que la méthode GetCustomersAndOrders, mappéeà la procédure stockée "Get Customers And Orders", lui passe le paramètre CustomerIDde type NChar(5).

DbType (string)La propriété DbType d’un attribut est au format string. Elle indique le type d’un para-mètre de la procédure stockée/de la fonction définie par l’utilisateur.

Name (string)La propriété Name d’un attribut est au format string. Elle spécifie le nom actuel duparamètre de la procédure stockée/de la fonction définie par l’utilisateur. Si la propriétéName n’est pas spécifiée, ce nom est supposé être identique à celui du paramètre de laméthode.

InheritanceMapping

L’attribut InheritanceMapping est utilisé pour mapper un "code discriminant" à uneclasse de base ou une sous-classe de cette classe de base. Le code discriminant est lavaleur de la colonne discriminateur d’une classe d’entité (celle dont la propriété IsDis-criminator de l’attribut vaut true). Examinons l’attribut InheritanceMapping :

[InheritanceMapping(Code = "G", Type = typeof(Shape), IsDefault = true)]

L’attribut InheritanceMapping précédent indique que si le code discriminant d’unenregistrement de la base de données vaut "G" (en d’autres termes, si la valeur de sacolonne discriminateur est "G"), cet enregistrement sera instancié en tant qu’un objetShape en utilisant la classe Shape. La propriété IsDefault de l’attribut étant initialiséeà true, si le code discriminant d’un enregistrement ne correspond à aucune des valeursCode des attributs InheritanceMapping, cet enregistrement sera instancié en tantqu’objet Shape en utilisant la classe Shape.

Linq.book Page 490 Mercredi, 18. février 2009 7:58 07

Page 506: LINQ Language Integrated Query en C

Chapitre 15 Les classes d’entité LINQ to SQL 491

Pour utiliser le mappage d’héritage, il suffit d’affecter la valeur true à la propriété quicorrespond à la propriété Column de l’attribut IsDiscriminator lors de la déclaration dela classe d’entité de base. La valeur de cette colonne détermine donc, par discrimina-tion, de quelle classe (classe de base ou sous-classe) un enregistrement d’une tabled’une base de données est une instance. Un attribut InheritanceMapping est précisédans la classe de base pour chacune de ses sous-classes et pour la classe de base elle-même. Un et un seul de ces attributs InheritanceMapping doit avoir une propriétéIsDefault initialisée à true. Un enregistrement d’une table de la base de données dontla colonne discriminateur ne correspond à aucun des codes discriminants spécifiés dansles attributs InheritanceMapping sera donc quand même instancié dans une classe. Ilest probablement plus courant pour l’attribut InheritanceMapping de la classe de based’être spécifié comme l’attribut InheritanceMapping par défaut.

Rappelons à nouveau que les attributs InheritanceMapping sont seulement spécifiésdans la classe de base et qu’ils associent un code discriminant à la classe de base ou àune de ses sous-classes.

Aucune table de la base de données Northwind n’étant utilisée de la sorte, nous allonsfaire appel à trois autres classes.

Quelques classes exemples pour illustrer le mappage d’héritage

[Table][InheritanceMapping(Code = "G", Type = typeof(Shape), IsDefault = true)][InheritanceMapping(Code = "S", Type = typeof(Square))][InheritanceMapping(Code = "R", Type = typeof(Rectangle))]public class Shape{ [Column(IsPrimaryKey = true, IsDbGenerated = true, DbType = "Int NOT NULL IDENTITY")] public int Id;

[Column(IsDiscriminator = true, DbType = "NVarChar(2)")] public string ShapeCode;

[Column(DbType = "Int")] public int StartingX;

[Column(DbType = "Int")] public int StartingY;}

public class Square : Shape{ [Column(DbType = "Int")] public int Width;}

public class Rectangle : Square{ [Column(DbType = "Int")] public int Length;}

Linq.book Page 491 Mercredi, 18. février 2009 7:58 07

Page 507: LINQ Language Integrated Query en C

492 LINQ to SQL Partie V

Dans ce listing, la classe Shape a été mappée à une table. La propriété Name de l’attributn’ayant pas été spécifiée, la classe Shape sera mappée par défaut à une table nomméeShape.

Les premières lignes définissent trois attributs InheritanceMapping. Le premier indi-que que, si la valeur de la colonne discriminateur d’un enregistrement de la table Shapea pour valeur "G", cet enregistrement doit être instancié en tant qu’objet Shape en utili-sant la classe Shape. Ici, la lettre "G" signifie "générique". Il s’agit donc d’une formenon définie générique.

Le discriminant étant la propriété ShapeCode de la classe Shape (sa propriété IsDiscri-minator est initialisée à true), si un enregistrement a une propriété ShapeCode égale à"G", il sera instancié en un objet Shape.

Vous pouvez également remarquer que le premier attribut InheritanceMapping a unepropriété IsDefault initialisée à true. Si la colonne ShapeCode d’un enregistrementShape ne correspond à aucun des codes discriminants spécifiés ("G", "S" et "R"), lemappage par défaut sera utilisé et l’enregistrement sera instancié en tant qu’objetShape.

Le deuxième attribut InheritanceMapping associe le code discriminant "S" à la classeSquare. Si un enregistrement dans la table Shape a un ShapeCode égal à "S", il serainstancié en tant qu’objet Square.

Le troisième attribut InheritanceMapping associe le code discriminant "R" à la classeRectangle. Si un enregistrement dans la table Shape a un ShapeCode égal à "R", il serainstancié en tant qu’objet Rectangle.

Tout enregistrement dont le ShapeCode diffère des trois valeurs spécifiées sera instanciéen un objet Shape, car la classe Shape a été spécifiée par défaut en utilisant la propriétéIsDefault.

INFO

Vous trouverez une présentation plus complète et des exemples du mappage d’héritage auChapitre 18.

Code (object)La propriété Code d’un attribut spécifie quel est le code discriminant du mappage de laclasse spécifiée dans la propriété Type de l’attribut.

IsDefault (bool)La propriété IsDefault d’un attribut est de type bool. Elle indique si l’attribut Inheri-tanceMapping doit être utilisé dans le cas où une colonne discriminateur d’un enregis-trement d’une table de la base de données ne correspond à aucun des attributsInheritanceMapping spécifiés.

Linq.book Page 492 Mercredi, 18. février 2009 7:58 07

Page 508: LINQ Language Integrated Query en C

Chapitre 15 Les classes d’entité LINQ to SQL 493

Type (Type)La propriété Type d’un attribut spécifie le type de la classe en lequel l’enregistrementsera instancié lorsque la colonne discriminateur correspond au code discriminantmappé.

Compatibilité de type de donnéeCertains attributs de classe d’entité possèdent une propriété DbType qui permet despécifier le type de donnée de la colonne. Cette propriété n’est utilisée qu’à la créationde la base de données avec la méthode CreateDatabase. Le mappage entre les types dedonnées .NET et les types de données SQL Server n’étant pas de type "un-à-un", vousdevrez spécifier la propriété DbType si vous utilisez la méthode CreateDatabase.

Étant donné que les types de données de la CLR (Common Language Runtime) .NETutilisés dans le code LINQ ne sont pas identiques à ceux utilisés dans la base dedonnées, vous devriez consulter la documentation MSDN à la rubrique "SQL-CLRType Mapping (LINQ to SQL)". Vous y trouverez un tableau qui définit les correspon-dances entre les types CLR et les types SQL. Si vous avez du mal à trouver ces informa-tions, reportez-vous à la section "Additional LINQ to SQL Resources" sur le site webde l’ouvrage, www.linqdev.com. Vous y trouverez un lien qui mène directement à labonne page sur MSDN.

Faites attention, toutes les conversions ne sont pas possibles, et d’autres peuvent provo-quer la perte de données, en fonction du type des données impliquées et du sens de laconversion !

N’ayez crainte, le processus de conversion fonctionnera convenablement dans laplupart des situations. Lors de la mise au point des exemples de ce chapitre, je n’airencontré aucun problème lié à la conversion des types de données. Dans tous les cas,utilisez votre bon sens : si vous essayez de convertir des types incompatibles, par exem-ple un type numérique .NET en un type caractère SQL, vous courez droit vers unproblème de conversion…

Schéma de fichier de mappage externe XML

Comme il a été dit dans la section relative à SQLMetal du Chapitre 13, cet outil peutêtre utilisé pour mapper des classes à une base de données, mais vous pouvez égalementpasser par un fichier de mappage externe XML. Vous en apprendrez plus à ce sujet lorsquenous parlerons des constructeurs de la classe DataContext, au Chapitre 16.

La façon la plus simple d’obtenir un fichier de mappage externe XML (voir Chapi-tre 13) consiste à utiliser le programme SQLMetal, en spécifiant l’option /map. Si vousavez l’intention de définir le fichier de mappage manuellement, vous devrez connaîtrele schéma à utiliser.

Linq.book Page 493 Mercredi, 18. février 2009 7:58 07

Page 509: LINQ Language Integrated Query en C

494 LINQ to SQL Partie V

Reportez-vous à la documentation MSDN, et en particulier à la page "ExternalMapping Reference (LINQ to SQL)" pour avoir des renseignements complémentaires àce sujet. Si vous avez du mal à trouver cette page, reportez-vous à la section "AdditionalLINQ to SQL Resources" sur le site web de l’ouvrage, www.linqdev.com. Vous y trou-verez un lien qui mène directement à la bonne page sur MSDN.

Projection dans des classes d’entité/des classes de non-entité

Lorsque vous effectuez des requêtes LINQ to SQL, les résultats retournés peuvent êtreprojetés dans une classe d’entité ou dans une classe de non-entité (une classe nomméeou anonyme). Il existe une différence majeure entre ces deux types de projections.

Lors d’une projection dans une classe d’entité, cette dernière profite de la recherched’identité de l’objet DataContext, ainsi que de la recherche de changement et des servi-ces associés. Ainsi, vous pouvez effectuer des modifications dans la classe d’entité etles rendre persistantes dans la base de données en utilisant la méthode SubmitChanges.

Lors d’une projection dans une classe de non-entité, mis à part un cas bien particulier,vous ne pouvez pas profiter de la recherche d’identité de l’objet DataContext, de larecherche de changement et des services associés. Cela signifie que vous ne pouvez pasmodifier la classe de non-entité et voir les modifications reportées dans la base dedonnées en utilisant LINQ to SQL. Ceci est facile à comprendre. Effectivement, laclasse n’a pas les attributs ou le fichier de mappage nécessaires pour mapper la classe àla base de données. Si elle les avait, ce serait par définition… une classe d’entité.

Voici un exemple de requête qui se projette dans une classe d’entité :

La projection dans une classe d’entité donne accès aux services du DataContext

IEnumerable<Customer> custs = from c in db.Customers select c;

Après avoir exécuté cette requête, nous pourrions modifier n’importe quel objet entitéCustomer en utilisant la séquence custs et rendre des modifications permanentes enappelant la méthode SubmitChanges.

Voici un exemple de requête qui se projette dans une classe de non-entité :

La projection dans une classe d’entité ne donne pas accès aux services du DataContext

var custs = from c in db.Customers select new { Id = c.CustomerID, Name = c.ContactName };

En projetant les résultats dans une classe de non-entité, il n’est pas possible de rendrepermanentes les modifications apportées aux objets via la séquence custs en appelantla méthode SubmitChanges.

Un peu plus haut, il a été dit qu’un cas particulier permettait à la projection dans uneclasse de non-entité de profiter des facilités de persistance inhérentes au DataContext.Ce cas se produit lorsque la classe dans laquelle sont projetés les résultats contient desmembres qui sont des classes d’entité (voir Listing 15.1).

Linq.book Page 494 Mercredi, 18. février 2009 7:58 07

Page 510: LINQ Language Integrated Query en C

Chapitre 15 Les classes d’entité LINQ to SQL 495

Listing 15.1 : Projection dans une classe de non-entité qui contient des classes d’entité.

Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind");

var cusorders = from o in db.Orders where o.Customer.CustomerID == "CONSH" orderby o.ShippedDate descending select new { Customer = o.Customer, Order = o };

// Première commandeOrder firstOrder = cusorders.First().Order;

// Sauvegarde du premier champ shipcountry de la commande pour une utilisation futurestring shipCountry = firstOrder.ShipCountry;Console.WriteLine("Avant modification, pays d’expédition de la commande : {0}", shipCountry);

// Modification de la valeur du champ shipcountry en "USA"firstOrder.ShipCountry = "USA";db.SubmitChanges();

// Interrogation de la base pour voir si le champ ShipCountry a été modifiéstring country = (from o in db.Orders where o.Customer.CustomerID == "CONSH" orderby o.ShippedDate descending select o.ShipCountry).FirstOrDefault<string>();

Console.WriteLine("Avant modification, pays d’expédition de la commande : {0}", ➥country);

// Restauration de la valeur par défaut dans le champ ShipCountryfirstOrder.ShipCountry = shipCountry;db.SubmitChanges();

Le Listing 15.1 récupère les commandes passées par le client "CONSH". Les comman-des sont retournées dans un type anonyme qui contient un client (Customer) et une ouplusieurs commandes (Order). La classe anonyme ne bénéficie pas des services duDataContext. Cependant, ses composants Customer et Order en bénéficient, car ils’agit de classes d’entité. Une deuxième requête est appliquée aux résultats de lapremière pour obtenir la première commande. Le champ ShipCountry est sauvegardé,afin de pouvoir restaurer la valeur originale à la fin de l’exemple, et sa valeur actuelleest affichée dans la console. La valeur du champ ShipCountry est alors modifiée et lamodification est enregistrée en appelant la méthode SubmitChanges. La base dedonnées est à nouveau interrogée pour obtenir la valeur du champ ShipCountry. Cettevaleur est affichée pour prouver que la modification a été reportée dans la base dedonnées. La méthode SubmitChanges a donc bien fonctionné et les composants desclasses d’entité du type anonyme ont bénéficié des services de l’objet DataContext.Pour terminer, le champ ShipCountry est restauré à sa valeur originale et sauvegardédans la base de données. Ceci afin de pouvoir exécuter plusieurs fois l’exemple et de nepas affecter les exemples suivants.

Voici les résultats du Listing 15.1 :

Avant modification, pays d’expédition de la commande : UKAprès modification, pays d’expédition de la commande : USA

Linq.book Page 495 Mercredi, 18. février 2009 7:58 07

Page 511: LINQ Language Integrated Query en C

496 LINQ to SQL Partie V

Cet exemple montre qu’il est possible de profiter des services de persistance inhérentsau DataContext lorsque la projection est effectuée dans une classe de non-entité, àcondition qu’une classe d’entité soit incluse dans la classe de non-entité.

Dans le code précédent, vous avez peut-être remarqué que la requête qui obtient uneréférence à la première commande apparaît en gras. C’était dans le but d’attirer votreattention. L’opérateur First a été invoqué avant de sélectionner la portion de laséquence à laquelle le code s’intéresse, à savoir le membre Order. Ceci a été fait pouraméliorer les performances. En effet, d’une manière générale, plus vite vous pouvezlimiter les résultats, meilleures sont les performances.

Dans une projection, préférez l’initialisation d’objet à la construction paramétrée

Il est toujours possible d’effectuer une projection dans des classes avant la fin d’unerequête pour effectuer d’autres opérations de requêtes. Si vous avez recours à ceprocédé, préférez l’initialisation d’objet à la construction paramétrée. Pour biencomprendre pourquoi, reportez-vous au Listing 15.2, qui utilise l’initialisation d’objetdans la projection.

Listing 15.2 : Projection en utilisant l’initialisation d’objet.

Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind");

db.Log = Console.Out;

var contacts = from c in db.Customers where c.City == "Buenos Aires" select new { Name = c.ContactName, Phone = c.Phone } into co orderby co.Name select co;

foreach (var contact in contacts){ Console.WriteLine("{0} - {1}", contact.Name, contact.Phone);}

Dans ce listing, la projection a été effectuée dans une classe anonyme et nous avonsutilisé l’initialisation d’objets pour initialiser les objets anonymes créés. Examinons lerésultat du Listing 15.2 :

SELECT [t0].[ContactName] AS [Name], [t0].[Phone]FROM [dbo].[Customers] AS [t0]WHERE [t0].[City] = @p0ORDER BY [t0].[ContactName]-- @p0: Input String (Size = 12; Prec = 0; Scale = 0) [Buenos Aires]-- Context: SqlProvider(Sql2005) Model: AttributedMetaModel Build: 3.5.20706.1

Patricio Simpson - (1) 135-5555Sergio Gutiérrez - (1) 123-5555Yvonne Moncada - (1) 135-5333

Linq.book Page 496 Mercredi, 18. février 2009 7:58 07

Page 512: LINQ Language Integrated Query en C

Chapitre 15 Les classes d’entité LINQ to SQL 497

Les résultats de la requête importent peu. Ce qui importe ici, c’est la requête SQL géné-rée. Mais alors, pourquoi avoir utilisé une boucle foreach ? Eh bien, tout simplementpour provoquer l’exécution de la requête différée.

Les parties intéressantes de la requête LINQ to SQL sont les déclarations select etorderby. Ici, nous demandons à la requête de créer le membre Name dans la classeanonyme et de lui affecter le champ ContactName de la table Customers. Nous deman-dons alors à la requête de trier les membres Name de l’objet anonyme dans lequel a étéeffectuée la projection. Toutes ces informations sont passées à l’objet DataContext. Eneffet, l’initialisation d’objet mappe le champ source ContactName de la classe Customerdans le champ de destination Name de la classe anonyme, et l’objet DataContext est aucourant de ce mappage. Fort de cette information, il est en mesure de savoir que lesclients sont triés par l’intermédiaire du champ ContactName. Il peut donc générer larequête SQL correspondante.

Voyons maintenant ce qui se passe lorsque la projection s’effectue dans une classenommée en utilisant la construction paramétrée. Pour commencer, nous aurons besoind’une classe nommée :

La classe nommée utilisée dans le Listing 15.3

class CustomerContact{ public string Name; public string Phone; public CustomerContact(string name, string phone) { Name = name; Phone = phone; }}

Cette classe compte un seul constructeur qui admet deux paramètres : Name et Phone. LeListing 15.3 reprend le code du Listing 15.2 mais, ici, la projection s’effectue dans laclasse CustomerContact en utilisant la construction paramétrée.

Listing 15.3 : Projection en utilisant la construction paramétrée.

Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind");

db.Log = Console.Out;

var contacts = from c in db.Customers where c.City == "Buenos Aires" select new CustomerContact(c.ContactName, c.Phone) into co orderby co.Name select co;

foreach (var contact in contacts){ Console.WriteLine("{0} - {1}", contact.Name, contact.Phone);}

Linq.book Page 497 Mercredi, 18. février 2009 7:58 07

Page 513: LINQ Language Integrated Query en C

498 LINQ to SQL Partie V

Ici encore, nous nous intéresserons aux déclarations select et orderby. Comme vouspouvez le voir, la projection se fait non pas dans une classe anonyme, mais dans laclasse CustomerContact. Par ailleurs, plutôt qu’initialiser les objets créés en utilisantl’initialisation d’objets, nous avons recours à un constructeur paramétré. Ce code passel’étape de la compilation mais, à l’exécution, une exception est levée :

Que s’est-il passé ? Observez la requête LINQ to SQL précédente et posez-vous laquestion "comment le DataContext sait-il quel champ de la classe Customer est mappéau membre CustomerContact.name auquel nous tentons d’accéder ?". Dans leListing 15.2, comme les noms des champs de la classe anonyme lui étaient passés, ilsavait que le champ source dans la classe Customer était ContactName et que le champde destination dans la classe anonyme était Name. Dans le Listing 15.3, ce mappagen’est pas effectué dans la requête LINQ to SQL : il se produit dans le constructeur de laclasse CustomerContact, et le DataContext n’en est pas informé. Par ailleurs, il ne saitpas sur quel champ de la classe source Customer il doit agir lors de la génération de ladéclaration SQL. Voilà d’où vient le problème !

Sachez cependant qu’il est possible d’utiliser la construction paramétrée tant qu’aucuneréférence aux membres de la classe nommée n’est faite après la projection dans larequête (voir Listing 15.4).

Listing 15.4 : Projection en utilisant la construction paramétrée sans référencer des membres.

Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind");

db.Log = Console.Out;

var contacts = from c in db.Customers where c.City == "Buenos Aires" select new CustomerContact(c.ContactName, c.Phone);

foreach (var contact in contacts){ Console.WriteLine("{0} - {1}", contact.Name, contact.Phone);}

Dans ce listing, étant donné que nous utilisons la syntaxe de l’expression de requête etque cette dernière nécessite que la requête se termine par une déclaration select, nouspouvons utiliser la construction paramétrée dans l’élément select (le dernier élément)de la requête. Ceci est possible car aucune référence aux membres de la classe nomméen’est faite après la déclaration select. Voici les résultats du Listing 15.4 :

Exception non gérée : System.InvalidOperationException: erreur de binding : ➥le membre’LINQChapter15.CustomerContact.Name’ n’est pas mappé à ➥’LINQChapter15.CustomerContact’.…

SELECT [t0].[ContactName], [t0].[Phone]FROM [dbo].[Customers] AS [t0]

Linq.book Page 498 Mercredi, 18. février 2009 7:58 07

Page 514: LINQ Language Integrated Query en C

Chapitre 15 Les classes d’entité LINQ to SQL 499

La syntaxe traditionnelle "à point" ne nécessite pas que la requête se termine par unedéclaration select. Il est donc possible qu’une requête qui utilise la construction para-métrée n’arrive pas à s’exécuter. Dans le Listing 15.5, la requête utilise une syntaxestandard "à point". La dernière projection (Listing 15.3) utilise la construction paramé-trée. Cependant, comme la ligne suivante de la requête référence un membre de laclasse nommée, l’exécution de la requête produit une exception.

Listing 15.5 : Projection en utilisant la construction paramétrée et en référençant un membre.

Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind");

db.Log = Console.Out;

var contacts = db.Customers.Where(c => c.City == "Buenos Aires"). Select(c => new CustomerContact(c.ContactName, c.Phone)). OrderBy(c => c.Name);

foreach (var contact in contacts){ Console.WriteLine("{0} - {1}", contact.Name, contact.Phone);}

La requête du Listing 15.5 est très semblable à celle du Listing 15.4, si ce n’est qu’elleutilise une syntaxe traditionnelle "à point" et non une syntaxe d’expression de requêteet qu’un appel à l’opérateur OrderBy est effectué après la déclaration select. Bien quenous utilisions la construction paramétrée dans la dernière projection, une exception estgénérée parce que l’opérateur OrderBy fait référence à un membre de la classe nommée.Voici les résultats du Listing 15.5 :

Pour éviter ce genre de problème, je vous conseille d’utiliser l’initialisation d’objet etnon la construction paramétrée à chaque fois que cela est possible.

Extension des classes d’entité avec des méthodes partielles

Les nouveaux programmeurs LINQ se sont souvent plaints qu’il n’était pas possible desavoir ce qui se passait à l’intérieur d’une classe d’entité. Pendant la phase d’incubationde LINQ, il n’y avait aucun moyen pour un développeur de savoir quand une propriété

WHERE [t0].[City] = @p0-- @p0: Input String (Size = 12; Prec = 0; Scale = 0) [Buenos Aires]-- Context: SqlProvider(Sql2005) Model: AttributedMetaModel Build: 3.5.20706.1Patricio Simpson - (1) 135-5555Yvonne Moncada - (1) 135-5333Sergio Gutiérrez - (1) 123-5555

Exception non gérée : System.InvalidOperationException: erreur de binding : ➥le membre’LINQChapter15.CustomerContact.Name’ n’est pas mappé à ➥’LINQChapter15.CustomerContact’.…

Linq.book Page 499 Mercredi, 18. février 2009 7:58 07

Page 515: LINQ Language Integrated Query en C

500 LINQ to SQL Partie V

d’un objet d’une classe d’entité était changée ou quand une classe d’entité était créée, sice n’est en modifiant le code de classe de l’entité générée. Malheureusement, toutemodification dans ce sens est systématiquement perdue lorsque le code nécessite d’êtregénéré une nouvelle fois. Cette technique n’est donc pas pérenne. Heureusement, lesingénieurs de Microsoft étaient à l’écoute…

Au Chapitre 2, nous avons introduit les méthodes partielles. C’est treize chapitres plusloin qu’elles vont se révéler très utiles. Microsoft a déterminé l’endroit précis dans lavie d’une classe d’entité où les développeurs ont le plus de chance de vouloir apporterdes modifications et ajouter des appels à des méthodes partielles.

Voici la liste des méthodes partielles utilisables dans une classe d’entité :

Les méthodes partielles utilisables dans une classe d’entité

partial void OnLoaded();partial void OnValidate(ChangeAction action);partial void OnCreated();partial void On[Property]Changing([Type] value);partial void On[Property]Changed();

Dans les deux dernières méthodes, [Property] doit être remplacé par un nom depropriété et [Type], par un type de propriété. Pour illustrer quelques-unes des méthodespartielles supportées par les classes d’entité, nous allons ajouter la classe suivante dansla classe d’entité Contact :

Une autre déclaration pour la classe Contact permettant d’implémenter des méthodespartielles

namespace nwind{ public partial class Contact { partial void OnLoaded() { Console.WriteLine("OnLoaded() appelée."); } partial void OnCreated() { Console.WriteLine("OnCreated() appelée."); } partial void OnCompanyNameChanging(string value) { Console.WriteLine("OnCompanyNameChanging()appelée."); } partial void OnCompanyNameChanged() { Console.WriteLine("OnCompanyNameChanged()appelée."); } }}

L’espace de noms est nwind. Ce nom a été intentionnellement choisi : il doit correspon-dre à l’espace de noms de la classe étendue. Comme le nom nwind a été spécifié lorsqueles classes d’entité ont été générées avec SQLMetal, la classe partielle Contact doit

Linq.book Page 500 Mercredi, 18. février 2009 7:58 07

Page 516: LINQ Language Integrated Query en C

Chapitre 15 Les classes d’entité LINQ to SQL 501

également se trouver dans l’espace de noms nwind. Dans un code de production, vouspréférerez certainement loger cette classe partielle dans un module séparé.

Comme vous pouvez le voir, les méthodes OnLoaded, OnCreated, OnCompanyNameChan-ging et OnCompanyNameChanged ont été implémentées. Ces méthodes se contententd’afficher un message dans la console. Bien entendu, vous pouvez utiliser un tout autrecode dans vos implémentations.

Nous allons maintenant illustrer l’utilisation des méthodes partielles. Le Listing 15.6accède à un enregistrement Contact dans la base de données et modifie sa propriétéCompanyName.

Listing 15.6 : Interrogation d’une classe avec des méthodes partielles.

Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind");

Contact contact = db.Contacts.Where(c => c.ContactID == 11).SingleOrDefault();Console.WriteLine("CompanyName = {0}", contact.CompanyName);

contact.CompanyName = "Joe’s House of Booze";Console.WriteLine("CompanyName = {0}", contact.CompanyName);

Ce code n’a rien de très original, si ce n’est qu’il implémente des méthodes partiellesutilisables dans les classes d’entité. Dans un premier temps, nous effectuons unerequête sur un contact et affichons le nom de sa société dans la console. Dans un secondtemps, le nom du contact est modifié et affiché dans la console. Voici les résultats affi-chés suite à l’appui sur Ctrl+F5 :

Après l’appel des méthodes OnCreated puis OnLoaded, l’enregistrement est extrait de labase de données et chargé dans l’objet entité Contact. La troisième ligne indique lenom du contact. La méthode OnCompanyNameChanging est alors appelée, suivie par laméthode OnCreated. Apparemment, le DataContext crée un autre objet entité Contactlors de la procédure de pistage des modifications. La méthode OnCompanyNameChangedest enfin appelée, suivie par l’affichage du nouveau nom de la société.

Vous savez maintenant comment étendre des classes d’entité en utilisant des méthodespartielles, et ce sans modifier le code généré.

Les classes API importantes de System.Data.Linq

Il existe un certain nombre de classes de l’espace de noms System.Data.Linq que vousutiliserez régulièrement lors de votre pratique de LINQ to SQL. Cette section donne

OnCreated() appelée.OnLoaded() appelée.CompanyName = B’s BeveragesOnCompanyNameChanging() appelée.OnCreated() appelée.OnCompanyNameChanged() appelée.CompanyName = Joe’s House of Booze

Linq.book Page 501 Mercredi, 18. février 2009 7:58 07

Page 517: LINQ Language Integrated Query en C

502 LINQ to SQL Partie V

une vue d’ensemble de ces classes, indique leur utilité et leur utilisation dans LINQ toSQL.

EntitySet<T>

Une classe d’entité située du côté "un" d’une relation "un-à-plusieurs" stocke les clas-ses d’entité "plusieurs" dans un membre de classe du type EntitySet<T>, où T est letype de la classe d’entité associée.

Dans la base de données Northwind, la relation entre les tables Customers et Orders estde type "un-à-plusieurs". Dans la classe Customer, les Orders sont stockés dans unEntitySet<Order>.

private EntitySet<Order> _Orders;

La classe EntitySet<T> est une collection particulière utilisée par LINQ to SQL. Elleimplémente l’interface IEnumerable<T> et peut donc être interrogée avec des requêtesLINQ. Elle implémente également l’interface ICollection<T>.

EntityRef<T>

Une classe d’entité située du côté "plusieurs" d’une relation "un-à-plusieurs" stocke lesclasses d’entité "un" dans un membre de classe du type EntityRef<T>, où T est le typede la classe d’entité associée.

Dans la base de données Northwind, la relation entre les tables Customers et Orders estde type "un-à-plusieurs". Dans la classe Order, le Customer est stocké dans un Entity-Ref<Customer>.

private EntityRef<Customer> _Customer;

Entity

Lorsque l’on référence une classe d’entité associée qui se trouve sur le côté "un" d’unerelation "un-à-plusieurs" ou "un-à-un", il est facile de penser que la variable membre estdu même type que la classe d’entité.

Par exemple, lorsque l’on référence le Customer d’un objet Order, il est facile de penserque l’objet Customer est stocké dans un membre Customer de la classe Order. Enréalité, le Customer est stocké dans un EntityRef<Customer>. Si vous devez faire réfé-rence à l’objet Customer référencé par le membre EntityRef<Customer>, vous utiliserez lapropriété Entity de l’objet EntityRef<Customer>.

Dans certains cas, il est important d’être au courant de ces subtilités. En particulier sivous décidez d’écrire vos classes d’entité à la main. Si vous observez la classe d’entitéOrder générée par SQLMetal, vous verrez que les méthodes get et set de la propriétéCustomer utilisent la propriété Entity de l’objet EntityRef<Customer> pour référencerun Customer.

Linq.book Page 502 Mercredi, 18. février 2009 7:58 07

Page 518: LINQ Language Integrated Query en C

Chapitre 15 Les classes d’entité LINQ to SQL 503

Une propriété publique qui utilise la propriété EntityRef<T>.Entity pour accéder àl’objet entité

private EntityRef<Customer> _Customer;...public Customer Customer{ get { return this._Customer.Entity; } set { Customer previousValue = this._Customer.Entity; ... }}

HasLoadedOrAssignedValue

Cette propriété est de type bool. Elle vous permet de savoir si une propriété d’uneclasse d’entité stockée dans un EntityRef<T> a été initialisée, c’est-à-dire si une valeurlui a été affectée ou si un chargement a été effectué.

Elle est typiquement utilisée dans les méthodes set du côté "un" d’une association "un-à-plusieurs" pour empêcher la propriété de la classe d’entité contenant l’identifiantd’être différente de l’EntityRef<T> qui contient la référence au côté "un".

À titre d’exemple, voici la méthode set des propriétés CustomerID et Customer de laclasse d’entité Order :

La méthode set de la propriété CustomerID

public string CustomerID{ get { return this._CustomerID; } set { if ((this._CustomerID != value)) { if (this._Customer.HasLoadedOrAssignedValue) { throw new System.Data.Linq.ForeignKeyReferenceAlreadyHasValueException(); } this.OnCustomerIDChanging(value); this.SendPropertyChanging(); this._CustomerID = value; this.SendPropertyChanged("CustomerID"); this.OnCustomerIDChanged(); } }}

Dans la méthode set de la propriété CustomerID, si l’EntityRef<T> utilisé pour stockerun Customer a une propriété HasLoadedOrAssignedValue initialisée à true, une excep-tion est levée. Ceci empêche le développeur de modifier le CustomerID d’un objet entité

Linq.book Page 503 Mercredi, 18. février 2009 7:58 07

Page 519: LINQ Language Integrated Query en C

504 LINQ to SQL Partie V

Order si une entité Customer lui est déjà affectée. Grâce à cet artifice, les propriétésCustomerID et Customer de l’entité objet Order ne peuvent pas être incohérentes.

D’une manière similaire, dans la méthode set de la propriété Customer, la référenceCustomer peut être affectée si la propriété HasLoadedOrAssignedValue a pour valeurfalse.

La méthode set de la propriété Customer

public Customer Customer{ get { return this._Customer.Entity; } set { Customer previousValue = this._Customer.Entity; if (((previousValue != value) || (this._Customer.HasLoadedOrAssignedValue == false))) { this.SendPropertyChanging(); if ((previousValue != null)) { this._Customer.Entity = null; previousValue.Orders.Remove(this); } this._Customer.Entity = value; if ((value != null)) { value.Orders.Add(this); this._CustomerID = value.CustomerID; } else { this._CustomerID = default(string); } this.SendPropertyChanged("Customer"); } }}

En vérifiant la propriété HasLoaderOrAssigned dans chaque méthode set, le déve-loppeur ne peut pas provoquer une incohérence entre les références CustomerID etCustomer.

Table<T>

Ce type est utilisé par LINQ to SQL pour interfacer une table ou une base de donnéesSQL Server.

Généralement, la classe dérivée de DataContext, souvent référencée sous la forme[Your]DataContext dans les chapitres relatifs à LINQ to SQL, a une propriété publiquede type Table<T> (où T est une classe d’entité) pour chaque table de la base de donnéesmappée à la classe dérivée de DataContext.

Linq.book Page 504 Mercredi, 18. février 2009 7:58 07

Page 520: LINQ Language Integrated Query en C

Chapitre 15 Les classes d’entité LINQ to SQL 505

Pour faire référence à la table Customers de la base de données Northwind, vous utili-serez généralement la propriété publique Customers de type Table<Customer> dans laclasse dérivée de DataContext :

Une propriété Table<T> pour la table Customers de la base de données

public System.Data.Linq.Table<Customer> Customers{ get { return this.GetTable<Customer>(); }}

Table<T> implémente l’interface IQueryable<T>, qui elle-même étend IEnumera-ble<T>. Vous pouvez donc lui appliquer des requêtes LINQ to SQL. C’est le type dedonnées utilisé par la plupart des requêtes LINQ to SQL.

IExecuteResult

Lorsqu’une procédure stockée ou une fonction définie par l’utilisateur est appelée parl’intermédiaire de la méthode IExecuteMethodCall, les résultats sont retournés dans unobjet qui implémente l’interface IExecuteResult.

La méthode IExecuteMethodCall retourne un IExecuteResult

IExecuteResult result = this.ExecuteMethodCall(...);

L’interface IExecuteResult fournit la propriété ReturnValue (valeur retournée) et laméthode GetParameterValue (paramètres de sortie).

ReturnValue

En dehors des paramètres de sortie, les résultats d’une procédure stockée/d’une fonc-tion définie par l’utilisateur de type scalaire sont retournés dans la variable IExecute-Result.ReturnValue.

Pour accéder à la valeur retournée par une procédure stockée/une fonction définie parl’utilisateur de type scalaire, il suffit de référencer le membre ReturnValue de l’objetretourné. Voici le code à utiliser :

Accès à la valeur Integer retournée par une procédure stockée

IExecuteResult result = this.ExecuteMethodCall(...);int returnCode = (int)(result.ReturnValue);

Au Chapitre 16, nous nous intéresserons à la méthode ExecuteMethodCall et montre-rons comment obtenir un entier retourné par une procédure stockée.

Si une procédure stockée retourne d’autres données que sa valeur de retour, la variableReturnValue implémente l’interface ISingleResult<T> ou IMultipleResults, enfonction du nombre de données retournées.

Linq.book Page 505 Mercredi, 18. février 2009 7:58 07

Page 521: LINQ Language Integrated Query en C

506 LINQ to SQL Partie V

GetParameterValue

Pour accéder aux paramètres de sortie d’une procédure stockée, vous appellerez laméthode GetParameterValue sur l’objet retourné, en lui passant l’index du paramètre àaccéder (l’index du premier paramètre est 0). Si, par exemple, le troisième paramètreretourné par la procédure stockée est CompanyName, vous utiliserez le code suivant :

Accès aux paramètres retournés par une procédure stockée

IExecuteResult result = this.ExecuteMethodCall(..., param1, param2, companyName);string CompanyName = (string)(result.GetParameterValue(2));

Au Chapitre 16, nous nous intéresserons à la méthode ExecuteMethodCall et montreronscomment obtenir un entier retourné par une procédure stockée.

ISingleResult<T>

Lorsqu’une procédure stockée retourne un résultat unique, il est placé dans l’objetIExecuteResult.ReturnValue, qui implémente l’interface ISingleResult<T> (où Test une classe d’entité). Le code à utiliser est le suivant :

Accès à un résultat unique

IExecuteResult result = this.ExecuteMethodCall(...);ISingleResult<CustOrdersOrdersResult> results = (ISingleResult<CustOrdersOrdersResult>)(result.ReturnValue);

Pour obtenir le résultat, nous avons effectué un casting de type ISingleResult<T> surle membre ReturnValue de l’objet IExecuteResult.

ISingleResult<T> héritant de IEnumerable<T>, on accède aux résultats retournés aussisimplement qu’à une séquence LINQ d’un autre type.

Accès aux résultats d’un ISingleResult<T>

foreach (CustomersByCityResult cust in results){ ...}

Au Chapitre 16, nous nous intéresserons à la méthode ExecuteMethodCall et montre-rons comment obtenir la valeur unique retournée par une procédure stockée.

ReturnValue

L’interface ISingleResult<T> a une propriété ReturnValue qui se comporte comme lapropriété de même nom dans l’interface IExecuteResult. Reportez-vous à la sectionprécédente pour savoir comment accéder à cette propriété.

IMultipleResults

Lorsqu’une procédure stockée retourne plusieurs résultats, ils sont placés dans l’objetIExecuteResult.ReturnValue, qui implémente l’interface IMultipleResults. Lecode à utiliser est le suivant :

Linq.book Page 506 Mercredi, 18. février 2009 7:58 07

Page 522: LINQ Language Integrated Query en C

Chapitre 15 Les classes d’entité LINQ to SQL 507

Accès à des résultats multiples

IExecuteResult result = this.ExecuteMethodCall(...);IMultipleResults results = (IMultipleResults)(result.ReturnValue);

Pour accéder aux différents résultats retournés, il suffit d’appeler la méthode IMultipleRe-sults.GetResult<T>. Vous en saurez plus sur cette méthode à la fin de cette section.

Au Chapitre 16, nous nous intéresserons à la méthode ExecuteMethodCall et montreronscomment obtenir les valeurs retournées par une procédure stockée.

L’interface IMultipleResults a une propriété ReturnValue qui donne accès à la valeurretournée par la procédure stockée et une méthode GetResult<T> qui permet d’obtenirun IEnumerable<T> (où T est une classe d’entité qui correspond à la valeur) pourchaque valeur retournée.

ReturnValue

L’interface IMultipleResult<T> a une propriété ReturnValue qui se comporte commela propriété de même nom dans l’interface IExecuteResults. Reportez-vous à lasection précédente pour savoir comment accéder à cette propriété.

GetResult<T>

L’interface IMultipleResult<T> dispose d’une méthode GetResult<T> (où T repré-sente le type de donnée de la forme retournée). Cette méthode permet d’obtenir lesdivers enregistrements de la forme retournée. Ces enregistrements sont retournés dansun IEnumerable<T> (où T est la classe d’entité de la forme). Le code à utiliser est lesuivant :

Accès à plusieurs formes retournées par une procédure stockée

[StoredProcedure(Name="A Stored Procedure")][ResultType(typeof(Shape1))][ResultType(typeof(Shape2))]...IExecuteResult result = this.ExecuteMethodCall (...);IMultipleResults results = (IMultipleResults)(result.ReturnValue);

foreach(Shape1 x in results.GetResult<Shape1>()) {…}

foreach(Shape2 y in results.GetResult<Shape2>()) {…}

Les premières lignes du code correspondent aux attributs ResultType. Vous avez ainsiune idée de leur contexte et des formes retournées par la procédure stockée.

Dans cet exemple, nous savons que les enregistrements mappés au type Shape1 serontretournés par la procédure stockée, suivis des enregistrements mappés au type Shape2.Les deux dernières instructions énumèrent les séquences IEnumerable<Shape1> etIEnumerable<Shape2>, retournées respectivement par le premier et le deuxième appel àla méthode GetResult<T>. Il est important de connaître la chronologie des formes etd’utiliser la méthode GetResult<T> pour les retrouver dans le même ordre.

Linq.book Page 507 Mercredi, 18. février 2009 7:58 07

Page 523: LINQ Language Integrated Query en C

508 LINQ to SQL Partie V

Au Chapitre 16, nous nous intéresserons à la méthode ExecuteMethodCall et montreronscomment obtenir les formes multiples retournées par une procédure stockée.

Résumé

Ce chapitre a analysé en détail les classes d’entité LINQ to SQL, les difficultés inhéren-tes à leur écriture manuelle, leurs attributs et les propriétés de leurs attributs.

Il est important de bien avoir en mémoire que, si vous écrivez vos propres classesd’entité, vous devrez implémenter les notifications de changement et assurer la cohé-rence du graphe. Il ne s’agit pas là de détails triviaux et leur implémentation est loind’être un jeu d’enfant. Heureusement, vous pouvez utiliser SQLMetal et le ConcepteurObjet/Relationnel pour reléguer ces complications aux oubliettes.

Par ailleurs, pour écrire vos propres classes d’entité, vous devez avoir une connaissanceexhaustive des attributs des classes d’entité et de leurs propriétés. Ces éléments ont étéanalysés en détail dans ce chapitre et vous avez découvert leur implémentation à traversl’analyse des classes d’entité générées par SQLMetal pour la base de donnéesNorthwind.

Vous avez également vu qu’il était préférable de projeter les résultats de vos requêtesdans des classes d’entité plutôt que dans des classes de non-entité. Si vous n’avez pasbesoin de modifier les données et de les enregistrer dans la base de données, les classesde non-entité peuvent cependant être utilisées. Dans le cas contraire, projetez vosdonnées dans des classes d’entité.

La fin du chapitre a passé en revue quelques-unes des classes fréquemment utiliséesdans l’espace de noms System.Data.Linq et vous a montré comment les utiliser enLINQ to SQL.

Arrivé à ce point dans la lecture du livre, vous devriez être un expert en anatomie desclasses d’entité. Elles ont été étudiées en détail et le code généré a été analysé. Ces classesd’entité sont généralement référencées par une classe dérivée de la classe DataContext.Vous saurez tout sur cette classe en parcourant le chapitre suivant.

Linq.book Page 508 Mercredi, 18. février 2009 7:58 07

Page 524: LINQ Language Integrated Query en C

16

La classe DataContext

Les chapitres précédents relatifs à LINQ to SQL ont utilisé la classe DataContext, sanspour autant expliquer ses tenants et ses aboutissants. Cette lacune va maintenant êtreréparée.

Ce chapitre va vous montrer ce que la classe DataContext peut faire pour vous etcomment en tirer le maximum. Nous passerons en revue ses méthodes principales etfournirons un exemple pour chacune d’entre elles. Il est nécessaire de bien compren-dre le fonctionnement de la classe DataContext pour bien utiliser LINQ to SQL.Poursuivez votre lecture et la classe DataContext n’aura plus aucun secret pourvous.

Prérequis pour exécuter les exemples

Pour exécuter les exemples de ce chapitre, vous devez avoir téléchargé la version éten-due de la base de données Northwind et généré ses classes d’entité. Si nécessaire, repor-tez-vous à la section intitulée "Prérequis pour exécuter les exemples" du Chapitre 12pour savoir comment procéder.

Méthodes communes

Pour exécuter les exemples de ce chapitre, vous aurez également besoin de plusieursméthodes communes. Reportez-vous à la section intitulée "Méthodes communes" auChapitre 12 pour avoir toutes les informations nécessaires à ce sujet.

Utilisation de l’API LINQ to SQL

Pour exécuter les exemples de ce chapitre, vous devez également ajouter les référenceset directives using appropriées à votre projet. Encore une fois, reportez-vous à la

Linq.book Page 509 Mercredi, 18. février 2009 7:58 07

Page 525: LINQ Language Integrated Query en C

510 LINQ to SQL Partie V

section "Utilisation de l’API LINQ to SQL" du Chapitre 12 pour avoir plus de rensei-gnements à ce sujet.

Pour quelques exemples de ce chapitre, vous devrez également ajouter une directiveusing pour l’espace de noms System.Data.Linq.Mapping :

using System.Data.Linq.Mapping;

La classe [Your]DataContext

Bien qu’elle n’ait pas encore été étudiée, la classe System.Data.Linq.DataContextsera fréquemment utilisée dans LINQ to SQL. Elle permet en effet d’établir laconnexion avec la base de données utilisée. Lorsque vous définissez ou générez desclasses d’entité, elles dérivent généralement de la classe DataContext. La classe déri-vée a le plus souvent le même nom que la base de données à laquelle elle se réfère. Lesexemples de ce chapitre utilisent la base de données Northwind. La classe dérivée auradonc pour nom Northwind. Cependant, étant donné que le nom de la classe dérivée estfonction de la base de données, le nom de la classe sera également amené à changer, decode en code. Par commodité d’écriture, la classe dérivée sera souvent référencée sousle nom [Your]DataContext.

La classe DataContext

C’est la classe DataContext qui gère la connexion avec la base de données. Elle gèreégalement les requêtes, les mises à jour, les insertions, la recherche d’identité, la recher-che de changements, le processus de changement, l’intégrité transactionnelle et mêmela création de la base de données.

La classe DataContext traduit les requêtes de classes d’entité en déclarations SQL quisont exécutées dans la base de données.

La classe [Your]DataContext étant dérivée de la classe DataContext, elle a accès à desméthodes communes, telles que ExecuteQuery, ExecuteCommand et SubmitChanges.Outre ces méthodes héritées, la classe [Your]DataContext contient également despropriétés de type System.Data.Linq.Table<T> (où chaque T représente une classed’entité mappée à une table ou une vue spécifique) pour chaque table et vue de la basede données pour lesquelles vous voulez utiliser LINQ to SQL.

À titre d’exemple, examinons la classe Northwind, générée par l’outil SQLMetal. Ils’agit de la classe [Your]DataContext de la base de données Northwind. Les passagesen gras sont les plus intéressants.

Un extrait de la classe générée Northwind

public partial class Northwind : System.Data.Linq.DataContext{ ... static Northwind()

Linq.book Page 510 Mercredi, 18. février 2009 7:58 07

Page 526: LINQ Language Integrated Query en C

Chapitre 16 La classe DataContext 511

{ }

public Northwind(string connection) : base(connection, mappingSource) { OnCreated(); }

public Northwind(System.Data.IDbConnection connection) : base(connection, mappingSource) { OnCreated(); }

public Northwind(string connection, System.Data.Linq.Mapping.MappingSource mappingSource) : base(connection, mappingSource) { OnCreated(); }

public Northwind(System.Data.IDbConnection connection, System.Data.Linq.Mapping.MappingSource mappingSource) : base(connection, mappingSource) { OnCreated(); }

...

public System.Data.Linq.Table<Customer> Customers { get { return this.GetTable<Customer>(); } }

...

}

Comme vous le laisse voir la première ligne, cette classe hérite de la classe DataContext.Elle contient cinq constructeurs. Le constructeur par défaut est privé. En effet, l’indi-cateur de visibilité n’étant pas spécifié, il sera impossible d’instancier [Your]Data-Context sans paramètre. Les autres constructeurs sont tous publics. Ils sont corrélésaux constructeurs de la classe DataContext. Chaque constructeur [Your]DataContextappelle :

m dans son initialiseur, le constructeur de base équivalent de la classe DataContext ;

m dans le corps du constructeur, la méthode partielle OnCreated.

Linq.book Page 511 Mercredi, 18. février 2009 7:58 07

Page 527: LINQ Language Integrated Query en C

512 LINQ to SQL Partie V

Le programmeur peut donc implémenter une méthode partielle OnCreated qui lui estpropre. Cette méthode sera appelée à chaque instanciation d’un objet [Your]Data-Context.

Dans la classe Northwind, remarquez la propriété Customers de type Table<Customer>,où Customer est une classe d’entité. C’est la classe d’entité Customer qui est mappée àla table Customers de la base de données Northwind.

Il n’est pas nécessaire d’écrire du code qui utilise la classe [Your]DataContext : laclasse DataContext standard peut très bien convenir. Cependant, l’écriture de code estplus pratique si vous utilisez la classe [Your]DataContext. Si vous le faites, chaquetable est une propriété à laquelle on peut accéder directement à partir de l’objet[Your]DataContext (voir Listing 16.1).

Listing 16.1 : Accès à une table à l’aide d’une propriété.

Northwind db =new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind");

IQueryable<Customer> query = from cust in db.Customers where cust.Country == "USA" select cust;

foreach(Customer c in query){ Console.WriteLine("{0}", c.CompanyName);}

INFO

Dans les exemples, vous devrez peut-être modifier les chaînes de connexion pour qu’elless’adaptent à votre configuration.

Dans le code précédent, la connexion s’établit en utilisant la classe [Your]DataContextNorthwind. Les clients (Table<Customer>) sont donc accessibles comme de simplespropriétés (Customers) de la classe [Your]DataContext. Voici les résultats duListing 16.1 :

Great Lakes Food MarketHungry Coyote Import StoreLazy K Kountry StoreLet’s Stop N ShopLonesome Pine RestaurantOld World DelicatessenRattlesnake Canyon GrocerySave-a-lot MarketsSplit Rail Beer & AleThe Big CheeseThe Cracker BoxTrail’s Head Gourmet ProvisionersWhite Clover Markets

Linq.book Page 512 Mercredi, 18. février 2009 7:58 07

Page 528: LINQ Language Integrated Query en C

Chapitre 16 La classe DataContext 513

Si nous établissons la connexion en utilisant la classe DataContext, nous devons utili-ser la méthode GetTable<T> de l’objet DataContext, comme illustré dans leListing 16.2.

Listing 16.2 : Accès à une table à l’aide de la méthode GetTable<T>.

DataContext dc = new DataContext(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind");

IQueryable<Customer> query = from cust in dc.GetTable<Customer>() where cust.Country == "USA" select cust;

foreach(Customer c in query){ Console.WriteLine("{0}", c.CompanyName);}

Ce code donne les mêmes résultats que le précédent :

L’utilisation de la classe [Your]DataContext facilite donc l’écriture. Utilisez-la aussisouvent que possible.

Principaux objectifs

Outre les méthodes décrites dans ce chapitre, la classe DataContext fournit trois servi-ces principaux : la recherche d’identité, le traçage des modifications et l’exécution desmodifications.

Recherche d’identitéUn des problèmes résolus par LINQ to SQL est connu sous le nom object-relationalimpedance mismatch, que l’on pourrait traduire par "désaccord d’impédance objet/rela-tionnel". Ce terme se réfère aux difficultés inhérentes au fait que la plupart des bases dedonnées sont relationnelles et que la plupart des langages de programmation sont orientésobjets. Cette différence est à l’origine du problème.

Great Lakes Food MarketHungry Coyote Import StoreLazy K Kountry StoreLet’s Stop N ShopLonesome Pine RestaurantOld World DelicatessenRattlesnake Canyon GrocerySave-a-lot MarketsSplit Rail Beer & AleThe Big CheeseThe Cracker BoxTrail’s Head Gourmet ProvisionersWhite Clover Markets

Linq.book Page 513 Mercredi, 18. février 2009 7:58 07

Page 529: LINQ Language Integrated Query en C

514 LINQ to SQL Partie V

Une manifestation du désaccord d’impédance objet/relationnel concerne le comporte-ment de l’identité. Si une requête est effectuée sur le même enregistrement à plusieursemplacements dans le code, il semble logique de s’attendre à ce que les données retour-nées soient stockées dans différents emplacements en mémoire. Nous nous attendonségalement à ce que la modification des champs dans une partie du code n’affecte pas lesmêmes champs obtenus dans une autre partie du code. Ceci parce que les données sontstockées dans différents emplacements en mémoire.

Comparez ce qui vient d’être dit avec le comportement des objets. Lorsqu’un objet esten mémoire – un objet Customer, par exemple –, nous nous attendons à ce que toutes lesportions du code qui le référencent pointent vers une même adresse en mémoire. Si lapropriété name de cet objet est modifiée à un emplacement du programme, nous nousattendons à ce que le nouveau nom soit également modifié dans la totalité duprogramme.

Le service de recherche d’identité de la classe DataContext induit ce comportement :lorsqu’une requête est effectuée sur un enregistrement pour la première fois depuisl’instanciation de l’objet DataContext, l’enregistrement résultant est mémorisé dansune table d’identités en utilisant sa clé primaire, et un objet entité est créé et placé dansune mémoire cache. Les requêtes suivantes qui produisent un même résultat parcourentla table d’identités. Si l’enregistrement en question existe, l’objet entité correspondantest retourné à partir du cache. Ce concept est fondamental. Nous allons le formulerd’une autre manière. Lorsqu’une requête est exécutée, si un enregistrement de la basede données correspond aux critères de sélection, et si l’objet entité correspondant setrouve déjà dans le cache, il est retourné depuis le cache. Cela signifie que la donnéeretournée par la requête peut être différente de l’enregistrement de la base de données.La requête détermine quelles entités doivent être retournées en se basant sur lesdonnées contenues dans la base de données. Mais le service de recherche d’identité del’objet DataContext détermine quelle donnée doit être retournée. Ceci peut conduire auproblème de "discordance des résultats dans le cache".

Discordance des résultats dans le cacheLa discordance des résultats dans le cache se produit lorsqu’un enregistrement de labase de données diffère du même enregistrement dans le cache de l’objet DataContext.Lorsqu’une requête est exécutée, des enregistrements satisfaisant les critères sontrecherchés dans la base de données. Si un enregistrement correspond au critère, l’objetentité de cet enregistrement est placé dans les résultats. Cependant, si un enregistrementcorrespondant au critère se trouve dans le cache de l’objet DataContext, il est placédans les résultats.

Si un objet entité se trouve dans le cache du DataContext et que l’enregistrementcorrespondant est mis à jour dans la base de données, il y aura discordance entre les

Linq.book Page 514 Mercredi, 18. février 2009 7:58 07

Page 530: LINQ Language Integrated Query en C

Chapitre 16 La classe DataContext 515

deux représentations de l’enregistrement. Si vous lancez une requête dont le résultat estprécisément cet enregistrement, c’est la valeur du cache qui sera retournée…

Nous allons raisonner sur un exemple. Dans un premier temps, nous allons lancer unerequête sur le client LONEP et afficher sa région : OR. Dans un deuxième temps, nousallons rechercher puis afficher les clients dont la région est WA. Dans un troisièmetemps, nous allons utiliser ADO.NET pour affecter la région WA au client LONEP,comme si un autre contexte, extérieur au process, avait effectué cette modification. À cepoint précis, la région du client LONEP est OR dans l’objet entité et WA dans la basede données. Dans un quatrième temps, nous allons lancer la même requête que dans ledeuxième temps, afin d’afficher les clients dont la région est WA. Examinez le code :aucune requête n’est exécutée dans ce sens. Juste une énumération de la séquencecusts. Cela n’a rien de surprenant : étant donné la nature différée de la requête, il suffitd’énumérer la séquence de sortie pour obtenir les résultats. Le champ région du clientLONEP ayant pour valeur WA dans la base de données, il sera inclus dans les résultats.Mais, comme cet enregistrement se trouve déjà dans le cache, c’est l’enregistrement ducache qui sera placé dans les résultats. Pas de chance : le champ région de cet enregis-trement a pour valeur OR !

Enfin, pour terminer, nous allons afficher la région des objets entité retournés. Lors del’affichage du client LONEP, c’est la région OR qui sera affichée, et ce bien que larequête ait demandé la liste des enregistrements dont la région avait pour valeur WA. LeListing 16.3 illustre cette discordance.

Listing 16.3 : Illustration de la discordance des résultats dans le cache.

Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind");

// Sélection d’un client dont le champ region vaut "WA"Customer cust = (from c in db.Customers where c.CustomerID == "LONEP" select c).Single<Customer>();

Console.WriteLine("Client {0}, région = {1}.{2}", cust.CustomerID, cust.Region, System.Environment.NewLine);

// La région de LONEP est OR

// Sélection d’une séquence de clients dont le champ région vaut "WA"// LONEP n’en fait pas partie puisque sa région est ORIEnumerable<Customer> custs = (from c in db.Customers where c.Region == "WA" select c);

Console.WriteLine("Clients dont le champ région vaut WA, avant la modification ➥ADO.NET ...");foreach(Customer c in custs){ // Affichage de la région de chaque objet entité Console.WriteLine("Le client {0}a pour région {1}.", c.CustomerID, c.Region);}

Linq.book Page 515 Mercredi, 18. février 2009 7:58 07

Page 531: LINQ Language Integrated Query en C

516 LINQ to SQL Partie V

Console.WriteLine("Clients dont le champ région vaut WA avant la modification ➥ADO.NET - début{0}", System.Environment.NewLine);

// Affectation de la région WA au client LONEP// La modification se fait avec ADO.NETConsole.WriteLine("Modification de la région de LONEP en WA avec ADO.NET...");ExecuteStatementInDb( "update Customers set Region = ’WA’ where CustomerID = ’LONEP’");Console.WriteLine("La région de LONEP a été mise à jour.{0}", System.Environment.NewLine);

Console.WriteLine("La région de LONEP est WA dans la base de données, mais ...");Console.WriteLine("Le client {0} a pour région = {1} dans l’objet entité{2}", cust.CustomerID, cust.Region, System.Environment.NewLine);

// La région de LONEP est WA dans la base de données, mais toujours OR dans l’objet ➥entité

// Effectuons une autre requête// Affichage des régions de l’objet entité Console.WriteLine("Requête des objets entité après la modification ADO.NET – début ➥...");foreach(Customer c in custs){ // Affichage de la région de chaque entité objet Console.WriteLine("Le client {0}a pour région {1}.", c.CustomerID, c.Region);}Console.WriteLine("Requête des objets entité après la modification ADO.NET - ➥fin{0}", System.Environment.NewLine);

// Les valeurs modifiées doivent être rétablies pour que le code// puisse être exécuté plusieurs fois.Console.WriteLine("{0}Restauration des valeurs originales.", System.Environment.NewLine);ExecuteStatementInDb( "update Customers set Region = ’OR’ where CustomerID = ’LONEP’");

Voici les résultats :

Le client LONEP a pour région ORClients dont le champ région vaut WA avant la modification ADO.NET – début ...Le client LAZYK a pour région WA.Le client TRAIH a pour région WA.Le client WHITC a pour région WA.Clients dont le champ région vaut WA avant la modification ADO.NET – finModification de la région de LONEP en WA avec ADO.NET...La région de LONEP a été mise à jour.La région de LONEP est WA dans la base de données, mais...Le client LONEP a pour région OR dans l’objet entité.Requête des objets entité après la modification ADO.NET – début ...Le client LAZYK a pour région WA.Le client LONEP a pour région OR.Le client TRAIH a pour région WA.Le client WHITC a pour région WA.Requête des objets entité après la modification ADO.NET – finRestauration des valeurs originales

Linq.book Page 516 Mercredi, 18. février 2009 7:58 07

Page 532: LINQ Language Integrated Query en C

Chapitre 16 La classe DataContext 517

Comme vous le voyez, bien que la requête porte sur les clients dont la région est WA,LONEP fait partie des résultats alors que sa région est OR. Bien sûr, la région deLONEP est bien WA dans la base de données, mais la région est différente dans l’objetréférencé par le code.

Ce comportement erratique est également mis en évidence si vous tentez d’interroger labase de données sur un enregistrement qui vient d’être ajouté ou supprimé. Aprèsl’ajout d’un enregistrement, le résultat de la requête est déterminé à partir du contenu dela base de données, et non par le cache de l’objet DataContext. Si l’ajout de l’enregis-trement n’a pas été entériné par la méthode SubmitChanges, l’entité insérée ne se trouvepas encore dans la base de données. Une situation comparable concerne les entitéssupprimées (voir Listing 16.4).

Listing 16.4 : Une autre illustration de la discordance des résultats dans le cache.

Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind");

Console.WriteLine("Ajout du client LAWN");db.Customers.InsertOnSubmit( new Customer { CustomerID = "LAWN", CompanyName = "Lawn Wranglers", ContactName = "Mr. Abe Henry", ContactTitle = "Owner", Address = "1017 Maple Leaf Way", City = "Ft. Worth", Region = "TX", PostalCode = "76104", Country = "USA", Phone = "(800) MOW-LAWN", Fax = "(800) MOW-LAWO" });

Console.WriteLine("Requête sur le client LAWN");Customer cust = (from c in db.Customers where c.CustomerID == "LAWN" select c).SingleOrDefault<Customer>();Console.WriteLine("Le client LAWN {0}.{1}", cust == null ? "n’existe pas" : "existe", System.Environment.NewLine);

Console.WriteLine("Suppression du client LONEP");cust = (from c in db.Customers where c.CustomerID == "LONEP" select c).SingleOrDefault<Customer>();db.Customers.DeleteOnSubmit(cust);

Console.WriteLine("Requête sur le client LONEP");cust = (from c in db.Customers where c.CustomerID == "LONEP" select c).SingleOrDefault<Customer>();Console.WriteLine("Le client LONEP {0}.{1}", cust == null ? "n’existe pas" : "existe", System.Environment.NewLine);

// Inutile de réinitialiser la base de données, puisque la méthode // SubmitChanges() n’a pas été appelée

Linq.book Page 517 Mercredi, 18. février 2009 7:58 07

Page 533: LINQ Language Integrated Query en C

518 LINQ to SQL Partie V

Dans ce code, nous avons inséré le client LAWN et lancé une requête pour voir s’ilexiste. Nous avons ensuite supprimé le client LONEP et lancé une requête pour voir s’ilexiste. Ces différentes actions ont été effectuées sans appeler la méthode SubmitChan-ges. Les objets entité mis en cache n’ont donc pas été sauvegardés dans la base dedonnées. Voici les résultats de ce code :

Pour éviter ce genre de situation, toutes ces actions devraient être incluses dans unetransaction. Reportez-vous à la section intitulée "Concurrence pessimiste" au Chapi-tre 17 pour avoir un exemple concret.

Traçage des modificationsLorsque le service de recherche d’identité crée un objet entité dans le cache, letraçage des modifications se met en branle pour cet objet. Ce processus consiste àstocker la valeur originale d’un objet entité. Il se poursuit jusqu’à ce que laméthode SubmitChanges soit exécutée. Cette dernière sauvegarde les modificationsapportées aux objets entité dans la base de données. Les valeurs originales sontdonc perdues et remplacées par les valeurs actuelles, et le traçage des modifications peutreprendre.

Ceci fonctionne tant que les objets entité sont récupérés dans la base de données.Cependant, l’instanciation d’un nouvel objet entité ne lui associe aucune identité niaucun traçage des modifications tant que le DataContext n’est pas au courant de sonexistence. Pour lui faire connaître l’existence de ce nouvel objet, il suffit d’insérerl’objet entité dans une propriété Table<T>. À titre d’exemple, la classe Northwindpossède la propriété Table<Customer> Customers. Si nous appelons la méthodeInsertOnSubmit sur la propriété Customers pour insérer un objet entité Customer dansTable<Customer>, le service d’identité et le traçage des modifications commencerontsur cet objet entité. Voici le code permettant d’insérer un client :

db.Customers.InsertOnSubmit( new Customer { CustomerID = "LAWN", CompanyName = "Lawn Wranglers", ContactName = "Mr. Abe Henry", ContactTitle = "Owner", Address = "1017 Maple Leaf Way", City = "Ft. Worth", Region = "TX", PostalCode = "76104", Country = "USA", Phone = "(800) MOW-LAWN", Fax = "(800) MOW-LAWO"});

Ajout du client LAWNRequête sur le client LAWNLe client LAWN n’existe pasSuppression du client LONEPRequête sur le client LONEPLe client LONEP existe

Linq.book Page 518 Mercredi, 18. février 2009 7:58 07

Page 534: LINQ Language Integrated Query en C

Chapitre 16 La classe DataContext 519

L’appel de la méthode InsertOnSubmit déclenche le démarrage du service d’identité etdu traçage des modifications sur le client LAWN.

Lorsque j’ai découvert le traçage des modifications, ce concept me paraissait quelquepeu obscur. L’appréhension du concept de base est chose assez simple, mais la compré-hension de son fonctionnement n’est pas aussi immédiate. Si vous prévoyez d’écrirevos classes d’entité à la main, vous devez comprendre le fonctionnement du traçage desmodifications. Reportez-vous à la section intitulée "Notifications de changement" auChapitre 15 pour compléter votre apprentissage.

Exécution des modificationsUn des services les plus importants fournis par le DataContext est le traçage des modi-fications pour les objets entité : lorsque vous insérez, modifiez ou supprimez un objetentité, le DataContext monitore tout changement. Cependant, aucun changement n’estsauvegardé dans la base de données : chacun d’entre eux est mis en cache par le Data-Context jusqu’à ce que la méthode SubmitChanges soit exécutée.

Lorsque vous appelez la méthode SubmitChanges, le processeur de changement del’objet DataContext effectue la mise à jour de la base de données. Dans un premiertemps, il insère les nouveaux objets entité dans sa liste d’objets entité tracés. Ensuite, ilordonne les objets entité modifiés en utilisant leurs dépendances (clé étrangère etcontrainte d’unicité). Si aucune transaction n’est définie, il en crée une afin d’assurerl’intégrité transactionnelle de toutes les commandes SQL exécutées pendant l’invoca-tion de la méthode SubmitChanges. Pour ce faire, il utilise le niveau d’isolation pardéfaut de SQL Server, ReadCommitted. Les données lues ne seront pas physiquementcorrompues, et seules les données validées seront lues. Cependant, étant donné que leverrou du mode ReadCommitted est partagé, rien n’empêche de modifier les donnéesavant la fin de la transaction. Enfin, il énumère la liste ordonnée d’objets entité modifiés,crée les déclarations SQL nécessaires et les exécute.

Si une erreur se produit pendant l’énumération des objets entité modifiés :

m Si la méthode SubmitChanges utilise un ConflictMode de type FailOnFirst-Conflict, l’énumération est avortée, la transaction défait toutes les modificationseffectuées dans la base de données et une exception est levée.

m Si la méthode SubmitChanges utilise un ConflictMode de type ContinueOn-Conflict, tous les objets entité modifiés sont énumérés et traités malgré les erreurs,et le DataContext dresse la liste des conflits. La transaction défait toutes les modi-fications effectuées dans la base de données et une exception est levée.

Tant que les modifications n’ont pas été sauvegardées dans la base de données, lesobjets entité modifiés restent en l’état. Cela donne au développeur l’opportunité d’essayerde résoudre le problème, puis d’appeler à nouveau la méthode SubmitChanges.

Linq.book Page 519 Mercredi, 18. février 2009 7:58 07

Page 535: LINQ Language Integrated Query en C

520 LINQ to SQL Partie V

Si toutes les modifications ont pu être sauvegardées dans la base de données, la transac-tion est validée et le pistage des transactions pour les objets entité modifiés estsupprimé. Un nouveau pistage peut donc être initié.

Datacontext() et [Your]DataContext()

La classe DataContext est généralement dérivée pour créer une classe [Your]Data-Context. Elle est utilisée pour établir la connexion et gérer les interactions avec la basede données. Vous utiliserez un des constructeurs suivants pour instancier un objet Data-context()/[Your]DataContext().

PrototypesQuatre prototypes du constructeur DataContext seront étudiés dans cet ouvrage.

Premier prototype

DataContext(string fileOrServerOrConnection);

Ce prototype utilise une chaîne de connexion ADO.NET. C’est certainement celui quevous utiliserez le plus fréquemment. Il est utilisé par la plupart des exemples LINQ toSQL de cet ouvrage.

Deuxième prototype

DataContext (System.Data.IDbConnection connection);

Comme System.Data.SqlClient.SqlConnection hérite de System.Data.Common.Db-Connection, qui lui-même implémente System.Data.IDbConnection, vous pouvezinstancier un DataContext ou un [Your]DataContext avec un SqlConnection déjàcréé. Ce prototype est utile si vous souhaitez mélanger du code LINQ to SQL avec ducode ADO.NET existant.

Troisième prototype

DataContext(string fileOrServerOrConnection, System.Data.Linq.MappingSource mapping);

Ce prototype est utile si vous disposez non pas d’une classe [Your]DataContext, maisd’un fichier de mappage XML. Dans certaines situations, vous pouvez disposer d’uneclasse métier existante dans laquelle vous ne pouvez pas ajouter les attributs LINQ toSQL appropriés. Peut-être n’avez-vous même pas accès au code source. Dans ce cas,vous pouvez générer un fichier de mappage avec SQLMetal ou l’écrire à la main pourtravailler avec une classe métier existante, ou une autre classe quelconque. Dans lepremier argument de ce prototype, vous devrez fournir une chaîne de connexion tradi-tionnelle ADO.NET.

Quatrième prototype

DataContext (System.Data.IDbConnection connection, System.Data.Linq.MappingSource mapping)

Linq.book Page 520 Mercredi, 18. février 2009 7:58 07

Page 536: LINQ Language Integrated Query en C

Chapitre 16 La classe DataContext 521

Ce prototype vous permet de créer une connexion LINQ to SQL à partir d’uneconnexion ADO.NET existante et de fournir un fichier de mappage XML. Ce prototypeest utile si vous devez combiner du code LINQ to SQL avec du code ADO.NET existantet que vous ne disposiez pas des classes d’entité agrémentées d’attributs.

ExemplesPour illustrer le premier prototype du constructeur DataContext, nous allons nousconnecter à un fichier physique .mdf en utilisant une chaîne de connexion ADO.NET(voir Listing 16.5).

Listing 16.5 : Utilisation du premier prototype du constructeur DataContext pour se connecter à un fichier de base de données.

DataContext dc = new DataContext(@"C:\Northwind.mdf");

IQueryable<Customer> query = from cust in dc.GetTable<Customer>()

where cust.Country == "USA"

select cust;

foreach (Customer c in query)

{

Console.WriteLine("{0}", c.CompanyName);

}

INFO

Vous serez certainement amené à changer le chemin passé au constructeur du DataContextpour référencer votre fichier .mdf.

Dans ce listing, nous avons seulement fourni le chemin du fichier .mdf pour instancierl’objet DataContext. Étant donné qu’un objet DataContext (et non un objet[Your]DataContext) est créé, la méthode GetTable<T> doit être appelée pour accéderaux clients. Voici les résultats :

Great Lakes Food MarketHungry Coyote Import StoreLazy K Kountry StoreLet’s Stop N ShopLonesome Pine RestaurantOld World DelicatessenRattlesnake Canyon GrocerySave-a-lot MarketsSplit Rail Beer & AleThe Big CheeseThe Cracker BoxTrail’s Head Gourmet ProvisionersWhite Clover Markets

Linq.book Page 521 Mercredi, 18. février 2009 7:58 07

Page 537: LINQ Language Integrated Query en C

522 LINQ to SQL Partie V

Nous allons maintenant utiliser le même code basique, mais cette fois-ci en utilisant laclasse [Your]DataContext. Ici, la classe Northwind (voir Listing 16.6).

Listing 16.6 : Utilisation du premier prototype du constructeur [Your]DataContext pour se connecter à un fichier de base de données.

Northwind db = new Northwind(@"C:\Northwind.mdf");

IQueryable<Customer> query = from cust in db.Customers where cust.Country == "USA" select cust;

foreach(Customer c in query){ Console.WriteLine("{0}", c.CompanyName);}

Dans ce listing, au lieu d’appeler la méthode GetTable<T>, nous nous sommes conten-tés de faire référence à la propriété Customers pour accéder aux clients. Ce code donneles mêmes résultats que le précédent :

Pour être complet, nous allons donner un dernier exemple du premier prototype. Cettefois-ci, nous utiliserons une chaîne de connexion pour nous connecter à la base dedonnées Northwind, hébergée sur un serveur de bases de données SQL Express. Dansle Listing 16.7, nous utiliserons la classe [Your]DataContext.

Listing 16.7 : Utilisation du premier prototype du constructeur [Your]DataContext pour se connecter à une base de données.

Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind");

IQueryable<Customer> query = from cust in db.Customers where cust.Country == "USA" select cust;

foreach(Customer c in query){ Console.WriteLine("{0}", c.CompanyName);}

Great Lakes Food MarketHungry Coyote Import StoreLazy K Kountry StoreLet’s Stop N ShopLonesome Pine RestaurantOld World DelicatessenRattlesnake Canyon GrocerySave-a-lot MarketsSplit Rail Beer & AleThe Big CheeseThe Cracker BoxTrail’s Head Gourmet ProvisionersWhite Clover Markets

Linq.book Page 522 Mercredi, 18. février 2009 7:58 07

Page 538: LINQ Language Integrated Query en C

Chapitre 16 La classe DataContext 523

Les résultats sont toujours les mêmes :

Le deuxième prototype de la classe DataContext est utile lorsque du code LINQ toSQL doit être combiné avec du code ADO.NET (voir Listing 16.8). Dans un premiertemps, nous créons un objet SqlConnection et insérons un enregistrement dans la tableCustomers par son intermédiaire. Dans un deuxième temps, nous utilisons l’objetSqlConnection pour instancier une classe [Your]DataContext. Dans un troisièmetemps, une requête LINQ to SQL est appliquée à la table Customers et les résultats sontaffichés. Enfin, dans un quatrième temps, nous utilisons ADO.NET pour supprimerl’enregistrement inséré dans la table Customers, lançons une requête LINQ to SQL surla table Customers et affichons les résultats.

Listing 16.8 : Utilisation du deuxième prototype du constructeur [Your]DataContext pour effectuer une connexion ADO.NET partagée.

System.Data.SqlClient.SqlConnection sqlConn = new System.Data.SqlClient.SqlConnection( @"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind;Integrated Security=SSPI;");

string cmd = @"insert into Customers values (’LAWN’, ’Lawn Wranglers’, ’Mr. Abe Henry’, ’Owner’, ’1017 Maple Leaf Way’, ’Ft. Worth’, ’TX’, ’76104’, ’USA’, ’(800) MOW-LAWN’, ’(800) MOW-LAWO’)";

System.Data.SqlClient.SqlCommand sqlComm = new System.Data.SqlClient.SqlCommand(cmd);

sqlComm.Connection = sqlConn;try{ sqlConn.Open(); // Insertion de l’enregistrement sqlComm.ExecuteNonQuery();

Northwind db = new Northwind(sqlConn);

IQueryable<Customer> query = from cust in db.Customers where cust.Country == "USA" select cust;

Console.WriteLine("Les clients après l’insertion et avant la suppression");foreach (Customer c in query){

Great Lakes Food MarketHungry Coyote Import StoreLazy K Kountry StoreLet’s Stop N ShopLonesome Pine RestaurantOld World DelicatessenRattlesnake Canyon GrocerySave-a-lot MarketsSplit Rail Beer & AleThe Big CheeseThe Cracker BoxTrail’s Head Gourmet ProvisionersWhite Clover Markets

Linq.book Page 523 Mercredi, 18. février 2009 7:58 07

Page 539: LINQ Language Integrated Query en C

524 LINQ to SQL Partie V

Console.WriteLine("{0}", c.CompanyName);}

sqlComm.CommandText = "delete from Customers where CustomerID = ’LAWN’";// Delete the record.sqlComm.ExecuteNonQuery();

Console.WriteLine("{0}{0}Les clients après la suppression", System.Environment.NewLine);foreach (Customer c in query){ Console.WriteLine("{0}", c.CompanyName);}}finally{ // Fermeture de la connexion sqlComm.Connection.Close();}

Comme vous pouvez le voir, la requête LINQ a été définie une seule fois. En revanche,elle a été énumérée à deux reprises. Étant donné que la requête est différée, sa définitionne provoque pas son exécution immédiate : ce n’est qu’au moment de son énumérationqu’elle est réellement exécutée. Ceci est mis en évidence par le fait que les résultatsdiffèrent dans les deux énumérations. Le Listing 16.8 montre également qu’ADO.NETet LINQ to SQL peuvent cohabiter harmonieusement. Voici les résultats :

Les clients après l’insertion et avant la suppressionGreat Lakes Food MarketHungry Coyote Import StoreLawn WranglersLazy K Kountry StoreLet’s Stop N ShopLonesome Pine RestaurantOld World DelicatessenRattlesnake Canyon GrocerySave-a-lot MarketsSplit Rail Beer & AleThe Big CheeseThe Cracker BoxTrail’s Head Gourmet ProvisionersWhite Clover Markets

Les clients après la suppressionGreat Lakes Food MarketHungry Coyote Import StoreLazy K Kountry StoreLet’s Stop N ShopLonesome Pine RestaurantOld World DelicatessenRattlesnake Canyon GrocerySave-a-lot MarketsSplit Rail Beer & AleThe Big CheeseThe Cracker BoxTrail’s Head Gourmet ProvisionersWhite Clover Markets

Linq.book Page 524 Mercredi, 18. février 2009 7:58 07

Page 540: LINQ Language Integrated Query en C

Chapitre 16 La classe DataContext 525

Pour illustrer le troisième prototype, nous n’allons pas utiliser les classes d’entité de labase de données Northwind. Nous utiliserons une classe Customer qui a été écritemanuellement et un fichier de mappage abrégé. Pour dire la vérité, la classe Customerest issue de SQLMetal, privée des attributs LINQ to SQL.

La classe "écrite à la main"

namespace Linqdev{ public partial class Customer { private string _CustomerID; private string _CompanyName; private string _ContactName; private string _ContactTitle; private string _Address; private string _City; private string _Region; private string _PostalCode; private string _Country; private string _Phone; private string _Fax;

public Customer() { }

public string CustomerID { get { return this._CustomerID; } set { if ((this._CustomerID != value)) { this._CustomerID = value; } } }

public string CompanyName { get { return this._CompanyName; } set { if ((this._CompanyName != value)) { this._CompanyName = value; } } }

public string ContactName { get

Linq.book Page 525 Mercredi, 18. février 2009 7:58 07

Page 541: LINQ Language Integrated Query en C

526 LINQ to SQL Partie V

{ return this._ContactName; } set { if ((this._ContactName != value)) { this._ContactName = value; } } }

public string ContactTitle { get { return this._ContactTitle; } set { if ((this._ContactTitle != value)) { this._ContactTitle = value; } }}

public string Address { get { return this._Address; } set { if ((this._Address != value)) { this._Address = value; } } }

public string City { get { return this._City; } set { if ((this._City != value)) { this._City = value; } } }

public string Region { get { return this._Region;

Linq.book Page 526 Mercredi, 18. février 2009 7:58 07

Page 542: LINQ Language Integrated Query en C

Chapitre 16 La classe DataContext 527

} set { if ((this._Region != value)) { this._Region = value; } } }

public string PostalCode { get { return this._PostalCode; } set { if ((this._PostalCode != value)) { this._PostalCode = value; } } }

public string Country { get { return this._Country; } set { if ((this._Country != value)) { this._Country = value; } } }

public string Phone { get { return this._Phone; } set { if ((this._Phone != value)) { this._Phone = value; } } }

public string Fax { get { return this._Fax; } set

Linq.book Page 527 Mercredi, 18. février 2009 7:58 07

Page 543: LINQ Language Integrated Query en C

528 LINQ to SQL Partie V

{ if ((this._Fax != value)) { this._Fax = value; } } } }}

Cette classe d’entité est certainement la pire qui ait jamais été écrite : elle ne gère pasles notifications de changement, et ses attributs LINQ to SQL ont été supprimés. Jevous invite à consulter le Chapitre 15 pour apprendre à écrire des classes d’entité debonne facture.

Cette classe se trouve dans l’espace de noms Linqdev. Cette information est importantecar elle doit être spécifiée dans le code de l’exemple (pour la différencier de celle demême nom qui se trouve dans l’espace de noms nwind), mais aussi dans le fichier demappage externe.

Une chose est importante dans cet exemple : une propriété a été définie pour chaquechamp de la base de données mappé au fichier externe. Voyons maintenant le fichier demappage externe utilisé dans cet exemple.

Un fichier de mappage externe XML abrégé

<?xml version="1.0" encoding="utf-8"?><Database Name="Northwind" xmlns="http://schemas.microsoft.com/linqtosql/mapping/2007"> <Table Name="dbo.Customers" Member="Customers"> <Type Name="Linqdev.Customer"> <Column Name="CustomerID" Member="CustomerID" Storage="_CustomerID" DbType="NChar(5) NOT NULL" CanBeNull="false" IsPrimaryKey="true" /> <Column Name="CompanyName" Member="CompanyName" Storage="_CompanyName" DbType="NVarChar(40) NOT NULL" CanBeNull="false" /> <Column Name="ContactName" Member="ContactName" Storage="_ContactName" DbType="NVarChar(30)" /> <Column Name="ContactTitle" Member="ContactTitle" Storage="_ContactTitle" DbType="NVarChar(30)" /> <Column Name="Address" Member="Address" Storage="_Address" DbType="NVarChar(60)" /> <Column Name="City" Member="City" Storage="_City" DbType="NVarChar(15)" /> <Column Name="Region" Member="Region" Storage="_Region" DbType="NVarChar(15)" /> <Column Name="PostalCode" Member="PostalCode" Storage="_PostalCode" DbType="NVarChar(10)" /> <Column Name="Country" Member="Country" Storage="_Country" DbType="NVarChar(15)" /> <Column Name="Phone" Member="Phone" Storage="_Phone" DbType="NVarChar(24)" /> <Column Name="Fax" Member="Fax" Storage="_Fax" DbType="NVarChar(24)" /> </Type> </Table></Database>

Linq.book Page 528 Mercredi, 18. février 2009 7:58 07

Page 544: LINQ Language Integrated Query en C

Chapitre 16 La classe DataContext 529

Comme vous pouvez le voir dans les premières lignes, il est précisé que ce fichier demappage s’applique à la classe d’entité Customer située dans l’espace de nomsLinqdev.

Ce fichier XML a pour nom abbreviatednorthwindmap.xml. Il a été sauvegardé dans ledossier bin\Debug.

Dans le Listing 16.9, nous allons utiliser la classe d’entité écrite à la main Customer etle fichier de mappage abbreviatednorthwindmap.xml pour effectuer une requête LINQto SQL sans utiliser un seul attribut.

Listing 16.9 : Utilisation du troisième prototype du constructeur pour se connecter à une base de données en utilisant un fichier de mappage.

string mapPath = "abbreviatednorthwindmap.xml";XmlMappingSource nwindMap = XmlMappingSource.FromXml(System.IO.File.ReadAllText(mapPath));

DataContext db = new DataContext( @"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind;Integrated Security=SSPI;", nwindMap);

IQueryable<Linqdev.Customer> query = from cust in db.GetTable<Linqdev.Customer>() where cust.Country == "USA" select cust;

foreach (Linqdev.Customer c in query){ Console.WriteLine("{0}", c.CompanyName);}

INFO

Le fichier de mappage abbreviatednorthwindmap.xml a été placé dans le dossier bin\Debugdu projet Visual Studio, car nous allons compiler et exécuter le projet en mode de débogage.

Comme vous pouvez le voir, le premier bloc instancie l’objet XmlMappingSource àpartir du fichier de mappage et le deuxième passe cet objet au constructeur du Data-Context. Dans le troisième bloc, notez qu’il n’est pas possible d’utiliser la propriétéCustomers Table<Customer> dans l’objet DataContext de la requête LINQ to SQL.Ceci est dû au fait que nous utilisons la classe de base DataContext et non une classe[Your]DataContext.

Notez également que chaque référence à la classe Customer spécifie explicitementl’espace de noms Linqdev. Ceci afin de s’assurer que la classe Customer générée parSQLMetal n’est pas utilisée.

Linq.book Page 529 Mercredi, 18. février 2009 7:58 07

Page 545: LINQ Language Integrated Query en C

530 LINQ to SQL Partie V

Voici les résultats du Listing 16.9 :

Cet exemple utilise une classe Customer dans laquelle la plupart du code qui assure lebon fonctionnement d’une classe d’entité est absent. Cependant, il vous a montré qu’ilétait possible d’effectuer des requêtes en utilisant un fichier de mappage et une classedénuée d’attributs LINQ to SQL.

Le quatrième prototype est une combinaison des deuxième et troisième prototypes.Nous allons l’illustrer dans le Listing 16.10.

Listing 16.10 : Utilisation du quatrième prototype du constructeur DataContext pour se connecter à une base de données avec une connexion ADO.NET partagée et un fichier de mappage.

System.Data.SqlClient.SqlConnection sqlConn = new System.Data.SqlClient.SqlConnection( @"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind;Integrated Security=SSPI;");

string cmd = @"insert into Customers values (’LAWN’, ’Lawn Wranglers’, ’Mr. Abe Henry’, ’Owner’, ’1017 Maple Leaf Way’, ’Ft. Worth’, ’TX’, ’76104’, ’USA’, ’(800) MOW-LAWN’, ’(800) MOW-LAWO’)";

System.Data.SqlClient.SqlCommand sqlComm = new System.Data.SqlClient.SqlCommand(cmd);

sqlComm.Connection = sqlConn;try{ sqlConn.Open(); // Insertion de l’enregistrement sqlComm.ExecuteNonQuery();

string mapPath = "abbreviatednorthwindmap.xml"; XmlMappingSource nwindMap = XmlMappingSource.FromXml(System.IO.File.ReadAllText(mapPath));

DataContext db = new DataContext(sqlConn, nwindMap);

IQueryable<Linqdev.Customer> query = from cust in db.GetTable<Linqdev.Customer>() where cust.Country == "USA" select cust;

Console.WriteLine("Clients après l’insertion et avant la suppression"); foreach (Linqdev.Customer c in query)

Great Lakes Food MarketHungry Coyote Import StoreLazy K Kountry StoreLet’s Stop N ShopLonesome Pine RestaurantOld World DelicatessenRattlesnake Canyon GrocerySave-a-lot MarketsSplit Rail Beer & AleThe Big CheeseThe Cracker BoxTrail’s Head Gourmet ProvisionersWhite Clover Markets

Linq.book Page 530 Mercredi, 18. février 2009 7:58 07

Page 546: LINQ Language Integrated Query en C

Chapitre 16 La classe DataContext 531

{ Console.WriteLine("{0}", c.CompanyName); }

sqlComm.CommandText = "delete from Customers where CustomerID = ’LAWN’"; // Suppression de l’enregistrement sqlComm.ExecuteNonQuery();

Console.WriteLine("{0}{0}Customers after deletion.", System.Environment.NewLine); foreach (Linqdev.Customer c in query) { Console.WriteLine("{0}", c.CompanyName); }}finally{ // Fermeture de la connexion sqlComm.Connection.Close();}

Tout comme le listing précédent, celui-ci s’appuie sur la classe Linqdev.Customer et lefichier de mappage externe abbreviatednorthwindmap.xml.

Cet exemple montre comment interroger une base de données avec LINQ to SQL enutilisant une connexion ADO.NET et une classe d’entité privée d’attributs. Les résultatssont conformes aux attentes :

Les exemples précédents vous ont montré que la connexion via un DataContext ou un[Your]DataContext est un vrai jeu d’enfant.

Clients après l’insertion et avant la suppressionGreat Lakes Food MarketHungry Coyote Import StoreLawn WranglersLazy K Kountry StoreLet’s Stop N ShopLonesome Pine RestaurantOld World DelicatessenRattlesnake Canyon GrocerySave-a-lot MarketsSplit Rail Beer & AleThe Big CheeseThe Cracker BoxTrail’s Head Gourmet ProvisionersWhite Clover Markets

Clients après la suppression.Great Lakes Food MarketHungry Coyote Import StoreLazy K Kountry StoreLet’s Stop N ShopLonesome Pine RestaurantOld World DelicatessenRattlesnake Canyon GrocerySave-a-lot MarketsSplit Rail Beer & AleThe Big CheeseThe Cracker BoxTrail’s Head Gourmet ProvisionersWhite Clover Markets

Linq.book Page 531 Mercredi, 18. février 2009 7:58 07

Page 547: LINQ Language Integrated Query en C

532 LINQ to SQL Partie V

SubmitChanges()

Le DataContext place dans un cache les modifications effectuées dans les objets entité,jusqu’à ce que la méthode SubmitChanges soit appelée. Cette méthode lance le proces-seur de changement et les objets entité modifiés sont sauvegardés dans la base dedonnées.

Si une transaction n’est pas disponible pour le DataContext lorsque la méthodeSubmitChanges est appelée, elle est créée et les modifications s’effectuent par son inter-médiaire. Ainsi, si une transaction échoue, toutes les modifications déjà effectuées dansla base de données peuvent être défaites.

Si un conflit d’accès concurrentiel se produit, une exception ChangeConflictExcep-tion est levée afin que vous puissiez essayer de le résoudre et tenter une nouvellesauvegarde. Une chose appréciable : dans le DataContext, la méthode ResolveAll dela collection ChangeConflicts peut être utilisée pour résoudre automatiquement tousles conflits !

Si nécessaire, reportez-vous au Chapitre 17 pour avoir de plus amples détails sur lesconflits d’accès concurrentiel.

PrototypesDeux prototypes de la méthode SubmitChanges seront étudiés dans cet ouvrage.

Premier prototype

void SubmitChanges()

Ce prototype ne demande aucun argument. Il se comporte comme le second prototype,lorsque l’argument ConflictMode a pour valeur ConflictMode.FailOnFirstConflict.

Second prototype

void SubmitChanges(ConflictMode failureMode)

Ce prototype permet de préciser le mode de gestion des conflits. Les valeurs possiblessont ConflictMode.FailOnFirstConflict et ConflictMode.ContinueOnConflict. Sivous choisissez la première valeur, une exception ChangeConflictException est levéedès qu’un conflit est détecté. Si vous choisissez la seconde, SubmitChanges tented’effectuer toutes les modifications dans la base de données, de telle sorte qu’elles puis-sent être prises en compte et résolues en une seule étape lorsque l’exception Change-ConflictException est levée.

La comptabilisation des conflits s’effectue au niveau du nombre d’enregistrements etnon du nombre de champs. Si, par exemple, deux champs d’un même enregistrementproduisent un conflit, un seul conflit sera généré.

Linq.book Page 532 Mercredi, 18. février 2009 7:58 07

Page 548: LINQ Language Integrated Query en C

Chapitre 16 La classe DataContext 533

ExemplesLa plupart des exemples du Chapitre 14 font appel à la méthode SubmitChanges. Il estdonc un peu tard pour présenter un exemple d’utilisation basique de cette méthode.Plutôt que vous montrer pour la énième fois comment enregistrer les entités modifiéesdans une base de données en utilisant la méthode SubmitChanges, nous allons doncchoisir quelque chose d’un peu différent.

Pour illustrer le premier prototype, nous allons montrer que les modifications ne sontpas reportées dans la base de données tant que la méthode SubmitChanges n’est pasappelée. Cet exemple étant plus complexe que les précédents, nous donnerons desexplications à chaque fois que cela sera nécessaire (voir Listing 16.11).

Listing 16.11 : Un exemple d’utilisation du premier prototype de SubmitChanges.

System.Data.SqlClient.SqlConnection sqlConn = new System.Data.SqlClient.SqlConnection( @"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind;Integrated Security=SSPI;");

try { sqlConn.Open();

string sqlQuery = "select ContactTitle from Customers where CustomerID = ’LAZYK’"; string originalTitle = GetStringFromDb(sqlConn, sqlQuery); string title = originalTitle; Console.WriteLine("ContactTitle lu dans la base de données : {0}", title);

Northwind db = new Northwind(sqlConn);

Customer c = (from cust in db.Customers where cust.CustomerID == "LAZYK" select cust). Single<Customer>(); Console.WriteLine("ContactTitle lu dans l’entité objet : {0}", c.ContactTitle);

Dans les premières lignes, une connexion à une base de données ADO.NET est créée etouverte. Le client dont le champ ContactTitle a pour valeur LAZYK est alors récu-péré en utilisant la méthode GetStringFromDb, puis affiché. Un objet Northwind estalors créé en utilisant la connexion ADO.NET puis interrogé en utilisant LINQ to SQL,et son champ ContactTitle est affiché. À ce point, les deux ContactTitle devraientêtre identiques.

Console.WriteLine(String.Format( "{0}Affectation de la valeur ’Director of Marketing’ à l’entité objet ContactTitle", System.Environment.NewLine));c.ContactTitle = "Director of Marketing";

title = GetStringFromDb(sqlConn, sqlQuery);Console.WriteLine("ContactTitle lu dans la base de données : {0}", title);

Customer c2 = (from cust in db.Customers where cust.CustomerID == "LAZYK" select cust). Single<Customer>();Console.WriteLine("Title from entity object : {0}", c2.ContactTitle);

Linq.book Page 533 Mercredi, 18. février 2009 7:58 07

Page 549: LINQ Language Integrated Query en C

534 LINQ to SQL Partie V

Dans les premières lignes, l’objet entité LINQ to SQL ContactTitle du client estmodifié. Une requête sur le champ ContactTitle de la base de données et sur l’entitéobjet correspondante est à nouveau effectuée et ces deux informations sont affichées.Cette fois-ci, les deux valeurs ne devraient pas être identiques car la modification n’apas encore été sauvegardée dans la base de données.

db.SubmitChanges();Console.WriteLine(String.Format( "{0}La méthode SubmitChanges a été appelée", System.Environment.NewLine)); title = GetStringFromDb(sqlConn, sqlQuery); Console.WriteLine("ContactTitle lu dans la base de données : {0}", title);

Console.WriteLine("Restauration de la valeur originale de ContactTitle "); c.ContactTitle = "Marketing Manager"; db.SubmitChanges(); Console.WriteLine("ContactTitle restauré.");}finally{ sqlConn.Close();}

La méthode SubmitChanges est appelée, puis le champ ContactTitle est récupéré dansla base de données. Cette fois-ci, la valeur stockée dans la base de données a dû êtremise à jour, puisque la méthode SubmitChanges a été exécutée.

Les dernières lignes redonnent la valeur originale à l’entité objet ContactTitle etl’enregistrent dans la base de données en utilisant la méthode SubmitChanges. Ainsi,cet exemple pourra être exécuté plusieurs fois et les exemples suivants ne seront pasaffectés.

Ce code montre que les modifications effectuées dans les objets entité ne sont pasreportées dans la base de données tant que la méthode SubmitChanges n’est pas appe-lée. Il montre également qu’il suffit d’appeler la méthode GetStringFromDb pour lirele champ ContactTitle dans la base de données en utilisant ADO.NET. Voici lesrésultats :

Comme le montrent les résultats, la valeur du champ ContactTitle n’est pas modifiéetant que la méthode SubmitChanges n’est pas appelée.

Pour illustrer le second prototype de la méthode SubmitChanges, nous allons intention-nellement induire deux erreurs d’accès concurrentiel sur deux enregistrements. Pour ce

ContactTitle lu dans la base de données : Marketing ManagerContactTitle lu dans l’entité objet : Marketing ManagerAffectation de la valeur ’Director of Marketing’ à l’entité objet ContactTitleContactTitle lu dans la base de données : Marketing ManagerContactTitle lu dans l’entité objet : Director of MarketingLa méthode SubmitChanges() a été appeléeContactTitle lu dans la base de données : Director of MarketingRestauration de la valeur originale de ContactTitleContactTitle restauré

Linq.book Page 534 Mercredi, 18. février 2009 7:58 07

Page 550: LINQ Language Integrated Query en C

Chapitre 16 La classe DataContext 535

faire, nous les mettrons à jour avec ADO.NET entre le moment où ils sont obtenus(avec LINQ to SQL) et le moment où on tente de les mettre à jour (avec LINQ to SQL).Nous pourrons ainsi montrer la différence entre les modes ConflictMode.FailOn-FirstConflict et ConflictMode.ContinueOnConflict.

Vers la fin du code, nous restaurerons les valeurs originales dans la base de données afinque l’exemple puisse être exécuté plusieurs fois. Si vous stoppez le code avant qu’il nesoit entièrement exécuté, vous devrez peut-être réinitialiser manuellement ces valeurs.

Dans le premier exemple du second prototype (voir Listing 16.12), nous initialiseronsle paramètre ConflictMode à la valeur ContinueOnConflict afin de montrer queSubmitChanges est en mesure de gérer plusieurs conflits. Cet exemple étant assezcomplexe, nous donnerons des explications chaque fois que cela sera nécessaire.

Listing 16.12 : Démonstration du mode ContinueOnConflict via le second prototype de la méthode SubmitChanges.

Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind");

Console.WriteLine("Requête LINQ sur le client LAZYK");Customer cust1 = (from c in db.Customers where c.CustomerID == "LAZYK" select c).Single<Customer>();

Console.WriteLine("Requête LINQ sur le client LONEP");Customer cust2 = (from c in db.Customers where c.CustomerID == "LONEP" select c).Single<Customer>();

Ces quelques lignes créent un DataContext Northwind et effectuent une requête sur lesclients LAZYK et LONEP.

string cmd = @"update Customers set ContactTitle = ’Director of Marketing’ where CustomerID = ’LAZYK’; update Customers set ContactTitle = ’Director of Sales’ where CustomerID = ’LONEP’";ExecuteStatementInDb(cmd);

La valeur du champ ContactTitle des deux enregistrements est ensuite modifiée dansla base de données en utilisant la méthode commune ExecuteStatementInDb (les modi-fications sont faites via ADO.NET). Arrivés à ce point, nous sommes potentiellementprêts pour déclencher les conflits d’accès concurrentiel pour ces deux enregistrements.

Console.WriteLine("Modification de la colonne ContactTitle dans les objets entité pour LAZYK et LONEP");cust1.ContactTitle = "Vice President of Marketing";cust2.ContactTitle = "Vice President of Sales";

Ce bloc de code modifie le champ ContactTitle des deux clients. Ainsi, lorsque nousappellerons la méthode SubmitChanges dans le prochain bloc de code, le processeur dechangement de l’objet DataContext tentera d’enregistrer les modifications dans la basede données et de détecter les conflits d’accès concurrentiel.

try{

Linq.book Page 535 Mercredi, 18. février 2009 7:58 07

Page 551: LINQ Language Integrated Query en C

536 LINQ to SQL Partie V

Console.WriteLine("Appel de SubmitChanges() ..."); db.SubmitChanges(ConflictMode.ContinueOnConflict); Console.WriteLine("L’appel à SubmitChanges() a réussi");}

Ce bloc de code appelle la méthode SubmitChanges. Les modifications effectuées dansles objets entité vont tenter d’être sauvegardées dans la base de données par le proces-seur de changement. Comme les valeurs des champs ContactTitle ont été modifiéesdepuis leur première lecture dans la base de données, un conflit d’accès concurrentielva être détecté.

catch (ChangeConflictException ex){ Console.WriteLine("Un ou plusieurs conflits se sont produits en appelant ➥SubmitChanges() : {0}.", ex.Message);

foreach (ObjectChangeConflict objectConflict in db.ChangeConflicts) { Console.WriteLine("Un conflit pour {0} a été détecté", ((Customer)objectConflict.Object).CustomerID); foreach (MemberChangeConflict memberConflict in objectConflict.MemberConflicts) { Console.WriteLine(" Valeur LINQ : {0}{1} Valeur dans la base de données : ➥{2}", memberConflict.CurrentValue, System.Environment.NewLine, memberConflict.DatabaseValue); } }}

Ce bloc de code est là pour gérer l’exception ChangeConflictException. C’est là queles choses deviennent intéressantes. Dans un premier temps, nous énumérons la collec-tion ChangeConflicts, composée d’objets DataContext db. Cette collection mémori-sera des objets ObjectChangeConflict. Ces objets possèdent une propriété nomméeObject qui fait référence à l’objet entité qui est à l’origine du conflit d’accès concurren-tiel. Afin d’accéder aux valeurs des propriétés de l’objet entité Object, un casting lui estappliqué en utilisant le type de la classe d’entité. Nous pouvons ainsi accéder à lapropriété CustomerID.

Pour chacun des objets ObjectChangeConflict, nous énumérons sa collection d’objetsMemberChangeConflict et affichons les informations qui nous intéressent. Ici la valeurissue de LINQ et la valeur issue de la base de données.

Console.WriteLine("{0}Réinitialisation des données à leurs valeurs initiales", System.Environment.NewLine);

cmd = @"update Customers set ContactTitle = ’Marketing Manager’ where CustomerID = ’LAZYK’; update Customers set ContactTitle = ’Sales Manager’ where CustomerID = ’LONEP’";ExecuteStatementInDb(cmd);

Ce bloc de code restaure les enregistrements initiaux dans la base de données. L’exemplepourra donc être exécuté autant de fois que vous le souhaitez.

Linq.book Page 536 Mercredi, 18. février 2009 7:58 07

Page 552: LINQ Language Integrated Query en C

Chapitre 16 La classe DataContext 537

Les énumérations des collections relatives aux conflits ne sont pas une obligation. Cetexemple vous a montré comment extraire quelques-unes des informations correspon-dant aux conflits, pour le cas où vous en auriez besoin.

Comme vous pouvez le voir, ce listing ne fait rien pour résoudre les conflits : il secontente d’afficher les informations correspondantes.

Voici les résultats du code :

Deux conflits ont été détectés : un pour chacun des enregistrements modifiés parADO.NET. Cet exemple montre que le processeur de changement tente toujours desauvegarder les données après que le premier conflit eut été détecté. Ceci est dû au faitque le mode ContinueOnFirstConflict a été passé en argument lors de l’appel de laméthode SubmitChanges.

Le Listing 16.13 est identique au précédent mais, ici, le paramètre ConflictMode a pourvaleur FailOnFirstConflict lors de l’appel de la méthode SubmitChanges.

Listing 16.13 : Démonstration du mode FailOnFirstConflict via le second prototype de la méthode SubmitChanges.

Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind");

Console.WriteLine("Requête LINQ sur le client LAZYK");Customer cust1 = (from c in db.Customers where c.CustomerID == "LAZYK" select c).Single<Customer>();

Console.WriteLine("Requête LINQ sur le client LONEP");Customer cust2 = (from c in db.Customers where c.CustomerID == "LONEP" select c).Single<Customer>();string cmd = @"update Customers set ContactTitle = ’Director of Marketing’ where CustomerID = ’LAZYK’; update Customers set ContactTitle = ’Director of Sales’ where CustomerID = ’LONEP’";

Requête LINQ sur le client LAZYKRequête LINQ sur le client LONEPExécution d’une déclaration SQL sur la base de données avec ADO.NETBase de données mise à jourModification de la colonne ContactTitle dans les objets entité pour LAZYK et LONEPAppel de SubmitChanges()Un ou plusieurs conflits se sont produits en appelant SubmitChanges(): 2 sur 2 mises à jour ont échouéUn conflit pour LAZYK a été détectéValeur LINQ : Vice President of MarketingValeur dans la base de données : Director of MarketingUn conflit pour LONEP a été détectéValeur LINQ : Vice President of SalesValeur dans la base de données : Director of SalesRéinitialisation des données à leurs valeurs initialesExécution d’une déclaration SQL sur la base de données avec ADO.NETBase de données mise à jour

Linq.book Page 537 Mercredi, 18. février 2009 7:58 07

Page 553: LINQ Language Integrated Query en C

538 LINQ to SQL Partie V

ExecuteStatementInDb(cmd);

Console.WriteLine("Modification de la colonne ContactTitle dans les objets entité pour LAZYK et LONEP");cust1.ContactTitle = "Vice President of Marketing";cust2.ContactTitle = "Vice President of Sales";

try{ Console.WriteLine("Appel de SubmitChanges() ..."); db.SubmitChanges(ConflictMode.FailOnFirstConflict); Console.WriteLine("L’appel à SubmitChanges() a réussi");}catch (ChangeConflictException ex){ Console.WriteLine("Un ou plusieurs conflits se sont produits en appelant ➥SubmitChanges() : {0}.", ex.Message);

foreach (ObjectChangeConflict objectConflict in db.ChangeConflicts) { Console.WriteLine("Un conflit pour {0} a été détecté", ((Customer)objectConflict.Object).CustomerID); foreach (MemberChangeConflict memberConflict in objectConflict.MemberConflicts) { Console.WriteLine(" Valeur LINQ : {0}{1} Valeur dans la base de données : ➥{2}", memberConflict.CurrentValue, System.Environment.NewLine, memberConflict.DatabaseValue); } }}Console.WriteLine("{0}Réinitialisation des données à leurs valeurs initiales", System.Environment.NewLine);

cmd = @"update Customers set ContactTitle = ’Marketing Manager’ where CustomerID = ’LAZYK’; update Customers set ContactTitle = ’Sales Manager’ where CustomerID = ’LONEP’";ExecuteStatementInDb(cmd);

Cette fois-ci, les résultats devraient mettre en évidence que le processeur de change-ment arrête les mises à jour après le premier conflit de concurrence :

Requête LINQ sur le client LAZYKRequête LINQ sur le client LONEPExécution d’une déclaration SQL sur la base de données avec ADO.NETBase de données mise à jourModification de la colonne ContactTitle dans les objets entité pour LAZYK et LONEPAppel de SubmitChanges()Un ou plusieurs conflits se sont produits en appelant SubmitChanges(): Ligne non ➥trouvée ou modifiée.Un conflit pour LAZYK a été détectéValeur LINQ : Vice President of MarketingValeur dans la base de données : Director of MarketingRéinitialisation des données à leurs valeurs initialesExécution d’une déclaration SQL sur la base de données avec ADO.NETBase de données mise à jour

Linq.book Page 538 Mercredi, 18. février 2009 7:58 07

Page 554: LINQ Language Integrated Query en C

Chapitre 16 La classe DataContext 539

Comme vous pouvez le voir, même si deux conflits ont été provoqués, le processeur dechangement a arrêté la mise à jour de la base de données après la détection du premier.Par voie de conséquence, un seul conflit est signalé dans la console.

DatabaseExists()

La méthode DatabaseExists peut être utilisée pour déterminer l’existence d’une basede données. Cette détermination se base sur la chaîne de connexion spécifiée lors del’instanciation du DataContext. Si vous spécifiez un fichier .mdf, il sera recherché dansle chemin indiqué. Si vous spécifiez un serveur, il est recherché.

La méthode DatabaseExists est souvent utilisée conjointement aux méthodes Delete-Database et CreateDatabase.

PrototypeUn seul prototype de cette méthode sera étudié dans cet ouvrage :

bool DatabaseExists()

Cette méthode retourne la valeur true si la base de données spécifiée dans la chaîne deconnexion lors de l’instanciation du DataContext existe. Dans le cas contraire, elleretourne la valeur false.

ExemplePour une fois, la méthode à illustrer est très simple ! Le Listing 16.14 instancie unDataContext et appelle la méthode DatabaseExists pour voir si la base de donnéesNorthwind existe.

Listing 16.14 : Un exemple d’utilisation de la méthode DatabaseExists.

Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind");

Console.WriteLine("The Northwind database {0}.", db.DatabaseExists() ? "existe" : "n’existe pas");

Voici les résultats :

Juste pour le fun, détachez la base de données Northwind et exécutez à nouveau leprogramme. Vous obtiendrez le message suivant dans la console :

Si vous avez détaché la base de données Northwind, n’oubliez pas de l’attacher ànouveau pour que les autres exemples du livre puissent être exécutés.

La base de données Northwind existe.

La base de données Northwind n’existe pas.

Linq.book Page 539 Mercredi, 18. février 2009 7:58 07

Page 555: LINQ Language Integrated Query en C

540 LINQ to SQL Partie V

CreateDatabase()

Une classe d’entité possède une telle quantité d’informations sur la structure de la basede données à laquelle elle est mappée que Microsoft a cru bon de définir la méthodeCreateDatabase pour… créer la base de données à partir de sa classe d’entité.

Soyez bien conscient que la méthode CreateDatabase ne peut créer que les portions dela base de données qui correspondent aux attributs de la classe d’entité ou au fichier demappage. Les contenus tels que procédures stockées, triggers, fonctions définies parl’utilisateur et limitations de vérification ne seront pas inclus dans une base de donnéesdéfinie par cette méthode, puisque aucun attribut ne spécifie les informations corres-pondantes. Pour des applications de petite envergure, ceci peut néanmoins suffire…

ATTENTIONATTENTION

Contrairement à la plupart des modifications effectuées dans une base de données via leDataContext, la méthode CreateDatabase s’exécute immédiatement. Il n’est pas néces-saire d’appeler la méthode SubmitChanges, et l’exécution n’est pas différée. Cette instruc-tion vous permet donc de créer une base de données et d’y insérer immédiatement desdonnées.

PrototypeNous étudierons un seul prototype de la méthode CreateDatabase :

void CreateDatabase()

Cette méthode n’admet aucun argument et ne retourne aucune valeur.

ExempleLe Listing 16.15 donne un exemple élémentaire de la méthode CreateDatabase.

Listing 16.15 : Un exemple de la méthode CreateDatabase.

Northwind db = new Northwind(@"C:\Northwnd.mdf");db.CreateDatabase();

INFO

Le nom Northwnd.mdf a été choisi intentionnellement, afin de ne pas détruire la base dedonnées Northwind.

Ce code ne provoque aucune sortie dans la console. Cependant, si vous visualisez lecontenu de la racine du disque C:, vous verrez les fichiers Northwnd.mdf etNorthwnd.ldf. Par ailleurs, si vous ouvrez l’environnement intégré SQL Server Mana-gement Studio, vous verrez que le fichier Northwnd.mdf est attaché. Cette méthode estsouvent utilisée conjointement à la méthode DatabaseExists. Si vous essayez d’appe-ler la méthode CreateDatabase sur une base de données déjà existante, une exception

Linq.book Page 540 Mercredi, 18. février 2009 7:58 07

Page 556: LINQ Language Integrated Query en C

Chapitre 16 La classe DataContext 541

est levée. Pour illustrer cette dernière phrase, il vous suffit d’exécuter le code duListing 16.15 une deuxième fois, sans supprimer ni détacher la base de données de SQLServer Management Studio ou Entreprise Manager. Le message suivant sera alors affichédans la console :

Ne pensez pas qu’il suffise de supprimer les deux fichiers relatifs à la base de donnéespour que vous puissiez exécuter à nouveau le code du Listing 16.15 : SQL Server engarde en effet une trace. Pour pouvoir réexécuter ce code, vous devez supprimer oudétacher la base de données en utilisant la technique exposée dans la section suivante.

DeleteDatabase()

La méthode DeleteDatabase de l’objet DataContext permet de supprimer "propre-ment" une base de données. Si vous essayez de supprimer une base de données inexis-tante, une exception est levée. Il est donc prudent de n’utiliser DeleteDatabasequ’après avoir testé l’existence de la base de données avec la méthode DatabaseExists.

ATTENTIONATTENTION

Contrairement à la plupart des modifications effectuées dans une base de données via leDataContext, la méthode DeleteDatabase s’exécute immédiatement. Il n’est pas néces-saire d’appeler la méthode SubmitChanges, et l’exécution n’est pas différée.

PrototypeNous étudierons un seul prototype de la méthode CreateDatabase :

void DeleteDatabase()

Cette méthode n’admet aucun argument et ne retourne aucune valeur.

ExempleLe Listing 16.16 supprime la base de données créée dans le Listing 16.15.

Listing 16.16 : Un exemple de la méthode DeleteDatabase.

Northwind db = new Northwind(@"C:\Northwnd.mdf");db.DeleteDatabase();

Tant que la base de données spécifiée existe, ce code ne provoque aucune sortie dans laconsole. Cependant, si vous visualisez le contenu de la racine du disque C:, vous verrezque les fichiers Northwnd.mdf et Northwnd.ldf ont disparu.

Exception non gérée : System.Data.SqlClient.SqlException : La base de données➥’C:\Northwnd.mdf’ existe. Choisissez un autre nom de base de données....

Linq.book Page 541 Mercredi, 18. février 2009 7:58 07

Page 557: LINQ Language Integrated Query en C

542 LINQ to SQL Partie V

Si vous exécutez l’instruction DeleteDatabase sur une base de données inexistante,l’exception suivante est levée :

CreateMethodCallQuery()

Avant de décrire cette méthode, sachez qu’il s’agit d’une méthode protégée. Cela signi-fie qu’elle ne peut pas être appelée depuis le code d’une application. Pour pouvoirl’appeler, vous devez dériver une classe de la classe DataContext.

La méthode CreateMethodCallQuery permet d’appeler des fonctions table définies parl’utilisateur. La méthode ExecuteMethodCall est utilisée pour appeler des fonctionsscalaires définies par l’utilisateur. Vous en saurez plus à son sujet un peu plus loin dansce chapitre.

PrototypeNous étudierons un seul prototype de cette méthode dans cet ouvrage :

protected internal IQueryable<T> CreateMethodCallQuery<T>( object instance, System.Reflection.MethodInfo methodInfo, params object[] parameters)

Trois arguments sont passés à la méthode CreateMethodCallQuery :

m une référence au DataContext ou au [Your]DataContext de la méthode appelante ;

m l’objet MethodInfo de la méthode appelante ;

m un tableau params contenant les paramètres de la fonction table définie par l’utilisa-teur.

ExempleComme il a été dit précédemment, la méthode CreateMethodCallQuery est protégée etne peut être appelée qu’à partir de la classe DataContext ou d’une classe qui en est déri-vée. Plutôt que choisir un exemple qui appelle la méthode CreateMethodCallQuery,nous allons analyser la méthode générée par SQLMetal pour la fonction définie parl’utilisateur ProductsUnderThisUnitPrice de la base de données Northwind :

[Function(Name="dbo.ProductsUnderThisUnitPrice", IsComposable=true)]public IQueryable<ProductsUnderThisUnitPriceResult> ProductsUnderThisUnitPrice( [Parameter(DbType="Money")] System.Nullable<decimal> price){ return this.CreateMethodCallQuery<ProductsUnderThisUnitPriceResult>( this, ((MethodInfo)(MethodInfo.GetCurrentMethod())), price);}

Échec d’une tentative d’attachement d’une base de données nomméeautomatiquement pour le fichier Dataforum.mdf. Il existe une base de donnéesdu même nom ou le fichier spécifié ne peut être ouvert ou il se trouve sur unpartage UNC....

Linq.book Page 542 Mercredi, 18. février 2009 7:58 07

Page 558: LINQ Language Integrated Query en C

Chapitre 16 La classe DataContext 543

Dans ce code, vous pouvez voir que la méthode ProductsUnderThisUnitPrice estpassée en argument de l’attribut Function. Cette méthode appellera donc la procédurestockée ou la fonction définie par l’utilisateur ProductsUnderThisUnitPrice. Lapropriété IsComposable de l’attribut Function étant initialisée à true, il s’agit doncd’une fonction définie par l’utilisateur, et non d’une procédure stockée. Le code généréappelant la méthode CreateMethodCallQuery, nous savons que la fonction table définiepar l’utilisateur est ProductsUnderThisUnitPrice.

Examinons les paramètres passés à la méthode CreateMethodCallQuery. Le premierargument est une référence à la classe dérivée de DataContext générée par SQLMetal.Le deuxième argument est l’objet MethodInfo de la méthode actuelle. Cela permettra àla méthode CreateMethodCallQuery d’accéder aux attributs : elle aura connaissancedes informations nécessaires pour appeler la fonction définie par l’utilisateur. Le troi-sième argument est le seul paramètre accepté par la fonction définie par l’utilisateur.

La valeur retournée par la méthode CreateMethodCallQuery provient de la méthodeProductsUnderThisUnitPrice. Ici, il s’agit d’une séquence d’objets ProductsUnder-ThisUnitPrice (la classe ProductsUnderThisUnitPrice a été automatiquement géné-rée par SQLMetal).

Ce code vous a montré comment appeler la méthode CreateMethodCallQuery. Voyonsmaintenant un exemple d’appel de la méthode générée ProductsUnderThisUnitPrice(voir Listing 16.17).

Listing 16.17 : Un exemple d’appel de la méthode ProductsUnderThisPrice.

Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind");

IQueryable<ProductsUnderThisUnitPriceResult> results = db.ProductsUnderThisUnitPrice(new Decimal(5.50));

foreach(ProductsUnderThisUnitPriceResult prod in results){ Console.WriteLine("{0} - {1:C}", prod.ProductName, prod.UnitPrice);}

Voici les résultats :

ExecuteQuery()

À un moment ou à un autre, vous avez certainement éprouvé le besoin de lancer unerequête SQL. Eh bien, sachez que LINQ to SQL autorise cette digression et que lesvaleurs retournées sont… des objets entité !

Il vous suffit pour cela de faire appel à la méthode ExecuteQuery. Spécifiez la requêteSQL dans une chaîne et, si nécessaire, ajoutez les paramètres à substituer dans la

Guaraná Fantástica - $4.50Geitost - $2.50

Linq.book Page 543 Mercredi, 18. février 2009 7:58 07

Page 559: LINQ Language Integrated Query en C

544 LINQ to SQL Partie V

chaîne, tout comme vous le feriez lors de l’appel de la méthode String.Format. Lesrésultats seront alors automatiquement convertis en une séquence d’objets entité.

C’est aussi simple que cela ! Mais qu’en est-il des erreurs d’injection SQL ? Pour lesgérer, ne suffit-il pas d’utiliser des paramètres ? La méthode ExecuteQuery gère toutcela pour vous !

PrototypeNous étudierons un seul prototype de cette méthode dans cet ouvrage :

IEnumerable<T> ExecuteQuery<T>(string query, params object[] parameters)

Le premier argument de cette méthode est obligatoire. Il représente la requête SQL àexécuter. Un ou plusieurs arguments peuvent alors être passés à la méthode via ledeuxième argument. La chaîne de la requête SQL et les paramètres optionnels secomportent comme la méthode String.Format. La méthode retourne une séquence detype T, où T est une classe d’entité.

Attention, si vous indiquez une valeur de colonne dans la clause where de la requêteSQL, elle doit être encadrée d’apostrophes (tout comme dans une requête SQL tradi-tionnelle) et non de guillemets ! En revanche, si vous passez une valeur de colonne dansun paramètre ({0}, par exemple), il n’est pas nécessaire de le délimiter en utilisant desapostrophes.

Pour qu’une colonne dans la requête puisse être transformée en objet entité, son nomdoit être identique à celui du champ mappé correspondant de l’objet entité. Bienentendu, ceci peut être réalisé en ajoutant "as <nom de colonne>" au nom de lacolonne (où <nom de colonne> est une colonne mappée dans l’objet entité).

Si tous les champs mappés ne sont pas nécessairement retournés par la requête, les clésprimaires le sont forcément. Par ailleurs, vous pouvez obtenir des champs de la requêtequi ne sont mappés à aucun champ de l’objet entité, mais ils ne seront pas propagésdans l’objet entité.

ExemplesPour illustrer la méthode ExecuteQuery, nous allons effectuer une requête sur la tableCustomer (voir Listing 16.18).

Listing 16.18 : Un exemple d’appel de la méthode ExecuteQuery.

Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind");

IEnumerable<Customer> custs = db.ExecuteQuery<Customer>( @"select CustomerID, CompanyName, ContactName, ContactTitle from Customers where Region = {0}", "WA");

foreach (Customer c in custs){ Console.WriteLine("ID = {0} : Nom = {1} : Contact = {2}",

Linq.book Page 544 Mercredi, 18. février 2009 7:58 07

Page 560: LINQ Language Integrated Query en C

Chapitre 16 La classe DataContext 545

c.CustomerID, c.CompanyName, c.ContactName);}

Cet exemple est très simple. La valeur "WA" étant spécifiée dans un paramètre (et nonincluse dans la requête), la fonctionnalité de substitution de paramètre sera utilisée.Cette valeur doit donc être entourée non pas d’apostrophes mais de guillemets. Voici lesrésultats :

Pour exécuter la même requête sans faire appel à la substitution de paramètre, il suffitd’inclure la valeur WA dans la requête en la délimitant d’apostrophes (voirListing 16.19).

Listing 16.19 : Un autre exemple de la méthode ExecuteQuery.

Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind");

IEnumerable<Customer> custs = db.ExecuteQuery<Customer>( @"select CustomerID, CompanyName, ContactName, ContactTitle from Customers where Region = ’WA’");

foreach (Customer c in custs){ Console.WriteLine("ID = {0} : Name = {1} : Contact = {2}", c.CustomerID, c.CompanyName, c.ContactName);}

La valeur affectée au champ Region apparaît en gras dans le listing. Les résultats sontidentiques à ceux du listing précédent :

Pour en terminer avec cette méthode, nous allons vous montrer comment ajouter unnom d’une colonne, si le nom spécifié n’est pas trouvé dans la base de données. Étantdonné qu’il est possible d’effectuer des jointures dans la chaîne de la requête, vouspourriez lancer une requête sur des colonnes en utilisant un nom différent issu d’uneautre table, tout en les reliant à des champs mappés dans la classe d’entité (voirListing 16.20).

Listing 16.20 : Appel de la méthode ExecuteQuery en spécifiant un nom de champ mappé.

Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind");

IEnumerable<Customer> custs = db.ExecuteQuery<Customer>( @"select CustomerID, Address + ’, ’ + City + ’, ’ + Region as Address from Customers where Region = ’WA’");

foreach (Customer c in custs)

ID = LAZYK : Nom = Lazy K Kountry Store : Contact = John SteelID = TRAIH : Nom = Trail’s Head Gourmet Provisioners : Contact = Helvetius NagyID = WHITC : Nom = White Clover Markets : Contact = Karl Jablonski

ID = LAZYK : Nom = Lazy K Kountry Store : Contact = John SteelID = TRAIH : Nom = Trail’s Head Gourmet Provisioners : Contact = Helvetius NagyID = WHITC : Nom = White Clover Markets : Contact = Karl Jablonski

Linq.book Page 545 Mercredi, 18. février 2009 7:58 07

Page 561: LINQ Language Integrated Query en C

546 LINQ to SQL Partie V

{ Console.WriteLine("Id = {0} : Addresse = {1}", c.CustomerID, c.Address);}

Dans cette requête, nous concaténons plusieurs colonnes de la base de donnéesNorthwind avec des chaînes littérales et spécifions un nom de champ mappé. Sont ainsiobtenues l’adresse, la ville et la région (dans le membre Address de l’objet entité). Danscet exemple, tous les champs viennent de la même table. Mais ils auraient tout aussibien pu provenir d’une jointure ou d’une autre table. Voici les résultats :

Si vous utilisez cette technique, ayez bien en tête que, si un objet entité est modifié etque la méthode SubmitChanges est appelée, il se peut que vous obteniez des donnéesquelque peu fantaisistes. Cependant, utilisée correctement, cette technique se révèletrès pratique.

Translate()

La méthode Translate est semblable à la méthode ExecuteQuery, car elle traduit le résul-tat d’une requête SQL en une séquence d’objets entité. En revanche, la requête SQL luiest passée non pas sous la forme d’une chaîne, mais sous la forme d’un objetSystem.Data.Common.DbDataReader (un SqlDataReader, par exemple). Cette méthodeest très utile quand il s’agit d’intégrer du code LINQ to SQL dans du code ADO.NETexistant.

PrototypeNous étudierons un seul prototype de cette méthode dans cet ouvrage :

IEnumerable<T> Translate<T>(System.Data.Common.DbDataReader reader)

Un objet de type System.Data.Common.DbDataReader est passé à la méthode et laséquence d’objets entité spécifiée est retournée.

ExemplesDans le Listing 16.21, nous allons créer et exécuter une requête en utilisant ADO.NET.Cette étape effectuée, nous utiliserons la méthode Translate pour transformer lesrésultats de la requête en une séquence d’objets entité Customer. Le Listing 16.21 étantassez complexe, nous donnerons des informations chaque fois que cela sera nécessaire.

Listing 16.21 : Un exemple de la méthode Translate.

System.Data.SqlClient.SqlConnection sqlConn = new System.Data.SqlClient.SqlConnection( @"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind;Integrated Security=SSPI;");

Id = LAZYK : Addresse = 12 Orchestra Terrace, Walla Walla, WAId = TRAIH : Addresse = 722 DaVinci Blvd., Kirkland, WAId = WHITC : Addresse = 305 - 14th Ave. S. Suite 3B, Seattle, WA

Linq.book Page 546 Mercredi, 18. février 2009 7:58 07

Page 562: LINQ Language Integrated Query en C

Chapitre 16 La classe DataContext 547

string cmd = @"select CustomerID, CompanyName, ContactName, ContactTitle from Customers where Region = ’WA’";

System.Data.SqlClient.SqlCommand sqlComm = new System.Data.SqlClient.SqlCommand(cmd);

sqlComm.Connection = sqlConn;try{ sqlConn.Open(); System.Data.SqlClient.SqlDataReader reader = sqlComm.ExecuteReader();

Nous allons supposer que tout le code listé jusqu’ici existe et qu’il s’agit d’un codehérité qui doit être mis à jour en utilisant LINQ. Comme vous pouvez le voir, aucuneréférence à LINQ n’est faite dans ce code : une connexion SqlConnection est établie,une requête est définie, une commande SqlCommand est créée, la connexion est ouverteet la requête, exécutée. Nous allons maintenant ajouter du code LINQ :

Northwind db = new Northwind(sqlConn);

IEnumerable<Customer> custs = db.Translate<Customer>(reader);

foreach (Customer c in custs){ Console.WriteLine("ID = {0} : Nom = {1} : Contact = {2}", c.CustomerID, c.CompanyName, c.ContactName);}

Dans ce bloc de code, le DataContext Northwind est instancié en utilisant la connexionADO.NET. La méthode Translate est alors appelée en lui passant le reader existant.Les résultats de la requête peuvent donc être convertis en une séquence d’objets entitéqui peuvent être énumérés et dont les résultats peuvent être affichés.

Étant donné qu’il s’agit d’un code hérité, il devrait y avoir quelques instructions addi-tionnelles qui exploitent les résultats. Mais, pour illustrer la méthode Translate, cecode additionnel n’a aucun intérêt. Le listing se termine par la fermeture de laconnexion :

}finally{ sqlComm.Connection.Close();}

Ce listing vous a montré à quel point il est simple pour LINQ to SQL de dialoguer avecADO.NET. Voici les résultats :

ExecuteCommand()

Tout comme la méthode ExecuteQuery, ExecuteCommand vous permet de spécifier ladéclaration SQL à exécuter. Vous pouvez utiliser cette méthode pour exécuter une déclara-tion insert, update ou delete, ou encore une procédure stockée. Une autre analogie

ID = LAZYK : Nom = Lazy K Kountry Store : Contact = John SteelID = TRAIH : Nom = Trail’s Head Gourmet Provisioners : Contact = Helvetius NagyID = WHITC : Nom = White Clover Markets : Contact = Karl Jablonski

Linq.book Page 547 Mercredi, 18. février 2009 7:58 07

Page 563: LINQ Language Integrated Query en C

548 LINQ to SQL Partie V

avec la méthode ExecuteQuery : vous pouvez passer un ou plusieurs paramètres à laméthode.

La méthode ExecuteCommand s’exécute immédiatement : aucun appel à la méthodeSubmitChanges n’est donc nécessaire.

PrototypeNous étudierons un seul prototype de cette méthode dans cet ouvrage :

int ExecuteCommand(string command, params object[] parameters)

Cette méthode admet une chaîne de commande et zéro, un ou plusieurs paramètresoptionnels. Elle retourne un entier qui indique le nombre de lignes affectées par larequête.

Attention, si vous indiquez une valeur de colonne dans la clause where de la requêteSQL, elle doit être encadrée d’apostrophes (tout comme dans une requête SQL tradi-tionnelle) et non de guillemets ! En revanche, si vous passez une valeur de colonne dansun paramètre ({0}, par exemple), il n’est pas nécessaire de le délimiter en utilisant desapostrophes.

ExemplesDans le Listing 16.22, nous allons insérer un enregistrement en utilisant la méthodeExecuteCommand. Nous utiliserons cette même méthode pour supprimer les modifica-tions effectuées dans la base de données, afin que l’exemple puisse être exécuté àplusieurs reprises.

Listing 16.22 : Utilisation de la méthode ExecuteCommand pour insérer et supprimer un enregistrement.

Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind");

Console.WriteLine("Insertion d’un client ...");int rowsAffected = db.ExecuteCommand( @"insert into Customers values ({0}, ’Lawn Wranglers’, ’Mr. Abe Henry’, ’Owner’, ’1017 Maple Leaf Way’, ’Ft. Worth’, ’TX’, ’76104’, ’USA’, ’(800) MOW-LAWN’, ’(800) MOW-LAWO’)", "LAWN");Console.WriteLine("Fin de l’insertion.{0}", System.Environment.NewLine);

Console.WriteLine("{0} ligne(s) a(ont) été affectée(s). Le client ajouté est-il dans ➥la base de données ?", rowsAffected);

Customer cust = (from c in db.Customers where c.CustomerID == "LAWN" select c).DefaultIfEmpty<Customer>().Single<Customer>();

Console.WriteLine("{0}{1}", cust != null ? "Oui, le client a été trouvé dans la base de données." : "Non, le client n’a pas ➥été trouvé dans la base de données.", System.Environment.NewLine);

Linq.book Page 548 Mercredi, 18. février 2009 7:58 07

Page 564: LINQ Language Integrated Query en C

Chapitre 16 La classe DataContext 549

Console.WriteLine("Suppression du client ajouté ...");rowsAffected = db.ExecuteCommand(@"delete from Customers where CustomerID = {0}", "LAWN");

Console.WriteLine("Fin de la suppression.{0}", System.Environment.NewLine);

Cet exemple est assez simple. Une chaîne SQL et plusieurs paramètres sont fournis à laméthode ExecuteCommand pour définir un nouvel enregistrement dans la table Custo-mers. Une requête LINQ to SQL s’assure que l’enregistrement a bien été inséré dans latable Customers et les résultats sont affichés dans la console. L’enregistrement ajoutédans la table Customers est enfin supprimé en invoquant une nouvelle fois la méthodeExecuteCommand.

Voici les résultats affichés dans la console :

ExecuteMethodCall()

Avant de décrire cette méthode, sachez qu’il s’agit d’une méthode protégée. Cela signi-fie qu’elle ne peut pas être appelée depuis le code d’une application. Pour pouvoirl’appeler, vous devez dériver une classe de la classe DataContext.

La méthode ExecuteMethodCall permet d’appeler des procédures stockées et des fonc-tions scalaires définies par l’utilisateur. Si vous souhaitez appeler des fonctions définiespar l’utilisateur de type table, vous devez utiliser la méthode CreateMethodCallQuery.Le cas échéant, reportez-vous à la section correspondante, quelques pages plus tôt dansce chapitre.

PrototypeNous étudierons un seul prototype de cette méthode dans cet ouvrage :

protected internal IExecuteResult ExecuteMethodCall( object instance, System.Reflection.MethodInfo methodInfo, params object[] parameters)

Plusieurs paramètres sont passés à la méthode ExecuteMethodCall :

m une référence à l’objet DataContext ou [Your]DataContext dont la méthode appe-lante est membre ;

m l’objet MethodInfo de la méthode appelante ;

m un tableau params contenant les paramètres de la procédure stockée ou de la fonc-tion scalaire définie par l’utilisateur.

Insertion d’un client …Fin de l’insertion.1 ligne(s) a(ont) été affectée(s). Le client ajouté est-il dans la base de données ?Oui, le client a été trouvé dans la base de données.Suppression du client ajouté …Fin de la suppression.

Linq.book Page 549 Mercredi, 18. février 2009 7:58 07

Page 565: LINQ Language Integrated Query en C

550 LINQ to SQL Partie V

Étant donné qu’un objet MethodInfo est passé à la méthode, cette dernière doit êtredotée de l’attribut procédure stockée/fonction définie par l’utilisateur correspondantainsi que des propriétés d’attribut correspondantes. LINQ to SQL utilise l’objet Metho-dInfo pour accéder à l’attribut Function de la méthode et ainsi obtenir le nom de laprocédure stockée/de la fonction scalaire définie par l’utilisateur. Il utilise égalementl’objet MethodInfo pour obtenir les noms et types des paramètres.

La méthode ExecuteMethodCall retourne un objet qui implémente l’interface IExecu-teResult. Si nécessaire, reportez-vous au Chapitre 15 pour en savoir plus sur cetteinterface.

Si vous utilisez SQLMetal pour générer vos classes d’entité, spécifiez l’option :

m /sprocs pour générer les méthodes de classe qui appellent la méthode Execute-MethodCall pour les procédures stockées de la base de données ;

m /function pour générer les méthodes de classe qui appellent la méthode Execute-MethodCall pour les fonctions définies par l’utilisateur de la base de données.

ExemplesAvant de nous intéresser au code du premier exemple, nous allons donner quelquesexplications sur la méthode nommée CustomersCountByRegion, générée par SQLMetalpour appeler la procédure stockée Customers Count By Region de la base de données.Voici le code de la méthode ExecuteMethodCall générée par SQLMetal :

Utilisation de la méthode ExecuteMethodCall pour appeler une procédure stockée

[Function(Name="dbo.Customers Count By Region")][return: Parameter(DbType="Int")]public int CustomersCountByRegion([Parameter(DbType="NVarChar(15)")] string param1){ IExecuteResult result = this.ExecuteMethodCall( this, ((MethodInfo)(MethodInfo.GetCurrentMethod())), param1); return ((int)(result.ReturnValue));}

Comme vous pouvez le voir, la méthode CustomersCountByRegion se voit passer unparamètre chaîne. Ce paramètre est lui-même passé comme paramètre de la méthodeExecuteMethodCall, qui est passé comme paramètre de la procédure stockée Custo-mers Count By Region.

La méthode ExecuteMethodCall retourne une variable qui implémente IExecuteRe-sult. Pour obtenir la valeur entière retournée, la méthode ExecuteMethodCall réfé-rence l’objet retourné ReturnValue et lui applique un casting de type int.

Maintenant, examinons le Listing 16.23, qui appelle la méthode générée Customers-CountByRegion.

Linq.book Page 550 Mercredi, 18. février 2009 7:58 07

Page 566: LINQ Language Integrated Query en C

Chapitre 16 La classe DataContext 551

Listing 16.23 : Un exemple d’appel de la méthode générée CustomersCountByRegion.

Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind");int rc = db.CustomersCountByRegion("WA");Console.WriteLine("Il y a {0} clients dans l’état WA.", rc);

Cet exemple est très simple. Voici les résultats :

Nous allons maintenant nous intéresser à l’appel d’une procédure stockée qui retourneun paramètre en sortie. Ici encore, nous allons observer la classe d’entité générée parSQLMetal pour la base de données Northwind, et en particulier la méthode CustOrder-Total, également générée par SQLMetal, qui se charge d’appeler la procédure stockéeCustOrderTotal.

Un exemple qui utilise la méthode ExecuteMethodCall pour appeler une procédurestockée qui retourne un paramètre en sortie

[Function(Name="dbo.CustOrderTotal")][return: Parameter(DbType="Int")]public int CustOrderTotal( [Parameter(Name="CustomerID", DbType="NChar(5)")] string customerID, [Parameter(Name="TotalSales", DbType="Money")] ref System.Nullable<decimal> totalSales){ IExecuteResult result = this.ExecuteMethodCall( this, ((MethodInfo)(MethodInfo.GetCurrentMethod())), customerID, totalSales);

totalSales = ((System.Nullable<decimal>)(result.GetParameterValue(1))); return ((int)(result.ReturnValue));}

Le deuxième paramètre de la méthode CustOrderTotal (TotalSales) utilise le mot-cléref. Cet indice nous laisse penser que cette valeur va être retournée par la procédurestockée. Après l’appel à la méthode ExecuteMethodCall, pour obtenir cette valeur nousappliquons la méthode GetParameterValue sur l’objet retourné, qui implémente IExe-cuteResult, et lui appliquons la valeur 1 pour accéder au deuxième paramètre. LeListing 16.24 appelle la méthode CustOrderTotal.

Listing 16.24 : Un exemple d’appel de la méthode générée CustOrderTotal.

Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind");decimal? totalSales = 0;int rc = db.CustOrderTotal("LAZYK", ref totalSales);Console.WriteLine("Ventes totales du client LAZYK : {0:C}", totalSales);

Il y a 3 clients dans l’état WA.

Linq.book Page 551 Mercredi, 18. février 2009 7:58 07

Page 567: LINQ Language Integrated Query en C

552 LINQ to SQL Partie V

Comme vous pouvez le voir, le mot-clé ref a été spécifié pour le deuxième paramètre,totalSales. Voici le résultat :

Le prochain exemple appelle une procédure stockée qui retourne ses résultats dans une"forme" unique. Nous allons raisonner sur la procédure stockée Customers By City dela base de données Northwind.

Examinons la méthode générée par SQLMetal qui appelle cette procédure stockée viala méthode ExecuteMethodCall.

Cet exemple utilise la méthode ExecuteMethodCall pour appeler une procédure stockéequi retourne une simple forme

[Function(Name="dbo.Customers By City")]

public ISingleResult<CustomersByCityResult>

CustomersByCity([Parameter(DbType="NVarChar(20)")] string param1)

{

IExecuteResult result =

this.ExecuteMethodCall(

this,

((MethodInfo)(MethodInfo.GetCurrentMethod())),

param1);

return ((ISingleResult<CustomersByCityResult>)(result.ReturnValue));

}

La méthode générée retourne un objet de type ISingleResult<CustomersByCityRe-sult>. La méthode générée récupère cet objet en effectuant un casting de ce type sur lapropriété ReturnValue de l’objet retourné. La classe CustomersByCityResult a égale-ment été générée par SQLMetal. Nous n’y reviendrons pas. Le Listing 16.25 représentele code qui appelle la méthode CustomersByCity.

Listing 16.25 : Un exemple d’appel de la méthode générée CustomersByCity.

Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind");

ISingleResult<CustomersByCityResult> results = db.CustomersByCity("London");

foreach (CustomersByCityResult cust in results)

{

Console.WriteLine("{0} - {1} - {2} - {3}", cust.CustomerID, cust.CompanyName,

cust.ContactName, cust.City);

}

Comme vous pouvez le voir, ce code effectue une énumération de l’objet retourné, detype ISingleResult<CustomersByCityResult>, tout comme s’il s’agissait d’une

Ventes totales du client LAZYK : $357.00

Linq.book Page 552 Mercredi, 18. février 2009 7:58 07

Page 568: LINQ Language Integrated Query en C

Chapitre 16 La classe DataContext 553

séquence LINQ. Cette énumération est possible parce que ce type dérive de IEnumera-ble<T> (voir Chapitre 15). Les résultats sont affichés dans la console :

Examinons maintenant quelques exemples qui retournent plusieurs formes de résultat.Si le mot "forme" ne vous est pas familier, sachez qu’il caractérise les types de donnéesretournés. Lorsqu’une requête retourne un numéro d’une commande et le nom duclient, ces deux informations constituent une forme. Lorsqu’une requête retourne unnuméro de commande, une date de commande et un code de pays, ces trois informa-tions constituent une autre forme. Si une requête retourne plusieurs ensembles d’infor-mations (comme les deux précédents), on dit qu’elle retourne plusieurs formes derésultats. Les procédures stockées étant en mesure de retourner plusieurs formes derésultats, LINQ to SQL doit être en mesure de gérer ce type d’informations.

Dans notre premier exemple de formes multiples, nous allons supposer que la forme durésultat est conditionnelle. La méthode Northwind étendue possède une procédure stoc-kée de ce type nommée Whole Or Partial Customers Set. SQLMetal a généré laméthode WholeOrPartialCustomerSet pour appeler cette procédure stockée. Voici soncode :

Un exemple qui exécute la méthode ExecuteMethodCall pour appeler une procédurestockée qui retourne conditionnellement plusieurs formes

[Function(Name="dbo.Whole Or Partial Customers Set")][ResultType(typeof(WholeOrPartialCustomersSetResult1))][ResultType(typeof(WholeOrPartialCustomersSetResult2))]public IMultipleResults WholeOrPartialCustomersSet( [Parameter(DbType="Int")] System.Nullable<int> param1){ IExecuteResult result = this.ExecuteMethodCall( this, ((MethodInfo)(MethodInfo.GetCurrentMethod())), param1);

return ((IMultipleResults)(result.ReturnValue));}

Comme vous pouvez le voir, ce code comprend deux attributs ResultType qui corres-pondent aux deux formes possibles du résultat. Les deux classes correspondantes ontété générées automatiquement par SQLMetal. Le développeur qui appelle la méthodeWholeOrPartialCustomersSet doit savoir que la procédure stockée retourne un résultatdont la forme dépend de la valeur de param1. Après avoir examiné la procédure stockée,j’ai pu en déduire que, lorsque param1 vaut 1, la procédure stockée retourne tous les

AROUT - Around the Horn - Thomas Hardy - LondonBSBEV - B’s Beverages - Victoria Ashworth - LondonCONSH - Consolidated Holdings - Elizabeth Brown - LondonEASTC - Eastern Connection - Ann Devon - LondonNORTS - North/South - Simon Crowther - LondonSEVES - Seven Seas Imports - Hari Kumar – London

Linq.book Page 553 Mercredi, 18. février 2009 7:58 07

Page 569: LINQ Language Integrated Query en C

554 LINQ to SQL Partie V

champs de la table Customers sous la forme d’une séquence d’objets de typeWholeOrPartialCustomersSetResult1. Si param1 vaut 2, une version réduite deschamps sera retournée sous la forme d’une séquence d’objets de typeWholeOrPartialCustomersSetResult2.

Dans ce code, remarquez également que le type de retour de la méthode WholeOrPar-tialCustomersSet est IMultipleResults. La méthode obtient ce type en effectuant uncasting en IMultipleResults de la propriété ReturnValue de l’objet retourné par laméthode ExecuteMethodCall. Cette interface a été étudiée en détail au Chapitre 15.

Le Listing 16.26 donne un exemple d’appel de la méthode WholeOrPartialCusto-mersSet.

Listing 16.26 : Un exemple d’appel de la méthode générée WholeOrPartialCustomersSet.

Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind");

IMultipleResults results = db.WholeOrPartialCustomersSet(1);

foreach (WholeOrPartialCustomersSetResult1 cust in results.GetResult<WholeOrPartialCustomersSetResult1>()){ Console.WriteLine("{0} - {1} - {2} - {3}", cust.CustomerID, cust.CompanyName, cust.ContactName, cust.City);}

Ce code montre clairement que les résultats sont de type IMultipleResults. La valeurpassée étant 1, le résultat est donc de type WholeOrPartialCustomersSetResult1.Remarquez également que, pour obtenir les résultats, la méthode GetResult<T> estappelée sur la variable IMultipleResults (où T est le type de la donnée retournée).Voici les résultats :

Cette procédure stockée obtient les clients dont le champ région a pour valeur "WA". Sinous avions passé la valeur 2 à la méthode WholeOrPartialCustomersSet, la séquenceobtenue aurait été de type WholeOrPartialCustomersSetResult2. Dans le code précé-dent, chaque occurrence du type WholeOrPartialCustomersSetResult1 aurait dû êtreremplacée par un type WholeOrPartialCustomersSetResult2.

Nous allons maintenant nous intéresser à une procédure qui retourne simultanémentplusieurs formes dans un seul appel. Ici encore, nous allons nous servir d’une procédurestockée de la base de données étendue Northwind. Cette procédure a pour nom GetCustomer And Orders. Dans un premier temps, nous allons nous intéresser à laméthode générée par SQLMetal pour appeler cette procédure stockée.

LAZYK - Lazy K Kountry Store - John Steel - Walla WallaTRAIH - Trail’s Head Gourmet Provisioners - Helvetius Nagy - KirklandWHITC - White Clover Markets - Karl Jablonski – Seattle

Linq.book Page 554 Mercredi, 18. février 2009 7:58 07

Page 570: LINQ Language Integrated Query en C

Chapitre 16 La classe DataContext 555

Un exemple qui exécute la méthode ExecuteMethodCall pour appeler une procédurestockée qui retourne plusieurs formes

[Function(Name="dbo.Get Customer And Orders")][ResultType(typeof(GetCustomerAndOrdersResult1))][ResultType(typeof(GetCustomerAndOrdersResult2))]public IMultipleResults GetCustomerAndOrders( [Parameter(Name="CustomerID", DbType="NChar(5)")] string customerID){ IExecuteResult result = this.ExecuteMethodCall( this, ((MethodInfo)(MethodInfo.GetCurrentMethod())), customerID);

return ((IMultipleResults)(result.ReturnValue));}

Comme vous pouvez le voir, la méthode retourne un objet de type IMultipleResults.Étant donné que la procédure renvoie simultanément plusieurs formes, nous devonsconnaître l’ordre de retour des différentes formes. Après avoir examiné la procédureGet Customer And Orders, j’ai pu en déduire qu’elle retournait l’enregistrement issude la table Customers en premier, puis les enregistrements correspondants de la tableOrders.

Le Listing 16.27 appelle la méthode générée à partir du code précédent.

Listing 16.27 : Un exemple d’appel de la méthode générée GetCustomerAndOrders.

Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind");

IMultipleResults results = db.GetCustomerAndOrders("LAZYK");

GetCustomerAndOrdersResult1 cust = results.GetResult<GetCustomerAndOrdersResult1>().Single();

Console.WriteLine("Commandes de {0} :", cust.CompanyName);

foreach (GetCustomerAndOrdersResult2 order in results.GetResult<GetCustomerAndOrdersResult2>()){ Console.WriteLine("{0} - {1}", order.OrderID, order.OrderDate);}

Sachant que la procédure stockée ne retournera qu’un seul enregistrement de typeGetCustomerAndOrdersResult1, l’opérateur Single est appelé sur la séquence dece type, à condition que le client correspondant au CustomerID spécifié existe. Sicette dernière condition n’était pas toujours vérifiée, nous aurions pu appelerl’opérateur SingleOrDefault à la place de l’opérateur Single. Nous savons égale-ment que, après l’objet GetCustomerAndOrdersResult1, zéro, un ou plusieurs objets

Linq.book Page 555 Mercredi, 18. février 2009 7:58 07

Page 571: LINQ Language Integrated Query en C

556 LINQ to SQL Partie V

GetCustomerAndOrdersResult2 seront retournés. Il a donc suffi d’énumérer ces objetset d’afficher les données souhaitées. Voici les résultats :

Nous en avons fini avec les exemples concernant la méthode ExecuteMethodCall appli-quée aux procédures stockées. Au début de cette section, nous avons indiqué que cetteméthode pouvait également être utilisée pour appeler des méthodes scalaires définiespar l’utilisateur. Nous allons donner un exemple d’un tel appel.

Mais, avant tout, commençons par donner le code de la méthode ExecuteMethodCallgénéré par SQLMetal pour appeler une fonction scalaire définie par l’utilisateur :

Un exemple qui exécute la méthode ExecuteMethodCall pour appeler une fonctionscalaire définie par l’utilisateur

[Function(Name="dbo.MinUnitPriceByCategory", IsComposable=true)][return: Parameter(DbType="Money")]public System.Nullable<decimal> MinUnitPriceByCategory( [Parameter(DbType="Int")] System.Nullable<int> categoryID){ return ((System.Nullable<decimal>)(this.ExecuteMethodCall(this, ((MethodInfo)(MethodInfo.GetCurrentMethod())), categoryID).ReturnValue));}

La valeur scalaire retournée par la fonction définie par l’utilisateur est obtenue en réfé-rençant la propriété ReturnValue de l’objet retourné par la méthode ExecuteMethod-Call.

Nous pourrions nous contenter de créer un exemple qui appelle la méthode généréeMinUnitPriceByCategory, mais nous allons aller un peu plus loin. En effet, les fonc-tions définies par l’utilisateur peuvent être utilisées comme s’il s’agissait de fonctionsSQL.

Le Listing 16.28 insère la méthode MinUnitPriceByCategory dans une requête pouridentifier les produits les moins chers de leur catégorie.

Listing 16.28 : Un exemple d’insertion d’une fonction définie par l’utilisateur dans une requête.

Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind");

IQueryable<Product> products = from p in db.Products where p.UnitPrice == db.MinUnitPriceByCategory(p.CategoryID) select p;

foreach (Product p in products){ Console.WriteLine("{0} - {1:C}", p.ProductName, p.UnitPrice);}

Commandes de Lazy K Kountry Store :10482 - 3/21/1997 12:00:00 AM10545 - 5/22/1997 12:00:00 AM

Linq.book Page 556 Mercredi, 18. février 2009 7:58 07

Page 572: LINQ Language Integrated Query en C

Chapitre 16 La classe DataContext 557

Dans cet exemple, l’appel à la méthode MinUnitPriceByCategory (qui provoquel’appel à la fonction scalaire définie par l’utilisateur de même nom) est inséré dans laclause where. Voici les résultats :

GetCommand()

La méthode GetCommand est potentiellement très utile. Lorsqu’elle est appelée surl’objet DataContext et qu’un objet LINQ to SQL IQueryable lui est passé, un objet detype System.Data.Common.DbCommand est retourné. Ce dernier donne accès à plusieurscomposants clés qui peuvent être utilisés sur la requête passée.

Par l’intermédiaire d’un objet DbCommand instancié avec la méthode GetCommand, vousobtenez une référence sur les objets CommandText, CommandTimeout, Connection, Para-meters et Transaction, ainsi que d’autres objets relatifs à la requête passée. Par leurintermédiaire, vous pouvez non seulement examiner ces objets, mais aussi modifierleurs valeurs par défaut sans modifier les mêmes valeurs dans toutes les requêtes quiseront exécutées avec l’instance courante du DataContext. À titre d’exemple, pour unerequête spécifique, vous pourriez vouloir incrémenter la valeur CommandTimeout sansque les autres requêtes exécutées avec le même objet DataContext n’utilisent la valeurCommandTimeout modifiée.

PrototypeUn seul prototype de cette méthode sera étudié dans cet ouvrage :

System.Data.Common.DbCommand GetCommand(IQueryable query)

Une requête LINQ to SQL est passée à cette méthode, sous la forme d’un IQueryable.L’objet retourné est un System.Data.Common.DbCommand pour la requête LINQ passéeen argument.

ExemplesDans le Listing 16.29, un objet DbCommand est défini pour modifier le champ Command-Timeout d’une requête et pour afficher le champ CommandText, c’est-à-dire la requêteSQL elle-même.

Aniseed Syrup - $10.00Konbu - $6.00Teatime Chocolate Biscuits - $9.20Guaraná Fantástica - $4.50Geitost - $2.50Filo Mix - $7.00Tourtière - $7.45Longlife Tofu - $10.00

Linq.book Page 557 Mercredi, 18. février 2009 7:58 07

Page 573: LINQ Language Integrated Query en C

558 LINQ to SQL Partie V

Listing 16.29 : Un exemple de la méthode GetCommand.

Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind");

IQueryable<Customer> custs = from c in db.Customers where c.Region == "WA" select c;

System.Data.Common.DbCommand dbc = db.GetCommand(custs);

Console.WriteLine("Timeout de la requête : {0}{1}", dbc.CommandTimeout, System.Environment.NewLine);

dbc.CommandTimeout = 1;

Console.WriteLine("Requête SQL : {0}{1}", dbc.CommandText, System.Environment.NewLine);

Console.WriteLine("Timeout de la requête : {0}{1}", dbc.CommandTimeout, System.Environment.NewLine);

foreach (Customer c in custs){ Console.WriteLine("{0}", c.CompanyName);}

Cet exemple est assez simple à comprendre. Après avoir défini une requête, elle estpassée à la méthode GetCommand. La valeur CommandTimeout de l’objet DbCommandretourné est alors affichée. Cette valeur est initialisée à 1, puis la requête SQL et lanouvelle valeur CommandTimeout sont affichées. Enfin, les résultats renvoyés par larequête sont énumérés et affichés.

Voici les résultats du code sur ma machine :

Si l’exécution de cette requête est trop longue sur votre machine, il y aura timeout et lesrésultats seront différents.

GetChangeSet()

Il peut parfois être utile d’obtenir la liste des objets entité qui seront insérés, modifiésou supprimés par la méthode SubmitChanges. Vous utiliserez pour cela la méthodeGetChangeSet.

Timeout de la requête : 30

Requête SQL : SELECT [t0].[CustomerID], [t0].[CompanyName], [t0].[ContactName],[t0].[ContactTitle], [t0].[Address], [t0].[City], [t0].[Region], [t0].[PostalCode],[t0].[Country], [t0].[Phone], [t0].[Fax]FROM [dbo].[Customers] AS [t0]WHERE [t0].[Region] = @p0

Timeout de la requête : 1

Lazy K Kountry StoreTrail’s Head Gourmet ProvisionersWhite Clover Markets

Linq.book Page 558 Mercredi, 18. février 2009 7:58 07

Page 574: LINQ Language Integrated Query en C

Chapitre 16 La classe DataContext 559

PrototypeUn seul prototype de cette méthode sera étudié dans cet ouvrage :

ChangeSet GetChangeSet()

Cette méthode n’admet aucun argument. Elle retourne un objet ChangeSet qui contientdes collections de type IList<T> (où T est une classe d’entité). Les propriétés de collec-tion Inserts, Updates et Deletes représentent respectivement les objets entité insérés,modifiés et supprimés.

Il suffit d’énumérer ces collections pour examiner les objets entité correspondants.

ExemplesDans le Listing 16.30, nous allons modifier, insérer et supprimer des objets entité.L’objet ChangeSet sera alors récupéré avec la méthode GetChangeSet et ses diversescollections énumérées.

Listing 16.30 : Un exemple d’utilisation de la méthode GetChangeSet.

Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind");

Customer cust = (from c in db.Customers where c.CustomerID == "LAZYK" select c).Single<Customer>();cust.Region = "Washington";

db.Customers.InsertOnSubmit( new Customer { CustomerID = "LAWN", CompanyName = "Lawn Wranglers", ContactName = "Mr. Abe Henry", ContactTitle = "Owner", Address = "1017 Maple Leaf Way", City = "Ft. Worth", Region = "TX", PostalCode = "76104", Country = "USA", Phone = "(800) MOW-LAWN", Fax = "(800) MOW-LAWO" });

Customer cust2 = (from c in db.Customers where c.CustomerID == "LONEP" select c).Single<Customer>();db.Customers.DeleteOnSubmit(cust2);cust2 = null;

ChangeSet changeSet = db.GetChangeSet();

Console.WriteLine("{0} Entités ajoutées :", System.Environment.NewLine);foreach (Customer c in changeSet.Inserts){ Console.WriteLine("Le client {0} sera ajouté.", c.CompanyName);}

Linq.book Page 559 Mercredi, 18. février 2009 7:58 07

Page 575: LINQ Language Integrated Query en C

560 LINQ to SQL Partie V

Console.WriteLine("{0} Entités modifiées :", System.Environment.NewLine);foreach (Customer c in changeSet.Updates){ Console.WriteLine("Le client {0} sera modifié.", c.CompanyName);}

Console.WriteLine("{0} Entités supprimées :", System.Environment.NewLine);foreach (Customer c in changeSet.Deletes){ Console.WriteLine("Le client {0} sera supprimé.", c.CompanyName);}

Le premier bloc d’instructions modifie le champ Region du client LAZYK, le deuxièmeinsère le client LAWN et le troisième supprime le client LONEP. L’objet ChangeSetchangeset est alors obtenu en appelant la méthode GetChangeSet. Les trois derniersblocs d’instructions énumèrent les collections Inserts, Updates et Deletes et affichentleur contenu.

Voici les résultats :

Dans cet exemple, les énumérations des collections s’appuient sur le fait que chaqueélément est un objet Customer. Dans de nombreux cas, les objets placés dans les collec-tions peuvent être de plusieurs types et il n’est pas possible de faire des suppositions apriori sur leur type. Le cas échéant, vous devrez écrire le code d’énumération afin detenir compte des différents types possibles. Dans cette tâche, l’opérateur OfType voussera d’un grand intérêt.

GetTable()

Vous utiliserez la méthode GetTable pour obtenir la référence d’une séquence Tabled’un DataContext correspondant à une table mappée. Cette méthode est généralementutilisée lorsque le code s’appuie sur la classe DataContext (et non [Your]Data-Context). L’utilisation de la classe [Your]DataContext est bien plus aisée, puisqu’elledispose de la propriété Table qui fait référence à chacune des tables mappées.

PrototypesNous étudierons deux prototypes de la méthode GetTable dans cet ouvrage.

Premier prototype

Table<T> GetTable<T>()

Ce prototype se voit passer une entité mappée de type T. Il retourne une séquence Tablede type T.

Entités ajoutées :Le client Lawn Wranglers sera ajouté.

Entités modifiées :Le clientLazy K Kountry Store sera modifié.

Entités supprimées :Le client Lonesome Pine Restaurant sera supprimé.

Linq.book Page 560 Mercredi, 18. février 2009 7:58 07

Page 576: LINQ Language Integrated Query en C

Chapitre 16 La classe DataContext 561

Second prototype

ITable GetTable(Type type)

Cette méthode se voit passer un objet entité Type. Elle retourne l’interface de la table.Cette interface peut alors être utilisée selon vos besoins. Si vous l’utilisez en tant quetable, n’oubliez pas de lui appliquer l’opérateur de casting IQueryable<T>.

ExemplesLe Listing 16.31 illustre le premier prototype. Dans ce listing, la classe DataContext (etnon [Your]DataContext) est utilisée pour récupérer un certain client dans la tableCustomer de la base de données Northwind.

Listing 16.31 : Un exemple du premier prototype de la méthode GetTable.

DataContext db = new DataContext(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind");

Customer cust = (from c in db.GetTable<Customer>() where c.CustomerID == "LAZYK" select c).Single<Customer>();

Console.WriteLine("Le client {0} a été récupéré.", cust.CompanyName);

Dans le deuxième bloc d’instructions, la méthode GetTable est appelée pour obtenirune référence à la table Customer et pour extraire le client dont le champ CustomerIDvaut "LAZYK". La dernière ligne affiche le nom de la société de ce client. Voici lerésultat :

Le Listing 16.32 illustre le second prototype. Tout comme dans le premier listing, laclasse DataContext (et non [Your]DataContext) est utilisée pour récupérer un certainclient dans la table Customer de la base de données Northwind.

Listing 16.32 : Un exemple du second prototype de la méthode GetTable.

DataContext db = new DataContext(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind");

Customer cust = (from c in ((IQueryable<Customer>)db.GetTable(typeof(Customer))) where c.CustomerID == "LAZYK" select c).Single<Customer>();

Console.WriteLine("Le client {0} a été récupéré.", cust.CompanyName);

Le résultat de ce listing est le même que celui du listing précédent :

Le client Lazy K Kountry Store a été récupéré.

Le client Lazy K Kountry Store a été récupéré.

Linq.book Page 561 Mercredi, 18. février 2009 7:58 07

Page 577: LINQ Language Integrated Query en C

562 LINQ to SQL Partie V

Refresh()

La méthode Refresh vous permet de rafraîchir manuellement les objets entité de la basede données. Dans certaines situations, cette action est effectuée lorsque la méthodeResolveAll de la collection ChangeConflicts de l’objet DataContext est appelée pourrésoudre un conflit d’accès concurrentiel, pendant l’appel à la méthode SubmitChanges.Dans d’autres cas, la méthode SubmitChanges n’est jamais appelée, mais il est cepen-dant nécessaire de mettre à jour la base de données.

Ce cas peut se produire lorsqu’une application affiche des données à lecture seuleprovenant d’une entité, du système ou d’un processus : la méthode Refresh permetd’afficher régulièrement les données rafraîchies lues dans la base de données.

La méthode Refresh permet de rafraîchir un objet entité unique ou une séquenced’objets entité provenant des résultats d’une requête LINQ to SQL.

PrototypesTrois prototypes de la méthode Refresh seront étudiés dans cet ouvrage.

Premier prototype

void Refresh(RefreshMode mode, object entity)

Ce prototype admet deux arguments (le mode de rafraîchissement et un objet entitéunique) et ne retourne aucune valeur.

Deuxième prototype

void Refresh(RefreshMode mode, params object[] entities)

Ce prototype admet deux arguments (le mode de rafraîchissement et le tableau params,qui contient plusieurs objets entité) et ne retourne aucune valeur.

Troisième prototype

void Refresh(RefreshMode mode, System.Collections.IEnumerable entities)

Ce prototype admet deux arguments (le mode de rafraîchissement et une séquenced’objets entité) et ne retourne aucune valeur.

Le paramètre RefreshMode peut prendre pour valeur KeepChanges, KeepCurrentValuesou OverwriteCurrentValues. Le Tableau 16.1 donne les définitions de ces trois valeurspar Visual Studio.

Tableau 16.1 : L’énumération RefreshMode

Nom du membre Description

KeepChanges Force la méthode Refresh à conserver la valeur actuelle qui a été modifiée, mais met à jour les autres valeurs avec les valeurs de base de données.

Linq.book Page 562 Mercredi, 18. février 2009 7:58 07

Page 578: LINQ Language Integrated Query en C

Chapitre 16 La classe DataContext 563

Le comportement de ces trois valeurs est étudié en détail au Chapitre 17.

ExemplesDans le Listing 16.33, nous allons appliquer une requête à un client en utilisant LINQto SQL, puis afficher ses entités ContactTitle et ContactName. Le nom du contact dece client sera alors modifié dans la base de données en utilisant ADO.NET, puis l’entitéContactTitle sera modifiée. Pour s’assurer que l’objet entité ContactName n’a pas étémodifié et ne reflète pas le contenu de la base de données, nous afficherons à nouveaules entités ContactTitle et ContactName.

La méthode Refresh sera alors appelée avec un paramètre RefreshMode initialisé àKeepChanges, puis les entités ContactTitle et ContactName seront à nouveau affichées.Vous verrez ainsi que le champ ContactName de la base de données a été reporté dansl’entité correspondante et que l’entité ContactTitle reste inchangée.

Le champ ContactName sera alors restauré à sa valeur initiale dans la base de donnéesafin que l’exemple puisse être exécuté plusieurs fois.

Listing 16.33 : Un exemple du premier prototype de la méthode Refresh.

Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind");

Customer cust = (from c in db.Customers where c.CustomerID == "GREAL" select c).Single<Customer>();

Console.WriteLine("Le nom original du client est {0}, ContactTitle a pour valeur ➥{1}.{2}", cust.ContactName, cust.ContactTitle, System.Environment.NewLine);

ExecuteStatementInDb(String.Format( @"update Customers set ContactName = ’Brad Radaker’ where CustomerID = ’GREAL’"));

cust.ContactTitle = "Chief Technology Officer";

Console.WriteLine("Le nom du client avant le rafraîchissement est {0}, ContactTitle ➥a pour valeur {1}.{2}", cust.ContactName, cust.ContactTitle, System.Environment.NewLine);

db.Refresh(RefreshMode.KeepChanges, cust);

KeepCurrentValues Force la méthode Refresh à permuter les valeurs d’origine avec les valeurs récupérées de la base de données. Aucune valeur actuelle n’est modifiée.

OverwriteCurrentValues Force la méthode Refresh à substituer toutes les valeurs actuelles par les valeurs de la base de données.

Tableau 16.1 : L’énumération RefreshMode (suite)

Nom du membre Description

Linq.book Page 563 Mercredi, 18. février 2009 7:58 07

Page 579: LINQ Language Integrated Query en C

564 LINQ to SQL Partie V

Console.WriteLine("Le nom du client après le rafraîchissement est {0}, ContactTitle ➥a pour valeur {1}.{2}",

cust.ContactName, cust.ContactTitle, System.Environment.NewLine);

// Les valeurs modifiées doivent être restaurées de telle sorte que le code

// puisse être exécuté plusieurs fois

Console.WriteLine("{0}Réinitialisation des valeurs originales",

System.Environment.NewLine);

ExecuteStatementInDb(String.Format(

@"update Customers set ContactName = ’John Steel’ where CustomerID = ’GREAL’"));

Une requête LINQ to SQL est exécutée pour récupérer la référence à l’objetclient GREAL. Les entités ContactName et ContactTitle de cet objet sont alorsaffichées.

Le champ ContactName de ce client est alors modifié dans la base de données en utili-sant ADO.NET, et l’entité ContactTitle de ce client est modifiée. Arrivé à ce pointdans le code, l’objet entité Customer ne sait pas que le champ ContactName a été modi-fié dans la base de données. Ceci est mis en évidence par l’affichage des entités objetContactName et ContactTitle.

La méthode Refresh est alors appelée en lui passant un paramètre RefreshMode initia-lisé à KeepChanges. Cette méthode devrait provoquer la mise à jour des entités objetsqui n’ont pas été modifiées par les champs modifiés dans la base de données. Ici, lechamp ContactName ayant été modifié dans la base de données, il devrait mettre à jourl’entité correspondante.

Les entités ContactName et ContactTitle sont alors affichées. Elles devraient refléter lavaleur du champ ContactName modifié dans la base de données et la valeur du champContactTitle modifié dans l’entité.

Les dernières instructions restaurent les valeurs originales dans la base de données.Ainsi, cet exemple pourra être exécuté plusieurs fois et les suivants ne seront pasaffectés.

Voici les résultats :

Le nom original du client est John Steel, ContactTitle a pour valeur Marketing ➥Manager.Exécution de la déclaration SQL sur la base de données avec ADO.NET ...Base de données mise à jour.Le nom du client avant le rafraîchissement est John Steel, ContactTitle a pour valeur ➥Chief Technology Officer.Le nom du client avant le rafraîchissement est Brad Radaker, ContactTitle a pour ➥valeur Chief Technology Officer.Réinitialisation des valeurs originales.Exécution de la déclaration SQL sur la base de données avec ADO.NET ...Base de données mise à jour.

Linq.book Page 564 Mercredi, 18. février 2009 7:58 07

Page 580: LINQ Language Integrated Query en C

Chapitre 16 La classe DataContext 565

Comme vous pouvez le voir, l’objet entité n’est pas au courant de la modification effec-tuée dans la base de données avant que la méthode Refresh ne soit appelée.

Nous allons maintenant illustrer le deuxième prototype de la méthode Refresh. Dans leListing 16.34, nous allons utiliser LINQ to SQL pour récupérer le client dont la régionest "WA". La séquence d’objets Customer retournée sera énumérée et les entités Custo-merId, Region et Country seront affichées. Ensuite, en utilisant ADO.NET, nousmettrons à jour le champ Country de chacun des clients dont la région est WA. Lorsquenous sommes arrivés à ce point dans l’exécution du code, le champ Country de cesclients a une valeur différente dans la base de données et dans les objets entité récupérés.Pour mettre en évidence cette différence, les objets entité seront énumérés.

L’opérateur ToArray sera alors invoqué sur la séquence d’objets Customer afin d’obte-nir un tableau composé d’objets Customer, puis la méthode Refresh sera appelée eninitialisant à KeepChanges le paramètre RefreshMode et en passant les premier,deuxième et troisième éléments du tableau d’objets Customer.

La séquence d’objets entité Customer sera alors énumérée une dernière fois. Ceci nouspermettra de mettre en évidence que les entités CustomerID, Region et Country ont étérafraîchies à partir des informations stockées dans la base de données.

Bien entendu, nous restaurerons les données originales dans la base de données pourque l’exemple puisse être exécuté plusieurs fois.

Listing 16.34 : Un exemple du deuxième prototype de la méthode Refresh.

Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind");

IEnumerable<Customer> custs = (from c in db.Customers where c.Region == "WA" select c);

Console.WriteLine("Objets entité avant la modification ADO.NET et l’appel de ➥Refresh() :");foreach (Customer c in custs){ Console.WriteLine("La région du client {0} est {1}, le pays est {2}.", c.CustomerID, c.Region, c.Country);}

Console.WriteLine("{0} Affectation de la valeur United States au pays de ces clients ➥avec ADO.NET ...", System.Environment.NewLine);ExecuteStatementInDb(String.Format( @"update Customers set Country = ’United States’ where Region = ’WA’"));Console.WriteLine("Pays des clients mis à jour{0}", System.Environment.NewLine);

Console.WriteLine("Objets entité après la modification ADO.NET et avant l’appel de ➥Refresh() :");foreach (Customer c in custs){ Console.WriteLine("La région du client {0}’s est {1}, le pays est {2}.", c.CustomerID, c.Region, c.Country);}

Linq.book Page 565 Mercredi, 18. février 2009 7:58 07

Page 581: LINQ Language Integrated Query en C

566 LINQ to SQL Partie V

Customer[] custArray = custs.ToArray();Console.WriteLine("{0} Rafraîchissement du tableau params d’objets entité Customer ➥...", System.Environment.NewLine);db.Refresh(RefreshMode.KeepChanges, custArray[0], custArray[1], custArray[2]);Console.WriteLine("Le tableau a été rafraîchi.{0}", System.Environment.NewLine);

Console.WriteLine("Objets entité après la modification ADO.NET et l’appel de ➥Refresh() :");foreach (Customer c in custs){ Console.WriteLine("La région du client {0}’s est {1}, le pays est {2}.", c.CustomerID, c.Region, c.Country);}

// Les valeurs modifiées doivent être restaurées de telle sorte que le code// puisse être exécuté plusieurs foisConsole.WriteLine("{0}Resetting data to original values.", System.Environment.NewLine);ExecuteStatementInDb(String.Format( @"update Customers set Country = ’USA’ where Region = ’WA’"));

Le code précédent devient intéressant à partir de l’appel à l’opérateur ToArray. Aprèsavoir récupéré le tableau d’objets Customer, la méthode Refresh est appelée, en luipassant les valeurs custArray[0], custArray[1] et custArray[2].

Voici les résultats :

Objets entité avant la modification ADO.NET et l’appel de Refresh() :La région du client LAZYK est WA, le pays est USA.La région du client TRAIH est WA, le pays est USA.La région du client WHITC est WA, le pays est USA.

Affectation de la valeur United States au pays de ces clients avec ADO.NET...Exécution de la déclaration SQL sur la base de données avec ADO.NET ...Base de données mise à jour.Pays des clients mis à jour.

Objets entité après la modification ADO.NET et avant l’appel de Refresh() :La région du client LAZYK est WA, le pays est USA.La région du client TRAIH est WA, le pays est USA.La région du client WHITC est WA, le pays est USA.

Rafraîchissement du tableau params d’objets entité Customer ...Le tableau a été rafraîchi.

Objets entité après la modification ADO.NET et l’appel de Refresh() :La région du client LAZYK est WA, le pays est United States.La région du client TRAIH est WA, le pays est United States.La région du client WHITC est WA, le pays est United States.

Restauration des données originales.Exécution de la déclaration SQL sur la base de données avec ADO.NET ...Base de données mise à jour.

Linq.book Page 566 Mercredi, 18. février 2009 7:58 07

Page 582: LINQ Language Integrated Query en C

Chapitre 16 La classe DataContext 567

Comme vous pouvez le voir dans les résultats, les modifications apportées au champCountry de la base de données ne sont pas reportées dans les objets entité Customerjusqu’à ce que la méthode Refresh soit appelée.

Dans le Listing 16.34, chaque entité objet rafraîchie était du même type : Customer. Ledeuxième prototype de la méthode Refresh s’accommode fort bien de types d’entitésobjet différents. Dans le cas du Listing 16.34, il aurait été pratique de passer uneséquence d’objet entité à la méthode Refresh. Heureusement, le troisième prototype dela méthode va nous permettre de passer un tel objet.

Le Listing 16.35 illustre le troisième prototype de la méthode Refresh. Il utilise lemême code que le listing précédent, mais, au lieu de définir un tableau et de passer seséléments à la méthode Refresh, c’est la séquence d’objets Customer récupérée par larequête qui va être passée.

Listing 16.35 : Un exemple d’utilisation du troisième prototype de la méthode Refresh.

Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind");

IEnumerable<Customer> custs = (from c in db.Customers where c.Region == "WA" select c);

Console.WriteLine("Objets entité avant la modification ADO.NET et l’appel de ➥Refresh() :");foreach (Customer c in custs){ Console.WriteLine("La région du client {0} est {1}, le pays est {2}.", c.CustomerID, c.Region, c.Country);}

Console.WriteLine("{0} Affectation de la valeur United States au pays de ces clients ➥avec ADO.NET ...", System.Environment.NewLine);ExecuteStatementInDb(String.Format( @"update Customers set Country = ’United States’ where Region = ’WA’"));Console.WriteLine("Pays des clients mis à jour{0}", System.Environment.NewLine);

Console.WriteLine("Objets entité après la modification ADO.NET et avant l’appel de ➥Refresh() :");foreach (Customer c in custs){ Console.WriteLine("La région du client {0}’s est {1}, le pays est {2}.", c.CustomerID, c.Region, c.Country);}

Console.WriteLine("{0}Rafraîchissement de la séquence d’objets entité Customer ...", System.Environment.NewLine);db.Refresh(RefreshMode.KeepChanges, custs);Console.WriteLine("La séquence d’objets entité Customer a été rafraîchie.{0}", System.Environment.NewLine);

Console.WriteLine("Objets entité après la modification ADO.NET et l’appel de ➥Refresh() :");

Linq.book Page 567 Mercredi, 18. février 2009 7:58 07

Page 583: LINQ Language Integrated Query en C

568 LINQ to SQL Partie V

foreach (Customer c in custs){ Console.WriteLine("La région du client {0}’s est {1}, le pays est {2}.", c.CustomerID, c.Region, c.Country);}

// Les valeurs modifiées doivent être restaurées de telle sorte que le code// puisse être exécuté plusieurs foisConsole.WriteLine("{0}Resetting data to original values.", System.Environment.NewLine);ExecuteStatementInDb(String.Format( @"update Customers set Country = ’USA’ where Region = ’WA’"));

Le code du Listing 16.35 est le même que celui du Listing 16.34 mais, lors de l’appelde la méthode Refresh, la séquence custs est passée en argument. Voici les résultats :

Comme vous pouvez le voir dans les résultats, les modifications apportées au champCountry de la base de données ne sont pas reportées dans les objets entité Customerjusqu’à ce que la méthode Refresh soit appelée.

Résumé

Il a fallu attendre jusqu’à ce chapitre pour savoir exactement ce que la classe Data-Context pouvait faire pour vous. La compréhension de LINQ to SQL n’est pas immé-diate, car elle nécessite l’entendement de LINQ, des requêtes de bases de données et deSQL. C’est pour cela que LINQ to SQL est un sujet prolixe et que la plupart desconcepts inhérents à la classe DataContext vont de pair avec les classes d’entité. Il abien fallu qu’un de ces sujets soit traité en premier…

Objets entité avant la modification ADO.NET et l’appel de Refresh() :La région du client LAZYK est WA, le pays est USA.La région du client TRAIH est WA, le pays est USA.La région du client WHITC est WA, le pays est USA.

Affectation de la valeur United States au pays de ces clients avec ADO.NET...Exécution de la déclaration SQL sur la base de données avec ADO.NET ...Base de données mise à jour.Pays des clients mis à jour.

Objets entité après la modification ADO.NET et avant l’appel de Refresh() :La région du client LAZYK est WA, le pays est USA.La région du client TRAIH est WA, le pays est USA.La région du client WHITC est WA, le pays est USA.

Rafraîchissement de la séquence d’objets entité Customer ...La séquence d’objets entité Customer a été rafraîchie.

Objets entité après la modification ADO.NET et l’appel de Refresh() :La région du client LAZYK est WA, le pays est United States.La région du client TRAIH est WA, le pays est United States.La région du client WHITC est WA, le pays est United States.

Restauration des données originales.Exécution de la déclaration SQL sur la base de données avec ADO.NET ...Base de données mise à jour.

Linq.book Page 568 Mercredi, 18. février 2009 7:58 07

Page 584: LINQ Language Integrated Query en C

Chapitre 16 La classe DataContext 569

Ce chapitre regroupe un grand nombre d’informations, mais les sujets les plus impor-tants sont certainement liés aux services du DataContext : la recherche d’identité, larecherche de changements et le processus de changement. Bien entendu, ces servicesn’ont aucun intérêt si vous ne savez pas instancier un objet DataContext ou[Your]DataContext. Les constructeurs des classes DataContext et [Your]Datacontextsont donc également très importants.

Outre ces constructeurs, vous utiliserez très fréquemment la méthode SubmitChangesdu DataContext pour sauvegarder les modifications des classes d’entité dans la base dedonnées.

Enfin, il est important d’avoir à l’esprit que, lorsque vous utilisez la méthode Submit-Changes, un conflit d’accès concurrentiel peut se produire et provoquer une exception.Ces conflits ont été mentionnés à plusieurs reprises dans les chapitres relatifs à LINQ toSQL, mais ils n’ont jamais été étudiés en détail. Pour en savoir plus à leur sujet, consul-tez le chapitre suivant, qui leur est dédié.

Linq.book Page 569 Mercredi, 18. février 2009 7:58 07

Page 585: LINQ Language Integrated Query en C

Linq.book Page 570 Mercredi, 18. février 2009 7:58 07

Page 586: LINQ Language Integrated Query en C

17

Les conflits d’accès concurrentiels

Combien de fois avez-vous entendu parler des conflits d’accès concurrentiels et de leurrésolution dans cet ouvrage ? Dans la plupart des chapitres précédents relatifs à LINQto SQL, nous avons parlé de ces types de conflits, sans toutefois leur accorder toutel’attention qu’ils méritent. Ce chapitre va combler cette lacune.

Prérequis pour exécuter les exemples

Pour exécuter les exemples de ce chapitre, vous devez être en possession de la versionétendue de la base de données Northwind et des classes d’entité générées correspon-dantes. Si nécessaire, reportez-vous à la section intitulée "Prérequis pour exécuter lesexemples" du Chapitre 12 pour savoir comment procéder.

Méthodes communes

Plusieurs méthodes communes sont également nécessaires à la bonne exécution desexemples. Reportez-vous à la section intitulée "Méthodes communes" au Chapitre 12pour en savoir plus à ce sujet.

Utilisation de l’API LINQ to SQL

Pour exécuter les exemples de ce chapitre, vous pouvez être amené à ajouter des réfé-rences et des directives using à vos projets. Reportez-vous à la section intitulée "Utili-sation de l’API LINQ to SQL" au Chapitre 12 pour en savoir plus à ce sujet.

Conflits d’accès concurrentiels

Lorsqu’une connexion à une base de données tente de mettre à jour des données qui ontété modifiées par une autre connexion de base de données depuis leur lecture dans lapremière connexion, un conflit d’accès concurrentiel se produit. Imaginez un processus

Linq.book Page 571 Mercredi, 18. février 2009 7:58 07

Page 587: LINQ Language Integrated Query en C

572 LINQ to SQL Partie V

P1 qui lit des données, puis un processus P2 qui lit les mêmes données. Si le processusP2 met à jour les données avant le processus P1, un conflit d’accès concurrentiel seproduit lorsque le processus P1 tente de mettre à jour les données. De la même manière,si le processus P1 met à jour les données avant le processus P2, un conflit d’accèsconcurrentiel se produit lorsque le processus P2 tente de mettre à jour les données. Siplusieurs connexions peuvent accéder à une même base de données, tôt ou tard, unconflit d’accès concurrentiel se produira.

Lorsqu’un conflit se produit, l’application doit effectuer plusieurs actions pour le résou-dre. À titre d’exemple, un administrateur de site web peut se trouver sur une page quiaffiche et permet de mettre à jour les données relatives à un utilisateur. Si, après l’affi-chage des données par l’administrateur, l’utilisateur va sur une page qui lui permet demettre à jour ses données et effectue quelques modifications, un conflit se produira lors-que l’administrateur sauvegardera ses modifications dans la base de données. Si aucunconflit ne se produit, les modifications effectuées par l’utilisateur seront écrasées parcelles de l’administrateur. Une autre alternative consisterait à sauvegarder les modifica-tions de l’utilisateur et à ignorer celles de l’administrateur. Choisir le bon comporte-ment est un problème assez complexe mais, dans tous les cas, la première étape consisteà détecter le conflit et la seconde, à le régler.

Deux approches sont possibles : l’optimiste et la pessimiste. Nous allons les étudier endétail dans les sections suivantes.

Contrôle d’accès concurrentiel optimiste

Comme son nom l’indique, le contrôle d’accès concurrentiel optimiste suppose que,dans la plupart des cas, aucun conflit concurrentiel ne se produira. Par conséquent,aucun verrou n’est placé sur les données pendant la lecture dans la base de données. Siun conflit se produit lors de la mise à jour de la base de données, il est examiné à cemoment-là. La gestion optimiste est plus complexe que la gestion pessimiste, mais ellefonctionne mieux dans les applications modernes, auxquelles un grand nombre d’utili-sateurs accèdent souvent simultanément. Imaginez votre frustration si, lorsque vousvoulez visualiser la page d’un objet sur un site de ventes aux enchères, l’accès vous estrefusé parce qu’un autre internaute est déjà en train de la visualiser ! Ou encore si vousne pouvez pas enchérir sur un objet parce qu’un autre utilisateur est en train de le faire !

LINQ to SQL utilise l’approche optimiste du contrôle d’accès concurrentiel. Commevous allez le voir, la détection et la résolution de conflits d’accès concurrentiel sontgrandement simplifiées par LINQ to SQL. Si vous le souhaitez, vous pouvez mêmeutiliser une méthode de résolution automatique…

Détection de conflitComme il a été dit précédemment, la première étape va consister à détecter un conflit.LINQ to SQL propose deux approches. Si la propriété IsVersion est spécifiée dans une

Linq.book Page 572 Mercredi, 18. février 2009 7:58 07

Page 588: LINQ Language Integrated Query en C

Chapitre 17 Les conflits d’accès concurrentiels 573

propriété de classe d’entité et a pour valeur true, la valeur de cette propriété (et seule-ment de cette propriété) sera utilisée pour déterminer si un conflit concurrentiel s’estproduit.

Si aucune propriété de classe d’entité n’a une propriété IsVersion initialisée à true,LINQ to SQL vous permet de choisir les propriétés de classe d’entité qui seront prisesen compte pour détecter un conflit. Ceci par l’intermédiaire de la propriété Update-Check de l’attribut Column, spécifiée sur une propriété mappée d’une classe d’entité.UpdateCheck peut prendre l’une des trois valeurs suivantes : Never, Always ou When-Changed.

UpdateCheck

Si la propriété UpdateCheck d’une propriété de classe d’entité mappée est initialisée àUpdateCheck.Never, elle ne prendra pas part à la détection de conflit concurrentiel. Sila propriété UpdateCheck est initialisée à UpdateCheck.Always, elle prendra toujourspart à la détection de conflit, indépendamment du fait que la valeur a été/n’a pas étémodifiée depuis sa mise en cache par le DataContext. Si la propriété UpdateCheck estinitialisée à UpdateCheck.WhenChanged, elle prendra part à la détection de conflit si lavaleur a changé depuis sa mise en cache par le DataContext. Si l’attribut UpdateCheckn’est pas spécifié, sa valeur par défaut est UpdateCheck.Always.

En ayant une idée de l’implémentation de la détection de conflit concurrentiel, vouscomprendrez mieux comment elle fonctionne. Lorsque vous appelez la méthodeSubmitChanges, le processeur de changement génère les déclarations SQL nécessairespour sauvegarder les modifications apportées aux objets entité dans la base de données.Lorsqu’un enregistrement doit être mis à jour, plutôt que se contenter de spécifier la cléprimaire de l’enregistrement dans la clause where afin de le récupérer, il y ajoute lescolonnes qui peuvent être la source du conflit. Si la propriété UpdateCheck d’unepropriété d’une classe d’entité est initialisée à UpdateCheck.Always, la colonne mappéede cette propriété et sa valeur originale seront toujours spécifiées dans la clause where.Si la propriété UpdateCheck d’une propriété d’une classe d’entité est initialisée à Upda-teCheck.WhenChanged, la colonne mappée de cette propriété et sa valeur originaleseront spécifiées dans la clause where si la valeur actuelle de la propriété de l’objetentité est différente de sa valeur originale. Enfin, si la propriété UpdateCheck d’unepropriété d’une classe d’entité est initialisée à UpdateCheck.Never, la colonne mappéede cette propriété ne sera pas incluse dans la clause where.

À titre d’exemple, concernant l’objet entité Customer, supposons que la propriété Update-Check du champ :

m CompanyName ait pour valeur UpdateCheck.Always ;

m ContactName ait pour valeur UpdateCheck.WhenChanged ;

m ContactTitle ait pour valeur UpdateCheck.Never.

Linq.book Page 573 Mercredi, 18. février 2009 7:58 07

Page 589: LINQ Language Integrated Query en C

574 LINQ to SQL Partie V

Si chacune de ces propriétés a été modifiée dans l’objet entité d’un client, la déclarationSQL aura l’allure suivante :

Update CustomersSet CompanyName = ’Art Sanders Park’, ContactName = ’Samuel Arthur Sanders’, ContactTitle = ’President’Where CompanyName = ’Lonesome Pine Restaurant’ AND ContactName = ’Fran Wilson’ AND CustomerID = ’LONEP’

Dans cet exemple, les valeurs de colonnes incluses dans la clause where sont celles quiont été lues dans la base de données lorsque l’objet entité a été récupéré, lorsque laméthode SubmitChanges a été entièrement exécutée ou lorsque la méthode Refresh aété appelée.

La propriété UpdateCheck de la propriété CompanyName ayant pour valeur Update-Check.Always, CompanyName est incluse dans la clause where, que cette entité objetait changé ou non. Comme la propriété UpdateCheck de la propriété ContactName apour valeur UpdateCheck.WhenChanged et que la valeur de cette entité a changé,ContactName est incluse dans la clause where. Enfin, comme la propriété Update-Check de la propriété ContactTitle a pour valeur UpdateCheck.Never, Contact-Title n’est pas incluse dans la clause where, que la valeur de cette propriété aitchangé ou non.

Lorsque la déclaration SQL est exécutée, si une ou plusieurs des valeurs des propriétésde la classe d’entité spécifiées dans la clause where ne correspondent pas aux valeursstockées dans la base de données, l’enregistrement n’est pas trouvé. Il n’est donc pasmis à jour. C’est ainsi qu’un conflit concurrentiel est détecté et qu’une exception Chan-geConflictException est levée.

Ce que nous avons dit à propos de la détection des conflits est un peu vague, mais sonimplémentation n’est pas spécifiée par Microsoft et le code n’est pas aussi accessiblequ’il l’était dans les préversions de LINQ. Dans la version finale, après l’exécutiond’une déclaration update, une déclaration select générée contenant une comparaisondu @@ROWCOUNT retourné par la déclaration update serait exécutée, permettant ainsi auprocesseur de changement de savoir si aucun enregistrement n’a été mis à jour et,donc…, qu’un conflit a eu lieu.

Dans tous les cas, ne prenez pas ce raisonnement pour argent comptant, puisquel’implémentation du contrôle d’accès concurrentiel n’est pas spécifiée par Microsoft.Quoi qu’il en soit, vous aurez au moins pris connaissance d’une technique d’implémen-tation du contrôle d’accès concurrentiel. On ne sait jamais, vous pourriez un jour êtreamené à implémenter quelque chose de similaire dans le cadre d’un projet.

Pour avoir une idée précise de la déclaration update générée, examinez le Listing 17.1.

Linq.book Page 574 Mercredi, 18. février 2009 7:58 07

Page 590: LINQ Language Integrated Query en C

Chapitre 17 Les conflits d’accès concurrentiels 575

Listing 17.1 : Mise à jour de la base de données pour voir comment les conflits d’accès concurrentiels sont détectés.

Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind");

db.Log = Console.Out;

Customer cust = db.Customers.Where(c => c.CustomerID == "LONEP").SingleOrDefault();string name = cust.ContactName; // A restaurer par la suite

cust.ContactName = "Neo Anderson";

db.SubmitChanges();

// Restauration de la base de donnéescust.ContactName = name;db.SubmitChanges();

Il n’y a pas grand-chose à dire sur cette requête. La seule chose qui mérite votreattention est l’appel à l’opérateur SingleOrDefault en lieu et place du traditionnelopérateur Single. Ceci afin de ne pas provoquer de problème si un enregistrementn’est pas trouvé. Dans ce cas précis, nous savons que l’enregistrement recherché nesera pas trouvé. À vous de vous assurer que le code gère en toute sécurité ce genrede situation…

Ce qui est vraiment intéressant, c’est la déclaration update générée :

SELECT [t0].[CustomerID], [t0].[CompanyName], [t0].[ContactName],[t0].[ContactTitle], [t0].[Address], [t0].[City], [t0].[Region], [t0].[PostalCode],[t0].[Country], [t0].[Phone], [t0].[Fax]FROM [dbo].[Customers] AS [t0]WHERE [t0].[CustomerID] = @p0-- @p0: Input String (Size = 5; Prec = 0; Scale = 0) [LONEP]-- Context: SqlProvider(Sql2005) Model: AttributedMetaModel Build: 3.5.20706.1

UPDATE [dbo].[Customers]SET [ContactName] = @p11WHERE ([CustomerID] = @p0) AND ([CompanyName] = @p1) AND ([ContactName] = @p2) AND([ContactTitle] = @p3) AND ([Address] = @p4) AND ([City] = @p5) AND ([Region] = @p6)AND ([PostalCode] = @p7) AND ([Country] = @p8) AND ([Phone] = @p9) AND ([Fax] =@p10)-- @p0: Input StringFixedLength (Size = 5; Prec = 0; Scale = 0) [LONEP]-- @p1: Input String (Size = 24; Prec = 0; Scale = 0) [Lonesome Pine Restaurant]-- @p2: Input String (Size = 11; Prec = 0; Scale = 0) [Fran Wilson]-- @p3: Input String (Size = 13; Prec = 0; Scale = 0) [Sales Manager]-- @p4: Input String (Size = 18; Prec = 0; Scale = 0) [89 Chiaroscuro Rd.]-- @p5: Input String (Size = 8; Prec = 0; Scale = 0) [Portland]-- @p6: Input String (Size = 2; Prec = 0; Scale = 0) [OR]-- @p7: Input String (Size = 5; Prec = 0; Scale = 0) [97219]-- @p8: Input String (Size = 3; Prec = 0; Scale = 0) [USA]-- @p9: Input String (Size = 14; Prec = 0; Scale = 0) [(503) 555-9573]-- @p10: Input String (Size = 14; Prec = 0; Scale = 0) [(503) 555-9646]-- @p11: Input String (Size = 12; Prec = 0; Scale = 0) [Neo Anderson]-- Context: SqlProvider(Sql2005) Model: AttributedMetaModel Build: 3.5.20706.1

Linq.book Page 575 Mercredi, 18. février 2009 7:58 07

Page 591: LINQ Language Integrated Query en C

576 LINQ to SQL Partie V

Dans la première déclaration update, la clause where a spécifié que le champ ContactNamedevait être égal à "Fran Wilson", c’est-à-dire la valeur originale du champ. Si un processusavait modifié la valeur du champ depuis sa première lecture, aucun enregistrement n’auraitété sélectionné par la clause where, et aucun enregistrement n’aurait donc été mis à jour.

Étant donné qu’aucune des propriétés de la classe d’entité de Customer ne spécifie lapropriété UpdateCheck, tous les champs ont la valeur par défaut UpdateCheck.Always,et toutes les propriétés de la classe d’entité mappées sont donc incluses dans la clausewhere de la déclaration update.

SubmitChanges()

La détection des conflits d’accès concurrentiels se produit lorsque la méthode Submit-Changes est appelée. Lors de son appel, vous pouvez spécifier si le processus de sauve-garde des modifications dans la base de données doit s’arrêter au premier conflit ou s’ildoit se poursuivre jusqu’à la dernière donnée, en collectant les divers conflits. Cecomportement est contrôlé avec l’argument ConflictMode, passé à la méthode Submit-Changes. Cet argument peut prendre les valeurs ConflictMode.FailOnFirstConflict(fin du processus de sauvegarde au premier conflit) ou ConflictMode.ContinueOn-Conflict (tentative de sauvegarde de toutes les modifications, même si un conflit estdétecté). Si l’argument ConflictMode n’est pas spécifié, il prendra la valeur par défautConflictMode.FailOnFirstConflict.

Sans tenir compte de la valeur affectée à l’argument ConflictMode, si l’appel à laméthode SubmitChanges ne se trouve pas dans la portée d’une transaction, une transac-tion est créée pour toutes les tentatives de modifications faites pendant l’invocation dela méthode SubmitChanges. Si SubmitChanges se trouve dans la portée d’une transac-tion, le DataContext l’utilise. Si une exception est levée pendant l’appel à la méthodeSubmitChanges, la transaction est annulée et les modifications effectuées dans la basede données sont restaurées à leur état initial.

UPDATE [dbo].[Customers]SET [ContactName] = @p11WHERE ([CustomerID] = @p0) AND ([CompanyName] = @p1) AND ([ContactName] = @p2) AND([ContactTitle] = @p3) AND ([Address] = @p4) AND ([City] = @p5) AND ([Region] = @p6)AND ([PostalCode] = @p7) AND ([Country] = @p8) AND ([Phone] = @p9) AND ([Fax] =@p10)-- @p0: Input StringFixedLength (Size = 5; Prec = 0; Scale = 0) [LONEP]-- @p1: Input String (Size = 24; Prec = 0; Scale = 0) [Lonesome Pine Restaurant]-- @p2: Input String (Size = 12; Prec = 0; Scale = 0) [Neo Anderson]-- @p3: Input String (Size = 13; Prec = 0; Scale = 0) [Sales Manager]-- @p4: Input String (Size = 18; Prec = 0; Scale = 0) [89 Chiaroscuro Rd.]-- @p5: Input String (Size = 8; Prec = 0; Scale = 0) [Portland]-- @p6: Input String (Size = 2; Prec = 0; Scale = 0) [OR]-- @p7: Input String (Size = 5; Prec = 0; Scale = 0) [97219]-- @p8: Input String (Size = 3; Prec = 0; Scale = 0) [USA]-- @p9: Input String (Size = 14; Prec = 0; Scale = 0) [(503) 555-9573]-- @p10: Input String (Size = 14; Prec = 0; Scale = 0) [(503) 555-9646]-- @p11: Input String (Size = 11; Prec = 0; Scale = 0) [Fran Wilson]-- Context: SqlProvider(Sql2005) Model: AttributedMetaModel Build: 3.5.20706.1

Linq.book Page 576 Mercredi, 18. février 2009 7:58 07

Page 592: LINQ Language Integrated Query en C

Chapitre 17 Les conflits d’accès concurrentiels 577

ChangeConflictException

Lorsqu’un conflit d’accès concurrentiel se produit, quelle que soit la valeur de l’attributConflictMode, une exception ChangeConflictException est générée.

C’est en capturant cet exception que vous détectez les conflits d’accès concurrentiels.

Résolution des conflitsAprès avoir détecté un conflit en capturant l’événement ChangeConflictException, laprochaine étape consiste à le résoudre. Vous pouvez choisir d’effectuer une autre action,mais il y a de grandes chances pour que la résolution du conflit soit votre préoccupationmajeure. N’ayez crainte, le code à mettre en place n’est pas aussi complexe que vouspourriez le penser : grâce à la méthode ResolveAll et à deux méthodes Resolve, LINQto SQL facilite vraiment cette tâche.

RefreshMode

Si vous utilisez la méthode ResolveAll ou une des deux méthodes Resolve de LINQ toSQL pour résoudre un conflit, vous devez indiquer la façon dont le conflit sera résolu endéfinissant un RefreshMode. Les trois valeurs possibles sont KeepChanges, KeepCur-rentValues et OverwriteCurrentValues. Ces valeurs indiquent quelles données sontconservées dans les valeurs courantes des propriétés des objets d’entité lorsque le Data-Context effectue la résolution.

Si RefreshMode est initialisé à RefreshMode.KeepChanges, la méthode ResolveAll/Resolve affecte les valeurs contenues dans la base de données dans les valeurs couran-tes des propriétés de la classe d’entité pour toute colonne modifiée depuis sa premièrelecture dans la base de données. Si l’utilisateur a modifié une propriété, cette dernièreest conservée. Les données sont conservées selon l’ordre de priorité suivant (du plusbas au plus haut) : valeurs initiales des propriétés de la classe d’entité, valeurs descolonnes changées dans la base de données et valeurs des propriétés de la classed’entité modifiées par l’utilisateur.

Si RefreshMode est initialisé à RefreshMode.KeepCurrentValues, la méthode Resol-veAll/Resolve conserve les valeurs des propriétés de la classe d’entité de l’utilisateurcourant et ne tient pas compte des modifications effectuées dans la base de donnéesdepuis la première lecture. Les données sont conservées selon l’ordre de priorité suivant(du plus bas au plus haut) : valeurs initiales des propriétés de la classe d’entité etvaleurs des propriétés de la classe d’entité modifiées par l’utilisateur.

Si RefreshMode est initialisé à RefreshMode.OverwriteCurrentValues, la méthodeResolveAll/Resolve affecte les valeurs contenues dans la base de données dans lesvaleurs courantes des propriétés de la classe d’entité pour toute colonne modifiéedepuis sa première lecture dans la base de données et ne tient pas compte des valeursmodifiées par l’utilisateur dans la classe d’entité. Les données sont conservées selon

Linq.book Page 577 Mercredi, 18. février 2009 7:58 07

Page 593: LINQ Language Integrated Query en C

578 LINQ to SQL Partie V

l’ordre de priorité suivant (du plus bas au plus haut) : valeurs initiales des propriétés dela classe d’entité et valeurs des colonnes changées dans la base de données.

Résolution des conflitsTrois approches permettent de résoudre les conflits : la plus simple, une facile et uneautre manuelle. La méthode la plus simple consiste à appliquer la méthode ResolveAllsur la collection DataContext.ChangeConflicts, en lui passant un paramètre Refres-hMode et un paramètre bool optionnel qui indique si la résolution doit porter sur lesenregistrements supprimés.

La résolution automatique des enregistrements supprimés consiste à marquer les objetsentité supprimés comme étant effectivement supprimés, même si cela n’est pas le cas àcause d’un conflit d’accès concurrentiel. Ainsi, au prochain appel de la méthodeSubmitChanges, le DataContext n’essayera pas de supprimer une nouvelle fois l’enre-gistrement de la base de données qui correspond à l’objet entité. Par essence, nousdemandons à LINQ to SQL de faire semblant d’avoir supprimé l’enregistrement, mêmesi, en fait, quelqu’un d’autre l’a supprimé à sa place.

L’approche facile consiste à énumérer chaque ObjectChangeConflict de la collectionDataContext.ChangeConflicts et d’appeler la méthode Resolve pour chaque Object-ChangeConflict.

Si vous devez effectuer un traitement spécifique, vous pouvez toujours gérer la résolu-tion des conflits manuellement, en énumérant la collection ChangeConflicts du Data-Context, puis en énumérant la collection MemberConflicts de chaqueObjectChangeConflict, et en appelant la méthode Resolve sur chaque objet Member-ChangeConflict de cette collection. Cette approche n’est pas très compliquée car desméthodes sont là pour vous épauler.

DataContext.ChangeConflicts.ResolveAll()

La façon la plus simple de résoudre les conflits consiste à capturer l’exception Change-ConflictException et à appeler la méthode ResolveAll sur la collection Data-Context.ChangeConflicts. Tout ce que vous avez à faire consiste à choisir quelRefreshMode utiliser et à indiquer si vous voulez résoudre automatiquement les enregis-trements supprimés.

Si vous choisissez cette approche, tous les conflits seront résolus d’une manière identi-que, en fonction du paramètre RefreshMode passé. Si vous avez besoin d’une résolutionplus fine, utilisez une des deux autres approches, étudiées un peu plus loin dans cettesection.

Le Listing 17.2 montre comment résoudre les conflits en utilisant la méthode Resol-veAll. Cet exemple étant assez complexe, nous donnerons des explications chaque foisque cela sera nécessaire.

Linq.book Page 578 Mercredi, 18. février 2009 7:58 07

Page 594: LINQ Language Integrated Query en C

Chapitre 17 Les conflits d’accès concurrentiels 579

Listing 17.2 : Résolution des conflits avec la méthode DataContext.ChangeConflicts.ResolveAll().

Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind");

Customer cust = db.Customers.Where(c => c.CustomerID == "LAZYK").SingleOrDefault();

ExecuteStatementInDb(String.Format( @"update Customers set ContactName = ’Samuel Arthur Sanders’ where CustomerID = ’LAZYK’"));

Ces instructions créent le DataContext Northwind, lancent une requête LINQ to SQLsur la table Customer et modifient sur la colonne ContactName du client récupéré dansla base de données en utilisant ADO.NET. Cette dernière action met en place un poten-tiel conflit d’accès concurrentiel.

Nous allons maintenant modifier l’objet entité et essayer d’enregistrer la modificationdans la base de données.

cust.ContactTitle = "President";try{ db.SubmitChanges(ConflictMode.ContinueOnConflict);}catch (ChangeConflictException){

La méthode SubmitChanges a été insérée dans un bloc try/catch. Pour gérer convena-blement les conflits, l’exception ChangeConflictException est donc capturée. Il suffitmaintenant d’appeler la méthode ResolveAll pour essayer de sauvegarder les modificationsdans la base de données.

db.ChangeConflicts.ResolveAll(RefreshMode.KeepChanges); try { db.SubmitChanges(ConflictMode.ContinueOnConflict); cust = db.Customers.Where(c => c.CustomerID == "LAZYK").SingleOrDefault(); Console.WriteLine("ContactName = {0} : ContactTitle = {1}", cust.ContactName, cust.ContactTitle); } catch (ChangeConflictException) { Console.WriteLine("Conflict again, aborting."); }}

Ce code appelle la méthode ResolveAll et lui passe un attribut RefreshMode.Keep-Changes. La méthode SubmitChanges est à nouveau appelée à l’intérieur d’un nouveaubloc try/catch. Le client est à nouveau recherché dans la base de données et seschamps ContactName et ContactTitle sont affichés pour prouver que ni les modifica-tions effectuées par ADO.NET ni celles effectuées dans LINQ to SQL n’ont étéperdues. Si l’appel à la méthode SubmitChanges lève une exception, cette dernière estsignalée et la mise à jour est annulée.

Linq.book Page 579 Mercredi, 18. février 2009 7:58 07

Page 595: LINQ Language Integrated Query en C

580 LINQ to SQL Partie V

Il ne reste plus qu’à restaurer la base de données dans son état original pour que l’exem-ple puisse être exécuté à plusieurs reprises.

// Restauration de la base de données

ExecuteStatementInDb(String.Format(

@"update Customers

set ContactName = ’John Steel’, ContactTitle = ’Marketing Manager’

where CustomerID = ’LAZYK’"));

Si vous ne tenez pas compte du code qui a mis en place le conflit (dans une situationréelle, ce code ne sera bien sûr pas écrit) ni du code qui restaure la base de données àson état original (de même, ce code ne sera pas écrit dans une situation réelle), cetteapproche facilite grandement la résolution de conflits : il suffit d’insérer la méthodeSubmitChanges dans un bloc try/catch, de capturer l’exception ChangeConflictEx-ception, d’appeler la méthode ResolveAll et de répéter l’appel à la méthode Submit-Change. Voici les résultats du Listing 17.2 :

Comme vous pouvez le voir dans les résultats, la modification effectuée par ADO.NETet celle effectuée par LINQ to SQL ont toutes deux été sauvegardées dans la base dedonnées.

ObjectChangeConflict.Resolve()

Si la résolution de conflits avec un même RefreshMode et/ou une action autoResolve-Deletes ne vous convient pas, vous pouvez utiliser une autre approche consistant àénumérer les conflits dans la collection DataContext.ChangeConflicts et à les gérerindividuellement. Vous pouvez par exemple gérer chacun d’entre eux en faisant appel àla méthode Resolve. Cela vous permet de choisir un RefreshMode et/ou un autoResolve-Deletes adapté à chaque conflit.

Cette approche revient à résoudre les conflits au niveau des objets entité. L’argumentRefreshMode passé à la méthode Resolve s’applique à toutes les propriétés de la classed’entité de l’objet qui est à l’origine du conflit. Si cette approche vous semble insuffi-sante, vous utiliserez l’approche manuelle, examinée un peu plus loin dans cettesection.

Le Listing 17.3 donne un exemple de cette deuxième approche. Le code est identiqueà celui du Listing 17.2 mais, ici, l’appel à la méthode DataContext.Change-

Conflicts.ResolveAll est remplacé par une énumération de la collection Change-Conflicts.

Exécution de la déclaration SQL sur la base de données avec ADO.NET ...Base de données mise à jour.ContactName = Samuel Arthur Sanders : ContactTitle = PresidentExécution de la déclaration SQL sur la base de données avec ADO.NET ...Base de données mise à jour.

Linq.book Page 580 Mercredi, 18. février 2009 7:58 07

Page 596: LINQ Language Integrated Query en C

Chapitre 17 Les conflits d’accès concurrentiels 581

Listing 17.3 : Résolution des conflits avec la méthode ObjectChangeConflict.Resolve().

Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind");

Customer cust = db.Customers.Where(c => c.CustomerID == "LAZYK").SingleOrDefault();

ExecuteStatementInDb(String.Format( @"update Customers set ContactName = ’Samuel Arthur Sanders’ where CustomerID = ’LAZYK’"));

cust.ContactTitle = "President";try{ db.SubmitChanges(ConflictMode.ContinueOnConflict);}catch (ChangeConflictException){ foreach (ObjectChangeConflict conflict in db.ChangeConflicts) { Console.WriteLine("Un conflit s’est produit sur le client {0}.", ((Customer)conflict.Object).CustomerID); Console.WriteLine("Appel de la méthode Resolve ..."); conflict.Resolve(RefreshMode.KeepChanges); Console.WriteLine("Conflit résolu.{0}", System.Environment.NewLine); }

try { db.SubmitChanges(ConflictMode.ContinueOnConflict); cust = db.Customers.Where(c => c.CustomerID == "LAZYK").SingleOrDefault(); Console.WriteLine("ContactName = {0} : ContactTitle = {1}", cust.ContactName, cust.ContactTitle); } catch (ChangeConflictException) { Console.WriteLine("Nouveau conflit, annulation des modifications."); }}

// Restauration de la base de données.ExecuteStatementInDb(String.Format( @"update Customers set ContactName = ’John Steel’, ContactTitle = ’Marketing Manager’ where CustomerID = ’LAZYK’"));

Comme vous pouvez le voir, au lieu d’appeler la méthode DataContext.Change-Conflicts.ResolveAll, ce code énumère la collection ChangeConflicts et appelle laméthode Resolve sur chacun des objets ObjectChangeConflict de la collection. Toutcomme dans le listing précédent, la méthode SubmitChanges est à nouveau invoquée, leclient est à nouveau récupéré dans la base de données et les propriétés ContactName etContactTitle sont à nouveau affichées. Bien entendu, le code se termine par la restau-ration de l’état initial de la base de données.

Linq.book Page 581 Mercredi, 18. février 2009 7:58 07

Page 597: LINQ Language Integrated Query en C

582 LINQ to SQL Partie V

Voici les résultats du Listing 17.3 :

Tout s’est déroulé comme souhaité. Dans un code de production, vous pourriez néan-moins effectuer une boucle sur l’appel de la méthode SubmitChanges et la résolution duconflit, pour le cas où de nouveaux conflits se produiraient. Si vous choisissez cetteapproche, assurez-vous que la boucle ne s’exécute pas indéfiniment…

MemberChangeConflict.Resolve()

Dans la première approche, nous avons appelé une méthode pour résoudre tous lesconflits d’une seule et unique façon. Dans la deuxième approche, nous avons appeléune méthode spécifique pour chacun des conflits détectés. Ceci permet d’appliquer untraitement particulier à chaque objet entité. Nous allons maintenant nous intéresser àune approche manuelle.

Que le mot "manuel" ne vous impressionne pas trop : la détection manuelle de conflitsest certainement bien plus simple que ce que vous imaginez. En choisissant cette appro-che, vous allez pouvoir appliquer plusieurs valeurs RefreshModes à chacune despropriétés d’objet entité.

Tout comme dans la deuxième approche, nous allons énumérer les objets ObjectChan-geConflict de la collection DataContext.ChangeConflicts. Mais, ici, au lieu d’appe-ler la méthode Resolve sur chaque objet ObjectChangeConflict, nous allons énumérerleur collection MemberConflicts et appliquer la méthode Resolve sur chaque membrede cette collection.

À ce niveau, un objet MemberChangeConflict est relatif à une propriété spécifiqued’une classe d’entité de l’objet ayant provoqué le conflit. Cela vous permet de choi-sir un RefreshMode différent pour chaque propriété de la classe d’entité qui le néces-site.

Le paramètre de la méthode Resolve peut être un Refreshmode ou la valeur courante àaffecter à la propriété. Ceci assure une grande flexibilité à cette troisième approche.

Le Listing 17.4 donne un exemple de résolution manuelle de conflit d’accès concurren-tiel. Dans cet exemple, nous allons décider que, si un conflit se produit sur la colonneContactName, la base de données doit rester inchangée. En revanche, si un conflit seproduit sur une autre colonne, la base de données doit être mise à jour.

Exécution de la déclaration SQL sur la base de données avec ADO.NET ...Base de données mise à jour.Un conflict s’est produit sur le client LAZYK.Appel de la méthode Resolve ...Conflit résolu.

ContactName = Samuel Arthur Sanders : ContactTitle = PresidentExécution de la déclaration SQL sur la base de données avec ADO.NET ...Base de données mise à jour.

Linq.book Page 582 Mercredi, 18. février 2009 7:58 07

Page 598: LINQ Language Integrated Query en C

Chapitre 17 Les conflits d’accès concurrentiels 583

Pour implémenter ce comportement, nous utiliserons le même code que dans leListing 17.3 mais, au lieu d’appeler la méthode Resolve sur les objets ObjectChange-Conflict, nous allons énumérer les objets de la collection MemberConflicts. Pourchacun d’entre eux, si la propriété de l’objet entité qui entre en conflit est ContactName,la valeur contenue dans la base de données sera conservée. Pour ce faire, nous affecte-rons la valeur RefreshMode.OverwriteCurrentValues au paramètre RefreshMode de laméthode Resolve. Si la propriété de l’objet entité qui entre en conflit n’est pas Contac-tName, la valeur de l’entité sera sauvegardée dans la base de données. Pour ce faire,nous affecterons la valeur RefreshMode.KeepChanges au paramètre RefreshMode de laméthode Resolve.

Pour que l’exemple soit plus intéressant, lors de la mise en place du conflit en modifiantla base de données via ADO.NET, le champ ContactTitle sera également modifié.Cela provoquera deux conflits sur des propriétés d’objet entité. Le conflit sur lapropriété ContactName devrait laisser inchangée la valeur stockée dans la base dedonnées, et celui sur la propriété ContactTitle devrait sauvegarder la valeur LINQ toSQL dans la base de données.

Listing 17.4 : Un exemple de résolution manuelle de conflits.

Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind");

Customer cust = db.Customers.Where(c => c.CustomerID == "LAZYK").SingleOrDefault();

ExecuteStatementInDb(String.Format( @"update Customers set ContactName = ’Samuel Arthur Sanders’, ContactTitle = ’CEO’ where CustomerID = ’LAZYK’"));

cust.ContactName = "Viola Sanders";cust.ContactTitle = "President";try{ db.SubmitChanges(ConflictMode.ContinueOnConflict);}catch (ChangeConflictException){ foreach (ObjectChangeConflict conflict in db.ChangeConflicts) { Console.WriteLine("Un conflit s’est produit pour le client {0}.", ((Customer)conflict.Object).CustomerID); foreach (MemberChangeConflict memberConflict in conflict.MemberConflicts) { Console.WriteLine("Appel de la méthode Resolve pour {0} ...", memberConflict.Member.Name); if (memberConflict.Member.Name.Equals("ContactName")) { memberConflict.Resolve(RefreshMode.OverwriteCurrentValues); } else { memberConflict.Resolve(RefreshMode.KeepChanges); }

Linq.book Page 583 Mercredi, 18. février 2009 7:58 07

Page 599: LINQ Language Integrated Query en C

584 LINQ to SQL Partie V

Console.WriteLine("Conflit résolu.{0}", System.Environment.NewLine); } }

try { db.SubmitChanges(ConflictMode.ContinueOnConflict); cust = db.Customers.Where(c => c.CustomerID == "LAZYK").SingleOrDefault(); Console.WriteLine("ContactName = {0} : ContactTitle = {1}", cust.ContactName, cust.ContactTitle); } catch (ChangeConflictException) { Console.WriteLine("Nouveau conflit, abandon de la mise à jour."); }}

// Restauration de la base de donnéesExecuteStatementInDb(String.Format( @"update Customers set ContactName = ’John Steel’, ContactTitle = ’Marketing Manager’ where CustomerID = ’LAZYK’"));

Un des changements significatifs dans ce code concerne la modification du champContactTitle avec ADO.NET. Ceci provoque un conflit entre la propriété del’objet entité et la base de données lors de l’appel de la méthode SubmitChanges.Ensuite, plutôt qu’appeler la méthode Resolve sur l’objet ObjectChangeConflict,sa collection MemberConflicts est énumérée pour examiner chaque propriété desobjets entité. S’il s’agit de la propriété ContactName, la méthode Resolve est appe-lée avec un paramètre RefreshMode égal à RefreshMode.OverwriteCurrentValuesafin de conserver la valeur stockée dans la base de données. S’il s’agit d’une autrepropriété, la méthode Resolve est appelée avec un paramètre RefreshMode égal àRefreshMode.KeepChanges pour sauvegarder la valeur stockée dans le code LINQto SQL.

Voici les résultats du Listing 17.4 :

Comme le montrent les résultats, les conflits relatifs aux propriétés ContactName etContactTitle de l’objet entité ont tous deux été résolus. En examinant la valeur despropriétés ContactName et ContactTitle, vous voyez que la valeur de ContactName

Exécution de la déclaration SQL sur la base de données avec ADO.NET ...Base de données mise à jour.Un conflict s’est produit sur le client LAZYK.Appel de la méthode Resolve pour ContactName...Conflit résolu.Appel de la méthode Resolve pour ContactTitle...Conflit résolu.ContactName = Samuel Arthur Sanders : ContactTitle = PresidentExécution de la déclaration SQL sur la base de données avec ADO.NET ...Base de données mise à jour.

Linq.book Page 584 Mercredi, 18. février 2009 7:58 07

Page 600: LINQ Language Integrated Query en C

Chapitre 17 Les conflits d’accès concurrentiels 585

contenue dans la base de données a été conservée et que la valeur de ContactTitlestockée dans le code LINQ to SQL a été sauvegardée dans la base de données.

Avouez que ce code n’est pas très compliqué. Cependant, vous n’adopterez cette appro-che que dans les cas qui le nécessitent, et vous utiliserez l’une des deux approchesprécédentes dans tous les autres cas.

Contrôle d’accès concurrentiel pessimiste

Comme son nom l’indique, le contrôle d’accès concurrentiel pessimiste suppose la piresituation, à savoir que, dans la plupart des cas, un conflit d’accès concurrentiel seproduira lors de la sauvegarde dans la base de données. Cette situation n’est pas aussidésespérée qu’il y paraît : il suffit d’inclure la lecture et la mise à jour de la base dedonnées dans une transaction.

En adoptant une approche pessimiste, il n’y a aucun conflit à résoudre, puisque la basede données bloque la transaction. Personne ne peut donc modifier son contenu dansvotre dos.

Pour tester ce mode de fonctionnement (voir Listing 17.5), nous allons créer un objetTransactionScope et récupérer un objet entité pour le client LAZYK. Un autre objetTransactionScope sera alors créé avec l’option TransactionScopeOption initialisée àRequiresNew. Ceci afin que le code ADO.NET ne participe pas à la première transac-tion. Par la suite, nous essayerons de mettre à jour ce même enregistrement dans la basede données en utilisant ADO.NET. Étant donné qu’une transaction bloque l’accès à labase de données, la mise à jour ADO.NET sera bloquée et finalement annulée partimeout. Par la suite, nous modifierons l’objet entité ContactName, appellerons laméthode SubmitChanges et effectuerons une nouvelle requête sur le client pour afficherson champ ContactName et prouver qu’il a été mis à jour par LINQ to SQL, puis nousfermerons la transaction.

INFO

Vous devez ajouter une référence à l’assembly System.Transactions.dll dans votreprojet pour que cet exemple passe l’étape de la compilation.

Listing 17.5 : Un exemple d’accès concurrentiel pessimiste.

Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind");

using (System.Transactions.TransactionScope transaction = new System.Transactions.TransactionScope()){ Customer cust = db.Customers.Where(c => c.CustomerID == "LAZYK").SingleOrDefault();

try

Linq.book Page 585 Mercredi, 18. février 2009 7:58 07

Page 601: LINQ Language Integrated Query en C

586 LINQ to SQL Partie V

{ Console.WriteLine("Tentative de mise à jour du champ ContactName du client LAZYK ➥avec ADO.NET."); Console.WriteLine("Patience, nous devons attendre le timeout..."); using (System.Transactions.TransactionScope t2 = new System.Transactions.TransactionScope( System.Transactions.TransactionScopeOption.RequiresNew)) { ExecuteStatementInDb(String.Format( @"update Customers set ContactName = ’Samuel Arthur Sanders’ where CustomerID = ’LAZYK’"));

t2.Complete(); }

Console.WriteLine("Le champ ContactName de LAZYK a été mis à jour.{0}", System.Environment.NewLine); }

catch (Exception ex) { Console.WriteLine( "Une exception s’est produite pendant la mise à jour du client LAZYK avec ➥ADO.NET :{0} {1}{0}", System.Environment.NewLine, ex.Message); }

cust.ContactName = "Viola Sanders"; db.SubmitChanges();

cust = db.Customers.Where(c => c.CustomerID == "LAZYK").SingleOrDefault(); Console.WriteLine("Champ ContactName du client : {0}", cust.ContactName);

transaction.Complete();}

// Restauration de la base de donnéesExecuteStatementInDb(String.Format( @"update Customers set ContactName = ’John Steel’, ContactTitle = ’Marketing Manager’ where CustomerID = ’LAZYK’"));

ASTUCE

Si vous obtenez une exception du type "MSDTC on server ’[server]\SQLEXPRESS’ is unavaila-ble" lorsque vous travaillez avec un exemple qui utilise l’objet TransactionScope, assurez-vous que le service Distributed Transaction Coordinator est lancé.

Le code n’est pas aussi complexe qu’il peut le sembler de prime abord. Après avoircréé un objet TransactionScope, nous adoptons de facto une approche pessimiste ettoute modification extérieure des données est impossible. Le client LAZYK est récu-péré en utilisant une requête LINQ to SQL. Nous créons alors un autre objet Tran-sactionScope pour empêcher le code ADO.NET suivant de participer à la première

Linq.book Page 586 Mercredi, 18. février 2009 7:58 07

Page 602: LINQ Language Integrated Query en C

Chapitre 17 Les conflits d’accès concurrentiels 587

transaction. Le deuxième objet TransactionScope créé, nous tentons de mettre àjour le client dans la base de données en utilisant ADO.NET. Le code de mise à journ’est pas en mesure d’effectuer son travail, car la première transaction l’en empê-chera, et une exception de type timeout sera générée. La propriété ContactName duclient est alors modifiée puis enregistrée dans la base de données à l’aide de laméthode SubmitChanges. Le client est à nouveau récupéré à l’aide d’une requêteLINQ to SQL et son champ ContactName est affiché pour prouver que la modifica-tion a bien été effectuée. La première transaction est enfin terminée en lui appliquantla méthode Complete.

Bien entendu, comme toujours, la base de données est restaurée à la fin du code. Voiciles résultats :

Comme vous le voyez, lorsque nous essayons de modifier la base de données avecADO.NET, une exception timeout se produit. Dans cet exemple, la requête LINQ toSQL appelle l’opérateur SingleOrDefault, qui n’est pas un opérateur différé. Larequête n’est donc pas différée, et elle doit être déclarée dans la portée d’un objet Tran-sactionScope. Si l’opérateur SingleOrDefault n’avait pas été appelé, la requête auraitpu être déclarée avant la création de l’objet TransactionScope, à condition qu’elle soitexécutée dans la portée de l’objet TransactionScope. Par conséquent, nous aurions trèsbien pu obtenir le retour de la requête LINQ (une séquence IEnumerable<T>) avant lacréation de l’objet TransactionScope, puis lui appliquer l’opérateur SingleOrDefaultdans la portée de l’objet TransactionScope, afin d’obtenir le client unique résultant dela requête.

Lorsque vous adoptez cette approche, soyez toujours bien conscient des opérationseffectuées dans la portée de l’objet TransactionScope, car pendant ces opérations labase de données sera verrouillée.

INFO

Si vous exécutez cet exemple en mode débogage, il se peut qu’un timeout apparaissependant la transaction de l’objet TransactionScope.

Tentative de mise à jour du champ ContactName du client LAZYK avec ADO.NET.Patience, nous devons attendre le timeout...Exécution de la déclaration SQL sur la base de données avec ADO.NET ...Une exception s’est produite en essayant de mettre à jour le champ LAZYK avec ADO.NETLe timeout a expiré. La période du timeout s’est écoulée avant la fin de l’opération ➥ou le serveur ne répond pas.La déclaration a été arrêtée.Champ ContactName du client: Viola SandersExécution de la déclaration SQL sur la base de données avec ADO.NET ...Base de données mise à jour.

Linq.book Page 587 Mercredi, 18. février 2009 7:58 07

Page 603: LINQ Language Integrated Query en C

588 LINQ to SQL Partie V

Une approche alternative pour les middle-tier et les serveurs

Il existe une autre approche pour gérer les conflits d’accès concurrentiels lorsqu’ils seproduisent dans un middle-tier (serveur de couche intermédiaire) et les serveurs.Parfois, lorsqu’un conflit d’accès concurrentiel se produit, il peut être plus simple decréer un nouveau DataContext, d’appliquer les modifications et d’appeler à nouveau laméthode SubmitChanges.

Considérons une application web ASP.NET. Étant donné que le navigateur web n’a pasforcément besoin qu’une connexion Internet soit établie avant son utilisation, unnouveau DataContext doit être créé à chaque fois qu’un post HTTP est fait sur leserveur web, et une requête LINQ to SQL doit être exécutée. Étant donné que lesdonnées issues d’une base de données sont obsolètes quasiment dès leur lecture, il n’estpas raisonnable de garder un DataContext ouvert pendant une longue période en ayantl’intention de lui appliquer des modifications.

Lorsqu’un utilisateur se rend sur une page web et que des données sont récupéréesdepuis une base de données, il ne paraît pas raisonnable d’utiliser l’objet DataContextpour retourner les données de la page au serveur (postback) afin de mettre à jour la basede données. Le DataContext ne survivra pas à l’attente du postback, à moins qu’il soitmémorisé entre deux connexions, par exemple dans des variables de session. Cepen-dant, même s’il survit, le délai entre deux connexions peut être très long, et unenouvelle connexion peut même ne jamais avoir lieu. Plus le délai entre la premièrelecture dans la base de données et le postback est élevé, plus les données ont des chan-ces d’être obsolètes. Dans ce type de scénario, plutôt que stocker les données dans leDataContext, il peut être plus intelligent de créer un DataContext à chaque postback,lorsque les données doivent être sauvegardées. Dans ce cas, si un conflit d’accèsconcurrentiel se produit, un moindre mal consisterait à créer un autre DataContext, àmodifier les données et à appeler à nouveau la méthode SubmitChanges pour les enre-gistrer dans la base de données. Et, comme le délai serait bref entre la lecture desdonnées, leur modification et l’appel de la procédure SubmitChanges, il n’y a que peude chances qu’un conflit se produise à la première tentative, et encore moins à laseconde.

Si vous décidez d’adopter cette approche sur le postback après la construction de l’objetDataContext, vous pourriez récupérer l’objet entité souhaité, comme il vient d’êtreindiqué, ou utiliser une autre méthode.

Plutôt que récupérer l’objet entité, vous pourriez en créer un nouveau, définir sespropriétés et l’attacher à une table en utilisant la méthode Attach de l’objet Table<T>.À ce point, c’est comme si l’objet entité avait été récupéré depuis la base de données, àceci près que ses champs ne contiennent aucune donnée.

Avant d’attacher un objet entité à un Table<T>, vous devez définir les propriétés de laclasse d’entité avec les valeurs appropriées. Cela ne signifie nullement que vous deviez

Linq.book Page 588 Mercredi, 18. février 2009 7:58 07

Page 604: LINQ Language Integrated Query en C

Chapitre 17 Les conflits d’accès concurrentiels 589

interroger la base de données pour obtenir ces valeurs : elles pourraient venir d’unendroit quelconque, par exemple d’un autre tier. Les propriétés nécessaires de la classed’entité correspondent à toutes les propriétés de la classe d’entité qui constituent la cléprimaire (ou qui établissent une identité), aux propriétés qui vont être modifiées et àtoutes celles qui participent à la vérification de la mise à jour. Vous devez inclure lespropriétés de la classe d’entité qui définissent l’identité, afin que le DataContext puissese repérer dans la classe de l’entité. Vous devez inclure les propriétés de la classed’entité que vous allez modifier, afin qu’elles puissent être mises à jour et que le proces-sus de détection de conflit concurrentiel puisse être mis en place. Enfin, vous devezinclure toutes les propriétés des classes d’entité qui participent à la vérification de lamise à jour dans la détection des conflits d’accès concurrentiels. Si la classe d’entité aune propriété IsVersion initialisée à true pour l’attribut Column, cette propriété doitêtre définie avant d’appeler la méthode Attach.

Examinons le Listing 17.6.

Listing 17.6 : Utilisation de la méthode Attach() pour attacher un objet entité fraîchement construit.

Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind");

// Création de l’objet entitéConsole.WriteLine("Construction d’un objet Customer vide.");Customer cust = new Customer();

// Tous les champs qui définissent l’identité doivent être définisConsole.WriteLine("Définition de la clé primaire.");cust.CustomerID = "LAZYK";

// Tous les champs qui vont être modifiés doivent être définisConsole.WriteLine("Définition des champs qui vont être modifiés.");cust.ContactName = "John Steel";

// Tous les champs qui participent à la vérification de la mise à jour //doivent être définis. En ce qui concerne la classe Customer,// tous les champs doivent être inclusConsole.WriteLine("Définition des champs qui participent à la vérification de la ➥mise à jour.");cust.CompanyName = "Lazy K Kountry Store";cust.ContactTitle = "Marketing Manager";cust.Address = "12 Orchestra Terrace";cust.City = "Walla Walla";cust.Region = "WA";cust.PostalCode = "99362";cust.Country = "USA";cust.Phone = "(509) 555-7969";cust.Fax = "(509) 555-6221";

// Utilisation de la méthode Attach() sur la table Customers Table<T>Console.WriteLine("Appel de la méthode Attach() sur le Customers Table<Customer>.");db.Customers.Attach(cust);

Linq.book Page 589 Mercredi, 18. février 2009 7:58 07

Page 605: LINQ Language Integrated Query en C

590 LINQ to SQL Partie V

// À ce point, les modifications peuvent être effectuées// et la méthode SubmitChanges() appeléeConsole.WriteLine("Modification des données et appel de la méthode ➥SubmitChanges().");cust.ContactName = "Vickey Rattz";db.SubmitChanges();

cust = db.Customers.Where(c => c.CustomerID == "LAZYK").SingleOrDefault();Console.WriteLine("Valeur du champ ContactName dans la base de données : {0}", cust.ContactName);

Console.WriteLine("Restauration de l’état original de la base de données.");cust.ContactName = "John Steel";db.SubmitChanges();

Comme vous pouvez le voir, la clé primaire est définie en premier, suivie des propriétésqui vont être modifiées, puis des propriétés qui participent à la vérification de la mise àjour. Comme il a été mentionné précédemment, les valeurs appropriées doivent êtreaffectées à ces propriétés. Cela ne signifie nullement que vous deviez interroger la basede données pour les obtenir : elles pourraient se trouver dans des variables cachées ouêtre passées par un autre tier. La méthode Attach est alors appelée sur l’objet Custo-mers Table<Customer>. Les modifications sont ensuite effectuées, puis la méthodeSubmitChanges est appelée. Une requête est alors effectuée dans la base de données, etle champ ContactName du client est affiché, pour prouver qu’il a bien été modifié dansla base de données. Et, pour finir, l’état original de la base de données est restauré. Voiciles résultats du Listing 17.6 :

L’insertion ou la suppression d’objets provenant d’une classe d’entité ne nécessitent pascette approche : il vous suffit d’insérer ou de supprimer un objet de la classe d’entitéavant d’appeler la méthode SubmitChanges. Reportez-vous aux sections intitulées"Insertions" et "Suppressions" du Chapitre 14 pour en savoir plus à ce sujet.

Construction d’un objet Customer vide.Définition de la clé primaire.Définition des champs qui vont être modifiés.Définition des champs qui participent à la vérification de la mise à jour.Appel de la méthode Attach() sur le Customers Table<Customer>.Modification des données et appel de la méthode SubmitChanges().Valeur du champ ContactName dans la base de données : Vickey RattzRestauration de l’état original de la base de données.

Linq.book Page 590 Mercredi, 18. février 2009 7:58 07

Page 606: LINQ Language Integrated Query en C

Chapitre 17 Les conflits d’accès concurrentiels 591

Résumé

La détection et la résolution de conflits d’accès concurrentiels avaient été utilisées à denombreuses reprises dans les chapitres relatifs à LINQ to SQL sans être étudiées endétail. Ce chapitre a comblé ce vide.

J’espère que vous avez été aussi impressionné que moi par la facilité avec laquelleLINQ to SQL permet de détecter et de résoudre les conflits d’accès concurrentiels.J’espère également que ce chapitre, au demeurant plutôt intimidant, vous a aidé àretrouver une paix intérieure.

Notre voyage dans l’univers LINQ to SQL est sur le point de s’achever. Pour conclurecet ouvrage, le chapitre suivant va vous donner des informations complémentaires surLINQ to SQL.

Linq.book Page 591 Mercredi, 18. février 2009 7:58 07

Page 607: LINQ Language Integrated Query en C

Linq.book Page 592 Mercredi, 18. février 2009 7:58 07

Page 608: LINQ Language Integrated Query en C

18

Informations complémentairessur SQL

Le dernier chapitre de cet ouvrage donne des informations diverses et variées sur LINQto SQL. Nous parlerons entre autres des vues d’une base de données, de l’héritage desclasses d’entité et des transactions.

Prérequis pour exécuter les exemples

Pour pouvoir exécuter les exemples de ce chapitre, vous devez être en possession de labase de données étendue Northwind et avoir généré les classes d’entité correspondan-tes. Si nécessaire, reportez-vous à la section intitulée "Prérequis pour exécuter lesexemples" du Chapitre 12 pour avoir des informations complémentaires à ce sujet.

Utilisation de l’API LINQ to SQL

Pour exécuter les exemples de ce chapitre, vous pouvez être amené à ajouter des réfé-rences et des directives using à vos projets. Reportez-vous à la section intitulée "Utili-sation de l’API LINQ to SQL" au Chapitre 12 pour en savoir plus à ce sujet.

Utilisation de l’API LINQ to XML

Plusieurs exemples de ce chapitre ont besoin d’une directive using pour référencerl’espace de noms System.Xml.Linq.

Les vues d’une base de données

Lorsque les classes d’entité de la base de données Northwind ont été générées auChapitre 12, nous avons utilisé l’option /views pour créer des mappages pour les diffé-rentes vues de la base de données. Nous allons maintenant vous montrer comment les

Linq.book Page 593 Mercredi, 18. février 2009 7:58 07

Page 609: LINQ Language Integrated Query en C

594 LINQ to SQL Partie V

interroger. Les outils de génération de classes d’entité (SQLMetal et le ConcepteurObjet/Relationnel) déclarent une propriété Table<T> dans la classe [Your]DataContextpour chaque vue de la base de données et créent la classe d’entité T correspondante. Lesvues peuvent être interrogées comme des tables. Elles se comportent d’une façon simi-laire, à ceci près qu’elles ne sont accessibles qu’en lecture seule.

Étant donné que les classes d’entité générées pour des vues ne contiennent pas depropriétés mappées en tant que clés primaires, elles ne sont accessibles qu’en lectureseule. Et, sans clé primaire, le DataContext n’est pas en mesure d’effectuer une recherched’identité.

À titre d’exemple, la base de données Northwind a une vue nommée "Category Salesfor 1997". SQLMetal a donc généré une propriété publique nomméeCategorySalesFor1997s :

Une propriété publique pour une vue d’une base de données

public System.Data.Linq.Table<CategorySalesFor1997> CategorySalesFor1997s{ get { return this.GetTable<CategorySalesFor1997>(); }}

SQLMetal a également généré une classe d’entité CategorySalesFor1997s (voirListing 18.1).

Listing 18.1 : Interrogation d’une vue d’une base de données.

Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind");

IQueryable<CategorySalesFor1997> seq = from c in db.CategorySalesFor1997s where c.CategorySales > (decimal)100000.00 orderby c.CategorySales descending select c;

foreach (CategorySalesFor1997 c in seq){ Console.WriteLine("{0} : {1:C}", c.CategoryName, c.CategorySales);}

Comme vous pouvez le voir, la vue a été interrogée comme s’il s’agissait d’une table.Voici les résultats :

Comme indiqué précédemment, les vues sont accessibles en lecture seule. LeListing 18.2 va tenter d’insérer un enregistrement dans une vue.

Dairy Products : $114,749.78Beverages : $102,074.31

Linq.book Page 594 Mercredi, 18. février 2009 7:58 07

Page 610: LINQ Language Integrated Query en C

Chapitre 18 Informations complémentaires sur SQL 595

Listing 18.2 : Tentative ratée d’insertion d’un enregistrement dans une vue.

Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind");

db.CategorySalesFor1997s.InsertOnSubmit( new CategorySalesFor1997 { CategoryName = "Legumes", CategorySales = (decimal) 79043.92 });

Dans ce listing, nous n’avons pas pris la peine d’appeler la méthode SubmitChanges.Ceci parce que nous savons pertinemment qu’une exception va être levée. Voici lesrésultats :

Alors que les méthodes InsertOnSubmit et DeleteOnSubmit génèrent des exceptionslorsqu’elles sont appelées sur un Table<T> mappé à une vue d’une base de données,rien ne vous empêche de faire des modifications sur une propriété d’un objet entitéd’une vue. Vous pouvez modifier la valeur d’une propriété et même appeler la méthodeSubmitChanges sans qu’aucune exception ne soit levée, mais les modifications effec-tuées dans la propriété de l’objet entité de la vue ne seront pas répercutées dans la basede données.

Héritage des classes d’entité

Jusqu’ici, dans tous les chapitres relatifs à LINQ to SQL, chaque classe d’entité étaitmappée à une et une seule table. Le mappage entre les classes d’entité et les tables étaitdonc de type "un-à-un".

ATTENTIONATTENTION

L’exemple utilisé dans cette section crée un modèle de données qui contient les classesSquare et Rectangle. D’un point de vue géométrique, un carré est un rectangle, mais unrectangle n’est pas nécessairement un carré. Cependant, dans le modèle de données de cetexemple, la relation inverse est également vérifiée. Le modèle de classe indique qu’unrectangle est dérivé d’un carré. Par conséquent, un rectangle est un carré, mais un carré n’estpas nécessairement un rectangle. Ce raisonnement sera expliqué un peu plus bas.

LINQ to SQL offre à ce raisonnement une alternative connue sous le nom "héritage declasse d’entité". Par son intermédiaire, une hiérarchie de classe peut être mappée à unetable unique d’une base de données. Pour cette table, il doit y avoir une classe d’entitéde base et les mappages appropriés des attributs de la classe d’entité. Cette classe debase contiendra toutes les propriétés communes aux classes de la hiérarchie qui en déri-vent, alors que les classes dérivées ne contiendront que les propriétés qui leur sont

Exception non gérée : System.InvalidOperationException: Impossible d’effectuer uneopération Create, Update ou Delete sur la table ’(CategorySalesFor1997)’, car elleest accessible en lecture seule....

Linq.book Page 595 Mercredi, 18. février 2009 7:58 07

Page 611: LINQ Language Integrated Query en C

596 LINQ to SQL Partie V

spécifiques. Voici un exemple de classe d’entité de base sans aucune classe dérivéemappée.

La classe d’entité de base, sans classes dérivées mappées

[Table]public class Shape{ [Column(IsPrimaryKey = true, IsDbGenerated = true, DbType = "Int NOT NULL IDENTITY")] public int Id;

[Column(IsDiscriminator = true, DbType = "NVarChar(2)")] public string ShapeCode;

[Column(DbType = "Int")] public int StartingX;

[Column(DbType = "Int")] public int StartingY;}

Comme vous pouvez le voir, l’attribut Table a été spécifié. Étant donné qu’aucunepropriété de l’attribut Name n’a été spécifiée, la classe d’entité de base est mappée à unetable dont le nom est identique à celui de la classe. Elle est donc mappée à la tableShape. La table Shape n’a pas encore été définie. Un peu plus loin dans cette section,nous utiliserons la méthode CreateDatabase de l’objet DataContext pour créer la basede données. Pour l’instant, aucune classe dérivée n’a encore été mappée. Dans quelquespages, nous reviendrons à la classe d’entité de base pour lui mapper quelques classesdérivées.

Une idée se cache derrière l’héritage des classes d’entité : la table Shape a une colonnedont la valeur indique dans quelle classe d’entité l’enregistrement devrait être construitlorsqu’il est récupéré par LINQ to SQL. Cette colonne est appelée "discriminant". Elleest spécifiée en utilisant la propriété IsDiscriminator de l’attribut Column.

Une valeur affectée à un discriminant est appelée "valeur discriminante" ou "codediscriminant". Lorsque vous mappez une classe d’entité de base à une table d’une basede données, outre les attributs Table, vous pouvez spécifier les attributs Inheritance-Mapping pour mapper les codes discriminants aux classes dérivées de la classe d’entitéde base. Dans la classe Shape précédente, aucun héritage n’a été mappé.

Cette classe de base contient plusieurs membres publics. Chacun d’entre eux est mappéà une colonne de la base de données, et le type de la colonne est spécifié en argument.Dans notre cas, la spécification des types des colonnes de la base de données est néces-saire à la méthode CreateDatabase, appelée un peu plus loin dans le code. La propriétéIsDiscriminator du membre ShapeCode a été initialisée à true. Cette colonne est doncle discriminant. Elle dictera le type de la classe d’entité utilisé pour construire chacundes enregistrements d’un objet de classe d’entité.

Linq.book Page 596 Mercredi, 18. février 2009 7:58 07

Page 612: LINQ Language Integrated Query en C

Chapitre 18 Informations complémentaires sur SQL 597

Les membres de cette classe sont Id, ShapeCode, StartingX et StartingY (les coordon-nées X et Y de la forme sur l’écran). À ce point, ce sont les seuls membres dont nouspouvons prévoir qu’ils seront communs aux différentes formes.

Pour créer une hiérarchie de classe, il suffit de dériver des classes de la classe de base.Les classes dérivées doivent hériter de la classe de base. Elles ne spécifieront pas l’attri-but Table, mais devront indiquer les attributs Column pour chaque membre publicmappé à la base de données. Voici les classes d’entité dérivées :

Les classes d’entité dérivées

public class Square : Shape{ [Column(DBType = "Int")] public int Width;}

public class Rectangle : Square{ [Column(DBType = "Int")] public int Length;}

D’un point de vue géométrique, un carré est un rectangle, mais un rectangle n’est pasforcément un carré. Dans cet exemple, comme les côtés du carré doivent être égaux,une seule mesure est nécessaire : Width. Un rectangle nécessite deux mesures (unelargeur et une longueur). Il hérite donc du carré et ajoute un membre pour définir lalongueur : Length. D’un point de vue héritage de classe, un rectangle est un carré, maisun carré n’est pas un rectangle. Pendant les quelques pages relatives à cet exemple,vous devez donc oublier tout ce que vous avez appris en classe…

Nous avons maintenant nos classes dérivées. Il ne nous manque plus que le mappageentre les valeurs discriminantes, la classe d’entité de base et les classes d’entité déri-vées. Après avoir ajouté les attributs InheritanceMapping nécessaires, la classe de basea l’allure suivante :

La classe d’entité de base avec les mappages des classes dérivées

[Table][InheritanceMapping(Code = "G", Type = typeof(Shape), IsDefault = true)][InheritanceMapping(Code = "S", Type = typeof(Square))][InheritanceMapping(Code = "R", Type = typeof(Rectangle))]public class Shape{ [Column(IsPrimaryKey = true, IsDbGenerated = true, DbType = "Int NOT NULL IDENTITY")] public int Id;

[Column(IsDiscriminator = true, DbType = "NVarChar(2)")] public string ShapeCode;

[Column(DbType = "Int")]

Linq.book Page 597 Mercredi, 18. février 2009 7:58 07

Page 613: LINQ Language Integrated Query en C

598 LINQ to SQL Partie V

public int StartingX;

[Column(DbType = "Int")] public int StartingY;}

Les mappages relient les différentes valeurs discriminantes aux classes d’entité. Lacolonne ShapeCode est le discriminant. Si un enregistrement a une valeur "G" dans cettecolonne, il sera construit dans la classe Shape. S’il a une valeur "S", il sera construitdans la classe Square. S’il a une valeur "R", il sera construit dans la classe Rectangle.

Lorsque le discriminateur d’un enregistrement ne correspond à aucune des valeursmappées aux classes d’entité, une classe est utilisée par défaut. Le mappage utilisépar défaut est défini avec la propriété IsDefault (ici, Shape). Ainsi, par exemple, siun enregistrement a une valeur "Q" dans la colonne ShapeCode, il sera construitdans la classe Shape, puisque cette valeur ne correspond à aucune des valeursmappées.

Voyons maintenant le code complet de la classe DataContext.

La classe DataContext

public partial class TestDB : DataContext{ public Table<Shape> Shapes;

public TestDB(string connection) : base(connection) { }

public TestDB(System.Data.IDbConnection connection) : base(connection) { }

public TestDB(string connection, System.Data.Linq.Mapping.MappingSource mappingSource) : base(connection, mappingSource) { }

public TestDB(System.Data.IDbConnection connection, System.Data.Linq.Mapping.MappingSource mappingSource) : base(connection, mappingSource) { }}

[Table][InheritanceMapping(Code = "G", Type = typeof(Shape), IsDefault = true)][InheritanceMapping(Code = "S", Type = typeof(Square))][InheritanceMapping(Code = "R", Type = typeof(Rectangle))]public class Shape{ [Column(IsPrimaryKey = true, IsDbGenerated = true, DbType = "Int NOT NULL IDENTITY")] public int Id;

Linq.book Page 598 Mercredi, 18. février 2009 7:58 07

Page 614: LINQ Language Integrated Query en C

Chapitre 18 Informations complémentaires sur SQL 599

[Column(IsDiscriminator = true, DbType = "NVarChar(2)")] public string ShapeCode; [Column(DbType = "Int")] public int StartingX;

[Column(DbType = "Int")] public int StartingY;}

public class Square : Shape{ [Column(DbType = "Int")] public int Width;}

public class Rectangle : Square{ [Column(DbType = "Int")] public int Length;}

Rien de nouveau dans cette classe : nous avons simplement inclus les classes introdui-tes dans les pages précédentes dans le [Your]DataContext TestDB et ajouté quelquesconstructeurs. Dans le Listing 18.3, nous allons mettre en place le code qui définira labase de données.

Listing 18.3 : Ce code définit la base de données exemple héritée de la classe d’entité.

TestDB db = new TestDB(@"Data Source=.\SQLEXPRESS;Initial Catalog=TestDB");db.CreateDatabase();

Ce code ne produit aucune sortie dans la console mais, si vous consultez le serveur debase de données, vous verrez qu’une base de données TestDB comprenant une tableShape a été créée. Si vous ouvrez la table Shape, vous verrez qu’elle ne contient aucunedonnée. Nous allons maintenant remplir cette table en utilisant quelques instructionsLINQ to SQL (voir Listing 18.4).

Listing 18.4 : Définition de données dans la table Shape.

TestDB db = new TestDB(@"Data Source=.\SQLEXPRESS;Initial Catalog=TestDB");

db.Shapes.InsertOnSubmit(new Square { Width = 4 });db.Shapes.InsertOnSubmit(new Rectangle { Width = 3, Length = 6 });db.Shapes.InsertOnSubmit(new Rectangle { Width = 11, Length = 5 });db.Shapes.InsertOnSubmit(new Square { Width = 6 });db.Shapes.InsertOnSubmit(new Rectangle { Width = 4, Length = 7 });db.Shapes.InsertOnSubmit(new Square { Width = 9 });

db.SubmitChanges();

Ce code définit le DataContext et les objets de la classe d’entité, puis il insère cesobjets dans la table Shapes. Les données sont sauvegardées dans la base de données en

Linq.book Page 599 Mercredi, 18. février 2009 7:58 07

Page 615: LINQ Language Integrated Query en C

600 LINQ to SQL Partie V

appelant la méthode SubmitChanges. Après avoir exécuté ce code, la table Shapecontient les données représentées dans le Tableau 18.1.

La colonne Id étant utilisée pour identifier les enregistrements, les valeurs changerontsi vous utilisez le code à plusieurs reprises.

Nous allons maintenant appliquer plusieurs requêtes à la table. Dans le Listing 18.5,nous allons faire une requête sur les carrés. Cette requête inclura les rectangles, puisqueles rectangles héritent des carrés. Enfin, nous effectuerons une autre requête qui neportera que sur les rectangles.

Listing 18.5 : Interrogation de la base de données.

TestDB db = new TestDB(@"Data Source=.\SQLEXPRESS;Initial Catalog=TestDB");

// Récupération des carrés (rectangles inclus)IQueryable<Shape> squares = from s in db.Shapes where s is Square select s;

Console.WriteLine("Carrés :");foreach (Shape s in squares){ Console.WriteLine("{0} : {1}", s.Id, s.ToString());}

// Récupération des rectangles (carrés exclus)IQueryable<Shape> rectangles = from r in db.Shapes where r is Rectangle select r;

Console.WriteLine("{0}Rectangles :", System.Environment.NewLine);foreach (Shape r in rectangles){ Console.WriteLine("{0} : {1}", r.Id, r.ToString());}

Ce code effectue deux requêtes identiques, à un petit détail près. Dans la première, lesenregistrements instanciés dans la classe Square (pour cause d’héritage, les enregistre-ments instanciés dans la classe Rectangle font également partie de la requête) sont

Tableau 18.1 : Le résultat du code précédent.

Id ShapeCode StartingX StartingY Length Width

1 S 0 0 NULL 4

2 R 0 0 6 3

3 R 0 0 5 11

4 S 0 0 NULL 6

5 R 0 0 7 4

6 S 0 0 NULL 9

Linq.book Page 600 Mercredi, 18. février 2009 7:58 07

Page 616: LINQ Language Integrated Query en C

Chapitre 18 Informations complémentaires sur SQL 601

interrogés. Dans la seconde requête, seuls les enregistrements instanciés dans la classeRectangle sont interrogés (ceux instanciés dans la classe Square sont exclus). Voici lesrésultats :

L’héritage des classes d’entité est un concept qui peut se révéler utile pour construireune hiérarchie d’entité associée à une base de données.

Transactions

Dans le chapitre précédent, vous avez appris que la méthode SubmitChanges s’exécuteau sein d’une transaction. Si une transaction n’est pas déjà ouverte, elle est créée defaçon à englober toutes les tentatives de modifications faites pendant l’invocation de laméthode SubmitChanges.

Ceci est très pratique, mais que faire si vous avez besoin d’une transaction qui doits’étendre au-delà de la portée de la méthode SubmitChanges ?

Nous allons donner un exemple pour montrer comment effectuer des mises à jour enutilisant plusieurs méthodes SubmitChanges à l’intérieur d’une même transaction.Encore plus fort : les appels aux méthodes SubmitChanges vont mettre à jour des basesde données différentes. Dans le Listing 18.6, nous effectuerons des modifications dansles bases de données Northwind et TestDB (créée dans les pages précédentes). Norma-lement, chaque appel à la méthode SubmitChanges devrait comprendre sa propre tran-saction. Mais, dans cet exemple, vous allez voir que les deux SubmitChanges font partiede la même transaction.

Le Listing 18.6 étant plus complexe que les précédents, nous donnerons des explications àchaque fois que cela sera nécessaire.

INFO

Cet exemple nécessite une référence à l’assembly System.Transactions.dll.

Carrés :1 : LINQChapter18.Square2 : LINQChapter18.Rectangle3 : LINQChapter18.Rectangle4 : LINQChapter18.Square5 : LINQChapter18.Rectangle6 : LINQChapter18.Square

Rectangles :2 : LINQChapter18.Rectangle3 : LINQChapter18.Rectangle5 : LINQChapter18.Rectangle

Linq.book Page 601 Mercredi, 18. février 2009 7:58 07

Page 617: LINQ Language Integrated Query en C

602 LINQ to SQL Partie V

Listing 18.6 : Rejoindre une transaction à portée.

Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind");TestDB testDb = new TestDB(@"Data Source=.\SQLEXPRESS;Initial Catalog=TestDB");

Customer cust = db.Customers.Where(c => c.CustomerID == "LONEP").SingleOrDefault();cust.ContactName = "Barbara Penczek";

Rectangle rect = (Rectangle)testDb.Shapes.Where(s => s.Id == 3).SingleOrDefault();rect.Width = 15;

Ce code définit le DataContext des deux bases de données, il effectue une requête surchacune d’entre elles, puis il modifie les objets entité ainsi obtenus.try{ using (System.Transactions.TransactionScope scope = new System.Transactions.TransactionScope()) { db.SubmitChanges(); testDb.SubmitChanges(); throw (new Exception("Annulation de la transaction.")); // Un avertissement sera émis car la ligne suivante ne peut pas être exécutée scope.Complete(); }}catch (Exception ex){ Console.WriteLine(ex.Message);}

INFO

Du code ayant été défini après la levée de l’exception, le compilateur indiquera que laméthode scope.Complete n’est jamais exécutée.

Ce code instancie un objet TransactionCode de façon à mettre en place une transac-tion pour "envelopper" les deux méthodes SubmitChanges. Après le second appel àla méthode SubmitChanges, une exception est intentionnellement déclenchée afinque la méthode scope.Complete ne soit pas appelée et que la transaction ne soit paseffectuée.

Si nous n’avions pas enveloppé les deux appels à la méthode SubmitChanges dans unobjet TransactionScope, chaque méthode SubmitChanges aurait créé sa propre tran-saction et les modifications auraient été reportées dans la base de données après chaqueSubmitChanges.

Lorsque l’exception est levée, les méthodes SubmitChanges ne sont plus dans la portéede la transaction. Étant donné que la méthode Complete n’a pas été appelée, les actionseffectuées dans la transaction sont annulées.

db.Refresh(System.Data.Linq.RefreshMode.OverwriteCurrentValues, cust);Console.WriteLine("Nom du contact = {0}", cust.ContactName);

Linq.book Page 602 Mercredi, 18. février 2009 7:58 07

Page 618: LINQ Language Integrated Query en C

Chapitre 18 Informations complémentaires sur SQL 603

testDb.Refresh(System.Data.Linq.RefreshMode.OverwriteCurrentValues, rect);

Console.WriteLine("Largeur du rectangle = {0}", rect.Width);

Les modifications n’ont pas été sauvegardées dans la base de données, mais les objetsentité contiennent toujours les valeurs modifiées. Vous vous rappelez certainement que,lorsqu’une méthode SubmitChanges ne s’exécute pas jusqu’au bout, les données modi-fiées sont conservées dans les objets entité afin que les conflits d’accès concurrentielspuissent être résolus et que la méthode SubmitChanges puisse être appelée une nouvellefois (voir Chapitre 17). De même, vous vous rappelez certainement qu’une nouvelleinterrogation de la base de données ne lit pas les valeurs stockées dans la base dedonnées (voir la section "Discordance des résultats dans le cache" du Chapitre 16). Larequête se contente de déterminer quelles entités devraient être incluses dans les résul-tats. Si ces entités font partie du cache du DataContext, elles seront retournées. Poursavoir quelles valeurs étaient stockées dans la base de données, les objets entité doiventêtre rafraîchis à l’aide de la méthode Refresh.

Ainsi donc, les deux objets entité récupérés de la base de données sont rafraîchis puisaffichés dans la console afin de prouver que la mise à jour n’a pas eu lieu dans la basede données. Voici les résultats :

ASTUCE

Si une exception du type "MSDTC on server ’[server]\SQLEXPRESS’ is unavailable" est levéelorsque vous travaillez avec un exemple qui utilise l’objet TransactionScope, assurez-vousque le service Distributed Transaction Coordinator est démarré.

Résumé

Dans ce chapitre, vous avez appris à appliquer des requêtes sur les vues d’une base dedonnées. Rappelez-vous qu’une vue n’est accessible qu’en lecture seule.

Nous avons ensuite parlé de l’héritage dans les classes d’entité. Cette technique permetà des enregistrements d’une table unique d’être instanciés dans des objets de classesdifférentes mais liés par héritage.

Enfin, nous nous sommes intéressés d’un peu plus près aux transactions et avonsmontré comment "envelopper" plusieurs mises à jour LINQ to SQL dans une mêmetransaction.

Vous avez pu en juger dans les derniers chapitres de cet ouvrage : LINQ to SQL, c’estde la dynamite ! Mais ne pensez pas que ce soit la seule partie de LINQ. Si vous avezsauté les premiers chapitres pour vous plonger directement dans LINQ to SQL, je vous

Annulation de la transaction.Nom du contact = Fran WilsonLargeur du rectangle = 11

Linq.book Page 603 Mercredi, 18. février 2009 7:58 07

Page 619: LINQ Language Integrated Query en C

604 LINQ to SQL Partie V

conseille de vous y reporter maintenant. Vous verrez : d’autres API de LINQ sont vrai-ment formidables ! Vous apprendrez entre autres à interroger des collections enmémoire et à transformer des données issues d’une collection d’un type donné en unautre type. Si vous écrivez du code XML, vous serez certainement émerveillé par lespossibilités de LINQ to XML.

Ce chapitre étant le dernier dédié à LINQ to SQL, mais également le dernier de cetouvrage, nous allons nous quitter avec un dernier exemple. J’ai souvent entendu la criti-que suivante à propos de LINQ, exprimée sous la forme d’une question : "Que peutfaire LINQ pour moi que je ne puisse déjà faire ?" Il est vrai qu’il existe de nombreusestechniques qui permettent de faire la plupart des tâches rendues simples par LINQ, maisn’oubliez pas que LINQ a apporté une réelle abstraction en matière d’interrogation dedonnées et qu’il a rassemblé les techniques d’interrogation utilisées dans des domainestrès divers.

Notre dernier exemple abondera dans ce cens : nous allons mixer des données issuesd’une base de données et des données XML… juste pour montrer que cet éclectisme estpossible.

Dans le Listing 18.7, nous définissons un objet XElement en parsant une chaîne. Lesdonnées XML sont utilisées pour mapper les abréviations des différents États des États-Unis à leur nom in extenso. Nous utilisons alors LINQ to SQL pour interroger les clientsqui résident aux États-Unis. La méthode AsEnumerable est appelée sur la séquenceretournée, afin de pouvoir exécuter le reste de la requête localement, et non dans la basede données. Ceci est nécessaire, car le reste de la requête ne peut pas être traduit en SQL.Les résultats de la requête sont alors joints aux données XML créées en comparant lechamp Region du client à l’attribut ID des éléments XML. En mixant ces deux sources dedonnées, nous pouvons obtenir le nom complet de l’État spécifié dans le champ Region.Les résultats de la jointure sont alors projetés dans un objet de type anonyme qui contientl’objet entité Customer et la description de l’État issue des données XML. Ensuite, nousénumérons les résultats et affichons le nom de la société (CompanyName), le nom de l’État(Region) et l’intitulé de l’État issu des données XML.

Nous venons de réaliser une jointure entre des données provenant d’une base dedonnées et d’autres données au format XML. N’est-ce pas remarquable ? Faisiez-vous partie des personnes qui déclaraient que LINQ ne pouvait rien apporter denouveau ?

Voici les résultats retournés par la jointure :

Client = Great Lakes Food Market : OR : OregonClient = Hungry Coyote Import Store : OR : OregonClient = Lazy K Kountry Store : WA : WashingtonClient = Let’s Stop N Shop : CA : CaliforniaClient = Lonesome Pine Restaurant : OR : OregonClient = Old World Delicatessen : AK : Alaska

Linq.book Page 604 Mercredi, 18. février 2009 7:58 07

Page 620: LINQ Language Integrated Query en C

Chapitre 18 Informations complémentaires sur SQL 605

Comme vous pouvez le voir, nous avons effectivement pu rapprocher les enregistre-ments sélectionnés par la requête LINQ to SQL des données XML correspondantespour obtenir l’intitulé de chaque État. Comment auriez-vous fait si vous n’aviez euqu’ADO.NET et le W3C XML DOM à votre disposition ?

Pour terminer en beauté, voici le code responsable de cette jointure.

Listing 18.7 : Une requête qui réalise une jointure entre une base de données et des données XML.

string statesXml = @"<States> <State ID=""OR"" Description=""Oregon"" /> <State ID=""WA"" Description=""Washington"" /> <State ID=""CA"" Description=""California"" /> <State ID=""AK"" Description=""Alaska"" /> <State ID=""NM"" Description=""New Mexico"" /> <State ID=""ID"" Description=""Idaho"" /> <State ID=""MT"" Description=""Montana"" /> </States>";

XElement states = XElement.Parse(statesXml);

Northwind db = new Northwind(@"Data Source=.\SQLEXPRESS;Initial Catalog=Northwind");

var custs = (from c in db.Customers where c.Country == "USA" select c).AsEnumerable(). Join(states.Elements("State"), c => c.Region, s => (string) s.Attribute("ID"), (c, s) => new { Customer = c, State = (string)s.Attribute("Description") });

foreach (var cust in custs){ Console.WriteLine("Customer = {0} : {1} : {2}", cust.Customer.CompanyName, cust.Customer.Region, cust.State);}

Client = Rattlesnake Canyon Grocery : NM : New MexicoClient = Save-a-lot Markets : ID : IdahoClient = The Big Cheese : OR : OregonClient = The Cracker Box : MT : MontanaClient = Trail’s Head Gourmet Provisioners : WA : WashingtonClient = White Clover Markets : WA : Washington

Linq.book Page 605 Mercredi, 18. février 2009 7:58 07

Page 621: LINQ Language Integrated Query en C

Linq.book Page 606 Mercredi, 18. février 2009 7:58 07

Page 622: LINQ Language Integrated Query en C

Index

A

Accès aux annotations 258

ADO.NET 333, 387, 533

Aggregate 176

Ajoutannotations 258nœuds 238

All 162

Ancêtres d’un nœud 231, 232

Annotationsaccès 258ajout 258exemples 259XML 258

ANSI Master XI

Any 160

API 378

API W3C DOM XML 185

Application Programming Interface Voir API

Arbresd’expressions 25XML 190

AsEnumerable 119, 359

ASP.NET 185

Association 483ajouter 408

Associations 382, 425

Attach 588

Attribut Database 478

Attributes 228

Attributs XML 250création 250déplacements dans un attribut 250dernier 252précédent 251premier 250suivant 251

autoResolveDeletes 580

AutoSync (AutoSync enum) 481

Average 174

B

Bases de donnéesassociations 425chargement différé 428connexion du DataContext 403enregistrements liés 421insertions 418

objets entité liés 421jointures 437requêtes 423vues 593

Bogue d’Halloween 267

Boxing 347, 351

C

C# 3.0, nouveautés 19

Cache, discordance des résultats 514

CanBeNull (bool) 481

Cast 11, 14, 115exemple 14

Casting 348

Linq.book Page 607 Mercredi, 18. février 2009 7:58 07

Page 623: LINQ Language Integrated Query en C

608 Index

Champs 347

ChangeConflictException 383, 577

ChangeConflicts 532, 578, 580

Changed 475

Changing 475

Chargementdifféré 428immédiat 430

EntitySets 432hiérarchie de classes associées 434

Classe DataContext 380, 509, 510

Classe DataLoadOptions 430

Classes associées 432d’entité 381, 494

ajouter 408attributs 477écriture manuelle 463enfant 426génération 462héritage 595parent 426responsabilités annexes 463

de non-entité 494

Clausefrom 47Group 44group 49into 44join 45Let 46OrderBy 48Select 44, 48using 6Where 46where 4, 6

Clé étrangère 382primaire 382

Code (object) 492

Cohérence du graphe 468

Collections héritées 14

Column 480

CommandTimeout 557

Concat 83

Concepteur 402ajouter une classe d’entité 404classes LINQ to SQL 402d’objets relationnels 6Objet/Relationnel 401, 594

surcharge 457utiliser 414

surcharge 457

ConflictMode 535

Conflits d’accès concurrentiels 383, 571middle-tier 588résolution 383serveurs 588

Connexion à une base de données 522, 529, 530ADO.NET partagée 523, 530du DataContext 403

Constructionfonctionnelle 190, 306paramétrée 496

Contains 163, 445

ContinueOnConflict 535

Contrôle d’accès concurrentieloptimiste 572pessimiste 585

CopyToDataTable 360

Count 165

CreateDatabase() 540

CreateMethodCallQuery() 542

Créationd’attributs 205d’éléments 202

streaming 213d’espaces de noms 211d’instructions de traitement 211de commentaires 206de conteneurs 207de déclarations 207

Linq.book Page 608 Mercredi, 18. février 2009 7:58 07

Page 624: LINQ Language Integrated Query en C

Index 609

de documents 209de nœuds 211de noms 210de textes 215de types de documents 208XML 202

D

Database 478

DatabaseExists() 539

DataColumn 333

DataContext 17, 380, 381, 403, 417, 441, 461, 509, 510, 520, 560, 594

DataContext.ChangeConflicts.ResolveAll() 578

DataContext.Log 392

DataLoadOptions 430

DataRow 333, 336Distinct 336Except 340Intersect 342Union 344

DataSet 367typé requête 367

DataSets 8typés 367, 369

DataTable 333, 359

DbDataReader 546

DBML 399, 400schéma de fichier intermédiaire 401

DbType (string) 482, 490

Déclaration SQLIN 445

DefaultIfEmpty 122

Délégué Func 58

Delete 388surcharger 409, 454

DeleteDatabase() 541

DeleteOnNull (bool) 485

DeleteOnSubmit 450

DeleteRule (string) 485

Déplacements XML 221propriétés 222

Descendants d’un nœud 233

Détection de conflit 572

Discordance des résultats dans le cache 514

Distinct 110, 334, 336

Document XSD 316

E

ElementAt 158

ElementAtOrDefault 159

Éléments frèresprécédents 237suivants 235

elementSelector 137

Empty 128

Entity 502

EntityRef 426, 502problèmes relatifs à 475

EntitySet 382, 425, 502problèmes relatifs à 476

EntitySets 432

Enumerable 34

Événements XML 262exemples 263

Except 114, 340

Exception ChangeConflictException 383

ExecuteCommand 510, 547

ExecuteMethodCall() 549

ExecuteQuery 510, 543

ExecuteStatementInDb() 388

Explorateur de serveurs 403ajouter une classe d’entité 404

Linq.book Page 609 Mercredi, 18. février 2009 7:58 07

Page 625: LINQ Language Integrated Query en C

610 Index

Expressionsde requête 39

étapes de la traduction 43grammaire 41traduction 42

lambda 20filtre 24utilisation 22

string 482

Extension des classes d’entité 499

F

FailOnFirstConflict 537

Fichierde mappage 399, 529, 530

externe, schéma 493intermédiaire DBML 399XML

lecture 218sauvegarde 216

Field 351, 368

First 148

FirstOrDefault 150

Fonction définie par l’utilisateur, ajouter 409

foreach 39

from 47

Func 58

Function 487

G

Gestionnaire d’événements XObject 264

GetChangeSet() 393, 558

GetCommand() 557

GetParameterValue 506

GetResult 507

GetStringFromDb() 387

GetTable 513, 560

Graphe, cohérence 468

group 43, 44,49

GroupBy 44, 104

GroupJoin 102

H

HasLoadedOrAssignedValue 503

Héritage 595

I

Identificateurs transparents 42

IEnumerable 7, 12, 14, 16, 26, 54, 131

IEnumerable.Remove() 243, 255

IExecuteResult 505

IMultipleResults 506

IN 445

InheritanceMapping 490

Initialisationd’objets 26, 29

vs construction paramétrée 496de collections 30

INotifyPropertyChanged 464

INotifyPropertyChanging 464

Insert 388surcharger 409, 453

Insertions 418

Intermediate Language 25

Interrogation d’une classe 501

Intersect 112, 342

into 43

IQueryable 386, 424

IsComposable (bool) 489

IsDbGenerated (bool) 482

IsDefault (bool) 492

IsDiscriminator (bool) 482

Linq.book Page 610 Mercredi, 18. février 2009 7:58 07

Page 626: LINQ Language Integrated Query en C

Index 611

IsForeignKey (bool) 485

ISingleResult 506

IsPrimaryKey (bool) 482

IsUnique (bool) 485

IsVersion 572

IsVersion (bool) 483

J

join 45, 101, 438

Jointure 349, 437entre une base de données et des données

XML 605externes 438internes 438

L

Last 152

LastOrDefault 153

Let 46

LINQ 4forum 18hello 3to DataSet 8, 333, 367

code commun 334espaces de noms référencés 334, 367référence des assemblies 334

to Entities 8to Objects 8, 51

vue d’ensemble 53to SQL 8, 375

astuces et outils 391classes d’entité 461informations complémentaires 593introduction 378requête intraduisible 458traduction 457utilisation de l’API 386

to XML 4, 8, 181, 189, 269améliorations de l’API 190bogue d’Halloween 200

espaces de noms référencés 189exécution différée des requêtes 200extraction de valeurs de nœuds 196modèle d’objet 199noms, espaces de noms et préfixes 194opérateurs 269suppression de nœuds 200

Linqdev 528

linqdev XI

Log du DataContext 17

LongCount 166

M

Max 172

MemberChangeConflict.Resolve() 582

Méthodesanonymes

utilisation 21d’extension 31, 33

déclaration 35invocations 35précédence 36

d’instance 32de mise à jour

surcharger 453Helper 308nommées 20partielles 37, 394, 474, 499, 501

exemple 37pourquoi les utiliser 38

statiques 32

Middle-tier 588

Min 170

Mises à jour 446d’une référence

enfant 449parent 447

de nœuds 245

Modèle de classe d’entitéajout d’objets 408édition 407

Linq.book Page 611 Mercredi, 18. février 2009 7:58 07

Page 627: LINQ Language Integrated Query en C

612 Index

Modificationsde données XML 238de la valeur des attributs 256exécution 519traçage 518

Mot-clé, var 12

N

Name (string) 480, 483, 485, 489, 490

Nœudsenfants 225, 229frères

précédents 236suivants 234

multiples 311

Northwindclasses d’entité 384fichier de mappage 385version étendue 384

Notifications de changement 464

O

ObjectChangeConflict 578

ObjectChangeConflict.Resolve() 580, 581

Object-relational impedance mismatch 513

Object-Relational Mapping 379

Objetentité attaché, suppression 451Log du DataContext 17XElement 5XMLDocument 5

Objet CData 215

OfType 14, 117, 227exemple 14vs Cast 15

OnLoaded 475

OnValidate 475

OpérateursCast 14

Contains 445de requête standard 11, 54, 59différés 63

AsEnumerable 119assemblies référencés 64Cast 115classes communes 64Concat 83DefaultIfEmpty 122Distinct 110Empty 128espaces de noms référencés 63Except 114GroupBy 104GroupJoin 102Intersect 112Join 101OfType 117OrderBy 86OrderByDescending 90par groupes fonctionnels 65Range 126Repeat 127Reverse 100, 121Select 67SelectMany 72Skip 80SkipWhile 81Take 76TakeWhile 78ThenBy 93ThenByDescending 97Union 111Where 65

join 438LINQ to XML 269

Ancestors 270AncestorsAndSelf 274Attributes 277DescendantNodes 279DescendantNodesAndSelf 280Descendants 282DescendantsAndSelf 284Elements 287InDocumentOrder 289Nodes 290

Linq.book Page 612 Mercredi, 18. février 2009 7:58 07

Page 628: LINQ Language Integrated Query en C

Index 613

Remove 292non différés 131

Aggregate 176All 162Any 160Average 174classes communes 131Contains 163Count 165ElementAt 158ElementAtOrDefault 159espaces de noms référencés 131First 148FirstOrDefault 150Last 152LastOrDefault 153LongCount 166Max 172Min 170par groupes fonctionnels 134SequenceEqual 145Single 155SingleOrDefault 156Sum 168ToArray 134ToDictionary 137ToList 136ToLookup 141

OfType 227pour DataRow

Distinct 336Except 340Intersect 342Union 344

pour DataTable 359AsEnumerable 359CopyToDataTable 360

pour les champs 347Field 351SetField 356

Opérations, standard de bases de données 418

OrderBy 25, 48, 86

orderby 43

OrderByDescending 90

OrderedSequence 131

ORM 379

OtherKey (string) 486

P

Paradigme, changement 3

Parameter 490

partiel 39

Persistance 441

Premier nœud enfant 230

private 39

Procédure stockée, ajouter 409

Projection 494construction paramétrée 497, 498, 499initialisation d’objet 496

On 475

R

Range 126

Rattz, Joseph C. XI

Recherche d’identité 513

Refresh() 562

RefreshMode 577, 578, 580

RefreshMode.KeepChanges 583, 584

Rejoindre une transaction à portée 602

Relation, suppression 452

Repeat 127

Requêtes 423création par programmation 443différées 15, 442

avantages 16, 443conséquences 442mises à jour 446suppressions 450

erreur à l’énumération 16expressions de requête 40intraduisibles 458LINQ to SQL 5, 423

Linq.book Page 613 Mercredi, 18. février 2009 7:58 07

Page 629: LINQ Language Integrated Query en C

614 Index

Requêtes (suite)IQueryable 424table 425

notation à point 40SQL 3

Résolution des conflits 577, 578automatique 578manuelle 583

Resolve 577

ResolveAll 532, 577, 578

Résultats persistants 441

ResultType 489

return 489

ReturnValue 505, 506, 507

Reverse 100, 121

S

Schéma 493XML 315

Select 30, 44, 48, 67

SelectMany 30, 72

SendPropertyChanged 466

SequenceEqual 145

SetField 356, 368

Simplifier des tâches complexes 308

Single 155

SingleOrDefault 156

Skip 80

SkipWhile 81

SQL 3

SQL Server 5

SqlDataAdapter 370

SQLMetal 6, 17, 394, 477, 594/dbml 399/map 399options 396utiliser 414

static 39

Storage (string) 483, 486

Structure aplatie 311, 440

SubmitChanges 383, 450, 510, 532, 534

Sum 168

Suppression 450d’annotations 258d’attributs 254de nœuds 242

à la construction 309

System.Collection 14

System.Data.Linqclasses importantes 501Entity 502EntityRef 502EntitySet 502GetParameterValue 506HasLoadedOrAssignedValue 503IExecuteResult 505IMultipleResults 506ISingleResult 506ReturnValue 505Table 504

System.Data.Linq.dll 8

System.Linq.Enumerable 8, 25

System.Linq.Queryable 26

System.Xml.Linq.dll 8

T

Table 425, 450, 480, 504, 594

Take 76

TakeWhile 78

The Gallows XI

ThenBy 93

ThenByDescending 97

ThisKey (string) 486

ToArray 57, 134

ToDictionary 57, 137

ToList 57, 136

ToLookup 57, 141

Linq.book Page 614 Mercredi, 18. février 2009 7:58 07

Page 630: LINQ Language Integrated Query en C

Index 615

Traçage des modifications 518

Traduction SQL 457

Transactions 601

TransactionScope 585

TransactionScopeOption 585

Transformation 306document XML

helper 308XML 303

XSLT 304

Translate() 546

Typesanonymes 26, 30de donnée

compatibilité 493

U

Unboxing 347

Union 111, 344

Update 388

update, surcharger 409

UpdateCheck 573

UpdateCheck (UpdateCheck enum) 483

V

Validation XML 314méthodes d’extension 314

var 12, 26code 13déclaration invalide 27implicitement typé 27type anonyme 28

Variables d’énumérationtypes explicites 44

void 39

Vues 593insertion d’un enregistrement 595interrogation 594

W

W3C DOM XML 185

Where 25, 33, 46, 65

where 43

X

XAttribute 205

XAttribute.NextAttribute 251

XAttribute.PreviousAttribute 251

XAttribute.Remove() 254

XCData 215

XComment 206

XComment.Value 245

XContainer 207

XContainer.Add() 239

XContainer.AddFirst() 239

XContainer.Descendants() 233

XContainer.Element() 230

XContainer.Elements 269

XContainer.Elements() 229

XContainer.Nodes() 225

XDeclaration 207

XDocument 209

XDocument.Load() 218

XDocument.Parse() 220

XDocument.Save() 216

XDocumentType 208

XDocumentType.InternalSubset 246

XDocumentType.Name 246

XDocumentType.PublicId 246

XDocumentType.SystemId 246

XElement 202, 248

XElement.AncestorsAndSelf() 232

XElement.Attribute() 252

XElement.Attributes() 253

Linq.book Page 615 Mercredi, 18. février 2009 7:58 07

Page 631: LINQ Language Integrated Query en C

616 Index

XElement.DescendantsAndSelf() 233

XElement.FirstAttribute 250

XElement.LastAttribute 252

XElement.Load() 219

XElement.Parse() 220

XElement.RemoveAll() 244

XElement.ReplaceAll() 247

XElement.Save 217

XElement.SetAttributeValue() 257

XElement.SetElementValue() 248

XElement.Value 245

XML 4, 295élément 192espaces de noms référencés 295requêtes 296

complexes 298

XName 210

XNamespace 211

XNode 211

XNode.AddAfterSelf() 241

XNode.AddBeforeSelf() 240

XNode.Ancestors() 231

XNode.ElementsAfterSelf() 235

XNode.ElementsBeforeSelf() 237

XNode.NodesAfterSelf() 234

XNode.NodesBeforeSelf() 236

XNode.Remove() 242

XObject.AddAnnotation() 258

XObject.Annotation() 258

XObject.Annotations() 258

XObject.Changed 262

XObject.Changing 262

XObject.Document 223

XObject.Parent 224

XObject.RemoveAnnotations() 258

XPath 328

XProcessingInstruction 211, 247

XProcessingInstruction Objects 247

XProcessingInstruction.Data 247

XProcessingInstruction.Target 247

XSLT 304

XStreamingElement 213

XText 215

XText.Value 245

Y

Yield 55

Linq.book Page 616 Mercredi, 18. février 2009 7:58 07

Page 632: LINQ Language Integrated Query en C

LINQ, composant du framework .NET de Microsoft,est un moteur de requêtage universel qui a révolutionné l’appel de données, en permettant l’interrogationde n’importe quel type d’objets inhérents aux différents langages .NET (C#, Visual Basic, etc.). Bibliothèque d’exemples fondamentaux écrits en LINQ,cet ouvrage est une véritable mine d’or pour le programmeur. L’auteur s’est efforcé de couvrirtous les domaines de LINQ, afi n de montrer l’immense étendue des opérateurs et des prototypes de ce langage. LINQ to Objects, LINQ to XML, LINQ to DataSet, LINQ to SQL : chaque partie offre du code prêt à l’emploi pour effectuer des requêtes, fi ltrer et projeter des données dans des collections, des classes énumérables, des structures XML, etc. Ces techniques acquises, vous pourrez utiliser toutes les fonctionnalités de LINQ et récolter le maximum de votre investissement.Résolument pratique, l’ouvrage n’aborde que les éléments réellement utiles au programmeur. Les exemples sont directement exploitables, et permettent de mettre à jour les points les plus ardus. Ainsi, pour montrer comment gérer les confl its d’accès concurrentiels, l’exemple en génère justement un. Vous pouvez ainsi tracer le code et comprendre tous les mécanismes mis en œuvre.Ce livre requiert les connaissances de base en C#, mais pas davantage : en effet, l’auteur prend soin d’expliquer toutes les notions les plus complexes afi n d’être accessibles aux lecteurs les moins exercés.

Codes sources téléchargeables sur www.pearson.fr !

Référ

ence

ISBN : 978-2-7440-4106-8

Niveau : Intermédiaire/avancéProgrammation

Pearson Education FrancePearson Education France47 bis, rue des Vinaigriers47 bis, rue des Vinaigriers75010 Paris75010 ParisTél. : 01 72 74 90 00Tél. : 01 72 74 90 00Fax : 01 42 05 22 17Fax : 01 42 05 22 17www.pearson.frwww.pearson.fr

LINQ Language Integrated Query en C# 2008

À propos de l’auteur : Joseph C.Ratz Jr. est expert en développement d’interfaces utilisateurs et de programmes exécutés côté serveur. Il a travaillé en tant que développeur pour le compte de grandes entreprises (IBM, Policy Management Systems,SCT, DocuCorp,CheckFree,Delta Technology, etc).Retrouvez-le sur son sitewww.linqdev.com.

TABLE DES MATIÈRES

• Hello LINQ• Améliorations de C# 3.0 pour LINQ• Introduction à LINQ to Objects• Les opérateurs différés• Les opérateurs non différés• Introduction à LINQ to XML• L’API LINQ to XML• Les opérateurs LINQ to XML• Les autres possibilités de XML• LINQ to DataSet• Possibilités complémentaires des

DataSet• Introduction à LINQ to SQL• Astuces et outils pour LINQ to SQL• Opérations standard

sur les bases de données• Les classes d’entité LINQ to SQL• La classe DataContext• Les confl its d’accès concurrentiels• Informations complémentaires

sur SQL