1. Intro
Imagine une situation classique : t’as deux collections qui sont logiquement liées. Par exemple, une liste de catégories de produits et une liste des produits eux-mêmes. Il faut pour chaque catégorie récupérer tous les produits de cette catégorie. Ou alors, t’as une liste de départements d’entreprise et une liste d’employés, et tu veux afficher tous les employés pour chaque département.
En SQL, ça s’appelle une "jointure groupée" (GROUP JOIN ou, plus précisément, LEFT OUTER JOIN avec groupement). En LINQ, il y a un opérateur spécial pour ça – GroupJoin. C’est un peu entre la jointure classique (Join), où chaque élément de gauche correspond à un seul de droite, et le groupement par clé. GroupJoin relie chaque élément d’une collection à tous les éléments associés de l’autre collection sous forme de collection.
Une analogie
Si le Join classique, c’est comme faire des paires “papa et fils” par nom de famille, alors GroupJoin, c’est construire un arbre : à chaque papa, tu ajoutes la liste de tous ses enfants.
Schéma
Catégories Produits
+--------------+ +---------------------+
| Id | Nom | | Nom | CatId |
+----+---------+ +-----------+---------+
| 1 | Pain | ---> | Baguette | 1 |
| 2 | Boissons| | Saucisson | 3 |
| 3 | Viande | | Pepsi | 2 |
| | | | Thé | 2 |
+----+---------+ +-----------+---------+
Après GroupJoin :
- Pain — [Baguette]
- Boissons — [Pepsi, Thé]
- Viande — [Saucisson]
2. Signature de la méthode et bases
Méthode d’extension
public static IEnumerable<TResult> GroupJoin<TOuter, TInner, TKey, TResult>(
this IEnumerable<TOuter> outer, // Collection "externe" (genre catégories)
IEnumerable<TInner> inner, // Collection "interne" (genre produits)
Func<TOuter, TKey> outerKeySelector, // Comment choper la clé de l’élément externe
Func<TInner, TKey> innerKeySelector, // Comment choper la clé de l’élément interne
Func<TOuter, IEnumerable<TInner>, TResult> resultSelector // Fabrique pour créer l’objet/le record résultat
)
- outer : la collection qu’on parcourt, à laquelle on joint les éléments (genre les catégories).
- inner : la collection d’où on prend les éléments à joindre (genre les produits).
- outerKeySelector : lambda qui retourne la clé pour l’élément "de gauche".
- innerKeySelector : lambda qui retourne la clé pour l’élément "de droite".
- resultSelector : fonction qui permet de définir à quoi ressemble le résultat pour chaque paire (gauche+groupe de droite).
3. Exemple pratique : catégories et produits
Imaginons qu’on a ces modèles :
public class Category
{
public int Id { get; set; }
public string Name { get; set; }
}
public class Product
{
public string Name { get; set; }
public int CategoryId { get; set; }
}
Collections pour l’exemple :
var categories = new List<Category>
{
new Category { Id = 1, Name = "Pain" },
new Category { Id = 2, Name = "Boissons" },
new Category { Id = 3, Name = "Viande" }
};
var products = new List<Product>
{
new Product { Name = "Baguette", CategoryId = 1 },
new Product { Name = "Pepsi", CategoryId = 2 },
new Product { Name = "Thé", CategoryId = 2 },
new Product { Name = "Saucisson", CategoryId = 3 }
};
Utilisation de GroupJoin (Syntaxe méthode)
var groupJoin = categories.GroupJoin(
products,
category => category.Id, // clé de la catégorie
product => product.CategoryId, // clé du produit
(category, prods) => new // on construit le résultat à la volée
{
CategoryName = category.Name,
Products = prods.Select(p => p.Name).ToList() // liste des noms de produits de cette catégorie
}
);
Comment parcourir le résultat :
foreach (var group in groupJoin)
{
Console.WriteLine($"Catégorie : {group.CategoryName}");
foreach (var product in group.Products)
{
Console.WriteLine($" - {product}");
}
}
Sortie :
Catégorie : Pain
- Baguette
Catégorie : Boissons
- Pepsi
- Thé
Catégorie : Viande
- Saucisson
4. GroupJoin : Syntaxe requête (query syntax)
LINQ supporte une syntaxe qui ressemble au SQL. Pour le group join, on utilise le mot-clé join ... into ..., et cette requête marche quasi pareil que l’exemple d’avant.
var groupJoin2 = from c in categories
join p in products on c.Id equals p.CategoryId into prodGroup
select new
{
CategoryName = c.Name,
Products = prodGroup.Select(p => p.Name).ToList()
};
C’est très proche d’une requête SQL avec LEFT OUTER JOIN ... GROUP BY.
Schéma visuel : comment marche GroupJoin
[Catégorie] [Produit] Groupement (GroupJoin)
Pain --------> Baguette => Pain : [Baguette]
Boissons --------> Pepsi => Boissons : [Pepsi, Thé]
Boissons --------> Thé
Viande --------> Saucisson => Viande : [Saucisson]
Chaque catégorie reçoit sa “poche” (IEnumerable<Product>), où tombent tous les produits de cette catégorie.
5. Particularités et pièges
GroupJoin vs Join classique
La différence entre le Join classique et GroupJoin, c’est le nombre de résultats. Join retourne une paire pour chaque correspondance, alors que GroupJoin retourne un élément pour chaque élément de la collection externe, et à l’intérieur, une collection de tous les éléments correspondants.
Si dans notre schéma, il y a une catégorie sans produits, avec GroupJoin elle apparaîtra quand même, mais sa collection de produits sera vide. Ce comportement est pareil que le LEFT OUTER JOIN en SQL (jointure externe gauche).
Voilà un exemple avec une catégorie sans produits :
categories.Add(new Category { Id = 4, Name = "Fromages" });
var groupJoin3 = categories.GroupJoin(
products,
c => c.Id,
p => p.CategoryId,
(c, prods) => new
{
CategoryName = c.Name,
Products = prods.Select(p => p.Name).ToList()
});
foreach (var group in groupJoin3)
{
Console.WriteLine($"Catégorie : {group.CategoryName}");
if (group.Products.Count == 0)
Console.WriteLine(" (Aucun produit)");
else
foreach (var product in group.Products)
Console.WriteLine($" - {product}");
}
Catégorie : Pain
- Baguette
Catégorie : Boissons
- Pepsi
- Thé
Catégorie : Viande
- Saucisson
Catégorie : Fromages
(Aucun produit)
Ce genre de scénario est souvent demandé dans les applis métier : il faut afficher toutes les catégories (ou groupes), même si certaines n’ont pas d’éléments.
Faire ça avec GroupBy ? Nope !
Beaucoup se plantent en essayant “d’émuler” GroupJoin avec un double GroupBy. Faut pas — GroupJoin est fait pour ça, et fait la “jointure gauche” nativement.
6. Utilisation avec des vraies données
En plus des confs précédentes, on va ajouter à notre appli d’apprentissage la possibilité d’afficher un rapport : “Pour chaque catégorie — la liste de ses produits”. Ce besoin revient souvent dans les boutiques en ligne, CRM, systèmes de gestion ou de reporting.
On ajoute le code dans notre appli démo :
// Imaginons qu’on a les classes Category et Product et les collections déjà créées
Console.WriteLine("RAPPORT PAR CATÉGORIE ET PRODUITS :");
var categoryReport = categories.GroupJoin(
products,
cat => cat.Id,
prod => prod.CategoryId,
(cat, prods) => new
{
cat.Name,
ProductNames = prods.Select(p => p.Name).ToList()
});
foreach (var row in categoryReport)
{
Console.WriteLine($"Catégorie : {row.Name}");
if (row.ProductNames.Count == 0)
Console.WriteLine(" (Aucun article)");
else
foreach (var prodName in row.ProductNames)
Console.WriteLine($" - {prodName}");
}
7. Groupements imbriqués et agrégats
Tu peux combiner GroupJoin avec des fonctions d’agrégat pour faire des rapports plus costauds.
Exemple : Compter le nombre de produits dans chaque catégorie
var reportWithCount = categories.GroupJoin(
products,
category => category.Id,
product => product.CategoryId,
(category, prods) => new
{
Category = category.Name,
Count = prods.Count() // Fonction d’agrégat !
});
foreach (var rec in reportWithCount)
{
Console.WriteLine($"{rec.Category} : {rec.Count} article(s)");
}
Imaginons que les collections categories et products contiennent ces données :
var categories = new[]
{
new { Id = 1, Name = "Fruits" },
new { Id = 2, Name = "Légumes" },
new { Id = 3, Name = "Produits laitiers" }
};
var products = new[]
{
new { Id = 1, Name = "Pomme", CategoryId = 1 },
new { Id = 2, Name = "Banane", CategoryId = 1 },
new { Id = 3, Name = "Carotte", CategoryId = 2 }
};
La sortie console sera :
Catégorie : Fruits — 2 article(s)
Catégorie : Légumes — 1 article(s)
Catégorie : Produits laitiers — 0 article(s)
8. GroupJoin et erreurs classiques des débutants
Erreur fréquente : croire que le résultat de GroupJoin sera une table plate de paires, comme avec le Join classique. Par "plate", on veut dire une structure où chaque ligne est une paire : un élément externe et un interne correspondant (genre une table SQL après INNER JOIN).
C’est surtout perturbant pour ceux qui ont un peu touché aux bases de données : ils s’attendent à ce que GroupJoin se comporte comme un LEFT JOIN, mais qu’il retourne des paires en lignes, pas des groupes. Mais GroupJoin retourne un élément de la collection externe et une collection des éléments internes associés — donc une structure imbriquée.
N’oublie pas de “déplier” les collections imbriquées si besoin — par exemple avec SelectMany, si tu veux une séquence classique de paires.
Autre erreur courante : oublier que pour les éléments sans correspondance, le sous-groupe sera juste une liste vide. C’est le comportement par défaut, pas un bug — mais faut le savoir pour pas s’étonner si “rien ne s’affiche”.
Quand utiliser GroupJoin, et quand pas ?
Utilise GroupJoin :
- Quand t’as deux jeux de données (genre départements et employés, catégories et produits) et que tu veux les afficher de façon hiérarchique : pour chaque “parent” tous ses “enfants”.
- Pour faire des rapports complexes, où il faut montrer tous les éléments principaux même si y’a pas de “descendants”.
- Quand tu veux l’équivalent SQL du LEFT OUTER JOIN avec groupement par clé.
N’utilise pas GroupJoin si tu veux juste croiser des collections ou avoir exactement une paire par correspondance — là, le Join classique suffit.
GO TO FULL VERSION