This article discusses design challenges developers face as complexity of their T4 templates increases; defines qualities of a reusable T4 templates; discusses techniques that can be used to achieve these qualities and compares their pros and cons.
Increasing Complexity of T4 Templates
When adopting T4, developers may start by generating one-of-a-kind artifact, such as navigation class with constants for pages in an ASP.NET application. Early in adoption, a given project may have one or two standalone templates generating different artifacts. At this stage, template simplicity is important and reuse is a non-issue.
As developers become familiar and comfortable with T4, they begin generating multiple artifacts of the same type, such as CRUD stored procedures. It becomes important to reuse template code to generate all outputs of the same type. Having one physical copy of the shared template code allows developers to fix bugs that affect all artifacts in a single place.
As the number of artifact types generated with T4 templates increases, so does the complexity of the templates. It becomes increasingly important to reduce duplication and reuse common template code. For example, a template that generates an INSERT stored procedure may use the same code to retrieve database table metadata as the template that generates an UPDATE stored procedure. At this stage developers also look into generating multiple outputs from a single template (single model). Although it is possible to put all CRUD stored procedures in one file, having them in separate files improves our ability to track individual changes in source control and helps to maintain a clean mapping between logical structure of the source code (namespaces, classes, stored procedures) and its physical structure (folders and files).
Eventually developers may want to reuse their T4 templates in different projects they are working on over time. At this stage, it becomes important to be able to customize previously developed templates for unique requirements of each new project.
Finally, developers may need to develop and maintain templates for use by other developers on different teams and projects. For example, a software tool vendor may be selling a commercial T4 template library; a large software company may have a team developing templates for other teams. This scenario presents the biggest challenge as template users need to be able to customize pre-packaged templates while template developers need to release new versions of the pre-packaged templates that preserve users’ customizations and backward compatibility.
Limitations of T4 Template Design
Complexity of template usage scenarios ranges, on one side, from one-of-a-kind templates may not need to be reused at all. On the other side, commercially produced templates need to be extensible, composable, released independently and backward compatible. This wide range in complexity is not unlike complexity of software in general, which ranges from one-off "Hello World" programs to software frameworks, such as .NET Framework Class Library.
Modern software frameworks use object-oriented design to deal with this complexity. Classes are used to encapsulate software components. Inheritance and virtual methods are used to make components extensible. Careful design of public and protected members allows components to be backward compatible. It would be ideal to use the same object-oriented approach when designing reusable T4 templates. However, our options are limited by the constraints imposed by the T4 template syntax and compilation process.
- T4 compiles a template into a single TextTransformation class. All templates it references using include directive become a part of this one class.
- T4 compiles templates in temporary assemblies. There is no practical way to instantiate or inherit directly from a compiled template class.
- T4 does not support code-behind model. All template code must be placed in statement blocks and class feature blocks.
- While it is possible to use an assembly directive to reference a custom assembly with shared code, T4 caches it in memory and locks the file to improve code generation performance. This requires you to reopen the code generation solution in order to compile changes made in the custom assembly.
What Makes a T4 Template Reusable?
Several specific design qualities affect developer’s ability to reuse a T4 template.
- Parameterization. Reusable template should allow template developer to define a set of one or more template parameters. Template user should be able to specify parameter values when using the template. Consider a template that generates an INSERT stored procedure. Template user needs to be able to specify a database table for which the INSERT procedure needs to be generated.
- Composability. Template users should be able to use two or more templates when generating multiple outputs from a single template (model). Consider a template that generates all CRUD stored procedures for a given database table. This template may need to use separate INSERT, UPDATE, DELETE and SELECT templates to generate all required code.
- Encapsulation. Reusable template should hide its implementation details and prevent template user from accidentally changing it or breaking it, which could happen when using two different templates to generate two outputs from a single main template.
- Extensibility. Template developer should be able to define extension points for template user to customize output generation. Consider a template that generates a Data Transfer Object as a class with data properties. Template user may need to be able to specify different serialization attributes for the generated properties depending on which serialization mechanism (XmlSerializer, DataContractSerializer, etc) is being used.
T4 Template Design Techniques
Techniques in this section are listed (approximately) in the order of increasing complexity. This roughly matches stages of T4 adoption. Follow links to separate articles for detailed information about each technique.
Merged Template Class
Merged Template Class technique uses class feature blocks to define parameters as fields or properties in the main template. The calling template uses an include directive to merge code of main template with the code of the calling template and assigns parameter values in a statement block.
This technique allows to quickly parameterize a one-off template and start generating multiple artifacts of the same type. It is appropriate during early stages in adoption of T4 code generation or for simple code generation scenarios. Due to weak encapsulation and fragile, implicit interface between main and calling templates, this technique does not scale beyond a single project or a single development team.
Template Method technique encapsulates the main template as a method in a class feature block. Template parameters are defined as parameters of the method. The calling template uses an include directive to merge code of the template method with the code of the calling template and calls the method from a statement block.
Advantages of this technique compared to Merged Template Class include explicit definition of template parameters and ability to compose templates to generate multiple outputs from a single calling template. However, it’s lack of explicit extensibility interface does not allow this technique to scale out beyond a single development team.
Standalone Template technique defines parameters as properties that retrieve their values from an external data store, such as CallContext or an XML file. The calling template uses T4 engine to compile the standalone template into a separate assembly and run the compiled code independently of the calling template.
Compared to Merged Template Class and Template Method this technique offers a better encapsulation. Similar to Template Method this technique allows compose templates to generate multiple outputs from a single calling template. Although better encapsulation will allow template reuse in larger projects and multiple development teams, its lack of extensibility mechanisms may force developers to modify templates manually and increase maintenance costs.
Nested Template Class
Nested Template Class technique defines the main template as a nested TextTransformation class in a class feature block. The nested class uses properties to define template parameters and virtual methods to define extension points. The calling template uses an include directive to merge code of the main template into the calling template. Main template can be extended by subclassing the nested template class and overriding its virtual methods. The nested template class is instantiated in the statement block of the calling template.
Nested Template Class technique allows using all standard C# and Visual Basic language features in T4 template design, such as properties to define template parameters, private visibility for encapsulation, inheritance and virtual methods for extensibility.
Complexity of templates increases as development teams adopt T4 code generation and need to reuse their templates within a single project, in multiple projects and across multiple teams. Although T4 imposes significant constraints on template design, several techniques can be used to make templates reusable. Developers may start by developing one-off templates and apply Merged Template Class, Template Method and Nested Template Class techniques to make their templates more and more reusable. Although many developers (especially those familiar with CodeSmith) may be inclined to use Standalone Template technique, its complexity and lack of extensibility make it inferior to Nested Template Class. Standalone Template technique is the only available option for designing templates that need to be called by external code.
T4 (Text Template Transformation Toolkit) is a template-based code generation engine. It is available in Visual Studio 2008 and as a download in DSL and GAT toolkits for Visual Studio 2005. T4 engine allows you to use ASP.NET-like template syntax to generate C#, T-SQL, XML or any other text files.
For more information about T4, check out my previous article.