[.NET]Entity Generator with StringLength ValidationAttribute by T4

  • 4864
  • 0

[.NET]Entity Generator with StringLength ValidationAttribute by T4

前言

在前面的文章: [.NET]Entity Generator by T4 with Table Description and Column Description 有提到如何透過 T4 來自動產生 mapping table 的 model ,並且將 table description 與 column description 帶到 <summary> 的註解中。

在另一篇文章 [NestedModelValidation]使用 ValidationAttribute 來驗證 Model 是否符合 Validation Rules 也有提到,怎麼透過 DataAnnotation 的 ValidationAttribute ,結合 .NET 4.0 內建的 Vaildator.TryValidateObject() 來做到 Nested Model Validation。

這一篇文章,則是再強化一下我們的 T4 generator, 讓產生出來的 model 可以依據 DB column 的長度限制,讓 model 的 property 為 string 型態時,能自動加上 StringLength 的限制。

 

工具推薦

工欲善其事,必先利其器。先前在編輯 T4 時,一直覺得有點痛苦,template 跟 code logic 併在一起,相當考驗 developer 的眼睛以及腦袋中的編譯器。剛好看到有個 Visaul Studio 的 T4 Add-in ,這邊也介紹給大家:tangible T4 Editor 2.2.1 plus UML modeling tools 。除了很 fancy 的 UML modeling tool 我還沒用過以外,有幾個很不錯的 feature 一整個幫助很大:

  1. Syntax-Highlighting:針對 template 與自己編寫的 code 會有不同的 highlight 顏色。讓你一目了然哪一些是 code template, 哪一些是 logic 。如下圖所示(雖然我的位置排的很醜,但這也是我還沒法突破 T4 的一點):
    image
  2. Intelli-Sense:這對一個 “modern” developer 來說,再重要不過了。尤其是在 T4 中還有參考其他 library 的情況,沒有 intellisense ,寫起來會相當痛苦。(都已經用 Visual Studio 在寫程式了,還沒 Intellisense 的話,跟葉問不可以用腳一樣悲情)
    image
  3. Debug:在設計 T4 往往會有一些很難看的判斷式跟噁心的巢狀迴圈,這時候真的需要偵錯模式來輔助。不過這一套工具的 debug 似乎要 pro 的版本才有,這時候還是用一下 VS2012 內建的 debug 比較實際一些。不過我剛剛在 solution explorer 的 shortcut menu 中沒有找到 Debug T4 Template 的功能,所以我就直接去 VS2012 的 KeyBoard 設定中,找到 Debug T4 Template 的功能,設成快速鍵。
    image
    啟動 Debug T4 Template 後,就像一般程式的偵錯一樣:
    image
  4. 支援 Collapse 跟 Expand T4 logic 的部份:可以快速地了解 template 跟 logic 的全貌,如下圖所示。
    image
    image
    當然,針對 <# #> 可以縮起來,也是相當實用的。

 

如何取得 StringLength

回到文章前言中所描述的需求,我們希望可以從 Schema 中獲得長度限制,在 model 中針對宣告為 string type 的 property ,自動加上 StringLength 這個 ValidationAttribute 。

首先要釐清幾件事:

  1. 在 C# 中的 value type 已經可以直接跟 DB schema 的 column type 做對應,且長度限制會是 value type 本身的限制,所以我們要針對的 string type。
  2. 是否所有的 string type 都有長度限制?答案是:NO!因為針對 XML, ntext, nvarchar(MAX) 等等定義,基本上是沒有長度限制的。

有了以上的了解後,只需要找到在 schema 中定義的長度限制在哪,以及 type 的部份做處理即可。

註: 在我的環境中,有用 xml,但沒有 ntext 與 nvarchar(MAX) ,因此我只有針對 xml 處理

長度限制被定義在 syscolumns table 的 prec 欄位中,因此修改一下我們原本 T4 中的 SQL statement ,如下所示:

			SELECT  	c.name AS [column],    		
						cd.value AS [column_desc],    		
						c.isnullable AS [isNullable],
						c.prec, 
						n.name as [columnType]
			FROM    	sysobjects t WITH(nolock)			
			INNER JOIN  syscolumns c WITH(nolock)
				ON		c.id = t.id
			INNER JOIN  dbo.systypes n WITH(nolock) 
				ON c.xusertype = n.xusertype
			LEFT OUTER JOIN sys.extended_properties cd WITH(nolock)
				ON		cd.major_id = c.id
				AND		cd.minor_id = c.colid
				AND		cd.name = 'MS_Description'
			WHERE t.type = 'u'
			and t.name='@tableName'
			ORDER BY    t.name, c.colorder;

接著,透過迴圈產生每一個 property 的部份,將欄位長度限制定義出來,也將 columntype 讀出來,用來判定當 type 為 xml 時,就不加上 StringLength 的限制,並在 <summary> 中加上此欄位對應的 type 為 xml 。如下所示:

image

最後產生出來的 model 範例,請見下面的程式碼:

    /// <summary>			
    /// mapping table name: holiday
    /// </summary>
    public class holiday
    {
        /// <summary>
        /// 假日序號
        /// </summary>
        [DBColumnMapping("holiday_id")]
        public Int32 holiday_id { get; set; }

        /// <summary>
        /// 假日名稱
        /// </summary>
        [DBColumnMapping("holiday_name")]
        [StringLength(50)]
        public String holiday_name { get; set; }

        /// <summary>
        /// 假日日期
        /// </summary>
        [DBColumnMapping("holiday_date")]
        public DateTime holiday_date { get; set; }

        /// <summary>
        /// 備註
        /// </summary>
        [DBColumnMapping("holiday_note")]
        [StringLength(50)]
        public String holiday_note { get; set; }

        /// <summary>
        /// 實際建檔日期
        /// </summary>
        [DBColumnMapping("holiday_sysdate")]
        public DateTime holiday_sysdate { get; set; }

        /// <summary>
        /// 修改次數
        /// </summary>
        [DBColumnMapping("holiday_updated")]
        public Byte holiday_updated { get; set; }

        /// <summary>
        /// 最後修改日期
        /// </summary>
        [DBColumnMapping("holiday_updateddate")]
        public DateTime holiday_updateddate { get; set; }

        /// <summary>
        /// 最後修改人
        /// </summary>
        [DBColumnMapping("holiday_updateduser")]
        [StringLength(50)]
        public String holiday_updateduser { get; set; }
    }

完整的 T4 程式碼如下 (直接看肯定是很醜的…):

<#@ template language="C#" debug="True" hostspecific="True" #>
<#@ output extension=".cs" #>
<#@ assembly name="System.Data.DataSetExtensions" #>
<#@ assembly name="System.Core" #>
<#@ assembly name="System.Data" #>
<#@ assembly name="System.xml" #>
<#@ import namespace="System.Collections.Generic" #>
<#@ import namespace="System.Data.SqlClient" #>
<#@ import namespace="System.Data" #>
<#@ import namespace="System.Linq" #>

using System;
using System.ComponentModel.DataAnnotations;

namespace 你的namespace
{        
		<#  //修改connection string
			string connectionString = "你的connection string"; 			
			SqlConnection conn = new SqlConnection(connectionString); 
			conn.Open(); 
			
			var tableName = "你的table name";

			//如果需要database中全部table,則使用conn.GetSchema("Tables")即可
			string[] restrictions = new string[4];
			restrictions[1] = "dbo";
			//修改table名稱
			restrictions[2] = tableName;
			DataTable schema = conn.GetSchema("Tables", restrictions);
			
			string selectQuery = @"
			SELECT top 1 * from  @tableName WITH(nolock);

