【譯】如何在 ASP.NET Core DI 中注冊(cè)具有多個(gè)接口的服務(wù)

在本文中调塌,我將介紹如何在 ASP.NET Core 中萎胰,將一個(gè)包含多個(gè)公共接口的具體類注冊(cè)到 Microsoft.Extensions.DependencyInjection 容器中靠汁。使用這種方法王暗,你將能夠使用它實(shí)現(xiàn)的任何接口檢索具體的類姊扔。例如鳖枕,如果你有下面的類:

public class MyTestClass: ISomeInterface, ISomethingElse { }

接下來(lái)魄梯,你將會(huì)去注入 ISomeInterfaceISomethingElse 二者的其中之一,接下來(lái)宾符,你會(huì)獲取到相同的MyTestClass 實(shí)例酿秸。

有一點(diǎn)是很重要的,注冊(cè) MyTestClass 時(shí)須以特定的方式以避免生存周期問(wèn)題魏烫。比如一個(gè)單例中有兩個(gè)實(shí)例辣苏!

在本文中,我簡(jiǎn)要概述了 ASP.NET Core 中的 DI 容器和一些第三方容器相比的局限性哄褒。然后稀蟋,我將介紹對(duì)一個(gè)具體類型的多接口請(qǐng)求“轉(zhuǎn)發(fā)”的概念,以及如何在 ASP.NET Core 中的 DI 容器實(shí)現(xiàn)它呐赡。

TL;DR ASP.NET Core DI 容器原生并不支持多個(gè)服務(wù)實(shí)現(xiàn)的注冊(cè)(有時(shí)被稱為"轉(zhuǎn)發(fā)"). 相應(yīng)的退客,你必須手動(dòng)將服務(wù)的解析委托給工廠函數(shù),例如: services.AddSingleton<IFoo>(x=> x.GetRequiredService<Foo>())

ASP.NET Core 中的依賴注入

ASP.NET Core 的關(guān)鍵特性之一就是其使用了依賴注入(DI)。ASP.NET Core 框架本身是圍繞 “標(biāo)準(zhǔn)容器” 這一個(gè)抽象概念設(shè)計(jì)的萌狂,它允許自身使用一些簡(jiǎn)單的容器档玻,同時(shí)還允許你插入功能更豐富的第三方容器。

“標(biāo)準(zhǔn)容器”的想法并不是沒(méi)有爭(zhēng)議的 - 我建議你去閱讀 Mark Seemann 這篇關(guān)于標(biāo)準(zhǔn)容器作為反模式的文章粥脚,或者這篇來(lái)自 SimpleInjector 團(tuán)隊(duì)關(guān)于 ASP.NET Core DI 容器的特殊性窃肠。

為了使第三方容器實(shí)現(xiàn)起來(lái)更簡(jiǎn)單,“標(biāo)準(zhǔn)容器”被設(shè)計(jì)得更簡(jiǎn)單刷允,它公開(kāi)的 API 數(shù)量非常有限冤留。對(duì)于給定的服務(wù)(例如: IFOO ),你可以定義一個(gè)具體的類去實(shí)現(xiàn)它(例如: Foo),以及它的生命周期 (例如: 單例)树灶。在這個(gè)基礎(chǔ)上纤怒,可以直接提供服務(wù)實(shí)例,也可以提供工廠方法天通,但是這種方法會(huì)更復(fù)雜泊窘。

相比之下,.NET 的第三方 DI 容器通常提供更高級(jí)的注冊(cè) API. 例如像寒, 多數(shù) DI 容器為配置公開(kāi)一個(gè) "scan" API , 你可以通過(guò)它搜索程序集中的所有類型烘豹。然后,把它們添加到你的 DI 容器中诺祸。下面是一個(gè) Autofac 框架的例子:

var dataAccess = Assembly.GetExecutingAssembly();

