【译】WebAPI,Autofac,以及生命周期作用域
说明
原文地址:http://decompile.it/blog/2014/03/13/webapi-autofac-lifetime-scopes/
介绍
这是一篇关于AutoFac的生命周期作用域的文章。
关于生命周期域一直以来都是一个令人头疼的命题,其中有些概念极易造成误解和混淆,比如域内单例(PerLifetimeScope)和请求内单例(InstancePerRequest)有什么区别、以及它们可不可以替换使用等等......
这些问题之前也一直困扰着我,直到我在stackoverflow上发现了这篇文章的链接,作者利用示例代码 + 图文并茂的方式,彻底地解答了我的所有疑惑,感谢之余我就顺手把它翻译了下来。
在阅读原文之前,可以先看看下面几个问题,如果你对这些问题都已经很清楚了,那么恭喜你,你已经强大到不需要浪费时间阅读该文,可以直接出门右转了:
域内单例(PerLifetimeScope)是什么意思?
请求内单例(InstancePerRequest)是什么意思?
域内单例和请求内单例有什么区别?在WebApi类型的项目中,它们可不可以相互替换使用?
在.NET Core中,AutoFac的请求内单例(InstancePerRequest)将不再有效,但是有些对象又需要被注册为请求内单例(比如EF的DbContext),那可以使用域内单例(PerLifetimeScope)来替换吗?会产生什么影响?
如果对其中任何一个问题还抱有疑惑,那么我相信这篇文章对你一定会有所帮助的(正如当初对我一样)。
提示
这篇文章中提到的Http请求内单例(InstancePerHttpRequest)和Api请求内单例(InstancePerApiRequest)现在在AutoFac中已经过时了,取而代之的是整合后的请求内单例(InstancePerRequest)
原作者的源码是在GitHub开源的,地址就在文章的末尾。我Fork了一份,将AutoFac更新到了最新版本,并且添加了中文文档,有需要的也可以去下载或浏览我的GitHub
本文是以默认读者已经了解了依赖注入与AutoFac的基础知识为前提的,如果有朋友还是初学者,我建议可以先去读一读AutoFac的技术文档,或者也可以去看下我之前写过的两篇半小时大话.NET依赖注入的文章~
原文
当我们使用AutoFac(或者任何其他用于依赖注入的容器)时,经常有一个非常困扰我们的命题,那就是生命周期作用域。
如果你是一个初学者,我建议可以先读一读 Nicholas Blumhardt 的一篇很棒的文章:An Autofac Lifetime Primer。鉴于你可能需要反复多读几遍来消化这些知识,我建议可以保存个书签。
针对 AuotoFac,我在众多场合下都听到过这样一个疑问:
域内单例(InstancePerLifetimeScope)、Http请求内单例(InstancePerHttpRequest)和Api请求内单例(InstancePerApiRequest)有什么区别?
一直以来我也对这个问题感到疑惑,而且目前为止我还没有找到一个令人满意的回答。所以,今天我将尝试着自己来解答下这个问题。
先抛出我的终极结论:
如果你想让你注册的依赖在任何域内都可已被解析,那么请使用域内单例(InstancePerLifetimeScope)。你的依赖项会同生命周期域一同释放。如果这个域是根域,那么它将一直存在直到程序终结。
如果你想要让你注册的依赖只能在request类型(HTTP/API)的请求上下文中被解析,那么请使用请求内单例(InstancePerApiRequest/InstancePerHttpRequest)。依赖会在请求结束后被释放。
这里我将不会再去解释作用域和生命周期的概念了,Nicholas已经很好地完成了这部分工作,我上面也已经把他文章的链接贴出来了。所以,我将假定你们已经具有依赖注入的基础知识,现在你们只是想知道针对Web程序它们是如何运作的。
为了更好的讲解,我自己写了个简单的程序,需要的可以自己下载下来试着跑一跑。程序里我创建了4个Resolvables
类——它们每个都很简单,唯一的功能就是展示出服务是从哪儿被解析出来的。注册它们的代码如下所示:
private static void RegisterResolvables(ContainerBuilder builder)
{
builder.RegisterType<SingletonResolvable>()
.SingleInstance();
builder.RegisterType<PerLifetimeResolvable>()
.InstancePerLifetimeScope();
builder.RegisterType<PerRequestResolvable>()
.InstancePerApiRequest();
builder.RegisterType<PerDependencyResolvable>()
.InstancePerDependency();
}
程序还有一个负责解析的类,它唯一的任务就是负责解析上面的4个Resolvables
类。下面是该类的构造方法:
public ResolvableConsumer(
SingletonResolvable singleton,
PerLifetimeResolvable lifetime,
PerRequestResolvable request,
PerDependencyResolvable dependency)
{
// ...
}
现在,我要做一件神奇的事情了!我创造了一个ScopeToken
类,并简单地封装了一下它,使它可以展示它自己是被哪个作用域解析出来的,然后让4个Resolvables
类都依赖这个ScopeToken
类。在注册ScopeToken
类时,我们可以通过修改它的生命周期作用域来观察到底会对程序产生什么变化。下面,我们就先把它注册为瞬时实例(InstancePerDependency)试试看。
private static void RegisterToken(ContainerBuilder builder)
{
var tokenRegistration = builder.RegisterType<ScopeToken>();
// TODO: 挨个尝试
// tokenRegistration.SingleInstance();
// tokenRegistration.InstancePerLifetimeScope();
// tokenRegistration.InstancePerApiRequest();
tokenRegistration.InstancePerDependency();
}
我们可以通过请求TestController
下的一个GET请求来测试我们的程序。我这里用了HTTPie工具来模拟Web请求(关于这个工具的使用,可以参考Scott Hanselman的安装笔记)
现在我们的准备工作已经全部完成了,接下来我们一起看下使用不同的生命周期作用域注册,会对解析ScopeToken
有什么样的影响。
瞬时单例(InstancePerDependency)
在使用AutoFac注册组件时,如果我们不自己指定生命周期域,该域将是默认的选项。在技术文档里是这么解释的:
注册时用该域标注组件,那么每一个依赖组件或每一次通过Resolve()解析出的都将是一个全新的实例。
我们来看下,调用GET接口会发生什么:
不出所料,每个解析对象
内都被注入了一个属于他们自己的唯一的token。看,依赖注入起作用了!
我们可以看到几点有趣的地方:
SingletonResolvable
的token是从根域
内(root scope)解析出的其他解析类的token全部是从一个叫AutofacWebRequest的域内解析出的
如下图所示:
出于好奇,我们来看下如果再调用一次接口会发生什么:
Token #1没有变。这是因为根域的生命周期和程序是保持一致的。换句话说,SingletonResolvable
对象以及它所依赖的ScopeToken
对象将一直存在,直到程序停止运行为止。
相反,Tokens #2, #3 和 #4已经全部被释放掉了,因为AutofacWebRequest域
的生命周期是和Web请求保持一致的。也就是,该域在请求发起时被创建,当请求结束后就立即被释放掉了。
全局单例(SingleInstance)
private static void RegisterToken(ContainerBuilder builder)
{
var tokenRegistration = builder.RegisterType<ScopeToken>();
tokenRegistration.SingleInstance();
}
下一个比较容易理解的是全局单例,其含义就像它的名字所表达的:任何时候都将得到一个唯一实例。实际上,Autofac会将单例对象归属到根域
(root scope)内(或者叫“container” scope),而其他的所有域都是这个根域下的子域。下面是调用接口的输出结果:
再次不出所料地,每个解析对象获得的都是同一个ScopeToken
实例。
有两点需要指出:
- 所有单例都处于根域内,并且,上面已经说过,根域的生命周期和程序一样长。
- AutoFac解析组件时,会依次向上到其父类域内查找依赖
域内单例(PerLifetimeScope)
private static void RegisterToken(ContainerBuilder builder)
{
var tokenRegistration = builder.RegisterType<ScopeToken>();
tokenRegistration.InstancePerLifetimeScope();
}
从这儿开始事情就要变得有趣了。AutoFac文档对域内单例的解释如下:
用该生命周期作用域注册,以后的每个依赖组件或通过Resolve()解析出的对象,在同一个生命周期作用域内是相同的,它们共享同一个单例,而在不同的生命周期作用域内则是不同的。
还记得上面瞬时单例的例子吗?SingletonResolvable
类是解析在根域中的,其他的Resolvables
类都被解析到了AutofacWebRequest域
。我们来看下域内单例又会发生什么:
正如预期的,我们有两个“激活”的域,而且每个域内都有一个ScopeToken
实例。
让我们来看下当再次调用接口会发生什么:
和之前的瞬时单例一样,处在根域内的Token #1一直存在着,而处在AutofacWebRequest域
内的Token #2在请求结束后被释放掉了。
一直以来有一个普遍的错误认知,就是认为在WebAPI项目中如果组件被注册为域内单例(InstancePerLifetimeScope)的话,那么意思就是它将存活在一次request请求内,即它的生命周期就是一次request请求的生命周期。但是正如上面的例子所展示的,这种认知是错误的。
被注册为域内单例的组件,它的生命周期是由解析它的域所决定的。
因为SingletonResolvable
实例是在根域
内解析它的token,所以这个token实例就存在于根域
内,而不是一次web请求的生命周期域。之前已经说过,这里我要再重复一遍:这个token会一直存在直到整个应用程序停止运行为止(即IIS工作进程被回收时)。任何对象只要是在根域内要求获取依赖的ScopeToken
,那么它就会得到这个唯一单例的对象。
Api请求内单例(InstancePerApiRequest)
private static void RegisterToken(ContainerBuilder builder)
{
var tokenRegistration = builder.RegisterType<ScopeToken>();
tokenRegistration.InstancePerApiRequest();
}
最后,也是最重要的,让我们来看下Api请求内单例。下面是调用接口后的情况:
请求出现了一个令人不快的异常,内容是:
被请求获取的实例所在的域内,找不到一个标签为‘AutofacWebRequest’的域。这通常表明,有一个被注册为每次HTTP请求内单例的组件被一个全局单例的组件请求获取(或者是类似的其他场景)。web项目通常是从DependencyResolver.Current或者ILifetimeScopeProvider.RequestLifetime中获取依赖,但是不允许直接从根容器中获取。
为了明白为什么会发生这样的异常,我们需要回到AutoFac的技术文档上来。里面说,Api请求内单例(InstancePerApiRequest)实际上是每个匹配域内单例(InstancePerMatchingLifetimeScope)的一种特殊情况,文档原文是这样的 :
用Api请求内单例来注册组件,那么每个依赖组件或者每次通过Resolve()解析,只要是在打了统一标签名称的域内,就会得到同一个对象,即它们共享同一个单例。在这个特定标签域下面的所有子域中,依赖组件也会共享其父域中的单例。如果在当前域和它的父域中都找不到这个标签域,那么一个类型为DependencyResolutionException的异常将会被抛出。
具体来说,Api请求内单例(InstancePerApiRequest)实质上是在一个特定标签域内单例,正如你所猜测的,这个特定标签域就是AutofacWebRequest域
。这个域会在一次请求开始时被创建,并且在请求结束后被立即释放。综上,如果使用Api请求内单例(InstancePerApiRequest)来注册组件,那么这个组件只允许在AutofacWebRequest域
内或其子域内被解析。
我们的异常就发生在解析SingletonResolvable
对象的时候。之前我们把它注册为全局单例(SingleInstance),所以它就处于根域
内,而根域
(正如名字所表达的)是所有其他域的父域。对依赖的解析是不允许向下朝着子域方向查找的,只允许向上照着其父域去查找依赖。综上所述,SingletonResolvable
对象不可以去AutofacWebRequest标签域
内查找其依赖,所以它就不能获得它的依赖项ScopeToken
,再而,我们就得到了上面抛出的异常。
Http请求内单例(InstancePerHttpRequest)
上面我没有提Http请求内单例(InstancePerHttpRequest),是因为它本质上和Api请求内单例(InstancePerApiRequest)是相同的,只是它只用于HTTP请求(相对WebApi而言)。实际上,它内部使用的依然是匹配域内单例(InstancePerMatchingLifetimeScope),同样的,这个用于匹配的标签名称也叫做AutofacWebRequest
。所以,被注册为Http请求内单例的组件可以解析被注册为Api请求内单例的对象,反之亦然。
希望这篇文章能帮你更好地理解WebAPI项目下的AutoFac的生命周期作用域。需要的朋友可以自由下载源码并使用。
Gerrod 发表于 2014年5月13日 .NET板块
结束
读完再回头去看开头那几个问题,是不是就已经有答案了?
【译】WebAPI,Autofac,以及生命周期作用域的更多相关文章
- autofac 实例生命周期
转自逆心博客园 autofac 实例生命周期 实例生命周期决定在同一个服务的每个请求的实例是如何共享的. 当请求一个服务的时候,Autofac会返回一个单例 (single instance作用域), ...
- Autofac实例生命周期
1.默认,每次请求都会返回一个实例 builder.RegisterType<X>().InstancePerDependency(); 2.Per Lifetime Scope:这个作用 ...
- Autofac之生命周期和事件
Autofac为注册的类型对象提供了一套生命周期事件,覆盖了一个类型从注册到最后“释放”的一套事件.有了这些事件,我们可以相对方便的在类型对象的各个阶段进行AOP操作. builder.Registe ...
- autofac 注入生命周期
创建实例方法 1.InstancePerDependency 对每一个依赖或每一次调用创建一个新的唯一的实例.这也是默认的创建实例的方式. 官方文档解释:Configure the component ...
- ASP.NET Core :容器注入(二):生命周期作用域与对象释放
//瞬时生命周期 ServiceCollection services = new ServiceCollection(); services.AddTransient<TestServiceI ...
- [译]MVC应用程序生命周期
原文:MVC Application Lifecycle 来一探究竟在MVC应用程序中参与请求处理的各个不同组件. 目录: 序言 背景 UrlRoutingModule RouteHandler Mv ...
- [译]9-spring bean的生命周期
spring中bean的生命周期比较容易理解.bean在实例化之后有时需要调用某个初始化方法进行一些初始化的工作.同样的 ,当bean在销毁之前有时需要做一些资源回收的工作. 尽管bean在实例化和销 ...
- 【译】ModSecurity事务生命周期
本篇简要介绍ModSecurity Transaction Lifecycle,也即ModSecurity的事务生命周期. Transaction Lifecycle In ModSecurity, ...
- asp.net webapi http请求生命周期
先附上webapi http生命周期图. 原始的图片地址为:https://www.asp.net/media/4071077/aspnet-web-api-poster.pdf
随机推荐
- Linux正则和grep命令
设置命令的默认参数和别名 每次都要输入 ls -l ,烦不烦,我想用 ll 来表示 ls -l, 可以,只要在 ~/.bashrc 中加上 alias ll='ls -l' ,然后运行 source ...
- android核心系列--2,关于任务栈(task)
一,任务 任务是由界面组件组成的一个栈,这些界面组件可以来自多个进程,多个应用,为共同完成一项任务而存在,比如写邮件时会用到邮件应用和联系人应用中的界面组件,这些界面组件在同一个任务中运行. 二,界面 ...
- SYN4201型 同步分频钟
SYN4201型 同步分频钟 产品概述 SYN4201型同步分频钟是由西安同步电子科技有限公司精心设计.自行研发生产的一款高精度分频时钟,对输入的8路10MHz正弦信号分别进行同步分频处理,相应的输出 ...
- Spring Boot:整合MyBatis框架
综合概述 MyBatis 是一款优秀的持久层框架,它支持定制化 SQL.存储过程以及高级映射.MyBatis 避免了几乎所有的 JDBC 代码和手动设置参数以及获取结果集.MyBatis 可以使用简单 ...
- 文件识别浅谈(含office文件区分)
前言 本文主要根据后台接口识别Office文件类型这一话题做一些分享,主要方向还是放在不能获取到文件名情况下的Office文件识别. 可获取到文件名 如果后端接口可以获取到完成的文件名称,则整个过程会 ...
- Python的魔术方法详解
构造和初始化 __init__我们很熟悉了,它在对象初始化的时候调用,我们一般将它理解为"构造函数". 实际上, 当我们调用x = SomeClass()的时候调用,__init_ ...
- Flume 简介及基本使用
一.Flume简介 Apache Flume是一个分布式,高可用的数据收集系统.它可以从不同的数据源收集数据,经过聚合后发送到存储系统中,通常用于日志数据的收集.Flume 分为 NG 和 OG (1 ...
- 【对象属性复制】BeanUtils.copyProperties(obj1, obj2);
实现对象的属性值复制,只会复制命名相同的文件. import org.springframework.beans.BeanUtils; BeanUtils.copyProperties(obj1, o ...
- python logging模块使用总结
目录 logging模块 日志级别 logging.basicConfig()函数中的具体参数含义 format参数用到的格式化信息 使用logging打印日志到标准输出 使用logging.base ...
- HDU 5117:Fluorescent(状压DP + 思维)***
题目链接 题意 给出n个灯,m个开关,每个开关控制一些灯,如果打开这个开关,这个开关控制的灯如果本来灭的就会亮,如果本来亮的就会灭.问在每个开关按下与否的一共2^m情况下,每种状态下亮灯的个数的立方的 ...