Tag Archives: T4

Transformer les templates T4 pendant la build, et passer des variables du projet

T4 (Text Template Transformation Toolkit) est un excellent outil pour générer du code ; on peut, par exemple, créer des classes POCO à partir des tables d’une base de données, générer du code répétitif, etc. Dans Visual Studio, les fichiers T4 (extension .tt) sont associés au custom tool TextTemplatingFileGenerator, qui transforme un template pour générer un fichier de sortie à chaque fois qu’on enregistre le template. Mais il arrive que ce ne soit pas suffisant, et qu’on souhaite regénérer les sorties des templates à chaque build. C’est assez facile à mettre en œuvre, mais il y a quelques écueils à éviter.

Transformer les templates lors du build

Si votre projet est un csproj ou vbproj "classique" (c’est-à-dire pas un projet .NET Core "SDK-style"), les choses sont assez simples et bien documentées sur cette page.

Déchargez votre projet, et ouvrez le dans l’éditeur. Ajoutez le PropertyGroup suivant vers le début du fichier :

<PropertyGroup>
    <!-- 15.0 is for VS2017, adjust if necessary -->
    <VisualStudioVersion Condition="'$(VisualStudioVersion)' == ''">15.0</VisualStudioVersion>
    <VSToolsPath Condition="'$(VSToolsPath)' == ''">$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)</VSToolsPath>
    <!-- This is what will cause the templates to be transformed when the project is built (default is false) -->
    <TransformOnBuild>true</TransformOnBuild>
    <!-- Set to true to force overwriting of read-only output files, e.g. if they're not checked out (default is false) -->
    <OverwriteReadOnlyOutputFiles>true</OverwriteReadOnlyOutputFiles>
    <!-- Set to false to transform files even if the output appears to be up-to-date (default is true)  -->
    <TransformOutOfDateOnly>false</TransformOutOfDateOnly>
</PropertyGroup>

Et ajoutez l’Import suivant à la fin, après l’import de Microsoft.CSharp.targets ou Microsoft.VisualBasic.targets :

<Import Project="$(VSToolsPath)\TextTemplating\Microsoft.TextTemplating.targets" />

Rechargez votre projet, et le tour est joué. Générer le projet devrait maintenant transformer les templates et regénérer leur sortie.

Projets "SDK-style"

Si vous utilisez le nouveau format de projet du SDK .NET Core (souvent appelé de façon informelle projet "SDK-style"), l’approche décrite ci-dessus nécessite un petit changement pour fonctionner. C’est parce que le fichier de cibles par défaut (Sdk.targets dans le SDK .NET Core) est maintenant importé implicitement à la toute fin du projet, il n’est donc pas possible d’importer les cibles T4 après les cibles par défaut. Du coup la variable BuildDependsOn, qui est modifiée par les cibles T4, est écrasée par les cibles par défaut, et la cible TransformAll ne s’exécute plus avant la cible Build.

Heureusement, il y a un workaround : vous pouvez importer les cibles par défaut explicitement, et importer les cibles T4 après celles-ci :

<Import Project="Sdk.targets" Sdk="Microsoft.NET.Sdk" />
<Import Project="$(VSToolsPath)\TextTemplating\Microsoft.TextTemplating.targets" />

Notez que cela causera un avertissement MSBuild dans la sortie de la build (MSB4011), parce que Sdk.targets est importé deux fois ; cet avertissement peut être ignoré sans risque.

Passer des variables MSBuild aux templates

À un moment donné, il est possible que la logique de génération de code devienne trop complexe pour rester entièrement dans le template T4. Vous pourriez vouloir en extraire une partie dans un assembly utilitaire, qui sera référencé depuis le template comme ceci :

<#@ assembly name="../CodeGenHelper/bin/Release/net462/CodeGenHelper.dll" #>

Mais spécifier le chemin de l’assembly de cette façon n’est pas très pratique… par exemple, si vous êtes actuellement sur la configuration Debug, la version Release de CodeGenHelper.dll ne sera pas forcément à jour. Heureusement, le custom tool TextTemplatingFileGenerator de Visual Studio supporte l’utilisation de variables MSBuild du projet, il est donc possible de faire ceci :

<#@ assembly name="$(SolutionDir)/CodeGenHelper/bin/$(Configuration)/net462/CodeGenHelper.dll" #>

Les variables $(SolutionDir) et $(Configuration) seront remplacées par leurs valeurs respectives. Si vous enregistrez le template, il sera bien transformé en utilisant l’assembly CodeGenHelper.dll. Pratique !

Mais il y a un hic… Si vous avez configuré votre projet pour transformer les templates lors de la build, comme décrit plus haut, la build va maintenant échouer, avec une erreur comme celle-ci :

System.IO.FileNotFoundException: Could not find a part of the path ‘C:\Path\To\The\Project\$(SolutionDir)\CodeGenHelper\bin\$(Configuration)\net462\CodeGenHelper.dll’.

