In the evolving landscape of .NET Development, code generation has been a cornerstone for creating boilerplate code, improving developer productivity and automating processes. T4 (Text Template Transformation Toolkit) has been the go-to solution for well over a decade for this purpose, offering a powerful but also a somewhat uderappreciated templating engine right inside Visual Studio. They have enabled developers to generate everything from data models to fully fledged API clients.

But, working with T4 has had issues since the start. There is no easy way to work with them, since there is no propper editor experience for them. They are hard to debug and by default you find yourself working in a ‘plain text’ environment, having no intellisense, syntax highlighting or language support. The open source community provided some solutions for this, but often they are incomplete.

I myself have been developing tools the past few years that provide some support when working with T4 templates. T4Editor provides a minimal editor experience, and T4Executer helps you control the execution of T4 templates during build time.

The birth of Source Generators

The introduction of source generators in C# 9.0 and .NET 5 marks a significant evolution in how .NET developers approach code generation. Built on the Roslyn compiler platform, source generators promise to enhance, or can we say revolutionize code generation by integrating directly into the compile-time environment, offering a fluent and more performant alternative to traditional methods.

This leads to the question - are source generators here to make T4 templates a thing from te past? In this post we provide a high level overview and comparison between these tools.

T4 Templates

T4 (Text Template Transformation Toolkit) templates have long been the standard for code generation within the .NET ecosystem. Embedded within Visual Studio, T4 templates offer a dynamic way to generate textual content based on predefined templates. These templates can include any sort of text, ranging from HTML and XML to fully functional C# code, making them incredibly versatile for a wide range of applications.

How T4 Templates Work

A T4 template file (.tt) consists of two main parts: template directives and control logic. The directives provide the compiler with information about how to process the template, such as the output file type. The control logic, written in a mixture of C# or VB.NET and T4 syntax, defines the actual content generation logic. When a T4 template is executed, it reads these instructions to produce the final text output, which can be another source code file, a configuration file, or any text-based file that suits the developer’s needs.

Example

Generating DTOs from Database Models

Imagine you have a set of entity models corresponding to database tables in your .NET application. You frequently need to create Data Transfer Objects (DTOs) based on these entity models for safe data transmission over the network. Instead of manually crafting each DTO, you can use a T4 template to automate this process.

<#@ template debug="false" hostspecific="false" language="C#" #>
<#@ output extension=".cs" #>
<#@ assembly name="System.Core" #>
<#@ import namespace="System.Collections.Generic" #>

namespace MyApplication.DTOs
{
<# foreach(var model in ModelEntities) { #>
    public class <#= model.Name #>Dto
    {
    <# foreach(var property in model.Properties) { #>
        public <#= property.Type #> <#= property.Name #> { get; set; }
    <# } #>
    }
<# } #>
}

In this example, ModelEntities would be a collection of your entity models, each with Name, Properties, and other necessary metadata. This T4 template iterates over each model, generating a corresponding DTO class with matching properties. Note the syntax using <# #> codeblocks, which require a learning curve to master.

Applications and Strengths

T4 templates excel in scenarios where repetitive code patterns are common, such as in:

  • Data model generation based on database schemas
  • Generating boilerplate code for repetitive tasks (e.g., CRUD operations)
  • Automatic generation of configuration files or localization resources

The key strengths of T4 templates include:

  • Integration with Visual Studio: They offer a seamless development experience, with Visual Studio providing direct support for creating, editing, and executing T4 templates.
  • Flexibility: T4 templates can generate any text-based file, making them extremely versatile.
  • Customizability: Developers have full control over the generation process, allowing for highly customized outputs based on complex logic.

Challenges and Limitations

Despite their strengths, T4 templates come with their own set of challenges:

  • Learning Curve: The syntax and setup for T4 can be daunting for newcomers.
  • Debugging Difficulty: Debugging T4 templates can be cumbersome, as it often requires attaching a debugger to the Visual Studio process.
  • Performance: For large projects, T4 template execution can be slow, impacting overall development time.

The Role of T4 Templates Today

I still use T4 templates in some projects today, and they have stood the test of time, proving their worth by automating tedious coding tasks and ensuring consistency across large codebases. However, with the introduction of source generators, developers are beginning to question the future role of T4 templates in .NET development.

Source Generators in .NET

With the release of C# 9.0 and .NET 5, the .NET ecosystem was introduced to source generators, a compelling new feature designed to enhance the developer’s toolkit for code generation. Unlike T4 templates that operate outside the compilation process, source generators are intricately woven into the compilation pipeline itself. This integration allows for the dynamic generation of C# source code during compilation, effectively bridging the gap between the initial code written by developers and the final assembly produced by the compiler.

How Source Generators Work

Source generators work by analyzing a program’s structure, including its syntax trees and semantic model, and then generating additional source files as part of the compilation process. This means the generated code is available to all subsequent phases of compilation, allowing for a seamless development experience. Developers implement source generators by creating a .NET Standard library that references Microsoft.CodeAnalysis.CSharp and Microsoft.CodeAnalysis, then implementing the ISourceGenerator interface.

