Learning notes of c-orm: T4 introduction and database entity generation

What is T4?

1.1 T4 introduction

T4, the four letter combination with t beginning: Text Template Transformation Toolkit, is the code generation engine officially used by Microsoft in Visual Studio 2008. T4 is a mixed template composed of some text blocks and control logic. In short, T4 can generate the files you want according to the template, such as class files, text files, HTML and so on.

VS provides a code generation execution environment based on T4 engine, which consists of the following assemblies:

    Microsoft.VisualStudio.TextTemplating.10.0.dll

    Microsoft.VisualStudio.TextTemplating.Interfaces.10.0.dll

    Microsoft.VisualStudio.TextTemplating.Modeling.10.0.dll

    Microsoft.VisualStudio.TextTemplating.VSHost.10.0.dll

1.2 type of T4 formwork

There are two types of T4 formwork:

1) runtime template

Run time T4 text templates are executed in the application to generate text strings.

To create a runtime template, add a runtime text template file to your project. In addition, you can add a plain text file and set its custom tools property to TextTemplatingFilePreprocessor.

2) formwork during design

Perform design time T4 text templates in VS to define parts of the application's source code and other resources.

To create a design time template, add a text template file to your project. In addition, you can add a plain text file and set its custom tools property to TextTemplatingFileGenerator.

1.3 plug in installation

VS: the default editing tool is not highlighted or prompted. It is not easy to locate errors. It is recommended to install tangible T4 Editor Plug in to write T4 code.

II. T4 Hello World example

Suppose there is a console application LinkTo.Test.ConsoleT4. Now you want to output the "Hello World" string. The Program code is as follows:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace LinkTo.Test.ConsoleT4
{
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Hello World");
            Console.Read();
        }
    }
}

Now delete the Program.cs file and use T4 template to generate a Program.cs with the same code as above. The operation method is as follows:

1) right click add new item text template to change the name to Program.tt.

2) the code of Program.tt is as follows:

<#@ output extension=".cs" #>
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

<# 
    string ClassName = "Program";
#>
namespace LinkTo.Test.ConsoleT4
{
    class <#=ClassName #>
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Hello World");
            Console.Read();
        }
    }
}

3) Click Save to see a Program.cs file generated under Program.tt. The code is the same as the original Hello World.

III. T4 Hello World example extension

Now extend the Hello World example to add two classes to the program:

1) Hello class, output "hello".

2) World class, output "World".

The code is as follows:

<#@ 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" #>
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
<# 
    string ClassName = "Program";
#>
<# 
    List<string> classNames = new List<string>() {"Hello","World"};
    List<string> callMethods = new List<string>();
#>

namespace LinkTo.Test.ConsoleT4
{
<#    
foreach (string className in classNames)
{
    callMethods.Add($"{className}.Show();");
#>
    class <#=className #>
    {
        /// <summary>
        /// <#=className #>class Show()Method
        /// </summary>
        public static void Show()
        {
            Console.WriteLine("<#=className #>");
        }
    }
    
<#
}
#>
    class <#=ClassName #>
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Hello World");

<#
        foreach (string callMethod in callMethods)
        {
#>
            //<#=callMethod #>Method call
            <#=callMethod #>
<#
        }
#>

            Console.Read();
        }
    }
}
Program.tt

The generated files are as follows:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace LinkTo.Test.ConsoleT4
{
    class Hello
    {
        /// <summary>
        /// Hello class Show()Method
        /// </summary>
        public static void Show()
        {
            Console.WriteLine("Hello");
        }
    }
    
    class World
    {
        /// <summary>
        /// World class Show()Method
        /// </summary>
        public static void Show()
        {
            Console.WriteLine("World");
        }
    }
    
    class Program
    {
        static void Main(string[] args)
        {
            Console.WriteLine("Hello World");

            //Hello.Show();Method call
            Hello.Show();
            //World.Show();Method call
            World.Show();

            Console.Read();
        }
    }
}
Program.cs

IV. basic structure of T4 formwork

Code block can be divided into two types: text and program script.

4.1 text: the text to be generated

4.2 program script: internal execution to generate the desired text. The parts in T4 are program script contents.

For ease of understanding, use "block" to subdivide the syntax. Block is the basic unit of T4 template, which can be divided into five categories: Directive Block, Text Block, Statement Block, Expression Block and Class Feature Block.

