在前面的章节里,用户表的结构非常简单,没有控制如何映射到数据库。通常,需要对字段的长度、是否可为空甚至特定数据类型进行设置,因为EntityFramework的默认映射规则相对而言比较简单和通用。在这篇日志里,将演示如何对数据实体进行映射配置,并利用T4模板自动创建映射配置类文件。
配置方式
EntityFramework的实体映射配置有2种。
第一种是直接在实体类中以特性的方式进行控制,这些特性有部分是EF实现的,也有部分是非EF实现的。也就是说,在数据实体层不引用EF的情况下,只能使用不全的特性对实体映射进行控制。这种方式有2个明显的弊端:实体类没有尽可能地简单,数据实体层需要引用EF。
第二种是通过EF提供的特定接口进行控制。这种控制方式将完全通过EF提供的配置API,能保证数据实体的干净和独立,并且将映射配置独立出来,便于维护。
映射配置
毫无疑问,这里将采用第二种配置方式。
配置信息是要传递给EF数据上下文的,并且需要引用EF,因此实体配置可以直接建立在数据核心层(S.Framework.DataCore)中。在EntityFramework文件夹下建立新的文件夹,名称Configurations。
需要提醒的是,在数据实体中,我们通过“不同名称的一级文件夹”对不同数据库的实体进行了隔离,在创建EF数据库上下文时,也用同样的名称作为前缀。因此,在实现实体配置时我们也需要进行区分,区分的方式也是文件夹隔离——在Configurations文件夹下创建文件夹,名称Master。如下图:
现在开始创建用户实体配置。
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 using System.Threading.Tasks; 6 using System.Data.Entity.ModelConfiguration; 7 using System.Data.Entity.ModelConfiguration.Configuration; 8 9 using S.Framework.Entity.Master; 10 11 namespace S.Framework.DataCore.EntityFramework.Configurations.Master 12 { 13 /// <summary> 14 /// 数据表映射 15 /// </summary> 16 public class SysUserConfiguration : EntityTypeConfiguration<SysUser> 17 { 18 /// <summary> 19 /// 数据表映射构造函数 20 /// </summary> 21 public SysUserConfiguration() 22 { 23 SysUserConfigurationAppend(); 24 } 25 26 /// <summary> 27 /// 数据映射 28 /// </summary> 29 public void SysUserConfigurationAppend() 30 { 31 //设置ID属性非空、最大长度38 32 //由于EF会自动识别名为ID的属性作为主键(会自动非空),所以其实这里只需要设置最大长度即可 33 this.Property(p => p.ID).IsRequired().HasMaxLength(38); 34 35 //设置UserName属性非空、最大长度100 36 this.Property(p => p.UserName).IsRequired().HasMaxLength(100); 37 38 //设置Password属性非空、最大长度100 39 this.Property(p => p.Password).IsRequired().HasMaxLength(100); 40 41 //设置CreateUser属性非空、最大长度100 42 this.Property(p => p.CreateUser).IsRequired().HasMaxLength(100); 43 44 //设置LastModifyUser属性最大长度100 45 this.Property(p => p.LastModifyUser).HasMaxLength(100); 46 } 47 48 /// <summary> 49 /// 将映射配置注册给配置注册器 50 /// </summary> 51 /// <param name="configurations">配置注册器对象</param> 52 public void RegistTo(ConfigurationRegistrar configurations) 53 { 54 configurations.Add(this); 55 } 56 } 57 } 58
用户实体配置类
可配置的内容还有很多,比如数据精度、字段数据类型等等,请查API。
这里稍微提几条常见的EF对实体属性映射的默认规则,这里以Sql server为例。
值类型(int、bool等)的属性映射到数据库后,字段的数据类型是相应的int、bit等,且不允许为空;
引用类型(好像只有string?)的属性映射到数据库后,字段的数据类型是nvarchar(max),允许为空;
属性名为“ID”或者“实体类名ID”的属性,将自动被识别为主键;
注册配置
创建了实体配置类之后,还需要让EF数据库上下文知道有这个配置。
在MasterEntityContext中的OnModelCreating方法中追加,使该方法变成这样:
1 /// <summary> 2 /// 模型配置重写 3 /// </summary> 4 /// <param name="modelBuilder">数据实体生成器</param> 5 protected override void OnModelCreating(System.Data.Entity.DbModelBuilder modelBuilder) 6 { 7 // 禁用一对多级联删除 8 modelBuilder.Conventions.Remove<OneToManyCascadeDeleteConvention>(); 9 // 禁用多对多级联删除 10 modelBuilder.Conventions.Remove<ManyToManyCascadeDeleteConvention>(); 11 // 禁用表名自动复数规则 12 modelBuilder.Conventions.Remove<PluralizingTableNameConvention>(); 13 14 new SysUserConfiguration().RegistTo(modelBuilder.Configurations); 15 }
注册配置类给数据库上下文
一切就绪,可以再创建一次数据库看看效果。
删除原数据库,开启Home控制器下Index中的“测试访问数据库”的代码,运行。
新生成的数据库结构如下图:
可以看到,字段的信息发生了改变。
利用T4自动生成实体配置类
当实体数量过多时,为各个实体创建配置类就变成了体力活。我们可以利用T4模板来偷懒,让它自动创建实体配置里。
VS自带的T4模板,略有不足。请先安装2个关于T4的VS插件。
工具 => 扩展和更新,联机搜索Devart T4 Editor和T4 Toolbox for Visual Studio 2013。
需要注意的是,针对不同版本的VS,有不同版本的插件,请选择相应的版本。至于VS2015,听说没有这个插件,那我就不清楚了,还没用上VS2015。若有读者发现VS2015下这2个插件的变化还望留言告知。
在EntityFramework文件夹下创建文件夹,名称T4,如下图:
装了2个插件之后,添加新项时会增加一个新的类型“T4 Toolbox”,我们将用Template文件创建模板,再用Script文件创建执行器。此处,我们先创建一个用于表示“实体配置类”的模板。如下图:
模板代码如下:
1 <#+ 2 // <copyright file="Configuration.tt" company=""> 3 // Copyright © . All Rights Reserved. 4 // </copyright> 5 public class Configuration : CSharpTemplate 6 { 7 private string _modelName; //实体名称 8 private string _prefixName; //实体前缀名称,表示不同的数据库 9 public Configuration(string modelName, string prefixName) 10 { 11 _modelName = modelName; 12 _prefixName = prefixName; 13 } 14 public override string TransformText() 15 { 16 base.TransformText(); 17 #> 18 using System; 19 using System.Data.Entity.ModelConfiguration; 20 using System.Data.Entity.ModelConfiguration.Configuration; 21 using S.Framework.Entity.<#= _prefixName #>; 22 namespace S.Framework.DataCore.EntityFramework.Configurations.<#= _prefixName #> 23 { 24 /// <summary> 25 /// 数据表映射 26 /// </summary> 27 public class <#= _modelName #>Configuration : EntityTypeConfiguration<<#= _modelName #>> 28 { 29 /// <summary> 30 /// 数据表映射构造函数 31 /// </summary> 32 public <#= _modelName #>Configuration() 33 { 34 <#= _modelName #>ConfigurationAppend(); 35 } 36 /// <summary> 37 /// 数据映射 38 /// </summary> 39 public void <#= _modelName #>ConfigurationAppend() 40 { 41 } 42 /// <summary> 43 /// 将映射配置注册给配置注册器 44 /// </summary> 45 /// <param name="configurations">配置注册器对象</param> 46 public void RegistTo(ConfigurationRegistrar configurations) 47 { 48 configurations.Add(this); 49 } 50 } 51 } 52 <#+ 53 return this.GenerationEnvironment.ToString(); 54 } 55 } 56 #>
实体配置模板文件
T4不是重点,此处不做介绍,有兴趣的读者可以自行查阅资料学习。
模板只是表示“目标文件的格式规则是怎样的”,还需要有一个“以一定条件去执行模板”的生成器。上面已经提到过Script执行器文件,创建一个吧。
执行器文件与模板文件有个明显可视的区别是:
执行器文件可以展开。模板文件的使命就是定义结构和规则,只要定义完成,就没它什么事情了。我们需要关注的是如何去调用模板文件。此处不多展开,直接放出Exec.tt文件代码。
1 <#@ template language="C#" debug="True" #> 2 <#@ output extension="cs" #> 3 <#@ import namespace="System.IO" #> 4 <#@ import namespace="System.Text" #> 5 <#@ include file="T4Toolbox.tt" #> 6 <#@ include file="Configuration.tt" #> 7 <# 8 9 string coreName = "S.Framework", projectName = coreName + ".DataCore"; 10 //当前完整路径 11 string currentPath = Path.GetDirectoryName(Host.TemplateFile); 12 //T4文件夹的父级文件夹路径 13 string projectPath = currentPath.Substring(0, currentPath.IndexOf(@"\T4")); 14 //解决方案路径 15 string solutionFolderPath = currentPath.Substring(0, currentPath.IndexOf(@"\" + projectName)); 16 17 //实体名称和实体所在的数据库标识名称 18 string modelName= "SysUser", prefixName= "Master"; 19 //目标文件的路径和名称(嵌套Generate文件夹是为了标识T4生成的类文件) 20 string folderName= @"\Configurations\", fileName= prefixName + @"\Generate\" + modelName + "Configuration.cs"; 21 //执行实体配置模板,自动创建文件 22 Configuration configuration = new Configuration(modelName, prefixName); 23 configuration.Output.Encoding = Encoding.UTF8; 24 string path = projectPath + folderName + fileName; 25 configuration.RenderToFile(path); 26 #> 27
模板执行器文件
注意,这个文件只要保存就会自动执行,也可以通过右键来运行或调试。
在T4里写代码会有点不适应,智能提示、dll库等都是缺失得非常厉害,需要自己手动import相应的dll才行,有问题多调试,会很快搞定的。
执行Exec.tt之后,我们会发现一个新文件被自动创建了,如下图:
这个自动生成的配置类,按照模板中定义的那样,与底下那个手动创建的配置类是一致的。先不急删除“底下那个手动创建的配置类”,因为此时还有几个问题需要解决。
第一个问题:模板文件既然叫做模板,必然是通用于各实体类的。那么必然无法在模板中指定“每个字段的配置”,因为每个实体类的字段都不同的。这样就导致“自动生成的配置类中并不包含配置”,那怎么解决呢?
第二个问题:在模板执行器文件中,硬编码的方式写死了“要生成的目标实体的名称SysUser”,那岂不是需要在为每个实体生成配置类的时候都需要手动调整代码?
先解决第一个问题。C#中可以通过关键字partial来表示部分的意思,可以用在类中,也可以中在方法中。这个关键字就是解决第一个问题的方式。调整配置类的模板文件,修改2处代码。
把配置类的修饰符public改成partial,再把数据映射方法ConfigurationAppend修改成:
1 /// <summary> 2 /// 数据映射 3 /// </summary> 4 partial void <#= _modelName #>ConfigurationAppend();
修改后的数据映射方法
重新执行exec.tt。
这样,“T4生成的配置类”和“手动创建的配置类”就存在一部分重复的内容,重复的内容交给T4,在“手动创建的配置类”中只需关注不定的内容。修改“手动创建配置类”,只保留“部分方法即可”,如下:
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 using System.Threading.Tasks; 6 7 using S.Framework.Entity.Master; 8 9 namespace S.Framework.DataCore.EntityFramework.Configurations.Master 10 { 11 /// <summary> 12 /// 数据表映射 13 /// </summary> 14 partial class SysUserConfiguration 15 { 16 /// <summary> 17 /// 数据映射 18 /// </summary> 19 partial void SysUserConfigurationAppend() 20 { 21 //设置ID属性非空、最大长度38 22 //由于EF会自动识别名为ID的属性作为主键(会自动非空),所以其实这里只需要设置最大长度即可 23 this.Property(p => p.ID).IsRequired().HasMaxLength(38); 24 25 //设置UserName属性非空、最大长度100 26 this.Property(p => p.UserName).IsRequired().HasMaxLength(100); 27 28 //设置Password属性非空、最大长度100 29 this.Property(p => p.Password).IsRequired().HasMaxLength(100); 30 31 //设置CreateUser属性非空、最大长度100 32 this.Property(p => p.CreateUser).IsRequired().HasMaxLength(100); 33 34 //设置LastModifyUser属性最大长度100 35 this.Property(p => p.LastModifyUser).HasMaxLength(100); 36 } 37 } 38 } 39
手动创建的配置类
为了目录结构上的统一,我们把调整文件夹为如下结构:
需要注意的是:配置类的命名空间必须是S.Framework.DataCore.EntityFramework.Configurations.Master,否则关键字partial就没有相应的作用。
现在解决第二个问题。
我们用反射来解决这个问题。思路其实很简单,反射数据实体层,获取每个实体类的名称和数据库标识名称,循环执行配置模板文件。
为了能够准确表示“这个类是数据实体类”,我们添加一个“实体类必须要继承的实体基本类”,这样一来,只需要寻找“继承了实体基本类的类”就可以了。
同时再增加一个“实体通用属性公共类”,用来定义通用的属性,比如创建人、创建时间等字段,减少每个实体类的代码量。
这2个类,先定义在数据实体层根目录中,将来会再做调整。
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 using System.Threading.Tasks; 6 7 namespace S.Framework.Entity 8 { 9 /// <summary> 10 /// 所有实体模型必须直接或间接继承此类,T4模板中反射 Entity.dll 时将只识别此类的派生类为实体模型 11 /// </summary> 12 public abstract class EntityModelBaseForReflection 13 { 14 } 15 } 16
实体模型基本类
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 using System.Threading.Tasks; 6 7 namespace S.Framework.Entity 8 { 9 public abstract class EntityBaseModel : EntityModelBaseForReflection 10 { 11 /// <summary> 12 /// 获取或设置一个 <see cref="string"/> 值,该值表示实体对象的数据创建者。 13 /// </summary> 14 public virtual string CreateUser { get; set; } 15 16 /// <summary> 17 /// 获取或设置一个 <see cref="DateTime"/> 值,该值表示实体对象的数据创建时间。 18 /// </summary> 19 public virtual DateTime CreateDate { get; set; } 20 21 /// <summary> 22 /// 获取或设置一个 <see cref="string"/> 值,该值表示实体对象的数据最后修改者。 23 /// </summary> 24 public virtual string LastModifyUser { get; set; } 25 26 /// <summary> 27 /// 获取或设置一个 <see cref="DateTime"/> 值,该值表示实体对象的数据最后修改时间。 28 /// </summary> 29 public virtual DateTime? LastModifyDate { get; set; } 30 } 31 } 32
实体模型通用类
这2个类都加上了abstract,表示不能被实例化。
现在,让SysUser实体继承EntityBaseModel,并且删除SysUser中的创建人、创建时间、最后修改人、最后修改时间这4个属性。
然后修改Exec.tt,通过反射来获取实体信息。
1 <#@ template language="C#" debug="True" #> 2 <#@ assembly name="System.Core" #> 3 <#@ output extension="cs" #> 4 <#@ import namespace="System.IO" #> 5 <#@ import namespace="System.Text" #> 6 <#@ import namespace="System.Reflection" #> 7 <#@ import namespace="System.Linq" #> 8 <#@ import namespace="System.Collections.Generic" #> 9 <#@ include file="T4Toolbox.tt" #> 10 <#@ include file="Configuration.tt" #> 11 <# 12 13 string coreName = "S.Framework", projectName = coreName + ".DataCore", entityProjectName = coreName + ".Entity"; 14 string entityBaseModelName = entityProjectName + ".EntityBaseModel"; 15 string entityBaseModelNameForReflection = entityProjectName + ".EntityModelBaseForReflection"; 16 //当前完整路径 17 string currentPath = Path.GetDirectoryName(Host.TemplateFile); 18 //T4文件夹的父级文件夹路径 19 string projectPath = currentPath.Substring(0, currentPath.IndexOf(@"\T4")); 20 //解决方案路径 21 string solutionFolderPath = currentPath.Substring(0, currentPath.IndexOf(@"\" + projectName)); 22 23 //加载数据实体.dll 24 string entityFilePath = string.Concat(solutionFolderPath, ("\\"+ entityProjectName +"\\bin\\Debug\\" + entityProjectName + ".dll")); 25 byte[] fileData = File.ReadAllBytes(entityFilePath); 26 Assembly assembly = Assembly.Load(fileData); 27 //反射出实体类,不知道为啥此处不能成功判定“是否继承EntityModelBaseForReflection类” 28 //因此只能通过名称比较的方式来判定 29 IEnumerable<Type> modelTypes = assembly.GetTypes().Where(m => m.IsClass && !m.IsAbstract && (m.BaseType.FullName.Equals(entityBaseModelName) || m.BaseType.FullName.Equals(entityBaseModelNameForReflection))); 30 31 //循环实体类 32 List<string> prefixNames = new List<string>(); 33 foreach (Type item in modelTypes) 34 { 35 //找 实体文件夹 名称 36 string tempNamespace= item.Namespace, nameSpaceWithoutProjectName = tempNamespace.Substring(entityProjectName.Length); 37 if(nameSpaceWithoutProjectName.IndexOf(".") != 0 || nameSpaceWithoutProjectName.LastIndexOf(".") > 0) 38 { continue; } 39 40 //是否直接继承实体基本类 41 bool purity = item.BaseType.FullName.Equals(entityBaseModelNameForReflection); 42 //实体所在的数据库标识名称 43 string targetName = nameSpaceWithoutProjectName.Substring(1); 44 if(!prefixNames.Any(a => a == targetName)){ prefixNames.Add(targetName); } 45 //目标文件的路径和名称(嵌套Generate文件夹是为了标识T4生成的类文件) 46 string fileName= targetName + @"\Generate\" + item.Name + "Configuration.cs"; 47 48 //配置文件 49 string folderName= @"\Configurations\"; 50 Configuration configuration = new Configuration(item.Name, targetName); 51 configuration.Output.Encoding = Encoding.UTF8; 52 string path = projectPath + folderName + fileName; 53 configuration.RenderToFile(path); 54 } 55 #> 56
执行器文件
这里注意一下,由于是通过反射S.Framework.Entity/bin/下的S.Framework.Entity.dll来获取实体信息,因此当新增或移除实体,请先编译S.Framework.Entity,再来运行模板执行器。
既然有了“实体通用属性类EntityBaseModel”,那么就可以确定:继承于EntityBaseModel的实体,都需要配置“创建人、创建时间、最后修改人、最后修改时间”这4个字段。那也加到模板中吧,减少手动要写的代码。
修改Configuration.tt如下:
1 <#+ 2 // <copyright file="Configuration.tt" company=""> 3 // Copyright © . All Rights Reserved. 4 // </copyright> 5 6 public class Configuration : CSharpTemplate 7 { 8 private string _modelName; //实体名称 9 private string _prefixName; //实体前缀名称,表示不同的数据库 10 private bool _purity; //是否为纯净的实体,若为纯净则表示无任何额外属性,不纯净则表示包含“创建、最后修改”等额外属性 11 public Configuration(string modelName, string prefixName, bool purity) 12 { 13 _modelName = modelName; 14 _prefixName = prefixName; 15 _purity = purity; 16 } 17 18 public override string TransformText() 19 { 20 base.TransformText(); 21 #> 22 23 using System; 24 using System.Data.Entity.ModelConfiguration; 25 using System.Data.Entity.ModelConfiguration.Configuration; 26 27 using S.Framework.Entity.<#= _prefixName #>; 28 29 namespace S.Framework.DataCore.EntityFramework.Configurations.<#= _prefixName #> 30 { 31 /// <summary> 32 /// 数据表映射 33 /// </summary> 34 partial class <#= _modelName #>Configuration : EntityTypeConfiguration<<#= _modelName #>> 35 { 36 /// <summary> 37 /// 数据表映射构造函数 38 /// </summary> 39 public <#= _modelName #>Configuration() 40 { 41 <#= _modelName #>ConfigurationDefault(); 42 <#= _modelName #>ConfigurationAppend(); 43 } 44 45 /// <summary> 46 /// 默认的数据映射 47 /// </summary> 48 public void <#= _modelName #>ConfigurationDefault() 49 { 50 <#+ 51 if(!this._purity) 52 { 53 #> 54 this.Property(p => p.CreateUser).IsRequired().HasMaxLength(100); 55 56 this.Property(p => p.LastModifyUser).HasMaxLength(100); 57 <#+ 58 } 59 #> 60 } 61 62 /// <summary> 63 /// 数据映射 64 /// </summary> 65 partial void <#= _modelName #>ConfigurationAppend(); 66 67 /// <summary> 68 /// 将映射配置注册给配置注册器 69 /// </summary> 70 /// <param name="configurations">配置注册器对象</param> 71 public void RegistTo(ConfigurationRegistrar configurations) 72 { 73 configurations.Add(this); 74 } 75 } 76 } 77 <#+ 78 return this.GenerationEnvironment.ToString(); 79 } 80 } 81 #> 82
修改后的配置模板文件
同时,需要在Exec.tt中增加参数的传递。
将
1 Configuration configuration = new Configuration(item.Name, targetName);
修改为
1 Configuration configuration = new Configuration(item.Name, targetName, purity);
再运行Exec.tt,打开生成的用户配置类,检查正确与否。
验收成果
为了检验T4功能和配置功能,我们添加一个角色实体类。
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 using System.Threading.Tasks; 6 7 namespace S.Framework.Entity.Master 8 { 9 /// <summary> 10 /// 角色 11 /// </summary> 12 public class SysRole : EntityBaseModel 13 { 14 /// <summary> 15 /// 主键 16 /// </summary> 17 public string ID { get; set; } 18 19 /// <summary> 20 /// 名称 21 /// </summary> 22 public string Name { get; set; } 23 24 /// <summary> 25 /// 排序号 26 /// </summary> 27 public int SortNumber { get; set; } 28 } 29 } 30
角色实体
注意命名空间!别忘记删除“分类文件夹名System”。
编译S.Framework.Entity,然后去右键运行Exec.tt。得到以下结果:
如果要自定义一些配置,请在Customize文件夹中建立同名同命名空间的角色配置类,注意关键字partial。参照自定义用户配置类。
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 using System.Threading.Tasks; 6 7 namespace S.Framework.DataCore.EntityFramework.Configurations.Master 8 { 9 partial class SysRoleConfiguration 10 { 11 partial void SysRoleConfigurationAppend() 12 { 13 this.Property(p => p.ID).IsRequired().HasMaxLength(100); 14 15 this.Property(p => p.Name).IsRequired().HasMaxLength(100); 16 } 17 } 18 } 19
自定义角色配置类
现在把新实体关联到EF数据库上下文中,在MasterEntityContext中增加属性:
1 /// <summary> 2 /// 角色 3 /// </summary> 4 public DbSet<SysRole> Roles { get; set; }
同时在OnModelCreating方法中增加对角色配置类的注册:
1 new SysRoleConfiguration().RegistTo(modelBuilder.Configurations);
删除原数据库,重新跑一下项目,让EF再次创建数据库吧。
下一章节,将实现EntityFramework自动合并/迁移/数据初始化。