T4 Multiple File Outputs in a .NET Core Project

· 1 min

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:

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:

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();
}
}
#>

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.