Generating code using T4 templates
Recently I was updating a library that contains two keyed
collection classes. These collections aren't the usual
run-of-the-mill collections as they need to be able to support
duplicate keys. Normally I'd inherit from KeyedCollection
but
as with most collection implementations, duplicate keys are not
permitted in this class.
I'd initially solved the problem by simply creating my own base class to fit my requirements, and this works absolutely fine. However, this wasn't going to suffice as a long term solution as I don't want that base class to be part of a public API, especially a public API that has nothing to do with offering custom base collections to consumers.
Another way I could have solved the problem would be to just duplicate all that boilerplate code, but that was pretty much a last resort. If there's one thing I really don't like doing it's fixing the same bugs over and over again in duplicated code!
Then I remembered about T4 Templates, which has been a feature of Visual Studio for some time I believe. Previously my only interaction with them has been via PetaPoco, a rather marvellous library which generates C# classes based on a database model, provides a micro ORM, and has powered cyotek.com for years. This proved to be a nice solution for my collection issue, and I thought I'd document the process here, firstly as it's been a while since I blogged, and secondly as a reference for "next time".
Creating the template
First, we need to create a template. To do this from Visual Studio, open the Project menu and click Add New Item. The select Text Template from the list of templates, give it a name, and click Add.
This will create a simple file containing something similar to the following
<#@ 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 T4 template is basically the content you want to output, with one or more control blocks for dynamically changing the content. In other words, it's just like a Razor HTML file, WebForms, Classic ASP, PHP... the list is probably endless.
Each block is delimited by <#
and #>
, the @
symbols above
are directives. We can use the =
symbol to inject content. For
example, if modify the template to include the following lines
<html>
<head>
<title><#=DateTime.Now#></title>
</head>
</html>
Save the file, then in the Project Explorer, expand the node for the file - by default the auto generated content will be nested beneath your template file, as with any other designer code. Open the generated file and you should see something like this
<html>
<head>
<title>03/12/2016 12:41:07</title>
</head>
</html>
Changing the file name
The name of the auto-generated file is based on the underlying template, so make sure your template is named appropriately. You can get the desired file extension by including the following directive in the template
<#@ output extension=".txt" #>
If no directive at all is present, then .cs
will be used.
Including other files
So far, things are looking positive - we can create a template that will spit out our content, and dynamically manipulate it. But it's still one file, and in my use case I'll need at least two. Enter - the include directive. By including this directive, the contents of another file will be injected, allowing us to have multiple templates generated from one common file.
<#@ include file="CollectionBase.ttinclude" #>
If your include file makes use of variables, they are automatically inherited from the parent template, which is the key piece of magic I need.
Adding conditional logic
So far I've mentioned the <%@ ... %>
directives, and the <%= ... %>
insertion blocks. But what about if you want to include
code for decision making, branching, and so on? For this, you
use the <% ... %>
syntax without any symbols on the opening
delimiter. For example, I use the following code to include a
certain using
statement if a variable has been set
using System.Collections.Generic;
<# if (UsePropertyChanged) { #>
using System.ComponentModel;
<# } #>
In the above example, the line using
System.Collections.Generic; will always be written. On the
other hand, the using System.ComponentModel; line will only be
written if the UsePropertyChanged
variable has been set.
Note: Remember that T4 templates are compiled and executed. So syntax errors in your C# code (such as forgetting to assign (or define) the
UsePropertyChanged
variable above) will cause the template generation to fail, and any related output files to be only partially generated, if at all.
Debugging templates
I haven't really tested this much, as my own templates were
fairly straight forward and didn't have any complicated logic.
However, you can stick breakpoints in your .tt
or .ttinclude
files, and then debug the template generation by context
clicking the template file and choosing Debug T4 Template
from the menu. For example, this may be useful if you create
helper methods in your templates for performing calculations.
Putting it all together
The two collections I want to end up with are
ColorEntryCollection
and ColorEntryContainerCollection
. Both
will share a lot of boilerplate code, but also some custom code,
so I'll need to include dedicated CS files in addition to the
auto-generated ones.
To start with, I create my ColorEntryCollection.cs
and
ColorEntryContainerCollection.cs
files with the following
class definitions. Note the use of the partial
keyword so I
can have the classes built from multiple code files.
public partial class ColorEntryCollection
{
}
public partial class ColorEntryContainerCollection
{
}
Next, I created two T4 template files,
ColorEntryCollectionBase.tt
and
ColorEntryContainerCollectionBase.tt
. I made sure these had
different file names to avoid the auto-generated .cs
files
from overwriting the custom ones (I didn't test to see if VS
handles this, better safe than sorry).
The contents of the ColorEntryCollectionBase.tt
file looks
like this
<#
string ClassName = "ColorEntryCollection";
string CollectionItemType = "ColorEntry";
bool UsePropertyChanged = true;
#>
<#@ include file="CollectionBase.ttinclude" #>
The contents of ColorEntryContainerCollectionBase.tt
are
<#
string ClassName = "ColorEntryContainerCollection";
string CollectionItemType = "ColorEntryContainer";
bool UsePropertyChanged = false;
#>
<#@ include file="CollectionBase.ttinclude" #>
As you can see, the templates are very simple - basically just setting it up the key information that is required to generate the template, then including another file - and it is this file that has the true content.
The final piece of the puzzle therefore, was to create my
CollectionBase.ttinclude
file. I copied into this my original
base class, then pretty much did a search and replace to replace
hard coded class names to use T4 text blocks. The file is too
big to include in-line in this article, so I've just included
the first few lines to show how the different blocks fit
together.
using System;
using System.Collections;
using System.Collections.Generic;
<# if (UsePropertyChanged) { #>
using System.ComponentModel;
<# } #>
namespace Cyotek.Drawing
{
partial class <#=ClassName#> : IList<<#=CollectionItemType#>>
{
private readonly IList<<#=CollectionItemType#>> _items;
private readonly IDictionary<string, SmallList<<#=CollectionItemType#>>> _nameLookup;
public <#=ClassName#>()
{
_items = new List<<#=CollectionItemType#>>();
_nameLookup = new Dictionary<string, SmallList<<#=CollectionItemType#>>>(StringComparer.OrdinalIgnoreCase);
}
All the <#=ClassName#>
blocks get replaced with the
ClassName
value from the parent .tt
file, as do the
<#=CollectionItemType#>
blocks. You can also see the
UsePropertyChanged
variable logic I described earlier for
inserting a using
statement - I used the same functionality in
other places to include entire methods or just extra lines where
appropriate.
Then it was just a case of right clicking the two .tt
files I
created earlier and selecting Run Custom Tool from the
content menu which caused the contents of my two collections to
be fully generated from the template. The only thing left to do
was to then add the custom implementation code to the two main
class definitions and job done.
I also used the same process to create a bunch of standard tests for those collections rather than having to duplicate those too.
That's all folks
Although normally you probably won't need this sort of functionality, the fact that it is built right into Visual Studio and so easy to use is pretty nice. It has certainly solved my collection issue and I'll probably use it again in the future.
While writing this article, I had a quick look around the MSDN documentation and there's plenty of advanced functionality you can use with template generation which I haven't covered, as just the basics were sufficient for me.
Although I haven't included the usual sample download with this article, I think it's straightforward enough that it doesn't need one. The final code will be available on our GitHub page at some point in the future, once I've finished adding more tests, and refactored a whole bunch of extremely awkwardly named classes.
Update History
- 2016-03-20 - First published
- 2020-11-21 - Updated formatting
Leave a Comment
While we appreciate comments from our users, please follow our posting guidelines. Have you tried the Cyotek Forums for support from Cyotek and the community?