LINQ permet d'effectuer simplement, de façon compacte et intuitive des opérations sur des séquences. Certaines opérations peuvent cependant être déroutantes.

LINQ (Language-Integrated Query) est le nom d’un ensemble de technologies basé sur l’intégration de fonctions de requête directement dans le langage C#.

Il permet notamment d'effectuer simplement, de façon compacte et intuitive des opérations sur des séquences comme des filtres, des projections, des partitionnements, des agrégations.

Il est souvent perçu comme un outil sûr, élégant et lisible. On enchaîne des opérations, on exprime une intention, et le résultat suit.

Mais certaines méthodes cachent des contrats implicites qui, si on ne les connaît pas, peuvent poser des soucis et introduire des bugs dans vos applications.

Un cas métier très classique

Prenons un exemple simple : une commande e-commerce.

Chaque commande contient des lignes, et chaque ligne a un prix unitaire et une quantité.

public class OrderLine {

    public decimal UnitPrice { get; }
    public int Quantity { get; }

    public OrderLine(decimal unitPrice, int quantity) {
        UnitPrice = unitPrice;
        Quantity = quantity;
    }

    public decimal GetTotal() => UnitPrice * Quantity;
}

On veut calculer le montant total de la commande.

Cas normal :

var lines = new[] {
    new OrderLine(10m, 2),
    new OrderLine(5m, 1),
    new OrderLine(3m, 4)
};

decimal total = lines
    .Select(l => l.GetTotal())
    .Aggregate((prev, next) => prev + next);

Console.WriteLine(total); // 37

Tout fonctionne.

Le cas réel : commande vide

Maintenant, cas parfaitement réaliste : une commande créée mais encore vide.

var lines = Array.Empty<OrderLine>();

decimal total = lines
    .Select(l => l.GetTotal())
    .Aggregate((prev, next) => prev + next);

Et là :

InvalidOperationException: Sequence contains no elements

Surprenant, non ? On pourrait s’attendre à un total de 0.

Le vrai problème

Le problème ne vient ni de Select, ni de LINQ en général. Il vient de la surcharge de Aggregate utilisée :

Aggregate((prev, next) => ...)

Cette version n’a pas de valeur initiale (seed). LINQ prend donc le premier élément de la séquence comme point de départ.

Donc :

  • s’il y a au moins un élément → OK
  • s’il n’y en a aucun → impossible de démarrer → exception

Autrement dit, cette version de Aggregate suppose implicitement :

“Je te garantis qu’il y a au moins un élément.”

Et dans un cas métier réel… ce n’est pas toujours vrai.

Les deux fausses bonnes idées

Quand on découvre le problème, deux solutions peuvent venir à l’esprit.

❌ 1. Tester avec Any()

decimal total = lines.Any()
    ? lines.Select(l => l.GetTotal()).Aggregate((p, n) => p + n)
    : 0m;

Ça fonctionne. Mais ce n’est pas une bonne solution.

Pourquoi ?

  • Double énumération : Any() parcourt la séquence, puis Aggregate() la parcourt à nouveau. Sur certaines sources (requêtes, flux…), ça peut être coûteux ou problématique.
  • Rupture du pipeline LINQ : on sort du flux fonctionnel pour injecter une logique conditionnelle externe.
  • Intention métier dispersée : la règle “une commande vide = total 0” n’est pas portée par l’agrégation elle-même.

❌ 2. Utiliser DefaultIfEmpty

decimal total = lines
    .Select(l => l.GetTotal())
    .DefaultIfEmpty()
    .Aggregate((p, n) => p + n);

Cette version fonctionne. Pas d’exception. Mais elle est conceptuellement trompeuse. Pourquoi ?

Elle modifie la nature des données

DefaultIfEmpty() transforme une séquence vide en séquence contenant un élément par défaut.

[] -> [0]

On ne calcule plus :

la somme de 0 éléments

mais :

la somme d’un élément artificiel égal à 0

Ce n’est pas la même sémantique.

Elle introduit une dépendance implicite au type

Ici ça marche parce que default(decimal) = 0. Mais pour d’autres types, la valeur par défaut n’a aucun sens métier.

Elle crée des effets de bord si le calcul évolue

Imagine que demain on calcule :

  • une moyenne
  • un nombre d’éléments
  • une statistique

Avec DefaultIfEmpty, on introduit un élément fictif qui fausse les résultats.

La bonne utilisation d’Aggregate

La bonne solution consiste à utiliser la surcharge avec valeur initiale :

decimal total = lines
    .Select(l => l.GetTotal())
    .Aggregate(0m, (acc, next) => acc + next);

Ici :

  • pas d’exception
  • une seule énumération
  • la règle métier est explicite
  • aucune donnée artificielle n’est introduite

Et en pratique… ce n’est même pas le bon outil

Pour ce cas précis, LINQ fournit déjà une méthode adaptée :

decimal total = lines.Sum(l => l.GetTotal());

C’est plus lisible, plus direct, et parfaitement aligné avec l’intention métier.


À retenir

Quand vous utilisez Aggregate, posez-vous toujours ces questions :

  1. La séquence peut-elle être vide ?
  2. Si oui, ai-je fourni une valeur initiale ?
  3. Existe-t-il une méthode LINQ plus adaptée (Sum, Join, Count, etc.) ?

Parce que sinon, le jour où la séquence est vide… ce n’est pas un 0 que vous obtiendrez. C’est une exception. 💥

Références