T4 (Text Template Transformation Toolkit) is a great tool to generate code at design time; you can, for instance, create POCO classes from database tables, generate repetitive code, etc. In Visual Studio, T4 files (.tt extension) are associated with the
TextTemplatingFileGenerator custom tool, which transforms the template to generate an output file every time you save the template. But sometimes it’s not enough, and you want to ensure that the template’s output is regenerated before build. It’s pretty easy to set this up, but there are a few gotchas to be aware of.
Transforming templates at build time
If your project is a classic csproj or vbproj (i.e. not a .NET Core SDK-style project), things are actually quite simple and well documented on this page.
Unload your project, and open it in the editor. Add the following
PropertyGroup near the beginning of the file:
<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>
And add the following
Import at the end, after the import of
<Import Project="$(VSToolsPath)\TextTemplating\Microsoft.TextTemplating.targets" />
Reload your project, and you’re done. Building the project should now transform the templates and regenerate their output.
If you’re using the new project format that comes with the .NET Core SDK (sometimes informally called "SDK-style project"), the approach described above will need a small change to work. This is because the default targets file (
Sdk.targets in the .NET Core SDK) is now imported implicitly at the very end of the project, so you can’t import the text templating targets after the default targets. This causes the
BuildDependsOn variable, which is modified by the T4 targets, to be overwritten, so the
TransformAll target doesn’t run before the
Fortunately, there’s a workaround: you can import the default targets file explicitly, and import the text templating targets after that:
<Import Project="Sdk.targets" Sdk="Microsoft.NET.Sdk" /> <Import Project="$(VSToolsPath)\TextTemplating\Microsoft.TextTemplating.targets" />
Note that it will cause a MSBuild warning in the build output (MSB4011) because
Sdk.targets is imported twice; you can safely ignore this warning.
Passing MSBuild variables to templates
At some point, the code generation logic might become too complex to remain entirely in the T4 template file. You might want to extract some of it into a helper assembly, and reference this assembly from the template, like this:
<#@ assembly name="../CodeGenHelper/bin/Release/net462/CodeGenHelper.dll" #>
Of course, specifying the path like this isn’t very very convenient… For instance, if you’re currently in
Debug configuration, the
Release version of CodeGenHelper.dll might be out of date. Fortunately, Visual Studio’s
TextTemplatingFileGenerator custom tool recognizes MSBuild variables from the project, so you can do this instead:
<#@ assembly name="$(SolutionDir)/CodeGenHelper/bin/$(Configuration)/net462/CodeGenHelper.dll" #>
$(Configuration) variables will be expanded to their actual values. If you save the template, the template will be transformed using the CodeGenHelper.dll assembly. Nice!
However, there’s a catch… if you configured the project to transform templates on build as described above, the build will now fail, with an error like this:
System.IO.FileNotFoundException: Could not find a part of the path ‘C:\Path\To\The\Project\$(SolutionDir)\CodeGenHelper\bin\$(Configuration)\net462\CodeGenHelper.dll’.
$(Configuration) variables in the path? They were not expanded! This is because the MSBuild target that transforms the templates and the
TextTemplatingFileGenerator custom tool don’t use the same text transformation engine. And unfortunately, the one used by MSBuild doesn’t recognize MSBuild properties out of the box… Ironic, isn’t it?
All is not lost, though. All you have to do is explicitly specify the variables you want to pass as T4 parameters. Edit your project file again, and create a new
ItemGroup with the following items:
<ItemGroup> <T4ParameterValues Include="SolutionDir"> <Value>$(SolutionDir)</Value> <Visible>False</Visible> </T4ParameterValues> <T4ParameterValues Include="Configuration"> <Value>$(Configuration)</Value> <Visible>False</Visible> </T4ParameterValues> </ItemGroup>
Include attribute is the name of the parameter as it will be passed to the text transformation engine. The
Value element is, well, the value. And the
Visible element prevents the
T4ParameterValues item from appearing under the project in the solution explorer.
With this change, the build should now successfully transform the templates again.
So, just keep in mind that the
TextTemplatingFileGenerator custom tool and the MSBuild text transformation target have different mechanisms for passing variables:
TextTemplatingFileGeneratorsupports only MSBuild variables from the project
- MSBuild supports only
So if you use variables in your template and you want to be able to transform it when you save the template in Visual Studio and when you build the project, the variables have to be defined both as MSBuild variables and as