4.2.1. Directive Block

As with the instructions for ASP.NET pages, they appear in the header of the file, through the < × @ #>Represents. Where < template #>Instructions are required to define the basic attributes of a template, such as programming language, cu lt ure based, and support for modes.

Directives are usually the first element in a template file or contained file. They should not be placed inside a code block or after a class function block.

T4 template instruction

    <#@ template [language="C#"] [hostspecific="true"] [debug="true"] [inherits="templateBaseClass"] [culture="code"] [compilerOptions="options"] #>

T4 parameter instruction

    <#@ parameter type="Full.TypeName" name="ParameterName" #>

T4 output command

    <#@ output extension=".fileNameExtension" [encoding="encoding"] #>

T4 assembly instruction

    <#@ assembly name="[assembly strong name|assembly file name]" #>

$(SolutionDir): the solution directory where the current project is located

$(ProjectDir): directory where the current project is located

$(TargetPath): absolute path of the current project's compiled output file

$(TargetDir): directory of compilation output of current project

T4 import instruction

    <#@ import namespace="namespace" #>

T4 contains instructions

    <#@ include file="filePath" #>

4.2.2 Text Block

The text block is the static text output directly as is, without any label.

4.2.3 Statement Block

Code statement block is represented in the form of statement, and in the middle is a program call written in the corresponding programming language. We can quickly control the text conversion process through code statement.

4.2.4 Expression Block

The expression block is represented in the form of < ා = expression ා, through which the string expression can be dynamically parsed and embedded in the output text.

4.2.5 Class Feature Block

If text conversion requires some complex logic, the code may need to be written in a separate auxiliary method, or even define some separate classes. The representation of class attribute block is < ා + featurecode ා.

V. T4 template generates database entity class

5.1. Add a T4Code folder, and create two new text templates below. Note that the default. tt extension is not used here, but. ttinclude. Use them as include files.

