T4 Multiple File Outputs in a .NET Core Project
I recently needed to generate boilerplate classes for a project from reflection metadata. It had been a few years since I'd used T4 to generate SQL stored procedures, and the awesome T4MVC template, so surely there was a shiny new way by now?
Scripty comes up as an alternative, script-based approach to code generation. Unfortunately, it has major issues with VS 2017 and the Roslyn compiler.
So T4 templating is still the way to go. The problem is splitting the output into multiple files in other directories, because by default T4 nests generated files under the parent template.
Prerequisites
This uses the T4 Toolbox extension, which works in VS 2017 at the time of writing (I'm on update 15.8.9).
Getting started
There are two templates:
- A runner template sets up the code generation template. It enumerates a collection and passes each item's data to the template.
- A code generation template contains the templating code itself.
This is a basic example, not production code.
TemplateRunner.tt
<#@ template language="C#" debug="false" #> //#A
<#@ output extension="cs" #> //#A
<#@ import namespace="System.Linq" #> //#A
<#@ import namespace="System.Text" #> //#A
<#@ import namespace="System.IO" #> //#A
<#@ import namespace="System.Collections.Generic" #> //#A
<#@ include file="BaseGeneratorApi.tt" #> //#B
<#
BaseGeneratorApi baseTemplate = new BaseGeneratorApi(); //#B
var currentFolder = new FileInfo(Host.ResolvePath(Host.TemplateFile)).DirectoryName; //#C
var webProject = Path.GetFullPath(Path.Combine(currentFolder, @"../", "src")); //#C
var apiControllerNames = File.ReadAllLines(Path.Combine(currentFolder, "bin", "Debug", "netcoreapp2.1", "webapinames.txt")); //#D
if(apiControllerNames != null && apiControllerNames.Any()) {
foreach (string className in apiControllerNames)
{
baseTemplate.controllerName = className; //#E
baseTemplate.controllerNameTitled = char.ToLower(className[0]) + className.Substring(1); //#E
var controllerFileName = $"{className}Controller.cs";
baseTemplate.RenderToFile(Path.Combine("Autogenerated", controllerFileName)); //#F
}
}
#>Going through it:
- #A outputs
.csfiles and imports the namespaces needed for enumeration and file reading. - #B includes and instantiates the code generation template, which here generates a blank Web API controller.
- #C resolves paths. At runtime, the template's base
directory is a subdirectory of your Visual Studio folder, so it needs a
little configuration to find the text file in the
binfolder. - #D reads the text file.
- #E passes the class name strings to the code generation template.
- #F calls
RenderToFilewith the file path for the generated class.
BaseGeneratorApi.tt
<#@ output extension="cs" #>
<#@ include file="T4Toolbox.tt" #> //#A
<#@ assembly name="System.Core" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Text" #>
<#@ import namespace="System.IO" #>
<#@ import namespace="System.Collections.Generic" #>
<#+
public class BaseGeneratorApi : CSharpTemplate //#B
{
public string controllerName;
public string controllerNameTitled;
public override string TransformText() //#B
{
base.TransformText();
#>
//All your code goes here!
[Route("api/[controller]")]
[Authorize]
public class <#= $"{controllerName}Controller" #> : ControllerBase
{
}
<#+
return this.GenerationEnvironment.ToString();
}
}
#>- #A includes T4 Toolbox, for its helpers.
- #B inherits from T4 Toolbox's abstract
CSharpTemplateclass and overridesTransformText, calling the base method before appending its own code.
One runner can use several code generation templates, but each one adds another place for a transformation to go wrong, so one runner per template is better.