Partager via


LINQ : requête Language-Integrated .NET

 

Don Box, Anders Hejlsberg

Février 2007

S’applique à :
   Nom de code Visual Studio « Orcas »
   .NET Framework 3.5

Résumé: Les fonctionnalités de requête à usage général ajoutées au .NET Framework s’appliquent à toutes les sources d’informations, pas seulement aux données relationnelles ou XML. Cette fonctionnalité est appelée .NET Language-Integrated Query (LINQ). (32 pages imprimées)

Contenu

Requête Language-Integrated .NET
Prise en main avec des opérateurs de requête standard
Fonctionnalités de langage prenant en charge le projet LINQ
Autres opérateurs de requête standard
Syntaxe de la requête
LINQ to SQL : Intégration SQL
LINQ to XML : Intégration XML
Résumé

Requête Language-Integrated .NET

Après deux décennies, l’industrie a atteint un point stable dans l’évolution des technologies de programmation orientées objet (OO). Les programmeurs prennent désormais pour acquis des fonctionnalités telles que les classes, les objets et les méthodes. En examinant les technologies actuelles et de la prochaine génération, il est devenu évident que le prochain grand défi de la technologie de programmation consiste à réduire la complexité de l’accès et de l’intégration de l’information qui n’est pas définie en mode natif à l’aide de la technologie OO. Les deux sources les plus courantes d’informations non oo sont les bases de données relationnelles et LE XML.

Au lieu d’ajouter des fonctionnalités relationnelles ou propres à XML à nos langages de programmation et à notre runtime, avec le projet LINQ, nous avons adopté une approche plus générale et nous ajoutons des fonctionnalités de requête à usage général au .NET Framework qui s’appliquent à toutes les sources d’informations, pas seulement aux données relationnelles ou XML. Cette fonctionnalité est appelée .NET Language-Integrated Query (LINQ).

