Creating a text templating engine Host

I’m in the process of creating a little application to codegen some source code for me from an XML schema (yes I can do this with xsd but I wanted the code to be more configurable). Instead of writing my own template language etc. I decided to try and leverage the T4 templating language.

I could simply write some code that can be called from a T4 template, but I decided it would be nicer if the codegen application simply acted as a host to the T4 template and allowed the template to call code on the host, so here’s what I did…

Running a T4 template from your application

The first thing I needed was to be able to actually run a T4 template. To do this you’ll need to add the following references

  • Microsoft.VisualStudio.TextTemplating.11.0
  • Microsoft.VisualStudio.TextTemplating.Interfaces10.0
  • Microsoft.VisualStudio.TextTemplating.Interfaces.11.0

Obviously these are the versions at the time of writing, things may differ in the future.

Next we need to instantiate the T4 engine, this is achieved by using the Microsoft.VisualStudio.TextTemplating namespace and with the following code

Engine engine = new Engine();
string result = engine.ProcessTemplate(File.ReadAllText("sample.tt"), host);

Note: The host will be supplied by us in the next section and obviously “sample.tt” would be supplied at runtime in the completed version of the code.

So, here we create a Engine and supply the template string and host to the ProcessTemplate method. The result of this call is the processed template.

Creating the host

Our host implementation will need to derive from MarshalByRefObject and implement the ITextTemplatingEngineHost interface.

Note: See Walkthrough: Creating a Custom Text Template Host for more information of creating a custom text template.

What follows is a basic implementation of the ITextTemplatingEngineHost based upon the Microsoft article noted above.

public class TextTemplatingEngineHost : MarshalByRefObject, ITextTemplatingEngineHost
{
   public virtual object GetHostOption(string optionName)
   {
      return (optionName == "CacheAssemblies") ? (object)true : null;
   }

   public virtual bool LoadIncludeText(string requestFileName, 
            out string content, out string location)
   {
      content = location = String.Empty;

      if (File.Exists(requestFileName))
      {
         content = File.ReadAllText(requestFileName);
         return true;
      }
      return false;
   }

   public virtual void LogErrors(CompilerErrorCollection errors)
   {
   }

   public virtual AppDomain ProvideTemplatingAppDomain(string content)
   {
      return AppDomain.CreateDomain("TemplatingHost AppDomain");
   }

   public virtual string ResolveAssemblyReference(string assemblyReference)
   {
      if (File.Exists(assemblyReference))
      {
         return assemblyReference;
      }

      string candidate = Path.Combine(Path.GetDirectoryName(TemplateFile), 
            assemblyReference);
      return File.Exists(candidate) ? candidate : String.Empty;
   }

   public virtual Type ResolveDirectiveProcessor(string processorName)
   {
      throw new Exception("Directive Processor not found");
   }

   public virtual string ResolveParameterValue(string directiveId, 
            string processorName, string parameterName)
   {
      if (directiveId == null)
      {
         throw new ArgumentNullException("directiveId");
      }
      if (processorName == null)
      {
         throw new ArgumentNullException("processorName");
      }
      if (parameterName == null)
      {
         throw new ArgumentNullException("parameterName");
      }

      return String.Empty;
   }

   public virtual string ResolvePath(string path)
   {
      if (path == null)
      {
         throw new ArgumentNullException("path");
      }

      if (File.Exists(path))
      {
         return path;
      }
      string candidate = Path.Combine(Path.GetDirectoryName(TemplateFile), path);
      if (File.Exists(candidate))
      {
         return candidate;
      }
      return path;
   }

   public virtual void SetFileExtension(string extension)
   {
   }

   public virtual void SetOutputEncoding(Encoding encoding, bool fromOutputDirective)
   {
   }

   public virtual IList<string> StandardAssemblyReferences
   {
      // bare minimum, returns the location of the System assembly
      get { return new[] { typeof (String).Assembly.Location }; }
   }

   public virtual IList<string> StandardImports
   {
      get { return new[] { "System" }; }
   }

   public string TemplateFile { get; set; }
}

Now the idea is that we can subclass the TextTemplatingEngineHost to implement a version specific for our needs.

Before we look at a specialization of this for our purpose, let’s look at a T4 template sample for generating our code

Note: mycodegen is both my assembly name and the namespace for my code generator which itself hosts the T4 engine.

<#@ template debug="false" hostspecific="true" language="C#" #>
<#@ assembly name="mycodegen" #>
<#@ import namespace="mycodegen" #>
<#@ output extension=".cs" #>

<#
   ICodeGenerator cg = ((ICodeGenerator)this.Host);
#>

namespace <#= cg.Namespace #>
{
   <# 
   foreach(var c in cg.Classes) 
   {
   #>
   public partial class <#= c.Name #>
   { 
      <#  
      foreach(Property p in c.Properties)
      {
          if(p.IsArray)
          {
      #>
         public <#= p.Type #>[] <#= p.Name #> { get; set; }
      <#
           }
           else
           {
      #>
         public <#= p.Type #> <#= p.Name #> { get; set; }
      <#
           }
      }
      #>
   }
   <#
   }
   #>
}

So in the above code you can see that our host will support an interface named ICodeGenerator (which is declared in the mycodegen assembly and namespace). ICodeGenerator will simply supply the class names and properties for the classes extracted from the XML schema and we’ll use the T4 template to generate the output. By using this template we can easily change how we output our classes and properties, for example xsd creates fields which are not required if we use auto-implemented property syntax, plus we can change the naming convention, property name case and so on an so forth. Whilst we could add code to the partial classes generated by including other files implementing further partial methods etc. if a type no longer exists in a month or two we need to ensure we deleted the manually added code if we want to keep our code clean. Using the T4 template we can auto generate everything we need.

References

Walkthrough: Creating a Custom Text Template Host
Processing Text Templates by using a Custom Host