Example

Just like in the T4 example above, we want to generate DTO’s based on existing entities. In this example, any class that implements IModelEntity will have a corresponding DTO class generated for it at compile time. The generated DTOs will be placed in the *.DTOs namespace and will include properties that mirror those of the original model classes.

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;
using System;
using System.Linq;
using System.Text;

[Generator]
public class DtoGenerator : ISourceGenerator
{
    public void Initialize(GeneratorInitializationContext context)
    {
        // No initialization actions required for this example
    }

    public void Execute(GeneratorExecutionContext context)
    {
        // Iterate over all syntax trees in the compilation
        foreach (var syntaxTree in context.Compilation.SyntaxTrees)
        {
            var semanticModel = context.Compilation.GetSemanticModel(syntaxTree);
            var classes = syntaxTree.GetRoot().DescendantNodes().OfType<ClassDeclarationSyntax>();

            foreach (var classDeclaration in classes)
            {
                var classSymbol = semanticModel.GetDeclaredSymbol(classDeclaration);
                if (classSymbol.Interfaces.Any(i => i.ToDisplayString() == "IModelEntity"))
                {
                    // Generate the DTO class source
                    var dtoSource = GenerateDtoClassSource(classSymbol);
                    context.AddSource($"{classSymbol.Name}Dto.g.cs", SourceText.From(dtoSource, Encoding.UTF8));
                }
            }
        }
    }

    private string GenerateDtoClassSource(INamedTypeSymbol classSymbol)
    {
        var sb = new StringBuilder();
        sb.AppendLine("using System;");
        sb.AppendLine($"namespace {classSymbol.ContainingNamespace.ToDisplayString()}.DTOs");
        sb.AppendLine("{");
        sb.AppendLine($"    public class {classSymbol.Name}Dto");
        sb.AppendLine("    {");

        foreach (var member in classSymbol.GetMembers().OfType<IPropertySymbol>())
        {
            sb.AppendLine($"        public {member.Type} {member.Name} ");
        }

        sb.AppendLine("    }");
        sb.AppendLine("}");

        return sb.ToString();
    }
}

This is 100% c# code and requires no other syntax compared to the T4 example. in other words, any C# developer can hop right in and start using Source Generators.

Applications and Advantages

Source generators are particularly powerful in scenarios such as:

  • Automating Boilerplate Code Generation: Similar to T4, but integrated into the compile-time, improving build performance and developer workflow.
  • Performance Optimizations: Generating code that is tailored for performance-critical sections, such as serialization and deserialization routines.
  • Meta-programming: Allowing for advanced scenarios where code can be generated based on attributes, external files, or other sources of metadata.

The advantages of source generators include:

  • Compile-Time Integration: Since source generators are part of the compilation process, they can produce code that is immediately available to other parts of the application without additional steps.
  • Performance: They do not significantly impact the overall build time and, in some cases, can reduce runtime overhead by generating optimized code.
  • Debugging and Tooling Support: Being a newer technology, source generators benefit from the latest IDE features, including debugging support, making them easier to work with compared to T4 templates.

Considerations and Best Practices

While source generators offer a powerful addition to the .NET developer’s arsenal, they come with their own considerations:

  • Complexity for Advanced Scenarios: Writing source generators that handle complex scenarios can be challenging and requires a good understanding of Roslyn APIs.
  • Incremental Generation: To optimize performance, developers should aim to make their generators incremental, only re-generating code when necessary.

The Future of Code Generation with Source Generators

As source generators continue to mature, they promise to redefine the landscape of code generation in .NET. By offering a tightly integrated, performant, and versatile solution, they may indeed pose a significant shift away from traditional methods like T4 templates, catering to modern development practices and performance needs.

Choosing between T4 and Source Generators

The decision to use T4 templates or source generators does not have to be binary. Each tool serves its purpose and excels under different circumstances:

  • T4 Templates may still be the tool of choice for projects that require generating non-C# files or that heavily rely on the specific templating capabilities and customizations that T4 provides. They are also beneficial in scenarios where developers are already familiar with T4 and the project’s existing infrastructure is built around it.
  • Source Generators are particularly well-suited for projects that benefit from performance optimizations at compile time, require generation of C# code based on the project’s codebase itself, and aim to leverage the latest .NET features with minimal impact on the build process.

Ultimately, the choice depends on the specific needs of the project, the team’s familiarity with the technologies, and the desired outcomes in terms of development efficiency and application performance.

Looking Ahead

As the .NET ecosystem continues to evolve, so too will the tools and methodologies for code generation. You should stay informed about the latest updates from Microsoft and the broader .NET community, as enhancements to both T4 templates and source generators are likely to expand their capabilities and use cases.

Additional resources

Documentation:

T4 Tools:

Also check out:


<
Previous Post
Comparing .NET Dependency Injection Libraries: Bindicate, Ninject, Autofac, and Scrutor
>
Blog Archive
Archive of all previous blog posts