Préambule : Ceci n’est absolument pas un billet destiné à râler sur un bug ou sur un défaut de conception honteux dans LINQ / C# 3.0 : le comportement décrit ci-dessous est une erreur de programmation de notre coté (voir Préambule N°2) et non quelque chose d’induit par LINQ/IQueryable. Cela nous apprendra à ne pas lire la documentation d’une méthode et à ne pas faire attention à la méthode juste en dessous dans la boite de dialogue Intellisense.
Préambule N°2: Nous connaissons IQueryable.FirstOrDefault() qui constitue la réponse aux mots maux (raaaah des fois c’est bien de se relire, ca évite de passer pour un c…ne sachant même pas faire la différence entre “mots” et “maux”) décrits ci dessous.
Cette fin de matinée, un samedi qui plus est, a été source de grande joie pour moi : je viens de passer un peu plus de deux heures sur un bug d’une incroyable stupidité…
L’un des nos clients, en ces derniers jours de préparation de colis pour Noël, a eu besoin de l’écriture en urgence d’un outil destiné à remplacer des terminaux défectueux. Aussi tôt dit, presque (il fallait quand arriver à trouver le temps de programmer…) aussitôt fait : nous avons réalisé une mini-application à partir de WinForms et LINQ. Celle-ci, bien qu’elle n’est pas près de figurer dans la catégorie des applications les mieux conçues, remplissait son métier… enfin… pendant quelques heures : ce matin, catastrophe, il y a beaucoup de messages d’erreurs et cela ralenti considérablement une logistique déjà sous pression.
Les symptômes sont assez curieux : lors du traitement, les gens obtiennent souvent une erreur correspondant à des règles de gestion internes alors qu’ils sont sûr que celles-ci sont bien respectées. Chose encore plus étrange, parfois un simple nouvel essai permet de corriger le problème, d’autre fois il suffit d’attendre un peu et de re-essayer et cela fonctionne. Après examen du code, nous réduisons le code “fautif” à ces quelques lignes :
var produits = from ... in linqContext. ...
where .....
select ....;
try
{
var produit = produits.First();
if(produit == null)
{
//... affiche l'erreur ...
}
else
{
// ...fait le traitement nécessaire...
}
}
catch // plusieurs blocs catch pour traiter les différents cas
{
//... affiche l'erreur ...
}
Code qui me semble parfaitement valide, j’entreprends donc de monter Visual studio sur l’environnement de production et de commencer à tester en production. Après quelques ratés, nous trouvons des cas où, effectivement, l’erreur apparaît sans raison. Celle ci est affichée par une InvalidOperationException lors de l’appel à IQueryable.First() et dont le message est “le résultat ne contient aucune ligne” (ou quelque chose du genre, je n’ai plus le message en tête). Et la, donc, drame : IQueryable.First() renvoie sous forme d’erreur quelque chose qui n’est, pour moi, pas de l’ordre de l’exception mais bien d’un traitement “normal”.
Hormis le fait que la documentation MSDN ne liste pas cette exception dans celles possibles pour cette méthode (pour la première fois de ma vie, l’obligation de déclaration des exceptions propre à java m’a semblé utile… comparé aux milliers de fois où il m’a énervé lorsque j’en faisait ^^), le retour sous forme d’exception m’a quelque peu choqué. Combien de fois, en effet, avez vous écrit en (T-) SQL : select top 1 … from …. ? Dans ce genre de cas, car c’est bien cela que semble exprimer IQueryable.First(), on peux raisonnablement s’attendre à ce que le résultat “il n’y a pas de première ligne”, ne soit pas de type “exceptionnel”.
Tout cela pour dire que, si vous mettez à disposition d’une équipe de développeurs un composant, il est primordial de bien comprendre et de bien définir le contrat que votre composant propose, et en particulier d’évaluer le plus possible ce qui tient des postulats “implicites” de vos fonctions (dans le cas présent, le postulat étant qu’un IQueryable ne peut pas contenir 0 éléments) et de les documenter au fur et à mesure que ceux-ci apparaissent (certain n’apparaîtront en effet que longtemps après l’implémentation, lorsqu’un développeur aura la mauvaise idée de ne pas le(s) respecter).
Comme je le disais en préambule, l’équipe chargée de cette partie de LINQ/C# 3.0 a parfaitement géré le problème : il existe deux méthodes : First() et FirstOrDefault() (conservant même la notion de …OrDefault propre aux objets “nullables”) qui permettent de choisir le type de comportement que l’on souhaite.
Pour la petite histoire, l’erreur ne provenait absolument pas de cette erreur. Nous n’affichions pas le message d’erreur exact, peut-être, mais le message d’erreur était proche de ce qu’il devait être. Le fautif a fini par être trouvé : les outils de saisie, parfois, transmettaient des informations incohérentes (quand je vous disais que le bug était d’un incroyable stupidité…).
Post-Scriptum : Ce billet ne doit son existence qu’à “de saines lectures”, en l’occurrence Eric Lippert. L’un de ses billets, lu par hasard il y a quelques jours, a très fortement résonné dans mon esprit ce matin pendant cette phase de “debug en production” et, sans celui-ci, j’aurais probablement haussé les épaules et continué. Bien que le sujet ne soit pas exactement le même, il y parle du même type de problématique : l’implémentation d’une méthode, ainsi que les postulats implicites de l’équipe développant un composant, font partie intégrante du contrat.