为什么要设计数据接口
首先来看一下3层的主要逻辑:数据层 => 业务层 => 应用层。作为通用的项目模板,其中最可能按需而大变的就是数据层,因为不同的项目,使用的数据库、数据驱动技术,是很有可能不同的。项目A,MsSql+EF(就像我正在演示的),项目B,也用这套模板,但变成了MySql+ADO.NET,那么就要尽可能地维持项目的整洁,减少需要修改的代码的量和范围。最佳的做法自然就是“数据层暴露出接口,业务层不关心数据实现”。
要设计哪些接口
凡是数据实现层要暴露给业务逻辑层使用的,都需要设计接口。比如仓储实现、数据库初始化实现。
实现数据接口
新建类库项目,名称S.Framework.DataInterface。
数据实现层中需要暴露的有:
基本仓储实现类BaseRepository
数据库初始化实现类MasterDatabaseInitializer
各实体仓储实现类
接下来就是为以上3种实现写对应的接口。
先说一句,数据实现层中,用一级文件夹EntityFramework进行了不同数据驱动的划分,但数据接口层中则不需要这样,因为只是定义规范,不需要关心实现。
创建目录结构及接口如下图:
作为BaseRepository的接口,IBaseRepository并不需要包含所有已实现的方法。接口只是定义通用的规范,一定要注意通用性。比如BaseRepository中的Query方法,是返回数据查询器对象的,是EF特有的,那就不用定义在接口规范中。把常规的Find、Add、Update、Delete等通用方法定义在接口中即可,如下:
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Linq.Expressions; 5 using System.Text; 6 using System.Threading.Tasks; 7 8 namespace S.Framework.DataInterface 9 { 10 /// <summary> 11 /// 仓储基本接口 12 /// </summary> 13 /// <typeparam name="TEntity">实体类型</typeparam> 14 public interface IBaseRepository<TEntity> where TEntity : class 15 { 16 /// <summary> 17 /// 主键查询 18 /// </summary> 19 /// <param name="keyValues">键值</param> 20 /// <returns>实体</returns> 21 TEntity Find(IEnumerable<object> keyValues); 22 23 /// <summary> 24 /// 主键查询 25 /// </summary> 26 /// <param name="keyValues">键值</param> 27 /// <returns>实体</returns> 28 TEntity Find(params object[] keyValues); 29 30 /// <summary> 31 /// 添加实体 32 /// </summary> 33 /// <param name="entity">实体</param> 34 void Add(TEntity entity); 35 36 /// <summary> 37 /// 批量添加实体 38 /// </summary> 39 /// <param name="entities">实体集合</param> 40 void AddRange(IEnumerable<TEntity> entities); 41 42 /// <summary> 43 /// 更改实体 44 /// </summary> 45 /// <param name="entity">实体对象</param> 46 void Update(TEntity entity); 47 48 /// <summary> 49 /// 批量更改实体 50 /// </summary> 51 /// <param name="entities">实体集合</param> 52 void UpdateRange(IEnumerable<TEntity> entities); 53 54 /// <summary> 55 /// 主键删除实体 56 /// </summary> 57 /// <param name="key">键值</param> 58 void Delete(object key); 59 60 /// <summary> 61 /// 删除实体 62 /// </summary> 63 /// <param name="entity">实体</param> 64 void Delete(TEntity entity); 65 66 /// <summary> 67 /// 批量删除实体 68 /// </summary> 69 /// <param name="entities">实体集合</param> 70 void DeleteRange(IEnumerable<TEntity> entities); 71 } 72 } 73
基本仓储接口
数据库初始化实现,其实有EF独特的地方(比如合并),假设数据驱动换成dapper或者ADO.NET,也是没办法实现部分初始化功能的。因此在接口IDatabaseInitializer中,只要关注“数据库的数据初始化”就行。
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.Initializes 8 { 9 public interface IDatabaseInitializer 10 { 11 /// <summary> 12 /// 设置数据库初始化策略 13 /// </summary> 14 /// <param name="migrate">是否合并(自动迁移)。若是,则会检查数据库是否存在,若不存在则创建,若存在则进行自动迁移。若否,则不进行初始化操作(这样能避开EF访问sys.databases检测数据库是否存在,项目稳定后可将参数设置为false。)。</param> 15 void Initialize(bool migrate); 16 } 17 } 18
数据库初始化接口
实体仓储接口的实现,跟实体仓储一样,也可以通过T4模板来自动生成部分类。
先让数据接口层引用数据实体层,因为T4中需要反射实体类。
仓储接口模板:
1 <#+ 2 // <copyright file="IRepository.tt" company=""> 3 // Copyright ? . All Rights Reserved. 4 // </copyright> 5 6 public class IRepository : CSharpTemplate 7 { 8 private string _modelName; 9 private string _prefixName; 10 11 public IRepository(string modelName, string prefixName) 12 { 13 _modelName = modelName; 14 _prefixName = prefixName; 15 } 16 17 public override string TransformText() 18 { 19 base.TransformText(); 20 #> 21 using System; 22 23 using S.Framework.Entity.<#= _prefixName #>; 24 25 namespace S.Framework.DataInterface.IRepositories.<#= _prefixName #> 26 { 27 /// <summary> 28 /// 仓储接口 29 /// </summary> 30 public partial interface I<#= _modelName #>Repository : IBaseRepository<<#= _modelName #>> 31 { 32 33 } 34 } 35 <#+ 36 return this.GenerationEnvironment.ToString(); 37 } 38 } 39 #> 40
实体仓储接口模板
执行器文件:
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 <# 12 13 string coreName = "S.Framework", projectName = coreName + ".DataInterface", 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 Dictionary<string, List<Type>> prefixModelTypes = new Dictionary<string, List<Type>>();//存储[数据库标识名称]和[实体类型集合] 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 List<Type> temp; 45 if(prefixModelTypes.TryGetValue(targetName, out temp)) 46 { 47 temp.Add(item); 48 } 49 else 50 { 51 temp = new List<Type>{ item }; 52 prefixModelTypes.Add(targetName, temp); 53 } 54 55 //目标文件的路径和名称(嵌套Generate文件夹是为了标识T4生成的类文件) 56 string fileName= targetName + @"\Generate\I" + item.Name + "Repository.cs"; 57 //仓储文件 58 string folderName= @"\IRepositories\"; 59 IRepository irepository = new IRepository(item.Name, targetName); 60 irepository.Output.Encoding = Encoding.UTF8; 61 string path = projectPath + folderName + fileName; 62 irepository.RenderToFile(path); 63 } 64 65 #> 66
执行器文件
运行T4之后,数据接口层文档结构目录如下:
最后,让数据实现层引用数据接口层,再调整对接口的继承。
先让BaseRepository继承IBaseRepository接口,如下图:
1 public abstract class BaseRepository<TEntity> : IBaseRepository<TEntity> where TEntity : class, new()
由于数据库初始化类和实体仓储实现都是T4自动生成的,因此想让生成的文件继承接口,需要调整相应的T4模板。
调整DatabaseInitializer模板,最终代码如下:
1 <#+ 2 // <copyright file="DatabaseInitializer.tt" company=""> 3 // Copyright ? . All Rights Reserved. 4 // </copyright> 5 6 public class DatabaseInitializer : CSharpTemplate 7 { 8 private string _prefixName; 9 10 public DatabaseInitializer(string prefixName) 11 { 12 this._prefixName = prefixName; 13 } 14 15 public override string TransformText() 16 { 17 base.TransformText(); 18 #> 19 20 using System; 21 using System.Collections.Generic; 22 using System.Linq; 23 using System.Text; 24 using System.Threading.Tasks; 25 using System.Data.Entity; 26 27 using S.Framework.DataCore.EntityFramework.EntityContexts; 28 using S.Framework.DataInterface.Initializes; 29 30 namespace S.Framework.DataAchieve.EntityFramework.Initializes 31 { 32 /// <summary> 33 /// <#= _prefixName #> 数据库初始化操作类 34 /// </summary> 35 public class <#= _prefixName #>DatabaseInitializer : IDatabaseInitializer 36 { 37 /// <summary> 38 /// 设置数据库初始化策略 39 /// </summary> 40 /// <param name="migrate">是否合并(自动迁移)。若是,则会检查数据库是否存在,若不存在则创建,若存在则进行自动迁移。若否,则不进行初始化操作(这样能避开EF访问sys.databases检测数据库是否存在,项目稳定后可将参数设置为false)。</param> 41 public void Initialize(bool migrate) 42 { 43 if (!migrate) 44 { 45 System.Data.Entity.Database.SetInitializer<<#= _prefixName #>EntityContext>(null); 46 } 47 else 48 { 49 System.Data.Entity.Database.SetInitializer(new MigrateDatabaseToLatestVersion<<#= _prefixName #>EntityContext, <#= _prefixName #>MigrateConfiguration>(true)); 50 } 51 } 52 } 53 } 54 <#+ 55 return this.GenerationEnvironment.ToString(); 56 } 57 } 58 #> 59
数据库初始化模板文件
调整Repository模板,实体仓储除了需要继承BaseRepository类,还需要继承数据接口层中相应的实体仓储接口,最终代码如下:
1 <#+ 2 // <copyright file="Repository.tt" company=""> 3 // Copyright ? . All Rights Reserved. 4 // </copyright> 5 6 public class Repository : CSharpTemplate 7 { 8 private string _modelName; 9 private string _prefixName; 10 11 public Repository(string modelName, string prefixName) 12 { 13 this._modelName = modelName; 14 this._prefixName = prefixName; 15 } 16 public override string TransformText() 17 { 18 base.TransformText(); 19 #> 20 using System; 21 using System.Collections.Generic; 22 using System.Linq; 23 using System.Text; 24 using System.Threading.Tasks; 25 26 using S.Framework.Entity.<#= _prefixName #>; 27 using S.Framework.DataInterface.IRepositories.<#= _prefixName #>; 28 29 namespace S.Framework.DataAchieve.EntityFramework.Repositories.<#= _prefixName #> 30 { 31 /// <summary> 32 /// 实体仓储 33 /// </summary> 34 public partial class <#= _modelName #>Repository : BaseRepository<<#= _modelName #>>, I<#= _modelName #>Repository 35 { 36 37 } 38 } 39 <#+ 40 return this.GenerationEnvironment.ToString(); 41 } 42 } 43 #> 44
实体仓储模板文件
另外,由于在WebUI层中的Global.asax里调用了数据库初始化方法(该方法继承于接口),因此也需要让WebUI层引用数据接口层,才能正常运行项目。
下一章节,将演示仓储与工作单元的设计和实现,是非常核心的内容。