为什么要设计数据接口
首先来看一下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层引用数据接口层,才能正常运行项目。
下一章节,将演示仓储与工作单元的设计和实现,是非常核心的内容。