builder.RegisterAssemblyTypes(dataAccess) // 查找程序集中的所有類型
       .Where(t => t.Name.EndsWith("Repository")) // 過(guò)濾類型
       .AsImplementedInterfaces()  // 注冊(cè)服務(wù)及其所有公共接口
       .SingleInstance(); // 將服務(wù)注冊(cè)為單例

在這個(gè)例子中携悯, Autofac 會(huì)找到程序集中那些所有名字以 "Repository" 結(jié)尾的具體類,并且根據(jù)他們實(shí)現(xiàn)的所有公共接口在容器中注冊(cè)筷笨。例如憔鬼,給定的下列類和接口:

public interface IStubRepository {}
public interface ICachingRepository {}

public class StubRepository : IStubRepository {}
public class MyRepository : ICachingRepository {}

前面的 Autofac 代碼相當(dāng)于在 ASP.NET Core 容器的 Startup.ConfigureServices 中手動(dòng)注冊(cè)這兩個(gè)類及其各自的接口:

services.AddSingleton<IStubRepository, StubRepository>();
services.AddSingleton<ICachingRepository, MyRepository>();

不過(guò),如果一個(gè)類實(shí)現(xiàn)了多個(gè)接口胃夏,會(huì)發(fā)生什么呢轴或?

將單個(gè)實(shí)現(xiàn)注冊(cè)為多個(gè)服務(wù)

實(shí)現(xiàn)多個(gè)接口的類很常見(jiàn),例如:

public interface IBar {}
public interface IFoo {}

public class Foo : IFoo, IBar {}

讓我們編寫一個(gè)快速測(cè)試仰禀,看看如果我們使用 ASP.NET Core DI 容器對(duì)這兩個(gè)接口注冊(cè)該類會(huì)發(fā)生什么:

[Fact]
public void WhenRegisteredAsSeparateSingleton_InstancesAreNotTheSame()
{
    var services = new ServiceCollection();

    services.AddSingleton<IFoo, Foo>();
    services.AddSingleton<IBar, Foo>();

    var provider = services.BuildServiceProvider();

    var foo1 = provider.GetService<IFoo>(); // An instance of Foo
    var foo2 = provider.GetService<IBar>(); // An instance of Foo

    Assert.Same(foo1, foo2); // FAILS
}

我們將 Foo 注冊(cè)為 IFooIBar 二者的單例照雁,但是結(jié)果可能并不是你預(yù)計(jì)的。我們實(shí)際上有兩個(gè) Foo “單例”實(shí)例答恶,每個(gè)都是以服務(wù)的方式被注冊(cè)囊榜。

轉(zhuǎn)發(fā)服務(wù)請(qǐng)求

針對(duì)多個(gè)服務(wù)注冊(cè)實(shí)現(xiàn)的一般模式是常見(jiàn)的。大多數(shù)第三方 DI 容器框架都實(shí)現(xiàn)了這種概念亥宿。例如:

  • Autofac 將該種模式當(dāng)作默認(rèn)的模式 - 上面的測(cè)試代碼已經(jīng)證明了這點(diǎn)
  • Windsor 中有一個(gè)“轉(zhuǎn)發(fā)類型”的概念,它允許你“轉(zhuǎn)發(fā)”多個(gè)服務(wù)到一個(gè)單例實(shí)現(xiàn)中
  • StructureMap (now sunsetted) 也同樣有“轉(zhuǎn)發(fā)”類型的類似概念砂沛。據(jù)我所知烫扼,它的繼承者 Lamar 并沒(méi)有提供類似的概念,不過(guò)碍庵,我可能是錯(cuò)的映企。

