为什么要有工作单元
在EF中,对实体集合的添加、更新和删除操作都只是内存级别的处理,需要通过db.SaveChanges()来执行数据库更新。这样就可以在SaveChanges之前对不同实体集合进行不同的操作,最后一次性提交到数据库,以保证事务的嵌套(SaveChanges时会自动嵌套事务)。举例来说,EF中可以这样操作db:
1 var db1 = new MasterEntityContext("matrixkey"); 2 db1.Users.Add(user);//新增用户 3 db1.Roles.Add(role);//新增角色 4 5 var the = db1.Users.Find(3); 6 the.UserName = "test99";//更新用户 7 8 db1.SaveChanges();//db1一次性提交,EF会自动嵌套事务 9 10 var db2 = new MasterEntityContext("matrixkey"); 11 db2.Set<SysUser>().Attach(the); //把 the 强行附加给db2 12 13 db2.SaveChanges();//db2一次性提交,会报错。因为the是被关联在db1中的,无法通过db2来更新。
注意,常规情况下,通过db1获取的实体对象,也得通过db1去提交。
在目前的实体仓储里,每个仓储都自带一个db对象。当你初始化SysUserRepository,会调用父类BaseRepository的构造方法,而该构造方法中,就创建了自己的db对象。
1 public BaseRepository() 2 { 3 this.Db = new MasterEntityContext(S.Framework.DatabaseConfig.ConnectionStringName); 4 }
通过该仓储对象进行的Add、Update、Remove、Select操作,都是对this.Db的操作。
如果在Add方法中的末尾,直接调用this.Db.SaveChanges(),就不能多种操作一次提交了。可如果不在末尾加上SaveChanges,又该如何实现“对多种仓储的操作一次性提交”呢?换个问法就是以下代码:
1 var repUser = new SysUserRepository(); 2 var repRole = new SysRoleRepository(); 3 4 //此处仅作演示,忽略实体数据 5 //仓储中封装的Add方法,仅仅是对内存的操作,并不会调用SaveChanges 6 repUser.Add(new SysUser { }); 7 repRole.Add(new SysRole { }); 8 9 //怎么让以上操作在一次提交里完成?
这里有2个问题需要解决:
设计实体仓储的时候就提过“不暴露数据库上下文(db)”,那怎么在不暴露db的情况下,保证操作的事务一致?
即使暴露了db,不同仓储中的db也是相互独立的,那怎么让多个仓储共用一个db?
于是我们需要这样一个东西,首先其内部包装了一个db,然后还通过该db实现了对所有实体仓储的调用,最后封装了一次性提交的方法(内部其实就是调用db的SaveChanges)。通过这个东西,就变成“初始化工作单元对象 => 调用工作单元中的实体仓储对象中的方法 => 工作单元一次性提交”的操作方式,就完全解决了上述2个问题。
这个东西就是工作单元。
基于多数据库的工作单元设计
上面提到,工作单元是来保证事务一致性的,那么其核心自然也是事务的3个大点:开启事务、执行、回滚。因此工作单元接口中只需要定义开启事务、执行、回滚这3个方法即可。
由于数据实体的设计是支持多数据库的,因此工作单元的设计也要支持多数据库。即使不支持跨数据库提交时嵌套事务,也应该从语法糖层面上实现。从语法糖层面实现的意思是,代码要能够这样写:
工作单元对象.数据库标识(其实是该数据库的实体仓储对象工厂).实体仓储对象.方法/属性
这样一来,要做的事情就比较清晰:
1、以数据库为单位,实现该数据库下实体仓储对象工厂,该工厂可用于调用实体仓储对象
2、创建工作单元实现类,应继承工作单元接口
3、在工作单元实现中,定义各数据库的db
4、实现工作单元接口中定义的3个方法
5、在实体仓储中,定义所属工作单元对象,以保证“一个工作单元对象下的实体仓储对象共用一个db(该db定义在工作单元对象中)”
6、实体仓储中构造方法重新调整,因为不需要初始化db
工作单元接口
在数据接口层(S.Framework.DataInterface)根目录创建工作单元接口,名称IUnitOfWork,代码如下:
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.DataInterface 8 { 9 /// <summary> 10 /// 工作单元接口 11 /// </summary> 12 public partial interface IUnitOfWork : IDisposable 13 { 14 /// <summary> 15 /// 开始数据事务 16 /// </summary> 17 void BeginTransaction(); 18 19 /// <summary> 20 /// 提交工作单元 21 /// </summary> 22 /// <returns>受影响行数</returns> 23 int Commit(); 24 25 /// <summary> 26 /// 执行回滚事务 27 /// </summary> 28 void Rollback(); 29 } 30 } 31
工作单元初步实现
在数据实现层(S.Framework.DataAchieve)的EntityFramework文件夹下建立工作单元实现类,继承工作单元接口,代码如下:
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.DataInterface; 8 using S.Utilities; 9 10 namespace S.Framework.DataAchieve.EntityFramework 11 { 12 public partial class UnitOfWork : IUnitOfWork 13 { 14 /// <summary> 15 /// 当前工作单元中 Master 数据库连接字符串 16 /// </summary> 17 internal string MasterConnString 18 { 19 get; 20 private set; 21 } 22 23 /// <summary> 24 /// 当前工作单元中 Master 数据库提供程序名称 25 /// </summary> 26 internal string MasterProviderName 27 { 28 get; 29 private set; 30 } 31 32 private System.Data.Entity.DbContext _dbMaster; 33 34 /// <summary> 35 /// 当前工作单元中 Master 数据库上下文 36 /// </summary> 37 internal System.Data.Entity.DbContext DbMaster 38 { 39 get 40 { 41 if (!this.MasterDbIsExist) 42 { 43 this._dbMaster = new S.Framework.DataCore.EntityFramework.EntityContexts.MasterEntityContext(this.MasterConnString); 44 } 45 return this._dbMaster; 46 } 47 } 48 49 /// <summary> 50 /// Master 数据库上下文是否存在 51 /// </summary> 52 private bool MasterDbIsExist { get { return this._dbMaster != null; } } 53 54 #region 构造函数 55 56 /// <summary> 57 /// 构造函数 58 /// <param name="connectionStringNames">数据库连接字符串名称集合,Key表示数据库标识名称,Value表示数据库连接字符串名称</param> 59 /// </summary> 60 public UnitOfWork(Dictionary<string, string> connectionStringNames) 61 { 62 if (connectionStringNames.IsNullOrEmpty()) 63 { 64 throw ExceptionHelper.ThrowDataAccessException("初始化工作单元对象时发生异常。", new ArgumentException("数据库连接信息集合参数不可为空。")); 65 } 66 if (connectionStringNames.ContainsKey("Master")) 67 { 68 var name = connectionStringNames["Master"]; 69 string connectionString = ConfigHelper.ConnectionString(name); 70 string providerName = ConfigHelper.ProviderName(name); 71 72 if (string.IsNullOrWhiteSpace(connectionString) || string.IsNullOrWhiteSpace(providerName)) 73 { throw ExceptionHelper.ThrowDataAccessException("初始化工作单元对象时发生异常。", new ArgumentException(name + "数据库连接信息有误。")); } 74 75 this.MasterConnString = connectionString; 76 this.MasterProviderName = providerName; 77 } 78 } 79 80 #endregion 81 82 /// <summary> 83 /// 开始数据事务 84 /// </summary> 85 public void BeginTransaction() 86 { } 87 88 /// <summary> 89 /// 提交工作单元 90 /// </summary> 91 /// <returns>受影响行数</returns> 92 public int Commit() 93 { return 0; } 94 95 /// <summary> 96 /// 执行回滚事务 97 /// </summary> 98 public void Rollback() 99 { } 100 } 101 } 102
此时的工作单元还比较简单,既没有真的实现接口定义的方法,也没有实现IDispose的资源释放方法,更没有考虑“多数据库情况下自动增补属性”,这些都将在最后完善。
实体仓储对象工厂接口
该工厂只有2个作用。第一个是能够通过工厂来调用当前所指数据的各实体仓储对象,第二个是传递工作单元对象。其中第二个作用的意思是指,定义在工作单元中的实体仓储对象工厂,将当前工作单元对象,存储在自定义属性中,并在初始化各实体仓储对象时再次将“当前所属工作单元对象”传给实体仓储对象,以保证这些实体仓储对象公用一个工作单元(这样才能保证共用相同的db)。
根据以上描述,可以拆分接口为“通用规范接口”和“自定义规范接口”,并且后者继承前者。其中通用规范就是指该工厂的第二个作用。
在数据接口层(SF.Framework.DataInterface)定义仓储工厂接口,建立目录结构如下图:
其中,IBaseRepositoryFactory接口如下:
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.DataInterface.IRepositoryFactories 8 { 9 public interface IBaseRepositoryFactory 10 { 11 /// <summary> 12 /// 设置工作单元对象。本方法设置了编辑器不可见,但只有跨解决方案时才有用。当前解决方案下,还是会被智能提示捕捉到。 13 /// <param name="unit">工作单元对象</param> 14 /// </summary> 15 [System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)] 16 void SetUnitOfWork(object unit); 17 } 18 } 19
而IMasterIRepositoryFactory接口,内部只需定义实体仓储作为属性而已。
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 using S.Framework.DataInterface.IRepositories.Master; 9 10 namespace S.Framework.DataInterface.IRepositoryFactories 11 { 12 /// <summary> 13 /// 仓储工厂接口 14 /// </summary> 15 public interface IMasterIRepositoryFactory : IBaseRepositoryFactory 16 { 17 /// <summary> 18 /// SysUser 仓储接口 19 /// </summary> 20 ISysUserRepository SysUser { get; } 21 /// <summary> 22 /// SysRole 仓储接口 23 /// </summary> 24 ISysRoleRepository SysRole { get; } 25 } 26 } 27
然而写完后就会发现,数据库标识Master可以反射出来,每个实体名称也可以反射出来,所以应该交给T4模板来自动生成。模板文件如下:
1 <#+ 2 // <copyright file="IRepositoryFactory.tt" company=""> 3 // Copyright © . All Rights Reserved. 4 // </copyright> 5 6 public class IRepositoryFactory : CSharpTemplate 7 { 8 9 private string _prefixName; 10 private List<Type> _typeList; 11 12 public IRepositoryFactory(string prefixName, List<Type> typeList) 13 { 14 _prefixName = prefixName; 15 _typeList = typeList; 16 } 17 18 public override string TransformText() 19 { 20 base.TransformText(); 21 #> 22 using System; 23 using System.Collections.Generic; 24 using System.Linq; 25 using System.Text; 26 27 using S.Framework.Entity.<#= _prefixName #>; 28 using S.Framework.DataInterface.IRepositories.<#= _prefixName #>; 29 30 namespace S.Framework.DataInterface.IRepositoryFactories 31 { 32 /// <summary> 33 /// 仓储工厂接口 34 /// </summary> 35 public partial interface I<#= _prefixName #>IRepositoryFactory : IBaseRepositoryFactory 36 { 37 #region 生成仓储实例 38 39 <#+ 40 foreach(Type item in _typeList) 41 { 42 #> 43 /// <summary> 44 /// <#= item.Name #> 仓储接口 45 /// </summary> 46 I<#= item.Name #>Repository <#= item.Name #> { get; } 47 <#+ 48 } 49 #> 50 51 #endregion 52 } 53 } 54 <#+ 55 return this.GenerationEnvironment.ToString(); 56 } 57 } 58 #> 59
修改执行器文件,其最终代码如下:
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="IRepository.tt" #> 11 <#@ include file="IRepositoryFactory.tt" #> 12 <# 13 14 string coreName = "S.Framework", projectName = coreName + ".DataInterface", entityProjectName = coreName + ".Entity"; 15 string entityBaseModelName = entityProjectName + ".EntityBaseModel"; 16 string entityBaseModelNameForReflection = entityProjectName + ".EntityModelBaseForReflection"; 17 //当前完整路径 18 string currentPath = Path.GetDirectoryName(Host.TemplateFile); 19 //T4文件夹的父级文件夹路径 20 string projectPath = currentPath.Substring(0, currentPath.IndexOf(@"T4")); 21 //解决方案路径 22 string solutionFolderPath = currentPath.Substring(0, currentPath.IndexOf(@"" + projectName)); 23 24 //加载数据实体.dll 25 string entityFilePath = string.Concat(solutionFolderPath, ("\"+ entityProjectName +"\bin\Debug\" + entityProjectName + ".dll")); 26 byte[] fileData = File.ReadAllBytes(entityFilePath); 27 Assembly assembly = Assembly.Load(fileData); 28 //反射出实体类,不知道为啥此处不能成功判定“是否继承EntityModelBaseForReflection类” 29 //因此只能通过名称比较的方式来判定 30 IEnumerable<Type> modelTypes = assembly.GetTypes().Where(m => m.IsClass && !m.IsAbstract && (m.BaseType.FullName.Equals(entityBaseModelName) || m.BaseType.FullName.Equals(entityBaseModelNameForReflection))); 31 32 //循环实体类 33 Dictionary<string, List<Type>> prefixModelTypes = new Dictionary<string, List<Type>>();//存储[数据库标识名称]和[实体类型集合] 34 foreach (Type item in modelTypes) 35 { 36 //找 实体文件夹 名称 37 string tempNamespace= item.Namespace, nameSpaceWithoutProjectName = tempNamespace.Substring(entityProjectName.Length); 38 if(nameSpaceWithoutProjectName.IndexOf(".") != 0 || nameSpaceWithoutProjectName.LastIndexOf(".") > 0) 39 { continue; } 40 41 //是否直接继承实体基本类 42 bool purity = item.BaseType.FullName.Equals(entityBaseModelNameForReflection); 43 //实体所在的数据库标识名称 44 string targetName = nameSpaceWithoutProjectName.Substring(1); 45 List<Type> temp; 46 if(prefixModelTypes.TryGetValue(targetName, out temp)) 47 { 48 temp.Add(item); 49 } 50 else 51 { 52 temp = new List<Type>{ item }; 53 prefixModelTypes.Add(targetName, temp); 54 } 55 56 //目标文件的路径和名称(嵌套Generate文件夹是为了标识T4生成的类文件) 57 string fileName= targetName + @"GenerateI" + item.Name + "Repository.cs"; 58 //仓储文件 59 string folderName= @"IRepositories"; 60 IRepository irepository = new IRepository(item.Name, targetName); 61 irepository.Output.Encoding = Encoding.UTF8; 62 string path = projectPath + folderName + fileName; 63 irepository.RenderToFile(path); 64 } 65 66 foreach(KeyValuePair<string, List<Type>> item in prefixModelTypes) 67 { 68 //实体仓储工厂接口文件 69 string fileName = "I" + item.Key + "IRepositoryFactory.Generate.cs"; 70 IRepositoryFactory iRepositoryFactory = new IRepositoryFactory(item.Key, item.Value); 71 iRepositoryFactory.Output.Encoding = Encoding.UTF8; 72 string path = string.Format(@"{0}IRepositoryFactories", projectPath) + fileName; 73 iRepositoryFactory.RenderToFile(path); 74 } 75 76 #> 77
运行一下执行器。删除原先手动创建的Master接口。最后文档结构如下:
实体仓储工厂实现
在数据实现层(S.Framework.DataAchieve)建立文档结构如下图:
其中BaseRepositoryFactory只管实现SetUnitOfWork即可,代码如下:
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.DataInterface.IRepositoryFactories; 8 using S.Utilities; 9 10 namespace S.Framework.DataAchieve.EntityFramework.RepositoryFactories 11 { 12 public abstract class BaseRepositoryFactory : IBaseRepositoryFactory 13 { 14 /// <summary> 15 /// 当前仓储工厂所在的工作单元对象 16 /// </summary> 17 protected UnitOfWork UnitOfWork { get; set; } 18 19 /// <summary> 20 /// 设置工作单元对象 21 /// <param name="unit">工作单元对象</param> 22 /// </summary> 23 public void SetUnitOfWork(object unit) 24 { 25 if (unit is UnitOfWork) { this.UnitOfWork = unit as UnitOfWork; } 26 else 27 { 28 throw ExceptionHelper.ThrowDataAccessException("给仓储工厂设置工作单元时发生异常,参数 unit 不是一个工作单元对象。"); 29 } 30 } 31 } 32 } 33
然后在实现MasterIRepositoryFactory的时候,会发现需要调整BaseRepository,原因如下图:
为了解决这个问题,有2种方式。第一种是给BaseRepository和所有实体Repository都实现带unit参数的构造方法,第二种是在BaseRepository中也定义一个SetUnitOfWork方法。从设计上来说,后者更符合后期需要,因此我们以第二种方式实现(具体原因后面会讲到)。
在IBaseRepository接口中定义SetUnitOfWork方法:
1 /// <summary> 2 /// 设置工作单元对象。设置了编辑器不可见,但只有跨解决方案时才有用。当前解决方案下,还是会被智能提示捕捉到。 3 /// <param name="unit">工作单元对象</param> 4 /// </summary> 5 [System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)] 6 void SetUnitOfWork(object unit);
然后,可以先不实现该方法,直接去MasterIRepositoryFactory中调整代码。
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.DataInterface.IRepositoryFactories; 8 using S.Framework.DataInterface.IRepositories.Master; 9 using S.Framework.DataAchieve.EntityFramework.Repositories.Master; 10 11 namespace S.Framework.DataAchieve.EntityFramework.RepositoryFactories 12 { 13 public class MasterIRepositoryFactory : BaseRepositoryFactory, IMasterIRepositoryFactory 14 { 15 /// <summary> 16 /// SysRole 仓储接口 17 /// </summary> 18 public ISysRoleRepository SysRole 19 { 20 get 21 { 22 ISysRoleRepository rep = new SysRoleRepository(); 23 rep.SetUnitOfWork(this.UnitOfWork); 24 25 return rep; 26 } 27 } 28 /// <summary> 29 /// SysUser 仓储接口 30 /// </summary> 31 public ISysUserRepository SysUser 32 { 33 get 34 { 35 ISysUserRepository rep = new SysUserRepository(); 36 rep.SetUnitOfWork(this.UnitOfWork); 37 38 return rep; 39 } 40 } 41 } 42 } 43
这样,Master数据库下的仓储对象工厂实现就算完成,至于IBaseRepository接口中定义SetUnitOfWork方法的实现,是另外一回事,等会再完善。
但以上仅仅是Master数据库的仓储对象工厂,如果存在多个数据库,难道还需要手动实现另外一个类吗?当然不用。接口用T4模板自动生成,其实现当然也可以这样。模板文件如下:
1 <#+ 2 // <copyright file="RepositoryFactories.tt" company=""> 3 // Copyright © . All Rights Reserved. 4 // </copyright> 5 6 public class RepositoryFactories : CSharpTemplate 7 { 8 9 private string _prefixName; 10 private IEnumerable<Type> _typeList; 11 12 public RepositoryFactories(string prefixName, IEnumerable<Type> typeList) 13 { 14 this._prefixName = prefixName; 15 this._typeList = typeList; 16 } 17 18 public override string TransformText() 19 { 20 base.TransformText(); 21 #> 22 using System; 23 using System.Collections.Generic; 24 using System.Linq; 25 using System.Text; 26 using System.Threading.Tasks; 27 28 using S.Framework.Entity.<#= _prefixName #>; 29 using S.Framework.DataInterface.IRepositories.<#= _prefixName #>; 30 using S.Framework.DataInterface.IRepositoryFactories; 31 using S.Framework.DataAchieve.EntityFramework.Repositories.<#= _prefixName #>; 32 33 namespace S.Framework.DataAchieve.EntityFramework.RepositoryFactories 34 { 35 public class <#= _prefixName #>IRepositoryFactory : BaseRepositoryFactory, I<#= _prefixName #>IRepositoryFactory 36 { 37 #region 仓储对象 38 39 <#+ 40 foreach(Type item in _typeList) 41 { 42 #> 43 /// <summary> 44 /// <#= item.Name #> 仓储接口 45 /// </summary> 46 public I<#= item.Name #>Repository <#= item.Name #> 47 { 48 get 49 { 50 I<#= item.Name #>Repository rep = new <#= item.Name #>Repository(); 51 rep.SetUnitOfWork(this.UnitOfWork); 52 53 return rep; 54 } 55 } 56 <#+ 57 } 58 #> 59 60 #endregion 61 } 62 } 63 <#+ 64 return this.GenerationEnvironment.ToString(); 65 } 66 } 67 #> 68
修改执行器文件,其最终代码如下:
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="Repository.tt" #> 11 <#@ include file="MigrateConfiguration.tt" #> 12 <#@ include file="DatabaseInitializer.tt" #> 13 <#@ include file="RepositoryFactories.tt" #> 14 <# 15 16 string coreName = "S.Framework", projectName = coreName + ".DataAchieve", entityProjectName = coreName + ".Entity"; 17 string entityBaseModelName = entityProjectName + ".EntityBaseModel"; 18 string entityBaseModelNameForReflection = entityProjectName + ".EntityModelBaseForReflection"; 19 //当前完整路径 20 string currentPath = Path.GetDirectoryName(Host.TemplateFile); 21 //T4文件夹的父级文件夹路径 22 string projectPath = currentPath.Substring(0, currentPath.IndexOf(@"T4")); 23 //解决方案路径 24 string solutionFolderPath = currentPath.Substring(0, currentPath.IndexOf(@"" + projectName)); 25 26 //加载数据实体.dll 27 string entityFilePath = string.Concat(solutionFolderPath, ("\"+ entityProjectName +"\bin\Debug\" + entityProjectName + ".dll")); 28 byte[] fileData = File.ReadAllBytes(entityFilePath); 29 Assembly assembly = Assembly.Load(fileData); 30 //反射出实体类,不知道为啥此处不能成功判定“是否继承EntityModelBaseForReflection类” 31 //因此只能通过名称比较的方式来判定 32 IEnumerable<Type> modelTypes = assembly.GetTypes().Where(m => m.IsClass && !m.IsAbstract && (m.BaseType.FullName.Equals(entityBaseModelName) || m.BaseType.FullName.Equals(entityBaseModelNameForReflection))); 33 34 //循环实体类 35 Dictionary<string, List<Type>> prefixModelTypes = new Dictionary<string, List<Type>>();//存储[数据库标识名称]和[实体类型集合] 36 foreach (Type item in modelTypes) 37 { 38 //找 实体文件夹 名称 39 string tempNamespace= item.Namespace, nameSpaceWithoutProjectName = tempNamespace.Substring(entityProjectName.Length); 40 if(nameSpaceWithoutProjectName.IndexOf(".") != 0 || nameSpaceWithoutProjectName.LastIndexOf(".") > 0) 41 { continue; } 42 43 //是否直接继承实体基本类 44 bool purity = item.BaseType.FullName.Equals(entityBaseModelNameForReflection); 45 //实体所在的数据库标识名称 46 string targetName = nameSpaceWithoutProjectName.Substring(1); 47 List<Type> temp; 48 if(prefixModelTypes.TryGetValue(targetName, out temp)) 49 { 50 temp.Add(item); 51 } 52 else 53 { 54 temp = new List<Type>{ item }; 55 prefixModelTypes.Add(targetName, temp); 56 } 57 58 //目标文件的路径和名称(嵌套Generate文件夹是为了标识T4生成的类文件) 59 string fileName= targetName + @"Generate" + item.Name + "Repository.cs"; 60 //仓储文件 61 string folderName= @"Repositories"; 62 Repository repository = new Repository(item.Name, targetName); 63 repository.Output.Encoding = Encoding.UTF8; 64 string path = projectPath + folderName + fileName; 65 repository.RenderToFile(path); 66 } 67 68 foreach(KeyValuePair<string, List<Type>> item in prefixModelTypes) 69 { 70 //数据库初始化策略配置文件 71 string fileName = "/"+ item.Key +"/" + item.Key + "MigrateConfiguration.Generate.cs"; 72 MigrateConfiguration mc = new MigrateConfiguration(item.Key); 73 mc.Output.Encoding = Encoding.UTF8; 74 string path = string.Format(@"{0}Initializes", projectPath) + fileName; 75 mc.RenderToFile(path); 76 77 //数据库初始化类文件 78 fileName = "/"+ item.Key +"/" + item.Key + "DatabaseInitializer.Generate.cs"; 79 DatabaseInitializer temp = new DatabaseInitializer(item.Key); 80 temp.Output.Encoding = Encoding.UTF8; 81 path = string.Format(@"{0}Initializes", projectPath) + fileName; 82 temp.RenderToFile(path); 83 84 //各仓储工厂实现类文件 85 fileName = item.Key + "IRepositoryFactory.Generate.cs"; 86 RepositoryFactories repositoryFactories = new RepositoryFactories(item.Key, item.Value); 87 repositoryFactories.Output.Encoding = Encoding.UTF8; 88 path = string.Format(@"{0}RepositoryFactories", projectPath) + fileName; 89 repositoryFactories.RenderToFile(path); 90 } 91 #> 92
运行一下执行器,最终文档结构如下图:
工作单元实现的部分完善
在初步实现工作单元时就提过未实现的内容,其中有一个是没有考虑“多数据库情况下自动增补属性”。意思是当存在多个数据库标识时,工作单元实现类中应该以数据库为单位,定义不同的Db、不同的数据库连接字符串和数据库提供程序名称。
很明显,此处关键是数据库标识。因此也利用T4模板来生成吧,但需要把工作单元类文件一分为二。
创建工作单元模板,代码如下:
1 <#+ 2 // <copyright file="UnitOfWork.tt" company=""> 3 // Copyright © . All Rights Reserved. 4 // </copyright> 5 6 public class UnitOfWork : CSharpTemplate 7 { 8 9 private IEnumerable<string> _prefixNameList; 10 11 public UnitOfWork(IEnumerable<string> prefixNameList) 12 { 13 this._prefixNameList = prefixNameList; 14 } 15 public override string TransformText() 16 { 17 base.TransformText(); 18 #> 19 using System; 20 using System.Collections.Generic; 21 using System.Linq; 22 using System.Text; 23 using System.Threading.Tasks; 24 25 using S.Framework.DataInterface; 26 using S.Utilities; 27 28 namespace S.Framework.DataAchieve.EntityFramework 29 { 30 public partial class UnitOfWork : IUnitOfWork 31 { 32 <#+ 33 foreach(string item in _prefixNameList) 34 { 35 #> 36 #region <#= item #> 的数据库连接字符串、数据库提供程序名称、上下文对象 37 38 /// <summary> 39 /// 当前工作单元中 <#= item #> 数据库连接字符串 40 /// </summary> 41 internal string <#= item #>ConnString { get; private set; } 42 43 /// <summary> 44 /// 当前工作单元中 <#= item #> 数据库提供程序名称 45 /// </summary> 46 internal string <#= item #>ProviderName { get; private set; } 47 48 private System.Data.Entity.DbContext _db<#= item #>; 49 50 /// <summary> 51 /// 当前工作单元中 <#= item #> 数据库上下文 52 /// </summary> 53 internal System.Data.Entity.DbContext Db<#= item #> 54 { 55 get 56 { 57 if (!this.<#= item #>DbIsExist) 58 { 59 this._db<#= item #> = new S.Framework.DataCore.EntityFramework.EntityContexts.<#= item #>EntityContext(this.<#= item #>ConnString); 60 } 61 return this._db<#= item #>; 62 } 63 } 64 65 /// <summary> 66 /// <#= item #> 数据库上下文是否存在 67 /// </summary> 68 private bool <#= item #>DbIsExist { get { return this._db<#= item #> != null; } } 69 70 #endregion 71 72 <#+ 73 } 74 #> 75 76 #region 构造函数 77 78 /// <summary> 79 /// 构造函数 80 /// <param name="connectionStringNames">数据库连接字符串名称集合,Key表示数据库标识名称,Value表示数据库连接字符串名称</param> 81 /// </summary> 82 public UnitOfWork(Dictionary<string, string> connectionStringNames) 83 { 84 if (connectionStringNames.IsNullOrEmpty()) 85 { 86 throw ExceptionHelper.ThrowDataAccessException("初始化工作单元对象时发生异常。", new ArgumentException("数据库连接信息集合参数不可为空。")); 87 } 88 <#+ 89 foreach(string item in _prefixNameList) 90 { 91 #> 92 if (connectionStringNames.ContainsKey("<#= item #>")) 93 { 94 var name = connectionStringNames["<#= item #>"]; 95 string connectionString = ConfigHelper.ConnectionString(name); 96 string providerName = ConfigHelper.ProviderName(name); 97 98 if (string.IsNullOrWhiteSpace(connectionString) || string.IsNullOrWhiteSpace(providerName)) 99 { throw ExceptionHelper.ThrowDataAccessException("初始化工作单元对象时发生异常。", new ArgumentException(name + "数据库连接信息有误。")); } 100 101 this.<#= item #>ConnString = connectionString; 102 this.<#= item #>ProviderName = providerName; 103 } 104 <#+ 105 } 106 #> 107 } 108 109 #endregion 110 111 /// <summary> 112 /// 开始数据事务 113 /// </summary> 114 public void BeginTransaction() 115 { } 116 117 /// <summary> 118 /// 提交工作单元 119 /// </summary> 120 /// <returns>受影响行数</returns> 121 public int Commit() 122 { return 0; } 123 124 /// <summary> 125 /// 执行回滚事务 126 /// </summary> 127 public void Rollback() 128 { } 129 } 130 } 131 <#+ 132 return this.GenerationEnvironment.ToString(); 133 } 134 } 135 #> 136
调整执行器文件,其最终代码如下:
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="Repository.tt" #> 11 <#@ include file="MigrateConfiguration.tt" #> 12 <#@ include file="DatabaseInitializer.tt" #> 13 <#@ include file="RepositoryFactories.tt" #> 14 <#@ include file="UnitOfWork.tt" #> 15 <# 16 17 string coreName = "S.Framework", projectName = coreName + ".DataAchieve", entityProjectName = coreName + ".Entity"; 18 string entityBaseModelName = entityProjectName + ".EntityBaseModel"; 19 string entityBaseModelNameForReflection = entityProjectName + ".EntityModelBaseForReflection"; 20 //当前完整路径 21 string currentPath = Path.GetDirectoryName(Host.TemplateFile); 22 //T4文件夹的父级文件夹路径 23 string projectPath = currentPath.Substring(0, currentPath.IndexOf(@"T4")); 24 //解决方案路径 25 string solutionFolderPath = currentPath.Substring(0, currentPath.IndexOf(@"" + projectName)); 26 27 //加载数据实体.dll 28 string entityFilePath = string.Concat(solutionFolderPath, ("\"+ entityProjectName +"\bin\Debug\" + entityProjectName + ".dll")); 29 byte[] fileData = File.ReadAllBytes(entityFilePath); 30 Assembly assembly = Assembly.Load(fileData); 31 //反射出实体类,不知道为啥此处不能成功判定“是否继承EntityModelBaseForReflection类” 32 //因此只能通过名称比较的方式来判定 33 IEnumerable<Type> modelTypes = assembly.GetTypes().Where(m => m.IsClass && !m.IsAbstract && (m.BaseType.FullName.Equals(entityBaseModelName) || m.BaseType.FullName.Equals(entityBaseModelNameForReflection))); 34 35 //循环实体类 36 Dictionary<string, List<Type>> prefixModelTypes = new Dictionary<string, List<Type>>();//存储[数据库标识名称]和[实体类型集合] 37 foreach (Type item in modelTypes) 38 { 39 //找 实体文件夹 名称 40 string tempNamespace= item.Namespace, nameSpaceWithoutProjectName = tempNamespace.Substring(entityProjectName.Length); 41 if(nameSpaceWithoutProjectName.IndexOf(".") != 0 || nameSpaceWithoutProjectName.LastIndexOf(".") > 0) 42 { continue; } 43 44 //是否直接继承实体基本类 45 bool purity = item.BaseType.FullName.Equals(entityBaseModelNameForReflection); 46 //实体所在的数据库标识名称 47 string targetName = nameSpaceWithoutProjectName.Substring(1); 48 List<Type> temp; 49 if(prefixModelTypes.TryGetValue(targetName, out temp)) 50 { 51 temp.Add(item); 52 } 53 else 54 { 55 temp = new List<Type>{ item }; 56 prefixModelTypes.Add(targetName, temp); 57 } 58 59 //目标文件的路径和名称(嵌套Generate文件夹是为了标识T4生成的类文件) 60 string fileName= targetName + @"Generate" + item.Name + "Repository.cs"; 61 //仓储文件 62 string folderName= @"Repositories"; 63 Repository repository = new Repository(item.Name, targetName); 64 repository.Output.Encoding = Encoding.UTF8; 65 string path = projectPath + folderName + fileName; 66 repository.RenderToFile(path); 67 } 68 69 foreach(KeyValuePair<string, List<Type>> item in prefixModelTypes) 70 { 71 //数据库初始化策略配置文件 72 string fileName = "/"+ item.Key +"/" + item.Key + "MigrateConfiguration.Generate.cs"; 73 MigrateConfiguration mc = new MigrateConfiguration(item.Key); 74 mc.Output.Encoding = Encoding.UTF8; 75 string path = string.Format(@"{0}Initializes", projectPath) + fileName; 76 mc.RenderToFile(path); 77 78 //数据库初始化类文件 79 fileName = "/"+ item.Key +"/" + item.Key + "DatabaseInitializer.Generate.cs"; 80 DatabaseInitializer temp = new DatabaseInitializer(item.Key); 81 temp.Output.Encoding = Encoding.UTF8; 82 path = string.Format(@"{0}Initializes", projectPath) + fileName; 83 temp.RenderToFile(path); 84 85 //各仓储工厂实现类文件 86 fileName = item.Key + "IRepositoryFactory.Generate.cs"; 87 RepositoryFactories repositoryFactories = new RepositoryFactories(item.Key, item.Value); 88 repositoryFactories.Output.Encoding = Encoding.UTF8; 89 path = string.Format(@"{0}RepositoryFactories", projectPath) + fileName; 90 repositoryFactories.RenderToFile(path); 91 } 92 93 //工作单元实现类文件 94 string fileName2 = "UnitOfWork.Generate.cs"; 95 UnitOfWork unitOfWork = new UnitOfWork(prefixModelTypes.Keys); 96 unitOfWork.Output.Encoding = Encoding.UTF8; 97 string path2 = string.Format(@"{0}", projectPath) + fileName2; 98 unitOfWork.RenderToFile(path2); 99 #> 100
运行一下执行器,会自动创建另外一个工作单元实现类,是个部分类。此时可以将手动创建的那个工作单元实现类清空成员,保留类的定义即可。不要删除,还要用到(定义仓储工厂属性时需要用到)。
此时文数据实现层档结构如下图:
在描述如何设计工作单元时提到过语法糖层面上要实现“工作单元对象.数据库标识(其实是该数据库的实体仓储对象工厂).实体仓储对象.方法/属性”这种使用方式,那么必然需要在工作单元中定义各数据库仓储对象工厂属性,而为了提高复用性能,应通过局部单例的方式进行缓存。
在UnitOfWork.cs中定义缓存和获取方式,代码如下:
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.DataInterface.IRepositoryFactories; 8 9 namespace S.Framework.DataAchieve.EntityFramework 10 { 11 public partial class UnitOfWork 12 { 13 /// <summary> 14 /// 仓储工厂缓存 15 /// </summary> 16 private Dictionary<Type, object> repositoryFactoryCache = new Dictionary<Type, object>(); 17 18 /// <summary> 19 /// 获取仓储工厂 20 /// </summary> 21 /// <typeparam name="R">仓储工厂接口</typeparam> 22 /// <returns>仓储工厂实例</returns> 23 private R GetRepositoryFactoryByInstance<R>() 24 where R : IBaseRepositoryFactory, new() 25 { 26 Type t = typeof(R); 27 if (!repositoryFactoryCache.ContainsKey(t)) 28 { 29 var repositoryFactory = new R(); 30 repositoryFactory.SetUnitOfWork(this); 31 32 repositoryFactoryCache.Add(t, repositoryFactory); 33 34 return repositoryFactory; 35 } 36 else { return (R)repositoryFactoryCache[t]; } 37 } 38 } 39 } 40
然后还需调整工作单元模板,让以下属性在模板中生效,并实现IDispose接口。
看到这里的读者,应该能够按需修改T4模板了,自己试试吧。
1 #region 仓储工厂对象 2 3 /// <summary> 4 /// Master 仓储工厂 5 /// </summary> 6 public IMasterIRepositoryFactory Master 7 { 8 get { return GetRepositoryFactoryByInstance<RepositoryFactories.MasterIRepositoryFactory>(); } 9 } 10 11 #endregion
1 #region 释放资源 2 3 /// <summary> 4 /// 释放资源 5 /// </summary> 6 public void Dispose() 7 { 8 Dispose(true); GC.SuppressFinalize(this); 9 } 10 11 /// <summary> 12 /// 释放资源 13 /// </summary> 14 /// <param name="disposing">是否释放</param> 15 protected virtual void Dispose(bool disposing) 16 { 17 if (disposing) 18 { 19 if (this.MasterDbIsExist) 20 { 21 try 22 { 23 this.DbMaster.Dispose(); 24 this._dbMaster = null; 25 } 26 catch { } 27 } 28 } 29 } 30 31 #endregion
调整BaseRepository
现在回头去实现BaseRepository临时增补的“SetUnitOfWork”方法。
实现这个方法很简单,无非就是定义个工作单元对象属性,然后在SetUnitOfWork方法里赋值给属性。比如像下面这样:
1 private UnitOfWork UnitOfWork { get; set; } 2 3 /// <summary> 4 /// 设置工作单元对象 5 /// <param name="unit">工作单元对象</param> 6 /// </summary> 7 public void SetUnitOfWork(object unit) 8 { 9 if (unit is UnitOfWork) { this.UnitOfWork = unit as UnitOfWork; } 10 else 11 { 12 throw ExceptionHelper.ThrowDataAccessException("给仓储设置工作单元时发生异常,参数 unit 不是一个工作单元对象。"); 13 } 14 }
但请注意,大问题要来了!
原先的Repository是有各自的db的,是通过在BaseRepository的构造方法中来完成db初始化的:
1 public BaseRepository() 2 { 3 this.Db = new MasterEntityContext(S.Framework.DatabaseConfig.ConnectionStringName); 4 }
现在有了UnitOfWork,自然不需要在Repository中初始化db了,可以去掉这句初始化db的代码。
但是这里存在一个衍生问题:UnitOfWork对象可能存在多个db(每个数据库db独立),但Repository的db只能有一个,那这个Repository的db究竟是UnitOfWork对象里的哪个db?
1 private System.Data.Entity.DbContext Db { get { return this.UnitOfWork.哪个Db; } }
BaseRepository肯定只能有一个db,UnitOfWork却可能多个db,但不要忘记了中间的实体仓储工厂。前面说过实体仓储工厂的第二个作用是“传递工作单元给实体仓储”,而实体仓储工厂是以数据库为单位的,这样就可以确定每个实体仓储工厂应该用的db,比如MasterIRepositoryFactory工厂应该用UnitOfWork.DbMaster。那就可以“给仓储传递工作单元的同时也传递相应db”。整理一下思路:
1、在BaseRepository中增加“设置db”方法,用来接收实体仓储工厂传递的db
2、在实体仓储工厂中初始化仓储后,调用“设置db”方法,并传入相应db
在接口IBaseRepository中增加定义:
1 /// <summary> 2 /// 设置数据库上下文对象。设置了编辑器不可见,但只有跨解决方案时才有用。当前解决方案下,还是会被智能提示捕捉到。 3 /// <param name="db">数据库上下文对象</param> 4 /// </summary> 5 [System.ComponentModel.EditorBrowsable(System.ComponentModel.EditorBrowsableState.Never)] 6 void SetDataContext(object db);
在其实现类BaseRepository中,先重置Db属性为:
1 private System.Data.Entity.DbContext Db { get; set; }
然后实现新增的SetDataContext方法:
1 /// <summary> 2 /// 设置当前仓储的 System.Data.Entity.DbContext 对象。本方法专供 各数据库仓储基类在初始化时调用。 3 /// </summary> 4 /// <param name="db">数据库上下文对象</param> 5 public void SetDataContext(object db) 6 { 7 if (db is System.Data.Entity.DbContext) { this.Db = db as System.Data.Entity.DbContext; } 8 else 9 { 10 throw ExceptionHelper.ThrowDataAccessException("给仓储设置数据库上下文时发生异常,参数 db 不是一个数据库上下文对象。"); 11 } 12 }
接下来就是调用了。实体仓储工厂是T4模板自动生成的,因此需要调整其T4模板文件。在模板中找到第一句代码,然后增加第二句代码:
1 rep.SetUnitOfWork(this.UnitOfWork); 2 rep.SetDataContext(this.UnitOfWork.Db<#= _prefixName #>);
运行T4,可以看到新生成的仓储工厂变为了这样:
这样就非常清晰了,再来理一把目前已经实现的设计逻辑。
(1)初始化工作单元(需传入数据库标识及相应数据库连接字符串名称)
(2)在工作单元的构造方法中,解析数据库连接信息,缓存起来
(3)当调用“工作单元对象.仓储工厂对象”时,初始化仓储工厂对象,并调用其SetUnitOfWork方法把当前工作单元对象传递过去后缓存起来
(4)当调用“工作单元对象.仓储工厂对象.仓储对象”时,初始化仓储对象,并依次调用其SetUnitOfWork和SetDataContext方法,分别将当前仓储工厂对象的所属工作单元和相应的数据库上下文传递过去后缓存起来
(5)步骤2实现了“多个数据库连接信息初始化成1个工作单元对象”
(6)步骤3实现了“多个仓储工厂对象共用1个工作单元对象”
(7)步骤4实现了“多个仓储对象共用1个工作单元对象,并能够以数据库为单位区分所用的数据库上下文对象”
步骤3中,利用缓存实现了“多次调用仓储工厂对象时无需重复初始化”,那么同样的,在步骤4中,也可以这么做。
在BaseRepositoryFactory中增加以下缓存功能代码:
1 /// <summary> 2 /// 仓储缓存 3 /// </summary> 4 private Dictionary<Type, object> repositoryCache = new Dictionary<Type, object>(); 5 6 /// <summary> 7 /// 获取仓储 8 /// </summary> 9 /// <typeparam name="TEntity">实体类型</typeparam> 10 /// <typeparam name="R">仓储接口</typeparam> 11 /// <param name="db">数据库上下文</param> 12 /// <returns>仓储实例</returns> 13 protected R GetRepositoryByInstance<TEntity, R>(System.Data.Entity.DbContext db) 14 where TEntity : class 15 where R : class, IBaseRepository<TEntity>, new() 16 { 17 if (!repositoryCache.ContainsKey(typeof(TEntity))) 18 { 19 var repository = new R(); 20 repository.SetUnitOfWork(this.UnitOfWork); 21 repository.SetDataContext(db); 22 23 repositoryCache.Add(typeof(TEntity), repository); 24 25 return repository; 26 } 27 else { return (R)repositoryCache[typeof(TEntity)]; } 28 }
同时调整RepositoryFactories模板,将get部分修改为:
1 return this.GetRepositoryByInstance<<#= item.Name #>, <#= item.Name #>Repository>(this.UnitOfWork.Db<#= _prefixName #>);
运行一下执行器,看看新的仓储工厂。
最后一步,调整数据库配置类中的数据库连接字符串(S.Framework中的DatabaseConfig类)。
因为工作单元的构造方法中,需要接收“数据库连接信息集合”,而先前定义在配置类中的数据库连接信息仅仅是一个:
1 /// <summary> 2 /// web.config文件中数据库连接字符串的名称 3 /// </summary> 4 public static string ConnectionStringName = "matrixkey";
应调整成:
1 /// <summary> 2 /// web.config文件中数据库连接字符串的名称集合,Key表示数据库标识名称(必须与数据实体中的一级文件夹一致),Value表示数据库连接字符串名称 3 /// </summary> 4 public static Dictionary<string, string> ConnectionStringNames = new Dictionary<string, string>() { { "Master", "matrixkey" } };
好了,现在去修改登录的代码。
原先的代码是直接初始化用户仓储的:
1 var rep = new SysUserRepository(); 2 var entity = rep.GetByUserName(model.UserName);
现在工作单元才是唯一的入口,最后登录的action代码是这样的:
1 using (var unit = new S.Framework.DataAchieve.EntityFramework.UnitOfWork(S.Framework.DatabaseConfig.ConnectionStringNames)) 2 { 3 S.Framework.DataInterface.IRepositories.Master.ISysUserRepository rep = unit.Master.SysUser; 4 //由于暴露出来的实体仓储,是接口类型,因此这里无法通过 rep2 来调用 GetByUserName 方法,因为该方法只被定义在仓储实现类中。需要在接口中增加这个方法的定义。 5 var entity = rep.GetByUserName(model.UserName); 6 if (entity == null) 7 { 8 model.Message = "用户名不存在"; 9 } 10 else 11 { 12 if (entity.Password != model.Password) 13 { 14 model.Message = "密码输入不正确"; 15 } 16 else 17 { 18 return RedirectToAction("Index", "Home", new { }); 19 } 20 } 21 22 return View(model); 23 }
注意注释中的内容,需要在数据接口层中给用户仓储接口定义GetByUserName方法,文件结构如下图中红框所示:
注意创建的ISysUserRepository,其命名空间一定要和Generate下的ISysUserRepository一致!顺便给接口加上“partial”标记,表示部分接口。
编译运行,去登录页面尝试登录一下吧,成功跳转到首页就说明没问题。
再次完善工作单元
还有事情没做:工作单元接口中那3个方法的实现。
开启事务,不用管,因为EF是在SaveChanges时自动嵌套事务的。
提交,实现也很简单,对每个数据库的db调用SaveChanges即可。修改工作单元T4模板文件,让”提交“方法实现以下代码:
1 /// <summary> 2 /// 提交工作单元 3 /// </summary> 4 /// <returns>受影响行数</returns> 5 public int Commit() 6 { 7 int result = 0; 8 if (this.MasterDbIsExist && this._dbMaster.ChangeTracker.HasChanges()) 9 { 10 try 11 { result += this._dbMaster.SaveChanges(); } 12 catch (Exception e) 13 { 14 throw ExceptionHelper.ThrowDataAccessException("dbMaster执行SaveChange时发生异常。", e); 15 } 16 } 17 return result; 18 }
修改T4模板比较熟了吧,我就不贴工作单元的T4模板代码了。
回滚,由于EF是在SaveChanges的时候自动嵌套事务,因此真正的回滚也是在内部自动执行的。我们应该处理的是”提交之前的回滚“,也就是在提交之前把”db上做的所有更改“消除掉,防止下一次提交时这些更改仍然存在。修改T4模板,让”回滚“方法实现以下代码:
1 /// <summary> 2 /// 执行回滚事务 3 /// </summary> 4 public void Rollback() 5 { 6 if (this.MasterDbIsExist && this._dbMaster.ChangeTracker.HasChanges()) 7 { 8 var entities = this._dbMaster.ChangeTracker.Entries(); 9 foreach (var entity in entities.Where(e => e.State == System.Data.Entity.EntityState.Added || e.State == System.Data.Entity.EntityState.Modified || e.State == System.Data.Entity.EntityState.Deleted)) 10 { 11 entity.State = System.Data.Entity.EntityState.Detached; 12 } 13 } 14 }
修改T4模板比较熟了吧,我就不贴工作单元的T4模板代码了。
事务的3大方法实现完毕,现在来测试一下效果。
还是在登录的action中测试,先测”事务-提交“功能。
用户名密码验证成功之后,跳转到首页之前,也就是在以下代码之前:
1 return RedirectToAction("Index", "Home", new { });
加入测试代码:
1 //每成功登录一次,就往用户表新增一个用户(数据随意) 2 var newUser = new SysUser { ID = Guid.NewGuid().ToString(), UserName = Guid.NewGuid().ToString().Replace("-", string.Empty), Password = "123456", CreateUser = "admin", CreateDate = DateTime.Now }; 3 unit.Master.SysUser.Add(newUser); 4 5 //更新当前登录用户的最后修改时间字段 6 entity.LastModifyDate = DateTime.Now; 7 unit.Master.SysUser.Update(entity); 8 9 //最后一次性提交 10 unit.Commit();
编译运行后登录,登录成功自动跳转到首页。打开数据库查看用户表,如果新增了一条用户数据,并且admin这个用户的最后修改时间更新了,说明测试代码执行成功。
在以上代码之后,下面继续测试”事务-回滚“功能。
1 //先新增一个角色,指定名称为“七色花测试角色007” 2 var newRole = new SysRole { ID = Guid.NewGuid().ToString(), Name = "七色花测试角色007", CreateUser = "admin", CreateDate = DateTime.Now }; 3 unit.Master.SysRole.Add(newRole); 4 5 //回滚 6 unit.Rollback(); 7 8 //再新增一个角色,数据随意 9 var ohterRole = new SysRole { ID = Guid.NewGuid().ToString(), Name = Guid.NewGuid().ToString(), CreateUser = "admin", CreateDate = DateTime.Now }; 10 unit.Master.SysRole.Add(ohterRole); 11 12 //最后提交 13 unit.Commit();
编译运行后登录,登录成功后自动跳转到首页。打开数据库查看用户表,如果只新增了一条角色数据,说明测试代码执行成功。
这一章节很长,因为仓储和工作单元的结合是非常核心的内容,未完全理解的建议多看会。
下一章节中,将演示EntityFramework的配置优化。
截止本章节,项目源码下载:点击下载(存在百度云盘中)