What are T4 templates
First off, T4 stands for Text Template Transformation Toolkit. It allows us to embed C# (or VB.NET) code into documents and optionally combine the code with plain text, which can then be used to generate new documents – therefore similar to the way ASP or PHP engines work when producing HTML. The output text document might be a C# file, HTML file or pretty much any other file type you want.
A T4 template file ends in a .tt extension.
Within Visual, when a T4 template is executed the resultant file is created as a child node (in the solution explorer) off of the template’s node.
Let’s get started
If you create a solution (or have a solution ready), then on a selected project, select the “Add New Item” option. Then select “Text Template”, give it a name (mine’s named Test.tt) and add to the project.
By default the file will look similar to the following (this is from the Visual Studio 2012/2013 T4 item template)
<#@ 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" #>
Note: by default Visual Studio (well 2013 at least) does not offer syntax highlighting. You can use Tools | Extensions and Updates to add T4 Toolbox which is a free syntax highlighter etc. for T4.
So in the template code above we can see that <#@ #> are used to enclose “directives”.
The template adds a reference to the System.Core assembly and imported the namespaces System.Linq, System.Text and System.Collections.Generic. The template sets the output extension (by default) to .txt. Hence the template will generate a file name Test.txt file (in my case). Obviously the name of the file is a copy of the name of your template file.
The template directive is described here.
We can see that the language attribute denotes whether we want to write VB or C# code within our code statement blocks.
The hostspecific attribute denotes that we want the T4 template to allow us access to the this.Host property of type ITextTemplatingEngineHost. From this we can use methods such as the ResolvePath method, which allows us to get a combined path of our supplied filename or relative path, combined with the current path of the solution. See ITextTemplatingEngineHost Interface for more information.
Finally the debug attribute denotes that we want the intermediate code to include code which helps the debugger to identify more accurately where an exception or the likes has occurred.
Include Directive
We can also include T4 code using the <#@ include file=”mycode.tt” #> See T4 Include Directive.
Blocks
So we’ve looked at the <#@ #> directive, but there’s more…
<# Statement Blocks #>
Statement blocks allow you to embed code into your template. This code can simply be blocks of code or can be used to wrap loops or if statements which in turn wrap around output text, as per
<#
for(int i = 0; i < 3; i++)
{
#>
Hello
<#
}
#>
or, as mentioned, simply embedding code blocks, such as
<#
for(int i = 0; i < 3; i++)
{
WriteLine("Hello");
}
#>
which results in the same output at the previous sample. See also Statement Syntax.
<#+ Class Feature Blocks #>
Class feature blocks allow you to create code which you can reuse within your template. Statement blocks are like the inner blocks of a method, but ofcourse it would be useful to be able to actually create reusable methods. This is where Class Blocks come in.
For example
<#= WrapInComment("Hello") #>
<#+
private string WrapInComment(string text)
{
return "<!-- " + text + " -->";
}
#>
It’s important to note that the statement block calling code is actually before the method declaration.
In this example we return a string, which can be used from a <#= Expression Block #> (see below). If the method returned void then it would equally be callable from a <# Statement Blocks #>.
<#= Expression Blocks #>
Expression blocks allow us to write out results from our code to the generated output text, for example
<#
int sum = 10 + 4;
#>
<#= sum #>
So the first block is a statement block where we declare the sum variable and it is output in situ via the <#= #> code. See also Expression Syntax (Domain-Specific Languages).
Great, now let’s do something with all this!
Let’s write a T4 template which will take a C# class, which contains our model (as a partial class) and has properties marked with XmlElementAttribute and then generates another partial class for the type, with some new ShouldSerializeXXX methods – the idea being any string property which is empty or null, will not be serialized.
This example template is not perfect and I’m sure there are better ways to do things, but I really wanted to create a more meaningful example than a “Hello World”, so bare with me.
<#@ template debug="false" hostspecific="true" language="C#" #>
<#@ assembly name="System" #>
<#@ assembly name="System.Xml" #>
<#@ import namespace="System.IO" #>
<#@ import namespace="System.Text" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ import namespace="System.Xml.Serialization" #>
<#@ import namespace="System.Reflection" #>
<#@ import namespace="System.CodeDom.Compiler" #>
<#@ output extension=".cs" #>
<#
string model = File.ReadAllText(this.Host.ResolvePath("MyModel.cs"));
CodeDomProvider provider = CodeDomProvider.CreateProvider("CSharp");
CompilerParameters cp = new CompilerParameters();
cp.GenerateExecutable = false;
cp.GenerateInMemory = true;
cp.ReferencedAssemblies.Add("System.dll");
cp.ReferencedAssemblies.Add("System.Core.dll");
cp.ReferencedAssemblies.Add("System.Xml.dll");
CompilerResults cr = provider.CompileAssemblyFromSource(cp, new string[] { model });
if (cr.Errors.Count > 0)
{
Error("Errors detected in the compiled model");
}
else
{
#>
using System;
<#
bool nsAdded = false;
Type[] types = cr.CompiledAssembly.GetTypes();
foreach (Type type in types)
{
// time to generate code
if(nsAdded == false)
{
#>
namespace <#=type.Namespace#>
{
<#
}
nsAdded = true;
#>
public partial class <#=type.Name#>
{
<#
List<PropertyInfo> toGenerate = new List<PropertyInfo>();
foreach (PropertyInfo pi in type.GetProperties())
{
if(pi.PropertyType == typeof(string))
{
XmlElementAttribute[] a = (XmlElementAttribute[])pi.GetCustomAttributes(typeof(XmlElementAttribute), true);
if (a != null && a.Length > 0)
{
#>
public bool ShouldSerialize<#=pi.Name#>()
{
return !String.IsNullOrEmpty(<#=pi.Name#>);
}
<#
}
}
}
#>
}
<#
}
}
#>
}
Before we look at the output from this T4 template, let’s quickly review what’s in this template. Firstly we reference the two assembles, System and System.Xml then add our “using” clauses via the import directive. The template is marked as hostspecific so we can use the Host property.
We read the MyModel.cs file, in this instance. Although we could have possibly looked to interact with the Visual Studio environment instead to achieve this. Then we create a C# CodeDomProvider, we could use Roslyn instead for this. The purpose of the CodeDomProvider is solely to use it to compile the MyModel.cs and allow us to use reflection to get the properties with the XmlElementAttribute as I don’t really want to parse the source code myself (this is where Roslyn would have come in).
Now you can see interspersed with our T4 blocks of code is the output text which creates the namespace, class and ShouldSerializeXXX methods.
So the code gets the properties on our MyModel object, then finds those with a string type and also with the XmlElementAttribute attribute applied and then creates the namespace, class and methods to match these properties. Writing output which looks something like the following
using System;
namespace DomainObjects
{
public partial class MyModel
{
public bool ShouldSerializeName()
{
return !String.IsNullOrEmpty(Name);
}
public bool ShouldSerializeAge()
{
return !String.IsNullOrEmpty(Age);
}
public bool ShouldSerializeAddress()
{
return !String.IsNullOrEmpty(Address);
}
}
}
And finally (for now)…
Right mouse click on the T4 .tt file and you will see a Debug T4 template. I actually only discovered this after searching for something else related to this post and came across T4 TEMPLATE DEBUGGING which talks about it.
You can now put break points against your T4 code and run the T4 debugger!