Generating multiple files from one T4 template

In the previous posts about T4 I firstly drove T4 generation from EF entity definitions, then used this to make EF POCO classes with certain properties implement an interface. Please read these posts before reading this one – in particular the code in this post refers to code from the previous one.

In this post, I’ll extend what I’ve already built to handle multiple interfaces, and to generate a single file per interface.

For this example, I’m going to use two interfaces.

public interface IUserNameStamped
{
    string UserName { get; set; }
}

public interface ILookup
{
    string Code { get; set; }

    string Description { get; set; }
}

I want my EF POCOs to implement IUserNameStamped if they have a UserName property, and ILookup if they have a Code and Description property. I want the IUserNameStamped code in a file IUserNameStamped.cs, and ILookup code in a file ILookup.cs.

By default, a T4 template will generate a single file with the same name as the the template, and the extension defined by the <#@ output #> directive. The EntityFrameworkTemplateFileManager, used by EF to generate a file per entity, is the secret to generating multiple files from a single template.

The other change needed to the T4 code we already have is to break it into reusable methods that can be shared for each entity.

The method I’ve defined to generate an file for a given interface is CreateInterfaceFile, shown here with support classes.

<#+
void CreateInterfaceFile(EntityFrameworkTemplateFileManager fileManager,  
	CodeGenerationTools code,
	EdmItemCollection itemCollection,
	string namespaceName, 
	Action interfaceWriter, 
	string interfaceName, 
	params string[] requiredProperties)
{
    fileManager.StartNewFile(interfaceName + ".cs");
	BeginNamespace(namespaceName, code);
	interfaceWriter();
	var entities = GetEntitiesWithPropertyOrRelationship(itemCollection,
		requiredProperties);
	foreach (EntityType entity in entities.OrderBy(e => e.Name))
	{
		WriteInterfaceImplementation(entity.Name, interfaceName);
	}
	EndNamespace(namespaceName);
}
#>
<#+
void BeginNamespace(string namespaceName, CodeGenerationTools code)
{
    CodeRegion region = new CodeRegion(this);
    if (!String.IsNullOrEmpty(namespaceName))
    {
#>
namespace <#=code.EscapeNamespace(namespaceName)#>
{
<#+
        PushIndent(CodeRegion.GetIndent(1));
    }
}


void EndNamespace(string namespaceName)
{
    if (!String.IsNullOrEmpty(namespaceName))
    {
        PopIndent();
#>
}
<#+
    }
}

IEnumerable<EntityType> GetEntitiesWithPropertyOrRelationship(
	EdmItemCollection itemCollection, 
	params string[] requiredProperties)
{
	return itemCollection.GetItems<EntityType>().Where(entity => 
		EntityHasPropertyOrRelationship(entity, requiredProperties));
}

bool EntityHasPropertyOrRelationship
	(EntityType entity, params string[] requiredProperties)
{
	return requiredProperties.All(
		requiredProperty => entity.Properties.Any(property => property.Name == requiredProperty)
		|| entity.NavigationProperties.Any(property => property.Name == requiredProperty));
}

void WriteInterfaceImplementation(string entityName, string interfaceName)
{
#>

public partial class <#=entityName#> : <#=interfaceName#>
{
}
<#+
}

The parameters of CreateInterfaceFile:

  • The first three parameters are T4 and EF classes instantiated at the top of the template and passed in.
  • namespaceName is also provided by T4 – the namespace the interface and classes will belong to.
  • interfaceWriter is a action that writes out the definition of the interface itself.
  • interfaceName is the name of the interface.
  • requiredProperties is an array of all the properties a class must have to be considered to implement the interface.

