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

说明

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

介绍

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

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

这些问题之前也一直困扰着我,直到我在stackoverflow上发现了这篇文章的链接,作者利用示例代码 + 图文并茂的方式,彻底地解答了我的所有疑惑,感谢之余我就顺手把它翻译了下来。

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

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

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

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

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

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

提示

  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板块

结束

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

posted @ 2019-07-25 09:14 在7楼 阅读(...) 评论(...) 编辑 收藏