Software Dev Blog

Software Dev Blog

by Ash Grennan - Software Developer based in Burnley.

T4 Multiple File Outputs in a .NET Core Project

I recently needed to auto generate boilerplate classes for a project based upon reflection meta data. Having been a few years since I’d leveraged T4 for SQL Stored Proc generation and the awesome T4MVC template, surely there’s a shiny new method?

Scripty comes up as an alternative scripting approach for code generation, unfortunately it’s having major issues with VS 2017 and the Roslyn compiler.

So, it looks like T4 Templating is still the way to go. However whilst writing the template, it appears that splitting code into multiple files and moving directories is quite a problem as templating standard behaviour is to nest files under the parent template.

Prerequisites

First, we’re going to leverage the T4 Toolbox extension. Yes, this works in VS 2017 at the time of writing (I have update 15.8.9), you can find this here.

Getting Started

So our strategy here is to have a Runner template and code generation template.

Please note – The below represents a basic example, and is 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
}
}
#>

Lets go through the code above:

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 BaseGeneratorRepository : CSharpTemplate //#B
{
public string repositoryName;
public string repositoryNameTitled;

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

As above:

You can have multiple code generation templates which are used within a single template runner, however it increases the scope of a problem occurring in the transformations, and therefore a 1:1 ratio is better.

Comments