<#@ assembly name="System.Core" #>
<#@ assembly name="System.Data" #>
<#@ assembly name="$(ProjectDir)\Lib\MySql.Data.Dll" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Text" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ import namespace="System.Data"#>
<#@ import namespace="System.Data.SqlClient"#>
<#@ import namespace="MySql.Data.MySqlClient"#>
<#+
    #region T4Code
    /// <summary>
    /// Database schema interface
    /// </summary>
    public interface IDBSchema : IDisposable
    {
        List<string> GetTableList();
        Table GetTableMetadata(string tableName);
    }

    /// <summary>
    /// Database schema factory
    /// </summary>
    public class DBSchemaFactory
    {
        static readonly string DatabaseType = "SqlServer";
        public static IDBSchema GetDBSchema()
        {
            IDBSchema dbSchema;
            switch (DatabaseType) 
            {
                case "SqlServer":
                    {
                        dbSchema =new SqlServerSchema();
                        break;
                    }
                case "MySql":
                    {
                        dbSchema = new MySqlSchema();
                        break;
                    }
                default: 
                    {
                        throw new ArgumentException("The input argument of DatabaseType is invalid.");
                    }
            }
            return dbSchema;
        }
    }

    /// <summary>
    /// SqlServer
    /// </summary>
    public class SqlServerSchema : IDBSchema
    {
        public string ConnectionString = "Server=.;Database=CFDEV;Uid=sa;Pwd=********;";
        public SqlConnection conn;

        public SqlServerSchema()
        {
            conn = new SqlConnection(ConnectionString);
            conn.Open();
        }

        public List<string> GetTableList()
        {
            DataTable dt = conn.GetSchema("Tables");
            List<string> list = new List<string>();
            foreach (DataRow row in dt.Rows)
            {
                list.Add(row["TABLE_NAME"].ToString());
            }
            return list;
        }
        
        public Table GetTableMetadata(string tableName)
        {
            string commandText = string.Format("SELECT * FROM {0}", tableName); ;
            SqlCommand cmd = new SqlCommand(commandText, conn);
            SqlDataAdapter da = new SqlDataAdapter(cmd);
            DataSet ds = new DataSet();
            da.FillSchema(ds, SchemaType.Mapped, tableName);
            Table table = new Table(ds.Tables[0]);
            return table;
        }

        public void Dispose()
        {
            if (conn != null)
            {
                conn.Close();
            }
        }
    }

    /// <summary>
    /// MySql
    /// </summary>
    public class MySqlSchema : IDBSchema
    {
        public string ConnectionString = "Server=localhost;Port=3306;Database=ProjectData;Uid=root;Pwd=;";
        public MySqlConnection conn;

        public MySqlSchema()
        {
            conn = new MySqlConnection(ConnectionString);
            conn.Open();
        }

        public List<string> GetTableList()
        {
            DataTable dt = conn.GetSchema("Tables");
            List<string> list = new List<string>();
            foreach (DataRow row in dt.Rows)
            {
                list.Add(row["TABLE_NAME"].ToString());
            }
            return list;
        }

        public Table GetTableMetadata(string tableName)
        {
            string commandText = string.Format("SELECT * FROM {0}", tableName); ;
            MySqlCommand cmd = new MySqlCommand(commandText, conn);
            MySqlDataAdapter da = new MySqlDataAdapter(cmd);
            DataSet ds = new DataSet();
            da.FillSchema(ds, SchemaType.Mapped, tableName);
            Table table = new Table(ds.Tables[0]);
            return table;
        }

        public void Dispose()
        {
            if (conn != null)
            {
                conn.Close();
            }
        }
    }

    /// <summary>
    /// data sheet
    /// </summary>
    public class Table
    {
        public List<Column> PKs;
        public List<Column> Columns;
        public string DataTypes;

        public Table(DataTable dt)
        {
            PKs = GetPKList(dt);
            Columns = GetColumnList(dt);
            DataTypes = GetDataTypeList(Symbol.Normal);
        }

        public List<Column> GetPKList(DataTable dt)
        {
            List<Column> list = new List<Column>();
            Column column = null;
            if (dt.PrimaryKey.Length > 0)
            {
                list = new List<Column>();
                foreach (DataColumn dc in dt.PrimaryKey)
                {
                    column = new Column(dc);
                    list.Add(column);
                }
            }
            return list;
        }

        private List<Column> GetColumnList(DataTable dt)
        {
            List<Column> list = new List<Column>();
            Column column = null;
            foreach (DataColumn dc in dt.Columns)
            {
                column = new Column(dc);
                list.Add(column);
            }
            return list;
        }

        private string GetDataTypeList(Symbol symbol)
        {
            List<string> list = new List<string>();
            foreach (Column c in Columns)
            {
                if (symbol == Symbol.Normal)
                    list.Add(string.Format("{0} {1}", c.DataType, c.UpperColumnName));
                else if (symbol == Symbol.Underline)
                    list.Add(string.Format("{0} _{1}", c.DataType, c.UpperColumnName));
            }
            return string.Join(",", list.ToArray());
        }
    }

    /// <summary>
    /// Data column
    /// </summary>
    public class Column
    {
        DataColumn columnBase;

        public Column(DataColumn _columnBase)
        {
            columnBase = _columnBase;
        }

        public string ColumnName { get { return columnBase.ColumnName; } }

        public string DataType
        { 
            get 
            {
                string result = string.Empty;
                if (columnBase.DataType.Name == "Guid")//for mysql,Because for MySql If it is CHAR(36),The type is automatically Guid. 
                    result = "string";
                else if (columnBase.DataType.Name == "String")
                    result = "string";
                else if (columnBase.DataType.Name == "Int32")
                    result = "int";
                else
                    result = columnBase.DataType.Name;
                return result; 
            } 
        }

        public string MaxLength { get { return columnBase.MaxLength.ToString(); } }

        public bool AllowDBNull { get { return columnBase.AllowDBNull; } }

        public string UpperColumnName
        {
            get
            {
                return string.Format("{0}{1}", ColumnName[0].ToString().ToUpper(), ColumnName.Substring(1));
            }
        }

        public string LowerColumnName
        {
            get
            {
                return string.Format("{0}{1}", ColumnName[0].ToString().ToLower(), ColumnName.Substring(1));
            }
        }
    }

    /// <summary>
    /// Help class
    /// </summary>
    public class GeneratorHelper
    {
        public static readonly string StringType = "string";
        public static readonly string DateTimeType = "DateTime";
        public static string GetQuestionMarkByType(string typeName)
        {
            string result = typeName;
            if (typeName == DateTimeType)
            {
                result += "?";
            }
            return result;
        }
    }

    /// <summary>
    /// Symbol enumeration
    /// </summary>
    public enum Symbol
    {
        Normal = 1,
        Underline = 2
    }
    #endregion