Nous utilisons le terme requête intégrée au langage pour indiquer que la requête est une fonctionnalité intégrée des principaux langages de programmation du développeur (par exemple, Visual C#, Visual Basic). La requête intégrée au langage permet aux expressions de requête de tirer parti des métadonnées enrichies, de la vérification de la syntaxe au moment de la compilation, de la saisie statique et d’IntelliSense qui étaient auparavant disponibles uniquement pour le code impératif. La requête intégrée au langage permet également d’appliquer une seule fonctionnalité de requête déclarative à usage général à toutes les informations en mémoire, et pas seulement aux informations provenant de sources externes.

.NET Language-Integrated Query définit un ensemble d’opérateurs de requête standard à usage général qui permettent aux opérations de traversée, de filtre et de projection d’être exprimées de manière directe mais déclarative dans n’importe quel . Langage de programmation basé sur NET. Les opérateurs de requête standard permettent d’appliquer des requêtes à n’importe quelle source d’informations basée sur IEnumerable<T>. LINQ permet à des tiers d’augmenter l’ensemble d’opérateurs de requête standard avec de nouveaux opérateurs spécifiques au domaine qui conviennent au domaine ou à la technologie cible. Plus important encore, les tiers sont également libres de remplacer les opérateurs de requête standard par leurs propres implémentations qui fournissent des services supplémentaires tels que l’évaluation à distance, la traduction de requêtes, l’optimisation, etc. En adhérant aux conventions du modèle LINQ, ces implémentations bénéficient de la même intégration du langage et de la même prise en charge des outils que les opérateurs de requête standard.

L’extensibilité de l’architecture de requête est utilisée dans le projet LINQ lui-même pour fournir des implémentations qui fonctionnent sur des données XML et SQL. Les opérateurs de requête sur XML (LINQ to XML) utilisent une fonctionnalité XML en mémoire efficace, facile à utiliser pour fournir des fonctionnalités XPath/XQuery dans le langage de programmation hôte. Les opérateurs de requête sur les données relationnelles (LINQ to SQL) s’appuient sur l’intégration de définitions de schéma SQL dans le système de type CLR (Common Language Runtime). Cette intégration fournit un typage fort sur les données relationnelles tout en conservant la puissance expressive du modèle relationnel et les performances de l’évaluation des requêtes directement dans le magasin sous-jacent.

Prise en main avec des opérateurs de requête standard

Pour voir la requête intégrée au langage au travail, nous allons commencer par un programme C# 3.0 simple qui utilise les opérateurs de requête standard pour traiter le contenu d’un tableau :

using System;
using System.Linq;
using System.Collections.Generic;

class app {
  static void Main() {
    string[] names = { "Burke", "Connor", "Frank", 
                       "Everett", "Albert", "George", 
                       "Harris", "David" };

    IEnumerable<string> query = from s in names 
                               where s.Length == 5
                               orderby s
                               select s.ToUpper();

    foreach (string item in query)
      Console.WriteLine(item);
  }
}

Si vous deviez compiler et exécuter ce programme, vous verriez ceci en tant que sortie :

BURKE
DAVID
FRANK
To understand how language-integrated query works, we need to dissect the
 first statement of our program.
IEnumerable<string> query = from s in names 
                           where s.Length == 5
                           orderby s
                           select s.ToUpper();

La requête de variable locale est initialisée avec une expression de requête. Une expression de requête fonctionne sur une ou plusieurs sources d’informations en appliquant un ou plusieurs opérateurs de requête à partir des opérateurs de requête standard ou des opérateurs spécifiques au domaine. Cette expression utilise trois des opérateurs de requête standard : Where, OrderBy et Select.

Visual Basic 9.0 prend également en charge LINQ. Voici l’instruction précédente écrite en Visual Basic 9.0 :

Dim query As IEnumerable(Of String) = From s in names _
                                     Where s.Length = 5 _
                   Order By s _
                   Select s.ToUpper()

Les instructions C# et Visual Basic présentées ici utilisent des expressions de requête. Comme l’instruction foreach , les expressions de requête sont un raccourci déclaratif pratique sur le code que vous pouvez écrire manuellement. Les instructions ci-dessus sont sémantiquement identiques à la syntaxe explicite suivante illustrée en C#:

IEnumerable<string> query = names 
                            .Where(s => s.Length == 5) 
                            .OrderBy(s => s)
                            .Select(s => s.ToUpper());

Cette forme de requête est appelée requête basée sur une méthode . Les arguments des opérateurs Where, OrderBy et Select sont appelés expressions lambda, qui sont des fragments de code tout comme des délégués. Ils permettent aux opérateurs de requête standard d’être définis individuellement en tant que méthodes et de les assembler à l’aide de la notation par points. Ensemble, ces méthodes constituent la base d’un langage de requête extensible.

Fonctionnalités de langage prenant en charge le projet LINQ

LINQ s’appuie entièrement sur des fonctionnalités de langage à usage général, dont certaines sont nouvelles pour C# 3.0 et Visual Basic 9.0. Chacune de ces fonctionnalités a un utilitaire en soi, mais collectivement, ces fonctionnalités fournissent un moyen extensible de définir des requêtes et des API interrogeables. Dans cette section, nous explorons ces fonctionnalités de langage et la façon dont elles contribuent à un style de requêtes beaucoup plus direct et déclaratif.

Expressions lambda et arborescences d’expressions

De nombreux opérateurs de requête permettent à l’utilisateur de fournir une fonction qui effectue le filtrage, la projection ou l’extraction de clé. Les fonctionnalités de requête s’appuient sur le concept d’expressions lambda, qui fournissent aux développeurs un moyen pratique d’écrire des fonctions qui peuvent être transmises en tant qu’arguments pour une évaluation ultérieure. Les expressions lambda sont similaires aux délégués CLR et doivent respecter une signature de méthode définie par un type de délégué. Pour illustrer cela, nous pouvons développer l’instruction ci-dessus dans une forme équivalente mais plus explicite à l’aide du type de délégué Func :

Func<string, bool>   filter  = s => s.Length == 5;
Func<string, string> extract = s => s;
Func<string, string> project = s => s.ToUpper();

IEnumerable<string> query = names.Where(filter) 
                                 .OrderBy(extract)
                                 .Select(project);

Les expressions lambda sont l’évolution naturelle des méthodes anonymes en C# 2.0. Par exemple, nous aurions pu écrire l’exemple précédent à l’aide de méthodes anonymes comme suit :

Func<string, bool>   filter  = delegate (string s) {
                                   return s.Length == 5; 
                               };

Func<string, string> extract = delegate (string s) { 
                                   return s; 
                               };

Func<string, string> project = delegate (string s) {
                                   return s.ToUpper(); 
                               };

IEnumerable<string> query = names.Where(filter) 
                                 .OrderBy(extract)
                                 .Select(project);

En général, le développeur est libre d’utiliser des méthodes nommées, des méthodes anonymes ou des expressions lambda avec des opérateurs de requête. Les expressions lambda ont l’avantage de fournir la syntaxe la plus directe et la plus compacte pour la création. Plus important encore, les expressions lambda peuvent être compilées sous forme de code ou de données, ce qui permet aux expressions lambda d’être traitées au moment de l’exécution par des optimiseurs, des traducteurs et des évaluateurs.

L’espace de noms System.Linq.Expressions définit un type générique distingué, Expression<T>, qui indique qu’une arborescence d’expressions est souhaitée pour une expression lambda donnée plutôt que pour un corps de méthode classique basé sur IL. Les arborescences d’expressions sont des représentations de données en mémoire efficaces des expressions lambda et rendent la structure de l’expression transparente et explicite.

La détermination de la façon dont l’expression lambda est utilisée pour déterminer si le compilateur émettra une arborescence d’expressions exécutable ou une arborescence d’expressions. Lorsqu’une expression lambda est affectée à une variable, un champ ou un paramètre dont le type est un délégué, le compilateur émet un il identique à celui d’une méthode anonyme. Lorsqu’une expression lambda est affectée à une variable, un champ ou un paramètre dont le type est Expression<T> pour un type délégué T, le compilateur émet une arborescence d’expressions à la place.

Par exemple, considérez les deux déclarations de variables suivantes :

Func<int, bool>             f = n => n < 5;
Expression<Func<int, bool>> e = n => n < 5;

La variable f est une référence à un délégué qui est directement exécutable :

bool isSmall = f(2); // isSmall is now true

La variable e est une référence à une arborescence d’expressions qui n’est pas directement exécutable :

bool isSmall = e(2); // compile error, expressions == data

Contrairement aux délégués, qui sont en fait du code opaque, nous pouvons interagir avec l’arborescence des expressions comme n’importe quelle autre structure de données de notre programme.

Expression<Func<int, bool>> filter = n => n < 5;

BinaryExpression    body  = (BinaryExpression)filter.Body;
ParameterExpression left  = (ParameterExpression)body.Left;
ConstantExpression  right = (ConstantExpression)body.Right;

Console.WriteLine("{0} {1} {2}", 
                  left.Name, body.NodeType, right.Value);

L’exemple ci-dessus décompose l’arborescence d’expressions au moment de l’exécution et imprime la chaîne suivante :

n LessThan 5

Cette capacité à traiter des expressions comme des données au moment de l’exécution est essentielle pour permettre un écosystème de bibliothèques tierces qui tirent parti des abstractions de requête de base qui font partie de la plateforme. L’implémentation de l’accès aux données LINQ to SQL tire parti de cette fonctionnalité pour traduire les arborescences d’expressions en instructions T-SQL appropriées pour l’évaluation dans le magasin.

Méthodes d’extension

Les expressions lambda sont un élément important de l’architecture de requête. Les méthodes d’extension en sont une autre. Les méthodes d’extension combinent la flexibilité du « typage canard » rendu populaire dans les langages dynamiques avec les performances et la validation au moment de la compilation des langages typés statiquement. Avec les méthodes d’extension, des tiers peuvent augmenter le contrat public d’un type avec de nouvelles méthodes tout en permettant aux auteurs de types individuels de fournir leur propre implémentation spécialisée de ces méthodes.

Les méthodes d’extension sont définies dans les classes statiques en tant que méthodes statiques, mais sont marquées avec l’attribut [System.Runtime.CompilerServices.Extension] dans les métadonnées CLR. Les langages sont encouragés à fournir une syntaxe directe pour les méthodes d’extension. En C#, les méthodes d’extension sont indiquées par le modificateur qui doit être appliqué au premier paramètre de la méthode d’extension. Examinons la définition de l’opérateur de requête le plus simple, :

namespace System.Linq {
  using System;
  using System.Collections.Generic;

  public static class Enumerable {
    public static IEnumerable<T> Where<T>(
             this IEnumerable<T> source,
             Func<T, bool> predicate) {

      foreach (T item in source)
        if (predicate(item))
          yield return item;
    }
  }
}

Le type du premier paramètre d’une méthode d’extension indique le type auquel l’extension s’applique. Dans l’exemple ci-dessus, la méthode d’extension Where étend le type IEnumerable<T>. Étant donné que Where est une méthode statique, nous pouvons l’appeler directement comme n’importe quelle autre méthode statique :

IEnumerable<string> query = Enumerable.Where(names, 
                                          s => s.Length < 6);

Toutefois, ce qui rend les méthodes d’extension uniques, c’est qu’elles peuvent également être appelées à l’aide de instance syntaxe :

IEnumerable<string> query = names.Where(s => s.Length < 6);

Les méthodes d’extension sont résolues au moment de la compilation en fonction de l’étendue des méthodes d’extension. Lorsqu’un espace de noms est importé avec une instruction using en C# ou une instruction Import en Visual Basic, toutes les méthodes d’extension définies par les classes statiques de cet espace de noms sont introduites dans l’étendue.

Les opérateurs de requête standard sont définis en tant que méthodes d’extension dans le type System.Linq.Enumerable. Lorsque vous examinez les opérateurs de requête standard, vous remarquerez que tous, sauf quelques-uns, sont définis en termes d’interface IEnumerable<T> . Cela signifie que chaque source d’informations compatible T> IEnumerable< obtient les opérateurs de requête standard simplement en ajoutant l’instruction using suivante en C# :

using System.Linq; // makes query operators visible

Les utilisateurs qui souhaitent remplacer les opérateurs de requête standard pour un type spécifique peuvent : définir leurs propres méthodes nommées sur le type spécifique avec des signatures compatibles, ou définir de nouvelles méthodes d’extension portant le même nom qui étendent le type spécifique. Les utilisateurs qui souhaitent éviter complètement les opérateurs de requête standard ne peuvent tout simplement pas placer System.Linq dans l’étendue et écrire leurs propres méthodes d’extension pour IEnumerable<T>.

Les méthodes d’extension bénéficient de la priorité la plus faible en termes de résolution et ne sont utilisées que s’il n’existe aucune correspondance appropriée sur le type cible et ses types de base. Cela permet aux types définis par l’utilisateur de fournir leurs propres opérateurs de requête qui sont prioritaires sur les opérateurs standard. Par exemple, considérez la collection personnalisée suivante :

public class MySequence : IEnumerable<int> {
  public IEnumerator<int> GetEnumerator() {
    for (int i = 1; i <= 10; i++) 
      yield return i; 
  }

  IEnumerator IEnumerable.GetEnumerator() {
    return GetEnumerator(); 
  }

  public IEnumerable<int> Where(Func<int, bool> filter) {
    for (int i = 1; i <= 10; i++) 
      if (filter(i)) 
        yield return i;
  }
}

Compte tenu de cette définition de classe, le programme suivant utilise l’implémentation MySequence.Where, et non la méthode d’extension, car instance méthodes sont prioritaires sur les méthodes d’extension :

MySequence s = new MySequence();
foreach (int item in s.Where(n => n > 3))
    Console.WriteLine(item);

L’opérateur OfType est l’un des rares opérateurs de requête standard qui n’étend pas une source d’informations basée sur T> IEnumerable<. Examinons l’opérateur de requête OfType :

public static IEnumerable<T> OfType<T>(this IEnumerable source) {
  foreach (object item in source) 
    if (item is T) 
      yield return (T)item;
}

OfType accepte non seulement les sources basées sur T> IEnumerable<, mais également les sources écrites sur l’interface IEnumerable non paramétrable présente dans la version 1.0 du .NET Framework. L’opérateur OfType permet aux utilisateurs d’appliquer les opérateurs de requête standard aux collections .NET classiques comme suit :

// "classic" cannot be used directly with query operators
IEnumerable classic = new OlderCollectionType();

// "modern" can be used directly with query operators
IEnumerable<object> modern = classic.OfType<object>();

Dans cet exemple, la variable modern génère la même séquence de valeurs que classique. Toutefois, son type est compatible avec le code T> IEnumerable< moderne, y compris les opérateurs de requête standard.

L’opérateur OfType est également utile pour les sources d’informations plus récentes, car il permet de filtrer les valeurs d’une source en fonction du type. Lors de la production de la nouvelle séquence, OfType omet simplement les membres de la séquence d’origine qui ne sont pas compatibles avec l’argument de type. Considérez ce programme simple qui extrait des chaînes d’un tableau hétérogène :

object[] vals = { 1, "Hello", true, "World", 9.1 };
IEnumerable<string> justStrings = vals.OfType<string>();

Lorsque nous énumérons la variable justStrings dans une instruction foreach , nous obtenons une séquence de deux chaînes : « Hello » et « World ».

Évaluation différée des requêtes

Les lecteurs observants ont peut-être remarqué que l’opérateur Where standard est implémenté à l’aide de la construction de rendement introduite dans C# 2.0. Cette technique d’implémentation est courante pour tous les opérateurs standard qui retournent des séquences de valeurs. L’utilisation de yield présente un avantage intéressant qui est que la requête n’est pas réellement évaluée tant qu’elle n’est pas itérée, soit avec une instruction foreach , soit manuellement à l’aide des méthodes GetEnumerator et MoveNext sous-jacentes. Cette évaluation différée permet aux requêtes d’être conservées sous forme de valeurs T> IEnumerable< qui peuvent être évaluées plusieurs fois, produisant à chaque fois des résultats potentiellement différents.

Pour de nombreuses applications, il s’agit exactement du comportement souhaité. Pour les applications qui souhaitent mettre en cache les résultats de l’évaluation de requête, deux opérateurs, ToList et ToArray, sont fournis qui forcent l’évaluation immédiate de la requête et retournent une liste<T> ou un tableau contenant les résultats de l’évaluation de la requête.

Pour voir comment fonctionne l’évaluation différée des requêtes, envisagez ce programme qui exécute une requête simple sur un tableau :

// declare a variable containing some strings
string[] names = { "Allen", "Arthur", "Bennett" };

// declare a variable that represents a query
IEnumerable<string> ayes = names.Where(s => s[0] == 'A');

// evaluate the query
foreach (string item in ayes) 
  Console.WriteLine(item);

// modify the original information source
names[0] = "Bob";

// evaluate the query again, this time no "Allen"
foreach (string item in ayes) 
    Console.WriteLine(item);

La requête est évaluée chaque fois que la variable ayes est itérée. Pour indiquer qu’une copie mise en cache des résultats est nécessaire, nous pouvons simplement ajouter un opérateur ToList ou ToArray à la requête comme suit :

// declare a variable containing some strings
string[] names = { "Allen", "Arthur", "Bennett" };

// declare a variable that represents the result
// of an immediate query evaluation
string[] ayes = names.Where(s => s[0] == 'A').ToArray();

// iterate over the cached query results
foreach (string item in ayes) 
    Console.WriteLine(item);

// modifying the original source has no effect on ayes
names[0] = "Bob";

// iterate over result again, which still contains "Allen"
foreach (string item in ayes)
    Console.WriteLine(item);

ToArray et ToList forcent l’évaluation immédiate des requêtes. Il en va de même pour les opérateurs de requête standard qui retournent des valeurs singleton (par exemple : First, ElementAt, Sum, Average, All, Any).

Interface T> IQueryable<

Le même modèle d’exécution différée est généralement souhaité pour les sources de données qui implémentent la fonctionnalité de requête à l’aide d’arborescences d’expressions, telles que LINQ to SQL. Ces sources de données peuvent tirer parti de l’implémentation de l’interface T> IQueryable< pour laquelle tous les opérateurs de requête requis par le modèle LINQ sont implémentés à l’aide d’arborescences d’expressions. Chaque IQueryable<T> a une représentation du « code nécessaire pour exécuter la requête » sous la forme d’une arborescence d’expressions. Tous les opérateurs de requête différés retournent un nouveau T> IQueryable< qui augmente cette arborescence d’expressions par une représentation d’un appel à cet opérateur de requête. Ainsi, lorsqu’il devient temps d’évaluer la requête, généralement parce que le T> IQueryable< est énuméré, la source de données peut traiter l’arborescence d’expressions représentant l’ensemble de la requête dans un lot. Par exemple, une requête LINQ to SQL complexe obtenue par de nombreux appels aux opérateurs de requête peut entraîner l’envoi d’une seule requête SQL à la base de données.

L’avantage pour les implémenteurs de sources de données de réutiliser cette fonctionnalité différée en implémentant l’interface IQueryable<T> est évident. Pour les clients qui écrivent les requêtes, en revanche, il est très avantageux d’avoir un type commun pour les sources d’informations distantes. Non seulement il leur permet d’écrire des requêtes polymorphes qui peuvent être utilisées sur différentes sources de données, mais il ouvre également la possibilité d’écrire des requêtes qui vont dans des domaines.

Initialisation de valeurs composées

Les expressions lambda et les méthodes d’extension nous fournissent tout ce dont nous avons besoin pour les requêtes qui filtrent simplement les membres d’une séquence de valeurs. La plupart des expressions de requête effectuent également une projection sur ces membres, transformant efficacement les membres de la séquence d’origine en membres dont la valeur et le type peuvent différer de l’original. Pour prendre en charge l’écriture de ces transformations, LINQ s’appuie sur une nouvelle construction appelée initialiseurs d’objets pour créer de nouvelles instances de types structurés. Pour le reste de ce document, nous allons supposer que le type suivant a été défini :

public class Person {
  string name;
  int age;
  bool canCode;

  public string Name {
    get { return name; } set { name = value; }
  }

  public int Age {
    get { return age; } set { age = value; }
  }

  public bool CanCode {
    get { return canCode; } set { canCode = value; }
  }
}

Les initialiseurs d’objets nous permettent de construire facilement des valeurs basées sur les champs publics et les propriétés d’un type. Par exemple, pour créer une valeur de type Person, nous pouvons écrire cette instruction :

Person value = new Person {
    Name = "Chris Smith", Age = 31, CanCode = false
};

Sémantiquement, cette instruction équivaut à la séquence d’instructions suivante :

Person value = new Person();
value.Name = "Chris Smith";
value.Age = 31;
value.CanCode = false;

Les initialiseurs d’objets sont une fonctionnalité importante pour les requêtes intégrées au langage, car ils permettent la construction de nouvelles valeurs structurées dans des contextes où seules les expressions sont autorisées (par exemple, dans les expressions lambda et les arborescences d’expressions). Par exemple, considérez cette expression de requête qui crée une valeur Person pour chaque valeur de la séquence d’entrée :

IEnumerable<Person> query = names.Select(s => new Person {
    Name = s, Age = 21, CanCode = s.Length == 5
});

La syntaxe d’initialisation d’objet est également pratique pour initialiser des tableaux de valeurs structurées. Par exemple, considérez cette variable de tableau qui est initialisée à l’aide d’initialiseurs d’objets individuels :

static Person[] people = {
  new Person { Name="Allen Frances", Age=11, CanCode=false },
  new Person { Name="Burke Madison", Age=50, CanCode=true },
  new Person { Name="Connor Morgan", Age=59, CanCode=false },
  new Person { Name="David Charles", Age=33, CanCode=true },
  new Person { Name="Everett Frank", Age=16, CanCode=true },
};

Valeurs et types structurés

Le projet LINQ prend en charge un style de programmation centré sur les données dans lequel certains types existent principalement pour fournir une « forme » statique sur une valeur structurée plutôt qu’un objet complet avec un état et un comportement. Si l’on prend cette prémisse jusqu’à sa conclusion logique, il arrive souvent que le développeur ne s’intéresse qu’à la structure de la valeur, et que la nécessité d’un type nommé pour cette forme est peu d’utilité. Cela conduit à l’introduction de types anonymes qui permettent de définir de nouvelles structures « inline » avec leur initialisation.

En C#, la syntaxe des types anonymes est similaire à la syntaxe d’initialisation d’objet, sauf que le nom du type est omis. Par exemple, considérez les deux instructions suivantes :

object v1 = new Person {
    Name = "Brian Smith", Age = 31, CanCode = false
};

object v2 = new { // note the omission of type name
    Name = "Brian Smith", Age = 31, CanCode = false
};

Les variables v1 et v2 pointent vers un objet en mémoire dont le type CLR a trois propriétés publiques Name, Age et CanCode. Les variables diffèrent dans le fait que v2 fait référence à un instance de type anonyme. En termes CLR, les types anonymes ne sont pas différents de tout autre type. Ce qui rend les types anonymes spéciaux, c’est qu’ils n’ont pas de nom significatif dans votre langage de programmation. La seule façon de créer des instances d’un type anonyme consiste à utiliser la syntaxe indiquée ci-dessus.

Pour permettre aux variables de faire référence à des instances de types anonymes tout en bénéficiant toujours du typage statique, C# introduit des variables locales implicitement typées :le var mot clé peut être utilisé à la place du nom de type pour les déclarations de variable locale. Par exemple, considérez ce programme C# 3.0 juridique :

var s = "Bob";
var n = 32;
var b = true;

Le var mot clé indique au compilateur de déduire le type de la variable à partir du type statique de l’expression utilisée pour initialiser la variable. Dans cet exemple, les types de s, n et b sont string, int et bool, respectivement. Ce programme est identique à ce qui suit :

string s = "Bob";
int    n = 32;
bool   b = true;

Le var mot clé est pratique pour les variables dont les types ont des noms explicites, mais il s’agit d’une nécessité pour les variables qui font référence à des instances de types anonymes.

var value = new { 
  Name = " Brian Smith", Age = 31, CanCode = false
};

Dans l’exemple ci-dessus, la valeur de variable est d’un type anonyme dont la définition est équivalente au pseudo-C# suivant :

internal class ??? {
  string _Name;
  int    _Age;
  bool   _CanCode;

  public string Name { 
    get { return _Name; } set { _Name = value; }
  }

  public int Age{ 
    get { return _Age; } set { _Age = value; }
  }

  public bool CanCode { 
    get { return _CanCode; } set { _CanCode = value; }
  }

  public bool Equals(object obj) { ... }

  public bool GetHashCode() { ... }
}

Les types anonymes ne peuvent pas être partagés entre les limites de l’assembly ; Toutefois, le compilateur garantit qu’il existe au plus un type anonyme pour une séquence donnée de paires nom/type de propriété dans chaque assembly.

Étant donné que les types anonymes sont souvent utilisés dans les projections pour sélectionner un ou plusieurs membres d’une valeur structurée existante, nous pouvons simplement référencer des champs ou des propriétés d’une autre valeur dans l’initialisation d’un type anonyme. Ainsi, le nouveau type anonyme obtient une propriété dont le nom, le type et la valeur sont tous copiés à partir de la propriété ou du champ référencé.

Pour instance, considérez cet exemple qui crée une valeur structurée en combinant des propriétés d’autres valeurs :

var bob = new Person { Name = "Bob", Age = 51, CanCode = true };
var jane = new { Age = 29, FirstName = "Jane" };

var couple = new {
    Husband = new { bob.Name, bob.Age },
    Wife = new { Name = jane.FirstName, jane.Age }
};

int    ha = couple.Husband.Age; // ha == 51
string wn = couple.Wife.Name;   // wn == "Jane"

Le référencement des champs ou des propriétés indiqués ci-dessus est simplement une syntaxe pratique pour écrire la forme plus explicite suivante :

var couple = new {
    Husband = new { Name = bob.Name, Age = bob.Age },
    Wife = new { Name = jane.FirstName, Age = jane.Age }
};

Dans les deux cas, la variable couple obtient sa propre copie des propriétés Name et Age de bob et jane.

Les types anonymes sont le plus souvent utilisés dans la clause select d’une requête. Examinons, par exemple, la requête suivante :

var query = people.Select(p => new { 
               p.Name, BadCoder = p.Age == 11
           });

foreach (var item in query) 
  Console.WriteLine("{0} is a {1} coder", 
                     item.Name,
                     item.BadCoder ? "bad" : "good");

Dans cet exemple, nous avons pu créer une projection sur le type Person qui correspond exactement à la forme dont nous avions besoin pour notre code de traitement, tout en nous donnant les avantages d’un type statique.

Autres opérateurs de requête standard

En plus des fonctionnalités de requête de base décrites ci-dessus, un certain nombre d’opérateurs fournissent des moyens utiles de manipuler des séquences et de composer des requêtes, ce qui donne à l’utilisateur un degré élevé de contrôle sur le résultat dans le cadre pratique des opérateurs de requête standard.

Tri et regroupement

En général, l’évaluation d’une requête entraîne une séquence de valeurs produites dans un ordre intrinsèque dans les sources d’informations sous-jacentes. Pour donner aux développeurs un contrôle explicite sur l’ordre dans lequel ces valeurs sont produites, des opérateurs de requête standard sont définis pour contrôler l’ordre. Le plus simple de ces opérateurs est l’opérateur OrderBy .

Les opérateurs OrderBy et OrderByDescending peuvent être appliqués à n’importe quelle source d’informations et permettre à l’utilisateur de fournir une fonction d’extraction de clé qui produit la valeur utilisée pour trier les résultats. OrderBy et OrderByDescending acceptent également une fonction de comparaison facultative qui peut être utilisée pour imposer un ordre partiel sur les clés. Examinons un exemple de base :

string[] names = { "Burke", "Connor", "Frank", "Everett", 
                   "Albert", "George", "Harris", "David" };

// unity sort
var s1 = names.OrderBy(s => s); 
var s2 = names.OrderByDescending(s => s);

// sort by length
var s3 = names.OrderBy(s => s.Length); 
var s4 = names.OrderByDescending(s => s.Length);

Les deux premières expressions de requête produisent de nouvelles séquences basées sur le tri des membres de la source en fonction de la comparaison de chaînes. Les deux deuxièmes requêtes produisent de nouvelles séquences basées sur le tri des membres de la source en fonction de la longueur de chaque chaîne.

Pour autoriser plusieurs critères de tri, OrderBy et OrderByDescending renvoientOrderSequence<T> plutôt que le T générique IEnumerable<.> Deux opérateurs sont définis uniquement sur OrderedSequence<T>, à savoir ThenBy et ThenByDescending qui appliquent un critère de tri (subordonné) supplémentaire. ThenBy/ThenByDescending eux-mêmes retournent OrderedSequence<T>, ce qui permet à n’importe quel nombre d’opérateurs ThenBy/ThenByDescending d’être appliqués :

string[] names = { "Burke", "Connor", "Frank", "Everett", 
                   "Albert", "George", "Harris", "David" };

var s1 = names.OrderBy(s => s.Length).ThenBy(s => s);

L’évaluation de la requête référencée par s1 dans cet exemple génère la séquence de valeurs suivante :

"Burke", "David", "Frank", 
"Albert", "Connor", "George", "Harris", 
"Everett"

En plus de la famille d’opérateurs OrderBy , les opérateurs de requête standard incluent également un opérateur Reverse . Inverse énumère simplement sur une séquence et génère les mêmes valeurs dans l’ordre inverse. Contrairement à OrderBy, Reverse ne prend pas en compte les valeurs réelles elles-mêmes pour déterminer l’ordre, mais s’appuie uniquement sur l’ordre dans lequel les valeurs sont produites par la source sous-jacente.

L’opérateur OrderBy impose un ordre de tri sur une séquence de valeurs. Les opérateurs de requête standard incluent également l’opérateur GroupBy , qui impose un partitionnement sur une séquence de valeurs basée sur une fonction d’extraction de clé. L’opérateur GroupBy retourne une séquence de valeurs IGrouping, une pour chaque valeur de clé distincte rencontrée. Un IGrouping est un IEnumerable qui contient en outre la clé utilisée pour extraire son contenu :

public interface IGrouping<K, T> : IEnumerable<T> {
  public K Key { get; }
}

L’application la plus simple de GroupBy ressemble à ceci :

string[] names = { "Albert", "Burke", "Connor", "David",
                   "Everett", "Frank", "George", "Harris"};

// group by length
var groups = names.GroupBy(s => s.Length);

foreach (IGrouping<int, string> group in groups) {
    Console.WriteLine("Strings of length {0}", group.Key);

    foreach (string value in group)
        Console.WriteLine("  {0}", value);
}    

Lors de l’exécution, ce programme imprime les éléments suivants :

Strings of length 6
  Albert
  Connor
  George
  Harris
Strings of length 5
  Burke
  David
  Frank
Strings of length 7
  Everett

A la Select, GroupBy vous permet de fournir une fonction de projection utilisée pour remplir les membres des groupes.

string[] names = { "Albert", "Burke", "Connor", "David",
                   "Everett", "Frank", "George", "Harris"};

// group by length
var groups = names.GroupBy(s => s.Length, s => s[0]);
foreach (IGrouping<int, char> group in groups) {
    Console.WriteLine("Strings of length {0}", group.Key);

    foreach (char value in group)
        Console.WriteLine("  {0}", value);
}  

Cette variante imprime ce qui suit :

Strings of length 6
  A
  C
  G
  H
Strings of length 5
  B
  D
  F
Strings of length 7
  E

Note Dans cet exemple, le type projeté n’a pas besoin d’être identique à la source. Dans ce cas, nous avons créé un regroupement d’entiers en caractères à partir d’une séquence de chaînes.

Opérateurs d’agrégation

Plusieurs opérateurs de requête standard sont définis pour l’agrégation d’une séquence de valeurs en une seule valeur. L’opérateur d’agrégation le plus général est Aggregate, qui est défini comme suit :

public static U Aggregate<T, U>(this IEnumerable<T> source, 
                                U seed, Func<U, T, U> func) {
  U result = seed;

  foreach (T element in source) 
      result = func(result, element);

  return result;
}

L’opérateur Aggregate simplifie l’exécution d’un calcul sur une séquence de valeurs. L’agrégation fonctionne en appelant l’expression lambda une fois pour chaque membre de la séquence sous-jacente. Chaque fois qu’Aggregate appelle l’expression lambda, il passe à la fois le membre de la séquence et une valeur agrégée (la valeur initiale est le paramètre initial à Aggregate). Le résultat de l’expression lambda remplace la valeur agrégée précédente, et Aggregate retourne le résultat final de l’expression lambda.

Par exemple, ce programme utilise l’agrégation pour accumuler le nombre total de caractères sur un tableau de chaînes :

string[] names = { "Albert", "Burke", "Connor", "David",
                   "Everett", "Frank", "George", "Harris"};

int count = names.Aggregate(0, (c, s) => c + s.Length);
// count == 46

En plus de l’opérateur d’agrégation à usage général, les opérateurs de requête standard incluent également un opérateur Count à usage général et quatre opérateurs d’agrégation numérique (Min, Max, Sum et Average) qui simplifient ces opérations d’agrégation courantes. Les fonctions d’agrégation numérique fonctionnent sur des séquences de types numériques (par exemple, int, double, décimal) ou sur des séquences de valeurs arbitraires tant qu’une fonction est fournie qui projette les membres de la séquence dans un type numérique.

Ce programme illustre les deux formes de l’opérateur Sum qui vient d’être décrit :

int[] numbers = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 };
string[] names = { "Albert", "Burke", "Connor", "David",
                   "Everett", "Frank", "George", "Harris"};

