Avec .NET 9, les générateurs de source (Source Generators) sont devenus un levier essentiel pour automatiser, optimiser ou fiabiliser certaines tâches à la compilation. Ils permettent d’injecter du code C# généré dynamiquement en fonction du contenu du projet, sans dépendre du runtime. Comment s'en servir ?
Dans cet article, je fais le point sur leur fonctionnement, leur intégration, et les cas d’usage réels.
🧠 Qu’est-ce qu’un générateur de source ?
Un Source Generator est une extension du compilateur Roslyn qui peut :
- Analyser le code existant (syntaxique ou sémantique)
- Générer du code C# (via SourceText) à inclure à la compilation
- Être utilisé sans modifier manuellement le code source original
Ils n’exécutent aucun code à l’exécution (contrairement aux T4 ou à la réflexion).
📦 Exemple minimal de générateur
[Generator]
public class HelloGenerator : ISourceGenerator
{
public void Initialize(GeneratorInitializationContext context) { }
public void Execute(GeneratorExecutionContext context)
{
var source = """
namespace AutoGenerated {
public static class Hello {
public static string Message => \"Hello from generated code!\";
}
}
""";
context.AddSource("hello.g.cs", SourceText.From(source, Encoding.UTF8));
}
}
Ce générateur produit une classe statique avec une propriété. On notera que le système de génération automatique du toolkit MVVM Microsoft utilise ce type de facilité pour simplifier l'écriture du code.
🧪 Exemple complet : INotifyPropertyChanged automatique
- Création de l’attribut personnalisé
[AttributeUsage(AttributeTargets.Class)]
public class AutoNotifyAttribute : Attribute { }
- Classe annotée côté consommateur
[AutoNotify]
public partial class Person
{
private string _name;
private int _age;
}
- Générateur
[Generator]
public class AutoNotifyGenerator : ISourceGenerator
{
public void Initialize(GeneratorInitializationContext context)
{
context.RegisterForSyntaxNotifications(() => new AutoNotifySyntaxReceiver());
}
public void Execute(GeneratorExecutionContext context)
{
if (context.SyntaxReceiver is not AutoNotifySyntaxReceiver receiver) return;
foreach (var classDecl in receiver.Candidates)
{
var model = context.Compilation.GetSemanticModel(classDecl.SyntaxTree);
var symbol = model.GetDeclaredSymbol(classDecl);
if (symbol is not INamedTypeSymbol namedType) continue;
var ns = namedType.ContainingNamespace.ToDisplayString();
var props = new StringBuilder();
foreach (var member in namedType.GetMembers().OfType<IFieldSymbol>())
{
var propName = char.ToUpper(member.Name[1]) + member.Name.Substring(2);
var type = member.Type.ToDisplayString();
props.AppendLine($"public {type} {propName} {{ get => {member.Name}; set => SetProperty(ref {member.Name}, value); }}");
}
var source = $$"""
using System.ComponentModel;
namespace {ns}
{
public partial class {namedType.Name} : INotifyPropertyChanged
{
public event PropertyChangedEventHandler? PropertyChanged;
protected bool SetProperty<T>(ref T storage, T value, [System.Runtime.CompilerServices.CallerMemberName] string? propertyName = null)
{
if (EqualityComparer<T>.Default.Equals(storage, value)) return false;
storage = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
return true;
}
{props}
}
}
""";
context.AddSource($"{namedType.Name}.g.cs", SourceText.From(source, Encoding.UTF8));
}
}
}
class AutoNotifySyntaxReceiver : ISyntaxReceiver
{
public List<ClassDeclarationSyntax> Candidates { get; } = new();
public void OnVisitSyntaxNode(SyntaxNode syntaxNode)
{
if (syntaxNode is ClassDeclarationSyntax cls && cls.AttributeLists.Count > 0)
{
Candidates.Add(cls);
}
}
}
Ce générateur ajoute automatiquement l’implémentation de INotifyPropertyChanged et des propriétés publiques aux champs privés d’une classe annotée.
🔧 Mise en place dans un projet .NET
- Créer un projet de type Analyzer :
dotnet new analyzer -n DemoSourceGen
- Ajouter une référence au package Roslyn :
dotnet add package Microsoft.CodeAnalysis.CSharp
- Compiler le générateur et référencer le .dll dans un projet consommateur :
<ItemGroup>
<Analyzer Include="..\DemoSourceGen\bin\Debug\netstandard2.0\DemoSourceGen.dll" />
</ItemGroup>
- Utiliser le code généré dans le projet :
var person = new Person { Name = "Alice", Age = 42 };
person.PropertyChanged += (s, e) => Console.WriteLine($"Changed: {e.PropertyName}");p
erson. Name = "Bob";
📚 Cas d’usage fréquents
- Génération de code pour la sérialisation (Json, XML, BSON...)
- Implémentation automatique d’interfaces (comme INotifyPropertyChanged)
- Création de proxy statiques ou factories
- Code répétitif (validation, services, mappers...)
- Génération de constantes (resx, settings...)
⚠️ Limitations et points de vigilance
- Pas d’accès au runtime (aucun appel File.ReadAllText, etc.)
- Compilation plus longue (à équilibrer selon le nombre de fichiers générés)
- Complexité croissante si mauvaise structuration
Toujours documenter ce que le générateur produit, surtout pour les équipes... (et pour soi-même car revenir un an après sur un code qu'on a écrit est parfois aussi nébuleux que de regarder le code d'un autre si tout n'a pas été fait dans les règles et documenté !).
🧩 Environnement et outils
- Compatible avec Visual Studio, Rider, VS Code (IntelliSense fonctionne sur le code généré)
- Diagnostic via GeneratorExecutionContext.ReportDiagnostic
- Débogage possible via Debugger.Launch() dans Execute()
🔚 Conclusion
Les générateurs de source en .NET 9 permettent d’automatiser des pans entiers de code répétitif ou optimisé. Ils favorisent la maintenabilité, la performance et l’évolution de vos architectures.
Stay Tuned !