#>
DBSchema.ttinclude

DBSchema.ttinclude mainly implements the function of database factory. Note: please change the database connection string to your own.

<#@ assembly name="System.Core" #>
<#@ assembly name="System.Data" #>
<#@ assembly name="EnvDTE" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Text" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ import namespace="System.Data"#>
<#@ import namespace="System.IO"#>
<#@ import namespace="Microsoft.VisualStudio.TextTemplating"#>

<#+
// T4 Template Block manager for handling multiple file outputs more easily.
// Copyright (c) Microsoft Corporation.All rights reserved.
// This source code is made available under the terms of the Microsoft Public License (MS-PL)

// Manager class records the various blocks so it can split them up
class Manager
{
    public struct Block
    {
        public string Name;
        public int Start, Length;
    }

    public List<Block> blocks = new List<Block>();
    public Block currentBlock;
    public Block footerBlock = new Block();
    public Block headerBlock = new Block();
    public ITextTemplatingEngineHost host;
    public ManagementStrategy strategy;
    public StringBuilder template;
    public string OutputPath { get; set; }

    public Manager(ITextTemplatingEngineHost host, StringBuilder template, bool commonHeader)
    {
        this.host = host;
        this.template = template;
        OutputPath = string.Empty;
        strategy = ManagementStrategy.Create(host);
    }

    public void StartBlock(string name)
    {
        currentBlock = new Block { Name = name, Start = template.Length };
    }

    public void StartFooter()
    {
        footerBlock.Start = template.Length;
    }

    public void EndFooter()
    {
        footerBlock.Length = template.Length - footerBlock.Start;
    }

    public void StartHeader()
    {
        headerBlock.Start = template.Length;
    }

    public void EndHeader()
    {
        headerBlock.Length = template.Length - headerBlock.Start;
    }    

    public void EndBlock()
    {
        currentBlock.Length = template.Length - currentBlock.Start;
        blocks.Add(currentBlock);
    }

    public void Process(bool split)
    {
        string header = template.ToString(headerBlock.Start, headerBlock.Length);
        string footer = template.ToString(footerBlock.Start, footerBlock.Length);
        blocks.Reverse();
        foreach(Block block in blocks) {
            string fileName = Path.Combine(OutputPath, block.Name);
            if (split) {
                string content = header + template.ToString(block.Start, block.Length) + footer;
                strategy.CreateFile(fileName, content);
                template.Remove(block.Start, block.Length);
            } else {
                strategy.DeleteFile(fileName);
            }
        }
    }
}

class ManagementStrategy
{
    internal static ManagementStrategy Create(ITextTemplatingEngineHost host)
    {
        return (host is IServiceProvider) ? new VSManagementStrategy(host) : new ManagementStrategy(host);
    }

    internal ManagementStrategy(ITextTemplatingEngineHost host) { }

    internal virtual void CreateFile(string fileName, string content)
    {
        File.WriteAllText(fileName, content);
    }

    internal virtual void DeleteFile(string fileName)
    {
        if (File.Exists(fileName))
            File.Delete(fileName);
    }
}

class VSManagementStrategy : ManagementStrategy
{
    private EnvDTE.ProjectItem templateProjectItem;

    internal VSManagementStrategy(ITextTemplatingEngineHost host) : base(host)
    {
        IServiceProvider hostServiceProvider = (IServiceProvider)host;
        if (hostServiceProvider == null)
            throw new ArgumentNullException("Could not obtain hostServiceProvider");

        EnvDTE.DTE dte = (EnvDTE.DTE)hostServiceProvider.GetService(typeof(EnvDTE.DTE));
        if (dte == null)
            throw new ArgumentNullException("Could not obtain DTE from host");

        templateProjectItem = dte.Solution.FindProjectItem(host.TemplateFile);
    }

    internal override void CreateFile(string fileName, string content)
    {
        base.CreateFile(fileName, content);
        ((EventHandler)delegate { templateProjectItem.ProjectItems.AddFromFile(fileName); }).BeginInvoke(null, null, null, null);
    }