int total1 = numbers.Sum();            // total1 == 55
int total2 = names.Sum(s => s.Length); // total2 == 46

Note La deuxième instruction Sum est équivalente à l’exemple précédent utilisant Aggregate.

Sélectionner ou SélectionnerMany

L’opérateur Select nécessite que la fonction transform produise une valeur pour chaque valeur de la séquence source. Si votre fonction de transformation retourne une valeur qui est elle-même une séquence, il appartient au consommateur de parcourir les sous-séquences manuellement. Par exemple, considérez ce programme qui décompose les chaînes en jetons à l’aide de la méthode String.Split existante :

string[] text = { "Albert was here", 
                  "Burke slept late", 
                  "Connor is happy" };

var tokens = text.Select(s => s.Split(' '));

foreach (string[] line in tokens)
    foreach (string token in line)
        Console.Write("{0}.", token);

Lors de l’exécution, ce programme imprime le texte suivant :

Albert.was.here.Burke.slept.late.Connor.is.happy.

Dans l’idéal, nous aurions aimé que notre requête renvoie une séquence de jetons coalescée et n’expose pas la chaîne intermédiaire[] au consommateur. Pour ce faire, nous utilisons l’opérateur SelectMany au lieu de l’opérateur Select . L’opérateur SelectMany fonctionne de la même façon que l’opérateur Select. Il diffère dans le fait que la fonction de transformation est censée retourner une séquence qui est ensuite développée par l’opérateur SelectMany . Voici notre programme réécrit à l’aide de SelectMany :

