一. 前言
本节继续探讨一种新的框架搭建模式,框架的结构划分和上一节是相同的,本节IOC框架换成了Unity,并且采用构造函数注入的方式,另外服务层的封装模式也发生了变化,下面将详细的进行探讨。
(一). 技术选型
1. DotNet框架:4.6
2. 数据库访问:EF 6.2 (CodeFrist模式)
3. IOC框架:Unity 5.8.13
4. 日志框架:log4net 2.0.8
5. 开发工具:VS2017
(二). 框架目标
1. 一个项目同时连接多个相同种类的数据库,在一个方法中可以同时对多个数据进行操作。
2. 支持多种数据库:SqlServer、MySQL、Oracle,灵活的切换数据库。
3. 抽象成支持多种数据库连接方式:EF、ADO.Net、Dapper。
二. 搭建思路
1. 层次划分
将框架分为:Ypf.Data、Ypf.IService、Ypf.Service、Ypf.DTO、Ypf.Utils、Ypf.AdminWeb 六个基本层(后续还会补充 Ypf.Api层),每层的作用分别为:
①. Ypf.Data:存放连接数据库的相关类,包括EF上下文类、映射的实体类、实体类的FluentApi模式的配置类。
②. Ypf.IService:业务接口层,用来约束接口规范。
③. Ypf.Service:业务层,用来编写整套项目的业务方法,但需要符合Ypf.IService层的接口约束。
④. Ypf.DTO: 存放项目中用到的自定义的实体类。
⑤. Ypf.Utils: 工具类
⑥. Ypf.AdminWeb: 表现层,系统的展示、接受用户的交互,传递数据,业务对接。
PS:后续会补充Ypf.Api层,用于接受移动端、或其他客户端接口数据,进行相应的业务对接处理。
2. Ypf.Data层的剖析
把EF封装到【Ypf.Data】层,通过Nuget引入EF的程序集,利用【来自数据库的code first】的模式先进行映射,后续通过DataAnnotations 和 FluentAPI混合使用。该层结构如下:
PS:EF的关闭默认策略、EF的DataAnnotations、EF的FluentAPI模式, 在关闭数据库策略的情况下,无论哪种模式都需要显式的 ToTable来映射表名,否则会提示该类找不到。
EF配置详情参考:
第十五节: EF的CodeFirst模式通过DataAnnotations修改默认协定
第十六节: EF的CodeFirst模式通过Fluent API修改默认协定
给【Ypf.AdminWeb】层,通过Nuget引入EF的程序集,并配置数据库连接字符串,直接在该层测试数据库访问。【测试通过】
3. Service层和IService层简单的封装一下
①.【Ypf.Service】层只有一个BaseService普通类(非泛型)封装,【Ypf.IService】层有设置一个IBaseService接口,BaseService类实现IBaseService接口,里面的方法全部封装为泛型方法。
②.【Ypf.Service】层中有很多自定义的 xxxService,每个xxxService都要实现【Ypf.IService】层的IxxxService层接口,同时继承BaseService类,这里的xxxService层划分并不依赖表名划分,自定义根据业务合理起名即可。
③. xxxService类中,在构造函数中传入DbContext db,但此处并不实例化,而是利用Unity进行构造函数的注入,所有的子类xxxService类中,都注入相应的EF上下文,这样就不需要手动再传入了(这里需要特别注意:Unity默认支持构造函数注入,只要xxxService被配置,那么该类中的(参数最多)的构造函数中的参数类即可以进行注入,只要在配置文件中配置上即可实现注入)。
④.在Unity的配置文件中进行配置IOC,在控制器中进行构造函数注入。
⑤ . 控制器中的Action仅仅负责传值和简单的一些判断,核心业务全部都写在Service层中。
⑥. 子类xxxService中的方法中,可以直接通过 this.XXX的方式调用父类BaseService中的泛型方法,db是通过子类构造函数传到父类BaseService构造函数中。
分享BaseService类和IBaseService接口:
1 using System; 2 using System.Collections.Generic; 3 using System.Data.Entity; 4 using System.Data.SqlClient; 5 using System.Linq; 6 using System.Linq.Expressions; 7 using System.Reflection; 8 using System.Text; 9 using System.Threading.Tasks; 10 using Ypf.IService; 11 12 namespace Ypf.Service 13 { 14 public class BaseService: IBaseService 15 { 16 /// 17 /// 一个属性,在该类中使用 18 /// 19 public DbContext db { get; private set; } 20 21 /// 22 /// 通过构造函数传入EF的上下文 23 /// 该上下文可能是同种类型的不同数据库、也可能是相同结构的不同类型的数据库 24 /// 为后面的Untiy的构造函数注入埋下伏笔 25 /// 26 /// 27 public BaseService(DbContext db) 28 { 29 this.db = db; 30 } 31 32 33 //1. 直接提交数据库 34 35 #region 01-数据源 36 public IQueryable Entities() where T : class 37 { 38 return db.Set(); 39 } 40 41 #endregion 42 43 #region 02-新增 44 public int Add(T model) where T : class 45 { 46 DbSet dst = db.Set(); 47 dst.Add(model); 48 return db.SaveChanges(); 49 50 } 51 #endregion 52 53 #region 03-删除(适用于先查询后删除 单个) 54 /// 55 /// 删除(适用于先查询后删除的单个实体) 56 /// 57 /// 需要删除的实体 58 /// 59 public int Del(T model) where T : class 60 { 61 db.Set().Attach(model); 62 db.Set().Remove(model); 63 return db.SaveChanges(); 64 } 65 #endregion 66 67 #region 04-根据条件删除(支持批量删除) 68 /// 69 /// 根据条件删除(支持批量删除) 70 /// 71 /// 传入Lambda表达式(生成表达式目录树) 72 /// 73 public int DelBy(Expression> delWhere) where T : class 74 { 75 List listDels = db.Set().Where(delWhere).ToList(); 76 listDels.ForEach(d => 77 { 78 db.Set().Attach(d); 79 db.Set().Remove(d); 80 }); 81 return db.SaveChanges(); 82 } 83 #endregion 84 85 #region 05-单实体修改 86 /// 87 /// 修改 88 /// 89 /// 修改后的实体 90 /// 91 public int Modify(T model) where T : class 92 { 93 db.Entry(model).State = EntityState.Modified; 94 return db.SaveChanges(); 95 } 96 #endregion 97 98 #region 06-批量修改(非lambda) 99 /// 100 /// 批量修改(非lambda)101 /// 102 /// 要修改实体中 修改后的属性 103 /// 查询实体的条件104 /// lambda的形式表示要修改的实体属性名105 /// 106 public int ModifyBy(T model, Expression> whereLambda, params string[] proNames) where T : class107 {108 List listModifes = db.Set().Where(whereLambda).ToList();109 Type t = typeof(T);110 List proInfos = t.GetProperties(BindingFlags.Instance | BindingFlags.Public).ToList();111 Dictionary dicPros = new Dictionary();112 proInfos.ForEach(p =>113 {114 if (proNames.Contains(p.Name))115 {116 dicPros.Add(p.Name, p);117 }118 });119 foreach (string proName in proNames)120 {121 if (dicPros.ContainsKey(proName))122 {123 PropertyInfo proInfo = dicPros[proName];124 object newValue = proInfo.GetValue(model, null);125 foreach (T m in listModifes)126 {127 proInfo.SetValue(m, newValue, null);128 }129 }130 }131 return db.SaveChanges();132 }133 #endregion134 135 #region 07-根据条件查询136 /// 137 /// 根据条件查询138 /// 139 /// 查询条件(lambda表达式的形式生成表达式目录树)140 /// 141 public List GetListBy(Expression> whereLambda) where T : class142 {143 return db.Set().Where(whereLambda).ToList();144 }145 #endregion146 147 #region 08-根据条件排序和查询148 /// 149 /// 根据条件排序和查询150 /// 151 /// 排序字段类型152 /// 查询条件153 /// 排序条件154 /// 升序or降序155 /// 156 public List GetListBy(Expression> whereLambda, Expression> orderLambda, bool isAsc = true) where T : class157 {158 List list = null;159 if (isAsc)160 {161 list = db.Set().Where(whereLambda).OrderBy(orderLambda).ToList();162 }163 else164 {165 list = db.Set().Where(whereLambda).OrderByDescending(orderLambda).ToList();166 }167 return list;168 }169 #endregion170 171 #region 09-分页查询172 /// 173 /// 根据条件排序和查询174 /// 175 /// 排序字段类型176 /// 页码177 /// 页容量178 /// 查询条件179 /// 排序条件180 /// 升序or降序181 /// 182 public List GetPageList(int pageIndex, int pageSize, Expression> whereLambda, Expression> orderLambda, bool isAsc = true) where T : class183 {184 185 List list = null;186 if (isAsc)187 {188 list = db.Set().Where(whereLambda).OrderBy(orderLambda)189 .Skip((pageIndex - 1) * pageSize).Take(pageSize).ToList();190 }191 else192 {193 list = db.Set().Where(whereLambda).OrderByDescending(orderLambda)194 .Skip((pageIndex - 1) * pageSize).Take(pageSize).ToList();195 }196 return list;197 }198 #endregion199 200 #region 10-分页查询输出总行数201 /// 202 /// 根据条件排序和查询203 /// 204 /// 排序字段类型205 /// 页码206 /// 页容量207 /// 查询条件208 /// 排序条件209 /// 升序or降序210 /// 211 public List GetPageList(int pageIndex, int pageSize, ref int rowCount, Expression> whereLambda, Expression> orderLambda, bool isAsc = true) where T : class212 {213 int count = 0;214 List list = null;215 count = db.Set().Where(whereLambda).Count();216 if (isAsc)217 {218 var iQueryList = db.Set().Where(whereLambda).OrderBy(orderLambda)219 .Skip((pageIndex - 1) * pageSize).Take(pageSize);220 221 list = iQueryList.ToList();222 }223 else224 {225 var iQueryList = db.Set().Where(whereLambda).OrderByDescending(orderLambda)226 .Skip((pageIndex - 1) * pageSize).Take(pageSize);227 list = iQueryList.ToList();228 }229 rowCount = count;230 return list;231 }232 #endregion233 234 235 //2. SaveChange剥离出来,处理事务236 237 #region 01-批量处理SaveChange()238 /// 239 /// 事务批量处理240 /// 241 /// 242 public int SaveChange()243 {244 return db.SaveChanges();245 }246 #endregion247 248 #region 02-新增249 /// 250 /// 新增251 /// 252 /// 需要新增的实体253 public void AddNo(T model) where T : class254 {255 db.Set().Add(model);256 }257 #endregion258 259 #region 03-删除260 /// 261 /// 删除262 /// 263 /// 需要删除的实体264 public void DelNo(T model) where T : class265 {266 db.Entry(model).State = EntityState.Deleted;267 }268 #endregion269 270 #region 04-根据条件删除271 /// 272 /// 条件删除273 /// 274 /// 需要删除的条件275 public void DelByNo(Expression> delWhere) where T : class276 {277 List listDels = db.Set().Where(delWhere).ToList();278 listDels.ForEach(d =>279 {280 db.Set().Attach(d);281 db.Set().Remove(d);282 });283 }284 #endregion285 286 #region 05-修改287 /// 288 /// 修改289 /// 290 /// 修改后的实体291 public void ModifyNo(T model) where T : class292 {293 db.Entry(model).State = EntityState.Modified;294 }295 #endregion296 297 298 //3. EF调用sql语句299 300 #region 01-执行增加,删除,修改操作(或调用存储过程)301 /// 302 /// 执行增加,删除,修改操作(或调用存储过程)303 /// 304 /// 305 /// 306 /// 307 public int ExecuteSql(string sql, params SqlParameter[] pars)308 {309 return db.Database.ExecuteSqlCommand(sql, pars);310 }311 312 #endregion313 314 #region 02-执行查询操作315 /// 316 /// 执行查询操作317 /// 318 /// 319 /// 320 /// 321 /// 322 public List ExecuteQuery(string sql, params SqlParameter[] pars) where T : class323 {324 return db.Database.SqlQuery(sql, pars).ToList();325 }326 #endregion327 328 329 330 }331 }
1 using System; 2 using System.Collections.Generic; 3 using System.Data.SqlClient; 4 using System.Linq; 5 using System.Linq.Expressions; 6 using System.Text; 7 using System.Threading.Tasks; 8 9 namespace Ypf.IService 10 { 11 public interface IBaseService 12 { 13 //1. 直接提交数据库 14 15 #region 01-数据源 16 IQueryable Entities() where T : class; 17 18 #endregion 19 20 #region 02-新增 21 int Add(T model) where T : class; 22 23 #endregion 24 25 #region 03-删除(适用于先查询后删除 单个) 26 /// 27 /// 删除(适用于先查询后删除的单个实体) 28 /// 29 /// 需要删除的实体 30 /// 31 int Del(T model) where T : class; 32 33 #endregion 34 35 #region 04-根据条件删除(支持批量删除) 36 /// 37 /// 根据条件删除(支持批量删除) 38 /// 39 /// 传入Lambda表达式(生成表达式目录树) 40 /// 41 int DelBy(Expression> delWhere) where T : class; 42 43 #endregion 44 45 #region 05-单实体修改 46 /// 47 /// 修改 48 /// 49 /// 修改后的实体 50 /// 51 int Modify(T model) where T : class; 52 53 #endregion 54 55 #region 06-批量修改(非lambda) 56 /// 57 /// 批量修改(非lambda) 58 /// 59 /// 要修改实体中 修改后的属性 60 /// 查询实体的条件 61 /// lambda的形式表示要修改的实体属性名 62 /// 63 int ModifyBy(T model, Expression> whereLambda, params string[] proNames) where T : class; 64 65 #endregion 66 67 #region 07-根据条件查询 68 /// 69 /// 根据条件查询 70 /// 71 /// 查询条件(lambda表达式的形式生成表达式目录树) 72 /// 73 List GetListBy(Expression> whereLambda) where T : class; 74 75 #endregion 76 77 #region 08-根据条件排序和查询 78 /// 79 /// 根据条件排序和查询 80 /// 81 /// 排序字段类型 82 /// 查询条件 83 /// 排序条件 84 /// 升序or降序 85 /// 86 List GetListBy(Expression> whereLambda, Expression> orderLambda, bool isAsc = true) where T : class; 87 88 #endregion 89 90 #region 09-分页查询 91 /// 92 /// 根据条件排序和查询 93 /// 94 /// 排序字段类型 95 /// 页码 96 /// 页容量 97 /// 查询条件 98 /// 排序条件 99 /// 升序or降序100 /// 101 List GetPageList(int pageIndex, int pageSize, Expression> whereLambda, Expression> orderLambda, bool isAsc = true) where T : class;102 103 #endregion104 105 #region 10-分页查询输出总行数106 /// 107 /// 根据条件排序和查询108 /// 109 /// 排序字段类型110 /// 页码111 /// 页容量112 /// 查询条件113 /// 排序条件114 /// 升序or降序115 /// 116 List GetPageList(int pageIndex, int pageSize, ref int rowCount, Expression> whereLambda, Expression> orderLambda, bool isAsc = true) where T : class;117 118 #endregion119 120 121 //2. SaveChange剥离出来,处理事务122 123 #region 01-批量处理SaveChange()124 /// 125 /// 事务批量处理126 /// 127 /// 128 int SaveChange();129 130 #endregion131 132 #region 02-新增133 /// 134 /// 新增135 /// 136 /// 需要新增的实体137 void AddNo(T model) where T : class;138 139 #endregion140 141 #region 03-删除142 /// 143 /// 删除144 /// 145 /// 需要删除的实体146 void DelNo(T model) where T : class;147 148 #endregion149 150 #region 04-根据条件删除151 /// 152 /// 条件删除153 /// 154 /// 需要删除的条件155 void DelByNo(Expression> delWhere) where T : class;156 157 #endregion158 159 #region 05-修改160 /// 161 /// 修改162 /// 163 /// 修改后的实体164 void ModifyNo(T model) where T : class;165 166 #endregion167 168 169 //3. EF调用sql语句170 171 #region 01-执行增加,删除,修改操作(或调用存储过程)172 /// 173 /// 执行增加,删除,修改操作(或调用存储过程)174 /// 175 /// 176 /// 177 /// 178 int ExecuteSql(string sql, params SqlParameter[] pars);179 180 #endregion181 182 #region 02-执行查询操作183 /// 184 /// 执行查询操作185 /// 186 /// 187 /// 188 /// 189 /// 190 List ExecuteQuery(string sql, params SqlParameter[] pars) where T : class;191 192 #endregion193 194 }195 }
4. 利用Unity进行整合
(1). 通过Nuget给【Ypf.Utils】层引入“Unity”的程序集和“Microsoft.AspNet.Mvc”程序集。
(2). 新建类:DIFactory 用于读取Unity配置文件创建Unity容器。需要引入程序集“System.Configuration”
新建类:UnityControllerFactory 用于自定义控制器实例化工厂。需要引入程序集“System.Web”。
分享代码:
1 using Microsoft.Practices.Unity; 2 using Microsoft.Practices.Unity.Configuration; 3 using System; 4 using System.Collections.Generic; 5 using System.Configuration; 6 using System.IO; 7 using System.Linq; 8 using System.Text; 9 using System.Threading.Tasks;10 using Unity;11 12 namespace Ypf.Utils13 {14 /// 15 /// 依赖注入工厂(单例的 采用双if+lock锁)16 /// 读取Unity的配置文件,并创建Unity容器17 /// 18 public class DIFactory19 {20 //静态的私有变量充当Lock锁21 private static object _lock = new object();22 private static Dictionary _UnityDictory = new Dictionary();23 24 /// 25 /// 获取Unity容器26 /// 27 /// 对应配置文件中节点的名称,同时也当做字典中的key值28 /// 29 public static IUnityContainer GetContainer(string containerName = "EFContainer")30 {31 if (!_UnityDictory.ContainsKey(containerName))32 {33 lock (_lock)34 {35 if (!_UnityDictory.ContainsKey(containerName))36 {37 //1. 固定的4行代码读取配置文件38 ExeConfigurationFileMap fileMap = new ExeConfigurationFileMap();39 fileMap.ExeConfigFilename = Path.Combine(AppDomain.CurrentDomain.BaseDirectory + "CfgFiles甥楮祴Config.xml");//找配置文件的路径40 Configuration configuration = ConfigurationManager.OpenMappedExeConfiguration(fileMap, ConfigurationUserLevel.None);41 UnityConfigurationSection section = (UnityConfigurationSection)configuration.GetSection(UnityConfigurationSection.SectionName);42 //2. Unity层次的步骤43 IUnityContainer container = new UnityContainer();44 section.Configure(container, containerName);45 //3.将创建好的容器放到字典里46 _UnityDictory.Add(containerName, container);47 }48 } 49 }50 return _UnityDictory[containerName];51 }52 }53 }
1 using System; 2 using System.Collections.Generic; 3 using System.Linq; 4 using System.Text; 5 using System.Threading.Tasks; 6 using System.Web.Mvc; 7 using System.Web.Routing; 8 using Unity; 9 10 namespace Ypf.Utils11 {12 /// 13 /// 自定义控制器实例化工厂14 /// 15 public class UnityControllerFactory : DefaultControllerFactory16 {17 private IUnityContainer UnityContainer18 {19 get20 {21 return DIFactory.GetContainer();22 }23 }24 25 /// 26 /// 创建控制器对象27 /// 28 /// 29 /// 30 /// 31 protected override IController GetControllerInstance(RequestContext requestContext, Type controllerType)32 {33 if (null == controllerType)34 {35 return null;36 }37 IController controller = (IController)this.UnityContainer.Resolve(controllerType);38 return controller;39 }40 41 /// 42 /// 释放控制器43 /// 44 /// 45 public override void ReleaseController(IController controller)46 {47 //this.UnityContainer.Teardown(controller);//释放对象(老版本)48 49 base.ReleaseController(controller);50 }51 }52 }
(3). 通过Nuget给【Ypf.AdminWeb】层引入“Unity”的程序集,并新建CfgFiles文件夹和UnityConfig.xml文件,该xml文件需要改属性为“始终复制”。
分享代码:
1 2 3 4 5 6 7 8 9 10 11 12 15 16 17 18 19 20 21 22 23 24 25 26 27
(4). 将【Ypf.Service】层的程序集生成路径改为:..Ypf.AdminWebbin
(5). 在【Ypf.AdminWeb】层中的Global文件中进行注册 ,用Unity代替原有的控制器创建流程.
//注册自定义实例化控制器的容器(利用Unity代替原有的控制器创建流程)
ControllerBuilder.Current.SetControllerFactory(new UnityControllerFactory());
PS:横向对比,AutoFac中也有一句类似的话:
在【Ypf.AdminWeb】层测试Untiy的IOC和DI【测试通过】
5. 将Log4net整合到Ypf.Utils层中。
(1). 通过Nuget给【Ypf.Utils】层添加“Log4net”程序集。
(2). 新建Log文件,拷贝“log4net.xml”和“LogUtils.cs”两个类文件,“log4net.xml”要改为嵌入的资源。
(3). 在【Ypf.Admin】层的Global文件中进行注册。LogUtils.InitLog4Net();
(4). 解析:主要配置了两种模式,输出到“txt文本文档”和“SQLServer数据库中”。其中“文本文档”又分了两种模式,全部输入到一个文档中 和 不同类型的日志输入到不同文档下,在调用的时候通过传入参数来区分存放在哪个文件夹下。
代码详见下面的实战测试。
6. 完善【Ypf.Service】层中BaseService的封装,封装EF常用的增删改查的方法,这里暂时先不扩展EF插件的方法。
代码见上
7. 如何控制EF上下文中的生命周期呢?
在配置文件中可以通过lifetime这个节点进行配置,而上一套框架的模式是直接通过using的模式进行配置,这里可以使用默认的方式:每次使用时候都创建。
详见Unity专题:
Unity深入浅出(一)
Unity深入浅出(二)
三. 剖析核心
1. 相同数据库结构,不同类型的数据库如何快速切换。
解析:首先需要明白的是不同的数据库切换,实质上切换的就是 EF的上下文,该框架的模式EF的是使用Unity通过xxxService中子类的构造函数注入,需要在配置文件中配置构造函数注入EF上下文。
所以这里切换数据库(eg:SQLServer→MySQL)只需要通过Nuget引出EF对应数据库的程序集,编写好配置文件,将SQLServer的EF上下文(MyDbContext1)切换成MySQL的上下文即可。
【需要测试】
2. 在一个方法中如何同时访问多个数据库,并对其进行事务一体的增删改操作。
解析:首先需要在BaseService的构造函数参数拼写多个DbContext参数,
其次子类xxxService中同样也需要多个DbContext参数,当多个参数时候,需要通过命名的方式指定注入,否则相互覆盖,不能分别注入。
最后,Unity的配置文件也需要通过命名的方式进行注入。
思考,Dependency特性写在父类BaseService中是否可以?
答案:经测试,不可以,EF的命名方式的构造函数注入要写在子类xxxService中。
同时会带来一个弊端?
由于BaseSevice类中泛型方法中的db,直接使用默认一个数据库的时候的db属性,所有导致当一个方法中如果涉及到多个上下文,没法直接使用BaseService中的封装方法,需要写原生代码,有点麻烦。
如下图:
那么如何解决这个问题?
3. 连接多个数据库框架的局限性,如何改进。
将BaseSevice中的泛型方法使用的db通过参数的形式进行传入,而且默认为一个数据库时候对应的DbContext属性,这样当只有一个数据库的时候,不用管它,因为他有默认值;当需要同时操控数据库的时候,在子类XXXService中,根据需要传入相应的db接口。
【经测试,不可以,提示 默认参数必须是编译时候的常量】
后续将采用别的方案进行处理,请期待。
四. 实战测试
这里准备两个数据库,分别是:YpfFrame_DB 和 YpfFrameTest_DB
①:YpfFrame_DB中,用到了表:T_SysUser 和 T_SysLoginLog,表结构如下
②. YpfFrameTest_DB 表中用到了T_SchoolInfor,表结构如下
开始测试
1. 测试增删改查,包括基本的事务一体。
在【Ypf.IService】层中新建ITestService接口,在【Ypf.Service】层中新建TestService类,实现ITestService接口, 定义TestBasicCRUD方法,进行测试,代码如下。
1 /// 2 /// 1.测试基本的增删改查,事务一体 3 /// 4 /// 5 public int TestBasicCRUD() 6 { 7 //1.增加操作 8 T_SysUser t_SysUser = new T_SysUser() 9 {10 id = Guid.NewGuid().ToString("N"),11 userAccount = "123456