Vous avez remarqué les variables $(SolutionDir) et $(Configuration) dans le chemin ? Elles n’ont pas été remplacées ! C’est parce que la cible MSBuild qui transforme les templates et le custom tool TextTemplatingFileGenerator n’utilisent pas le même moteur de transformation de texte. Et malheureusement, celui utilisé par MSBuild ne supporte pas nativement les variables MSBuild… un comble !

Cependant tout n’est pas perdu. Il suffit de spécifier explicitement les variables que vous voulez passer en temps que paramètres T4. Éditez à nouveau votre fichier projet, et créez un nouveau ItemGroup avec les éléments suivants :

<ItemGroup>
    <T4ParameterValues Include="SolutionDir">
        <Value>$(SolutionDir)</Value>
        <Visible>False</Visible>
    </T4ParameterValues>
    <T4ParameterValues Include="Configuration">
        <Value>$(Configuration)</Value>
        <Visible>False</Visible>
    </T4ParameterValues>
</ItemGroup>

L’attribut Include est le nom du paramètre tel qu’il sera passé au moteur de transformation de texte. L’élément Value, comme son nom l’indique, est la valeur du paramètre. Et l’élément Visible permet d’éviter que l’élément T4ParameterValues n’apparaisse comme élément du projet dans l’explorateur de solution.

Avec ce changement, la build devrait à nouveau transformer les templates correctement.

Pour résumer, gardez à l’esprit que le custom tool TextTemplatingFileGenerator et la cible MSBuild de transformation de texte ont des mécanismes différents pour passer des variables aux templates :

  • TextTemplatingFileGenerator supporte uniquement les variables MSBuild du projet
  • MSBuild supporte uniquement T4ParameterValues

Donc si vous utilisez des variables dans votre template, et que vous voulez pouvoir transformer le template quand vous l’enregistrez dans Visual Studio et quand vous générez le projet, les variables doivent être définies à la fois comme variables MSBuild et comme T4ParameterValues.

Exécuter un outil personnalisé automatiquement quand un fichier est modifié

Aussi loin que je me souvienne, il y a toujours eu dans Visual Studio quelque chose appelé “outils personnalisés” (custom tools), également connus sous le nom de single-file generators. Quand vous appliquez un tel outil à un fichier de votre projet, il génère quelque chose (généralement du code, mais pas forcément) en fonction du contenu du fichier. Par exemple, l’outil personnalisé par défaut pour les fichiers de ressource s’appelle ResXFileCodeGenerator, et génère une classe qui permet d’accéder facilement aux ressources définies dans le fichier resx.

image

Quand vous enregistrez un fichier qui a un outil personnalisé associé, Visual Studio réexécute automatiquement l’outil personnalisé pour regénérer sa sortie. Vous pouvez aussi le faire manuellement, en utilisant la commande “Exécuter l’outil personnalisé” depuis le menu contextuel du fichier dans l’explorateur de solution.

Habituellement, les outils personnalisés ne se basent que sur un fichier d’entrée pour générer leur sortie, mais parfois les choses sont un peu plus complexes. Par exemple, prenons les templates T4 : ils ont un outil personnalisé associé (TextTemplatingFileGenerator), donc cet outil est exécuté quand le template est sauvegardé, mais bien souvent, le template lui-même utilise d’autres fichiers d’entrée pour générer sa sortie. Donc l’outil personnalisé doit être exécuté non seulement quand le template est modifié, mais également quand les fichiers dont il dépend sont modifiés. Puisqu’il n’y a pas de moyen d’indiquer à Visual Studio l’existence de cette dépendance, il faut exécuter l’outil personnalisé manuellement, ce qui est assez agaçant…

Comme j’étais dans cette situation, et que j’en avais assez d’aller exécuter manuellement l’outil personnalisé sur mes templates T4, j’ai finalement créé une extension Visual Studio pour le faire automatiquement : AutoRunCustomTool. Le nom manque un peu d’imagination, mais au moins il est descriptif…

Cet outil est conçu pour être très simple et discret : il fait son travail en silence, sans vous gêner, et vous oubliez très vite qu’il est là. Il ajoute une nouvelle propriété à chaque élément du projet : “Run custom tool on”. Cette propriété est une collection de noms de fichiers pour lesquels l’outil personnalisé doit être exécuté à chaque fois que cet élément de projet est enregistré. Par exemple, si vous avez un template T4 (Template.tt) qui génère un fichier (Output.txt) en fonction du contenu d’un autre fichier (Input.txt), il suffit d’ajouter “Template.tt” à la propriété “Run custom tool on” de Input.txt. A chaque fois que vous enregistrerez Input.txt, l’outil personnalisé sera exécuté automatiquement sur Template.tt, ce qui regénèrera le contenu de Output.txt. Vous trouverez un exemple concret sur la page de l’outil dans la galerie Visual Studio.

J’ai créé AutoRunCustomTool il y a 6 mois environ, mais la version initiale n’était pas très bien dégrossie, donc je n’ai pas communiqué à son sujet. J’ai publié la deuxième version il y a quelques jours, et je pense qu’il est maintenant prêt à être utilisé par tout le monde. Si vous êtes intéressé par le code, vous pouvez le trouver sur GitHub, qui est aussi l’endroit où vous pouvez signaler les problèmes éventuels.