string[] text = { "Albert was here", 
                  "Burke slept late", 
                  "Connor is happy" };

var tokens = text.SelectMany(s => s.Split(' '));

foreach (string token in tokens)
    Console.Write("{0}.", token);

L’utilisation de SelectMany entraîne l’extension de chaque séquence intermédiaire dans le cadre d’une évaluation normale.

SelectMany est idéal pour combiner deux sources d’informations :

string[] names = { "Burke", "Connor", "Frank", "Everett", 
                   "Albert", "George", "Harris", "David" };

var query = names.SelectMany(n => 
                     people.Where(p => n.Equals(p.Name))
                 );

Dans l’expression lambda passée à SelectMany, la requête imbriquée s’applique à une autre source, mais a dans l’étendue le n paramètre transmis à partir de la source externe. Donc les gens. Où est appelé une fois pour chaque n, les séquences résultantes étant aplatie par SelectMany pour la sortie finale. Le résultat est une séquence de toutes les personnes dont le nom apparaît dans le tableau de noms .

Opérateurs de jointure

Dans un programme orienté objet, les objets qui sont liés les uns aux autres sont généralement liés avec des références d’objets qui sont faciles à parcourir. Cela n’est généralement pas vrai pour les sources d’informations externes, où les entrées de données n’ont souvent pas d’autre option que de « pointer » l’une vers l’autre de manière symbolique, avec des ID ou d’autres données qui peuvent identifier de manière unique l’entité pointée vers. Le concept de jointures fait référence à l’opération consistant à rassembler les éléments d’une séquence avec les éléments avec lesquels ils « correspondent » à partir d’une autre séquence.