			SELECT  	c.name AS [column],    		
						cd.value AS [column_desc],    		
						c.isnullable AS [isNullable],
						c.prec, 
						n.name as [columnType]
			FROM    	sysobjects t WITH(nolock)			
			INNER JOIN  syscolumns c WITH(nolock)
				ON		c.id = t.id
			INNER JOIN  dbo.systypes n WITH(nolock) 
				ON c.xusertype = n.xusertype
			LEFT OUTER JOIN sys.extended_properties cd WITH(nolock)
				ON		cd.major_id = c.id
				AND		cd.minor_id = c.colid
				AND		cd.name = 'MS_Description'
			WHERE t.type = 'u'
			and t.name='@tableName'
			ORDER BY    t.name, c.colorder;

			SELECT	top 1 
					t.name AS [table_name],
					td.value AS [table_desc]
			FROM    	sysobjects t WITH(nolock)
			INNER JOIN sys.extended_properties td WITH(nolock)
				ON		td.major_id = t.id
				AND 	td.minor_id = 0
				AND		td.name = 'MS_Description'
			WHERE t.type = 'u'
			and t.name='@tableName';"; 

			SqlCommand command = new SqlCommand(selectQuery,conn); 
			SqlDataAdapter ad = new SqlDataAdapter(command); 
			System.Data.DataSet ds = new DataSet(); 						

			foreach(System.Data.DataRow row in schema.Rows) 
			{ 				
				command.CommandText = selectQuery.Replace("@tableName",row["TABLE_NAME"].ToString()); 
				ad.Fill(ds);

				var isExistData = ds.Tables[2].Rows.Count > 0 ;
				var tableDescription = isExistData ? ds.Tables[2].Rows[0]["table_desc"].ToString() : "";				
			#>             						
			/// <summary>			
<# if(!string.IsNullOrEmpty(tableDescription)){ #>			/// <#= tableDescription #> 
<# }#>
			/// mapping table name: <#= row["TABLE_NAME"].ToString() #>
			/// </summary>
			public class <#= row["TABLE_NAME"].ToString() #>
			{
				<#                 										
					foreach (DataColumn dc in ds.Tables[0].Columns)
					{					
						var columnDefinition = ds.Tables[1].AsEnumerable().Where(x => x["column"].ToString() == dc.ColumnName).FirstOrDefault();						
						var columnDescription = columnDefinition["column_desc"].ToString();
						var columnLength = columnDefinition["prec"].ToString();
						var columnType = columnDefinition["columnType"].ToString();
						var isXml = columnType == "xml" ;
						var isAllowNull = columnDefinition["isNullable"].ToString() == "1";
				#>
/// <summary>
				/// <#= columnDescription #>
<# if(isXml){ #>				/// 此欄為 xml 格式
<# }#>
				/// </summary>
				[DBColumnMapping("<#= dc.ColumnName #>")]
				<# if(!isXml && dc.DataType.Name == "String"){ #>[StringLength(<#= columnLength #>)]
				<# }#>
				<#
				if(isAllowNull && dc.DataType.Name != "String"){ #>public Nullable<<#= dc.DataType.Name #>> <#= dc.ColumnName #>  { get; set; }
				<#     }else{#>public <#= dc.DataType.Name #> <#= dc.ColumnName #> { get; set; }
				<#}#>                            	
				<#    }                 #>                                
			}                            
<#    
			}  
			conn.Close();			
#>
}

 

結論

透過 T4 自動產生這樣的 model ,當在設計 model validation 的 ValidationAttribute 時,希望預設限制跟 DB schema 相同時,就可以節省我們很多時間。

不需要再一一查詢 DB schema 的設定,手動刻 StringLength ,也可以有效率地自動產生與 table mapping 且帶有基本 ValidationAttribute 的 model ,讓 context 端可以直接結合 model validation solution ,避免不符合 validation rule 的資料,要等到新增或修改 DB 資料時,才由 DBMS 報出 exception 。

 

Reference

補上 Debug T4 Template 的參考:Tiny Happy Features #1 - T4 Template Debugging in Visual Studio 2012


blog 與課程更新內容,請前往新站位置:http://tdd.best/