Asp .Net Core 系列:基于 T4 模板生成代码

目录

简介

T4模板,即Text Template Transformation Toolkit,是微软官方在Visual Studio中引入的一种代码生成引擎。自Visual Studio 2008开始,T4模板就被广泛应用于生成各种类型的文本文件,包括网页、资源文件以及各种编程语言的源代码等。

T4模板是一种由文本块和控制逻辑组成的混合模板,它可以根据预设的规则和输入数据生成目标文本文件。

组成部分

T4模板主要由以下几部分组成:

  1. 指令块 :向文本模板化引擎提供关于如何生成转换代码和输出文件的一般指令。常见的指令包括<#@ template #><#@ parameter #><#@ assembly #><#@ import #><#@ include #><#@ output #>等。

    • 模板指令<#@ template #>):定义模板的基本属性,如使用的编程语言、是否开启调试模式等。
    • 参数指令<#@ parameter #>):声明模板代码中从外部上下文传入的值初始化的属性。
    • 程序集指令<#@ assembly #>):引用外部程序集,以便在模板中使用其中的类型和方法。
    • 导入指令<#@ import #>):允许在模板中引用其他命名空间中的类型,类似于C#中的using指令或Visual Basic中的Imports指令。
    • 包含指令<#@ include #>):在模板中包含另一个文件的内容,通常用于共享常用的代码片段或模板设置。
    • 输出指令<#@ output #>):定义输出文件的扩展名和编码方式。
  2. 文本块:直接复制到输出文件的内容,不会进行任何处理或转换。

  3. 代码语句块(Statement Block)

    代码语句块通过<#Statement#>的形式表示,中间是一段通过相应编程语言编写的程序调用,我们可以通过代码语句快控制文本转化的流程。在上面的代码中,我们通过代码语句块实现对一个数组进行遍历,输出重复的Console.WriteLine("Hello {0},Welcome to T4 World!","<#= p.Name #>");语句。

  4. 表达式块(Expression Block)

    表达式块以<#=Expression#>的形式表示,通过它之际上动态的解析的字符串表达内嵌到输出的文本中。比如在上面的foreach循环中,每次迭代输出的人名就是通过表达式块的形式定义的(<#= p.Name #>

  5. 类特性块(Class Feature Block)

    如果文本转化需要一些比较复杂的逻辑,我们需要写在一个单独的辅助方法中,甚至是定义一些单独的类,我们就是将它们定义在类特性块中。类特性块的表现形式为<#+ FeatureCode #>

分类

  1. 设计时模板(文本模版)

    在 Visual Studio 中执行设计时 T4 文本模板,以便定义应用程序的部分源代码和其他资源。通常,您可以使用读取单个输入文件或数据库中的数据的多个模板,并生成一些 .cs、.vb 或其他源文件。每个模板都生成一个文件。 在 Visual Studio 或 MSBuild 内执行它们。若要创建设计时模板,请向您的项目中添加"文本模板"文件。 另外,您还可以添加纯文本文件并将其"自定义工具"属性设置为"TextTemplatingFileGenerator"。

  2. 运行时模板(预处理模板)

    可在应用程序中执行运行时 T4 文本模板("预处理过的"模板)以便生成文本字符串(通常作为其输出的一部分)。若要创建运行时模板,请向您的项目中添加"已预处理的文本模板"文件。另外,您还可以添加纯文本文件并将其"自定义工具"属性设置为"TextTemplatingFilePreprocessor"。

Visual Studio 中使用T4模板

创建T4模板文件

  1. 新建文件 :在Visual Studio中,你可以通过右键点击项目,选择"添加" -> "新建项...",然后在搜索框中输入"T4"或"Text Template"来找到T4模板文件模板(通常称为"文本模板")。选择它并命名你的模板文件(例如:MyTemplate.tt)。
  1. 编辑模板 :双击新创建的.tt文件以在Visual Studio中打开它。此时,你可以看到模板的初始内容,包括一些基本的指令和控制块。

2. 编写T4模板

在T4模板中,你可以使用C#或VB.NET代码(取决于你的项目设置)来编写控制逻辑,并使用特定的语法来定义输出文本的格式。

  • 指令块:如前所述,使用指令块来定义模板的行为和引入必要的资源。
  • 控制块 :使用<# ... #>来包围代码块,这些代码块在模板转换时执行。
  • 表达式块 :使用<#= ... #>来输出表达式的值到生成的文本中。
  • 类特征块 :使用<#+ ... #>来定义辅助方法、属性或类,这些方法可以在模板的其他部分中被调用。
c# 复制代码
<#@ template debug="false" hostspecific="false" language="C#" #>  
<#@ output extension=".cs" #>
using System;  
  
namespace MyNamespace  
{  
    public class MyClass  
    {  
        public string MyProperty { get; set; }  
  
        public void MyMethod()  
        {  
            Console.WriteLine("Hello from T4 Template!");  
        }  
    }  
}

