Utiliser LINQ, le langage de requêtes intégré de C# .NET

LINQ, ou Language-Integrated Query (Requêtes intégrées au langage), est un espace de nom du framework .NET qui permet d’utiliser des requêtes directement dans le code.

En somme, nous décrivons au système la teneur d’une recherche, et grâce au fonctions de LINQ, nous allons pouvoir récupérer un ou plusieurs résultats pour les exploiter (ou savoir si rien ne correspond à la requête). C’est un outil puissant pour interroger une collection d’objets de manière lisible, sans devoir itérer sur toute la collection.

Il permet d’obtenir des sous-ensemble, ou directement une valeur voulue dans une liste, sans avoir à créer de méthode dédiée. Et tout ça avec du code compréhensible et lisible.

Connaissances de bases nécessaires pour utiliser LINQ

Le préambule ci-dessous contient des bases à avoir bien en tête pour utiliser LINQ.

Avant toute chose, pensez que vous aurez besoin du namespace System.Linq, qui est de toutes façons souvent ajouté d’office dans les gabarits de fichiers de code C# .NET :

using System.Linq;

Construction d’une requête en SQL

Les requêtes sont standardisées par le SQL (Structured Query Langage), un langage normalisé qui permet d’exploiter des bases de données relationnelles. La plupart des logiciels de gestion de base de données utilisent le SQL (MySQL, Oracle, Microsoft SQL Server…). C’est le langage de requêtes le plus globalement utilisé, et les opérateurs de requête de LINQ sont très proches de ceux-ci. Aussi, il est important de les connaître !

Une requête SQL va impérativement être composée des informations suivantes :

  1. L’ensemble qu’on interroge (FROM)
  2. L’ensemble qu’on souhaite obtenir (SELECT), qui peut contenir des champs venant de l’ensemble interrogé ou des champs calculés à la volée

Et généralement (même s’ils sont tous facultatifs), un ou plusieurs critères de requête :

  1. Des critères de sélection sur l’ensemble interrogé (WHERE)
  2. Des critères de regroupement (GROUP BY)
  3. Des critères de sélection sur l’ensemble obtenu (HAVING)
  4. Des critères de tri (ORDER BY) (trié par quelle(s) champ(s) ? Ascendant ou descendant ?)
  5. Des bornes sur l’ensemble obtenu (LIMIT, OFFSET)

Il existe également des commandes permettant de manipuler les ensembles, avec des opérations ensemblistes classiques (UNION, INTERSECT, EXCEPT).

Typiquement, si nous interrogeons un tableau people_highscores créé comme suit :

idfirst_namelast_namecountryhighscore
1LethiaElldredUnited States5247
2EmilieMcCuaigFrance1594
3WaylonGhioneUnited States6135
4MargaretaBleakmanFrance7949
5ChrisGrimbaldestonCanada8571
6WalshHenkerFrance7697
7EwenSaphinFrance4103
8WillytFeatherstonFrance862
9MikelScarsbrickUnited Kingdom8256
10FlemGallihawkCanada9545
Tableau people_highscores permettant d’illustrer les requêtes

La commande suivante, où nous définissions juste l’ensemble où nous recherchons et les champs que nous voulons avoir, va nous permettre d’obtenir un résultat avec la liste des prénoms et des noms :

SELECT first_name, last_name FROM people_highscores

Si nous voulons aller plus loin, nous pouvons ajouter un critère, par exemple pour obtenir uniquement les scores supérieur à 5000 :

SELECT first_name, last_name, highscore FROM people_highscores WHERE highscore > 5000

Il est possible d’obtenir la liste de tous les pays distincts, avec l’opérateur GROUP BY :

SELECT country FROM people_highscores GROUP BY country

Et dans SELECT, il est possible de créer un champ à la volée. Par exemple, en prenant la requête suivant, nous pouvons obtenir le score moyen par pays :

SELECT country, AVG(highscore) AS average_highscore FROM people_highscores GROUP BY country

Ici, « AVG » permet d’obtenir la moyenne et AS permet de renommer le champ dans le résultat final. Nous obtenons un tableau comme suit :