L’exemple précédent avec SelectMany fait exactement cela, en faisant correspondre des chaînes avec des personnes dont les noms sont ces chaînes. Toutefois, dans ce but particulier, l’approche SelectMany n’est pas très efficace : elle effectue une boucle dans tous les éléments de personnes pour chaque élément de noms. En réunissant toutes les informations de ce scénario (les deux sources d’informations et les « clés » par lesquelles elles sont mises en correspondance) dans un seul appel de méthode, l’opérateur Join est en mesure de faire un bien meilleur travail :

string[] names = { "Burke", "Connor", "Frank", "Everett", 
                   "Albert", "George", "Harris", "David" };

var query = names.Join(people, n => n, p => p.Name, (n,p) => p);

Il s’agit d’un peu d’une mise en bouche, mais voyez comment les éléments s’intègrent : la méthode Join est appelée sur la source de données « externe », les noms. Le premier argument est la source de données « interne », les personnes. Les deuxième et troisième arguments sont des expressions lambda pour extraire des clés des éléments des sources externes et internes, respectivement. Ces clés sont utilisées par la méthode Join pour faire correspondre les éléments. Ici, nous voulons que les noms eux-mêmes correspondent à la propriété Name des personnes. L’expression lambda finale est ensuite responsable de la production des éléments de la séquence résultante : elle est appelée avec chaque paire d’éléments correspondants n et p, et est utilisée pour mettre en forme le résultat. Dans ce cas, nous choisissons d’ignorer le n et de retourner le p. Le résultat final est la liste des éléments Person de personnes dont le nom figure dans la liste des noms.