3. 转换模板

  • 自动转换:在Visual Studio中,通常当你保存T4模板文件时,Visual Studio会自动执行模板转换并生成输出文件。
  • 手动转换:你也可以通过右键点击模板文件并选择"运行自定义工具"来手动触发模板的转换。

中心控制Manager

上面T4模板的简单内容。可以生成模板,但是只能保存在t4模板的目录下方,无法进行更多操作。假如是项目集,还需要手动赋值粘贴很麻烦,基于Manage类进行块控制和保存文件到指定位置

c# 复制代码
<#@ assembly name="System.Core"#>
<#@ assembly name="EnvDTE"#>
<#@ import namespace="System.Collections.Generic"#>
<#@ import namespace="System.IO"#>
<#@ import namespace="System.Text"#>
<#@ import namespace="Microsoft.VisualStudio.TextTemplating"#>
<#@ output extension=".cs" #>
<#+
class Manager
{
    public struct Block {
        public int Start, Length;
		public String Name,OutputPath;
    }

    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 Manager(ITextTemplatingEngineHost host, StringBuilder template, bool commonHeader) {
        this.host = host;
        this.template = template;
        strategy = ManagementStrategy.Create(host);
    }
    public void StartBlock(String name,String outputPath) {
        currentBlock = new Block { Name = name, Start = template.Length ,OutputPath=outputPath};
    }

    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(block.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;
            }
        }
    }
}#>

每一个文件就要进行一次block的开关,即manager.StartBlock(文件名)manager.EndBlock(),在文件都结束后,执行manager.Process(true),进行文件的写操作。

注意:Manager类实现了文件块的开关和保存位置的设定。
这里需要设置template指令 :hostspecific="true"

如果提示错误:T4 模板 错误 当前上下文中不存在名称"Host" ,请按照设置hostspecific="true"

根据 MySQL 数据生成 实体

MySqlHelper.tt

c# 复制代码
<#@ assembly name="C:\Users\xxxx\.nuget\packages\mysql.data\9.0.0\lib\net48\MySql.Data.dll" #>
<#@ assembly name="System.Core.dll" #>
<#@ assembly name="System.Data.dll" #>
<#@ assembly name="System.Xml.dll" #>
<#@ import namespace="System" #>
<#@ import namespace="System.Xml" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Data" #>
<#@ import namespace="MySql.Data.MySqlClient" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ import namespace="System.IO" #>

