Utiliser les générateurs de source en .NET 9 : aperçu, usages et exemples

 

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

  1. Création de l’attribut personnalisé
[AttributeUsage(AttributeTargets.Class)]
public class AutoNotifyAttribute : Attribute { }
  1. Classe annotée côté consommateur
[AutoNotify]
public partial class Person
{
    private string _name;
    private int _age;
}
  1. 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

  1. Créer un projet de type Analyzer :
dotnet new analyzer -n DemoSourceGen
  1. Ajouter une référence au package Roslyn :
dotnet add package Microsoft.CodeAnalysis.CSharp
  1. 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>
  1. 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 !