This content has moved - please find it at https://devblog.cyotek.com.

Although these pages remain accessible, some content may not display correctly in future as the new blog evolves.

Visit https://devblog.cyotek.com.

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

About The Author

Gravatar

The founder of Cyotek, Richard enjoys creating new blog content for the site. Much more though, he likes to develop programs, and can often found writing reams of code. A long term gamer, he has aspirations in one day creating an epic video game. Until that time, he is mostly content with adding new bugs to WebCopy and the other Cyotek products.

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?

Styling with Markdown is supported