<#+  
    public class EntityHelper
    {
        public static List<Entity> GetEntities(string connectionString, List<string> databases)
        {
            var list = new List<Entity>();
            var conn = new MySqlConnection(connectionString);
            try
            {
                conn.Open();
                var dbs = string.Join("','", databases.ToArray());
                var cmd = string.Format(@"SELECT `information_schema`.`COLUMNS`.`TABLE_SCHEMA`
                                                    ,`information_schema`.`COLUMNS`.`TABLE_NAME`
                                                    ,`information_schema`.`COLUMNS`.`COLUMN_NAME`
                                                    ,`information_schema`.`COLUMNS`.`DATA_TYPE`
                                                    ,`information_schema`.`COLUMNS`.`COLUMN_COMMENT`
                                                FROM `information_schema`.`COLUMNS`
                                                WHERE `information_schema`.`COLUMNS`.`TABLE_SCHEMA` IN ('{0}') ", dbs);
                using (var reader = MySqlHelper.ExecuteReader(conn, cmd))
                {
                    while (reader.Read())
                    {
                        var db = reader["TABLE_SCHEMA"].ToString();
                        var table = reader["TABLE_NAME"].ToString();
                        var column = reader["COLUMN_NAME"].ToString();
                        var type = reader["DATA_TYPE"].ToString();
                        var comment = reader["COLUMN_COMMENT"].ToString();
                        var entity = list.FirstOrDefault(x => x.EntityName == table);
                        if (entity == null)
                        {
                            entity = new Entity(table);
                            entity.Fields.Add(new Field
                            {
                                Name = column,
                                Type = GetCLRType(type),
                                Comment = comment
                            });

                            list.Add(entity);
                        }
                        else
                        {
                            entity.Fields.Add(new Field
                            {
                                Name = column,
                                Type = GetCLRType(type),
                                Comment = comment
                            });
                        }
                    }
                }
            }
            finally
            {
                conn.Close();
            }

            return list;
        }

        public static string GetCLRType(string dbType)
        {
            switch (dbType)
            {
                case "tinyint":
                case "smallint":
                case "mediumint":
                case "int":
                case "integer":
                    return "int";
                case "double":
                    return "double";
                case "float":
                    return "float";
                case "decimal":
                    return "decimal";
                case "numeric":
                case "real":
                    return "decimal";
                case "bit":
                    return "bool";
                case "date":
                case "time":
                case "year":
                case "datetime":
                case "timestamp":
                    return "DateTime";
                case "tinyblob":
                case "blob":
                case "mediumblob":
                case "longblog":
                case "binary":
                case "varbinary":
                    return "byte[]";
                case "char":
                case "varchar":
                case "tinytext":
                case "text":
                case "mediumtext":
                case "longtext":
                    return "string";
                case "point":
                case "linestring":
                case "polygon":
                case "geometry":
                case "multipoint":
                case "multilinestring":
                case "multipolygon":
                case "geometrycollection":
                case "enum":
                case "set":
                default:
                    return dbType;
            }
        }
    }

    public class Entity
    {
        public Entity()
        {
            this.Fields = new List<Field>();
        }

        public Entity(string name)
            : this()
        {
            this.EntityName = name;
        }

        public string EntityName { get; set; }
        public List<Field> Fields { get; set; }
        public string PascalEntityName
        {
            get
            {
                return CommonConver.ToPascalCase(this.EntityName);
            }
        }
        public string CamelEntityName
        {
            get
            {
                return CommonConver.ToCamelCase(this.EntityName);
            }
        }
    }

    public class Field
    {
        public string Name { get; set; }
        public string Type { get; set; }
        public string Comment { get; set; }
    }
    public class CommonConver
    {
        public static string ToPascalCase(string tableName)
        {
            string upperTableName = tableName.Substring(0, 1).ToUpper() + tableName.Substring(1, tableName.Length - 1);
            return upperTableName;
        }
        public static string ToCamelCase(string tableName)
        {
            string lowerTableName = tableName.Substring(0, 1).ToLower() + tableName.Substring(1, tableName.Length - 1);
            return lowerTableName;
        }
    }

    class config
    {

        public static readonly string ConnectionString = "Database=test;Data Source=127.0.0.1;User Id=root;Password=123456;pooling=false;CharSet=utf8;port=3306";
        public static readonly string ModelNameSpace = "App.Entities";
    }
#>

AutoCreateModel.tt

c# 复制代码
<#@ template debug="false" hostspecific="true" language="C#" #>
<#@ assembly name="System.Data.Common.dll" #>
<#@ assembly name="System.Core.dll" #>
<#@ assembly name="System.Data.dll" #>
<#@ assembly name="System.Xml.dll" #>
<#@ import namespace="System" #>
<#@ import namespace="System.Xml" #>
<#@ import namespace="System.Linq" #>
<#@ import namespace="System.Data" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ import namespace="System.IO" #>
<#@ include file="$(ProjectDir)Manage.tt"  #>
<#@ include file="$(ProjectDir)MySqlHelper.tt"  #>
<#@ output extension=".cs" #>
<# var manager = new Manager(Host, GenerationEnvironment, true); #>
<# 
    
      var OutputPath1 ="D:\\test"; //设置文件存储逇位置
      var entities =EntityHelper.GetEntities(config.ConnectionString,new List<string> { "test"});
      foreach(Entity entity in entities)
     {
	 manager.StartBlock(entity.EntityName+".cs",OutputPath1);
#>
using System;

namespace <#=config.ModelNameSpace#>
{
    /// <summary>
    /// <#= entity.EntityName #> Entity Model
    /// </summary>   

    public class <#= entity.EntityName #>
    {
<#
        for(int i = 0; i < entity.Fields.Count; i++)
        {
            if(i ==0)
            {
#>      
        /// <summary>
        /// <#= entity.Fields[i].Comment #>
        /// </summary>
        public <#= entity.Fields[i].Type #> <#= entity.Fields[i].Name #> { get; set; }
<#
            }
            else
            {
#>   
        /// <summary>
        /// <#= entity.Fields[i].Comment #>
        /// </summary>

        public <#= entity.Fields[i].Type #> <#= entity.Fields[i].Name #> { get; set; }
<#            
            }
        }
#>
    }
}
<#       
        manager.EndBlock();
    }
    manager.Process(true);
#>

介绍几个常用的$(variableName) 变量:

  • $(SolutionDir):当前项目所在解决方案目录
  • $(ProjectDir):当前项目所在目录
  • $(TargetPath):当前项目编译输出文件绝对路径
  • $(TargetDir):当前项目编译输出目录,即web项目的Bin目录,控制台、类库项目bin目录下的debug或release目录(取决于当前的编译模式)

举个例子:比如我们在D盘根目录建立了一个控制台项目TestConsole,解决方案目录为D:\LzrabbitRabbit,项目目录为

D:\LzrabbitRabbit\TestConsole,那么此时在Debug编译模式下

  • $(SolutionDir)的值为D:\LzrabbitRabbit
  • $(ProjectDir)的值为D:\LzrabbitRabbit\TestConsole
  • $(TargetPath)值为D:\LzrabbitRabbit\TestConsole\bin\Debug\TestConsole.exe
  • $(TargetDir)值为D:\LzrabbitRabbit\TestConsole\bin\Debug\