Code generation/scaffolding with Visual Studio T4 templates

Code generation/scaffolding with Visual Studio T4 templates

15 februari 2017  |  Developer

T4 templates are a feature of Visual Studio that is for some reason is quite hidden. Could we harness it's power to do some simple code generation? For example, given a model, generate code from a data class (DAO) and interface, a business logic class and interface and a test class? That would be a real time saver. Formally, this is not code generation, but scaffolding. Code generation, apparently would mean you only change the template, but never the generated code. That is not what we want. We want to step over the tedious repeating tasks of creating classes. The template should generate some code we can change and extend. Strangely, T4 templates (and yes, it is a Terminator reference) seem to be exclusively targeted for the code generation case. First, install the tangible T4 editor extension, so have some IntelliSense in the template (which, weirdly is not the case by default). Second, install the T4 toolbox. This toolbox allows us to output multiple files from a single template. The template consists of two parts: first, the commands to output the files, second the actual template code. The header looks like this:

<#@ template language="c#" hostspecific="true" debug="true" #>
<#@ output extension="cs" #>
<#@ assembly name="T4Toolbox.10.0"#>
<#@ include file="t4toolbox.tt" #>

Suppose our model is named 'User'. The first example part is this:

<#
/// 
/// Template for generating an new Dao + interface (IDao)
/// Just change the modelName here and the next
/// And the idColumn info if necessary

	string modelName = "User";

 	string idaoName = "I" + modelName + "Dao";
	string daoName = modelName + "Dao";
	
	new IDaoTemplate().RenderToFile(idaoName + ".cs");
    new DaoTemplate().RenderToFile(daoName + ".cs");
	
 	string iserviceName = "I" + modelName + "Service";
	string serviceName = modelName + "Service";
    new IServiceTemplate().RenderToFile(iserviceName + ".cs");
    new ServiceTemplate().RenderToFile(serviceName + ".cs");

	string daoTestName = modelName + "DaoTest";
	new DaoTestTemplate().RenderToFile(daoTestName + ".cs");
#>

Unfornately, passing arguments to the templates is not possible. So we'll have to include model name 'User' in the second part as well. The second example part looks like this (your namespaces, testing framework [mstest] and ORM [nhibernate] will probably be different):

