CodeGym /Cours /C# SELF /Source Generators et...

Source Generators et System.Reflection.Emit

C# SELF
Niveau 63 , Leçon 4
Disponible

1. Introduction

Soyons honnêtes : la plupart des gens voient la génération de code un peu comme un coupe-œufs — utile, mais rarement indispensable. Pourtant, les projets .NET modernes deviennent de plus en plus complexes, et automatiser le boulot routinier ou répétitif dans le code n'est pas juste une "feature" mais un outil important pour améliorer la qualité et la productivité.

Source Generators est un mécanisme apparu à partir de C# 9 et .NET 5. Ils agissent comme des super-héros à l'étape de compilation : ils peuvent générer du code C# qui sera ensuite compilé comme partie de votre projet. Ils n'interviennent pas dans le code assemblé déjà compilé à l'exécution, ils étendent simplement le projet avec de nouvelles sources avant la compilation.

Ci-dessous, les scénarios usuels d'utilisation des Source Generators.

Génération automatique de code template

Dans beaucoup de projets il faut écrire du code répétitif : constructeurs, méthodes ToString, serializers, property change notification (INotifyPropertyChanged) etc. Les Source Generators peuvent générer ce code automatiquement, évitant aux devs la routine et les risques de coquilles.

Exemple : Generator ToString

Imaginons que vous développez des classes DTO (Data Transfer Object) pour passer des données. Il faut implémenter pour chaque classe un ToString sensé qui énumère toutes les propriétés.

Au lieu de copier-coller à la main, vous pouvez créer un Source Generator qui pour chaque classe avec l'attribut [AutoToString] générera l'implémentation de la méthode ToString. C'est ce que font, par exemple, des libs comme AutoToString.


// Votre classe avec l'attribut
[AutoToString] 
public partial class Person 
{
    public string Name { get; set; }
    public int Age { get; set; }
}

// Le Source Generator générera (simplifié):
public partial class Person
{
    public override string ToString() => $"Person: Name={Name}, Age={Age}";
}

Avantages : le code est lisible et toujours mis à jour quand on ajoute de nouvelles propriétés.

Simplification de la sérialisation et de la désérialisation

Source Generators sont largement utilisés dans la librairie standard System.Text.Json pour générer du code rapide de sérialisation/désérialisation. Avant les generators, la sérialisation reposait souvent sur la reflection (coûteuse en perf), maintenant le code généré est rapide et sûr.

Qu'est-ce que le dev obtient ? Vous déclarez [JsonSerializable(typeof(MyType))] — et le generator crée un code performant de sérialisation pour ce type.

Génération de code pour configs, mappers, conteneurs DI

  • Configs : les generators créent automatiquement des classes de configuration à partir de fichiers JSON.
  • Mappers : par exemple, le projet Mapster utilise des generators pour créer le mapping entre types sans copier manuellement les champs.
  • Dependency Injection : certains conteneurs (par ex. StrongInject) utilisent des generators pour le code d'enregistrement des services.

Intégration avec des infrastructures externes

Certains generators analysent des ressources externes (descriptions d'API, schémas GraphQL, protocoles Thrift, etc.) et génèrent des classes C# pour travailler avec elles. Ça évite de maintenir manuellement le code quand les contrats changent.

Vérification et diagnostics au moment de la compilation

Source Generators peuvent aussi servir à vérifier le code : "Si le projet contient une méthode X mais que la condition Y n'est pas remplie — avertissement dans le compilateur !". Beaucoup de linters et analyzers font ça, mais un generator peut ajouter ses messages ou même injecter un stub de code prêt à l'emploi.

2. Votre Source Generator pour une sérialisation automatique

Regardons un petit exemple "brut" : un generator qui pour une classe avec l'attribut [AutoJson] génère une méthode de sérialisation en JSON.

Le code ci-dessous est seulement illustratif ; en production utilisez System.Text.Json.SourceGeneration.


// On écrit à la main :
[AutoJson]
public partial class Book
{
    public string Title { get; set; }
    public int Year { get; set; }
}

// Le generator ajoutera :
public partial class Book
{
    public string ToJson() => $"{{ \"Title\": \"{Title}\", \"Year\": {Year} }}";
}

Schéma d'interaction entre le Source Generator et le projet

flowchart TD
    A(Votre Source Code) -->|Le compilateur appelle| B(Source Generator)
    B -->|Code ajouté| C(Nouveaux .cs-fichiers)
    C --> D(Compilation du projet)
  • D'abord le compilateur (Roslyn) analyse vos sources.
  • Puis il appelle "votre" generator (implémentant ISourceGenerator).
  • Le generator ajoute de nouveaux .cs qui deviennent partie de l'arbre de compilation.
  • À la fin, l'assembly contient votre code et le code généré.

Tâches spécialisées : quoi d'autre générer

  • Bindings pour libs natives (wrappers pour du code C ou WinAPI).
  • AOP : autologging des appels, aspects (à la Fody, PostSharp, etc.).
  • Descriptions d'API pour générateurs automatiques de docs.
  • Traitement de ressources externes : SVG, SQL, Razor — le generator crée des classes strongly-typed pour un accès type-safe.

3. Génération dynamique de code avec System.Reflection.Emit

Si les Source Generators génèrent du C# avant la compilation, System.Reflection.Emit est l'outil pour la magie à l'exécution. Votre programme peut créer lui-même de nouveaux types, méthodes et même assemblies — à la volée !

Ça peut effrayer ? Un peu. Mais parfois c'est indispensable : par exemple pour écrire des frameworks de proxy dynamique (AOP, profiling, mocking), des generators de sérialiseurs basés sur des données runtime, des ORM dynamiques, etc.

Quand utiliser Reflection.Emit

  • Les types ne sont pas connus à l'avance (l'utilisateur définit la structure à la volée).
  • Proxy dynamique (wrappers pour intercepter des appels).
  • Sérialisation haute-perf (par exemple, protobuf-net).
  • Plugins et moteurs de scripts avec des scénarios de chargement complexes.

Ce qu'on peut créer via Reflection.Emit

  • AssemblyBuilder — création d'une nouvelle assembly.
  • ModuleBuilder — module dans l'assembly.
  • TypeBuilder — description d'un nouveau type.
  • MethodBuilder — méthode avec IL.
  • PropertyBuilder, FieldBuilder, EventBuilder — propriétés, champs, événements.

Mini-exemple : créer une nouvelle classe à la volée

using System;
using System.Reflection;
using System.Reflection.Emit;

public static class DynamicTypeGenerator
{
    public static Type GenerateSimpleType(string typeName)
    {
        // 1. On crée l'assembly et le module
        var assemblyName = new AssemblyName("DynamicAssembly");
        var assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run);
        var moduleBuilder = assemblyBuilder.DefineDynamicModule("MainModule");

        // 2. On crée une nouvelle classe
        var typeBuilder = moduleBuilder.DefineType(
            typeName, 
            TypeAttributes.Public | TypeAttributes.Class
        );

        // 3. On ajoute une propriété publique string Title
        var field = typeBuilder.DefineField("_title", typeof(string), FieldAttributes.Private);
        var prop = typeBuilder.DefineProperty("Title", PropertyAttributes.HasDefault, typeof(string), null);
        var getMethod = typeBuilder.DefineMethod("get_Title", MethodAttributes.Public, typeof(string), Type.EmptyTypes);
        var il = getMethod.GetILGenerator();
        il.Emit(OpCodes.Ldarg_0);      // this
        il.Emit(OpCodes.Ldfld, field); // _title
        il.Emit(OpCodes.Ret);
        prop.SetGetMethod(getMethod);

        var setMethod = typeBuilder.DefineMethod("set_Title", MethodAttributes.Public, null, new[] { typeof(string) });
        il = setMethod.GetILGenerator();
        il.Emit(OpCodes.Ldarg_0);
        il.Emit(OpCodes.Ldarg_1);
        il.Emit(OpCodes.Stfld, field);
        il.Emit(OpCodes.Ret);
        prop.SetSetMethod(setMethod);

        // 4. C'est prêt ! On crée le Type
        return typeBuilder.CreateTypeInfo();
    }
}

Maintenant on peut utiliser ce type comme un objet C# normal, par exemple via reflection :

var dynamicType = DynamicTypeGenerator.GenerateSimpleType("Book");
var obj = Activator.CreateInstance(dynamicType);
dynamicType.GetProperty("Title").SetValue(obj, "C# en personnes");
Console.WriteLine(dynamicType.GetProperty("Title").GetValue(obj)); // C# en personnes

C'est comme ça qu'on crée des ORM, des proxies, des serializers, des profilers et même certains frameworks de test.

4. Nuances utiles

Source Generators vs Reflection.Emit : qui pour quoi ?

Source Generators travaillent à la compilation : ils rendent votre code source plus intelligent et le résultat entre dans l'assembly compilé. On ne peut pas les utiliser pour générer du code basé sur des données reçues durant l'exécution du programme.

Reflection.Emit fonctionne à l'exécution : il permet de créer dynamiquement assemblies, types et méthodes, mais ce code est plus dur à déboguer et à maintenir.

Quand utiliser quoi ?

Une analogie :

  • Source Generators — comme une usine qui produit des pièces prêtes avant d'assembler la voiture.
  • Reflection.Emit — comme un ingénieur qui soude un moteur-fusée sur la voiture en plein trajet.

Particularités et pièges

  • Le code généré peut être difficile à déboguer. Les generators offrent souvent la possibilité d'écrire les sources sur le disque — cherchez-les dans le dossier obj\Generated.
  • Reflection.Emit crée des assemblies en mémoire qui ne sont pas unloadables via AppDomain. Utilisez AssemblyBuilderAccess.RunAndCollect pour des assemblies temporaires (si supporté).
  • N'abusez pas de Reflection.Emit pour des tâches simples — parfois un generator de sources ou un template de code ordinaire est plus simple et plus fiable.
  • Les generators demandent une compréhension de Roslyn (pour Source Generators) et de l'IL (pour Reflection.Emit).

Source Generators et Reflection.Emit

Critère Source Generators Reflection.Emit
Utilisation À l'étape de compilation À l'exécution
Résultat Sources C#, partie de l'assembly IL, nouveaux types/assemblies
Scénarios typiques Autogénération de code template, DI, mapping, sérialisation Proxy, ORM dynamiques, pipelines runtime spéciaux
Complexité d'utilisation Moyenne, nécessite des connaissances Roslyn Élevée, nécessite des connaissances IL
Support IDE et débogage Excellent (les sources sont visibles) Difficile
Performance Très élevée Peut être élevée

5. Scénarios pratiques

1. Génération d'un API strongly-typed

Une organisation fournit une spécification OpenAPI. Le generator l'analyse et crée des classes de contrôleurs, des DTO et du code client pour travailler avec l'API REST — de façon type-safe et avec support IntelliSense.

Code (pseudo) :
// Spec: GET /users -> returns User[]
// Source Generator générera :
public class ApiClient
{
    public Task<User[]> GetUsersAsync() { ... }
}

2. Injection automatique / conteneur DI (Compile-time IoC)

Les generators créent des builders pour enregistrer les dépendances et construire le graphe d'objets. Plus besoin d'écrire manuellement services.AddSingleton<IMyService, MyService>().

Code (pseudo) :
[Injectable]
public class MyService : IMyService { ... }

// Le Source Generator générera :
partial class DIContainer
{
    public void RegisterServices()
    {
        AddSingleton<IMyService, MyService>();
    }
}

3. Proxies dynamiques — exemple avec Reflection.Emit

La bibliothèque Castle DynamicProxy construit des types proxy qui interceptent les appels de méthodes — base pour AOP, logging, tracing, mocking.

Code (simplifié) :
public interface IBookService { string GetBook(); }
public class BookService : IBookService { public string GetBook() => "C#"; }

var proxy = ProxyGenerator.CreateProxy<IBookService>(new BookService(), interceptor);
proxy.GetBook(); // L'appel est intercepté, on peut logger/modifier le résultat

4. Sérialisation rapide sans reflection

Plutôt que de construire des descriptions de types en runtime via reflection (lent), les Source Generators peuvent générer à l'avance le code de sérialisation/désérialisation — maximalement rapide et sans overhead.

1
Étude/Quiz
Réflexion, niveau 63, leçon 4
Indisponible
Réflexion
Réflexion et types dynamiques
Commentaires
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION