从壹开始前后端分离【 .NET Core2.2/3.0 +Vue2.0 】框架之九 || 依赖注入IoC学习 + AOP界面编程初探
本文3.0版本文章
本文是NetCore 2.2的,请不要套用3.0
1、如果看不懂本文,或者比较困难,先别着急问问题,我单写了一个关于依赖注入的小Demo,可以下载看看,多思考思考注入的原理:
https://github.com/anjoy8/BlogArti/tree/master/Blog.Core_IOC%26DI
2、重要:如果你实现了解耦,也就是 api 层只引用了 IService 和 IRepository 的话,那每次修改 service 层,都需要清理解决方案,重新编译项目,因为这个时候你的api层的dll,还是之前未修改的代码。
3、重要+ :请注意,依赖注入的目的不是为了解耦,依赖注入是为了控制反转,通俗来说,就是不用我们自己去 new 服务实例了,所以大家不需要一定去解耦(比如下文说到的我没有引用 Service层 和 Repository层),我下一个DDD系列,依赖注入就没有解耦,因为我用的是自带的注入,不是Autofac的反射dll ,我解耦的目的,是为了让大家更好的理解,服务是怎么注入到宿主容器里的。
说接上文,上回说到了《八 || API项目整体搭建 6.3 异步泛型+依赖注入初探》,后来的标题中,我把仓储两个字给去掉了,因为好像大家对这个模式很有不同的看法,嗯~可能还是我学艺不精,没有说到其中的好处,现在在学DDD领域驱动设计相关资料,有了好的灵感再给大家分享吧。
到目前为止我们的项目已经有了基本的雏形,后端其实已经可以搭建自己的接口列表了,框架已经有了规模,原本应该说vue了,但是呢,又听说近来Vue-cli已经从2.0升级到了3.0了,还变化挺大,前端大佬们,都不停歇呀。当然我还在学习当中,我也需要了解下有关3.0的特性,希望给没有接触到,或者刚刚接触到的朋友们,有一些帮助,当然我这个不是随波逐流,只是在众多的博文中,给大家一个入门参考,届时说3.0的时候,还是会说2.0的相关问题的。
虽然项目整体可以运行了,但是我还有几个小知识点要说下,主要是1、依赖注入和AOP相关知识;2、跨域代理等问题(因为Vue是基于Node开发的,与后台API接口不在同一个地址);3、实体类的DTO相关小问题;4、Redis缓存等;5、部署服务器中的各种坑;虽然都是很小的知识点,我还是都下给大家说下的,好啦,开始今天的讲解;
零、今天完成的绿色部分
一、依赖注入的理解和思考
更新(19-04-17):如果想好好的理解依赖注入,可以从以下几个方面入手:
1、项目之间引用是如何起作用的,比如为啥 api 层只是引用了 service 层,那为啥也能使用 repository 和 model 等多层的类?
2、项目在启动的时候,也就是运行时,是如何动态 获取和访问 每一个对象的实例的?也就是 new 的原理
3、项目中有 n 个类,对应 m 个实例等,那这些服务,都放在了哪里?肯定每一个项目都有专属自己的一块。如果项目不启动的话,内存里肯定是没有这些服务的。
4、使用接口(面向抽象)的好处?
5、在项目后期,如何业务需要要全部修改接口的实现类,比如想把 IA = new A();全部 改成 IA = new B();
6、反射的重要性,为什么要用到反射 dll ?
如果这些每一条自己都能说清楚,那肯定就知道依赖注入是干啥的了。
说到依赖,我就想到了网上有一个例子,依赖注入和工厂模式中的相似和不同:
(1)原始社会里,没有社会分工。须要斧子的人(调用者)仅仅能自己去磨一把斧子(被调用者)。相应的情形为:软件程序里的调用者自己创建被调用者。
(2)进入工业社会,工厂出现。斧子不再由普通人完毕,而在工厂里被生产出来,此时须要斧子的人(调用者)找到工厂,购买斧子,无须关心斧子的制造过程。相应软件程序的简单工厂的设计模式。
(3)进入“按需分配”社会,需要斧子的人不需要找到工厂,坐在家里发出一个简单指令:须要斧子。斧子就自然出如今他面前。相应Spring的依赖注入。
在上篇文章中,我们已经了解到了,什么是依赖倒置、控制反转(IOC),什么是依赖注入(DI),网上这个有很多很多的讲解,我这里就不说明了,其实主要是见到这样的,就是存在依赖
public class A : D { public A(B b) { // do something } C c = new C(); }
就比如我们的项目中的BlogController,只要是通过new 实例化的,都是存在依赖
public async Task<List<Advertisement>> Get(int id) { IAdvertisementServices advertisementServices = new AdvertisementServices(); return await advertisementServices.Query(d => d.Id == id); }
使用依赖注入呢,有以下优点:
- 传统的代码,每个对象负责管理与自己需要依赖的对象,导致如果需要切换依赖对象的实现类时,需要修改多处地方。同时,过度耦合也使得对象难以进行单元测试。
- 依赖注入把对象的创造交给外部去管理,很好的解决了代码紧耦合(tight couple)的问题,是一种让代码实现松耦合(loose couple)的机制。
- 松耦合让代码更具灵活性,能更好地应对需求变动,以及方便单元测试。
举个栗子,就是关于日志记录的
日志记录:有时需要调试分析,需要记录日志信息,这时可以采用输出到控制台、文件、数据库、远程服务器等;假设最初采用输出到控制台,直接在程序中实例化ILogger logger = new ConsoleLogger(),但有时又需要输出到别的文件中,也许关闭日志输出,就需要更改程序,把ConsoleLogger改成FileLogger或者NoLogger, new FileLogger()或者new SqlLogger() ,此时不断的更改代码,就显得心里不好了,如果采用依赖注入,就显得特别舒畅。
我有一个个人的理解,不知道恰当与否,比如我们平时食堂吃饭,都是食堂自己炒的菜,就算是配方都一样,控制者(厨师)还是会有些变化,或者是出错,但是肯德基这种,由总店提供的,店面就失去了控制,就出现了第三方(总店或者工厂等),这就是实现了控制反转,我们只需要说一句,奥尔良鸡翅,嗯就拿出来一个,而且全部分店的都一样,我们不用管是否改配方,不管是否依赖哪些调理食材,哈哈。
二、常见的IoC框架有哪些
1、Autofac+原生
我常用的还是原生注入和 Autofac 注入。
Autofac:貌似目前net下用的最多吧
Ninject:目前好像没多少人用了
Unity:也是较为常见微软 core 自带的 DI
其实.Net Core 有自己的轻量级的IoC框架,
ASP.NET Core本身已经集成了一个轻量级的IOC容器,开发者只需要定义好接口后,在Startup.cs的ConfigureServices方法里使用对应生命周期的绑定方法即可,常见方法如下
services.AddTransient<IApplicationService,ApplicationService>//服务在每次请求时被创建,它最好被用于轻量级无状态服务(如我们的Repository和ApplicationService服务)
services.AddScoped<IApplicationService,ApplicationService>//服务在每次请求时被创建,生命周期横贯整次请求
services.AddSingleton<IApplicationService,ApplicationService>//Singleton(单例) 服务在第一次请求时被创建(或者当我们在ConfigureServices中指定创建某一实例并运行方法),其后的每次请求将沿用已创建服务。如果开发者的应用需要单例服务情景,请设计成允许服务容器来对服务生命周期进行操作,而不是手动实现单例设计模式然后由开发者在自定义类中进行操作。
当然.Net Core自身的容器还是比较简单,如果想要更多的功能和扩展,还是需要使用上边上个框架。
2、三种注入的生命周期
权重:
AddSingleton→AddTransient→AddScoped
AddSingleton的生命周期:
项目启动-项目关闭 相当于静态类 只会有一个
AddScoped的生命周期:
请求开始-请求结束 在这次请求中获取的对象都是同一个
AddTransient的生命周期:
请求获取-(GC回收-主动释放) 每一次获取的对象都不是同一个
这里来个简单的小DEMO:
1、定义四个接口,并分别对其各自接口实现,目的是测试Singleton,Scope,Transient三种,以及最后的 Service 服务:
public interface ISingTest { int Age { get; set; } string Name { get; set; } } public class SingTest: ISingTest { public int Age { get; set; } public string Name { get; set; } } //-------------------------- public interface ISconTest { int Age { get; set; } string Name { get; set; } } public class SconTest: ISconTest { public int Age { get; set; } public string Name { get; set; } } //-------------------------- public interface ITranTest { int Age { get; set; } string Name { get; set; } } public class TranTest: ITranTest { public int Age { get; set; } public string Name { get; set; } } //----------------------- public interface IAService { void RedisTest(); } public class AService : IAService { private ISingTest sing; ITranTest tran; ISconTest scon; public AService(ISingTest sing, ITranTest tran, ISconTest scon) { this.sing = sing; this.tran = tran; this.scon = scon; } public void RedisTest() { } }
2、项目注入
3、控制器调用
private ISingTest sing; ITranTest tran; ISconTest scon; IAService aService; public ValuesController(ISingTest sing, ITranTest tran, ISconTest scon, IAService aService) { this.sing = sing; this.tran = tran; this.scon = scon; this.aService = aService; } // GET api/values [HttpGet] public ActionResult<IEnumerable<string>> SetTest() { sing.Age = 18; sing.Name = "小红"; tran.Age = 19; tran.Name = "小明"; scon.Age = 20; scon.Name = "小蓝"; aService.RedisTest(); return new string[] { "value1", "value2" }; } // GET api/values/5 [HttpGet("{id}")] public ActionResult<string> Get(int id) { return "value"; }
4、开始测试,三种注入方法出现的情况
请求SetTest // GET api/values
AddSingleton的对象没有变
AddScoped的对象没有变化
AddTransient的对象发生变化
------------------------------------------------------------
请求 // GET api/values/5
AddSingleton的对象没有变
AddScoped的对象发生变化
AddTransient的对象发生变化
注意:
由于AddScoped对象是在请求的时候创建的
所以不能在AddSingleton对象中使用
甚至也不能在AddTransient对象中使用
所以权重为
AddSingleton→AddTransient→AddScoped
不然则会抛如下异常
三、较好用的IoC框架使用——Autofac
首先呢,我们要明白,我们注入是要注入到哪里——Controller API层。然后呢,我们看到了在接口调用的时候,如果需要其中的方法,需要using两个命名空间
[HttpGet("{id}", Name = "Get")] public async Task<List<Advertisement>> Get(int id) { IAdvertisementServices advertisementServices = new AdvertisementServices();//需要引用两个命名空间Blog.Core.IServices;Blog.Core.Services; return await advertisementServices.Query(d => d.Id == id); }
接下来我们就需要做处理:
1、引入nuget包
在Nuget中引入两个:Autofac.Extras.DynamicProxy(Autofac的动态代理,它依赖Autofac,所以可以不用单独引入Autofac)、Autofac.Extensions.DependencyInjection(Autofac的扩展)
2、接管ConfigureServices
让Autofac接管Starup中的ConfigureServices方法,记得修改返回类型IServiceProvider
(注意,netcore3.0+以上版本,不能这么写了,请查看我文章开通的微信公众号文章,或者看我的升级文档中的autofac部分 最全的 netcore 3.0 升级实战方案 )
public IServiceProvider ConfigureServices(IServiceCollection services) { services.AddMvc(); #region 配置信息 //Blog.Core.Repository.BaseDBConfig.ConnectionString = Configuration.GetSection("AppSettings:SqlServerConnection").Value; #endregion #region Swagger services.AddSwaggerGen(c => { c.SwaggerDoc("v1", new Info { Version = "v0.1.0", Title = "Blog.Core API", Description = "框架说明文档", TermsOfService = "None", Contact = new Swashbuckle.AspNetCore.Swagger.Contact { Name = "Blog.Core", Email = "Blog.Core@xxx.com", Url = "https://www.jianshu.com/u/94102b59cc2a" } }); //就是这里 #region 读取xml信息 var basePath = PlatformServices.Default.Application.ApplicationBasePath; var xmlPath = Path.Combine(basePath, "Blog.Core.xml");//这个就是刚刚配置的xml文件名 var xmlModelPath = Path.Combine(basePath, "Blog.Core.Model.xml");//这个就是Model层的xml文件名 c.IncludeXmlComments(xmlPath, true);//默认的第二个参数是false,这个是controller的注释,记得修改 c.IncludeXmlComments(xmlModelPath); #endregion #region Token绑定到ConfigureServices //添加header验证信息 //c.OperationFilter<SwaggerHeader>(); var security = new Dictionary<string, IEnumerable<string>> { { "Blog.Core", new string[] { } }, }; c.AddSecurityRequirement(security); //方案名称“Blog.Core”可自定义,上下一致即可 c.AddSecurityDefinition("Blog.Core", new ApiKeyScheme { Description = "JWT授权(数据将在请求头中进行传输) 直接在下框中输入{token}\"", Name = "Authorization",//jwt默认的参数名称 In = "header",//jwt默认存放Authorization信息的位置(请求头中) Type = "apiKey" }); #endregion }); #endregion #region Token服务注册 services.AddSingleton<IMemoryCache>(factory => { var cache = new MemoryCache(new MemoryCacheOptions()); return cache; }); services.AddAuthorization(options => { options.AddPolicy("Admin", policy => policy.RequireClaim("AdminType").Build());//注册权限管理,可以自定义多个 }); #endregion #region AutoFac //实例化 AutoFac 容器 var builder = new ContainerBuilder(); //注册要通过反射创建的组件 builder.RegisterType<AdvertisementServices>().As<IAdvertisementServices>(); //将services填充到Autofac容器生成器中 builder.Populate(services); //使用已进行的组件登记创建新容器 var ApplicationContainer = builder.Build(); #endregion return new AutofacServiceProvider(ApplicationContainer);//第三方IOC接管 core内置DI容器 }
这个时候我们就把AdvertisementServices的new 实例化过程注入到了Autofac容器中,
这个时候要看明白,前边的是实现类,后边的是接口,顺序不要搞混了。
3、构造函数方式来注入
依赖注入有三种方式(构造方法注入、setter方法注入和接口方式注入),我们平时基本都是使用其中的构造函数方式实现注入,
在BlogController中,添加构造函数,并在方法中,去掉实例化过程;
readonly IAdvertisementServices _advertisementServices;
/// <summary> /// 构造函数 /// </summary> /// <param name="advertisementServices"></param> public BlogController(IAdvertisementServices advertisementServices) { _advertisementServices = advertisementServices; }
[HttpGet("{id}", Name = "Get")] public async Task<List<Advertisement>> Get(int id) { //IAdvertisementServices advertisementServices = new AdvertisementServices();//需要引用两个命名空间Blog.Core.IServices;Blog.Core.Services; return await _advertisementServices.Query(d => d.Id == id); }
4、效果调试,已经成功
然后运行调试,发现在断点刚进入的时候,接口已经被实例化了,达到了注入的目的。
注意:这里经常会遇到一个错误:None of the constructors found with ........,
查看你的service服务,是不是用了其他的仓储repository,但是又缺少了构造函数。
如果没有问题,大家就需要想想,除了 Autofac 还有没有其他的不用第三方框架的注入方法呢?聪明如你,netcore 还真自带了注入扩展。
5、NetCore 自带的注入实现效果
当然,我们用 Asp.net core 自带的注入方式也是可以的,也挺简单的,这里先说下使用方法:
// 注入 service services.AddScoped<IAdvertisementServices, AdvertisementServices>(); // 注入 repository services.AddScoped<IAdvertisementRepository, AdvertisementRepository>();
这个时候,我们发现已经成功的注入了,而且特别简单,那为啥还要使用 Autofac 这种第三方扩展呢,我们想一想,上边我们仅仅是注入了一个 Service ,但是项目中有那么多的类,都要一个个手动添加么,多累啊,答案当然不是滴~
四、整个 dll 程序集批量注入
1、服务程序集注入方式 —— 未解耦(已采纳的方案)
通过反射将 Blog.Core.Services 和 Blog.Core.Repository 两个程序集的全部方法注入
修改如下代码,注意这个时候需要在项目依赖中,右键,添加引用 Blog.Core.Services 层和 Repository 层 到项目中,如下图,这个时候我们的程序依赖了具体的服务:
核心代码如下,注意这里是 Load 模式(程序集名):
var builder = new ContainerBuilder(); //注册要通过反射创建的组件 //builder.RegisterType<AdvertisementServices>().As<IAdvertisementServices>(); var assemblysServices = Assembly.Load("Blog.Core.Services");//要记得!!!这个注入的是实现类层,不是接口层!不是 IServices builder.RegisterAssemblyTypes(assemblysServices).AsImplementedInterfaces();//指定已扫描程序集中的类型注册为提供所有其实现的接口。 var assemblysRepository = Assembly.Load("Blog.Core.Repository");//模式是 Load(解决方案名) builder.RegisterAssemblyTypes(assemblysRepository).AsImplementedInterfaces(); //将services填充到Autofac容器生成器中 builder.Populate(services); //使用已进行的组件登记创建新容器 var ApplicationContainer = builder.Build();
其他不变,运行项目,一切正常,换其他接口也可以
到这里,Autofac依赖注入已经完成,基本的操作就是这样,不过可能你还没有真正体会到注入的好处,挑战下吧,看看下边的内容,将层级进行解耦试试!
注意,如果你不想多挑战,直接这么引用这两个实现层就行(Service 和 Repository),不是一定要用下边的方案来引用解耦。
而且我现在最新的在线项目,已经使用了不解耦的方案,并且去掉了仓储接口层,使用泛型仓储注入,更方便。
2、程序集注入 —— 实现层级引用的解耦
这是一个学习的思路,大家要多想想,可能会感觉无聊或者没用,但是对理解项目启动和加载,还是很有必要的。
1、项目最终只依赖抽象
最终的效果是这样的:工程只依赖抽象,把两个实现层删掉,引用这两个接口层。
2、配置仓储和服务层的程序集输出
将 Blog.Repository 层和 Service 层项目生成地址改成相对路径,这样大家就不用手动拷贝这两个 dll 了,F6编译的时候就直接生成到了 api 层 bin 下了:
“..\Blog.Core\bin\Debug\”
3、使用 LoadFile 加载服务层的程序集
var basePath = Microsoft.DotNet.PlatformAbstractions.ApplicationEnvironment.ApplicationBasePath;//获取项目路径 var servicesDllFile = Path.Combine(basePath, "Blog.Core.Services.dll");//获取注入项目绝对路径 var assemblysServices = Assembly.LoadFile(servicesDllFile);//直接采用加载文件的方法
3、其他依赖的接口也要注入
上边的代码,我们是把服务和仓储批量注入进去了,但是如果你看我的代码,最新的代码中,用到了事务,工作单元,并且用到了 ISqlSugarClient
这个接口不存在于我们的service.dll 和 Repository.dll 中,所以我们上边的批量注入,并不能够注入进去,那我们就需要单独对其进行注册,可以在api层的扩展文件夹中,找到:
public static class SqlsugarSetup { public static void AddSqlsugarSetup(this IServiceCollection services) { if (services == null) throw new ArgumentNullException(nameof(services)); // 默认添加主数据库连接 MainDb.CurrentDbConnId = Appsettings.app(new string[] { "MainDB" }); // 把多个连接对象注入服务,这里必须采用Scope,因为有事务操作 services.AddScoped<ISqlSugarClient>(o => { var listConfig = new List<ConnectionConfig>(); BaseDBConfig.MutiConnectionString.ForEach(m => { listConfig.Add(new ConnectionConfig() { ConfigId = m.ConnId.ObjToString().ToLower(), ConnectionString = m.Conn, DbType = (DbType)m.DbType, IsAutoCloseConnection = true, IsShardSameThread = false, AopEvents = new AopEvents { OnLogExecuting = (sql, p) => { // 多库操作的话,此处暂时无效果,在另一个地方有效,具体请查看BaseRepository.cs } }, MoreSettings = new ConnMoreSettings() { IsAutoRemoveDataCache = true } //InitKeyType = InitKeyType.SystemTable } ); }); return new SqlSugarClient(listConfig); }); } }
这一块内容涉及的比较多,还涉及到了 common 层中的 BaseDBConfig.cs 这个类,自己多多尝试吧。
五、注入中的问题和其他说明
如果按照上边的引用解耦的形式的话,可能我们编译成功后,页面能正常启动,证明我们已经把 Service 和 Repository 两个服务层的所有服务给注册上了,但是访问某一个接口,还是会出现错误:
这个错误表示,我们的 SqlSugar 服务,没有被注册成功,那肯定就是我们的 Sqlsugar 程序集没有正常的引用,怎么办呢,这里有两种方法,请往下看。
1、Sqlsugar 加载失败,方式一:api层引用 sugar
你可以直接在 api 层,添加对 sqlsugar 的使用,也可以直接不用修改,比如我的项目,多级之间存在级联的关系,api >> IService >> Model >> sqlSugar :
原因:在 Api层,引入 sqlSugarCore nuget 包,因为存在层级是存在依赖的
注意!这个地方很多人不理解为啥:
解耦,并不是啥直接不用了!
解耦仅仅是去掉引用耦合,目的是不用在修改了service.dll 层的某一个方法的时候,而停到api.dll这个整个服务,
当项目启动的时候,还是需要将所有的服务都注册到主机里,
autofac依赖注入,仅仅是用反射的方法,将service.dll 和 repository.dll 项目服务进行注册,这个过程和引用是一样的,因为如果你引用,当项目编译或启动的时候,也是把层服务全部注入到主机的过程,
所以sqlsugar还是需要引用,因为需要这个组件的服务。
2、方式二:不引用Sugar包!但是需要拷贝 .dll 文件
如果你就想要 api 层干净,就是不想引用 sqlsugar 层的话,那就除非是把 sugar下的所有dll文件都拷贝进去,其实这样也是可以的,只要把第三方的nuget包生成的dll文件全部拷贝就行,你可以看下,sqlsugar依赖了很多dll
但是这个时候我们需要使用 LoadFrom 模式,因为我们上边使用的是 LoadFile 作为反射加载,这样的话,有一个问题,就是Repository层引用的 sqlsugar 会提示找不到,那我们就换一个反射加载方式 —— LoadFrom:
//二者的区别
Assembly.LoadFile只载入相应的dll文件,比如Assembly.LoadFile("a.dll"),则载入a.dll,假如a.dll中引用了b.dll的话,b.dll并不会被载入。 Assembly.LoadFrom则不一样,它会载入dll文件及其引用的其他dll,比如上面的例子,b.dll也会被载入。
哦!是不是瞬间想到了什么,没错就是 Sqlsugar 加载问题,我们换成 LoadFrom ,并且删除引用 sqlsugar 来看看,但是!一定要把 sqlsugar.dll 和 它依赖的 MySql.Data.dll 给拷贝进来api层;
所以总体来说有三个情况:
1、loadfile 模式 + 引用 SqlSugar2、loadfile 模式+ 引用3、loadfrom 模式+ 拷贝 .dll 文件
3、连接字符串问题
注意:如果采用上边的方法,把 service 和 Repository 层虽然解耦了,但是必须采用 LoadFile( dll 文件) 的形式,这样就导致了,在 startup.cs 启动中,无法给其他类库中的静态属性赋值的能力,比如:
BaseDBConfig.ConnectionString = "数据库连接字符串";
这个在 startup.cs 的ConfigureServices 方法中,是无法生效的。解决办法:
1、不解耦,还是采用普通办法,引用两个层,用 Assembly.Load("Blog.Core.Services") 方式;
2、按照上边解耦,但是数据库连接字符串配置,需要在 Repostory 层
// public static string ConnectionString { get; set; }
public static string ConnectionString = Appsettings.app(new string[] { "AppSettings", "RedisCaching", "ConnectionString" });//获取连接字符串
//这个Appsettings是一个封装的操作类,用来获取配置文件的数据
/// <summary> /// appsettings.json操作类 /// </summary> public class Appsettings { static IConfiguration Configuration { get; set; } static string contentPath { get; set; } public Appsettings(string contentPath) { string Path = "appsettings.json"; //如果你把配置文件 是 根据环境变量来分开了,可以这样写 //Path = $"appsettings.{Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT")}.json"; Configuration = new ConfigurationBuilder() .SetBasePath(contentPath) .Add(new JsonConfigurationSource { Path = Path, Optional = false, ReloadOnChange = true })//这样的话,可以直接读目录里的json文件,而不是 bin 文件夹下的,所以不用修改复制属性 .Build(); } /// <summary> /// 封装要操作的字符 /// </summary> /// <param name="sections">节点配置</param> /// <returns></returns> public static string app(params string[] sections) { try { if (sections.Any()) { return Configuration[string.Join(":", sections)]; } } catch (Exception) { } return ""; } }
4、解除Service层和Repository层之间的耦合
还记得Blog.Core.Services中的BaseServices.cs么,它还是通过new 实例化的方式在创建,仿照contrller,修改BaseServices并在全部子类的构造函数中注入:
public class BaseServices<TEntity> : IBaseServices<TEntity> where TEntity : class, new() { //public IBaseRepository<TEntity> baseDal = new BaseRepository<TEntity>(); public IBaseRepository<TEntity> baseDal;//通过在子类的构造函数中注入,这里是基类,不用构造函数 //... } public class AdvertisementServices : BaseServices<Advertisement>, IAdvertisementServices { IAdvertisementRepository dal; public AdvertisementServices(IAdvertisementRepository dal) { this.dal = dal; base.baseDal = dal; } }
好啦,现在整个项目已经完成了相互直接解耦的功能,以后就算是Repository和Service如何变化,接口层都不用修改,因为已经完成了注入,第三方Autofac会做实例化的过程。
5、容器内查看注入的服务数据
如果你想看看是否注入到容器里成功了,可以直接看看容器 ApplicationContainer 的内容:
六、 无接口项目注入
1、接口形式的注入
上边我们讨论了很多,但是都是接口框架的,
比如:Service.dll 和与之对应的 IService.dll,Repository.dll和与之对应的 IRepository.dll,
这样,我们在多层之间使用服务的话,直接将我们需要使用的 new 对象,注入到容器里,然后我们就可以使用相应的接口了,
比如:我们想在 controller 里使用AdvertisementServices 类,那我们就可以直接使用它的接口 IAdvertisementServices,这样就很好的达到了解耦的目的,这样我们就可以在API层,就轻松的把 Service.dll 给解耦了;
如果我们需要在 Service类里,使用 AdvertisementRepository ,我们就直接使用对应的接口 IAdvertisementRepository,这样,我们就从 Service 层中,把仓储层给解耦了。
2、如果没有接口
案例是这样的:
如果我们的项目是这样的,没有接口,会怎么办:
// 服务层类 public class StudentService { StudentRepository _studentRepository; public StudentService(StudentRepository studentRepository) { _studentRepository = studentRepository; } public string Hello() { return _studentRepository.Hello(); } } // 仓储层类 public class StudentRepository { public StudentRepository() { } public string Hello() { return "hello world!!!"; } } // controller 接口调用 StudentService _studentService; public ValuesController(StudentService studentService) { _studentService = studentService; }
这样的话,我们就不能使用上边的接口注入模式了,因为我们上边是把注入的服务,对应注册给了接口了 .AsImplementedInterfaces() ,我们就无法实现解耦了,因为根本没有了接口层,所以我们只能引用实现类层,这样注入:
通过 builder.RegisterAssemblyTypes(assemblysRepository); 方法直接注入服务,没有其他的东西。
如果你需要这个小demo,可以从我的Github里下载:https://github.com/anjoy8/Blog.Core/blob/master/Blog.Core/wwwroot/NoInterAutofacIOC.rar
3、如果是没有接口的单独实体类
public class Love { // 一定要是虚方法 public virtual string SayLoveU() { return "I ♥ U"; } } //--------------------------- ////只能注入该类中的虚方法 builder.RegisterAssemblyTypes(Assembly.GetAssembly(typeof(Love))) .EnableClassInterceptors() .InterceptedBy(typeof(BlogLogAOP));
七、同一接口多实现类注入
这里暂时没有实例代码,如果你正好需要,可以看看这个博友的栗子:https://www.cnblogs.com/fuyujian/p/4115474.html
我会在之后的时间写个栗子放到这里。
一种:动态方式
// 定义一个接口,两个实现类 public interface IDemoService { string Get(); } public class DemoServiceA : IDemoService { public string Get() { return "Service A"; } } public class DemoServiceB : IDemoService { public string Get() { return "Service B"; } } // 依赖注入 services.AddSingleton(factory => { Func<string, IDemoService> accesor = key => { if (key.Equals("ServiceA")) { return factory.GetService<DemoServiceA>(); } else if (key.Equals("ServiceB")) { return factory.GetService<DemoServiceB>(); } else { throw new ArgumentException($"Not Support key : {key}"); } }; return accesor; }); // 使用 private IDemoService _serviceA; private IDemoService _serviceB; private readonly Func<string, IDemoService> _serviceAccessor; public ValuesController(Func<string, IDemoService> serviceAccessor) { this._serviceAccessor = serviceAccessor; _serviceA = _serviceAccessor("ServiceA"); _serviceB = _serviceAccessor("ServiceB"); }
二种,工厂模式
// 设计工厂 public class SingletonFactory { Dictionary<Type, Dictionary<string, object>> serviceDict; public SingletonFactory() { serviceDict = new Dictionary<Type, Dictionary<string, object>>(); } public TService GetService<TService>(string id) where TService : class { var serviceType = typeof(TService); return GetService<TService>(serviceType, id); } public TService GetService<TService>(Type serviceType, string id) where TService : class { if (serviceDict.TryGetValue(serviceType, out Dictionary<string, object> implDict)) { if (implDict.TryGetValue(id, out object service)) { return service as TService; } } return null; } public void AddService<TService>(TService service, string id) where TService : class { AddService(typeof(TService), service, id); } public void AddService(Type serviceType, object service, string id) { if (service != null) { if (serviceDict.TryGetValue(serviceType, out Dictionary<string, object> implDict)) { implDict[id] = service; } else { implDict = new Dictionary<string, object>(); implDict[id] = service; serviceDict[serviceType] = implDict; } } } } // 注入工厂 SingletonFactory singletonFactory = new SingletonFactory(); singletonFactory.AddService<IServiceA>(new ImplA1(), "impla1"); singletonFactory.AddService<IServiceA>(new ImplA2(), "impla2"); services.AddSingleton(singletonFactory); // 使用 private readonly IServiceA serviceA; public HomeController(SingletonFactory singletonFactory) { this.serviceA = singletonFactory.GetService<IServiceA>("impla2"); //使用标识从SingletonFactory获取自己想要的服务实现 }
八、简单了解通过AOP切面实现日志记录
什么是AOP?引用百度百科:AOP为Aspect Oriented Programming的缩写,意为:面向切面编程,通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。实现AOP主要由两种方式,
一种是编译时静态植入,优点是效率高,缺点是缺乏灵活性,.net下postsharp为代表者(好像是付费了。。)。
另一种方式是动态代理,优点是灵活性强,但是会影响部分效率,动态为目标类型创建代理,通过代理调用实现拦截。
AOP能做什么,常见的用例是事务处理、日志记录等等。
常见的AOP都是配合在Ioc的基础上进行操作,上边咱们讲了Autofac这个简单强大的Ioc框架,下面就讲讲Autofac怎么实现AOP。Autofac的AOP是通过Castle(也是一个容器)项目的核心部分实现的,名为Autofac.Extras.DynamicProxy,顾名思义,其实现方式为动态代理。当然AOP并不一定要和依赖注入在一起使用,自身也可以单独使用。
网上有一个博友的图片,大概讲了AOP切面编程
九、结语
今天的文章呢,主要说了依赖注入IoC在项目中的使用,从上边的说明中,可以看到,最大的一个优点就是实现解耦的作用,最明显的就是,在Controller中,不用在实例服务层或者业务逻辑层了,不过还是有些缺点的,缺点之一就是会占用一定的时间资源,效率稍稍受影响,不过和整体框架相比,这个影响应该也是值得的。
明天,我们继续将面向切面编程AOP中的,日志记录和面向AOP的缓存使用。
好文参考:https://www.cnblogs.com/stulzq/p/6880394.html