使用 T4 範本產生器產生對應資料庫的 Entity

  • 1120
  • 0
  • 2017-07-26

使用 T4 範本產生器產生對應資料庫的 Entity

T4 是「Text Template Transformation Toolkit」的簡稱,這工具是用來動態產生文字檔或是程式碼用的,

Visual Studio 2008 之後的板板都有內建,像是 EntityFramework就有用到他來產生對應 table 的 Entity,

他有「執行階段」和「設計階段」兩種執行方式,可以從檔案的「自訂工具」屬性設定,

分別是TextTemplatingFilePreprocessor 和 TextTemplatingFileGenerator。

這篇就來說如何用設計階段的 T4 產生自己想要的 Entity。

我們可以在任意專案新增項目,選擇文字範本

如果專案沒有文字範本可以選,也可以建立文字檔,然後把附檔名改成 .tt 。

產生之預設內容如下

<#@ 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=".txt" #>

此檔案的「自動工具」屬性會是TextTemplatingFileGenerator,

T4 的結構由「指示詞」、「文字區塊」、「控制區塊」三者組成

指示詞 有很多種,這邊只說明有用到的

在上面的程式碼就都是指示詞,

template (範本指示詞)設定一些範本的處理方式 

assembly (組件指示詞)是設定要載入的 dll,

import (匯入指示詞)用來匯入命名空間

output (輸出指示詞)用來定義副檔名和編碼 

文字區塊會將文字直接做輸出,以下的 Hello 就是文字區塊

<#@ output extension=".txt" #>  
Hello  

 

控制區塊就是用來寫程式碼的地方,預設的程式語言是C#

我們可以將範本指示詞的 language 屬性設定成 vb,表示要用 vb 來開發

而控制區塊又分三種

由 <#  #> 包起來的區塊稱作「標準控制區塊」,在這裡開發相關邏輯

<#=  #> 包起來的是「運算式控制區塊」,可以將運算式計算完成的內容轉成文字輸出。

第三個式「類別功能控制區塊」,由 <#+  #> 包起來,用來定義屬性,方法或是類別的地方。

接著我們就來直接看完整的程式碼吧,我也直接把說明寫在註解裡

<#@ template language="C#" debug="True" hostspecific="True" #>
<#@ assembly name="System.Data" #>
<#@ assembly name="System.Core" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ import namespace="System.Data.SqlClient" #>
<#@ import namespace="System.Data" #>
<#@ import namespace="System.IO" #>
<#@ import namespace="System.Diagnostics" #>
using System;

//改成自己的 namespace
namespace MyProject.Entities {
<#

		//修改connection string
		string connectionString = @"我的連線字串";
		SqlConnection conn = new SqlConnection(connectionString); 
		conn.Open(); 
		//取得所有 table 的 schema
		DataTable schema = conn.GetSchema("Tables");
			
		foreach(System.Data.DataRow row in schema.Rows)	{ 				
			var tableName = row["TABLE_NAME"].ToString().Trim('s');
			//取得必要資訊列表
			var schemaList = CodeStringGenerator.GetSchemaInfos(conn, tableName);
#>

	/// <summary>
	/// <#= tableName #>
	/// </summary>
	public class <#= tableName #>{
<#      				
			foreach (var s in schemaList) {
#>
		/// <summary>
<#
				//將DB裡對於欄位的描述排版顯示
				foreach(var d in s.Description.Split('\n')){
#>
		/// <#= d.Trim() #>
<#
				}
#>		/// </summary>
		public <#= s.TypeName #><#= s.AllowDBNull ? "?" : ""#> <#= s.ColumnName #>  { get; set; }

<#    
			}
#>                                
	}   
<#		
		}
#>
}

<#+
//以下是取得產生類別所需內容的程式碼,參考了 Necroskillz 的文章
//http://www.necronet.org/archive/2012/10/09/generate-c-pocos-from-sql-statement-in-linqpad.aspx
public class CodeStringGenerator{
	public static Dictionary<Type, string> TypeAliases = new Dictionary<Type, string> {
		{ typeof(int), "int" },
		{ typeof(short), "short" },
		{ typeof(byte), "byte" },
		{ typeof(byte[]), "byte[]" },
		{ typeof(long), "long" },
		{ typeof(double), "double" },
		{ typeof(decimal), "decimal" },
		{ typeof(float), "float" },
		{ typeof(bool), "bool" },
		{ typeof(string), "string" }
	};

	public static HashSet<Type> NullableTypes = new HashSet<Type> {
		typeof(int),
		typeof(short),
		typeof(long),
		typeof(double),
		typeof(decimal),
		typeof(float),
		typeof(bool),
		typeof(DateTime)
	};

	//取得欄位的描述
	//https://docs.microsoft.com/zh-tw/sql/relational-databases/system-functions/sys-fn-listextendedproperty-transact-sql
	public static string descriptionSQL = @"SELECT * FROM ::fn_listextendedproperty(NULL, 'schema', 'dbo', 'table', @tableName, 'column', NULL)";

	//取得欄位相關資訊以及描述文字
	public static List<SchemaInfo> GetSchemaInfos(SqlConnection connection, string table) {
		List<SchemaInfo> list = new List<SchemaInfo>();
		Dictionary<string, string> descriptionDir = new Dictionary<string, string>();
		if (connection.State != ConnectionState.Open)
			connection.Open();

		var desciptionCmd = connection.CreateCommand();
		desciptionCmd.CommandText = descriptionSQL;
		desciptionCmd.Parameters.Add(new SqlParameter("@tableName", table));
		var descReader = desciptionCmd.ExecuteReader();
		while (descReader.Read()) {
			descriptionDir.Add(descReader["objname"].ToString(), descReader["value"].ToString());
		}
		descReader.Close();

		var cmd = connection.CreateCommand();
		cmd.CommandText = $"select top(1) * from [{table}]";
		var reader = cmd.ExecuteReader();

		do {
			if (reader.FieldCount <= 1) continue;

			var schema = reader.GetSchemaTable();

			foreach (DataRow row in schema.Rows) {
				var info = new SchemaInfo();
				info.DataType = (Type)row["DataType"];
				info.TypeName = TypeAliases.ContainsKey(info.DataType) ? TypeAliases[info.DataType] : info.DataType.Name;
				info.AllowDBNull = (bool)row["AllowDBNull"] && NullableTypes.Contains(info.DataType);
				info.ColumnName = (string)row["ColumnName"];
				if (descriptionDir.ContainsKey(info.ColumnName)) info.Description = descriptionDir[info.ColumnName];
				else info.Description = "";
				list.Add(info);
			}

		} while (reader.NextResult());
		reader.Close();
		return list;
	}
}


public struct SchemaInfo {
	public Type DataType { get; set; }
	public bool AllowDBNull { get; set; }
	public string TypeName { get; set; }
	public string ColumnName { get; set; }
	public string Description { get; set; }
}
#>

存檔之後,就會自動產生 TextTemplate1.cs 檔案,內容大致如下

根據每個人的資料庫內容而定。

如此一來就可以自動產生一系列程式碼,不用自己 Key 囉。

 

參考資訊

MSDN 文件

Necroskillz 的文章

Marc Gravell的解答