一. 前言
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<T>的方式调用父类BaseService中的泛型方法,db是通过子类构造函数传到父类BaseService构造函数中。
分享BaseService类和IBaseService接口:
using System;
using System.Collections.Generic;
using System.Data.Entity;
using System.Data.SqlClient;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using System.Text;
using System.Threading.Tasks;
using Ypf.IService; namespace Ypf.Service
{
public class BaseService: IBaseService
{
/// <summary>
/// 一个属性,在该类中使用
/// </summary>
public DbContext db { get; private set; } /// <summary>
/// 通过构造函数传入EF的上下文
/// 该上下文可能是同种类型的不同数据库、也可能是相同结构的不同类型的数据库
/// 为后面的Untiy的构造函数注入埋下伏笔
/// </summary>
/// <param name="db"></param>
public BaseService(DbContext db)
{
this.db = db;
} //1. 直接提交数据库 #region 01-数据源
public IQueryable<T> Entities<T>() where T : class
{
return db.Set<T>();
} #endregion #region 02-新增
public int Add<T>(T model) where T : class
{
DbSet<T> dst = db.Set<T>();
dst.Add(model);
return db.SaveChanges(); }
#endregion #region 03-删除(适用于先查询后删除 单个)
/// <summary>
/// 删除(适用于先查询后删除的单个实体)
/// </summary>
/// <param name="model">需要删除的实体</param>
/// <returns></returns>
public int Del<T>(T model) where T : class
{
db.Set<T>().Attach(model);
db.Set<T>().Remove(model);
return db.SaveChanges();
}
#endregion #region 04-根据条件删除(支持批量删除)
/// <summary>
/// 根据条件删除(支持批量删除)
/// </summary>
/// <param name="delWhere">传入Lambda表达式(生成表达式目录树)</param>
/// <returns></returns>
public int DelBy<T>(Expression<Func<T, bool>> delWhere) where T : class
{
List<T> listDels = db.Set<T>().Where(delWhere).ToList();
listDels.ForEach(d =>
{
db.Set<T>().Attach(d);
db.Set<T>().Remove(d);
});
return db.SaveChanges();
}
#endregion #region 05-单实体修改
/// <summary>
/// 修改
/// </summary>
/// <param name="model">修改后的实体</param>
/// <returns></returns>
public int Modify<T>(T model) where T : class
{
db.Entry(model).State = EntityState.Modified;
return db.SaveChanges();
}
#endregion #region 06-批量修改(非lambda)
/// <summary>
/// 批量修改(非lambda)
/// </summary>
/// <param name="model">要修改实体中 修改后的属性 </param>
/// <param name="whereLambda">查询实体的条件</param>
/// <param name="proNames">lambda的形式表示要修改的实体属性名</param>
/// <returns></returns>
public int ModifyBy<T>(T model, Expression<Func<T, bool>> whereLambda, params string[] proNames) where T : class
{
List<T> listModifes = db.Set<T>().Where(whereLambda).ToList();
Type t = typeof(T);
List<PropertyInfo> proInfos = t.GetProperties(BindingFlags.Instance | BindingFlags.Public).ToList();
Dictionary<string, PropertyInfo> dicPros = new Dictionary<string, PropertyInfo>();
proInfos.ForEach(p =>
{
if (proNames.Contains(p.Name))
{
dicPros.Add(p.Name, p);
}
});
foreach (string proName in proNames)
{
if (dicPros.ContainsKey(proName))
{
PropertyInfo proInfo = dicPros[proName];
object newValue = proInfo.GetValue(model, null);
foreach (T m in listModifes)
{
proInfo.SetValue(m, newValue, null);
}
}
}
return db.SaveChanges();
}
#endregion #region 07-根据条件查询
/// <summary>
/// 根据条件查询
/// </summary>
/// <param name="whereLambda">查询条件(lambda表达式的形式生成表达式目录树)</param>
/// <returns></returns>
public List<T> GetListBy<T>(Expression<Func<T, bool>> whereLambda) where T : class
{
return db.Set<T>().Where(whereLambda).ToList();
}
#endregion #region 08-根据条件排序和查询
/// <summary>
/// 根据条件排序和查询
/// </summary>
/// <typeparam name="Tkey">排序字段类型</typeparam>
/// <param name="whereLambda">查询条件</param>
/// <param name="orderLambda">排序条件</param>
/// <param name="isAsc">升序or降序</param>
/// <returns></returns>
public List<T> GetListBy<T,Tkey>(Expression<Func<T, bool>> whereLambda, Expression<Func<T, Tkey>> orderLambda, bool isAsc = true) where T : class
{
List<T> list = null;
if (isAsc)
{
list = db.Set<T>().Where(whereLambda).OrderBy(orderLambda).ToList();
}
else
{
list = db.Set<T>().Where(whereLambda).OrderByDescending(orderLambda).ToList();
}
return list;
}
#endregion #region 09-分页查询
/// <summary>
/// 根据条件排序和查询
/// </summary>
/// <typeparam name="Tkey">排序字段类型</typeparam>
/// <param name="pageIndex">页码</param>
/// <param name="pageSize">页容量</param>
/// <param name="whereLambda">查询条件</param>
/// <param name="orderLambda">排序条件</param>
/// <param name="isAsc">升序or降序</param>
/// <returns></returns>
public List<T> GetPageList<T,Tkey>(int pageIndex, int pageSize, Expression<Func<T, bool>> whereLambda, Expression<Func<T, Tkey>> orderLambda, bool isAsc = true) where T : class
{ List<T> list = null;
if (isAsc)
{
list = db.Set<T>().Where(whereLambda).OrderBy(orderLambda)
.Skip((pageIndex - ) * pageSize).Take(pageSize).ToList();
}
else
{
list = db.Set<T>().Where(whereLambda).OrderByDescending(orderLambda)
.Skip((pageIndex - ) * pageSize).Take(pageSize).ToList();
}
return list;
}
#endregion #region 10-分页查询输出总行数
/// <summary>
/// 根据条件排序和查询
/// </summary>
/// <typeparam name="Tkey">排序字段类型</typeparam>
/// <param name="pageIndex">页码</param>
/// <param name="pageSize">页容量</param>
/// <param name="whereLambda">查询条件</param>
/// <param name="orderLambda">排序条件</param>
/// <param name="isAsc">升序or降序</param>
/// <returns></returns>
public List<T> GetPageList<T,Tkey>(int pageIndex, int pageSize, ref int rowCount, Expression<Func<T, bool>> whereLambda, Expression<Func<T, Tkey>> orderLambda, bool isAsc = true) where T : class
{
int count = ;
List<T> list = null;
count = db.Set<T>().Where(whereLambda).Count();
if (isAsc)
{
var iQueryList = db.Set<T>().Where(whereLambda).OrderBy(orderLambda)
.Skip((pageIndex - ) * pageSize).Take(pageSize); list = iQueryList.ToList();
}
else
{
var iQueryList = db.Set<T>().Where(whereLambda).OrderByDescending(orderLambda)
.Skip((pageIndex - ) * pageSize).Take(pageSize);
list = iQueryList.ToList();
}
rowCount = count;
return list;
}
#endregion //2. SaveChange剥离出来,处理事务 #region 01-批量处理SaveChange()
/// <summary>
/// 事务批量处理
/// </summary>
/// <returns></returns>
public int SaveChange()
{
return db.SaveChanges();
}
#endregion #region 02-新增
/// <summary>
/// 新增
/// </summary>
/// <param name="model">需要新增的实体</param>
public void AddNo<T>(T model) where T : class
{
db.Set<T>().Add(model);
}
#endregion #region 03-删除
/// <summary>
/// 删除
/// </summary>
/// <param name="model">需要删除的实体</param>
public void DelNo<T>(T model) where T : class
{
db.Entry(model).State = EntityState.Deleted;
}
#endregion #region 04-根据条件删除
/// <summary>
/// 条件删除
/// </summary>
/// <param name="delWhere">需要删除的条件</param>
public void DelByNo<T>(Expression<Func<T, bool>> delWhere) where T : class
{
List<T> listDels = db.Set<T>().Where(delWhere).ToList();
listDels.ForEach(d =>
{
db.Set<T>().Attach(d);
db.Set<T>().Remove(d);
});
}
#endregion #region 05-修改
/// <summary>
/// 修改
/// </summary>
/// <param name="model">修改后的实体</param>
public void ModifyNo<T>(T model) where T : class
{
db.Entry(model).State = EntityState.Modified;
}
#endregion //3. EF调用sql语句 #region 01-执行增加,删除,修改操作(或调用存储过程)
/// <summary>
/// 执行增加,删除,修改操作(或调用存储过程)
/// </summary>
/// <param name="sql"></param>
/// <param name="pars"></param>
/// <returns></returns>
public int ExecuteSql(string sql, params SqlParameter[] pars)
{
return db.Database.ExecuteSqlCommand(sql, pars);
} #endregion #region 02-执行查询操作
/// <summary>
/// 执行查询操作
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="sql"></param>
/// <param name="pars"></param>
/// <returns></returns>
public List<T> ExecuteQuery<T>(string sql, params SqlParameter[] pars) where T : class
{
return db.Database.SqlQuery<T>(sql, pars).ToList();
}
#endregion }
}
BaseService类
using System;
using System.Collections.Generic;
using System.Data.SqlClient;
using System.Linq;
using System.Linq.Expressions;
using System.Text;
using System.Threading.Tasks; namespace Ypf.IService
{
public interface IBaseService
{
//1. 直接提交数据库 #region 01-数据源
IQueryable<T> Entities<T>() where T : class; #endregion #region 02-新增
int Add<T>(T model) where T : class; #endregion #region 03-删除(适用于先查询后删除 单个)
/// <summary>
/// 删除(适用于先查询后删除的单个实体)
/// </summary>
/// <param name="model">需要删除的实体</param>
/// <returns></returns>
int Del<T>(T model) where T : class; #endregion #region 04-根据条件删除(支持批量删除)
/// <summary>
/// 根据条件删除(支持批量删除)
/// </summary>
/// <param name="delWhere">传入Lambda表达式(生成表达式目录树)</param>
/// <returns></returns>
int DelBy<T>(Expression<Func<T, bool>> delWhere) where T : class; #endregion #region 05-单实体修改
/// <summary>
/// 修改
/// </summary>
/// <param name="model">修改后的实体</param>
/// <returns></returns>
int Modify<T>(T model) where T : class; #endregion #region 06-批量修改(非lambda)
/// <summary>
/// 批量修改(非lambda)
/// </summary>
/// <param name="model">要修改实体中 修改后的属性 </param>
/// <param name="whereLambda">查询实体的条件</param>
/// <param name="proNames">lambda的形式表示要修改的实体属性名</param>
/// <returns></returns>
int ModifyBy<T>(T model, Expression<Func<T, bool>> whereLambda, params string[] proNames) where T : class; #endregion #region 07-根据条件查询
/// <summary>
/// 根据条件查询
/// </summary>
/// <param name="whereLambda">查询条件(lambda表达式的形式生成表达式目录树)</param>
/// <returns></returns>
List<T> GetListBy<T>(Expression<Func<T, bool>> whereLambda) where T : class; #endregion #region 08-根据条件排序和查询
/// <summary>
/// 根据条件排序和查询
/// </summary>
/// <typeparam name="Tkey">排序字段类型</typeparam>
/// <param name="whereLambda">查询条件</param>
/// <param name="orderLambda">排序条件</param>
/// <param name="isAsc">升序or降序</param>
/// <returns></returns>
List<T> GetListBy<T, Tkey>(Expression<Func<T, bool>> whereLambda, Expression<Func<T, Tkey>> orderLambda, bool isAsc = true) where T : class; #endregion #region 09-分页查询
/// <summary>
/// 根据条件排序和查询
/// </summary>
/// <typeparam name="Tkey">排序字段类型</typeparam>
/// <param name="pageIndex">页码</param>
/// <param name="pageSize">页容量</param>
/// <param name="whereLambda">查询条件</param>
/// <param name="orderLambda">排序条件</param>
/// <param name="isAsc">升序or降序</param>
/// <returns></returns>
List<T> GetPageList<T, Tkey>(int pageIndex, int pageSize, Expression<Func<T, bool>> whereLambda, Expression<Func<T, Tkey>> orderLambda, bool isAsc = true) where T : class; #endregion #region 10-分页查询输出总行数
/// <summary>
/// 根据条件排序和查询
/// </summary>
/// <typeparam name="Tkey">排序字段类型</typeparam>
/// <param name="pageIndex">页码</param>
/// <param name="pageSize">页容量</param>
/// <param name="whereLambda">查询条件</param>
/// <param name="orderLambda">排序条件</param>
/// <param name="isAsc">升序or降序</param>
/// <returns></returns>
List<T> GetPageList<T, Tkey>(int pageIndex, int pageSize, ref int rowCount, Expression<Func<T, bool>> whereLambda, Expression<Func<T, Tkey>> orderLambda, bool isAsc = true) where T : class; #endregion //2. SaveChange剥离出来,处理事务 #region 01-批量处理SaveChange()
/// <summary>
/// 事务批量处理
/// </summary>
/// <returns></returns>
int SaveChange(); #endregion #region 02-新增
/// <summary>
/// 新增
/// </summary>
/// <param name="model">需要新增的实体</param>
void AddNo<T>(T model) where T : class; #endregion #region 03-删除
/// <summary>
/// 删除
/// </summary>
/// <param name="model">需要删除的实体</param>
void DelNo<T>(T model) where T : class; #endregion #region 04-根据条件删除
/// <summary>
/// 条件删除
/// </summary>
/// <param name="delWhere">需要删除的条件</param>
void DelByNo<T>(Expression<Func<T, bool>> delWhere) where T : class; #endregion #region 05-修改
/// <summary>
/// 修改
/// </summary>
/// <param name="model">修改后的实体</param>
void ModifyNo<T>(T model) where T : class; #endregion //3. EF调用sql语句 #region 01-执行增加,删除,修改操作(或调用存储过程)
/// <summary>
/// 执行增加,删除,修改操作(或调用存储过程)
/// </summary>
/// <param name="sql"></param>
/// <param name="pars"></param>
/// <returns></returns>
int ExecuteSql(string sql, params SqlParameter[] pars); #endregion #region 02-执行查询操作
/// <summary>
/// 执行查询操作
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="sql"></param>
/// <param name="pars"></param>
/// <returns></returns>
List<T> ExecuteQuery<T>(string sql, params SqlParameter[] pars) where T : class; #endregion }
}
IBaseService接口
4. 利用Unity进行整合
(1). 通过Nuget给【Ypf.Utils】层引入“Unity”的程序集和“Microsoft.AspNet.Mvc”程序集。
(2). 新建类:DIFactory 用于读取Unity配置文件创建Unity容器。需要引入程序集“System.Configuration”
新建类:UnityControllerFactory 用于自定义控制器实例化工厂。需要引入程序集“System.Web”。
分享代码:
using Microsoft.Practices.Unity;
using Microsoft.Practices.Unity.Configuration;
using System;
using System.Collections.Generic;
using System.Configuration;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Unity; namespace Ypf.Utils
{
/// <summary>
/// 依赖注入工厂(单例的 采用双if+lock锁)
/// 读取Unity的配置文件,并创建Unity容器
/// </summary>
public class DIFactory
{
//静态的私有变量充当Lock锁
private static object _lock = new object();
private static Dictionary<string, IUnityContainer> _UnityDictory = new Dictionary<string, IUnityContainer>(); /// <summary>
/// 获取Unity容器
/// </summary>
/// <param name="containerName">对应配置文件中节点的名称,同时也当做字典中的key值</param>
/// <returns></returns>
public static IUnityContainer GetContainer(string containerName = "EFContainer")
{
if (!_UnityDictory.ContainsKey(containerName))
{
lock (_lock)
{
if (!_UnityDictory.ContainsKey(containerName))
{
//1. 固定的4行代码读取配置文件
ExeConfigurationFileMap fileMap = new ExeConfigurationFileMap();
fileMap.ExeConfigFilename = Path.Combine(AppDomain.CurrentDomain.BaseDirectory + "CfgFiles\\UnityConfig.xml");//找配置文件的路径
Configuration configuration = ConfigurationManager.OpenMappedExeConfiguration(fileMap, ConfigurationUserLevel.None);
UnityConfigurationSection section = (UnityConfigurationSection)configuration.GetSection(UnityConfigurationSection.SectionName);
//2. Unity层次的步骤
IUnityContainer container = new UnityContainer();
section.Configure(container, containerName);
//3.将创建好的容器放到字典里
_UnityDictory.Add(containerName, container);
}
}
}
return _UnityDictory[containerName];
}
}
}
DIFactory
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Web.Mvc;
using System.Web.Routing;
using Unity; namespace Ypf.Utils
{
/// <summary>
/// 自定义控制器实例化工厂
/// </summary>
public class UnityControllerFactory : DefaultControllerFactory
{
private IUnityContainer UnityContainer
{
get
{
return DIFactory.GetContainer();
}
} /// <summary>
/// 创建控制器对象
/// </summary>
/// <param name="requestContext"></param>
/// <param name="controllerType"></param>
/// <returns></returns>
protected override IController GetControllerInstance(RequestContext requestContext, Type controllerType)
{
if (null == controllerType)
{
return null;
}
IController controller = (IController)this.UnityContainer.Resolve(controllerType);
return controller;
} /// <summary>
/// 释放控制器
/// </summary>
/// <param name="controller"></param>
public override void ReleaseController(IController controller)
{
//this.UnityContainer.Teardown(controller);//释放对象(老版本) base.ReleaseController(controller);
}
}
}
UnityControllerFactory
(3). 通过Nuget给【Ypf.AdminWeb】层引入“Unity”的程序集,并新建CfgFiles文件夹和UnityConfig.xml文件,该xml文件需要改属性为“始终复制”。
分享代码:
<configuration>
<configSections>
<section name="unity" type="Microsoft.Practices.Unity.Configuration.UnityConfigurationSection, Unity.Configuration"/>
</configSections>
<unity>
<!-- unity容器配置注册节点-->
<containers>
<!--容器配置方式一:类型名称和程序集名称全部写在容器中-->
<container name="EFContainer">
<!-- type中的两个参数分别是:类型名称和DLL程序集的名称 -->
<!-- mapTo中的两个参数分别是:类型名称和DLL程序集的名称 -->
<!--
分析:这里我们需要使用的是TestService,但不直接使用它,而是使用它的接口,即将【mapTo】里的类型注册给【type】里的类型
-->
<register type="Ypf.IService.ITestService,Ypf.IService" mapTo="Ypf.Service.TestService,Ypf.Service"/>
<register type="Ypf.IService.ITestService2,Ypf.IService" mapTo="Ypf.Service.TestService2,Ypf.Service"/>
<!--调用构造函数注入-->
<!--1.TestService需要依赖BaseService的构造函数,所以要对其进行注入-->
<register type="Ypf.IService.IBaseService,Ypf.IService" mapTo="Ypf.Service.BaseService, Ypf.Service"/>
<!--2.TestService需要传入EF的上下文,所以要对其进行注入-->
<register type="System.Data.Entity.DbContext, EntityFramework" mapTo="Ypf.Data.MyDBContext1, Ypf.Data" name="db"/>
<register type="System.Data.Entity.DbContext, EntityFramework" mapTo="Ypf.Data.MyDBContext2, Ypf.Data" name="db2"/> </container>
</containers>
</unity>
</configuration>
UnityConfig.xml
(4). 将【Ypf.Service】层的程序集生成路径改为:..\Ypf.AdminWeb\bin\
(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专题:
三. 剖析核心
1. 相同数据库结构,不同类型的数据库如何快速切换。
解析:首先需要明白的是不同的数据库切换,实质上切换的就是 EF的上下文,该框架的模式EF的是使用Unity通过xxxService中子类的构造函数注入,需要在配置文件中配置构造函数注入EF上下文。
<register type="System.Data.Entity.DbContext, EntityFramework" mapTo="Ypf.Data.MyDBContext1, Ypf.Data" />
所以这里切换数据库(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方法,进行测试,代码如下。
/// <summary>
/// 1.测试基本的增删改查,事务一体
/// </summary>
/// <returns></returns>
public int TestBasicCRUD()
{
//1.增加操作
T_SysUser t_SysUser = new T_SysUser()
{
id = Guid.NewGuid().ToString("N"),
userAccount = "",
userPwd = "XXX",
userRealName = "XXX",
appLoginNum = ,
addTime = DateTime.Now
};
this.AddNo<T_SysUser>(t_SysUser); //2.修改操作
T_SysLoginLog t_SysLoginLog = this.Entities<T_SysLoginLog>().Where(u => u.id == "").FirstOrDefault();
if (t_SysLoginLog != null)
{
t_SysLoginLog.userId = "xxx";
t_SysLoginLog.userName = "xxx";
this.ModifyNo<T_SysLoginLog>(t_SysLoginLog);
}
//3.提交操作
return db.SaveChanges();
}
2. 测试一个方法中查询多个数据库。
在ITestService接口中定义ConnectManyDB方法,并在TestService中实现该方法,代码如下:
3. 测试一个方法中事务一体处理多个数据库的crud操作。
在ITestService接口中定义ManyDBTransaction方法,并在TestService中实现该方法,同样需要在构造函数中注入多个EF上下文,代码如下:
[InjectionConstructor]
public TestService([Dependency("db")]DbContext db, [Dependency("db2")]DbContext db2) : base(db, db2)
{ }
/// <summary>
/// 3. 同时对多个数据库进行事务一体的CRUD操作
/// 注:需要手动开启msdtc服务(net start msdtc)
/// </summary>
public void ManyDBTransaction()
{
using (TransactionScope trans = new TransactionScope())
{
try
{
var data1 = db.Set<T_SysUser>().Where(u => u.id == "").FirstOrDefault();
if (data1 != null)
{
db.Set<T_SysUser>().Attach(data1);
db.Set<T_SysUser>().Remove(data1);
db.SaveChanges();
}
var data2 = db2.Set<T_SchoolInfor>().Where(u => u.id == "").FirstOrDefault();
if (data2 != null)
{
db2.Set<T_SchoolInfor>().Attach(data2);
db2.Set<T_SchoolInfor>().Remove(data2);
db2.SaveChanges();
} //最终提交事务
trans.Complete();
}
catch (Exception ex)
{
var msg = ex.Message;
//事务回滚
Transaction.Current.Rollback();
throw;
}
}
}
分析:同时连接多个数据库,并对多个数据库进行事务性的crud操作,这个时候必须用 【TransactionScope事务】需要引入System.Transactions程序集,前提要手动 【net start msdtc 】开启对应服务,这样整个事务通过“Complete”方法进行提交,通过Transaction.Current.Rollback()方法进行事务回滚,各自db的SaveChange不起作用,但还是需要SaveChange的。
4. 测试xxxSevice子类中也可以通过Unity进行IxxxService的属性模式进行属性的注入。
PS:为了与前一节中AutoFac相呼应
在【Ypf.IService】层中新建ITestService2接口,在【Ypf.Service】层中新建TestService2类,实现ITestService接口, 定义GetUserInfor方法,进行测试,代码如下。
public class TestService2 : BaseService, ITestService2
{
/// <summary>
/// 调用父类的构造函数,这里的db通过Unity的配置文件实现构造函数注入
/// </summary>
/// <param name="db"></param>
[InjectionConstructor]
public TestService2([Dependency("db")]DbContext db, [Dependency("db2")]DbContext db2) : base(db, db2)
{ } public List<T_SysUser> GetUserInfor()
{
return this.GetListBy<T_SysUser>(u => true);
}
}
在TestService中定义ITestService2属性,如下:
在TestService中定义如下方法,内部用TestService2进行调用,可以调用成功,从而证明xxxSevice子类中也可以通过Unity进行IxxxService的模式进行“属性的注入”。
5. 测试Log4net的分文件夹和不分文件的使用。
分享配置文件
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
<!-- 一. 添加log4net的自定义配置节点-->
<configSections>
<section name="log4net" type="log4net.Config.Log4NetConfigurationSectionHandler,log4net" />
</configSections>
<!--二. log4net的核心配置代码-->
<log4net>
<!--1. 输出途径(一) 将日志以回滚文件的形式写到文件中--> <!--模式一:全部存放到一个文件夹里-->
<appender name="log0" type="log4net.Appender.RollingFileAppender">
<!--1.1 文件夹的位置(也可以写相对路径)-->
<param name="File" value="D:\MyLog\" />
<!--相对路径-->
<!--<param name="File" value="Logs/" />-->
<!--1.2 是否追加到文件-->
<param name="AppendToFile" value="true" />
<!--1.3 使用最小锁定模型(minimal locking model),以允许多个进程可以写入同一个文件 -->
<lockingModel type="log4net.Appender.FileAppender+MinimalLock" />
<!--1.4 配置Unicode编码-->
<Encoding value="UTF-8" />
<!--1.5 是否只写到一个文件里-->
<param name="StaticLogFileName" value="false" />
<!--1.6 配置按照何种方式产生多个日志文件 (Date:日期、Size:文件大小、Composite:日期和文件大小的混合方式)-->
<param name="RollingStyle" value="Composite" />
<!--1.7 介绍多种日志的的命名和存放在磁盘的形式-->
<!--1.7.1 在根目录下直接以日期命名txt文件 注意"的位置,去空格 -->
<param name="DatePattern" value="yyyy-MM-dd".log"" />
<!--1.7.2 在根目录下按日期产生文件夹,文件名固定 test.log -->
<!--<param name="DatePattern" value="yyyy-MM-dd/"test.log"" />-->
<!--1.7.3 在根目录下按日期产生文件夹,这是按日期产生文件夹,并在文件名前也加上日期 -->
<!--<param name="DatePattern" value="yyyyMMdd/yyyyMMdd"-test.log"" />-->
<!--1.7.4 在根目录下按日期产生文件夹,这再形成下一级固定的文件夹 -->
<!--<param name="DatePattern" value="yyyyMMdd/"OrderInfor/test.log"" />-->
<!--1.8 配置每个日志的大小。【只在1.6 RollingStyle 选择混合方式与文件大小方式下才起作用!!!】可用的单位:KB|MB|GB。不要使用小数,否则会一直写入当前日志,
超出大小后在所有文件名后自动增加正整数重新命名,数字最大的最早写入。-->
<param name="maximumFileSize" value="10MB" />
<!--1.9 最多产生的日志文件个数,超过则保留最新的n个 将value的值设置-1,则不限文件个数 【只在1.6 RollingStyle 选择混合方式与文件大小方式下才起作用!!!】
与1.8中maximumFileSize文件大小是配合使用的-->
<param name="MaxSizeRollBackups" value="5" />
<!--1.10 配置文件文件的布局格式,使用PatternLayout,自定义布局-->
<layout type="log4net.Layout.PatternLayout">
<conversionPattern value="记录时间:%date %n线程ID:[%thread] %n日志级别:%-5level %n出错类:%logger property: [%property{NDC}] - %n错误描述:%message%newline %n%newline"/>
</layout>
</appender> <!--模式二:分文件夹存放-->
<!--文件夹1-->
<appender name="log1" type="log4net.Appender.RollingFileAppender">
<param name="File" value="D:\MyLog\OneLog\" />
<param name="AppendToFile" value="true" />
<lockingModel type="log4net.Appender.FileAppender+MinimalLock" />
<Encoding value="UTF-8" />
<param name="StaticLogFileName" value="false" />
<param name="RollingStyle" value="Composite" />
<param name="DatePattern" value="yyyy-MM-dd".log"" />
<param name="maximumFileSize" value="10MB" />
<param name="MaxSizeRollBackups" value="5" />
<layout type="log4net.Layout.PatternLayout">
<conversionPattern value="%message%newline" />
</layout>
<!--下面是利用过滤器进行分文件夹存放,两种过滤器进行配合-->
<!--与Logger名称(OneLog)匹配,才记录,-->
<filter type="log4net.Filter.LoggerMatchFilter">
<loggerToMatch value="OneLog" />
</filter>
<!--阻止所有的日志事件被记录-->
<filter type="log4net.Filter.DenyAllFilter" />
</appender>
<!--文件夹2-->
<appender name="log2" type="log4net.Appender.RollingFileAppender">
<param name="File" value="D:\MyLog\TwoLog\" />
<param name="AppendToFile" value="true" />
<lockingModel type="log4net.Appender.FileAppender+MinimalLock" />
<Encoding value="UTF-8" />
<param name="StaticLogFileName" value="false" />
<param name="RollingStyle" value="Composite" />
<param name="DatePattern" value="yyyy-MM-dd".log"" />
<param name="maximumFileSize" value="10MB" />
<param name="MaxSizeRollBackups" value="5" />
<layout type="log4net.Layout.PatternLayout">
<conversionPattern value="%message%newline" />
</layout>
<!--下面是利用过滤器进行分文件夹存放,两种过滤器进行配合-->
<!--与Logger名称(TwoLog)匹配,才记录,-->
<filter type="log4net.Filter.LoggerMatchFilter">
<loggerToMatch value="TwoLog" />
</filter>
<!--阻止所有的日志事件被记录-->
<filter type="log4net.Filter.DenyAllFilter" />
</appender> <!--2. 输出途径(二) 记录日志到数据库-->
<appender name="AdoNetAppender" type="log4net.Appender.AdoNetAppender">
<!--2.1 设置缓冲区大小,只有日志记录超设定值才会一块写入到数据库-->
<param name="BufferSize" value="1" />
<!--2.2 引用-->
<connectionType value="System.Data.SqlClient.SqlConnection, System.Data, Version=1.0.3300.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" />
<!--2.3 数据库连接字符串-->
<connectionString value="data source=localhost;initial catalog=LogDB;integrated security=false;persist security info=True;User ID=sa;Password=123456" />
<!--2.4 SQL语句插入到指定表-->
<commandText value="INSERT INTO LogInfor ([threadId],[log_level],[log_name],[log_msg],[log_exception],[log_time]) VALUES (@threadId, @log_level, @log_name, @log_msg, @log_exception,@log_time)" />
<!--2.5 数据库字段匹配-->
<!-- 线程号-->
<parameter>
<parameterName value="@threadId" />
<dbType value="String" />
<size value="100" />
<layout type="log4net.Layout.PatternLayout">
<conversionPattern value="%thread" />
</layout>
</parameter>
<!--日志级别-->
<parameter>
<parameterName value="@log_level" />
<dbType value="String" />
<size value="100" />
<layout type="log4net.Layout.PatternLayout">
<conversionPattern value="%level" />
</layout>
</parameter>
<!--日志记录类名称-->
<parameter>
<parameterName value="@log_name" />
<dbType value="String" />
<size value="100" />
<layout type="log4net.Layout.PatternLayout">
<conversionPattern value="%logger" />
</layout>
</parameter>
<!--日志信息-->
<parameter>
<parameterName value="@log_msg" />
<dbType value="String" />
<size value="5000" />
<layout type="log4net.Layout.PatternLayout">
<conversionPattern value="%message" />
</layout>
</parameter>
<!--异常信息 指的是如Infor 方法的第二个参数的值-->
<parameter>
<parameterName value="@log_exception" />
<dbType value="String" />
<size value="2000" />
<layout type="log4net.Layout.ExceptionLayout" />
</parameter>
<!-- 日志记录时间-->
<parameter>
<parameterName value="@log_time" />
<dbType value="DateTime" />
<layout type="log4net.Layout.RawTimeStampLayout" />
</parameter>
</appender> <!--(二). 配置日志的的输出级别和加载日志的输出途径-->
<root>
<!--1. level中的value值表示该值及其以上的日志级别才会输出-->
<!--OFF > FATAL(致命错误) > ERROR(一般错误) > WARN(警告) > INFO(一般信息) > DEBUG(调试信息) > ALL -->
<!--OFF表示所有信息都不写入,ALL表示所有信息都写入-->
<level value="ALL"></level>
<!--2. append-ref标签表示要加载前面的日志输出途径代码 通过ref和appender标签的中name属性相关联--> <appender-ref ref="log0"></appender-ref>
<appender-ref ref="log1"></appender-ref>
<appender-ref ref="log2"></appender-ref> <!--<appender-ref ref="AdoNetAppender"></appender-ref>-->
</root>
</log4net> </configuration>
分享对应帮助类封装
using log4net;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reflection;
using System.Text;
using System.Threading.Tasks; namespace Ypf.Utils.Log
{
public class LogUtils
{
//声明文件夹名称(这里分两个文件夹)
static string log1Name = "OneLog";
static string log2Name = "TwoLog"; //可以声明多个日志对象
//模式一:不分文件夹(所有的log对存放在这一个文件夹下)
public static ILog log = LogManager.GetLogger(typeof(LogUtils)); //模式二:分文件夹
//如果是要分文件夹存储,这里的名称需要和配置文件中loggerToMatch节点中的value相配合
//1. OneLog文件夹
public static ILog log1 = LogManager.GetLogger(log1Name);
//2. TwoLog文件夹
public static ILog log2 = LogManager.GetLogger(log2Name); #region 01-初始化Log4net的配置
/// <summary>
/// 初始化Log4net的配置
/// xml文件一定要改为嵌入的资源
/// </summary>
public static void InitLog4Net()
{
Assembly assembly = Assembly.GetExecutingAssembly();
var xml = assembly.GetManifestResourceStream("Ypf.Utils.Log.log4net.xml");
log4net.Config.XmlConfigurator.Configure(xml);
}
#endregion /************************* 五种不同日志级别 *******************************/
//FATAL(致命错误) > ERROR(一般错误) > WARN(警告) > INFO(一般信息) > DEBUG(调试信息) #region 00-将调试的信息输出,可以定位到具体的位置(解决高层封装带来的问题)
/// <summary>
/// 将调试的信息输出,可以定位到具体的位置(解决高层封装带来的问题)
/// </summary>
/// <returns></returns>
private static string getDebugInfo()
{
StackTrace trace = new StackTrace(true);
return trace.ToString();
}
#endregion #region 01-DEBUG(调试信息)
/// <summary>
/// DEBUG(调试信息)
/// </summary>
/// <param name="msg">日志信息</param>
/// <param name="logName">文件夹名称</param>
public static void Debug(string msg, string logName = "")
{
if (logName == "")
{
log.Debug(getDebugInfo() + msg);
}
else if (logName == log1Name)
{
log1.Debug(msg);
}
else if (logName == log2Name)
{
log2.Debug(msg);
}
}
/// <summary>
/// Debug
/// </summary>
/// <param name="msg">日志信息</param>
/// <param name="exception">错误信息</param>
public static void Debug(string msg, Exception exception)
{
log.Debug(getDebugInfo() + msg, exception);
} #endregion #region 02-INFO(一般信息)
/// <summary>
/// INFO(一般信息)
/// </summary>
/// <param name="msg">日志信息</param>
/// <param name="logName">文件夹名称</param>
public static void Info(string msg, string logName = "")
{
if (logName == "")
{
log.Info(getDebugInfo() + msg);
}
else if (logName == log1Name)
{
log1.Info(msg);
}
else if (logName == log2Name)
{
log2.Info(msg);
}
}
/// <summary>
/// Info
/// </summary>
/// <param name="msg">日志信息</param>
/// <param name="exception">错误信息</param>
public static void Info(string msg, Exception exception)
{
log.Info(getDebugInfo() + msg, exception);
}
#endregion #region 03-WARN(警告)
/// <summary>
///WARN(警告)
/// </summary>
/// <param name="msg">日志信息</param>
/// <param name="logName">文件夹名称</param>
public static void Warn(string msg, string logName = "")
{
if (logName == "")
{
log.Warn(getDebugInfo() + msg);
}
else if (logName == log1Name)
{
log1.Warn(msg);
}
else if (logName == log2Name)
{
log2.Warn(msg);
}
}
/// <summary>
/// Warn
/// </summary>
/// <param name="msg">日志信息</param>
/// <param name="exception">错误信息</param>
public static void Warn(string msg, Exception exception)
{
log.Warn(getDebugInfo() + msg, exception);
}
#endregion #region 04-ERROR(一般错误)
/// <summary>
/// ERROR(一般错误)
/// </summary>
/// <param name="msg">日志信息</param>
/// <param name="logName">文件夹名称</param>
public static void Error(string msg, string logName = "")
{
if (logName == "")
{
log.Error(getDebugInfo() + msg);
}
else if (logName == log1Name)
{
log1.Error(msg);
}
else if (logName == log2Name)
{
log2.Error(msg);
}
}
/// <summary>
/// Error
/// </summary>
/// <param name="msg">日志信息</param>
/// <param name="exception">错误信息</param>
public static void Error(string msg, Exception exception)
{
log.Error(getDebugInfo() + msg, exception);
}
#endregion #region 05-FATAL(致命错误)
/// <summary>
/// FATAL(致命错误)
/// </summary>
/// <param name="msg">日志信息</param>
/// <param name="logName">文件夹名称</param>
public static void Fatal(string msg, string logName = "")
{
if (logName == "")
{
log.Fatal(getDebugInfo() + msg);
}
else if (logName == log1Name)
{
log1.Fatal(msg);
}
else if (logName == log2Name)
{
log2.Fatal(msg);
}
}
/// <summary>
/// Fatal
/// </summary>
/// <param name="msg">日志信息</param>
/// <param name="exception">错误信息</param>
public static void Fatal(string msg, Exception exception)
{
log.Fatal(getDebugInfo() + msg, exception);
} #endregion }
}
代码测试
五. 后续
后续将对比 Unity和AutoFac,对比这两套框架的搭建模式。
!
- 作 者 : Yaopengfei(姚鹏飞)
- 博客地址 : http://www.cnblogs.com/yaopengfei/
- 声 明1 : 本人才疏学浅,用郭德纲的话说“我是一个小学生”,如有错误,欢迎讨论,请勿谩骂^_^。
- 声 明2 : 原创博客请在转载时保留原文链接或在文章开头加上本人博客地址,否则保留追究法律责任的权利。