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.
GO TO FULL VERSION