    internal override void DeleteFile(string fileName)
    {
        ((EventHandler)delegate { FindAndDeleteFile(fileName); }).BeginInvoke(null, null, null, null);
    }

    private void FindAndDeleteFile(string fileName)
    {
        foreach(EnvDTE.ProjectItem projectItem in templateProjectItem.ProjectItems)
        {
            if (projectItem.get_FileNames(0) == fileName)
            {
                projectItem.Delete();
                return;
            }
        }
    }
}
#>
MultiDocument.ttinclude

Multi document.ttinclude mainly realizes the function of multi document.

5.2 add a multmodelaut.tt text template with the following code:

<#@ template debug="true" hostspecific="true" language="C#" #>
<#@ assembly name="System.Core" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Text" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ output extension=".cs" #>
<#@ include file="T4Code/DBSchema.ttinclude"#>
<#@ include file="T4Code/MultiDocument.ttinclude"#>
<# var manager = new Manager(Host, GenerationEnvironment, true) { OutputPath = Path.GetDirectoryName(Host.TemplateFile)}; #>
<#
    //System.Diagnostics.Debugger.Launch();//debugging
    var dbSchema = DBSchemaFactory.GetDBSchema();
    List<string> tableList = dbSchema.GetTableList();
    foreach(string tableName in tableList)
    {
        manager.StartBlock(tableName+".cs");
        Table table = dbSchema.GetTableMetadata(tableName);
#>
//-------------------------------------------------------------------------------
// This code is generated by T4 Template MultModelAuto Auto generate
// Generation time <#=DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")#>
// Changes to this file can cause incorrect behavior and will be lost if the code is regenerated.
//-------------------------------------------------------------------------------

using System;
using System.Collections.Generic;
using System.Text;

namespace Project.Model
{
    [Serializable]
    public class <#=tableName#>
    {
        #region Constructor
        public <#=tableName#>() { }

        public <#=tableName#>(<#=table.DataTypes#>)
        {
<#
        foreach(Column c in table.Columns)
        {
#>
            this.<#=c.UpperColumnName#> = <#=c.UpperColumnName#>;
<#
        }
#>
        }
        #endregion

        #region Attributes
<#
        foreach(Column c in table.Columns)
        {
#>
        public <#=GeneratorHelper.GetQuestionMarkByType(c.DataType)#> <#=c.UpperColumnName#> {get; set;}
<#
        }
#>
        #endregion

        #region Validator
        public List<string> ErrorList = new List<string>();
        private bool Validator()
        {    
            bool validatorResult = true;
<#
            foreach(Column c in table.Columns)
            {
                if (!c.AllowDBNull)
                {
                    if(c.DataType == GeneratorHelper.StringType)
                    {
#>
            if (string.IsNullOrEmpty(<#=c.UpperColumnName#>))
            {
                validatorResult = false;
                ErrorList.Add("The <#=c.UpperColumnName#> should not be empty.");
            }
<#
                    }

                    if(c.DataType == GeneratorHelper.DateTimeType)
                    {
#>
            if (<#=c.UpperColumnName#> == null)
            {
                validatorResult = false;
                ErrorList.Add("The <#=c.UpperColumnName#> should not be empty.");
            }
<#
                    }
                }

            if (c.DataType == GeneratorHelper.StringType)
            {
#>
            if (<#=c.UpperColumnName#> != null && <#=c.UpperColumnName#>.Length > <#=c.MaxLength#>)
            {
                validatorResult = false;
                ErrorList.Add("The length of <#=c.UpperColumnName#> should not be greater then <#=c.MaxLength#>.");
            }
<#
            }
            }
#>
            return validatorResult;
        }    
        #endregion
    }
}
<#
        manager.EndBlock();
    }
    dbSchema.Dispose();
    manager.Process(true);
#>
MultModelAuto.tt

After the code is saved, you can see that multiple entity class files have been generated according to the data table below this file:

 

Reference from:

    https://www.cnblogs.com/dataadapter/p/3844394.html

    https://www.cnblogs.com/yank/archive/2012/02/14/2342287.html

Attached:

    MSDN T4 documentation

Tags: C# Database MySQL Programming encoding

Posted on Mon, 20 Apr 2020 02:22:18 -0700 by youscript