Sunday, November 2, 2014

Getting Started with T4 templates

Recently, I had to troubleshoot an issue that involved TDS code generation of Glass mapper items. This got me looking at T4 templates, and I have been struck at how cleanly this is implemented and how good the MSDN documentation is (http://msdn.microsoft.com/en-us/library/dd820620.aspx, which some of the examples below are from.)  I like stuff you can get up and running in a few minutes.


The Basics
Suppose there is a class file you want to generate based on some external source.  You can get this up and running in Visual Studio by adding a file to your solution of type TextTemplate:


Let's start simple.  Add a template called Hello.tt.  That will create a file in your solution that looks like this:

<#@ template debug="false" hostspecific="false" language="C#" #>
<#@ assembly name="System.Core" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Text" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ output extension=".txt" #>

A few things to note. Like ASPX, we have a tag syntax: "<# .. #>". The "@" indicates directives that control the templating process.  (Directives are documented here.)  We will come back to these in a bit. For now, note the "language" directive that indicates the language you will use to generate templates (C#), and the output directive that controls the extension of the output.

OK, after the last directive, add some text, and click save.  Now you should find a "Hello.txt" file nested under the Hello.tt file, with the name Hello.txt.  Because the templating language is C#, you can mix in C# calls to your template output by including "<#= .. #>":

Hello, it is <#= DateTime.Now #>

On my system, this created:

Hello, today is 11/02/2014 11:55:12

We can easily add some C# logic by including it in "<# ... #>" tags.  For example, let's loop through some values.

<# for (int i = 0; i<=10; i++) 
{ #>
Number is <#= i #>, square is <#= i * i #>
<# } #>

The highlighted text is executed as code, and the nested text is emitted from the loop.

Creating a Constants file
To show how this can be adopted to C# code generation, lets make a few changes to generate a constants class from a text file.  First, lets change the output type to ".cs".  Now the emitted code will be a class file, and will automatically be included in the solution.  Now, lets add a text file to the solution called "Constants.txt".  To simplify things, lets treat this as a comma delimited file with this format: name, type, value.  

To access this file, we need to make a couple of changes to our directives. First, lets change "hostspecific" to true. This allows us to use "this.Host.ResolvePath("Constants.txt") to get the path of the file that will generate the process.Next, let's read in lines, and use string.Split to break into fields:

string fileName = this.Host.ResolvePath("Constants.txt");
var model = File.ReadLines(fileName);

To access File, I had to add an import directive, which is the T4 equivalent of the using directive:

<#@ import namespace="System.IO" #>


Now I can loop through the lines, parse the input, and output constants:


  <# foreach(var line in model)
  {
    var fields = line.Split(',');
string name = fields[0];
string type = fields[1];
string value = fields[2];
#>
public const <#= type #> <#= name #>  = <#= value #>;
  <# } #>

Wrapping Up
The complete file looks like this:
<#@ template debug="true" hostspecific="true" language="C#" #>
<#@ assembly name="System.Core" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Text" #>
<#@ import namespace="System.IO" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ output extension=".cs" #>

<# 
// Creates a constants class based on Constants.txt. 
// 
// The format is: 
// fieldName,type,initialization value.
//
// For example:
// ItemId,Guid,new Guid("170ba70c-bbfb-499a-9169-459a6acfe68a")

string fileName = this.Host.ResolvePath("Constants.txt");
var model = File.ReadLines(fileName);

#>
// This is generated code. Edit Constants.txt to add values.

public class Constants
{
  <# foreach(var line in model)
  {
    var fields = line.Split(',');
string name = fields[0];
string type = fields[1];
string value = fields[2];
#>
public const <#= type #> <#= name #>  = <#= value #>;
  <# } #>
}

With this Constants.txt:
FirstName,string,"Dan"
ItemID, System.Guid, new System.Guid("170ba70c-bbfb-499a-9169-459a6acfe68a")

this file is created and added to the project:

// This is generated code. Edit Constants.txt to add values.

public class Constants
{
  public const string FirstName  = "Dan";
  public const  System.Guid ItemID  =  new System.Guid("170ba70c-bbfb-499a-9169-459a6acfe68a");
  }

This file will be regenerated when ever the template is modified, or when you select "Transform all T4 templates" from the build menu.  And there you have it. Code generation in five minutes!  The fact that this is all built on top of C# is a real plus, in my view, as is the seamless integration into Visual Studio. The only pain point is the lack of syntax highlighting and intellisense, though there are a number of 3rd party plugins that do this, for example: this and this.) Of course, this example is not production ready, but there is clearly an opportunity here to automate boiler plate code.  I'd be curious to here in the comments if and how you are using this technology.

2 comments:

  1. hi,

    I think I'm having the same issue as you. I'm trying to use the TDS out of the box T4 templates to use generate my classes that are Solr compatible. But the fields are missing the postfix annotation.

    do you have the T4 templates that actually work?

    ReplyDelete
  2. hi,

    I think I'm having the same issue as you. I'm trying to use the TDS out of the box T4 templates to use generate my classes that are Solr compatible. But the fields are missing the postfix annotation.

    do you have the T4 templates that actually work?

    ReplyDelete