鑒于這個(gè)需求非常普遍悟狱,而 ASP.NET Core DI 容器中沒(méi)有,這看起來(lái)很奇怪堰氓。這個(gè)問(wèn)題2年前被提出來(lái)(David Fowler) 挤渐,不過(guò)已經(jīng)被關(guān)閉了。幸運(yùn)的是双絮,有一個(gè)概念上簡(jiǎn)單浴麻,但不是很優(yōu)雅的解決方案。

  1. 提供一個(gè)服務(wù)實(shí)例(僅限單例)

    最簡(jiǎn)單的方法就是在注冊(cè)你的服務(wù)時(shí)提供一個(gè) Foo 實(shí)例囤攀。每個(gè)已注冊(cè)的服務(wù)會(huì)在請(qǐng)求時(shí)返回你提供的確切實(shí)例软免,確保只有一個(gè)單例實(shí)例。

    [Fact]
    public void WhenRegisteredAsInstance_InstancesAreTheSame()
    {
        var foo = new Foo(); // The singleton instance
        var services = new ServiceCollection();
    
        services.AddSingleton<IFoo>(foo);
        services.AddSingleton<IBar>(foo);
    
        var provider = services.BuildServiceProvider();
    
        var foo1 = provider.GetService<IFoo>();
        var foo2 = provider.GetService<IBar>();
    
        Assert.Same(foo1, foo); // PASSES;
        Assert.Same(foo2, foo); // PASSES;
    }
    

    這里有一個(gè)非常不好的地方(警告)- 你必須在配置的時(shí)候能夠?qū)嵗?Foo 對(duì)象焚挠,并且你必須知道并且提供所有關(guān)于它的依賴膏萧。在某些情況下它是有用的,但這種方式不是很靈活蝌衔。

    另外榛泛,你只能用這種方法來(lái)注冊(cè)單例類。如果你希望 Foo 是各自請(qǐng)求范圍內(nèi)的單例實(shí)例(Scoped模式)噩斟,你很不走運(yùn)曹锨,你可能須要下面的技術(shù)。

  2. 使用工廠方法實(shí)現(xiàn)轉(zhuǎn)發(fā)

    如果我們分解我們的需求亩冬,那么替代的解決方案可能是:

    • 我們希望已注冊(cè)的服務(wù)(Foo)有特殊的生命周期(例如:Singleton or Scoped)
    • 當(dāng) IFoo 被請(qǐng)求艘希,返回一個(gè) Foo 實(shí)例
    • 當(dāng) IBar 被請(qǐng)求,也返回一個(gè) Foo 實(shí)例

    根據(jù)這三條規(guī)則硅急,我們可以再寫一個(gè)測(cè)試:

    [Fact]
    public void WhenRegisteredAsForwardedSingleton_InstancesAreTheSame()
    {
        var services = new ServiceCollection();
    
        services.AddSingleton<Foo>(); // We must explicitly register Foo
        services.AddSingleton<IFoo>(x => x.GetRequiredService<Foo>()); // Forward requests to Foo
        services.AddSingleton<IBar>(x => x.GetRequiredService<Foo>()); // Forward requests to Foo
    
        var provider = services.BuildServiceProvider();
    
        var foo1 = provider.GetService<Foo>(); // An instance of Foo
        var foo2 = provider.GetService<IFoo>(); // An instance of Foo
        var foo3 = provider.GetService<IBar>(); // An instance of Foo
    
        Assert.Same(foo1, foo2); // PASSES
        Assert.Same(foo1, foo3); // PASSES
    }
    

    為了“轉(zhuǎn)發(fā)”對(duì)具體類型接口的請(qǐng)求覆享,你必須做兩件事:

    • 使用 services.AddSingleton<Foo>() 顯示的注冊(cè)具體類型
    • 通過(guò)提供工廠函數(shù),將接口請(qǐng)求委托給具體的類型:services.AddSingleton<IFoo>(x => x.GetRequiredService<Foo>())

    通過(guò)這種方法营袜,你會(huì)得到一個(gè)真正的 Foo 單例實(shí)例撒顿,無(wú)論你請(qǐng)求的是哪種實(shí)現(xiàn)的服務(wù)。

    這種提供“轉(zhuǎn)發(fā)”類型的方式被記錄在原始的 issue 中荚板,順帶了一個(gè)警告 - 它不是很有效凤壁。通常最好盡可能避免“服務(wù)定位器樣式” GetService() 調(diào)用。不過(guò)跪另,我覺(jué)得在這種情況下拧抖,這絕對(duì)是更好的做法。

總結(jié)

在這篇文章中免绿,我總結(jié)了如果你使用 ASP.NET Core DI 服務(wù)去注冊(cè)一個(gè)以多服務(wù)模式的具體類型會(huì)發(fā)生什么唧席。特別的,我展示了如何使用單例對(duì)象的多個(gè)副本,這可能會(huì)導(dǎo)致一些小bug淌哟。為了解決這個(gè)問(wèn)題迹卢,你可以在注冊(cè)的時(shí)候提供一個(gè)實(shí)例,或者使用工廠方法代理服務(wù)的解析徒仓。盡管使用工廠方法不是非常高效的腐碱,但是卻是通常情況下的最好方法。

【原文】How to register a service with multiple interfaces in ASP.NET Core DI

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末掉弛,一起剝皮案震驚了整個(gè)濱河市症见,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌狰晚,老刑警劉巖筒饰,帶你破解...
    沈念sama閱讀 211,290評(píng)論 6 491
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異壁晒,居然都是意外死亡瓷们,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,107評(píng)論 2 385
  • 文/潘曉璐 我一進(jìn)店門秒咐,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)谬晕,“玉大人,你說(shuō)我怎么就攤上這事携取≡芮” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 156,872評(píng)論 0 347
  • 文/不壞的土叔 我叫張陵雷滋,是天一觀的道長(zhǎng)不撑。 經(jīng)常有香客問(wèn)我,道長(zhǎng)晤斩,這世上最難降的妖魔是什么焕檬? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,415評(píng)論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮澳泵,結(jié)果婚禮上实愚,老公的妹妹穿的比我還像新娘。我一直安慰自己兔辅,他們只是感情好腊敲,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,453評(píng)論 6 385
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著维苔,像睡著了一般碰辅。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上介时,一...
    開(kāi)封第一講書(shū)人閱讀 49,784評(píng)論 1 290
  • 那天乎赴,我揣著相機(jī)與錄音忍法,去河邊找鬼。 笑死榕吼,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的勉失。 我是一名探鬼主播羹蚣,決...
    沈念sama閱讀 38,927評(píng)論 3 406
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼乱凿!你這毒婦竟也來(lái)了顽素?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 37,691評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤徒蟆,失蹤者是張志新(化名)和其女友劉穎胁出,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體段审,經(jīng)...
    沈念sama閱讀 44,137評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡全蝶,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,472評(píng)論 2 326
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了寺枉。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片抑淫。...
    茶點(diǎn)故事閱讀 38,622評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖姥闪,靈堂內(nèi)的尸體忽然破棺而出始苇,到底是詐尸還是另有隱情,我是刑警寧澤筐喳,帶...
    沈念sama閱讀 34,289評(píng)論 4 329
  • 正文 年R本政府宣布催式,位于F島的核電站,受9級(jí)特大地震影響避归,放射性物質(zhì)發(fā)生泄漏荣月。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,887評(píng)論 3 312
  • 文/蒙蒙 一槐脏、第九天 我趴在偏房一處隱蔽的房頂上張望喉童。 院中可真熱鬧,春花似錦顿天、人聲如沸堂氯。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,741評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)咽白。三九已至,卻和暖如春鸟缕,著一層夾襖步出監(jiān)牢的瞬間晶框,已是汗流浹背排抬。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,977評(píng)論 1 265
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留授段,地道東北人蹲蒲。 一個(gè)月前我還...
    沈念sama閱讀 46,316評(píng)論 2 360
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像侵贵,于是被迫代替她去往敵國(guó)和親届搁。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,490評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容