T4 Templates: Code that writes code for you

January 30, 2014

Let’s say you have a scenario where you have to write a bunch of repetitive boilerplate code but you don’t really want to (and you don’t have anyone else to pass it off to). Here is our totally “mythical” scenario:

You have a MVC project with jQuery Ajax calls to a WCF service. Now because of the need to load balance, the WCF service and the MVC web project won’t exist on the same server. The problem exists because javascript doesn’t like cross-site scripting, so you can’t call your WCF service on a different host. You decide to just create a WCF proxy service that lives with the MVC project that javascript is ok with calling and this proxy service calls your WCF service implementation. It is basically just a pass through. With all of that back story, here is THE problem: you have to write the pass-through implementation for every single method call. This is a perfect example for T4 Templates.

Big thanks and credit for the assist on this blog post to Shawn Doucet, my coworker at Sparkhound for pair programming it with me and turning this task from many, many hours to just one or two. He also wrote the two helper methods to fix the names for generic types.

First, let’s look at what the end result should be so that we know where we are heading:

public UserValidationResponse ValidateUser(
            UserValidationRequest request, bool byPassAuthorization)
{
    SecurityServiceClient svcClient = new SecurityServiceClient();
    try
    {
        return svcClient.ValidateUser(request, byPassAuthorization);
    }
    finally
    {
        svcClient.Close();
        svcClient = null;
    }
}

Now that we know what we need, let’s look at the completed T4 Template and then go from there:

<#@ template debug="false" hostspecific="true" language="C#" #>
<#@ assembly name="System.Core" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.IO" #>
<#@ import namespace="System.Text" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ import namespace="System.Reflection" #>
<#@ output extension=".txt" #>
<#@ assembly name="$(ProjectDir)..\ServicesContract\bin\Debug\ServicesContract.dll"#>
<#@ assembly name="EnvDTE"#>
<#
    string[] serviceList = { "ICommonService", "ICrewService", "IPayrollService", "IScheduleService", "ISecurityService", "ITimesheetService", "IValidationService" };
    var assembly = Assembly.GetAssembly(typeof(ServicesContract.Timesheet.ITimesheetService));
    var types = assembly.GetTypes();
 
    foreach (var classType in types.Where(x => serviceList.Contains(x.Name)))
    {
        int interfaceOffset = classType.IsInterface ? 1 : 0;
        string className = classType.Name.Substring(interfaceOffset, classType.Name.IndexOf("Service") - interfaceOffset);
        if(className == "Payroll")
            className = "Workflow";
        string classTypeName = classType.Name.Substring(interfaceOffset);
    #>
//------------------------------------------------------------------------------
// <auto-generated>
//     This code was generated by a T4 template.
//     Generated on: <#=System.DateTime.Now.ToString()#>
//
//     Changes to this file may cause incorrect behavior and will be lost if
//     the code is regenerated. Re-run the T4 template to update this file.
// </auto-generated>
//------------------------------------------------------------------------------
using System;
 
namespace ServicesProxy
{
    public partial class <#=classTypeName#>Proxy : ServicesContract.<#=className#>.I<#=classTypeName#>
    {
<#
            foreach(var meth in classType.GetMethods(BindingFlags.DeclaredOnly | BindingFlags.Instance | BindingFlags.Public))
            {
                var param = meth.GetParameters();
 
                var name = meth.Name;
                var returnType = FixUpTypeName(meth.ReturnType);
 
                StringBuilder parameterList = new StringBuilder();
                StringBuilder parameterListWithTypes = new StringBuilder();
                foreach(var p in param)
                {
                    parameterList.Append(p.Name);
                    parameterListWithTypes.Append(FixUpTypeName(p.ParameterType) + " " + p.Name);
                    if(p != param.Last())
                    {
                        parameterList.Append(", ");
                        parameterListWithTypes.Append(", ");
                    }
                }
#>
        public <#=returnType#> <#=name#>(<#=parameterListWithTypes#>)
        {
            ServicesProxy.<#=classTypeName#>.<#=classTypeName#>Client svcClient = new <#=classTypeName#>.<#=classTypeName#>Client();
            try
            {
                <# if(returnType != "void"){#>return <#}#>svcClient.<#=name#>(<#=parameterList#>);
            }
            finally
            {
                svcClient.Close();
                svcClient = null;
            } 
        }
 
<#
        }
        #>
    }
}
 
<#        SaveOutput(classTypeName + "Proxy.cs");
    }
#>
 