<#+
	static string modelName = "User";
	static DbColumn idColumn = new DbColumn() { Name = "Id", DataType = "int", IsNullable = true, PropertyName = "Id", PropertyDataType = "int"};
	
	// Dao
 	static string idaoName = "I" + modelName + "Dao";
	static string daoName = modelName + "Dao";
	static string iDaoNamespace = "MyProject.Data.IDao";
	static string daoNamespace = "MyProject.Data.NHibernate.Dao";
	static string modelNamespace = "MyProject.Data.Models";

	// Service
 	static string iserviceName = "I" + modelName + "Service";
	static string serviceName = modelName + "Service";
	static string iServiceNamespace = "MyProject.Business.IServices";
	static string serviceNamespace = "MyProject.Business.Services";
	static string daoPropertyName = modelName[0].ToString().ToLower() + modelName.Substring(1, modelName.Length - 1) + "Dao";

	// Test
    static string daoTestName = modelName + "DaoTest";
	static string daoTestNamespace = "MyProjectTest.Tests.Data.NHibernate";	
	
    public class DbColumn {
        public string Name { get; set; }
        public string DataType { get; set; }
        public bool IsNullable { get; set; }

        public string PropertyName { get; set; }
        public string PropertyDataType { get; set; }
    }
	

	public class IDaoTemplate: T4Toolbox.Template
    {
		
        public override string TransformText()
        {		
			#>
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using <#= modelNamespace #>;

namespace <#= iDaoNamespace #> {
	public interface <#= idaoName#> {
		IQueryable<<#= modelName #>> FindAll();
		<#= modelName #> FindById(<#= idColumn.PropertyDataType#> <#= idColumn.PropertyName #>);
		<#= modelName #> Persist(<#= modelName #> obj); 
		void Remove(<#= modelName #> obj);
	}
}
						
			<#+
			return this.GenerationEnvironment.ToString();
		}
    }
	
    public class DaoTemplate: T4Toolbox.Template
    {
		
        public override string TransformText()
        {		
			#>
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using MyProject.Data.NHibernate.Dao;
using NHibernate.Criterion;
using <#=iDaoNamespace #>;
using <#=modelNamespace #>;

namespace <#=daoNamespace #> {
    public class <#=daoName #> : BaseDao, <#=idaoName#> {

        public <#=daoName #>(IUnitOfWorkFactory factory)
            : base(factory) { 
        }

        public IQueryable<<#=modelName #>> FindAll() {
            IQueryable<<#=modelName #>> resultList = null;
            DoRead(session => {
                resultList = session.CreateCriteria<<#=modelName #>>()
                                .SetCacheable(true)
                                .List<<#=modelName #>>()
                                .AsQueryable();

            });
            return resultList;
        }

        public <#=modelName #> FindById(<#=idColumn.PropertyDataType #> <#=idColumn.PropertyName #>) {
            <#=modelName #> result = null;
            DoRead(session => {
                result = session.CreateCriteria<<#=modelName #>>()
                                .Add(Restrictions.IdEq(<#=idColumn.PropertyName #>))
                                .SetCacheable(true)
                                .UniqueResult<<#=modelName #>>();

            });
            return result;
        }

        public <#=modelName #> Persist(<#=modelName #> obj) {
            DoTransaction(session => {
                session.SaveOrUpdate(obj);
            });
            return obj;
        }

        public void Remove(<#=modelName #> obj) {
            DoTransaction(session => {
                session.Delete(obj);
            });
        }

    }
}		 
			<#+
			return this.GenerationEnvironment.ToString();
		}
    }

	public class IServiceTemplate: T4Toolbox.Template
    {
		
        public override string TransformText()
        {		
			#>
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using <#= modelNamespace #>;

namespace <#= iServiceNamespace #> {
    public interface <#= iserviceName #> {
        IQueryable<<#= modelName #>> FindAll();
        <#= modelName #> FindById(<#= idColumn.PropertyDataType #> <#= idColumn.PropertyName #>);
        <#= modelName #> Persist(<#= modelName #> obj); 
        void Remove(<#= modelName #> obj);
    }
}			
			<#+
			return this.GenerationEnvironment.ToString();
		}
    }
	
    public class ServiceTemplate: T4Toolbox.Template
    {
		
        public override string TransformText()
        {		
			#>
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using <#= iServiceNamespace #>;
using <#= iDaoNamespace #>;
using <#= modelNamespace #>;

namespace <#= serviceNamespace #> {
    public class <#= serviceName #> : <#= iserviceName #> {

        protected <#= idaoName #> <#= daoPropertyName #>;

        public <#= serviceName #>(<#= idaoName #> arg_<#= daoPropertyName #>) {
            <#= daoPropertyName #> = arg_<#= daoPropertyName #>;
        }

        public IQueryable<<#= modelName #>> FindAll() {
            return <#= daoPropertyName#>.FindAll();
        }

        public <#= modelName #> FindById(<#= idColumn.PropertyDataType #> <#= idColumn.PropertyName #>) {
            return <#= daoPropertyName #>.FindById(<#= idColumn.PropertyName #>);
        }

        public <#= modelName #> Persist(<#= modelName #> obj) {
            return <#= daoPropertyName #>.Persist(obj);
        }

        public void Remove(<#= modelName #> obj) {
            <#= daoPropertyName #>.Remove(obj);
        }

    }
}			<#+
			return this.GenerationEnvironment.ToString();
		}
    }
	
	public class DaoTestTemplate: T4Toolbox.Template
    {
		
        public override string TransformText()
        {		
			#>
using System;
using System.Text;
using System.Collections.Generic;
using System.Linq;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using <#= daoNamespace #>;
using <#= modelNamespace #>;

namespace <#= daoTestNamespace #> {

    /// <summary>
    /// Summary description for <#= daoTestName #>
    /// </summary>
    [TestClass]
    public class <#= daoTestName #> : BaseDaoTest<<#= daoName #>> {
        private TestContext testContextInstance;

        /// <summary>
        ///Gets or sets the test context which provides
        ///information about and functionality for the current test run.
        ///</summary>
        public TestContext TestContext {
            get {
                return testContextInstance;
            }
            set {
                testContextInstance = value;
            }
        }

        public <#= daoTestName #>() {
        }


        [TestMethod]
        public void FindAll() {
            var dao = GetDao();
            IQueryable<<#= modelName #>> list = dao.FindAll();
            Assert.IsNotNull(list);
            Assert.IsTrue(list.Count() > 0);
        }

        [TestMethod]
        public void FindId() {
            var dao = GetDao();
            <#= idColumn.PropertyDataType #> <#= idColumn.PropertyName #> = 1;
            <#= modelName #> obj = dao.FindById(<#= idColumn.PropertyName #>);
            Assert.IsNotNull(obj);
            Assert.AreEqual(<#= idColumn.PropertyName #>, obj.<#= idColumn.PropertyName #>);
        }
    }
}						
			<#+
			return this.GenerationEnvironment.ToString();
		}
    }
	

#>

Note: in the properties of the generated files, set the compile action to none, so you won't get compilation errors. You cannot drag and drop the generated files in Visual Studio, for some reason. So you'll have to do this via Windows Explorer. If you want to generate for another model, just change the two appearance of 'User' to the other model's name. Concluding, it is possible, but not ideal to scaffold code from Visual Studio using T4 templates. We hope Microsoft can invest in improving the T4 experience, so Visual Studio will be more of a time saver.