Un cousin plus puissant de Join est l’opérateur GroupJoin . GroupJoin diffère de Join par la façon dont l’expression lambda de mise en forme des résultats est utilisée : au lieu d’être appelée avec chaque paire individuelle d’éléments externes et internes, elle ne sera appelée qu’une seule fois pour chaque élément externe, avec une séquence de tous les éléments internes qui correspondent à cet élément externe. Pour rendre cela concret :

string[] names = { "Burke", "Connor", "Frank", "Everett", 
                   "Albert", "George", "Harris", "David" };

var query = names.GroupJoin(people, n => n, p => p.Name,                   
                 (n, matching) => 
                      new { Name = n, Count = matching.Count() }
);

Cet appel produit une séquence des noms que vous avez commencé avec le nombre de personnes qui ont ce nom. Ainsi, l’opérateur GroupJoin vous permet de baser vos résultats sur l’ensemble du « jeu de correspondances » pour un élément externe.

Syntaxe de la requête

L’instruction foreach existante en C# fournit une syntaxe déclarative pour l’itération sur les méthodes IEnumerable/IEnumerator .NET Frameworks. L’instruction foreach est strictement facultative, mais elle s’est avérée être un mécanisme de langage très pratique et populaire.

S’appuyant sur ce précédent, les expressions de requête simplifient les requêtes avec une syntaxe déclarative pour les opérateurs de requête les plus courants : Where, Join, GroupJoin, SelectMany, GroupBy, OrderBy, ThenBy, OrderByDescending, ThenByDescending et Cast.

Commençons par examiner la requête simple avec laquelle nous avons commencé ce document :

IEnumerable<string> query = names 
                            .Where(s => s.Length == 5) 
                            .OrderBy(s => s)
                            .Select(s => s.ToUpper());

À l’aide d’une expression de requête, nous pouvons réécrire cette instruction exacte comme suit :

IEnumerable<string> query = from s in names 
                            where s.Length == 5
                            orderby s
                            select s.ToUpper();

Comme l’instruction foreach en C#, les expressions de requête sont plus compactes et plus faciles à lire, mais sont entièrement facultatives. Chaque expression qui peut être écrite en tant qu’expression de requête a une syntaxe correspondante (quoique plus détaillée) à l’aide de la notation par points.

Commençons par examiner la structure de base d’une expression de requête. Chaque expression de requête syntaxique en C# commence par une clause from et se termine par une clause select ou group . La clause initiale from peut être suivie de zéro ou plusieurs clauses de, let, where, join et orderby . Chaque clause de est un générateur qui introduit une variable de plage sur une séquence ; chaque clause let donne un nom au résultat d’une expression ; et chaque clause where est un filtre qui exclut les éléments du résultat. Chaque clause de jointure met en corrélation une nouvelle source de données avec les résultats des clauses précédentes. Une clause orderby spécifie un ordre pour le résultat :

query-expression ::= from-clause query-body

query-body ::= 

      query-body-clause* final-query-clause query-continuation?

query-body-clause ::=
 (from-clause 
      | join-clause 
      | let-clause 
      | where-clause 
      | orderby-clause)

from-clause ::=from itemName in srcExpr

join-clause ::=join itemName in srcExpr on keyExpr equals keyExpr 
       (into itemName)?

let-clause ::=let itemName = selExpr

where-clause ::= where predExpr

orderby-clause ::= orderby (keyExpr (ascending | descending)?)*

final-query-clause ::=
 (select-clause | groupby-clause)

select-clause ::= select selExpr

groupby-clause ::= group selExpr by keyExprquery-continuation ::= intoitemName query-body

Par exemple, considérez ces deux expressions de requête :

var query1 = from p in people
             where p.Age > 20
             orderby p.Age descending, p.Name
             select new { 
                 p.Name, Senior = p.Age > 30, p.CanCode
             };

var query2 = from p in people
             where p.Age > 20
             orderby p.Age descending, p.Name
             group new { 
                p.Name, Senior = p.Age > 30, p.CanCode
             } by p.CanCode;

Le compilateur traite ces expressions de requête comme si elles avaient été écrites à l’aide de la notation de point explicite suivante :

var query1 = people.Where(p => p.Age > 20)
                   .OrderByDescending(p => p.Age)
                   .ThenBy(p => p.Name)
                   .Select(p => new { 
                       p.Name, 
                       Senior = p.Age > 30, 
                       p.CanCode
                   });

var query2 = people.Where(p => p.Age > 20)
                   .OrderByDescending(p => p.Age)
                   .ThenBy(p => p.Name)
                   .GroupBy(p => p.CanCode, 
                            p => new {
                                   p.Name, 
                                   Senior = p.Age > 30, 
                                   p.CanCode
                   });

Les expressions de requête subissent une traduction mécanique en appels de méthodes avec des noms spécifiques. L’implémentation exacte de l’opérateur de requête choisie dépend donc à la fois du type des variables interrogées et des méthodes d’extension qui sont dans l’étendue.

Les expressions de requête affichées jusqu’à présent n’ont utilisé qu’un seul générateur. Lorsque plusieurs générateurs sont utilisés, chaque générateur suivant est évalué dans le contexte de son prédécesseur. Par exemple, considérez cette légère modification de notre requête :

var query = from s1 in names 
            where s1.Length == 5
            from s2 in names 
            where s1 == s2
            select s1 + " " + s2;

Lors de l’exécution sur ce tableau d’entrée :

string[] names = { "Burke", "Connor", "Frank", "Everett", 
                   "Albert", "George", "Harris", "David" };

nous obtenons les résultats suivants :

Burke Burke
Frank Frank
David David

L’expression de requête ci-dessus s’étend à cette expression de notation par point :

var query = names.Where(s1 => s1.Length == 5)
                 .SelectMany(s1 => names, (s1,s2) => new {s1,s2})
                 .Where($1 => $1.s1 == $1.s2) 
                 .Select($1 => $1.s1 + " " + $1.s2);

Note Cette version de SelectMany prend une expression lambda supplémentaire qui est utilisée pour produire le résultat en fonction des éléments des séquences externe et interne. Dans cette expression lambda, les deux variables de plage sont collectées dans un type anonyme. Le compilateur invente un nom de variable $1 pour désigner ce type anonyme dans les expressions lambda suivantes.

Un type spécial de générateur est la clause de jointure , qui introduit des éléments d’une autre source qui correspondent aux éléments des clauses précédentes en fonction de clés données. Une clause de jointure peut produire les éléments correspondants un par un, mais s’ils sont spécifiés avec une clause into , les éléments correspondants sont donnés sous forme de groupe :

var query = from n in names
            join p in people on n equals p.Name into matching
            select new { Name = n, Count = matching.Count() };

Il n’est pas surprenant que cette requête se développe assez directement dans une requête que nous avons vu précédemment :

var query = names.GroupJoin(people, n => n, p => p.Name,                   
           (n, matching) => 
                      new { Name = n, Count = matching.Count() }
);

Il est souvent utile de traiter les résultats d’une requête en tant que générateur dans une requête suivante. Pour prendre en charge cela, les expressions de requête utilisent le dans mot clé pour épissage d’une nouvelle expression de requête après une clause select ou group. C’est ce qu’on appelle une continuation de requête.

Le en mot clé est particulièrement utile pour le post-traitement des résultats d’une clause group by. Par exemple, considérez ce programme :

