Aller au contenu principal

LINQ — Language Integrated Query

Notions théoriques

LINQ (Language Integrated Query) est une fonctionnalité de C# qui permet d'écrire des requêtes directement dans le code, comme on le ferait avec SQL, mais sur n'importe quelle collection d'objets.

info

LINQ fonctionne sur toute collection qui implémente IEnumerable<T> : les tableaux, les List<T>, les Dictionary, et même les résultats de requêtes Entity Framework Core.

Les deux syntaxes LINQ

LINQ propose deux syntaxes équivalentes : la syntaxe méthode et la syntaxe query.

Syntaxe méthode (la plus courante) :

var vivants = personnages
.Where(p => p.PointsDeVie > 0)
.OrderBy(p => p.Nom)
.ToList();

Syntaxe query (proche du SQL) :

var vivants = from p in personnages
where p.PointsDeVie > 0
orderby p.Nom
select p;

Les deux syntaxes produisent exactement le même résultat. La syntaxe méthode est privilégiée dans la majorité des projets professionnels.

Les opérations LINQ principales

OpérationDescriptionExemple
WhereFiltrerliste.Where(p => p.Force > 10)
SelectTransformerliste.Select(p => p.Nom)
OrderByTrier (croissant)liste.OrderBy(p => p.Force)
OrderByDescendingTrier (décroissant)liste.OrderByDescending(p => p.Force)
GroupByRegrouperliste.GroupBy(p => p.Type)
FirstPremier élément (exception si vide)liste.First(p => p.Nom == "Aragorn")
FirstOrDefaultPremier élément ou nullliste.FirstOrDefault(p => p.Id == 1)
SingleUn seul élément (exception si plusieurs)liste.Single(p => p.Id == 5)
CountCompterliste.Count(p => p.Force > 10)
SumSommerliste.Sum(p => p.Force)
Max / MinMaximum / minimumliste.Max(p => p.Force)
AnyAu moins un élément correspondliste.Any(p => p.Force > 50)
AllTous les éléments correspondentliste.All(p => p.PointsDeVie > 0)

Matérialiser une requête LINQ

Une requête LINQ est paresseuse : elle n'est exécutée qu'au moment où vous itérez sur le résultat. Pour forcer l'exécution immédiate, utilisez une méthode de matérialisation :

var liste = requete.ToList(); // List<T>
var tableau = requete.ToArray(); // T[]
var dico = requete.ToDictionary(p => p.Id, p => p.Nom); // Dictionary<int, string>
attention

Appeler First() sur une collection vide lève une exception. Préférez FirstOrDefault() lorsque la collection peut être vide, et vérifiez ensuite si le résultat est null.

LINQ et Entity Framework Core

Avec Entity Framework Core, LINQ est traduit automatiquement en SQL. La requête n'est exécutée en base de données qu'au moment de la matérialisation :

// Cette requête génère : SELECT * FROM Personnages WHERE Force > 10 ORDER BY Nom
var personnages = await _context.Personnages
.Where(p => p.Force > 10)
.OrderBy(p => p.Nom)
.ToListAsync();

Exemple pratique

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

// Modèle de base
public class Personnage
{
public int Id { get; set; }
public string Nom { get; set; } = "";
public string Type { get; set; } = ""; // "Guerrier", "Mage", "Archer"
public int Force { get; set; }
public int PointsDeVie { get; set; }
}

class Program
{
static void Main()
{
var personnages = new List<Personnage>
{
new Personnage { Id = 1, Nom = "Aragorn", Type = "Guerrier", Force = 85, PointsDeVie = 120 },
new Personnage { Id = 2, Nom = "Gandalf", Type = "Mage", Force = 95, PointsDeVie = 0 },
new Personnage { Id = 3, Nom = "Legolas", Type = "Archer", Force = 70, PointsDeVie = 100 },
new Personnage { Id = 4, Nom = "Gimli", Type = "Guerrier", Force = 80, PointsDeVie = 90 },
new Personnage { Id = 5, Nom = "Saruman", Type = "Mage", Force = 90, PointsDeVie = 0 },
};

// 1. Filtrer les personnages vivants
var vivants = personnages.Where(p => p.PointsDeVie > 0).ToList();
Console.WriteLine("=== Personnages vivants ===");
foreach (var p in vivants)
Console.WriteLine($" {p.Nom} ({p.Type}) - Force : {p.Force}");

// 2. Trouver le plus fort (parmi les vivants)
var plusFort = vivants.OrderByDescending(p => p.Force).First();
Console.WriteLine($"\n=== Le plus fort : {plusFort.Nom} (Force {plusFort.Force}) ===");

// 3. Force moyenne des vivants
double moyenne = vivants.Average(p => p.Force);
Console.WriteLine($"\n=== Force moyenne des vivants : {moyenne:F1} ===");

// 4. Regrouper par type
var groupes = personnages.GroupBy(p => p.Type);
Console.WriteLine("\n=== Groupes par type ===");
foreach (var groupe in groupes)
{
Console.WriteLine($" {groupe.Key} :");
foreach (var p in groupe)
Console.WriteLine($" - {p.Nom} (PDV : {p.PointsDeVie})");
}

// 5. Dictionnaire Id → Nom
var dicoNoms = personnages.ToDictionary(p => p.Id, p => p.Nom);
Console.WriteLine($"\n=== Personnage #3 : {dicoNoms[3]} ===");
}
}

