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.
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ération | Description | Exemple |
|---|---|---|
Where | Filtrer | liste.Where(p => p.Force > 10) |
Select | Transformer | liste.Select(p => p.Nom) |
OrderBy | Trier (croissant) | liste.OrderBy(p => p.Force) |
OrderByDescending | Trier (décroissant) | liste.OrderByDescending(p => p.Force) |
GroupBy | Regrouper | liste.GroupBy(p => p.Type) |
First | Premier élément (exception si vide) | liste.First(p => p.Nom == "Aragorn") |
FirstOrDefault | Premier élément ou null | liste.FirstOrDefault(p => p.Id == 1) |
Single | Un seul élément (exception si plusieurs) | liste.Single(p => p.Id == 5) |
Count | Compter | liste.Count(p => p.Force > 10) |
Sum | Sommer | liste.Sum(p => p.Force) |
Max / Min | Maximum / minimum | liste.Max(p => p.Force) |
Any | Au moins un élément correspond | liste.Any(p => p.Force > 50) |
All | Tous les éléments correspondent | liste.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>
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
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.
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.
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.
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.
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.