Générer du code à la compilation : les source generators
Activator.CreateInstance résolu à la volée, tout ça se paie au runtime. Les source generators déplacent ce travail à l'autre bout du cycle, à la compilation . Le générateur lit votre code, en produit d'autre, et le compilateur intègre le résultat dans l'assembly comme si vous l'aviez tapé à la main.
Incremental, pas l'ancienne API
ISourceGenerator , avait un défaut structurel : elle re-exécutait tout le générateur à chaque frappe. Sur un gros projet, chaque caractère tapé relançait l'analyse complète, et l'éditeur ralentissait à mesure que le code grossissait.
IIncrementalGenerator change le modèle. Au lieu d'une fonction qui produit du code, on décrit un pipeline de transformations. Roslyn met en cache la sortie de chaque étape et compare, à la frappe suivante, l'entrée d'une étape avec sa valeur précédente. Si elle est identique, l'étape n'est pas rejouée : sa sortie déjà calculée est réutilisée. Un commentaire ajouté dans une méthode ne touche pas le modèle sémantique que lit votre générateur, le pipeline s'arrête tôt, et rien n'est régénéré.
Le pipeline en deux temps
ForAttributeWithMetadataName court-circuite tout ce filtrage pour le cas le plus courant, la détection d'un attribut marqueur. Le compilateur maintient un index des attributs et ne présente au générateur que les nœuds réellement décorés, ce qui évite de parcourir l'arbre entier. C'est l'entrée à privilégier ; CreateSyntaxProvider reste là pour les cas qui ne reposent pas sur un attribut.
static sur chaque lambda est délibéré. Une lambda qui capture une variable transporte cet état dans le pipeline et peut y introduire une référence non comparable, ce qui fait manquer le cache. RegisterPostInitializationOutput sert à émettre l'attribut lui-même : il devient disponible pour le reste de la compilation sans que le consommateur ait à référencer un package séparé.
Ce qui casse le cache
EqualityComparer<T>.Default . Si le type ne définit pas une égalité de valeur correcte, chaque frappe ressemble à un changement, le pipeline se rejoue en entier, et tout le bénéfice de l'incrémental disparaît.
ISymbol , un SyntaxNode , une SemanticModel ou une Compilation d'une étape à l'autre. Ces objets n'ont pas d'égalité de valeur, et surtout ils maintiennent en vie toute la compilation dont ils sont issus. La transformation doit en extraire aussitôt ce dont elle a besoin, dans un petit modèle plat.
ImmutableArray<T> compare son tableau sous-jacent par référence, pas élément par élément : un modèle qui expose un ImmutableArray<T> en champ paraîtra modifié à chaque passe, même à contenu identique. La parade habituelle est un petit wrapper qui compare par SequenceEqual , souvent nommé EquatableArray<T> , que la plupart des générateurs sérieux embarquent.
context.CompilationProvider : la Compilation change à chaque frappe. Le combiner directement à votre pipeline le fait tout recalculer. Si une seule information de la compilation vous est utile, réduisez-la d'abord avec Select vers une petite valeur comparable avant de la combiner.
Un cas concret : enregistrer la DI
[RegisterScoped] , laisser le générateur produire l'appel AddScoped correspondant, et collecter le tout dans une méthode d'extension. Program.cs cesse de s'allonger à chaque service ajouté, et le scan d'assembly par réflexion au démarrage disparaît.
Program.cs se réduit alors à builder.Services.AddGenerated(); . Le code produit est visible, débogable, et le compilateur le valide comme le vôtre. Activez <EmitCompilerGeneratedFiles> dans le .csproj pour retrouver les fichiers .g.cs sur le disque et les relire.
Remonter des diagnostics
context.ReportDiagnostic(Diagnostic.Create(MustBeConcrete, location, typeName)) dès qu'on repère le cas. L'erreur apparaît dans l'éditeur, soulignée sous le type fautif, à la même place qu'une erreur du compilateur, et n'atteint jamais l'exécution.
Build-time contre réflexion
ToStringFast sans allocation), la sérialisation : partout où l'on écrivait de la réflexion ou du code répétitif à la main, un générateur produit le même résultat à la compilation, une fois, et de façon lisible.
System.Text.Json génère ses convertisseurs via un générateur de source, et le logging à haute performance d'ASP.NET s'écrit avec [LoggerMessage] . La raison de fond dépasse la vitesse : le code généré est trimmable et compatible AOT/Native , là où la réflexion fait trébucher l'éditeur de liens. Et une erreur (un service oublié, un type non résolu) surgit à la compilation, pas à la première requête en production.
Un source generator produit le code que vous auriez écrit à la main, mais c'est le compilateur qui l'écrit et qui le vérifie. La métaprogrammation se joue à la compilation, et l'essentiel du métier est de garder le pipeline comparable par valeur pour que son cache tienne d'une frappe à l'autre.