<#+
        void SaveOutput(string outputFileName)
        {
            string templateDirectory = Path.GetDirectoryName(Host.TemplateFile);
            string outputFilePath = Path.Combine(templateDirectory, outputFileName);
            File.WriteAllText(outputFilePath, this.GenerationEnvironment.ToString()); 
 
            this.GenerationEnvironment.Remove(0, this.GenerationEnvironment.Length);
        }
 
        public string FixUpType(Type t)
        {
            string type = t.FullName;
            type = type.Replace("System.", "");
 
            switch (type)
            {
                case "String":
                    return "string";
                case "Byte":
                    return "byte";
                case "Byte[]":
                    return "byte[]";
                case "Int16":
                    return "short";
                case "Int32":
                    return "int";
                case "Int64":
                    return "long";
                case "Char":
                    return "char";
                case "Single":
                    return "float";
                case "Double":
                    return "double";
                case "Boolean":
                    return "bool";
                case "Decimal":
                    return "decimal";
                case "SByte":
                    return "sbyte";
                case "UInt16":
                    return "ushort";
                case "UInt32":
                    return "uint";
                case "UInt64":
                    return "ulong";
                case "Object":
                    return "object";
                case "Void":
                    return "void";
                default: 
                    return type;
            }
        }
 
        public string FixUpTypeName(Type type)
        {
            var ret = FixUpType(type);
            if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>))
            {
                ret = string.Format("{0}?", FixUpType(Nullable.GetUnderlyingType(type)));
            }
 
            else if (type.IsGenericType)
            {
                var inner = string.Empty;
                foreach (var t in type.GetGenericArguments())
                {
                    if (t.IsGenericType)
                    { 
                        var outer1 = t.GetGenericTypeDefinition().FullName;
                        var ary1 = outer1.Split(@"`".ToCharArray());
                        outer1 = ary1[0];
 
                        string inner1 = string.Empty;
                        foreach (var t1 in t.GetGenericArguments())
                        {
                            inner1 += t1.FullName;
                            inner1 += ",";
                        }
                        inner1 = inner1.TrimEnd(",".ToCharArray());
                        inner += string.Format("{1}<{0}>", inner1, outer1);
                    }
                    else
                    {
                        inner += t.FullName;
                        inner += ",";
                    }
                }
                inner = inner.TrimEnd(",".ToCharArray());
                string name = FixUpType(type.GetGenericArguments()[0]);
                string outer = type.GetGenericTypeDefinition().FullName;
                string[] ary = outer.Split(@"`".ToCharArray());
                outer = ary[0];
                ret = string.Format("{1}<{0}>", inner, outer); 
            }
            else
            {
                return ret;
            }
 
            return ret;
        }
#>

I decided to go the route of putting the full namespace in the file instead of trying to keep a running list of the using statements at the top. Let’s break apart the template into pieces.

<#@ template debug="false" hostspecific="true" language="C#" #>
<#@ assembly name="System.Core" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.IO" #>
<#@ import namespace="System.Text" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ import namespace="System.Reflection" #>
<#@ output extension=".txt" #>
<#@ assembly name="$(ProjectDir)..\ServicesContract\bin\Debug\ServicesContract.dll"#>
<#@ assembly name="EnvDTE"#>

As you can probably guess, these are imports or usings for the template itself. “Hostspecific” and “EnvDTE” are common things used when doing T4 generation. The “ServiceContract.dll” is the project for my interfaces that the WCF service implements. I decided to go with the Interface instead of the methods themselves so that it wouldn’t pick up other public or inherited methods that are in the class but not in the endpoint. I changed the output extension to “.txt” instead of “.cs” since I will be outputting multiple files. If you are writing to a single file then you can just have “.cs”.

Before we move on any further, you can see that there is some special syntax for writing the template. I found this very similar to writing MVC Razor syntax in which you are mixing HTML and server-side C#. To include a code-block is just a matter of wrapping it with “ <#   #> “. If you want to do in-line evaluations, then use “ <#=  #> “. Lastly, any method/functions have to be at the end of the file and use “ <#+   #> “.

