解耦WebUI层与EntityFramework
在还未实现实体仓储时,登录功能是在控制器中直接初始化EF数据库上下文来实现的,这样也导致WebUI层必须引用EntityFramework。在完成数据层的设计和实现之后,控制器中不再直接使用EF数据库上下文对象,而是通过工作单元去调用实体仓储,其实到了这一步就可以让WebUI层不再依赖EntityFramework。从WebUI层中通过nuget管理的方式移除EF,但要注意的是,EF包含2个dll,其中的EntityFramework.SqlServer.dll需要手动拷贝到WebUI的bin目录下,作为识别数据库管道。
解耦WebUI层与数据层
业务层的必要性就不谈了。
由于数据层分库、分实体仓储,所以业务层也同样需要分库分业务类。
先搭建业务层,并手动写业务类,让WebUI层和数据层解耦。
其中,BaseBll类很简单,目前只是一个空的业务基类:
1 /// <summary> 2 /// 业务基类 3 /// </summary> 4 /// <typeparam name="TEntity">实体类型</typeparam> 5 public abstract class BaseBll<TEntity> where TEntity : class 6 { 7 }
文件夹Other下是数据库初始化类,因为WebUI中的Global.asax中原先调用的是数据实现层的方法,因此需要在业务层中包装。该初始化类代码如下:
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.DataAchieve.EntityFramework.Initializes; 8 9 namespace S.Framework.BLL.Other 10 { 11 /// <summary> 12 /// 数据库初始化操作类 13 /// </summary> 14 public static class DatabaseInitializer 15 { 16 /// <summary> 17 /// 数据库初始化 18 /// </summary> 19 public static void Initialize() 20 { 21 new MasterDatabaseInitializer().Initialize(S.Framework.DatabaseConfig.IsMigrateDatabase); 22 } 23 } 24 } 25
这里的业务类其实就做2件事情:
1)封装业务(与数据库访问无关)
2)调用数据层
在WebUI层中,原先有2处位置调用了数据层。第一处是AccountController中的登录功能,另一处是TestController中的EF数据插入测试功能。把这2处的代码,移动到业务层中就行。插入测试代码之前没贴出来,这次一并贴一下。于是SysUserBll的代码如下:
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.BLL.Powerful 10 { 11 /// <summary> 12 /// SysUser 业务实现 13 /// </summary> 14 public class SysUserBll : BaseBll<SysUser> 15 { 16 /// <summary> 17 /// 根据用户名获取用户实体 18 /// </summary> 19 /// <param name="userName">用户名</param> 20 /// <returns>用户实体</returns> 21 public SysUser GetByUserName(string userName) 22 { 23 using (var unit = new S.Framework.DataAchieve.EntityFramework.UnitOfWork(S.Framework.DatabaseConfig.ConnectionStringNames)) 24 { 25 return unit.Master.SysUser.GetByUserName(userName); 26 } 27 } 28 29 /// <summary> 30 /// 测试以EF方式插入指定数量的用户数据 31 /// </summary> 32 /// <param name="count">要插入数据库的用户数据量</param> 33 /// <returns>操作结果</returns> 34 public object TestForEFInsert(int count = 1000) 35 { 36 long cost = 0; 37 List<string> msg = new List<string>(); 38 System.Diagnostics.Stopwatch sw = new System.Diagnostics.Stopwatch(); 39 sw.Start(); 40 using (var unit = new S.Framework.DataAchieve.EntityFramework.UnitOfWork(S.Framework.DatabaseConfig.ConnectionStringNames)) 41 { 42 sw.Stop(); 43 cost += sw.ElapsedMilliseconds; 44 msg.Add("初始化工作单元完成,耗时:" + sw.ElapsedMilliseconds.ToString() + "毫秒;"); 45 46 sw.Restart(); 47 var rep = unit.Master.SysUser; 48 sw.Stop(); 49 cost += sw.ElapsedMilliseconds; 50 msg.Add("初始化用户仓储完成,耗时:" + sw.ElapsedMilliseconds.ToString() + "毫秒;"); 51 52 sw.Restart(); 53 var users = this.CreateUsers(count); 54 sw.Stop(); 55 cost += sw.ElapsedMilliseconds; 56 msg.Add("初始化" + count + "条用户实体对象完成,耗时:" + sw.ElapsedMilliseconds.ToString() + "毫秒;"); 57 58 sw.Restart(); 59 rep.AddRange(users); 60 sw.Stop(); 61 cost += sw.ElapsedMilliseconds; 62 msg.Add("将" + count + "条用户实体对象附加至db,耗时:" + sw.ElapsedMilliseconds.ToString() + "毫秒;"); 63 64 sw.Restart(); 65 int result = unit.Commit(); 66 sw.Stop(); 67 cost += sw.ElapsedMilliseconds; 68 msg.Add("执行提交,影响行数[" + result + "],耗时:" + sw.ElapsedMilliseconds.ToString() + "毫秒;"); 69 } 70 71 return new { success = true, cost = cost, msg = string.Join("<br />", msg) }; 72 } 73 74 /// <summary> 75 /// 组装指定数量的用户实体 76 /// </summary> 77 /// <param name="count">需要组装的用户实体数量</param> 78 /// <returns>用户实体集合</returns> 79 private IEnumerable<S.Framework.Entity.Master.SysUser> CreateUsers(int count) 80 { 81 List<S.Framework.Entity.Master.SysUser> entities = new List<Entity.Master.SysUser>(); 82 DateTime dt = DateTime.Now; 83 for (int i = 0; i < count; i++) 84 { 85 entities.Add(new S.Framework.Entity.Master.SysUser { ID = Guid.NewGuid().ToString(), UserName = "test", Password = "123456", CreateUser = "admin", CreateDate = dt }); 86 } 87 88 return entities; 89 } 90 } 91 } 92
然后调整一下AccountController和TestController中的代码,通过调用业务类方法来完成原先的功能。
这里说一点,业务类中会存在部分方法,仅仅是对数据层方法的调用,不含任何业务,这种情况会让人觉得“还要去中间包装一层反而很麻烦(很多时候需要额外创建数据传输模型)”。正规项目更多考虑的是可维护性、健壮性、可扩展性,MVC时代模型为王,千万别省。
调整之后,记得移除“WebUI层中对数据层的引用”,增加“WebUI层对业务层的引用”,而业务层当然需要引用数据层(引用数据接口和数据实现,不用引用数据核心)。
这样,WebUI层和数据层就没有依赖关系了。
业务层的设计
跟数据层的设计类似,要能这样调用业务方法:业务层工厂.数据库标识(其实是该数据库下的业务类的工厂对象).各实体业务类.业务方法。
业务层的实现
业务类SysUserBll已经有了,把SysRoleBll也创建出来。然后创建数据库业务类工厂,用于快速初始化业务类,具体文档结构如下:
该工厂的代码比较简单:
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.BLL.PowerfulFactories 8 { 9 /// <summary> 10 /// Master 业务实现类工厂对象 11 /// </summary> 12 public class MasterPowerfulFactory 13 { 14 /// <summary> 15 /// SysUser 业务实现类对象 16 /// </summary> 17 public Powerful.SysUserBll SysUser 18 { 19 get { return new Powerful.SysUserBll(); } 20 } 21 22 /// <summary> 23 /// SysRole 业务实现类对象 24 /// </summary> 25 public Powerful.SysRoleBll SysRole 26 { 27 get { return new Powerful.SysRoleBll(); } 28 } 29 } 30 } 31
最后创建业务层工厂,用于快速初始化“数据库业务类工厂”:
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.BLL 8 { 9 /// <summary> 10 /// 业务工厂 11 /// </summary> 12 public class BllFactory 13 { 14 /// <summary> 15 /// Master 业务实现类工厂对象 16 /// </summary> 17 public static PowerfulFactories.MasterPowerfulFactory Master 18 { 19 get { return new PowerfulFactories.MasterPowerfulFactory(); } 20 } 21 22 } 23 } 24
此时,整个业务层结构如下图:
调整WebUI层控制器调用业务类的方式,如下:
1 //这里不用new业务类了,通过业务层工厂直接使用业务类即可 2 S.Framework.BLL.Powerful.SysUserBll UB = S.Framework.BLL.BllFactory.Master.SysUser; 3 var entity = UB.GetByUserName(model.UserName); 4 5 //TestController中对业务类的调用也是同样的改法
编译运行,登录测试。
利用T4模板自动生成业务类及工厂
和数据层一样,这些业务类和工厂,也可以通过T4模板来自动生成。此处不再详细赘述,处理逻辑在数据层实现的章节里详细提过。
模板代码也不贴了,下本章节源代码看吧。此时目录结构如下图:
业务层的完善和补充
在实体业务类中,初始化工作单元对象都是在方法中单独new的,这可以统一起来。
在业务层根目录下创建类,名称IUnitOfWorkFactory,其代码如下:
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 9 namespace S.Framework.BLL 10 { 11 internal static class IUnitOfWorkFactory 12 { 13 /// <summary> 14 /// 获取工作单元实例 15 /// </summary> 16 public static IUnitOfWork UnitOfWork 17 { 18 get 19 { 20 return new S.Framework.DataAchieve.EntityFramework.UnitOfWork(S.Framework.DatabaseConfig.ConnectionStringNames); 21 } 22 } 23 } 24 } 25
然后将原先的
1 using (var unit = new S.Framework.DataAchieve.EntityFramework.UnitOfWork(S.Framework.DatabaseConfig.ConnectionStringNames))
修改成
1 using (var unit = IUnitOfWorkFactory.UnitOfWork)
但会发现报错,如下图:
原因是,此时的unit是工作对象接口类型(IUnitOfWork),而不是工作单元类型(UnitOfWork),而在该接口中,当初在设计工作单元接口时只定义了“开启事务、提交、回滚”3个方法,没有定义“仓储工厂”。此时自然就无法识别。
解决的方法也很简单,在接口中定义“仓储工厂”即可,其实现已经在工作单元中完成了。这个定义本应该在前面的工作单元设计与实现章节中完成,不小心漏了。
在数据接口层(S.Framework.DataInterface),增加T4模板,用于生成工作单元接口,其代码如下:
1 <#+ 2 // <copyright file="IUnitOfWork.tt" company=""> 3 // Copyright © . All Rights Reserved. 4 // </copyright> 5 6 public class IUnitOfWork : CSharpTemplate 7 { 8 private List<string> _prefixNameList; 9 10 public IUnitOfWork(List<string> prefixNameList) 11 { 12 _prefixNameList = prefixNameList; 13 } 14 public override string TransformText() 15 { 16 base.TransformText(); 17 #> 18 using System; 19 using System.Collections.Generic; 20 using System.Linq; 21 using System.Text; 22 23 using S.Framework.DataInterface.IRepositoryFactories; 24 25 26 namespace S.Framework.DataInterface 27 { 28 /// <summary> 29 /// 工作单元接口 30 /// </summary> 31 public partial interface IUnitOfWork 32 { 33 #region 生成仓储接口工厂实例 34 35 <#+ 36 foreach(string item in _prefixNameList) 37 { 38 #> 39 /// <summary> 40 /// <#= item #> 仓储接口工厂 41 /// </summary> 42 I<#= item #>IRepositoryFactory <#= item #> { get; } 43 <#+ 44 } 45 #> 46 47 #endregion 48 } 49 } 50 <#+ 51 return this.GenerationEnvironment.ToString(); 52 } 53 } 54 #> 55
1 //工作单元接口文件 2 string fileName2 = "IUnitOfWork.Generate.cs"; 3 IUnitOfWork iUnitOfWork = new IUnitOfWork(prefixModelTypes.Keys.ToList()); 4 iUnitOfWork.Output.Encoding = Encoding.UTF8; 5 string path2 = string.Format(@"{0}", projectPath) + fileName2; 6 iUnitOfWork.RenderToFile(path2);
到此,业务层基本就完成了。后面实现各模块功能的时候,其实还需要进一步完善,比如封装业务处理结果类型。这就留到后面吧。
现在,数据层 => 业务层 => 界面层 已经成形了。
下一章节,将演示ORM辅助利器Dapper,并将其与EF结合,共同支撑数据层。
截止本章节,项目源码下载:点击下载(存在百度云盘中)