Introduction
Some jobs are tedious and errorprone. To avoid the errors we should unittest, but to avoid the tediousness we can make use of code generation. One such code generation tool is T4. It is available in Visual studio so why not use it? Well I can think of a few reasons. No intellisense no code coloring and no refactoring. But I guess we will have to learn to live with that. Or use a third party editor like the one from Clarius consulting. So I thought code generation would be a good fit for some of my factory classes I have.
The goal
Well that is simple I want to create this file automagicaly.
Imports ConsoleApplication1.Test
Namespace ConsoleApplication1.T4
''' <summary>
'''
''' </summary>
''' <remarks></remarks>
<CLSCompliant(True)> _
Public Interface IFactory
#Region "Methods"
''' <summary>
'''
''' </summary>
''' <returns></returns>
''' <remarks></remarks>
Function Interface3() As IInterface3
''' <summary>
'''
''' </summary>
''' <returns></returns>
''' <remarks></remarks>
Function Interface4() As IInterface4
#End Region
End Interface
End Namespace```
The point is that it should create a Function for every Interface I have in a certain namespace.
## Getting the interfaces
First problem to solve was how to easily get the interfaces in a nice list.
I made a ConsoleApplication to test this. First I added a few interfaces in 2 different namespace and then I created a program to find the interfaces in the namespace I want.
<div class="image_block">
<img src="/wp-content/uploads/blogs/DesktopDev/T4/T4.png" alt="" title="" width="177" height="179" />
</div>
```csharp
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using ConsoleApplication1.Test2;
namespace ConsoleApplication1
{
class Program
{
static void Main(string[] args)
{
var ty = typeof(Interface1);
var assem = ty.Assembly;
var types = from e in assem.GetTypes() where e.IsInterface && e.Namespace == "ConsoleApplication1.Test" select e;
if( types.Count() > 0)
{
foreach(var cls in types)
{
Console.WriteLine("Function {0}() As {1}",cls.Name.Substring(1),cls.Name);
}
}
Console.ReadLine();
}
}
}
Yes that is C#. And why is it C#? Because I decided to write T4 templates in C#. To keep the generated code apart from the generating code. You could also write the generating code in VB.Net if you like.
So we have this covered. Seemed simple enough.
The template
And now came the tricky part, writing the template.
First of all we create a file called IFactory.tt. Visual studio will give you a warning if you do that, but it is safe to ignore that warning, just pretend that you know what you are doing.
Then we add the generating language. and we tell it to output our file with a .vb extension.
<#@ template language="C#" #>
<#@ output extension="vb" #>
Now for the code.
Imports ConsoleApplication1.Test
Namespace ConsoleApplication1.T4
''' <summary>
'''
''' </summary>
''' <remarks></remarks>
<CLSCompliant(True)> _
Public Interface IFactory
#Region "Methods"
<# var ty = typeof(IInterface3);
var assem = ty.Assembly;
var types = from e in assem.GetTypes() where e.IsInterface && e.Namespace == "ConsoleApplication1.Test" select e;
if( types.Count() > 0)
{
foreach(var cls in types)
{
var _FunctionName = cls.Name.Substring(1);
#>
''' <summary>
'''
''' </summary>
''' <returns></returns>
''' <remarks></remarks>
Function <#=_FunctionName#>() As <#=cls.Name#>
<#
}
}
#>
#End Region
End Interface
End Namespace```
Text between <# #> tags is our code text between <#= #> tags is text that will be output to the file. Text between <#@ #> tags are directives.
The above will fail miserably and Google failed me too.
You will get 2 errors out of the above.
> Compiling transformation: The type or namespace name ‘IFactory’ could not be found (are you missing a using directive or an assembly reference?)
and this one
> Compiling transformation: Could not find an implementation of the query pattern for source type ‘System.Type[]’. ‘Where’ not found. Are you missing a reference to ‘System.Core.dll’ or a using directive for ‘System.Linq’?
Clearly it is complaining that you need to set references and imports.
However it is not always clear to me when you need to this and for which libraries. I guess we just wait for an error and then add it.
In my case I needed to add these.
<#@ assembly name=“C:..binx86DebugConsoleApplication1.exe” #> <#@ assembly name=“System.Core” #> <#@ import namespace=“System” #> <#@ import namespace=“System.Linq” #> <#@ import namespace=“System.Reflection” #> <#@ import namespace=“ConsoleApplication1.Test” #>
The directives with assembly are actualy what you would do when you do Add reference for your project. If it is a system assembly then you can just use the short name like I did for System.Core if you want to reference one of you project then you need to add the fullpath and filename. like I did for my project. If you then have all the references you need you can add import directives. And then it all just works.
So the final code would then be.
<#@ assembly name=“C:..binx86DebugConsoleApplication1.exe” #> <#@ assembly name=“System.Core” #> <#@ import namespace=“System” #> <#@ import namespace=“System.Linq” #> <#@ import namespace=“System.Reflection” #> <#@ import namespace=“ConsoleApplication1.Test” #> Imports ConsoleApplication1.Test
Namespace ConsoleApplication1.T4
''' <summary>
'''
''' </summary>
''' <remarks></remarks>
<CLSCompliant(True)> _
Public Interface IFactory
#Region “Methods”
<# var ty = typeof(IInterface3);
var assem = ty.Assembly;
var types = from e in assem.GetTypes() where e.IsInterface && e.Namespace == "ConsoleApplication1.Test" select e;
if( types.Count() > 0)
{
foreach(var cls in types)
{
var _FunctionName = cls.Name.Substring(1);
#>
''' <summary>
'''
''' </summary>
''' <returns></returns>
''' <remarks></remarks>
Function <#=_FunctionName#>() As <#=cls.Name#>
<#
}
}
#>
#End Region
End Interface
End Namespace``` The tt will now have a vb file attached to it.
And that will then look like this.
All ready for use. Now I can just add an interface and the factory will.
Warning
The above code works, but as soon as you build your project it wiil complain to say that the exe in the bin folder is blocked. This is a well known problem. It has to with the fact that the template engine does not release the AppDomain immediatly, but it caches it. There is however a nice solution to this problem. And it is called the T4 toolbox and the VolatileAssembly. You just need to download and install the Toolbox then you need to swap the assembly directive to this.
You can read more abot this problem in these locations.
<#@ VolatileAssembly processor=“T4Toolbox.VolatileAssemblyProcessor” name=“$(SolutionDir)/ConsoleApplication1/bin/Debug/ConsoleApplication1.exe” #>
Conclusion
Code generation is a powerful tool that can take a lot of work out of your hands. But it can also become a maintenance nightmare if you aren’t careful. I learned a lot while making this post. And all this because someone on Stackoverflow asked how to add the namespace to a class template for VB.Net.
Resources
- T4 (Text Template Transformation Toolkit) Code Generation – Best Kept Visual Studio Secret by Scott Hanselman
- Code generation using T4 Templates by DotNetThoughts
- Everything about T4 by Oleg Sych
- Walkthrough: Creating and Running Text Templates on MSDN
- How-To Q&A: How Can I Automate Code Without Resorting to Heavy Code-Generation Techniques? by Kathleen Dollard