【译】WebAPI,Autofac,以及生命周期做用域

2021年11月25日 阅读数:4
这篇文章主要向大家介绍【译】WebAPI,Autofac,以及生命周期做用域,主要内容包括基础应用、实用技巧、原理机制等方面,希望对大家有所帮助。

说明

原文地址:http://decompile.it/blog/2014/03/13/webapi-autofac-lifetime-scopes/html

介绍

这是一篇关于AutoFac的生命周期做用域的文章。git

关于生命周期域一直以来都是一个使人头疼的命题,其中有些概念极易形成误解和混淆,好比域内单例(PerLifetimeScope)和请求内单例(InstancePerRequest)有什么区别、以及它们可不能够替换使用等等......github

这些问题以前也一直困扰着我,直到我在stackoverflow上发现了这篇文章的连接,做者利用示例代码 + 图文并茂的方式,完全地解答了个人全部疑惑,感谢之余我就顺手把它翻译了下来。web

在阅读原文以前,能够先看看下面几个问题,若是你对这些问题都已经很清楚了,那么恭喜你,你已经强大到不须要浪费时间阅读该文,能够直接出门右转了:api

  1. 域内单例(PerLifetimeScope)是什么意思?ide

  2. 请求内单例(InstancePerRequest)是什么意思?工具

  3. 域内单例请求内单例有什么区别?在WebApi类型的项目中,它们可不能够相互替换使用?测试

  4. 在.NET Core中,AutoFac的请求内单例(InstancePerRequest)将再也不有效,可是有些对象又须要被注册为请求内单例(好比EF的DbContext),那可使用域内单例(PerLifetimeScope)来替换吗?会产生什么影响?ui

若是对其中任何一个问题还抱有疑惑,那么我相信这篇文章对你必定会有所帮助的(正如当初对我同样)。url

提示

  1. 这篇文章中提到的Http请求内单例(InstancePerHttpRequest)和Api请求内单例(InstancePerApiRequest)如今在AutoFac中已通过时了,取而代之的是整合后的请求内单例(InstancePerRequest)

  2. 原做者的源码是在GitHub开源的,地址就在文章的末尾。我Fork了一份,将AutoFac更新到了最新版本,而且添加了中文文档,有须要的也能够去下载或浏览个人GitHub

  3. 本文是以默认读者已经了解了依赖注入与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接口会发生什么:

PerDependency

不出所料,每一个解析对象内都被注入了一个属于他们本身的惟一的token。看,依赖注入起做用了!

咱们能够看到几点有趣的地方:

  • SingletonResolvable的token是从根域内(root scope)解析出的

  • 其余解析类的token所有是从一个叫AutofacWebRequest的域内解析出的

以下图所示:

PerDependency1

出于好奇,咱们来看下若是再调用一次接口会发生什么:

PerDependency_2

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),而其余的全部域都是这个根域下的子域。下面是调用接口的输出结果:

SingleInstance

再次不出所料地,每一个解析对象得到的都是同一个ScopeToken实例。

SingleInstance1

有两点须要指出:

  1. 全部单例都处于根域内,而且,上面已经说过,根域的生命周期和程序同样长。
  2. AutoFac解析组件时,会依次向上到其父类域内查找依赖

域内单例(PerLifetimeScope)

private static void RegisterToken(ContainerBuilder builder)
{
    var tokenRegistration = builder.RegisterType<ScopeToken>();
    tokenRegistration.InstancePerLifetimeScope();
}

从这儿开始事情就要变得有趣了。AutoFac文档对域内单例的解释以下:

用该生命周期做用域注册,之后的每一个依赖组件或经过Resolve()解析出的对象,在同一个生命周期做用域内是相同的,它们共享同一个单例,而在不一样的生命周期做用域内则是不一样的。

还记得上面瞬时单例的例子吗?SingletonResolvable类是解析在根域中的,其余的Resolvables类都被解析到了AutofacWebRequest域。咱们来看下域内单例又会发生什么:

PerLifetimeScope

正如预期的,咱们有两个“激活”的域,并且每一个域内都有一个ScopeToken实例。

PerLifetimeScope1

让咱们来看下当再次调用接口会发生什么:

PerLifetimeScope_2

和以前的瞬时单例同样,处在根域内的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请求内单例。下面是调用接口后的状况:

PerApiRequest

请求出现了一个使人不快的异常,内容是:

被请求获取的实例所在的域内,找不到一个标签为‘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,再而,咱们就获得了上面抛出的异常。

PerApiRequest1

Http请求内单例(InstancePerHttpRequest)

上面我没有提Http请求内单例(InstancePerHttpRequest),是由于它本质上和Api请求内单例(InstancePerApiRequest)是相同的,只是它只用于HTTP请求(相对WebApi而言)。实际上,它内部使用的依然是匹配域内单例(InstancePerMatchingLifetimeScope),一样的,这个用于匹配的标签名称也叫作AutofacWebRequest。因此,被注册为Http请求内单例的组件能够解析被注册为Api请求内单例的对象,反之亦然。

但愿这篇文章能帮你更好地理解WebAPI项目下的AutoFac的生命周期做用域。须要的朋友能够自由下载源码并使用。


Gerrod 发表于 2014年5月13日 .NET板块

结束

读完再回头去看开头那几个问题,是否是就已经有答案了?