Unlocking the Power of Code Generation with T4

Unlocking the Power of Code Generation with T4

Why This Tool is Underrated

·

7 min read

T4 Code generation is a Visual Studio tool developers can use to generate C# classes, SQL stored procedures and many other text-based files.

In this article, I will explain how and why you should use T4 with examples!

How does T4 work?

T4 code generation is similar to JSX (React) or Razor (ASP.NET): Code is used to control the content of the generated file with the help of conditional and loop blocks. To be exact, T4 template files (.tt file extension) are composed of three parts:

  • Directives: Usually in the header, directives dictate how the file is generated.

  • Text blocks: are the text pieces that will be inserted into the generated file.

  • Control blocks: these are C# or VB blocks inside the .tt file that helps dynamically output the text blocks.

T4 code generation can occur at design time (generate classes that are used in the code) or runtime (generate text that is used as output at runtime)

T4 can be used in two modes:

Design-time code generation: create files that will be used in the project code like C# classes, HTML views…

Runtime code generation: construct strings that are used in the app at runtime, like persisting data in CSV files, logs and traces…

T4 vs AI Code generation

For personal projects, it is possible to work with open.ai ChatGPT to generate code even though sometimes the output needs refining! But are companies ready to let developers use AI? I don’t think so. Besides, AI is still in its early stages and, models are being trained and released frequently, is the resulting code expected to be consistent if the models evolve?

One other problem with AI code generation is its integration in development environments, ChatGPT-generated code needs to be copied and pasted into the solution and then tested...

Yes, GitHub Copilot and AWS Code Whisperer integrate directly into the popular IDEs but the way they work is limited to generating methods when a comment is written in the code. Code generation with T4 is still a great tool to use because:

  • Developers control how code is generated

  • Code generation is consistent

  • No existing code is shared with AI to build upon

  • Set it and forget it

T4 example

Design-time code generation

Take as an example the Student C# class below:

public class Student
{
    public string Name { get; set; }
    public int Age { get; set; }
    public string Major { get; set; }

    public Student(string name, int age, string major)
    {
        Name = name;
        Age = age;
        Major = major;
    }

    public Student Clone()
    {
        return new Student(Name, Age, Major);
    }

    public override bool Equals(object obj)
    {
        if (obj == null || GetType() != obj.GetType())
        {
            return false;
        }

        Student other = (Student)obj;
        if (Name != other.Name)
        {
            return false;
        }
        if (Age != other.Age)
        {
            return false;
        }
        if (Major != other.Major)
        {
            return false;
        }
        return true;
    }

    public override int GetHashCode()
    {
        return HashCode.Combine(Name, Age, Major);
    }

    public override string ToString()
    {
        return $"Student {{ Name: {Name}, Age: {Age}, Major: {Major} }}";
    }
}

Adding the student email to this model requires 5 or 6 subsequent changes (not counting the other classes that are using this class). Doing this manually is not a big deal, but leaving this work to developers might involve a learning curve for some and also adds human error to, otherwise, a simple task.

To make up for the lost productivity and peace of mind, considering T4 code generation is a solid solution. Here is what the T4 template would look like:

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

// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
// Code generated from a T4 template
// ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~

<#
string className = "Student";
    Dictionary<string, string> propertyNames = new Dictionary<string, string>()
    {
        { "Name", "string" },
        { "Age", "int" },
        { "Major", "string" }
    };
#>

public class <#= className #>
{
<#
    foreach (KeyValuePair<string, string> property in propertyNames)
    {
#>
    public <#= property.Value #> <#= property.Key #> { get; set; }
<#
    }
#>

    public <#= className #>(<#= string.Join(", ", propertyNames.Select(p => $"{p.Value} {p.Key.ToLower()}")) #>)
    {
<#
    foreach (KeyValuePair<string, string> property in propertyNames)
    {
#>
        <#= property.Key #> = <#= property.Key.ToLower() #>;
<#
    }
#>
    }

    public <#= className #> Clone()
    {
        return new <#= className #>(<#= string.Join(", ", propertyNames.Select(p => p.Key)) #>);
    }

