一、什么是T4?
1.1、T4简介
T4,即4个T开头的英文字母组合:Text Template Transformation Toolkit,是微软官方在Visual Studio 2008开始使用的代码生成引擎。T4是由一些文本块和控制逻辑组成的混合模板,简单地说,T4可以根据模板生成您想要的文件,如类文件、文本文件、HTML等等。
VS提供了一套基于T4引擎的代码生成执行环境,由以下程序集构成:
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、T4模板类型
T4模板有两种类型:
1)运行时模板
在应用程序中执行运行时T4文本模板,以便生成文本字符串。
若要创建运行时模板,请向您的项目中添加"运行时文本模板"文件。另外,您还可以添加纯文本文件并将其"自定义工具"属性设置为"TextTemplatingFilePreprocessor"。
2)设计时模板
在VS中执行设计时T4文本模板,以便定义应用程序的部分源代码和其它资源。
若要创建设计时模板,请向您的项目中添加"文本模板"文件。 另外,您还可以添加纯文本文件并将其"自定义工具"属性设置为"TextTemplatingFileGenerator"。
1.3、插件安装
VS默认的编辑工具无高亮、无提示,错误不容易定位,建议安装tangible T4 Editor插件进行编写T4代码。
二、T4 Hello World示例
假设有一个控制台应用程序LinkTo.Test.ConsoleT4,现在要输出"Hello World"字符串,Program代码如下:
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();
}
}
}
现在把Program.cs文件删除掉,利用T4模板生成一个与上述代码相同的Program.cs,操作方法如下:
1)项目右键"添加"->"新建项"->"文本模板",将名称更改为Program.tt。
2)Program.tt的代码如下:
<#@ 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)点击保存,即可看到Program.tt下生成了一个Program.cs文件,代码与最初的Hello World一样。
三、T4 Hello World示例扩展
现在扩展一下Hello World示例,在程序中增加两个类:
1)Hello类,输出"Hello"。
2)World类,输出"World"。
代码如下:
<#@ 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 #>类Show()方法
/// </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 #>方法调用
<#=callMethod #>
<#
}
#>
Console.Read();
}
}
}
生成文件如下:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace LinkTo.Test.ConsoleT4
{
class Hello
{
/// <summary>
/// Hello类Show()方法
/// </summary>
public static void Show()
{
Console.WriteLine("Hello");
}
}
class World
{
/// <summary>
/// World类Show()方法
/// </summary>
public static void Show()
{
Console.WriteLine("World");
}
}
class Program
{
static void Main(string[] args)
{
Console.WriteLine("Hello World");
//Hello.Show();方法调用
Hello.Show();
//World.Show();方法调用
World.Show();
Console.Read();
}
}
}
四、T4模板的基本结构
代码块可分为两种:文本及程序脚本。
4.1、文本:就是需要生成的文本
4.2、程序脚本:内部执行,最终生成想要的文本。T4中<# #>中的部分,都属于程序脚本内容。
为了方便理解,使用"块"(Block)来细分语法。块是构成T4模板的基本单元,可以分成5类:指令块(Directive Block)、文本块(Text Block)、代码语句块(Statement Block)、表达式块(Expression Block)、类特性块(Class Feature Block)。
4.2.1、指令块(Directive Block)
和ASP.NET页面的指令一样,它们出现在文件头,通过<#@ … #>表示。其中<#@ template … #>指令是必须的,用于定义模板的基本属性,比如编程语言、基于的文化、是否支持调式等。
指令通常是模板文件或包含的文件中的第一个元素。不应将它们放置在代码块<#...#>内,也不应放置在类功能块<#+...#>之后。
T4 模板指令
<#@ template [language="C#"] [hostspecific="true"] [debug="true"] [inherits="templateBaseClass"] [culture="code"] [compilerOptions="options"] #>
T4 参数指令
<#@ parameter type="Full.TypeName" name="ParameterName" #>
T4 输出指令
<#@ output extension=".fileNameExtension" [encoding="encoding"] #>
T4 程序集指令
<#@ assembly name="[assembly strong name|assembly file name]" #>
$(SolutionDir):当前项目所在解决方案目录
$(ProjectDir):当前项目所在目录
$(TargetPath):当前项目编译输出文件绝对路径
$(TargetDir):当前项目编译输出目录
T4 导入指令
<#@ import namespace="namespace" #>
T4 包含指令
<#@ include file="filePath" #>
4.2.2、文本块(Text Block)
文本块就是直接原样输出的静态文本,不需要添加任何的标签。
4.2.3、代码语句块(Statement Block)
代码语句块通过<# Statement #>的形式表示,中间是一段通过相应编程语言编写的程序调用,我们可以通过代码语句快控制文本转化的流程。
4.2.4、表达式块(Expression Block)
表达式块以<#=Expression #>的形式表示,通过它可以动态地解析字符串表达式内嵌到输出的文本中。
4.2.5、类特性块(Class Feature Block)
如果文本转化需要一些比较复杂的逻辑,代码可能需要写在一个单独的辅助方法中,甚至是定义一些单独的类。类特性块的表现形式为<#+ FeatureCode #>。
五、T4模板生成数据库实体类
5.1、添加一个T4Code文件夹,并在下面新建两个文本模板,需要注意的是,这里不使用默认的.tt扩展名,而是.ttinclude,将它们作为包含文件使用。
<#@ 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>
/// 数据库架构接口
/// </summary>
public interface IDBSchema : IDisposable
{
List<string> GetTableList();
Table GetTableMetadata(string tableName);
}
/// <summary>
/// 数据库架构工厂
/// </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>
/// 数据表
/// </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>
/// 数据列
/// </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,因为对于MySql如果是CHAR(36),类型自动为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>
/// 帮助类
/// </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>
/// 符号枚举
/// </summary>
public enum Symbol
{
Normal = 1,
Underline = 2
}
#endregion
#>
DBSchema.ttinclude主要实现了数据库工厂的功能。注:请将数据库连接字符串改成您自己的。
<#@ 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主要实现了多文档的功能。
5.2、添加一个MultModelAuto.tt文本模板,代码如下:
<#@ 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();//调试
var dbSchema = DBSchemaFactory.GetDBSchema();
List<string> tableList = dbSchema.GetTableList();
foreach(string tableName in tableList)
{
manager.StartBlock(tableName+".cs");
Table table = dbSchema.GetTableMetadata(tableName);
#>
//-------------------------------------------------------------------------------
// 此代码由T4模板MultModelAuto自动生成
// 生成时间 <#=DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss")#>
// 对此文件的更改可能会导致不正确的行为,并且如果重新生成代码,这些更改将会丢失。
//-------------------------------------------------------------------------------
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);
#>
代码保存后,可以看到此文件下面已按数据表生成了多个实体类文件:
参考自:
https://www.cnblogs.com/dataadapter/p/3844394.html
https://www.cnblogs.com/yank/archive/2012/02/14/2342287.html
附:
来源:oschina
链接:https://my.oschina.net/u/4400630/blog/3290815