【框架总结】利用T4模板批量生成代码
很早就看过T4模板的介绍,是可以自定义规则来生成文件的,不过当时没时间研究就跳过了,继续使用动软生成代码。
现在终于抽时间学下T4模板,多亏大神们的无私分享,使我很快就用上T4模板了,灰常灰常感谢大大们~~
在这里我也总结下使用情况,有说的不好的欢迎指出~~
T4文件后缀主要有:tt和ttinclude,tt文件是模板文件,每次保存VS都会提示是否执行代码;ttinclude文件是tt的辅助文件,保存不会提示执行代码。
需要生成代码,一般都是映射数据库了,当然也可以用来生成其他比较统一格式的文件,全看您的模板怎么写,这里我是用来生成映射数据库表的实体类。
(如果想让T4模板代码高亮,有提示,可以下载安装T4模板插件)
这是我的T4模板文件结构
1.DBSchema.ttinclude
首先,我是找到一个DBSchema.ttinclude文件,此文件是用来访问数据库,从数据库读出表的信息,代码如下,我根据我自己的情况改动了一些
(由于找资料时没记住出处,忘记是copy哪个大神的了,大神请见谅^_^|||)
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
<#@ assembly name="System.Core" #> <#@ assembly name="System.Data" #> <#@ assembly name="System.xml" #> <#@ assembly name="$(ProjectDir)\T4Manager\MySql.Data.dll" #> <#@ import namespace="System" #> <#@ import namespace="System.Collections.Generic" #> <#@ import namespace="System.Data" #> <#@ import namespace="System.Data.SqlClient" #> <#@ import namespace="MySql.Data.MySqlClient" #> <#@ import namespace="System.Linq"#> <#+ public class DBSchemaFactory { public static IDBSchema GetDBSchema(string dbType) { IDBSchema dbSchema; string connectionString = String.Empty; switch (dbType) { case "SqlServer": connectionString = "Data Source=.;Initial Catelog=dbName;Persist Security Info=True;User ID=sa;Password=sa;"; dbSchema = new SqlServerSchema(connectionString); break; case "MySql": connectionString = "Server=localhost;Port=3306;Database=dbName;Uid=root;Pwd=pwd;charset=utf8;"; dbSchema = new MySqlSchema(connectionString); break; default: throw new ArgumentException("The input argument of DatabaseType is invalid!"); } return dbSchema; } public interface IDBSchema { List<Table> GetTables(string dbName); } public class SqlServerSchema : IDBSchema { public SqlConnection conn; public SqlServerSchema(string connString) { conn = new SqlConnection(connString); } public List<Table> GetTables(string dbName) { List<Table> list = new List<Table>(); try { conn.Open(); var cmd = string.Format(@"SELECT tab.name AS TABLE_NAME, col.name AS COLUMN_NAME, col.is_identity, per.value AS COLUMN_COMMENT, t.name AS DATA_TYPE FROM {0}.sys.columns col INNER JOIN {0}.sys.tables tab ON col.object_id = tab.object_id LEFT JOIN {0}.sys.extended_properties per ON col.column_id = per.minor_id AND per.major_id = tab.object_id INNER JOIN {0}.SYS.types t ON col.user_type_id = t.user_type_id", dbName); SqlCommand command = new SqlCommand(cmd, conn); using(SqlDataReader reader = command.ExecuteReader()) { while (reader.Read()) { string db = dbName, table = reader["TABLE_NAME"].ToString(), column = reader["COLUMN_NAME"].ToString(), type = reader["DATA_TYPE"].ToString(), comment = reader["COLUMN_COMMENT"].ToString(), pk = reader["is_identity"].ToString(); Table entity = list.FirstOrDefault(x => x.TableName == table); if (entity == null) { entity = new Table(table); entity.Columns.Add(new Column { Name = column, Type = GetCLRType(type), Comment = comment, IsPK = pk == "1" ? true : false }); list.Add(entity); } else { entity.Columns.Add(new Column { Name = column, Type = GetCLRType(type), Comment = comment, IsPK = pk == "1" ? true : false }); } } } } finally { conn.Close(); } return list; } } public class MySqlSchema : IDBSchema { public MySqlConnection conn; public MySqlSchema(string connString) { conn = new MySqlConnection(connString); } public List<Table> GetTables(string dbName) { List<Table> list = new List<Table>(); try { conn.Open(); 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` ,`information_schema`.`COLUMNS`.`COLUMN_KEY` FROM `information_schema`.`COLUMNS` WHERE `information_schema`.`COLUMNS`.`TABLE_SCHEMA` = '{0}'", dbName); MySqlCommand command = new MySqlCommand(cmd, conn); using(MySqlDataReader reader = command.ExecuteReader()) { while (reader.Read()) { string db = reader["TABLE_SCHEMA"].ToString(), table = reader["TABLE_NAME"].ToString(), column = reader["COLUMN_NAME"].ToString(), type = reader["DATA_TYPE"].ToString(), comment = reader["COLUMN_COMMENT"].ToString(), pk = reader["COLUMN_KEY"].ToString(); Table entity = list.FirstOrDefault(x => x.TableName == table); if (entity == null) { entity = new Table(table); entity.Columns.Add(new Column { Name = column, Type = GetCLRType(type), Comment = comment, IsPK = pk == "PRI" ? true : false }); list.Add(entity); } else { entity.Columns.Add(new Column { Name = column, Type = GetCLRType(type), Comment = comment, IsPK = pk == "PRI" ? true : false }); } } } } 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": 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 Table { public Table() { this.Columns = new List<Column>(); } public Table(string name) : this() { this.TableName = name; } public string TableName { get; set; } public List<Column> Columns { get; set; } } public class Column { //字段名 public string Name { get; set; } //类型 public string Type { get; set; } //备注 public string Comment { get; set; } //是否主键 public bool IsPK { get; set; } } #>
原先copy的只有针对SQL Server的,不过我需要MySql,所以上网找了个连接MySql的加上了。
-- <#@ assembly name="$(ProjectDir)\T4Manager\MySql.Data.dll" #>
这里的$(ProjectDir)当前项目所在目录路径,还可以用其他:
- $(SolutionDir):当前项目所在解决方案目录
- $(ProjectDir):当前项目所在目录
- $(TargetPath):当前项目编译输出文件绝对路径
- $(TargetDir):当前项目编译输出目录
-- public bool IsPK { get; set; } //这个是我用来记录该列是否主键,我生成实体类时需要用到,不需要可以去掉
SQL Server是通过sys.columns.is_identity获取,MySql是通过information_schema.COLUMNS.COLUMN_KEY获取。
OK,其他没怎么改动过了。
2.MultDocument.ttinclude
数据库连接有了,不过我根据大神的分享,生成实体类,发现每次只能生成一个表实体类,这完全不合理嘛,于是又上网找了下批量生成的资料。。。
很快就找到MultDocument.ttinclude文件了,据介绍,是外国一个大神分享的,这个文件没什么改的,我就直接用了一 一+
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
<#@ 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" #> <#+ // 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 u 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; } } } } #>
各位看官,直接copy存为ttinclude就可以了。
3.CommonAttr.ttinclude
准备工作差不多了,不过在写tt模板之前,我先说下我抽出来的公用参数/方法。
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
<#+ public class Attributes { // 文件版权信息 public static string Copyright = DateTime.Now.Year + " RoryLiu All Rights Reserved"; public static Version Version = Environment.Version; public static string Author = "auto generated by T4"; public static string DbType = "MySql";//数据库类型 public static string DbName = "dbName";//数据库名 public static string ProName = "Namespace";//命名空间 public static bool IsGo = true;//是否执行 public static string TableNames = "*";//全部用"*",部分表用",表名,表名,..." } //得到属性的Pascial风格名称,比如my_table => MyTable public string GetPropertyPascialName(string source) { string[] s = source.Split('_'); for(int i = 0; i < s.Length; i++) { string s1 = s[i].Substring(0, 1).ToUpper(); string s2 = s[i].Substring(1); s[i] = String.Concat(s1,s2); } return String.Concat(s); } #>
将这些参数抽出来主要是为了方便生成Model、DAL、BLL等模板,不一定要用这个文件。
4.ModelTemp.tt
准备工作都做完了,终于到模板了,各位看官等急了吧,先给大家看下生成实体类的模板:(ModelTemp.tt其实也是上网找的,然后自己再修改( ‵▽′)ψ)
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
<#@ template debug="true" hostSpecific="true" language="C#" #> <#@ include file="CodeTemplates/MultDocument.ttinclude" #> <#@ include file="CodeTemplates/DBSchema.ttinclude" #> <#@ include file="CodeTemplates/CommonAttr.ttinclude" #> <#@ output extension=".cs" #> <# //不生成则退出 if (!Attributes.IsGo) return ""; //命名空间 var nsName = Attributes.ProName + ".Models"; //实例化生成模板 var manager = new Manager(Host, GenerationEnvironment, true) { OutputPath = Path.GetDirectoryName(Host.TemplateFile)}; //获取连接的数据库 var dbSchema = DBSchemaFactory.GetDBSchema(Attributes.DbType); //获取数据库下的所有表 var entities = dbSchema.GetTables(Attributes.DbName); //实体名 var entityName = ""; foreach(Table entity in entities) { //只生成指定的表实体 if (!Attributes.TableNames.Equals("*") && !Attributes.TableNames.Contains("," + entity.TableName + ",")) continue; //生成实体名 entityName = GetPropertyPascialName(entity.TableName); manager.StartBlock(entityName + ".cs"); Column pkCol = entity.Columns.Find(c => c.IsPK); foreach (var col in entity.Columns) { if (col.IsPK) { pkCol = col; break; } } #> //----------------------------------------------------------------------- // <copyright file="<#= entity.TableName #>.cs"> // * Copyright (C) <#= Attributes.Copyright #> // * version : <#= Attributes.Version #> // * author : <#= Attributes.Author #> // * FileName: <#= entityName #>.cs // * history : Created by T4 <#= DateTime.Now #> // </copyright> //----------------------------------------------------------------------- using System; using System.ComponentModel.DataAnnotations; namespace <#= nsName #> { /// <summary> /// <#= entityName #> Entity Model /// </summary> [Serializable] [Table("<#= entity.TableName #>")] public partial class <#= entityName #> { public <#= entityName #>() { } <# for (int i = 0; i < entity.Columns.Count; i++) { #> /// <summary> /// <#= entity.Columns[i].Comment #> /// </summary> <# if (pkCol.Name == entity.Columns[i].Name) { #> [Key] <# } #> public <#= entity.Columns[i].Type #> <#= entity.Columns[i].Name #> { get; set; } <# } #> } } <# manager.EndBlock(); } manager.Process(true); #>
头部有三个<#@ include #>,相信大家都知道是神马了,没错,就是引用前面准备的三个ttinclude文件。
-- Attributes.IsGo,其实这个不用也没什么影响,我是因为不想每次保存都弹出提示是否执行才加的,我设置成不再提示,然后用IsGo来判断是否执行
-- 实体名我没用数据库表名,而是通过GetPropertyPascialName()将表名变成Pascial风格,真正的表名我是写在自定义特性Table里
-- 循环所有表时,我是通过Attributes.TableNames来确定哪些表要生成文件,这样后期有改动某个表时不需要全部生成
-- 为了给主键属性加上特性[Key],我是通过IsPK来判断
模板基本就改了这些。
5.Table.cs
Table类是自定义特性,继承了System.Attribute,代码如下:
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
/// <summary> /// 映射数据库表对象 /// </summary> [AttributeUsageAttribute(AttributeTargets.Class, Inherited = false, AllowMultiple = false), Serializable] public class Table : Attribute { /// <summary> /// 表名称 /// </summary> public string TableName; public Table(string tblName) { TableName = tblName; } }
我是直接放在根目录下,当然如果有多个自定义特性时,还是建议建个文件夹。
好了,现在只要把CommonAttr里的IsGo改为true,DBSchema里的connectionString改成您本地的mysql就可以保存ModelTemp来生成了。