Test de mémorisation/compréhension


Que signifie l'acronyme LINQ ?


Quelle méthode LINQ permet de filtrer une collection selon un critère ?


Quelle différence y a-t-il entre First() et FirstOrDefault() ?


Quelle méthode permet de matérialiser une requête LINQ en List<T> ?


Que fait la méthode GroupBy() ?


Quel est le comportement d'une requête LINQ sans appel à ToList() ou ToArray() ?


TP pour réfléchir et résoudre des problèmes

Dans ce TP, vous allez analyser une liste de personnages RPG en utilisant LINQ. Vous partirez du code suivant :

var personnages = new List<Personnage>
{
new Personnage { Id = 1, Nom = "Aragorn", Type = "Guerrier", Force = 85, PointsDeVie = 120 },
new Personnage { Id = 2, Nom = "Gandalf", Type = "Mage", Force = 95, PointsDeVie = 0 },
new Personnage { Id = 3, Nom = "Legolas", Type = "Archer", Force = 70, PointsDeVie = 100 },
new Personnage { Id = 4, Nom = "Gimli", Type = "Guerrier", Force = 80, PointsDeVie = 90 },
new Personnage { Id = 5, Nom = "Saruman", Type = "Mage", Force = 90, PointsDeVie = 0 },
new Personnage { Id = 6, Nom = "Boromir", Type = "Guerrier", Force = 75, PointsDeVie = 0 },
new Personnage { Id = 7, Nom = "Frodo", Type = "Hobbit", Force = 30, PointsDeVie = 80 },
};

Étape 1 — Filtrer les personnages vivants

Récupérez uniquement les personnages dont les points de vie sont strictement supérieurs à 0.


Bonne pratique - Toujours matérialiser quand on réutilise le résultat

Appelez .ToList() ou .ToArray() dès lors que vous allez utiliser le résultat plusieurs fois (affichage + .Count + itération). Sans matérialisation, la requête LINQ est réexécutée à chaque accès, ce qui peut être coûteux sur une base de données.

Étape 2 — Trouver le personnage le plus fort parmi les vivants

À partir de la liste vivants obtenue à l'étape 1, trouvez le personnage qui a la force la plus élevée.


Bonne pratique - Préférer MaxBy() pour trouver l'élément maximum

En .NET 6 et supérieur, MaxBy(p => p.Force) est plus lisible et plus efficace que OrderByDescending(...).First() car il ne trie pas toute la liste. Utilisez-le dès que vous cherchez l'élément avec la valeur maximale d'une propriété.

Étape 3 — Regrouper les personnages par type

Regroupez tous les personnages (vivants ou non) par leur propriété Type.


Bonne pratique - Utiliser groupe.Key pour accéder à la valeur de regroupement

Lorsque vous itérez sur un IGrouping<TKey, TElement>, la propriété .Key contient la valeur commune à tous les éléments du groupe (ici le type du personnage). Ne recalculez pas cette valeur à partir des éléments.

Étape 4 — Calculer la force moyenne par type

Pour chaque type, calculez la force moyenne des personnages de ce type.


Bonne pratique - Chaîner GroupBy et Select pour des agrégations par groupe

La combinaison GroupBy(...).Select(g => new { ... }) est un pattern très courant pour calculer des statistiques par catégorie. Elle est équivalente à un GROUP BY ... SELECT AVG(...) en SQL.

📌 Une solution