var query = from item in names
            orderby item
            group item by item.Length into lengthGroups
            orderby lengthGroups.Key descending
            select lengthGroups;

foreach (var group in query) { 
    Console.WriteLine("Strings of length {0}", group.Key);

    foreach (var val in group)
        Console.WriteLine("  {0}", val);
}

Ce programme génère les résultats suivants :

Strings of length 7
  Everett
Strings of length 6
  Albert
  Connor
  George
  Harris
Strings of length 5
  Burke
  David
  Frank

Dans cette section, il a été décrit comment C# implémente des expressions de requête. D’autres langages peuvent choisir de prendre en charge des opérateurs de requête supplémentaires avec une syntaxe explicite ou de ne pas avoir d’expressions de requête du tout.

Il est important de noter que la syntaxe de requête n’est en aucun cas câblée avec les opérateurs de requête standard. Il s’agit d’une fonctionnalité purement syntaxique qui s’applique à tout ce qui respecte le modèle de requête en implémentant des méthodes sous-jacentes avec les noms et signatures appropriés. Les opérateurs de requête standard décrits ci-dessus utilisent des méthodes d’extension pour augmenter l’interface IEnumerable<T> . Les développeurs peuvent exploiter la syntaxe de requête sur le type qu’ils souhaitent, tant qu’ils s’assurent qu’elle respecte le modèle de requête, soit en implémentation directe des méthodes nécessaires, soit en les ajoutant en tant que méthodes d’extension.

Cette extensibilité est exploitée dans le projet LINQ lui-même par la fourniture de deux API linq, à savoir LINQ to SQL, qui implémente le modèle LINQ pour l’accès aux données SQL, et LINQ to XML qui autorise les requêtes LINQ sur des données XML. Ces deux éléments sont décrits dans les sections suivantes.

LINQ to SQL : Intégration SQL

.NET Language-Integrated Query peut être utilisé pour interroger des magasins de données relationnelles sans quitter la syntaxe ou l’environnement de compilation du langage de programmation local. Cette fonctionnalité, nommée par le code LINQ to SQL, tire parti de l’intégration des informations de schéma SQL dans les métadonnées CLR. Cette intégration compile les définitions de table SQL et d’affichage en types CLR accessibles à partir de n’importe quel langage.

LINQ to SQL définit deux attributs principaux, [Table] et [Colonne] qui indiquent les types et propriétés CLR correspondant aux données SQL externes. L’attribut [Table] peut être appliqué à une classe et associe le type CLR à une table ou une vue SQL nommée. L’attribut [Colonne] peut être appliqué à n’importe quel champ ou propriété et associe le membre à une colonne SQL nommée. Les deux attributs sont paramétrés pour permettre la conservation des métadonnées spécifiques à SQL. Par exemple, considérez cette définition de schéma SQL simple :

create table People (
    Name nvarchar(32) primary key not null, 
    Age int not null, 
    CanCode bit not null
)

create table Orders (
    OrderID nvarchar(32) primary key not null, 
    Customer nvarchar(32) not null, 
    Amount int
)

L’équivalent CLR ressemble à ceci :

[Table(Name="People")]
public class Person {
  [Column(DbType="nvarchar(32) not null", Id=true)]
  public string Name; 

  [Column]
  public int Age;

  [Column]
  public bool CanCode;
}

[Table(Name="Orders")]
public class Order {
  [Column(DbType="nvarchar(32) not null", Id=true)]
  public string OrderID; 

  [Column(DbType="nvarchar(32) not null")]        
  public string Customer; 

  [Column]
  public int? Amount; 
}

Note Cet exemple montre comment mapper les colonnes nullables à des types nullables dans le CLR (les types nullables sont apparus pour la première fois dans la version 2.0 du .NET Framework), et que pour les types SQL qui n’ont pas de correspondance 1:1 avec un type CLR (par exemple, nvarchar, char, text), le type SQL d’origine est conservé dans les métadonnées CLR.

Pour émettre une requête sur un magasin relationnel, l’implémentation LINQ to SQL du modèle LINQ traduit la requête de sa forme d’arborescence d’expressions en expression SQL et ADO.NET objet DbCommand adapté à l’évaluation à distance. Par exemple, considérez cette requête simple :

// establish a query context over ADO.NET sql connection
DataContext context = new DataContext(
     "Initial Catalog=petdb;Integrated Security=sspi");

// grab variables that represent the remote tables that 
// correspond to the Person and Order CLR types
Table<Person> custs = context.GetTable<Person>();
Table<Order> orders   = context.GetTable<Order>();

// build the query
var query = from c in custs
            from o in orders
            where o.Customer == c.Name
            select new { 
                       c.Name, 
                       o.OrderID,
                       o.Amount,
                       c.Age
            }; 

// execute the query
foreach (var item in query) 
    Console.WriteLine("{0} {1} {2} {3}", 
                      item.Name, item.OrderID, 
                      item.Amount, item.Age);

Le type DataContext fournit un traducteur léger qui traduit les opérateurs de requête standard en SQL. DataContext utilise le ADO.NET IDbConnection existant pour accéder au magasin et peut être initialisé avec un objet de connexion ADO.NET établi ou une chaîne de connexion qui peut être utilisée pour en créer un.

La méthode GetTable fournit des variables compatibles IEnumerable qui peuvent être utilisées dans les expressions de requête pour représenter la table ou la vue distante. Les appels à GetTable ne provoquent aucune interaction avec la base de données. Ils représentent plutôt le potentiel d’interagir avec la table ou la vue distante à l’aide d’expressions de requête. Dans notre exemple ci-dessus, la requête n’est pas transmise au magasin tant que le programme n’itère pas sur l’expression de requête, dans ce cas à l’aide de l’instruction foreach en C#. Lorsque le programme itère pour la première fois sur la requête, la machine DataContext traduit l’arborescence d’expressions en l’instruction SQL suivante qui est envoyée au magasin :

SELECT [t0].[Age], [t1].[Amount], 
       [t0].[Name], [t1].[OrderID]
FROM [Customers] AS [t0], [Orders] AS [t1]
WHERE [t1].[Customer] = [t0].[Name]

Il est important de noter qu’en créant une fonctionnalité de requête directement dans le langage de programmation local, les développeurs obtiennent toute la puissance du modèle relationnel sans avoir à créer statiquement les relations dans le type CLR. Cela dit, le mappage objet/relationnel complet peut également tirer parti de cette fonctionnalité de requête principale pour les utilisateurs qui souhaitent cette fonctionnalité. LINQ to SQL fournit des fonctionnalités de mappage objet-relationnel avec lesquelles le développeur peut définir et parcourir les relations entre les objets. Vous pouvez faire référence à Orders en tant que propriété de la classe Customer à l’aide du mappage, de sorte que vous n’avez pas besoin de jointures explicites pour lier les deux. Les fichiers de mappage externes permettent de séparer le mappage du modèle objet pour des fonctionnalités de mappage plus riches.

LINQ to XML : Intégration XML

.NET Language-Integrated Query for XML (LINQ to XML) permet aux données XML d’être interrogées à l’aide des opérateurs de requête standard ainsi que des opérateurs spécifiques à l’arborescence qui fournissent une navigation de type XPath à travers les descendants, les ancêtres et les frères et sœurs. Il fournit une représentation en mémoire efficace pour XML qui s’intègre à l’infrastructure de lecteur/enregistreur deSystem.Xml existante et est plus facile à utiliser que le DOM W3C. Il existe trois types qui effectuent la majeure partie du travail d’intégration de XML aux requêtes : XName, XElement et XAttribute.

XName offre un moyen facile à utiliser pour traiter les identificateurs qualifiés par l’espace de noms (QNames) utilisés comme noms d’éléments et d’attributs. XName gère l’atomisation efficace des identificateurs en toute transparence et permet d’utiliser des symboles ou des chaînes simples partout où un QName est nécessaire.

Les éléments et attributs XML sont représentés à l’aide de XElement et XAttribute respectivement. XElement et XAttribute prennent en charge la syntaxe de construction normale, ce qui permet aux développeurs d’écrire des expressions XML à l’aide d’une syntaxe naturelle :