countryaverage_highscore
Canada9058
France4441
United Kingdom8256
United States5691
Résultat de moyenne avec GROUP BY

Vous pouvez approfondir votre connaissance du SQL avec des ressources comme SQL.sh (en français) et W3Schools (en anglais). Mais connaître ces principes de base évoqués ci-dessus est suffisant pour faire du LINQ.

Les types sur lesquels on peut faire une requête (extensions IEnumerable et IQueryable)

Le principe d’un système de requête, c’est d’exploiter un ensemble de données. C’est donc tout naturellement les types qui représentent des ensembles qui sont utilisables avec LINQ. Et naturellement, les collections C# sont particulièrement adaptées à cette tâche, notamment les listes.

D’un point de vue plus technique, les collections sont compatibles avec LINQ car elles utilisent l’interface IEnumerable. Cette interface définit plusieurs données énumérables, sur lesquels on peut par exemple faire une itération avec une boucle foreach.

L’interface IEnumerable est implémentée dans les types suivants : Array, ArrayList, List, HashTable, Dictionary, Collection, Stack et Queue. Mais par exemple, le type string implémente aussi cette interface (puisque c’est un tableau de char).

Le fait d’ajouter l’espace de nom System.Linq ajoute un jeu de méthode Enumerable, qui permet d’utiliser des requêtes sur tous les objets qui implémentent l’interface IEnumerable.

D’un côté nous avons IEnumerable, présent directement dans l’espace de nom Collections, et l’utilisation de System.Linq ajoute une autre interface, IQueryable. IQueryable hérite de IEnumerable et elle est surtout utile pour faire appel à des base de données et service distants (là où IEnumerable est utilisé pour les objets locaux).

L’essentiel à retenir, c’est que nous allons utiliser LINQ sur des types qui contiennent un jeu de données. Et quand nous lançons une requête, nous récupérerons une variable de type IEnumerable<T> (où T est le type présent dans l’objet IEnumerable sur lequel la requête est faite).

Typage implicite et types anonymes (avec le mot-clé var)

Deux dernières notions à connaître concernent les types.

Nous allons utiliser plusieurs fois le mot clé var qui permet de typer une variable de manière implicite. En somme, avec var, le compilateur cherche ce qu’on met dans la variable pour choisir son type.

var firstWord = "Implicite";        // firstWord est un string implicitement typé
var firstNumber = 123;              // firstNumber est un int implicitement typé

string secondWord = "Explicite";    // secondWord est un string explicitement typé
int secondNumber = 987;             // secondNumber est un int explicitement typé

La bonne pratique de code, c’est de toujours typer explicitement ses variables, donc on préférera mettre directement le nom du type que var. Mais il y a quelques exception, et c’est notamment le cas pour les types anonymes.

Un type anonyme, c’est un objet qui contient plusieurs propriétés de type différents. C’est un type générée à la volée pendant la compilation, et non disponible dans le code source Par exemple, nous pouvons construire un objet de type anonyme de cette manière, en définissant de manière implicite :

var landscape = new { Name = "Puy de Dôme", Height = 1465 };
// landscape est un object de type anonyme contenant une propriété Name et une propriété Height.
Console.WriteLine("Nom du lieu d'intérêt : " + landscape.Name);
Console.WriteLine("Altitude : " + landscape.Height);

Nous allons utiliser le typage implicite pour tous les résultats de requêtes obtenus avec LINQ. Mais quels que soient les résultats, ils implémenteront l’interface IEnumerable, ce qui permettra d’itérer sur ces résultats avec un foreach.

Utilisations de LINQ

Pour illustrer les exemples, nous avons repris la même base que vue dans la section consacrée au SQL, mais en créant une base d’objets de type Person :

public enum Country { France, UnitedKingdom, UnitedStates, Canada, Italy, Spain, Ireland }
public class Person
{
	public string FirstName;
	public string LastName;
	public Country Country;
	public int Highscore;
}

Avec une liste List<Person> peopleWithHighscore contenant toutes les données du tableau montré en exemple.

Création d’une requête et récupération de l’ensemble résultant

Il est possible de directement récupérer des éléments d’un ensemble. Ici, nous assignons directement la requête dans une variable peopleFromUS. Nous y retrouvons les mots-clés du langage SQL, en minuscules, avec les mêmes fonctions.

var peopleFromUS = from person in peopleWithHighscore
                   where person.Country == Country.UnitedStates
                   select new { FirstName = person.FirstName, LastName = person.LastName };

foreach(var person in peopleFromUS)
{
	Console.WriteLine($"Nom: {person.LastName}, prénom : {person.FirstName}");
}

Quelques explications :

  1. from permet de définir la source de données (spécifiée après le mot clé in) et une variable de portée locale qui représente les éléments dans la séquence source (ici, c’est person). Pour le reste de la requête, on peut exploiter les champs de cette variable (avec people.Country par exemple).
  2. where nous permet de définir les conditions de la requête, pour filtrer juste sur les valeurs qui nous intéressent
  3. select permet de définir les variables de sortie. Ici, nous créons un type à la volée avec le mot clé new.
  4. Le résultat est un ensemble de types Anonymes contenant les propriétés voulues .FirstName et .LastName.

Les tests dans where peuvent contenir plusieurs critères, avec les opérateurs logiques && (and) et || (or). Par exemple, si nous voulons avoir toutes les personnes de France ayant un score supérieur à 5000, nous pouvons utiliser la requête suivante :

var bestScoresFromFR = from person in peopleWithHighscore
                       where person.Highscore > 5000 && person.Country == Country.France
                       orderby person.Highscore descending
                       select person;

Ici, nous avons ajouté une clause orderby, suivie du champ sur lequel faire le tri, et l’ordre. Ainsi, bestScoresFromFR contient les données triées du score le plus élevé au score le moins élevé. Comme le select contient person, ce qu’on obtient est un IEnumerable qui contient directement des objets de la classe Person. Donc, si on itère sur les résultats, on pourrait directement utiliser des méthodes qui existent dans la classe.

Utilisation des méthodes Enumerable avec des fonctions lambda

Les clauses vu ci-dessous peuvent être directement remplacé par les méthodes. Avec une différence notable : il n’y a pas d’équivalent de from. Nous appliquons directement les méthodes sur notre ensemble. Et c’est directement dans les méthodes que nous écrivons une fonction lambda pour faire l’évaluation.

var peopleFromUS = peopleWithHighscore.Where(person => person.Country == Country.Canada)
                                      .Select(person => $"{person.FirstName} {person.LastName}");

Donc, cet exemple utilise la base peopleWithHighscore. Dans chacune des méthodes .Where et .Select, nous utilisons en argument la fonction lambda x => x.Propriété.

Ici, ce que nous sélectionnons, c’est une chaîne composée de FirstName et LastName.

Nous avons ainsi les méthodes suivantes disponibles :

Clause dans une requêteMéthode de EnumerableUtilisation
from element in EnsPas d’équivalentLes méthodes sont directement appliqués sur l’ensemble de type IEnumerable.
where element.Prop == aEns.Where(x => x.Prop == a)Un ou plusieurs opérateurs de sélection sur la propriété Prop.
select element.PropEns.Select(x => x.Prop);Choix de ce qui est renvoyé en valeurs de sortie.
orderby element.Age
orderby element.Score descending
Ens.OrderBy(x => x.Age)
Ensemble.OrderByDescending(x => x.Score)
Tri ascendant ou descendant sur un ou plusieurs champs.
groupby element.CountryEns.GroupBy(x => x.Country)Groupement par un champ particulier.
Requêtage LINQ par clause et par méthode

Méthodes de sélection avec LINQ : premier, dernier ou n-ième élément, valeurs extrêmes, moyenne, somme…

Outre ces requêtes, LINQ nous permet aussi de récupérer facilement des éléments dans une collection. Par exemple, si nous avons une série de notes comme ci-dessous, voici toutes ce que nous pouvons obtenir juste avec les méthodes de LINQ :

List<int> Grades = new List<int>() { 11, 12, 19, 16, 18, 19, 8, 14, 20, 9, 11, 12, 8, 16, 16, 20, 13, 12, 6, 11, 19, 9, 10 };

Console.WriteLine("Highest note: " + Grades.Max());         // Sortie console : 20
Console.WriteLine("Lowest note: " + Grades.Min());          // Sortie console : 6
Console.WriteLine("Class Average: " + Grades.Average());    // Sortie console : 13,434782608695652
Console.WriteLine("Number of notes: " + Grades.Count());    // Sortie console : 23
Console.WriteLine("Sum of all notes: " + Grades.Sum());     // Sortie console : 309

Les méthodes sont assez explicites, mais voici leur retour :

  1. Max() : valeur maximale dans la collection (ou dernier par ordre alphabétique)
  2. Min() : valeur minimale dans la collection (ou premier par ordre alphabétique)
  3. Count() : total d’éléments dans la collection
  4. Average() : moyenne des valeurs de la collection (types numériques uniquement)
  5. Sum() : total des valeurs de la collection (types numériques uniquement)

LINQ propose également des méthodes permettant de choisir un élément spécifique dans la liste. C’est très utile si vous utilisez une requête où vous obtenez en résultat l’énumération d’objets d’une classe précise. Vous pouvez obtenir le N-ième élément de type T et directement agir sur l’objet lui-même.

Ci-dessous, nous trions les éléments de peopleWithHighscore par ordre descendant. Avec les méthodes First() et Last(), nous pouvons obtenir dans le résultat le premier et le dernier élément (selon la manière dont la liste est classée). Nous pouvons également sélectionner un élément spécifique au rang N (la première valeur commence par 0), avec ElementAt(N). Nous vérifions au préalable que la liste contient bien 3 éléments ou plus.

var frenchPeople = peopleWithHighscore.Where(x => x.Country == Country.France).OrderByDescending(x => x.Highscore);
Console.WriteLine("Best score in France: " + frenchPeople.First().FirstName);
Console.WriteLine("Lowest score in France: " + frenchPeople.Last().FirstName);
if(frenchPeople.Count() >= 3)
{ 
	Console.WriteLine("Third score in France: " + frenchPeople.ElementAt(2).FirstName);
}

Chacune de ces méthodes a une version « XxxOrDefault() », qui permet de retourner une valeur par défaut si la valeur n’existe pas dans la collection (pour First et Last, c’est si la collection est vide). C’est la valeur par défaut du type qui est renvoyé (pour un int c’est 0, pour un string c’est Empty, etc.)

MéthodesMéthode sécuriséeUtilisation
Ensemble.First()Ensemble.FirstOrDefault()Premier élément de la collection
Ensemble.Last()Ensemble.LastOrDefault()Dernier élément de la collection
Ensemble.ElementAt(n)Ensemble.ElementAtOrDefault(n)Élément à l’index n de la collection (index commence par 0)
Exemple de méthodes pour sélectionner une valeur de l’ensemble

En combinant les requêtes vues plus haut avec ces possibilité, il vous est très facile de retrouver des objets dans une liste pour en faire ce que vous voulez.

Une dernière chose : il est possible de convertir un résultat de requête directement au format d’une List ou d’un Tableau, en utilisant respectivement les méthodes ToList() et ToArray(). En transformant le résultat de requête en une liste, vous pourrez alors manipuler et évaluer les éléments avec les méthodes disponibles pour une liste (Add, Remove, Contains…)

Par exemple, nous partons d’un résultat de requête, nous ajoutons un score et nous re-classons la liste :

List<Person> orderedPeople = frenchPeople.ToList();
orderedPeople.Add(new Person("Victor", "Winner", Country.France, 100000));
orderedPeople.OrderByDescending(person => person.Highscore);

Nous n’avons pas abordé les opérateurs de type jointure, intersection, et exclusion de LINQ, ça sera le sujet d’un prochain article.

2 thoughts on “Utiliser LINQ, le langage de requêtes intégré de C# .NET

Commentaires désactivés