Initialisation des systèmes...

Baptiste.Dev
Retour aux notes
LangagesIntermédiaireSérie : C# / .NET

C# : LINQ

Where, Select, OrderBy, GroupBy, exécution différée - les requêtes intégrées au langage C#.

7 min de lecture
Mis à jour il y a 4 mois
c#csharplinqwhereselectrequêtescollections

C# : LINQ

LINQ (Language Integrated Query) permet d'écrire des requêtes directement en C# pour filtrer, trier, transformer et regrouper des données. Ça fonctionne sur n'importe quelle collection : listes, tableaux, résultats de base de données, XML, JSON...

C'est l'une des fonctionnalités les plus puissantes de C#. Une fois que tu l'as adopté, tu ne peux plus t'en passer.


# Deux syntaxes

LINQ existe sous deux formes. Elles font exactement la même chose :

Syntaxe méthode (la plus courante)

List<int> nombres = new List<int> { 1, 5, 3, 8, 2, 9, 4, 7, 6 };
 
List<int> pairs = nombres
    .Where(n => n % 2 == 0)
    .OrderBy(n => n)
    .ToList();
// pairs = { 2, 4, 6, 8 }

Syntaxe query (ressemble au SQL)

List<int> pairs = (
    from n in nombres
    where n % 2 == 0
    orderby n
    select n
).ToList();

Les deux produisent le même résultat. La syntaxe méthode est plus utilisée dans la pratique parce qu'elle est plus flexible et se chaîne mieux.


# Les opérateurs essentiels

Where - filtrer

Garde uniquement les éléments qui respectent une condition :

List<string> villes = new List<string> { "Paris", "Lyon", "Marseille", "Lille", "Bordeaux" };
 
List<string> grandesVilles = villes
    .Where(v => v.Length > 4)
    .ToList();
// { "Paris", "Marseille", "Lille", "Bordeaux" }

Select - transformer

Projette chaque élément dans une nouvelle forme :

List<string> majuscules = villes
    .Select(v => v.ToUpper())
    .ToList();
// { "PARIS", "LYON", "MARSEILLE", "LILLE", "BORDEAUX" }

On peut aussi projeter vers un type anonyme ou un autre objet :

var infos = villes
    .Select(v => new { Nom = v, Longueur = v.Length })
    .ToList();
// { { Nom = "Paris", Longueur = 5 }, { Nom = "Lyon", Longueur = 4 }, ... }

OrderBy / OrderByDescending - trier

List<string> triees = villes
    .OrderBy(v => v)           // alphabétique
    .ToList();
 
List<string> inversees = villes
    .OrderByDescending(v => v.Length)  // par longueur décroissante
    .ToList();

Pour un tri secondaire, utilise ThenBy :

List<Personne> triees = personnes
    .OrderBy(p => p.Nom)
    .ThenBy(p => p.Prenom)
    .ToList();

First / FirstOrDefault - récupérer un élément

string premiere = villes.First();                        // "Paris"
string premierL = villes.First(v => v.StartsWith("L"));  // "Lyon"
 
// FirstOrDefault retourne null (ou la valeur par défaut) si rien ne matche
string? aucune = villes.FirstOrDefault(v => v.StartsWith("Z"));  // null

First() lève une InvalidOperationException si la collection est vide ou si aucun élément ne matche. Utilise FirstOrDefault() quand le résultat peut ne pas exister.

Any / All - vérifier une condition

bool auMoinsUn = villes.Any(v => v.StartsWith("P"));  // true
bool tousLongs = villes.All(v => v.Length > 3);         // true
bool estVide = villes.Any();                            // true (la liste n'est pas vide)

Count - compter

int total = villes.Count();                         // 5
int commencantParL = villes.Count(v => v.StartsWith("L"));  // 2

# Opérateurs avancés

GroupBy - regrouper

List<Personne> personnes = GetPersonnes();
 
var parVille = personnes
    .GroupBy(p => p.Ville)
    .ToList();
 
foreach (var groupe in parVille)
{
    Console.WriteLine($"{groupe.Key} : {groupe.Count()} personnes");
    foreach (Personne p in groupe)
    {
        Console.WriteLine($"  - {p.Nom}");
    }
}

GroupBy retourne des IGrouping<TKey, TElement> - chaque groupe a une Key (la valeur de regroupement) et contient les éléments du groupe.

SelectMany - aplatir

Quand chaque élément contient une collection, SelectMany les aplatit en une seule :

List<Classe> classes = GetClasses();
 
// Chaque classe a une liste d'élèves
List<Eleve> tousLesEleves = classes
    .SelectMany(c => c.Eleves)
    .ToList();

Sans SelectMany, tu aurais une List<List<Eleve>>. Avec, tu as directement une List<Eleve>.

Distinct - dédupliquer

