EF Code First:使用T4模板生成相似代碼
一、前言
經(jīng)過前面EF的《第一篇》與《第二篇》,我們的數(shù)據(jù)層功能已經(jīng)較為完善了,但有不少代碼相似度較高,比如負責實體映射的 EntityConfiguration,負責倉儲操作的IEntityRepository與EntityRepository。而且每添加一個實體類型,就要手動去添加一套相應的代碼,也是比較累的工作。如果能有一個根據(jù)實體類型自動生成這些相似度較高的代碼的解決方案,那將會減少大量的無聊的工作。
VS提供的“文本模板”(俗稱T4)功能,就是一個較好的解決方案。要添加一個實體類型,只要把實體類型定義好,然后運行一下定義好的T4模板,就可以自動生成相應的類文件。
二、工具準備
為了更好的使用 T4模板 功能,我們需要給VS安裝如下兩個插件:
- Devart T4 Editor:為VS提供智能提示功能。
- T4 Toolbox:在生成多文件時很有用。
三、T4代碼生成預熱
(一) 單文件生成:HelloWorld.cs
下面,我們先來體驗一個最簡單的T4代碼生成功能,輸出一個最簡單的類文件。
首先,在 GMF.Demo.Core.Data中 添加一個名為 T4 的文件夾,用于存放生成本工程內(nèi)的代碼的T4模板文件。并在其中添加一個名為 HelloWorld.tt的“文本模板”的項。
HelloWorld.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=".cs" #>
- using System;
- namespace GMF.Demo.Core.Data.T4
- {
- public class HelloWorld
- {
- private string _word;
- public HelloWorld(string word)
- {
- _word = word;
- }
- }
- }
直接保存文件(T4的生成將會在保存模板,模板失去焦點等情況下自動觸發(fā)生成。),將會在模板的當前位置生成一個同名的類文件:
HelloWorld.cs的內(nèi)容如下:
- using System;
- namespace GMF.Demo.Core.Data.T4
- {
- public class HelloWorld
- {
- private string _word;
- public HelloWorld(string word)
- {
- _word = word;
- }
- }
- }
這樣,我們的HelloWorld之旅就結(jié)束了,非常簡單。
(二) 多文件生成
當前位置方案的方案只能生成如下所示的代碼:
生成的文件會與T4模板在同一目錄中,這里就不詳述了,可以參考 蔣金楠 一個簡易版的T4代碼生成"框架" 。
本項目的多文件需要生成到指定文件夾中,但又想對T4模板進行統(tǒng)一的管理,T4文件夾里放置T4模板文件,但生成的映射文件EntityConfiguration將放置到文件夾Configurations中,倉儲操作的文件IEntityRepository與EntityRepository將放置到Repositories文件夾中。且生成的代碼文件應能自動的添加到解決方案中,而不是只是在文件夾中存在。
要實現(xiàn)此需求,一個簡單的辦法就是通過 T4 Toolbox 來進行生成。想了解 T4 Toolbox 的細節(jié),可以自己使用 ILSpy 來對 T4Toolbox.dll 文件進行反編譯來查看源代碼,如果是通過 T4 Toolbox 是通過VS的插件來安裝的,將在“C:\Users\Administrator\AppData\Local\Microsoft\VisualStudio\11.0\Extensions\dca4f0lt.jdx”文件夾中(我的機器上)。下面,我們直接進行多文件生成的演示。
首先,添加一個名為 HelloWorldTemplate.tt 的 T4 Toolbox 的代碼模板。
此模板將繼承于 T4 Toolbox 的 CSharpTemplate 類:
- <#+
- // <copyright file="HelloWorldTemplate.tt" company="郭明鋒@中國">
- // Copyright © 郭明鋒@中國. All Rights Reserved.
- // </copyright>
- public class HelloWorldTemplate : CSharpTemplate
- {
- private string _className;
- public HelloWorldTemplate(string className)
- {
- _className = className;
- }
- public override string TransformText()
- {
- #>
- using System;
- namespace GMF.Demo.Core.Data.T4
- {
- public class <#=_className #>
- {
- private string _word;
- public <#=_className #>(string word)
- {
- _word = word;
- }
- }
- }
- <#+
- return this.GenerationEnvironment.ToString();
- }
- }
- #>
模板類中定義了一個 className 參數(shù),用于接收一個表示要生成的類名的值。所有生成類的代碼都以字符串的形式寫在重寫的 TransformText 方法中。
再定義一個T4模板文件 HelloWorldMulti.tt,用于調(diào)用 上面定義的代碼模板進行代碼文件的生成。
- <#@ template debug="false" hostspecific="false" language="C#" #>
- <#@ assembly name="System.Core" #>
- <#@ import namespace="System.IO" #>
- <#@ import namespace="System.Linq" #>
- <#@ import namespace="System.Text" #>
- <#@ import namespace="System.Collections.Generic" #>
- <#@ include file="T4Toolbox.tt" #>
- <#@ include file="HelloWorldTemplate.tt" #>
- <#
- string curPath = Path.GetDirectoryName(Host.TemplateFile);
- string destPath = Path.Combine(curPath, "outPath");
- if(!Directory.Exists(destPath))
- {
- Directory.CreateDirectory(destPath);
- }
- string[] classNames = new[]{"HelloWorld1", "HelloWorld2", "HelloWorld3"};
- foreach(string className in classNames)
- {
- HelloWorldTemplate template = new HelloWorldTemplate(className);
- string fileName = string.Format(@"{0}\{1}.cs", destPath, className);
- template.Output.Encoding = Encoding.UTF8;
- template.RenderToFile(fileName);
- }
- #>
以上是整個T4模板的執(zhí)行方,在執(zhí)行方中,要引用所有需要用到的類庫文件,命名空間,包含的模板文件等。
最后,文件的生成是調(diào)用 T4 Toolbox 的Template基類中定義的 RenderToFile(string filename)方法來生成各個文件的,輸入的參數(shù)為生成文件的文件全名。在這里,生成將如下所示:
outPPath文件夾中生成了 HelloWorld1.cs、HelloWorld2.cs、HelloWorld3.cs 文件,而 HelloWorldMulti.tt 所在文件夾中也會生成一個空的 HelloWorldMulti.cs 類文件。
#p#
四、生成數(shù)據(jù)層實體相關相似代碼
(一) 生成準備
我們的生成代碼是完全依賴于業(yè)務實體的,所以,需要有一個類來對業(yè)務實體的信息進行提取封裝。
- namespace GMF.Component.Tools.T4
- {
- /// <summary>
- /// T4實體模型信息類
- /// </summary>
- public class T4ModelInfo
- {
- /// <summary>
- /// 獲取 模型所在模塊名稱
- /// </summary>
- public string ModuleName { get; private set; }
- /// <summary>
- /// 獲取 模型名稱
- /// </summary>
- public string Name { get; private set; }
- /// <summary>
- /// 獲取 模型描述
- /// </summary>
- public string Description { get; private set; }
- public IEnumerable<PropertyInfo> Properties { get; private set; }
- public T4ModelInfo(Type modelType)
- {
- var @namespace = modelType.Namespace;
- if (@namespace == null)
- {
- return;
- }
- var index = @namespace.LastIndexOf('.') + 1;
- ModuleName = @namespace.Substring(index, @namespace.Length - index);
- Name = modelType.Name;
- var descAttributes = modelType.GetCustomAttributes(typeof(DescriptionAttribute), true);
- Description = descAttributes.Length == 1 ? ((DescriptionAttribute)descAttributes[0]).Description : Name;
- Properties = modelType.GetProperties();
- }
- }
- }
另外,通過模板生成的代碼,與我們手寫的代碼有如下幾個區(qū)別:
- 手寫代碼可以自由定義,生成代碼是根據(jù)模板生成的,必然遵守模板定義的規(guī)范。
- 手寫代碼的修改可以永久保存,生成代碼的修改將會在下次生成后丟失,即生成代碼不應該進行修改。
基于以上幾個區(qū)別,我提出如下解決方案,來解決生成代碼的修改問題
- 通過模板生成的類應盡量定義為分部類(partial class),在需要進行修改及擴展的時候可以定義另外的同名分部類來進行修改。
- 對于需要外部實現(xiàn)的功能,定義分部方法來對外提供擴展。
- 生成代碼的類文件命名把擴展名由“.cs”更改為“generated.cs”,以區(qū)分生成代碼與手寫代碼文件。
(二) 生成實體相關相似代碼
1. 生成實體映射配置類
實體映射配置類模板 EntityConfigurationTemplate.tt 定義:
- <#+
- // <copyright file="EntityConfigurationTemplate.tt" company="郭明鋒@中國">
- // Copyright © 郭明鋒@中國. All Rights Reserved.
- // </copyright>
- public class EntityConfigurationTemplate : CSharpTemplate
- {
- private T4ModelInfo _model;
- public EntityConfigurationTemplate(T4ModelInfo model)
- {
- _model = model;
- }
- /// <summary>
- /// 獲取 生成的文件名,根據(jù)模型名定義
- /// </summary>
- public string FileName
- {
- get
- {
- return string.Format("{0}Configuration.generated.cs", _model.Name);
- }
- }
- public override string TransformText()
- {
- #>
- //------------------------------------------------------------------------------
- // <auto-generated>
- // 此代碼由工具生成。
- // 對此文件的更改可能會導致不正確的行為,并且如果
- // 重新生成代碼,這些更改將會丟失。
- // 如存在本生成代碼外的新需求,請在相同命名空間下創(chuàng)建同名分部類實現(xiàn) <#= _model.Name #>ConfigurationAppend 分部方法。
- // </auto-generated>
- //
- // <copyright file="<#= _model.Name #>Configuration.generated.cs">
- // Copyright(c)2013 GMFCN.All rights reserved.
- // CLR版本:4.0.30319.239
- // 開發(fā)組織:郭明鋒@中國
- // 公司網(wǎng)站:http://www.gmfcn.net
- // 所屬工程:GMF.Demo.Core.Data
- // 生成時間:<#= DateTime.Now.ToString("yyyy-MM-dd HH:mm") #>
- // </copyright>
- //------------------------------------------------------------------------------
- using System;
- using System.Data.Entity.ModelConfiguration;
- using System.Data.Entity.ModelConfiguration.Configuration;
- using GMF.Component.Data;
- using GMF.Demo.Core.Models;
- namespace GMF.Demo.Core.Data.Configurations
- {
- /// <summary>
- /// 實體類-數(shù)據(jù)表映射——<#= _model.Description #>
- /// </summary>
- internal partial class <#= _model.Name #>Configuration : EntityTypeConfiguration<<#= _model.Name #>>, IEntityMapper
- {
- /// <summary>
- /// 實體類-數(shù)據(jù)表映射構(gòu)造函數(shù)——<#= _model.Description #>
- /// </summary>
- public <#= _model.Name #>Configuration()
- {
- <#= _model.Name #>ConfigurationAppend();
- }
- /// <summary>
- /// 額外的數(shù)據(jù)映射
- /// </summary>
- partial void <#= _model.Name #>ConfigurationAppend();
- /// <summary>
- /// 將當前實體映射對象注冊到當前數(shù)據(jù)訪問上下文實體映射配置注冊器中
- /// </summary>
- /// <param name="configurations">實體映射配置注冊器</param>
- public void RegistTo(ConfigurationRegistrar configurations)
- {
- configurations.Add(this);
- }
- }
- }
- <#+
- return this.GenerationEnvironment.ToString();
- }
- }
- #>
生成模板調(diào)用方 EntityCodeScript.tt 定義
- <#@ template language="C#" debug="True" #>
- <#@ output extension="cs" #>
- <#@ Assembly Name="System.Core" #>
- <#@ Assembly Name="$(SolutionDir)\GMF.Component.Tools\bin\Debug\GMF.Component.Tools.dll" #>
- <#@ import namespace="System.IO" #>
- <#@ Import Namespace="System.Linq" #>
- <#@ Import Namespace="System.Text" #>
- <#@ import namespace="System.Reflection" #>
- <#@ Import Namespace="System.Collections.Generic" #>
- <#@ Import Namespace="GMF.Component.Tools" #>
- <#@ Import Namespace="GMF.Component.Tools.T4" #>
- <#@ include file="T4Toolbox.tt" #>
- <#@ include file="Include\EntityConfigurationTemplate.tt" #>
- <#
- string currentPath = Path.GetDirectoryName(Host.TemplateFile);
- string projectPath =currentPath.Substring(0, currentPath.IndexOf(@"\T4"));
- string solutionPath = currentPath.Substring(0, currentPath.IndexOf(@"\GMF.Demo.Core.Data"));
- string modelFile= Path.Combine(solutionPath, @"GMF.Demo.Core.Models\bin\Debug\GMF.Demo.Core.Models.dll");
- byte[] fileData= File.ReadAllBytes(modelFile);
- Assembly assembly = Assembly.Load(fileData);
- IEnumerable<Type> modelTypes = assembly.GetTypes().Where(m => typeof(Entity).IsAssignableFrom(m) && !m.IsAbstract);
- foreach(Type modelType in modelTypes)
- {
- T4ModelInfo model = new T4ModelInfo(modelType);
- //實體映射類
- EntityConfigurationTemplate config = new EntityConfigurationTemplate(model);
- string path = string.Format(@"{0}\Configurations", projectPath);
- config.Output.Encoding = Encoding.UTF8;
- config.RenderToFile(Path.Combine(path, config.FileName));
- }
- #>
調(diào)用方通過反射從業(yè)務實體程序集 GMF.Demo.Core.Models.dll 中獲取所有基類為 Entity 的并且不是抽象類的實體類型信息,再調(diào)用模板逐個生成實體配置類文件。
例如,生成的登錄記錄信息(LoginLog)的映射文件 LoginLogConfiguration.generated.cs 如下:
- //------------------------------------------------------------------------------
- // <auto-generated>
- // 此代碼由工具生成。
- // 對此文件的更改可能會導致不正確的行為,并且如果
- // 重新生成代碼,這些更改將會丟失。
- // 如存在本生成代碼外的新需求,請在相同命名空間下創(chuàng)建同名分部類實現(xiàn) LoginLogConfigurationAppend 分部方法。
- // </auto-generated>
- //
- // <copyright file="LoginLogConfiguration.generated.cs">
- // Copyright(c)2013 GMFCN.All rights reserved.
- // CLR版本:4.0.30319.239
- // 開發(fā)組織:郭明鋒@中國
- // 公司網(wǎng)站:http://www.gmfcn.net
- // 所屬工程:GMF.Demo.Core.Data
- // 生成時間:2013-06-16 17:45
- // </copyright>
- //------------------------------------------------------------------------------
- using System;
- using System.Data.Entity.ModelConfiguration;
- using System.Data.Entity.ModelConfiguration.Configuration;
- using GMF.Component.Data;
- using GMF.Demo.Core.Models;
- namespace GMF.Demo.Core.Data.Configurations
- {
- /// <summary>
- /// 實體類-數(shù)據(jù)表映射——登錄記錄信息
- /// </summary>
- internal partial class LoginLogConfiguration : EntityTypeConfiguration<LoginLog>, IEntityMapper
- {
- /// <summary>
- /// 實體類-數(shù)據(jù)表映射構(gòu)造函數(shù)——登錄記錄信息
- /// </summary>
- public LoginLogConfiguration()
- {
- LoginLogConfigurationAppend();
- }
- /// <summary>
- /// 額外的數(shù)據(jù)映射
- /// </summary>
- partial void LoginLogConfigurationAppend();
- /// <summary>
- /// 將當前實體映射對象注冊到當前數(shù)據(jù)訪問上下文實體映射配置注冊器中
- /// </summary>
- /// <param name="configurations">實體映射配置注冊器</param>
- public void RegistTo(ConfigurationRegistrar configurations)
- {
- configurations.Add(this);
- }
- }
- }
要配置登錄信息與用戶信息的 N:1 關系,只需要添加一個分部類 LoginLogConfiguration,并實現(xiàn)分類方法 LoginLogConfigurationAppend 即可。
- namespace GMF.Demo.Core.Data.Configurations
- {
- partial class LoginLogConfiguration
- {
- partial void LoginLogConfigurationAppend()
- {
- HasRequired(m => m.Member).WithMany(n => n.LoginLogs);
- }
- }
- }
#p#
2. 生成實體倉儲接口
實體映射配置類模板 EntityConfigurationTemplate.tt 定義:
- <#+
- // <copyright file="IEntityRepositoryTemplate.tt" company="郭明鋒@中國">
- // Copyright © 郭明鋒@中國. All Rights Reserved.
- // </copyright>
- public class IEntityRepositoryTemplate : CSharpTemplate
- {
- private T4ModelInfo _model;
- public IEntityRepositoryTemplate(T4ModelInfo model)
- {
- _model = model;
- }
- /// <summary>
- /// 獲取 生成的文件名,根據(jù)模型名定義
- /// </summary>
- public string FileName
- {
- get
- {
- return string.Format("I{0}Repository.generated.cs", _model.Name);
- }
- }
- public override string TransformText()
- {
- #>
- //------------------------------------------------------------------------------
- // <auto-generated>
- // 此代碼由工具生成。
- // 對此文件的更改可能會導致不正確的行為,并且如果
- // 重新生成代碼,這些更改將會丟失。
- // 如存在本生成代碼外的新需求,請在相同命名空間下創(chuàng)建同名分部類進行實現(xiàn)。
- // </auto-generated>
- //
- // <copyright file="I<#= _model.Name #>Repository.generated.cs">
- // Copyright(c)2013 GMFCN.All rights reserved.
- // CLR版本:4.0.30319.239
- // 開發(fā)組織:郭明鋒@中國
- // 公司網(wǎng)站:http://www.gmfcn.net
- // 所屬工程:GMF.Demo.Core.Data
- // 生成時間:<#= DateTime.Now.ToString("yyyy-MM-dd HH:mm") #>
- // </copyright>
- //------------------------------------------------------------------------------
- using System;
- using GMF.Component.Data;
- using GMF.Demo.Core.Models;
- namespace GMF.Demo.Core.Data.Repositories
- {
- /// <summary>
- /// 數(shù)據(jù)訪問層接口——<#= _model.Description #>
- /// </summary>
- public partial interface I<#= _model.Name #>Repository : IRepository<<#= _model.Name #>>
- { }
- }
- <#+
- return this.GenerationEnvironment.ToString();
- }
- }
- #>
相應的,在調(diào)用方 EntityCodeScript.tt 中添加模板調(diào)用代碼(如下 11-15 行所示):
- foreach(Type modelType in modelTypes)
- {
- T4ModelInfo model = new T4ModelInfo(modelType);
- //實體映射類
- EntityConfigurationTemplate config = new EntityConfigurationTemplate(model);
- string path = string.Format(@"{0}\Configurations", projectPath);
- config.Output.Encoding = Encoding.UTF8;
- config.RenderToFile(Path.Combine(path, config.FileName));
- //實體倉儲操作接口
- IEntityRepositoryTemplate irep= new IEntityRepositoryTemplate(model);
- path = string.Format(@"{0}\Repositories", projectPath);
- irep.Output.Encoding = Encoding.UTF8;
- irep.RenderToFile(Path.Combine(path, irep.FileName));
- }
生成的登錄記錄信息倉儲操作接口 ILoginLogRepository.generated.cs:
- //------------------------------------------------------------------------------
- // <auto-generated>
- // 此代碼由工具生成。
- // 對此文件的更改可能會導致不正確的行為,并且如果
- // 重新生成代碼,這些更改將會丟失。
- // 如存在本生成代碼外的新需求,請在相同命名空間下創(chuàng)建同名分部類進行實現(xiàn)。
- // </auto-generated>
- //
- // <copyright file="ILoginLogRepository.generated.cs">
- // Copyright(c)2013 GMFCN.All rights reserved.
- // CLR版本:4.0.30319.239
- // 開發(fā)組織:郭明鋒@中國
- // 公司網(wǎng)站:http://www.gmfcn.net
- // 所屬工程:GMF.Demo.Core.Data
- // 生成時間:2013-06-16 17:56
- // </copyright>
- //------------------------------------------------------------------------------
- using System;
- using GMF.Component.Data;
- using GMF.Demo.Core.Models;
- namespace GMF.Demo.Core.Data.Repositories
- {
- /// <summary>
- /// 數(shù)據(jù)訪問層接口——登錄記錄信息
- /// </summary>
- public partial interface ILoginLogRepository : IRepository<LoginLog>
- { }
- }
如果 IRepository<T> 中定義的倉儲操作不滿足登錄記錄倉儲操作的需求,只需要添加一個相應的分部接口,在其中進行需求的定義即可。這里當前沒有額外的需求,就不需要額外定義了。
3. 生成實體倉儲實現(xiàn)
實體倉儲操作的實現(xiàn)與倉儲操作接口類似,這里略過。
完成生成后,代碼結(jié)構(gòu)如下所示:
如果添加了或者刪除了一個實體,只需要重新運行一下調(diào)用模板 EntityCodeScript.tt 即可重新生成新的代碼。
五、源碼獲取
為了讓大家能第一時間獲取到本架構(gòu)的最新代碼,也為了方便我對代碼的管理,本系列的源碼已加入微軟的開源項目網(wǎng)站 http://www.codeplex.com,地址為: