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

asp,net,core,t4 · 浏览次数 : 46

正文

简介

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)。

image

  1. 编辑模板:双击新创建的.tt文件以在Visual Studio中打开它。此时,你可以看到模板的初始内容,包括一些基本的指令和控制块。

2. 编写T4模板

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

  • 指令块:如前所述,使用指令块来定义模板的行为和引入必要的资源。
  • 控制块:使用<# ... #>来包围代码块,这些代码块在模板转换时执行。
  • 表达式块:使用<#= ... #>来输出表达式的值到生成的文本中。
  • 类特征块:使用<#+ ... #>来定义辅助方法、属性或类,这些方法可以在模板的其他部分中被调用。
<#@ 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类进行块控制和保存文件到指定位置

<#@ 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

<#@ 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

<#@ 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\

与Asp .Net Core 系列:基于 T4 模板生成代码相似的内容:

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

目录简介组成部分分类Visual Studio 中使用T4模板创建T4模板文件2. 编写T4模板3. 转换模板中心控制Manager根据 MySQL 数据生成 实体 简介 T4模板,即Text Template Transformation Toolkit,是微软官方在Visual Studio中引

Asp .Net Core 系列:基于 Castle DynamicProxy + Autofac 实践 AOP 以及实现事务、用户填充功能

目录什么是 AOP ?.Net Core 中 有哪些 AOP 框架?基于 Castle DynamicProxy 实现 AOPIOC中使用 Castle DynamicProxy实现事务管理实现用户自动填充 什么是 AOP ? AOP(Aspect-Oriented Programming,面向切面

武装你的WEBAPI-OData使用Endpoint

本文属于 OData 系列文章 Introduction 更新: 由于新版的 OData 已经默认使用了 endpoint 模式(Microsoft.AspNetCore.OData 8.0.0),不再需要额外配置,本文已经过时(asp.net core 3.1)。 最近看 OData 的 devb

Asp .Net Core 系列:详解鉴权(身份验证)以及实现 Cookie、JWT、自定义三种鉴权 (含源码解析)

什么是鉴权(身份验证)? https://learn.microsoft.com/zh-cn/aspnet/core/security/authentication/?view=aspnetcore-8.0 定义 鉴权,又称身份验证,是确定用户身份的过程。它验证用户提供的凭据(如用户名和密码)是否有

Asp .Net Core 系列:集成 CAP + RabbitMQ + MySQL(含幂等性)

简介 官网:https://cap.dotnetcore.xyz/ CAP 是什么? 是一个 EventBus,同时也是一个在微服务或者 SOA 系统中解决分布式事务问题的一个框架。它有助于创建可扩展,可靠并且易于更改的微服务系统。 什么是 EventBus? 事件总线是一种机制,它允许不同的组件彼

Asp .Net Core 系列:国际化多语言配置

目录概述术语本地化器IStringLocalizer在服务类中使用本地化IStringLocalizerFactoryIHtmlLocalizerIViewLocalizer资源文件区域性回退配置 CultureProvider内置的 RequestCultureProvider实现自定义 Requ

.NET 8 Preview 5发布,了解一下Webcil 是啥

2023年6月13日 .NET 8 Preview 5,.NET 团队在官方博客发布了系列文章: Announcing .NET 8 Preview 5 ASP.NET Core updates in .NET 8 Preview 5 Announcing .NET MAUI in .NET 8 P

.NET 8 Preview 6发布,支持新的了Blazor呈现方案 和 VS Code .NET MAUI 扩展

2023年7月11日 .NET 8 Preview 6,.NET 团队在官方博客发布了系列文章:Announcing .NET 8 Preview 6[1]ASP.NET Core updates in .NET 8 Preview 6[2]Announcing .NET MAUI in .NET

Asp-Net-Core开发笔记:使用RateLimit中间件实现接口限流

前言 最近一直在忙(2月份沉迷steam,3月开始工作各种忙),好久没更新博客了,不过也积累了一些,忙里偷闲记录一下。 这个需求是这样的,我之前做了个工单系统,现在要对登录、注册、发起工单这些功能做限流,不能让用户请求太频繁。 从 .Net7 开始,已经有内置的限流功能了,但目前我们的项目还在使用

开发日志:Kylin麒麟操作系统部署ASP.NET CORE

需求场景: 我需要部署的项目是在Windows上开发的,目标框架为.net core 6.0 因此我们需要先在kylin上部署项目运行所需要的环境。 借助百度词条,先看看Kylin是什么: 服务器资源: 查看系统版本 cat /etc/kylin-release cat /proc/version