Using the .NET Compiler Platform in T4 Templates

Metaprogramming with Roslyn.

Published on Thursday, April 23, 2015

T4 templates provide a powerful way to generate code at design time (and sometimes at compile time if you set up Visual Studio appropriately). The traditional way of accessing the code of your solution from within a T4 template is to get the Visual Studio API (called DTE). This has always seemed like a bit of a kludge to me and feels a little too far removed from the code and what it represents. We now have another option by using the .NET Compiler Platform from within a T4 template to parse, query, and output content based on the files in our solution.

In my particular case I wanted to scan all the source files in the same folder as the template, look for classes that derive from a specific base class (Module), iterate over all their public constructors, and then output extension methods for the IPipeline interface for each constructor that instantiates the class using that constructor and adds it to the pipeline. Here's what the template looks like:

<#@ template debug="false" hostspecific="true" language="C#" #>
<#@ assembly name="System.Core" #>
<#@ assembly name="System.IO" #>
<#@ assembly name="System.Runtime" #>
<#@ assembly name="System.Text.Encoding" #>
<#@ assembly name="System.Threading.Tasks" #>
<#@ assembly name="$(TargetDir)Microsoft.CodeAnalysis.dll" #>
<#@ assembly name="$(TargetDir)Microsoft.CodeAnalysis.CSharp.dll" #>
<#@ assembly name="$(TargetDir)System.Collections.Immutable.dll" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Text" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ import namespace="System.IO" #>
<#@ import namespace="Microsoft.CodeAnalysis" #>
<#@ import namespace="Microsoft.CodeAnalysis.CSharp" #>
<#@ import namespace="Microsoft.CodeAnalysis.CSharp.Syntax" #>
<#@ output extension=".cs" #>
using System;
using System.Collections.Generic;
using System.IO;

<# Process(); #>
<#+
	public void Process()
	{
		// Get a SyntaxTree for every file
		foreach(CSharpSyntaxTree syntaxTree in Directory.EnumerateFiles(Path.GetDirectoryName(Host.TemplateFile))
			.Where(x => Path.GetExtension(x) == ".cs")
			.Select(x => CSharpSyntaxTree.ParseText(File.ReadAllText(x)))
			.Cast<CSharpSyntaxTree>())
		{
			// Get all class declarations in each file that derive from Module
			foreach(ClassDeclarationSyntax classDeclaration in syntaxTree.GetRoot()
				.DescendantNodes()
				.OfType<ClassDeclarationSyntax>()
				.Where(x => x.BaseList != null && x.BaseList.Types
					.Any(y => y.Type is Microsoft.CodeAnalysis.CSharp.Syntax.IdentifierNameSyntax 
						&& ((Microsoft.CodeAnalysis.CSharp.Syntax.IdentifierNameSyntax)y.Type).Identifier.Text == "Module")))
			{
				// Output the same namespace as the class
				SyntaxNode namespaceNode = classDeclaration.Parent;
				while(namespaceNode != null && !(namespaceNode is NamespaceDeclarationSyntax))
				{
					namespaceNode = namespaceNode.Parent;
				}
				if(namespaceNode != null)
				{
					WriteLine("namespace " + ((NamespaceDeclarationSyntax)namespaceNode).Name.ToString() + Environment.NewLine + "{");
				}
			
				// Output the extensions class
				WriteLine("    public static class " + classDeclaration.Identifier.Text + "PipelineExtensions" + Environment.NewLine + "    {");
			
				// Get all non-static public constructors
				foreach(ConstructorDeclarationSyntax constructor in classDeclaration.Members
					.OfType<ConstructorDeclarationSyntax>()
					.Where(x => x.Modifiers.Count == 1 && x.Modifiers[0].Text == "public"))
				{
					// Output the static constructor method
					WriteLine("        public static IPipeline " + classDeclaration.Identifier.Text + constructor.ParameterList.ToString().Insert(1, "this IPipeline pipeline, ") + Environment.NewLine + "        {");
				
					// Create and add the module
					WriteLine("            return pipeline.AddModule(new " + classDeclaration.Identifier.Text + "(" + string.Join(", ", constructor.ParameterList.Parameters.Select(x => x.Identifier.Text)) + "));");
				
					// Close method
					WriteLine("        }");
				}
			
				// Close extensions class
				WriteLine("    }");			
			
				// Close namespace
				if(namespaceNode != null)
				{
					WriteLine("}");
				}
			}
		}
	}
#>

And this is some example output:

using System;
using System.Collections.Generic;
using System.IO;

namespace Wyam.Core.Modules
{
    public static class AppendPipelineExtensions
    {
        public static IPipeline Append(this IPipeline pipeline, string content)
        {
            return pipeline.AddModule(new Append(content));
        }
    }
}

A couple things to note:

  • You must use the Roslyn assemblies from NuGet. If you try to build them yourself, they won't work out of the box in a T4 template because of the way Roslyn assemblies are delay signed.
  • In this case I didn't even need to compile or get a semantic model, the syntax tree was enough for me. If you need to go further (such as using symbol information) you can always bring in the Roslyn compilation APIs.
  • I prefer to write my T4 templates entirely in C# and output content using WriteLine(). You can obviously use a different approach such as interspersing control logic with template content.
comments powered by Disqus