var e = new XElement("Person", 
                     new XAttribute("CanCode", true),
                     new XElement("Name", "Loren David"),
                     new XElement("Age", 31));

var s = e.ToString();

Cela correspond au code XML suivant :

<Person CanCode="true">
  <Name>Loren David</Name> 
  <Age>31</Age> 
</Person>

Notez qu’aucun modèle de fabrique basé sur DOM n’était nécessaire pour créer l’expression XML, et que l’implémentation ToString a produit le code XML textuel. Les éléments XML peuvent également être construits à partir d’un XmlReader existant ou d’un littéral de chaîne :

var e2 = XElement.Load(xmlReader);
var e1 = XElement.Parse(
@"<Person CanCode='true'>
  <Name>Loren David</Name>
  <Age>31</Age>
</Person>");

XElement prend également en charge l’émission de XML à l’aide du type XmlWriter existant.

XElement s’aligne sur les opérateurs de requête, ce qui permet aux développeurs d’écrire des requêtes sur des informations non XML et de produire des résultats XML en construisant des XElements dans le corps d’une clause select :

var query = from p in people 
            where p.CanCode
            select new XElement("Person", 
                                  new XAttribute("Age", p.Age),
                                  p.Name);

Cette requête retourne une séquence de XElements. Pour permettre la génération de XElements à partir du résultat de ce type de requête, le constructeur XElement permet de passer directement des séquences d’éléments en tant qu’arguments :

var x = new XElement("People",
                  from p in people 
                  where p.CanCode
                  select 
                    new XElement("Person", 
                                   new XAttribute("Age", p.Age),
                                   p.Name));

Cette expression XML génère le code XML suivant :

<People>
  <Person Age="11">Allen Frances</Person> 
  <Person Age="59">Connor Morgan</Person> 
</People>

L’instruction ci-dessus a une traduction directe en Visual Basic. Toutefois, Visual Basic 9.0 prend également en charge l’utilisation de littéraux XML, qui permettent aux expressions de requête d’être exprimées à l’aide d’une syntaxe XML déclarative directement à partir de Visual Basic. L’exemple précédent peut être construit avec l’instruction Visual Basic :

 Dim x = _
        <People>
             <%= From p In people __
                 Where p.CanCode _

                 Select <Person Age=<%= p.Age %>>p.Name</Person> _
             %>
        </People>

Les exemples jusqu’à présent ont montré comment construire de nouvelles valeurs XML à l’aide d’une requête intégrée au langage. Les types XElement et XAttribute simplifient également l’extraction d’informations à partir de structures XML. XElement fournit des méthodes d’accesseur qui permettent d’appliquer des expressions de requête aux axes XPath traditionnels. Par exemple, la requête suivante extrait uniquement les noms de l’élément XElement indiqué ci-dessus :

IEnumerable<string> justNames =
    from e in x.Descendants("Person")
    select e.Value;

//justNames = ["Allen Frances", "Connor Morgan"]

Pour extraire des valeurs structurées du code XML, nous utilisons simplement une expression d’initialiseur d’objet dans notre clause select :

IEnumerable<Person> persons =
    from e in x.Descendants("Person")
    select new Person { 
        Name = e.Value,
        Age = (int)e.Attribute("Age") 
    };

Notez que XAttribute et XElement prennent en charge les conversions explicites pour extraire la valeur de texte en tant que type primitif. Pour traiter les données manquantes, nous pouvons simplement convertir en un type nullable :

IEnumerable<Person> persons =
    from e in x.Descendants("Person")
    select new Person { 
        Name = e.Value,
        Age = (int?)e.Attribute("Age") ?? 21
    };

Dans ce cas, nous utilisons une valeur par défaut de 21 lorsque l’attribut Age est manquant.

Visual Basic 9.0 fournit une prise en charge directe du langage pour les méthodes d’accesseur Elements, Attribute et Descendants de XElement, ce qui permet d’accéder aux données XML à l’aide d’une syntaxe plus compacte et directe appelée propriétés d’axe XML. Nous pouvons utiliser cette fonctionnalité pour écrire l’instruction C# précédente comme suit :

Dim persons = _
      From e In x...<Person> _   
      Select new Person { _
          .Name = e.Value, _
          .Age = IIF(e.@Age, 21) _
      } 

En Visual Basic, x...<Person> obtient tous les éléments de la collection Descendants de x portant le nom Person, tandis que l’expression e.@Age recherche tous les XAttributes portant le nom Age. La propriété Value obtient le premier attribut de la collection et appelle la propriété Value sur cet attribut.

Résumé

.NET Language-Integrated Query ajoute des fonctionnalités de requête au CLR et aux langages qui le ciblent. La fonctionnalité de requête s’appuie sur des expressions lambda et des arborescences d’expressions pour permettre aux prédicats, aux projections et aux expressions d’extraction de clés d’être utilisés comme code exécutable opaque ou comme données transparentes en mémoire adaptées au traitement ou à la traduction en aval. Les opérateurs de requête standard définis par le projet LINQ fonctionnent sur n’importe quelle source d’informations basée sur T> IEnumerable< et sont intégrés à ADO.NET (LINQ to SQL) et System.Xml (LINQ to XML) pour permettre aux données relationnelles et XML de bénéficier des avantages de la requête intégrée au langage.

Opérateurs de requête standard en bref

Opérateur Description
Where Opérateur de restriction basé sur la fonction de prédicat
Select/SelectMany Opérateurs de projection basés sur la fonction de sélecteur
Take/Skip/ TakeWhile/SkipWhile Opérateurs de partitionnement en fonction de la fonction de position ou de prédicat
Join/GroupJoin Opérateurs de jointure basés sur des fonctions de sélecteur de clé
Concat Concatenation (opérateur)
OrderBy/ThenBy/OrderByDescending/ThenByDescending Tri des opérateurs triant dans l’ordre croissant ou décroissant en fonction des fonctions facultatives de sélecteur de clé et de comparateur
Inverser Opérateur de tri inversant l’ordre d’une séquence
GroupBy Opérateur de regroupement basé sur des fonctions facultatives de sélecteur de clé et de comparateur
Distinct Définir l’opérateur supprimant les doublons
Union/Intersect Définir les opérateurs retournant l’union ou l’intersection du jeu
Except Opérateur set retournant la différence de jeu
AsEnumerable Opérateur de conversion en IEnumerable<T>
ToArray/ToList Opérateur de conversion en tableau ou liste<T>
ToDictionary/ToLookup Opérateurs de conversion en dictionnaire<K,T> ou recherche<K,T> (multi-dictionnaire) en fonction de la fonction de sélecteur de clé
OfType/Cast Opérateurs de conversion en IEnumerable<T> basés sur le filtrage par ou la conversion en argument de type
SequenceEqual Opérateur d’égalité vérifiant l’égalité des éléments au niveau des paires
First/FirstOrDefault/Last/LastOrDefault/Single/SingleOrDefault Opérateurs d’élément retournant un élément initial/final/uniquement basé sur une fonction de prédicat facultative
ElementAt/ElementAtOrDefault Opérateurs d’élément retournant l’élément en fonction de la position
DefaultIfEmpty Opérateur d’élément remplaçant la séquence vide par une séquence singleton à valeur par défaut
Plage Opérateur de génération retournant des nombres dans une plage
Répéter Opérateur de génération retournant plusieurs occurrences d’une valeur donnée
Vide Opérateur de génération retournant une séquence vide
Any/All Vérification du quantificateur de la satisfaction existentielle ou universelle de la fonction de prédicat
Contient Vérification du quantificateur pour la présence d’un élément donné
Count/LongCount Opérateurs d’agrégation qui comptent des éléments en fonction d’une fonction de prédicat facultative
Somme/Min/Max/Moyenne Opérateurs d’agrégation basés sur des fonctions de sélecteur facultatives
Agrégat Opérateur d’agrégation accumulant plusieurs valeurs basées sur la fonction d’accumulation et la valeur initiale facultative