List<string> villes = new List<string> { "Paris", "Lyon", "Paris", "Marseille", "Lyon" };
 
List<string> uniques = villes
    .Distinct()
    .ToList();
// { "Paris", "Lyon", "Marseille" }

Take / Skip - paginer

List<int> page = nombres
    .Skip(10)    // saute les 10 premiers
    .Take(5)     // prend les 5 suivants
    .ToList();

Aggregate - réduire

Applique une fonction d'accumulation sur toute la collection :

List<int> nombres = new List<int> { 1, 2, 3, 4, 5 };
 
int somme = nombres.Aggregate((acc, n) => acc + n);  // 15
string concat = villes.Aggregate((acc, v) => $"{acc}, {v}");  // "Paris, Lyon, Marseille, ..."

Pour la somme et la moyenne, utilise directement nombres.Sum() et nombres.Average() - pas besoin d'Aggregate.


# Exécution différée (deferred execution)

C'est le concept le plus important de LINQ et celui qui piège le plus de développeurs.

Le piège

List<int> nombres = new List<int> { 1, 2, 3, 4, 5 };
 
IEnumerable<int> pairs = nombres.Where(n => n % 2 == 0);
// Ici, RIEN ne s'est exécuté. pairs est juste une "requête en attente".
 
nombres.Add(6);
 
List<int> resultat = pairs.ToList();
// Maintenant la requête s'exécute - et 6 est inclus !
// resultat = { 2, 4, 6 }

Where, Select, OrderBy, GroupBy ne s'exécutent pas immédiatement. Ils retournent un IEnumerable<T> qui représente la requête. La requête ne s'exécute que quand on consomme le résultat (ToList(), ToArray(), foreach, First(), Count(), etc.).

Pourquoi c'est comme ça ?

  • Performance - si tu chaînes 5 opérations, LINQ ne parcourt la collection qu'une seule fois au moment de la consommation, pas 5 fois
  • Composabilité - tu peux construire une requête par morceaux avant de l'exécuter

Quand matérialiser ?

Appelle ToList() ou ToArray() quand :

  • Tu veux "figer" le résultat à un instant donné
  • Tu vas parcourir le résultat plusieurs fois (sinon la requête se ré-exécute à chaque fois)
  • Tu veux passer le résultat à du code qui attend une List<T>
// MAL - la requête se ré-exécute 3 fois
IEnumerable<int> pairs = nombres.Where(n => n % 2 == 0);
Console.WriteLine(pairs.Count());   // exécution 1
Console.WriteLine(pairs.First());   // exécution 2
Console.WriteLine(pairs.Last());    // exécution 3
 
// BIEN - matérialisé une seule fois
List<int> pairs = nombres.Where(n => n % 2 == 0).ToList();
Console.WriteLine(pairs.Count);     // lecture de la liste
Console.WriteLine(pairs.First());   // lecture de la liste
Console.WriteLine(pairs.Last());    // lecture de la liste

# Chaîner les opérations

La vraie puissance de LINQ, c'est le chaînage. Un exemple concret :

List<Commande> commandes = GetCommandes();
 
var rapport = commandes
    .Where(c => c.Date.Year == 2025)           // commandes de 2025
    .Where(c => c.Montant > 100)               // montant > 100
    .GroupBy(c => c.Client.Nom)                 // grouper par client
    .Select(g => new                            // projeter
    {
        Client = g.Key,
        NbCommandes = g.Count(),
        Total = g.Sum(c => c.Montant)
    })
    .OrderByDescending(x => x.Total)            // trier par total
    .Take(10)                                   // top 10
    .ToList();
 
foreach (var ligne in rapport)
{
    Console.WriteLine($"{ligne.Client} : {ligne.NbCommandes} commandes, {ligne.Total} euros");
}

En 10 lignes, tu as filtré, groupé, projeté, trié et paginé. L'équivalent en boucles for ferait 40+ lignes.


# Pièges courants

Modification pendant l'itération

// PLANTE - InvalidOperationException
foreach (string ville in villes.Where(v => v.StartsWith("L")))
{
    villes.Remove(ville);  // on modifie la collection pendant qu'on l'itère
}
 
// BIEN - matérialiser d'abord
List<string> aSupprimer = villes.Where(v => v.StartsWith("L")).ToList();
foreach (string ville in aSupprimer)
{
    villes.Remove(ville);
}

Null dans la collection

List<string?> noms = new List<string?> { "Alice", null, "Bob" };
 
// PLANTE - NullReferenceException sur le null
List<string> majuscules = noms.Select(n => n.ToUpper()).ToList();
 
// BIEN - filtrer les null d'abord
List<string> majuscules = noms
    .Where(n => n != null)
    .Select(n => n!.ToUpper())
    .ToList();

# La suite

Découvre aussi les autres piliers de la série :