[.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 一整個幫助很大:
-
Syntax-Highlighting:針對 template 與自己編寫的 code 會有不同的 highlight 顏色。讓你一目了然哪一些是 code template, 哪一些是 logic 。如下圖所示(雖然我的位置排的很醜,但這也是我還沒法突破 T4 的一點):

-
Intelli-Sense:這對一個 “modern” developer 來說,再重要不過了。尤其是在 T4 中還有參考其他 library 的情況,沒有 intellisense ,寫起來會相當痛苦。(都已經用 Visual Studio 在寫程式了,還沒 Intellisense 的話,跟葉問不可以用腳一樣悲情)

-
Debug:在設計 T4 往往會有一些很難看的判斷式跟噁心的巢狀迴圈,這時候真的需要偵錯模式來輔助。不過這一套工具的 debug 似乎要 pro 的版本才有,這時候還是用一下 VS2012 內建的 debug 比較實際一些。不過我剛剛在 solution explorer 的 shortcut menu 中沒有找到 Debug T4 Template 的功能,所以我就直接去 VS2012 的 KeyBoard 設定中,找到 Debug T4 Template 的功能,設成快速鍵。

啟動 Debug T4 Template 後,就像一般程式的偵錯一樣:

-
支援 Collapse 跟 Expand T4 logic 的部份:可以快速地了解 template 跟 logic 的全貌,如下圖所示。


當然,針對 <# #> 可以縮起來,也是相當實用的。
如何取得 StringLength
回到文章前言中所描述的需求,我們希望可以從 Schema 中獲得長度限制,在 model 中針對宣告為 string type 的 property ,自動加上 StringLength 這個 ValidationAttribute 。
首先要釐清幾件事:
- 在 C# 中的 value type 已經可以直接跟 DB schema 的 column type 做對應,且長度限制會是 value type 本身的限制,所以我們要針對的 string type。
- 是否所有的 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 。如下所示:
最後產生出來的 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/
