Transform T4 templates as part of the build, and pass variables from the project
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 Microsoft.CSharp.targets
or Microsoft.VisualBasic.targets
:
<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.
SDK-style projects
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 Build
target.
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" #>
The $(SolutionDir)
and $(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’.
Notice the $(SolutionDir)
and $(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>
The 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:
TextTemplatingFileGenerator
supports only MSBuild variables from the project- MSBuild supports only
T4ParameterValues
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 T4ParameterValues
.