Introduction aux Source Generators
Pour ce billet, j'ai eu l'envie d'aborder le sujet des Source Generators
. En effet, j'ai dans l'idée de créer un petit package pour générer automatiquement des DTOs, ainsi que le mapping entre domain model et DTOs. Et pourquoi pas aller plus loin en générant, par exemple, des controllers d'Api depuis une specification OpenApi dans un contexte design-first. Et les Source Generators
sont la solution toute designée pour ce cas d'usage.
Mais pourquoi faire ? Il existe des tas de librairies qui le font très bien ?
Alors, oui, il existe de nombreuses librairies qui le font déjà et qui le feront certainement mieux que je ne pourrai jamais le faire. Mais l'idée, ici, est d'explorer les Source Generators
sur un cas d'usage concret, d'effleurer leur potentiel et voir dans quel cas je peux en avoir l'utilité au quotidien.
Mais avant d'aller plus loin, prenons le temps d'explorer les Source Generators
pour comprendre ce qu'ils sont et à quoi ils servent exactement.
Les Source Generators
sont une fonctionnalité introduite avec .NET 5 qui offrent la possibilité d'inspecter notre code et de générer du code C# qui sera ajouté à notre code source à la compilation. Les Source Generators
font partie du compilateur Roslyn de .NET et exploitent les API .NET Standard 2.0.
Tout ça c'est intéressant. Mais finalement à quoi ça sert ? Pourquoi je n'écrirais pas mon code moi même ? Puis aujourd'hui avec IntelliSense, Copilot et ChatGPT je peux aussi générer du code rapidement.
Alors, effectivement. Mais ce sont des outils aux usages diamétralement opposés. Les outils susmentionnés sont des assistants que l'on utilise durant la rédaction de code pour gagner du temps et ils peuvent d'ailleurs même nous aider à écrire nos Source Generators
. Nous devons les guider pour qu'ils nous proposent du code. À la différence des Source Generators
pour lesquels il est nécessaire de rédiger le code qui servira à générer... du code.
Les Source Generators
offrent effectivement des API pour analyser notre code et générer du code à la compilation. Inutile d'écrire un prompt à GPT ou Copilot chaque fois que l'on a une nouvelle classe à créer. Ici, il suffit de recompiler sa solution et le code sera généré et ajouté à votre code source et utilisable dans votre projet. On parle ici de méta-programmation.
Le schéma ci-dessus nous montre le processus d'analyse, de génération et de compilation des Source Generators
. Le compilateur Roslyn commence par construire l'arbre syntaxique et le modèle sémantique de notre solution. Ce qui correspond à la structure de notre code, reprenant l'ensemble des classes, leurs méthodes, leurs boucles, leurs variables etc., tandis que le modèle sémantique correspond aux types et à la portée de votre code, la résolution des noms, etc.
Une fois cette opération réalisée, Roslyn exécute l'ensemble des Source Generators
présent dans notre code et ajoute ensuite le code généré à l'arbre syntaxique et au modèle sémantique de notre solution, pour finalement compiler l'ensemble du code que nous avons produit et le code généré ensemble.
Une fois cette opération réalisée, il nous est tout à fait possible d'utiliser le code généré dans notre solution en déclarant une nouvelle instance de notre classe auto-généré dans notre code par exemple ou en utilisant une méthode auto-générée qui vient étendre le fonctionnement de notre code existant.
Il est à noter que ce code n'est pas directement ajouté à la solution sous forme de fichier disponible à la modification, mais comme résultat de compilation. Toute modification de ces fichiers dans un IDE moderne vous retournera un warning vous alertant que vous tentez de modifier un code généré qui sera mis à jour à la prochaine compilation, écrasant de ce fait vos modifications en cours. Pour apporter une modification à un code auto-généré il convient de modifier le Source Generator
à l'origine de ce code, ou d'étendre la classe généré pour y ajouter le comportement souhaité.
Reflection vs Source Generator
Lorsque l'on regarde le fonctionnement des Source Generators
on ne peut s'empécher de penser à la Reflection
qui est partie intégrante de .NET depuis sa première version, publiée en 2002.
Pour rappel, la Reflection
permet d'analyser son code au runtime, d'inspecter ou d'intéragir avec notre code pour découvrir ses membres (méthodes, champs, propriétés, etc.), et même invoquer ses membres à l'exécution.
La réflexion est utilisée dans une variété de scénarios, notamment :
Sérialisation et désérialisation : JSON.NET utilise la réflexion pour découvrir les propriétés d'un objet à sérialiser ou à désérialiser.
Injection de dépendances : Les conteneurs d'injection de dépendances utilisent la réflexion pour découvrir les dépendances d'un type et pour créer des instances de ces types.
Mapping d'objet : AutoMapper utilisent la réflexion pour découvrir comment mapper un objet d'un type à un autre.
ORM (Object-Relational Mapping) : Entity Framework utilisent la réflexion pour mapper les objets aux tables de base de données.
Tests unitaires : Les frameworks de tests unitaires utilisent la réflexion pour découvrir les méthodes de test à exécuter.
À la lecture de tout cela, on se rend tout de suite compte de la principale différence entre la Reflection
et les Source Generators.
L'un est exécuté au runtime et est à utiliser avec parcimonie car il peut rapidement être très couteux en performance et l'autre est exécuté à la compilation et nous permet donc d'avoir un code compilé et performant sans analyse supplémentaire au runtime.
Voici un tableau comparatif entre ces deux solutions:
Caractéristiques | Reflection | Source Generators |
Performance | La réflexion peut être coûteuse en termes de performances, ca la phase d'analyse est executée à l'exécution. | Les générateurs de source améliorent les performances en déplaçant le travail à la compilation plutôt qu'à l'exécution. |
Sécurité | La réflexion peut poser des problèmes de sécurité car elle permet d'accéder à des membres privés et protégés. | Les générateurs de source sont plus sûrs car ils ne permettent pas d'accéder directement à des membres privés ou protégés. |
Maintenance | La réflexion peut rendre le code plus difficile à comprendre et à maintenir. | Les générateurs de source peuvent rendre le code plus lisible et plus facile à maintenir en générant du code clair et explicite. |
Flexibilité | La réflexion est très flexible et permet d'inspecter et de manipuler le code à l'exécution. | Les générateurs de source sont moins flexibles car ils génèrent du code à la compilation et ne peuvent pas le modifier à l'exécution. |
Complexité | La réflexion peut être complexe à utiliser correctement et peut entraîner des erreurs subtiles. | Les générateurs de source sont plus simples à utiliser. |
Compatibilité | La réflexion est compatible avec toutes les versions de .NET. | Les générateurs de source sont compatible avec .NET 5 ou ultérieure. |
Les cas d'usages
Génération de code boilerplate
Il arrive souvent que nous écrivions du code répétitif, qui ne nécessite aucune reflexion mais qui soit indispensable à de nombreux endroit du code. Je pense machinalement à l'interface INotifyPropertyChanged
dans les applications WPF, permettant le data binding. Cela nécessite d'ajouter une méthode OnPropertyChanged
et de l'appeler dans chaque setter de notre classe pour notifier de chaque changement. Un autre exemple, serait simplement la création de DTO dans notre couche Présentation. Ce code répétitif peut-etre considéré comme un boilerplate.
Dans ce context, les Source Generators
peuvent vous faire gagner du temps en générant automatiquement le code boilerplate nécessaire.
Performance
Les Source Generators
peuvent être utilisés pour améliorer les performances de votre application. En générant du code à la compilation, vous pouvez déplacer certaines charges de calcul du runtime à la compilation, en évitant la reflection ou en générant du code spécifique et donc moins coûteux en terme de performance. Tout cela peut rendre votre application plus rapide à l'exécution. C'est le cas notamment des NuGet MediatR et AutoMapper qui connaissent des équivalent Source Generated.
DSLs (Domain Specific Languages)
Les Source Generator
peuvent vous aider à transformer un DSL en code C#. Un DSL, est un langage conçu pour résoudre un ensemble spécifique de problèmes. Par exemple, SQL est un DSL pour interroger et manipuler des bases de données relationnelles et HTML est un DSL pour décrire la structure et la présentation des documents web. On peut penser par exemple à un source generator qui transformerait une specification OpenAPI en controller C# dans le cas d'un projet Design-First, comme le fait NSwag.
Conclusion
En somme, les Source Generators
sont un outil puissant qui ouvre de nouvelles possibilités pour les développeurs .NET et permettent de générer du code à la compilation, ce qui peut améliorer les performances, réduire le code boilerplate et rendre le code plus lisible et plus facile à maintenir. De plus, ils peuvent être utilisés pour convertir des DSL en code C#, ce qui peut simplifier le développement dans des domaines spécifiques.
Cependant, comme tout outil, ils doivent être utilisés judicieusement. Il est important de bien comprendre comment ils fonctionnent et de prendre en compte les implications en termes de maintenance et de performance. Avec une utilisation réfléchie, les Source Generators
peuvent être un atout précieux pour tout projet .NET.
Dans un prochain article, nous explorerons un peu plus les Source Generators
au travers d'exemple concret reprenant la consigne présenté dans l'introduction de ce billet.
Subscribe to my newsletter
Read articles from Yassine FERNANE directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Yassine FERNANE
Yassine FERNANE
Développeur passionné avec près de 10 ans d'expérience sur .NET, je me consacre à promouvoir le clean code, la clean architecture et les bonnes pratiques de développement. Mon blog est le fruit de mon expertise et de ma curiosité insatiable pour l'apprentissage continu. Je partage avec vous mes découvertes, astuces et connaissances pour vous aider à créer un code de qualité et à devenir un développeur .NET accompli