Les requêtes Linq proposent une nouvelle syntaxe permettant d’écrire des requêtes qui ressemblent de loin à des requêtes SQL. Pour ceux qui ne connaissent pas le SQL, il s’agit d’un langage permettant de faire des requêtes sur les bases de données.
Pour utiliser ces requêtes, il faut ajouter l’espace de noms adéquat, à savoir :
Code : C#
Ce
using est en général inclus par défaut lorsqu’on crée un nouveau fichier.
Jusqu’à maintenant, si nous voulions afficher les entiers d’une liste d’entiers qui sont strictement supérieur à 5, nous aurions fait :
Code : C# | List<int> liste = new List<int> { 4, 6, 1, 9, 5, 15, 8, 3 };
foreach (int i in liste)
{
if (i > 5)
{
Console.WriteLine(i);
}
}
|
Grâce à
Linq To Object, nous allons pouvoir filtrer en amont la liste afin de ne parcourir que les entiers qui nous intéressent, en faisant :
Code : C# | List<int> liste = new List<int> { 4, 6, 1, 9, 5, 15, 8, 3 };
IEnumerable<int> requeteFiltree = from i in liste
where i > 5
select i;
foreach (int i in requeteFiltree)
{
Console.WriteLine(i);
}
|
Qui donnera :
Code : Console
Nous avons ici créé une requête Linq qui contient des mots-clés, comme
from,
in,
where et
select. Ici, cette requête veut dire que nous partons (
from) de la liste « liste » en analysant chaque entier de la liste dans (
in) la variable i où (
where) i est supérieur à 5. Dans ce cas, il est sélectionné comme faisant partie du résultat de la requête grâce au mot-clé select, qui fait un peu office de
return.
Cette requête renvoie un
IEnumerable<int>. Le type générique est ici le type
int car c’est le type de la variable i qui est retournée par le
select. Le fait de renvoyer un
IEnumerable<> va permettre potentiellement de réutiliser le résultat de cette requête pour un filtre successif ou une expression différente. En effet, Linq travaille sur des
IEnumerable<> … Nous pourrions par exemple ordonner cette liste par ordre croissant grâce au mot-clé
orderby. Cela donnerait :
Code : C# | List<int> liste = new List<int> { 4, 6, 1, 9, 5, 15, 8, 3 };
IEnumerable<int> requeteFiltree = from i in liste
where i > 5
select i;
IEnumerable<int> requeteOrdonnee = from i in requeteFiltree
orderby i
select i;
foreach (int i in requeteOrdonnee)
{
Console.WriteLine(i);
}
|
qui pourrait également s’écrire en une seule fois avec :
Code : C# | List<int> liste = new List<int> { 4, 6, 1, 9, 5, 15, 8, 3 };
IEnumerable<int> requete = from i in liste
where i > 5
orderby i
select i;
foreach (int i in requete)
{
Console.WriteLine(i);
}
|
Et nous aurons :
Code : Console
L’intérêt est que grâce à ces syntaxes, nous pouvons combiner facilement plusieurs filtres et construire des requêtes plus ou moins complexes.
Par exemple, imaginons des clients :
Code : C# | public class Client
{
public int Identifiant { get; set; }
public string Nom { get; set; }
public int Age { get; set; }
}
|
que l’on souhaiterait savoir majeurs, puis triés par
Age puis par
Nom, nous pourrions faire :
Code : C# 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 | List<Client> listeClients = new List<Client>
{
new Client { Identifiant = 1, Nom = "Nicolas", Age = 30},
new Client { Identifiant = 2, Nom = "Jérémie", Age = 20},
new Client { Identifiant = 3, Nom = "Delphine", Age = 30},
new Client { Identifiant = 4, Nom = "Bob", Age = 10}
};
IEnumerable<string> requete = from client in listeClients
where client.Age > 18
orderby client.Age, client.Nom
select client.Nom;
foreach (string prenom in requete)
{
Console.WriteLine(prenom);
}
|
Ce qui donnera :
Code : Console
Notez ici que mon
select « renvoie » le nom du client, qui est un
string. Il est donc normal que ma requête renvoie un
IEnumerable<string> car j’ai choisi qu’elle ne sélectionne que les noms, qui sont de type
string.
Il est assez fréquent de construire des objets anonymes à l’intérieur d’une requête. Par exemple, plutôt que de renvoyer uniquement le nom du client, je pourrais également renvoyer l’
Age, mais comme je n’ai pas besoin de l’identifiant, je peux créer un objet anonyme juste avec ces deux propriétés :
Code : C# 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16 | List<Client> listeClients = new List<Client>
{
new Client { Identifiant = 1, Nom = "Nicolas", Age = 30},
new Client { Identifiant = 2, Nom = "Jérémie", Age = 20},
new Client { Identifiant = 3, Nom = "Delphine", Age = 30},
new Client { Identifiant = 4, Nom = "Bob", Age = 10},
};
var requete = from client in listeClients
where client.Age > 18
orderby client.Age , client.Nom
select new { client.Nom, client.Age };
foreach (var obj in requete)
{
Console.WriteLine("{0} a {1} ans", obj.Nom, obj.Age);
}
|
Et nous aurons :
Code : Console | Jérémie a 20 ans
Delphine a 30 ans
Nicolas a 30 ans |
Mon objet anonyme contient ici juste une propriété
Nom et une propriété
Age. À noter que je suis obligé à ce moment-là d’utiliser le mot-clé
var pour définir la requête, car je n’ai pas de type à donner à cette requête. De même, dans le
foreach je dois utiliser le mot-clé
var pour définir le type anonyme.
Les requêtes peuvent être de plus en plus compliquées, comme faisant des jointures. Par exemple, rajoutons une classe
Commande :
Code : C# | public class Commande
{
public int Identifiant { get; set; }
public int IdentifiantClient { get; set; }
public decimal Prix { get; set; }
}
|
Je peux créer des commandes pour des clients :
Code : C# 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17 | List<Client> listeClients = new List<Client>
{
new Client { Identifiant = 1, Nom = "Nicolas", Age = 30},
new Client { Identifiant = 2, Nom = "Jérémie", Age = 20},
new Client { Identifiant = 3, Nom = "Delphine", Age = 30},
new Client { Identifiant = 4, Nom = "Bob", Age = 10},
};
List<Commande> listeCommandes = new List<Commande>
{
new Commande{ Identifiant = 1, IdentifiantClient = 1, Prix = 150.05M},
new Commande{ Identifiant = 2, IdentifiantClient = 2, Prix = 30M},
new Commande{ Identifiant = 3, IdentifiantClient = 1, Prix = 99.99M},
new Commande{ Identifiant = 4, IdentifiantClient = 1, Prix = 100M},
new Commande{ Identifiant = 5, IdentifiantClient = 3, Prix = 80M},
new Commande{ Identifiant = 6, IdentifiantClient = 3, Prix = 10M},
};
|
Et grâce à une jointure, récupérer avec ma requête le nom du client et le prix de sa commande :
Code : C# | var liste = from commande in listeCommandes
join client in listeClients on commande.IdentifiantClient equals client.Identifiant
select new { client.Nom, commande.Prix };
foreach (var element in liste)
{
Console.WriteLine("Le client {0} a payé {1}", element.Nom, element.Prix);
}
|
Ce qui donne :
Code : Console | Le client Nicolas a payé 150,05
Le client Jérémie a payé 30
Le client Nicolas a payé 99,99
Le client Nicolas a payé 100
Le client Delphine a payé 80
Le client Delphine a payé 10 |
On utilise le mot-clé
join pour faire la jointure entre les deux listes puis on utilise le mot-clé
on et le mot-clé
equals pour indiquer sur quoi on fait la jointure.
À noter que ceci peut se réaliser en imbriquant également les
from et en filtrant sur l’égalité des identifiants clients :
Code : C# | var liste = from commande in listeCommandes
from client in listeClients
where client.Identifiant == commande.IdentifiantClient
select new { client.Nom, commande.Prix };
foreach (var element in liste)
{
Console.WriteLine("Le client {0} a payé {1}", element.Nom, element.Prix);
}
|
Il est intéressant de pouvoir regrouper les objets qui ont la même valeur. Par exemple pour obtenir toutes les commandes, groupées par client, on ferra :
Code : C# | var liste = from commande in listeCommandes
group commande by commande.IdentifiantClient;
foreach (var element in liste)
{
Console.WriteLine("Le client : {0} a réalisé {1} commande(s)", element.Key, element.Count());
foreach (Commande commande in element)
{
Console.WriteLine("\tPrix : {0}", commande.Prix);
}
}
|
Ici, cela donne :
Code : Console | Le client : 1 a réalisé 3 commande(s)
Prix : 150,05
Prix : 99,99
Prix : 100
Le client : 2 a réalisé 1 commande(s)
Prix : 30
Le client : 3 a réalisé 2 commande(s)
Prix : 80
Prix : 10 |
Il est possible de cumuler le
group by avec notre jointure précédente histoire d’avoir également le nom du client :
Code : C# 1
2
3
4
5
6
7
8
9
10
11
12 | var liste = from commande in listeCommandes
join client in listeClients on commande.IdentifiantClient equals client.Identifiant
group commande by new {commande.IdentifiantClient, client.Nom};
foreach (var element in liste)
{
Console.WriteLine("Le client {0} ({1}) a réalisé {2} commande(s)", element.Key.Nom, element.Key.IdentifiantClient, element.Count());
foreach (Commande commande in element)
{
Console.WriteLine("\tPrix : {0}", commande.Prix);
}
}
|
Et nous obtenons :
Code : Console | Le client Nicolas (1) a réalisé 3 commande(s)
Prix : 150,05
Prix : 99,99
Prix : 100
Le client Jérémie (2) a réalisé 1 commande(s)
Prix : 30
Le client Delphine (3) a réalisé 2 commande(s)
Prix : 80
Prix : 10 |
À noter que le
group by termine la requête, un peu comme le
select. Ainsi, si l’on veut sélectionner quelque chose après le
group by, il faudra utiliser le mot-clé
into et la syntaxe suivante :
Ce qui donnera :
Code : C# 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15 | var liste = from commande in listeCommandes
join client in listeClients on commande.IdentifiantClient equals client.Identifiant
group commande by new {commande.IdentifiantClient, client.Nom} into commandesGroupees
select
new
{
commandesGroupees.Key.IdentifiantClient,
commandesGroupees.Key.Nom,
NombreDeCommandes = commandesGroupees.Count()
};
foreach (var element in liste)
{
Console.WriteLine("Le client {0} ({1}) a réalisé {2} commande(s)", element.Nom, element.IdentifiantClient, element.NombreDeCommandes);
}
|
Avec pour résultat :
Code : Console | Le client Nicolas (1) a réalisé 3 commande(s)
Le client Jérémie (2) a réalisé 1 commande(s)
Le client Delphine (3) a réalisé 2 commande(s) |
L’intérêt d’utiliser le mot-clé
into est également de pouvoir enchainer avec une autre jointure ou autre filtre permettant de continuer la requête.
Il est également possible d’utiliser des variables à l’intérieur des requêtes grâce au mot-clé
let. Cela va nous permettre de stocker des résultats temporaires pour les réutiliser ensuite :
Code : C# 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18 | var liste = from commande in listeCommandes
join client in listeClients on commande.IdentifiantClient equals client.Identifiant
group commande by new {commande.IdentifiantClient, client.Nom} into commandesGroupees
let total = commandesGroupees.Sum(c => c.Prix)
where total > 50
orderby total
select new
{
commandesGroupees.Key.IdentifiantClient,
commandesGroupees.Key.Nom,
NombreDeCommandes = commandesGroupees.Count(),
PrixTotal = total
};
foreach (var element in liste)
{
Console.WriteLine("Le client {0} ({1}) a réalisé {2} commande(s) pour un total de {3}", element.Nom, element.IdentifiantClient, element.NombreDeCommandes, element.PrixTotal);
}
|
Par exemple, ici j’utilise le mot-clé
let pour stocker le total d’une commande groupée dans la variable
total (nous verrons la méthode
Sum() un tout petit peu plus bas), ce qui me permet ensuite de filtrer avec un
where pour obtenir les commandes dont le total est supérieur à 50 et de les trier par ordre de prix croissant.
Ce qui donne :
Code : Console | Le client Delphine (3) a réalisé 2 commande(s) pour un total de 90
Le client Nicolas (1) a réalisé 3 commande(s) pour un total de 350,04 |
Nous allons nous arrêter là pour cet aperçu des requêtes LINQ. Nous avons pu voir que le C# dispose d’un certain nombre de mots-clés qui permettent de manipuler nos données de manière très puissante mais d’une façon un peu inhabituelle.
Cette façon d’écrire des requêtes LINQ s’appelle en anglais la « sugar syntax », que l’on peut traduire par « sucre syntaxique ». Il désigne de manière générale les constructions d'un langage qui facilitent la rédaction du code sans modifier l'expressivité du langage.
Voyons à présent ce qu’il y a derrière cette jolie syntaxe.