    public override bool Equals(object obj)
    {
        if (obj == null || GetType() != obj.GetType())
        {
            return false;
        }

        <#= className #> other = (<#= className #>)obj;
<#
    foreach (KeyValuePair<string, string> property in propertyNames)
    {
#>
        if (<#= property.Key #> != other.<#= property.Key #>)
        {
            return false;
        }
<#
    }
#>
        return true;
    }

    public override int GetHashCode()
    {
        return HashCode.Combine(<#= string.Join(", ", propertyNames.Select(p => p.Key)) #>);
    }

    public override string ToString()
    {
        return $"<#= className #> {{ <#= string.Join(", ", propertyNames.Select(p => $"{p.Key}: {{{p.Key}}}")) #> }}";
    }
}

The code between <# and #> is considered as the template logic and is C# code (the control blocks). However, the first 7 lines are the directives: the template code itself is C# and we want the generate a .cs file, we will also use System.Collections.Generics and System.Linq in the template logic.

Outside <# and #> is the text to generate which will compose our C# Student class.

Whenever the code depends on the class properties, a foreach loop is used, the code generation is dynamic and adding a new property to the Student model can be done by just adding an element to the PropertyNames dictionary. This variable also referred to as “source”, is local in the example above. It is recommended to set up a global source using a global object, config files, database tables, etc to be reused for other templates ( for HTML forms, SQL queries)

The following template can be used to generate an HTML edit form for the student entity:

<#@ template debug="false" hostspecific="false" language="C#" #>
<#@ assembly name="System.Core" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Text" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ output extension=".html" #>
<#
    string className = "Student";
    Dictionary<string, string> propertyNames = new Dictionary<string, string>()
    {
        { "Name", "string" },
        { "Age", "int" },
        { "Major", "string" }
    };
#>
<!DOCTYPE html>
<html>
<head>
    <title>Edit <#= className #></title>
</head>
<body>
    <h1>Edit <#= className #></h1>
    <form action="#" method="post">
<#
    foreach (KeyValuePair<string, string> property in propertyNames)
    {
        string inputType = string.Empty;
        switch (property.Value)
        {
            case "string":
                inputType = "text";
                break;
            case "int": 
                inputType = "number";
                break;
            case "bool":
                inputType = "checkbox";
                break;
            case "DateTime":
                inputType = "date";
                break;
        };
#>
        <div>
            <label for="<#= property.Key #>"><#= property.Key #>:</label>
            <input type="<#= inputType #>" id="<#= property.Key #>" name="<#= property.Key #>" />
        </div>
<#
    }
#>
        <button type="submit">Save</button>
    </form>
</body>
</html>

In this template, a switch block is used to transform the property type in C# to an HTML input type (string to text, int to number, bool to checkbox)

The output of this template is:

<!DOCTYPE html>
<html>
<head>
    <title>Edit Student</title>
</head>
<body>
    <h1>Edit Student</h1>
    <form action="#" method="post">
        <div>
            <label for="Name">Name:</label>
            <input type="text" id="Name" name="Name" />
        </div>
        <div>
            <label for="Age">Age:</label>
            <input type="number" id="Age" name="Age" />
        </div>
        <div>
            <label for="Major">Major:</label>
            <input type="text" id="Major" name="Major" />
        </div>
        <button type="submit">Save</button>
    </form>
</body>
</html>

T4 Runtime code generation

The purpose of this article is briefly present the T4 code generation tool and its benefits. To continue reading, please refer to Microsoft Learn Code Generation and T4 Text Templates.

T4 code generation is only supported on Visual Studio. I will be working on a way to add T4 code generation support to VS Code, feel free to join my newsletter to stay up to date.

T4 code generation in Visual Studio

Transforming templates in Visual Studio can be done in the Build Menu >> Transform all T4 templates.

T4 control blocks are written with C# or VB, it is possible to debug the code generation in Visual Studio by Right Clicking the .tt in the Solution Exploring, then Debug T4 Template.

T4 Tips

  1. Design-Time generated files need to be excluded from source control. Developers should generate code from templates locally (prevent manual changes).

  2. Design-Time code generation can be done manually as mentioned above or triggered by builds or template changes.

  3. Runtime code generation templates can take parameters.

Closing thoughts

As a rule of thumb, if a simple task like adding a property to a class or expanding a service contract leads to multiple subsequent trivial changes, T4 code generation needs to be configured and will be beneficial for the development team.

Thanks for reading the whole article! If this article helped you learn a new concept, feel free to subscribe to my newsletter and never miss similar content.

Did you find this article valuable?

Support Sami Mejri by becoming a sponsor. Any amount is appreciated!