Learning to generate classes.

Serhii Korol - Sep 18 - - Dev Community

Hi folks, In this article, I want to show how to generate classes. Perhaps you face necessity when you have similar classes doing similar actions. For instance, you need to get data from a repository for different entities and often create data services. I'll show you how to generate all these classes from one file.

Preparing

First of all, you need to create an empty console project. When you've done that, let's add data. At the root of the project, you should create an XML file. Add this code:

<?xml version="1.0" encoding="utf-8"?>
<Models>
    <Employee>
        <Id>7a8f7b59-1e8a-4f5b-bb67-2f2d99a1c054</Id>
        <Name>John Doe</Name>
        <Hired>2024-09-17T00:00:00</Hired>
    </Employee>
    <User>
        <Id>f23aaf55-e0da-4225-ad84-c1bccf4acfe6</Id>
        <Name>Jane Doe</Name>
        <Registered>2024-09-17T00:00:00</Registered>
    </User>
</Models>
Enter fullscreen mode Exit fullscreen mode

Next, let's create data models. Actually, you do need to create these models using T4. I created a Models.ttinclude file and put these classes:

namespace ClassGenerator.Templates.Models
{
    public record Employee
    {
        public Guid Id { get; set; }
        public string Name { get; set; }
        public DateTime Hired { get; set; }
    }

    public record User
    {
        public Guid Id { get; set; }
        public string Name { get; set; }
        public DateTime Registered { get; set; }
    }
}
Enter fullscreen mode Exit fullscreen mode

With data, that's all.

Template

And now, let's create a .tt file. I'll be step-by-step to add code and explain what it does. The T4 format has its own syntax. All elements are wrapped to tags <# #>. I added it to the top of the document dependencies. It's similar to C#, but it has some settings. You can change the generated file name or other settings. Also, you can add external files.

<#@ template debug="false" hostspecific="true" #>
<#@ output extension="cs" #>
<#@ include file="Models.ttinclude" #>
<#@ assembly name="System.Core" #>
<#@ import namespace="System" #>
<#@ import namespace="System.Linq" #>
<#@ assembly name="System.Xml"#>
<#@ import namespace="System.Xml" #>
Enter fullscreen mode Exit fullscreen mode

Next, let's add the following block of code:

<#
    XmlDocument doc = new XmlDocument();
    doc.Load("../Data.xml");
    XmlNodeList data = doc.DocumentElement?.ChildNodes;
    if (data != null)
    {
        var nodes = data.Cast<XmlNode>().ToList();

        foreach (var node in nodes)
        {
            var modelSource = "Models.";
            var typeName = node.Name;

            var id = node["Id"]?.InnerText;
            var name = node["Name"]?.InnerText;
            var hired = node["Hired"];
            var registered = node["Registered"];

#>

Enter fullscreen mode Exit fullscreen mode

I grabbed the XML file and got values for each node iterating in the foreach block. The next step is adding the data repository class.
There, I used interpolation tags <#= #> for replacements. As you could notice, the IF blocks wrapped <# #> tags. It's necessary to close curly and wrap tags. For the interpolation, it used a similar as in C# strings. The last part of the code is creating data service:

public class DataService<T> where T : class, new()
    {
        private readonly DataRepository<<#= modelSource #><#= typeName #>> _repository;

        public DataService(DataRepository<<#= modelSource #><#= typeName #>> repository)
        {
            _repository = repository;
        }

        public <#= modelSource #><#= typeName #> Get()
        {
            return _repository.Get();
        }
    }
}

<#
        }
    }
#>
Enter fullscreen mode Exit fullscreen mode

This service to calls the repository service, which gets data and returns it. The final template should look like:

<#@ template debug="false" hostspecific="true" #>
<#@ output extension="cs" #>
<#@ include file="Models.ttinclude" #>
<#@ assembly name="System.Core" #>
<#@ import namespace="System" #>
<#@ import namespace="System.Linq" #>
<#@ assembly name="System.Xml"#>
<#@ import namespace="System.Xml" #>

<#
    XmlDocument doc = new XmlDocument();
    doc.Load("../Data.xml");
    XmlNodeList data = doc.DocumentElement?.ChildNodes;
    if (data != null)
    {
        var nodes = data.Cast<XmlNode>().ToList();

        foreach (var node in nodes)
        {
            var modelSource = "Models.";
            var typeName = node.Name;

            var id = node["Id"]?.InnerText;
            var name = node["Name"]?.InnerText;
            var hired = node["Hired"];
            var registered = node["Registered"];

#>


namespace ClassGenerator.Templates.<#= typeName #>
{
    public class DataRepository<T> where T : class, new()
    {
        public <#= modelSource #><#= typeName #> Get()
        {
            return new <#= modelSource #><#= typeName #>()
            {
                Id = Guid.Parse("<#= id #>"),
                Name = "<#= name #>",
<#
            if (hired != null)
            {
#>
                Hired = DateTime.Parse("<#= hired.InnerText #>"),
<#
            }
            if (registered != null)
            {
#>
                Registered = DateTime.Parse("<#= registered.InnerText #>")
<#
            }
#>
            };
        }
    }

    public class DataService<T> where T : class, new()
    {
        private readonly DataRepository<<#= modelSource #><#= typeName #>> _repository;

        public DataService(DataRepository<<#= modelSource #><#= typeName #>> repository)
        {
            _repository = repository;
        }

        public <#= modelSource #><#= typeName #> Get()
        {
            return _repository.Get();
        }
    }
}

<#
        }
    }
#>
Enter fullscreen mode Exit fullscreen mode

If you run this template, you will see the generated file where it has a repository, service, and models.
For the final action, you need to add code to the startup file:

internal static class Program
{
    private static void Main()
    {
        var employeeRepository = new Templates.Employee.DataRepository<Employee>();
        var userRepository = new Templates.User.DataRepository<User>();

        var employeeService = new Templates.Employee.DataService<Employee>(employeeRepository);
        var userService = new Templates.User.DataService<User>(userRepository);

        var employee = employeeService.Get();
        var user = userService.Get();


        Console.WriteLine("Employee:");
        Console.WriteLine($"Id: {employee.Id}");
        Console.WriteLine($"Name: {employee.Name}");
        Console.WriteLine($"Hired: {employee.Hired}");

        Console.WriteLine("\nUser:");
        Console.WriteLine($"Id: {user.Id}");
        Console.WriteLine($"Name: {user.Name}");
        Console.WriteLine($"Registered: {user.Registered}");
    }
}
Enter fullscreen mode Exit fullscreen mode

Testing

Check this out. You should get results in the console.

result

Pros and cons

Pros

This approach allows you to centrally automate the creation of an array of services and models. You can also create models from XML or JSON and generate any number of models.

Cons

You need to learn syntax, and writing code can be too complicated. Also, it doesn't allow you to separate into different files for each class. There is an approach to separating, but writing and maintaining code is too hard.

Conclution

I used an alternative approach to creating code. I hope it'll be helpful for you. I'll see you next week.

The Repo is HERE.

Buy Me A Beer

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .