Autofac 框架初识与应用
一、前言
这上一篇中,主要讲述了什么是IoC容器,以及了解到它是DI构造函注入的框架,它管理着依赖项的生命周期以及映射关系,同时也介绍实践了在ASP.Net Core中,默认提供的内置IoC容器,以及它的实例注册方式和相应的生命周期。
但考虑到在实际项目中,如果需要一个个添加实例,会略显麻烦,为了达到可以简化我们工作量,因此我们也可以引入其他的Ioc容器框架,实现更多的功能和扩展。
这里选择用Autofac,这也是在.net下比较流行的,其他的框架不做说明,可自行查阅了解。
二、说明
AutoFac是一个开源的轻量级的依赖注入容器,也是.net下比较流行的实现依赖注入的工具之一。
将Autofac整合到你的应用的基本流程如下:
- 按照 控制反转 (IoC) 的思想构建你的应用.
- 添加Autofac引用.
- 在应用的 startup 处
- 创建 ContainerBuilder.
- 注册组件.
- 创建容器,将其保存以备后续使用.
- 应用执行阶段
- 从容器中创建一个生命周期.
- 在此生命周期作用域内解析组件实例.
三、开始
3.1 默认容器
在上一篇中定义的三个接口,分别测试Singleton,Scope,Transient三种,一个 TestService服务,
在内置的IoC容器中,在Startup.cs
类文件ConfigureServices
方法中,注入依赖方式如下:
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddTransient<ITransientService, TransientService>();
services.AddSingleton<ISingletonService, SingletonService>();
services.AddScoped<IScopedService, ScopedService>();
services.AddScoped<ITestService, TestService>();
}
其他不清楚的可以回看上一篇说明
3.2 Autofac框架
现在我们使用其他的IoC容器框架来替换默认的内置IoC,这里选择使用Autofac框架
.net core 2.x和3.x 使用autofac注入方式不一样,此文章是针对.net core 3.x
首先,我们需要从nuget引用相关的包.
Autofac.Extensions.DependencyInjection(这个包扩展了一些微软提供服务的类.来方便替换autofac)
然后在Program.cs 新增一行代码
public static IHostBuilder CreateHostBuilder(string[] args)
{
//var assemblyName = typeof(Startup).GetTypeInfo().Assembly.FullName;
return Host.CreateDefaultBuilder(args)
.UseServiceProviderFactory(new AutofacServiceProviderFactory()) //设置工厂来替换实例
.ConfigureWebHostDefaults(webBuilder =>
{
webBuilder.UseStartup<Startup>();
});
}
UseServiceProviderFactory
设置工厂来替换实例。
然后在Startup类增加ConfigureContainer
方法,在方法中注入依赖:
public void ConfigureContainer(ContainerBuilder builder)
{
// Register your own things directly with Autofac, like:
builder.RegisterType<TransientService>().As<ITransientService>();
builder.RegisterType<SingletonService>().As<ISingletonService>().SingleInstance();
builder.RegisterType<ScopedService>().As<IScopedService>().InstancePerLifetimeScope();
builder.RegisterType<TestService>().As<ITestService>().InstancePerLifetimeScope();
}
说明
ASP.NET Core 引入了具有强类型容器配置的能力。 它提供了一个
ConfigureContainer
方法,您可以使用Autofac单独注册,而不是使用ServiceCollection
注册。使用ConfigureContainer配置
- 在配置
WebHostBuilder
的Program.Main
方法中,调用AddAutofac
将Autofac挂钩到启动管道中。- 在
Startup
类的ConfigureServices
方法中,使用其他库提供的扩展方法将内容注册到IServiceCollection
中。- 在
Startup
类的ConfigureContainer
方法中,将内容直接注册到AutofacContainerBuilder
中。
3.3 测试
启动运行项目,访问接口/Test
效果如下:
对比之前默认容器可以发现,在两次的请求访问都一样,可以得到了 4个Transient实例,2个Scope实例,1个Singleton实例。
四、说明
下面主要针对Autofac中的注册组件、解析服务两大步骤,以及其中容器中对应实例的生命周期,进行说明。
4.1 注册组件
通过创建 ContainerBuilder
来注册组件,并且告诉容器哪些组件,暴露了哪些服务。
使用 Register()
方法来注册实现:
ContainerBuilder
包含一组 Register()
注册方法,而组件暴露服务,可用使用 ContainerBuilder
上的 As()
方法。
即在容器初始化时候,向容器组件添加对象的操作过程。
通过梳理Autofac所有可用的注册组件方法,显示如下图展示的流程图。
这里我们只说明下几种便捷的注册方法
4.1.1 反射注册
直接注册的组件必须是具体的类型,并可用暴露抽象和接口作为服务,但不能注册一个抽象和接口组件。
使用RegisterType<T>()
或者RegisterType(typeof(T))
方法:
builder.RegisterType<TestService>().As<ITestService>();
// 或者
builder.RegisterType(typeof(TestService)).As(typeof(ITestService))
在多个构造函数时,如果需要,也可手动指定一个构造函数。
使用
UsingConstructor
方法和构造方法中代表参数类型的类型。builder.RegisterType<TestService>()
.UsingConstructor(typeof(TransientService), typeof(SingletonService));
4.1.2 实例注册
提前生成对象的实例并加入容器,以供注册组件时使用。
使用RegisterInstance()
方法
// new出一个对象注册:
var output = new StringWriter();
builder.RegisterInstance(output).As<TestService>();
如果单例中存在实例且需要在容器中被组件使用时,
builder.RegisterInstance(MySingleton.Instance).ExternallyOwned();
4.1.3 Lambda表达式注册
当组件创建不再是简单调用构造方法时,可用利用lambda表达式来实现一些常规反射无法实现的操作。
比如一些复杂参数注册,参数注入,以及选择参数值实现等。
builder.Register(x => new TransientService()).As<ITransientService>();
// 或者指定参数
builder.Register(x => new TestService(x.Resolve<ITransientService>(), x.Resolve<IScopedService>(), x.Resolve<ISingletonService>()))
.As<ITestService>().InstancePerLifetimeScope();
4.1.4 泛型注册
支持泛型注册操作,使用 RegisterGeneric()
方法:
builder.RegisterGeneric(typeof(NHibernateRepository<>))
.As(typeof(IRepository<>))
.InstancePerLifetimeScope();
4.1.5 条件注册
在一些特殊场景,可能需要通过加上判断条件,来决定是否执行该条注册语句。
两种方法:
OnlyIf()
- 提供一个表达式, 表示只有满足条件,才会执行语句。
builder.RegisterType<Manager>()
.As<IManager>()
.OnlyIf(reg =>
reg.IsRegistered(new TypedService(typeof(IService))) &&
reg.IsRegistered(new TypedService(typeof(HandlerB))));
IfNotRegistered()
- 表示没有其他服务注册的情况下,就执行语句。
方法在 ContainerBuilder.Build()
时执行并且以实际组件注册的顺序执行。
builder.RegisterType<ServiceA>()
.As<IService>();
builder.RegisterType<ServiceB>()
.As<IService>()
.IfNotRegistered(typeof(IService));
4.1.6 属性注入
构造方法参数注入是一种传值给组件的首选的方法。
在构造函数中是直接使用服务类型作为参数,然后AutoFac解析该类时,就会去容器内部已存在的组件中查找,然后将匹配的对象注入到构造函数中去。
但你同样也可以使用属性方法注入来传值。
是将容器内对应的组件直接注入到类内的属性中去,在注册该属性所属类的时候,需要使用
PropertiesAutowired()
方法额外标注。
这里不讨论属性注入的好坏,也不做说明服务层属性怎么注入,只讨论说明控制器中属性如何实现注入
- 注册组件方法,并使用属性注入
PropertiesAutowired()
标注。
builder.RegisterType<TransientService>().As<ITransientService>().PropertiesAutowired();
- 在控制器中使用属性来接收, 其中注入属性必须标注为public
[ApiController]
[Route("[controller]")]
public class TestController : ControllerBase
{
public ITransientService _transientService { get; set; }
[HttpGet]
public JsonResult Get()
{
var data1 = _transientService.GetGuid();
return new JsonResult(new {
data1
});
}
}
- 运行测试,发现如下
发现_transientService为null,所以根本没有注入成功。
这是因为控制器本身的实例(以及它的处理)是由框架创建和拥有的,而不是由容器所有
因此我们需要改变控制器本身的创建及其拥有。
- 在Startup.cs中修改
ConfigureServices
方法,替换从IServiceProvider中解析控制器实例的所有者。
public void ConfigureServices(IServiceCollection services)
{
//替换控制器的所有者
services.Replace(ServiceDescriptor.Transient<IControllerActivator, ServiceBasedControllerActivator>());
services.AddControllers();
}
注意,替换的方法一定要在AddControllers之前。
- 在
ContainerBuilder
中通过注册控制器,并使用属性注入功能实现.
public void ConfigureContainer(ContainerBuilder builder)
{
//找到所有的controller进行注册,并使用属性注入功能
var controllerTypesInassembly = typeof(Startup).Assembly.GetExportedTypes()
.Where(type => typeof(ControllerBase).IsAssignableFrom(type)).ToArray();
builder.RegisterTypes(controllerTypesInassembly).PropertiesAutowired();
builder.RegisterType<TransientService>().As<ITransientService>().PropertiesAutowired();
}
这样就可以在Controller中进行属性注入了;
- 再次运行查看,发现已经成功注入了。
4.1.7 程序集注册
当我们需要实现批量注册的时候,也可以使用程序集的方式来注册,这也是常用的方法。
可通过指定过滤类型,服务,扫描模块等方式来找到需要注册的组件。
var assemblies = Assembly.GetExecutingAssembly();
builder.RegisterAssemblyTypes(assemblies)//程序集内所有具象类
.Where(c => c.Name.EndsWith("Service"))
.PublicOnly()//只要public访问权限的
.Where(cc => cc.IsClass)//只要class型(主要为了排除值和interface类型)
.AsImplementedInterfaces();//自动以其实现的所有接口类型暴露(包括IDisposable接口)
说明:
- RegisterAssemblyTypes() :接收包含一个或多个程序集的数组作为参数
- RegisterAssemblyModules() : 接收模块作为参数,进行模块扫描注册
- PublicOnly() :指定公有方法被注册
- Where() :要过滤注册的类型
- Except() :要排除的类型
- As() :反射出其实现的接口
- AsImplementedInterfaces() : 自动以其实现的所有接口类型暴露(包括IDisposable接口)
4.2 暴露服务
上面提到了注册组件时, 我们得告诉Autofac, 组件暴露了哪些服务。
在上面注册实现中,大部分使用到了As()
方法。
当然,Autofac也提供了其他标注来暴露服务的方法。
4.2.1 默认暴露自身类型服务
常用的几种方法如下:
builder.RegisterType<CallLogger>();//不标注,默认以自身类型暴露服务
builder.RegisterType<CallLogger>().AsSelf();
builder.RegisterType<CallLogger>().As<CallLogger>();
builder.RegisterType<CallLogger>().As(typeof(CallLogger));
4.2.2 多个暴露服务类型
以其实现的接口(interface)暴露服务,暴露的类型可以是多个,比如CallLogger类实现了ILogger接口和ICallInterceptor接口。
暴露服务后, 可以解析基于该服务的组件了. 但请注意, 一旦将组件暴露为一个特定的服务, 默认的服务 (组件类型) 将被覆盖。
所以,为了防止被其他服务覆盖,可以使用 AsSelf()
方法。
Copybuilder.RegisterType<CallLogger>()
.As<ILogger>()
.As<ICallInterceptor>()
.AsSelf();
这样你既可以实现组件暴露一系列特定的服务, 又可以让它暴露默认的服务。
4.2.3 程序集注册指定暴露类型
- 可通过指定接口类型暴露服务,使用
As()
方法
publi void ConfigureContainer(ContainerBuilder builder)
{
builder.RegisterAssemblyTypes(assemblies)//程序集内所有具象类
.Where(cc =>cc.Name.EndsWith("Repository")|//筛选
cc.Name.EndsWith("Service"))
.As(x=>x.GetInterfaces()[0])//反射出其实现的接口,并指定以其实现的第一个接口类型暴露
}
- 指定所有实现的接口类型进行暴露
使用
AsImplementedInterfaces()
函数实现,相当于一个类实现了几个接口(interface)就会暴露出几个服务,等价于上面连写多个As()的作用。
publi void ConfigureContainer(ContainerBuilder builder)
{
builder.RegisterAssemblyTypes(asm)
.Where(t => t.Name.EndsWith("Repository"))
.AsImplementedInterfaces();//自动以其实现的所有接口类型暴露(包括IDisposable接口)
}
4.3 解析服务
在 注册完组件并暴露相应的服务后, 可以从创建的容器或其生命周期中解析服务。
使用 Resolve()
方法来解析实现:
通过梳理Autofac所有可用的解析服务方法,显示如下图展示的流程图。
在 注册完组件并暴露相应的服务后, 你可以从创建的容器或其子 生命周期 中解析服务. 让我们使用 Resolve()
方法来实现:
var builder = new ContainerBuilder();
builder.RegisterType<MyComponent>().As<IService>();
var container = builder.Build();
using(var scope = container.BeginLifetimeScope())
{
var service = scope.Resolve<IService>();
}
4.3.1 解析时传参
当解析服务时, 需要传参,可以使用Resolve()
方法来接受可变长度的参数。
- 可用参数类型
NamedParameter
- 通过名称匹配目标参数
TypedParameter
- 通过类型匹配目标参数 (需要匹配具体类型)
ResolvedParameter
- 灵活的参数匹配
- 反射组件的参数
var reader = scope.Resolve<ConfigReader>(new NamedParameter("configSectionName", "sectionName"));
Lambda表达式组件的参数
不显式调用Resolve传参
4.3.2 隐式关系类型
这里不做详细说明,详见官方文档
4.4 生命周期
下面讲下AutoFac定义的几种生命周期作用域,并与.NET Core默认的生命周期作了简要的对比。
4.4.1 暂时性
每次在向服务容器进行请求时都会创建新的实例,相当于每次都new出一个。
注册方式:
使用InstancePerDependency()
方法标注,如果不标注,这也是默认的选项。以下两种注册方法是等效的:
//不指定,默认就是瞬时的
builder.RegisterType<TransientService>().As<ITransientService>();
//指定其生命周期域为瞬时
builder.RegisterType<TransientService>().As<ITransientService>().InstancePerDependency();
对比:
与默认的容器中自带的生命周期AddTransient
相同,也是每次都是全新的实例。
使用AddTransient()
注册:
services.AddTransient<ITransientService, TransientService>()
4.4.2 作用域内
在每次Web请求时被创建一次实例,生命周期横贯整次请求。即在每个生命周期作用域内是单例的。
注册方式:
使用InstancePerLifetimeScope()
方法标识:
builder.RegisterType<ScopedService>().As<IScopedService>().InstancePerLifetimeScope();
对比
与默认的容器中自带的生命周期AddScoped
相同,.NET Core框架自带的容器全权接管了请求和生命周期作用域的创建,使用Scoped()
可以实现相同的效果。
使用AddScoped()
注册:
services.AddScoped<IScopedService, ScopedService>();
4.4.3 匹配作用域内
即每个匹配的
生命周期作用域一个实例。
该类型其实是上面的“作用域内”的其中一种,可以对实例的共享有更加精准的控制.。我们通过允许给域“打标签”,只要在这个特定的标签域内就是单例的。
- 注册
使用InstancePerMatchingLifetimeScope(string tagName)
方法注册:
var builder = new ContainerBuilder();
builder.RegisterType<Worker>().InstancePerMatchingLifetimeScope("myrequest");
当你开始一个生命周期时, 提供的标签值和它就关联起来了。
- 解析
// myrequest标签子域一
using(var scope1 = container.BeginLifetimeScope("myrequest"))
{
for(var i = 0; i < 100; i++)
{
var w1 = scope1.Resolve<Worker>();
using(var scope2 = scope1.BeginLifetimeScope())
{
var w2 = scope2.Resolve<Worker>();
//解析了2次,但2次都是同一个实例(w1和w2指向同一个内存块Ⅰ)
}
}
}
// //myrequest标签子域二
using(var scope3 = container.BeginLifetimeScope("myrequest"))
{
for(var i = 0; i < 100; i++)
{
//因为标签域内已注册过,所以可以解析成功
var w3 = scope3.Resolve<Worker>();
using(var scope4 = scope3.BeginLifetimeScope())
{
var w4 = scope4.Resolve<Worker>();
//因为和上面不是同一个子域,所以解析出的实例w3, w4是同一个实例,但与之前的w1, w2并不是同一个实例
}
}
}
//无标签子域三
using(var noTagScope = container.BeginLifetimeScope())
{
// 如果你尝试从一个名称并不匹配的生命周期中解析一个每个匹配生命周期作用域的组件你会得到一个异常!
var fail = noTagScope.Resolve<Worker>();
}
如果你尝试从一个名称并不匹配的生命周期中解析一个每个匹配生命周期作用域的组件你会得到一个异常!
4.4.4 全局单例
即全局只有一个实例,即每一个后续请求都使用同一个实例。
注册方式:
使用SingleInstance()
方法标识:
builder.RegisterType<SingletonService>().As<ISingletonService>().SingleInstance()
对比
与默认的容器中自带的生命周期AddSingleton
相同。
使用AddSingleton();
注册:
services.AddSingleton<ISingletonService, SingletonService>();
还有其他生命周期补充:
每个请求一个实例(Instance Per Request) : 其实是一种的“匹配作用域内单例”的一种。
每次被拥有一个实例(Instance Per Owned)
线程作用域(Thread Scope)
这几种在这不做详细说明,具体可以查看官网。
4.5 小结
在.NET Core中默认的容器自带生命周期只有3种类型,而相比于autofac,其显得更加丰富复杂些。
五、总结
本篇主要介绍autofac框架的使用,从注册组件,到暴露服务,及解析服务的各个过程。
同时也与.NET Core框架默认容器相比,更加丰富了一些注册方法和更复杂的生命周期,在应用上,也更加轻巧快捷,特别是在批量注册上更显实用。
文章展示的思维导图
https://www.processon.com/view/link/6072ed8863768912ae50b483
参考资料:Autofac官方网站:
https://autofaccn.readthedocs.io/en/latest/getting-started/index.html
好啦,这篇文章就先讲述到这里吧,希望对大家有所帮助。
如果有不对的或不理解的地方,希望大家可以多多指正,提出问题,一起讨论,不断学习,共同进步。
Autofac 框架初识与应用的更多相关文章
- ASP.NET MVC+WCF+NHibernate+Autofac 框架组合(一)
学习了Spring.NET+NHibernate的框架,觉得Spring.NET框架不够轻量,配置来配置去的比较头疼,所以把Spring.NET换成了Autofac框架,同时加入WCF框架整了一个组合 ...
- Vue框架初识01
摘要 vue简介 vue使用 一.Vue简介: 简介: Vue.js(读音 /vjuː/, 类似于 view)是一个构建数据驱动的 web 界面的渐进式框架.Vue.js 的目标是通过尽可能简单的 A ...
- GZAPI框架初识
新建一个MVC项目(GZAPIFramework.Demo): mvc:用于API接口文档查看,Log日志查看 webapi:api调用 新建一个Biz类库并添加nuget引用: 搜索GZAPI.Co ...
- python web框架——初识tornado
一 Tornado概述 Tornado是FriendFeed使用的可扩展的非阻塞式web框架及其相关工具的开源版本.这个Web框架看起来有些像web.py或者Google的 webapp,不过为了能有 ...
- Django框架初识
一.安装: pip3 install django 注意pip加入环境变量,安装好以后记得把Django加入环境变量 安装完成后,会在python目录下多了两个文件:1个django文件,1个 ...
- Android Multimedia框架总结(十四)Camera框架初识及自定义相机案例
转载请把头部出处链接和尾部二维码一起转载,本文出自逆流的鱼yuiop:http://blog.csdn.net/hejjunlin/article/details/52738492 前言:国庆节告一段 ...
- Android Multimedia框架总结(十三)CodeC部分之OpenMAX框架初识及接口与适配层实现
转载请把头部出处链接和尾部二维码一起转载,本文出自逆流的鱼yuiop:http://blog.csdn.net/hejjunlin/article/details/52629598 前言:上篇中介绍O ...
- MVC框架初识
MVC全名是Model View Controller,即模型-视图-控制器的缩写,一种软件设计典范,用一种业务逻辑,数据,界面显示分离方法组织代码, 将业务逻辑聚集到一个部件里面,在改进和个性化定制 ...
- Python学习---爬虫学习[scrapy框架初识]
Scrapy Scrapy是一个框架,可以帮助我们进行创建项目,运行项目,可以帮我们下载,解析网页,同时支持cookies和自定义其他功能. Scrapy是一个为了爬取网站数据,提取结构性数据而编写的 ...
随机推荐
- vue技术栈
1 vue 说明:vue生命周期:技术点:1:常用的API:computed,methods,props,mounted,created,components 2vue-cli说明:vue绞手架,用于 ...
- scala:函数作为值或参数进行传递、作为返回值进行返回
@ 目录 函数可以作为值进行传递 函数可以作为参数进行传递 函数可以作为返回值进行返回 什么是匿名函数 函数可以作为值进行传递 语法var f = 函数名 _ 如果明确了变量的数据类型,那么下划线可以 ...
- Java基础语法:final修饰符
一.final类 描述: 用'final'修饰的类不能被继承,没有子类. 例如,我们是无法写一个类去继承String类,然后对String类型扩展的,因为API中已经将String类定义为'final ...
- kubernets资源预留
一. Kubelet Node Allocatable Kubelet Node Allocatable用来为Kube组件和System进程预留资源,从而保证当节点出现满负荷时也能保证Kube和Sy ...
- 剑指 Offer 58 - I. 翻转单词顺序 + 双指针
剑指 Offer 58 - I. 翻转单词顺序 Offer_58_1 题目描述 方法一:使用Split函数 package com.walegarrett.offer; /** * @Author W ...
- PAT-1043(Is It a Binary Search Tree)JAVA实现
Is It a Binary Search Tree PAT-1043 主要涉及到根据前序遍历序列片段是否是一颗二叉树,这里有一个小tip就是插入序列就是二叉树的前序遍历序列. 第二个是会对排序二叉树 ...
- Java I/O流 01
文件IO·异常 和 File类 异常的概述和分类 * A:异常的概述 * 异常就是Java程序在运行过程中出现的错误 * B:异常的分类 * 用过API查看Throwable * Error * 服务 ...
- js--this指向的相关问题
前言 关于this的指向问题是前端面试中常考的知识点,也是我们开发学习中较难理解的问题.作为JavaScript的基础,需要我们彻底理解这一关键词.this作为JavaScript中非常复复杂的机制, ...
- Go语言|类型转换和类型别名
类型转换 同类型之间的转换 Go语言中只有强制类型转换,没有隐式类型转换.该语法只能在两个类型之间支持相互转换的时候使用. import "fmt" func main() { v ...
- FreeBSD 将降低对 i386 架构的支持力度
FreeBSD 开发团队宣布,从 FreeBSD 13.0 开始,对 i386 架构的支持级别将降级为 Tier 2,未来的 14.0 可能还将会在此基础上进一步降低对 i386 架构的支持.而对于 ...