【譯】WebAPI,Autofac,以及生命週期作用域

在7樓發表於2019-07-25

說明

原文地址: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板塊

結束

讀完再回頭去看開頭那幾個問題,是不是就已經有答案了?

相關文章