The logic is very simple

  • The EntityFrameworkTemplateFileManager is used to start a file for the interface – all output now goes to this file until the next time StartNewFile is called.
  • The namespace is written.
  • The declaration of the interface is written.
  • Entities matching this interface are found using GetEntitiesWithPropertyOrRelationship (as explained in the previous blog post.
  • An partial implementation of the class for each matching entity is written, with no content, simply stating that the class implements the interface in question.
  • The namespace is closed.

That’s about all there is to it. Once again, an extension to this code to match entity properties by type as well as name is left as an exercise to the reader.

Here is full source code:

<#@ template language="C#" debug="false" hostspecific="true"#>
<#@ include file="EF.Utility.CS.ttinclude"#><#@
 output extension=".cs"#><#

string inputFile = @"OticrsEntities.edmx";
EdmItemCollection itemCollection = new MetadataLoader(this).
	CreateEdmItemCollection(inputFile);

CodeGenerationTools code = new CodeGenerationTools(this);
string namespaceName = code.VsNamespaceSuggestion();

EntityFrameworkTemplateFileManager fileManager = 
	EntityFrameworkTemplateFileManager.Create(this);
WriteHeader(fileManager);

#>
// Default file generated by T4. Generation cannot be prevented. Please ignore.
<#

CreateInterfaceFile(fileManager, 
	code,
	itemCollection, 
	namespaceName,
	WriteILookupInterface,
	"ILookup",
	"ContractorCode", "Description");

CreateInterfaceFile(fileManager, 
	code,
	itemCollection, 
	namespaceName,
	WriteIUserNameStampedInterface,
	"IUserNameStamped",
	"UserName");
	
fileManager.Process(true);

#>
<#+
void CreateInterfaceFile(EntityFrameworkTemplateFileManager fileManager,  
	CodeGenerationTools code,
	EdmItemCollection itemCollection,
	string namespaceName, 
	Action interfaceWriter, 
	string interfaceName, 
	params string[] requiredProperties)
{
    fileManager.StartNewFile(interfaceName + ".cs");
	BeginNamespace(namespaceName, code);
	interfaceWriter();
	var entities = GetEntitiesWithPropertyOrRelationship(itemCollection, 
		requiredProperties);
	foreach (EntityType entity in entities.OrderBy(e => e.Name))
	{
		WriteInterfaceImplementation(entity.Name, interfaceName);
	}
	EndNamespace(namespaceName);
}
#>
<#+
void WriteHeader(EntityFrameworkTemplateFileManager fileManager, 
	params string[] extraUsings)
{
    fileManager.StartHeader();
#>
//------------------------------------------------------------------------------
// <auto-generated>
//     This code was generated from a template.
//
//     Changes to this file may cause incorrect behavior and will be lost if
//     the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------

using System.Collections.Generic;

<#=String.Join(String.Empty, extraUsings.
		Select(u => "using " + u + ";" + Environment.NewLine).
		ToArray())#>
<#+
    fileManager.EndBlock();
}

void BeginNamespace(string namespaceName, CodeGenerationTools code)
{
    CodeRegion region = new CodeRegion(this);
    if (!String.IsNullOrEmpty(namespaceName))
    {
#>
namespace <#=code.EscapeNamespace(namespaceName)#>
{
<#+
        PushIndent(CodeRegion.GetIndent(1));
    }
}


void EndNamespace(string namespaceName)
{
    if (!String.IsNullOrEmpty(namespaceName))
    {
        PopIndent();
#>
}
<#+
    }
}

IEnumerable<EntityType> GetEntitiesWithPropertyOrRelationship(
	EdmItemCollection itemCollection, 
	params string[] requiredProperties)
{
	return itemCollection.GetItems<EntityType>().Where(entity => 
		EntityHasPropertyOrRelationship(entity, requiredProperties));
}

bool EntityHasPropertyOrRelationship(EntityType entity, 
	params string[] requiredProperties)
{
	return requiredProperties.All(requiredProperty => 
		entity.Properties.Any(property => property.Name == requiredProperty)
		|| entity.NavigationProperties.Any(property => property.Name == requiredProperty));
}

void WriteInterfaceImplementation(string entityName, string interfaceName)
{
#>

public partial class <#=entityName#> : <#=interfaceName#>
{
}
<#+
}

void WriteILookupInterface()
{
#>
/// <summary>
/// A lookup entity, that can be looked up by a ContractorCode
/// </summary>
public interface ILookup
{
    string ContractorCode { get; set; }
	
	string Description { get; set; }
}
<#+
}

void WriteIUserNameStampedInterface()
{
#>
/// <summary>
/// An entity that is stamped with the Username that created it
/// </summary>
public interface IUserNameStamped
{
    string UserName { get; set; }
}
<#+
}
#>