<#
    string[] serviceList = { "ICommonService", "ICrewService", 
                            "IEquipmentService", "IScheduleService", 
                            "ISecurityService", "ITimesheetService", 
                            "IValidationService" };
    var assembly = Assembly.GetAssembly(
                typeof(ServicesContract.Timesheet.ITimesheetService));
    var types = assembly.GetTypes();
 
    foreach (var classType in types.Where(x => serviceList.Contains(x.Name)))
    {
        int interfaceOffset = classType.IsInterface ? 1 : 0;
        string className = 
                classType.Name.Substring(
                        interfaceOffset, 
                        classType.Name.IndexOf("Service") - interfaceOffset);
        if(className == "Payroll")
            className = "Workflow";
        string classTypeName = classType.Name.Substring(interfaceOffset);
    #>

Instead of dynamically grabbing all classes in the assembly, I just created a list “serviceList” since I knew that we wouldn’t be changing these. Using reflection, I grab the assembly and loop through all of the types that are in my list. Next I create local variables to hold the class names. The next few lines are just code that lets me swap interfaces or concrete implementations without having to change the code and I don’t have to worry about the “I” in the interface name throwing off all of my class names.

I next added the comment at the top of the file to let developers know that this file was auto-generated. It isn’t necessary, but I thought it would be helpful.

namespace ServicesProxy
{
    public partial class <#=classTypeName#>Proxy : ServicesContract.<#=className#>.I<#=classTypeName#>
    {
<#
        foreach(var meth in classType.GetMethods(
                    BindingFlags.DeclaredOnly | 
                    BindingFlags.Instance | 
                    BindingFlags.Public))
        {
            var param = meth.GetParameters();
            var name = meth.Name;
            var returnType = FixUpTypeName(meth.ReturnType);
 
            StringBuilder parameterList = new StringBuilder();
            StringBuilder parameterListWithTypes = new StringBuilder();
            foreach(var p in param)
            {
                parameterList.Append(p.Name);
                parameterListWithTypes
                    .Append(FixUpTypeName(p.ParameterType) + " " + p.Name);
                if(p != param.Last())
                {
                    parameterList.Append(", ");
                }
                parameterListWithTypes.Append(", ");
            }
#>

Everything that is outside of the “ <# #> “ will be the C# code that is inserted into the file. The difference between className and classTypeName is one has “Service” in the name. I create the method name using the variables. What is important is that I have these methods implementing the same interface that I am pulling the methods from. This ensures a compile time check that everything is implemented. After that, I needed to get the parameters for the method calls. I have two variables for parameters: one to store the parameters with their types for the method declaration and the other for just the names when calling the implementation. This is where the two methods are used to fix generics by removing the “`” from the generic’s name. It also converts the appropriate type to the correct lowercase keyword.

public <#=returnType#> <#=name#>(<#=parameterListWithTypes#>)
{
    ServicesProxy.<#=classTypeName#>.<#=classTypeName#>Client svcClient = new <#=classTypeName#>.<#=classTypeName#>Client();
    try
    {
        <# if(returnType != "void"){#>return <#}#>svcClient.<#=name#>(<#=parameterList#>);
    }
    finally
    {
        svcClient.Close();
        svcClient = null;
    }
 
}
 
<#
}
#>
    }
}

Now we have the code to actually make the method call. I had to put a special check in for void methods to make sure those didn’t contain the return keyword. At the very bottom are the curly braces to close the for each that loops through each method.

<#        SaveOutput(classTypeName + "Proxy.cs");
    }
#>
 
<#+
        void SaveOutput(string outputFileName)
        {
            string templateDirectory = 
                Path.GetDirectoryName(Host.TemplateFile);
            string outputFilePath = 
                Path.Combine(templateDirectory, outputFileName);
            File.WriteAllText(outputFilePath, 
                            this.GenerationEnvironment.ToString()); 
 
            this.GenerationEnvironment.Remove(0, 
                                this.GenerationEnvironment.Length);
        }

Lastly is the call to SaveOutput. This is how multiple files are generated. For each “Service” that I loop through, I create a file for it (ie SecurityServiceProxy.cs). Simply, it flushes the output to that file and then starts over. This could be removed and just dump everything to one file; it is just a matter of preference.

At the very end, we have the two methods for correcting types and type names.

This T4 template would just need to be re-ran anytime a new method was added. You can set it to regenerate it on build, however everyone would constantly have that file checked out. That would be better if you were at the beginning of the project and still in the process of creating all of the methods.

Is this interesting, want to learn more, or are having the same discussions in your own department? Get in touch with us so we can discuss how we can help your business achieve its goals through leadership and technology.

Information and material in our blog posts are provided "as is" with no warranties either expressed or implied. Each post is an individual expression of our Sparkies. Should you identify any such content that is harmful, malicious, sensitive or unnecessary, please contact marketing@sparkhound.com.

Meet Sparkhound

Review our capabilities and services, meet the leadership team, see our valued partnerships, and read about the hardware we've earned.

Learn How We Work

See how our Plan/Build/Run methodology drives real client success, and gain our team's perspectives on timely tech topics.

Engage With Us

Get in touch any of our offices, or checkout our open career positions and consider joining Sparkhound's dynamic team.