在本文中调塌,我將介紹如何在 ASP.NET Core 中萎胰,將一個(gè)包含多個(gè)公共接口的具體類注冊(cè)到 Microsoft.Extensions.DependencyInjection 容器中靠汁。使用這種方法王暗,你將能夠使用它實(shí)現(xiàn)的任何接口檢索具體的類姊扔。例如鳖枕,如果你有下面的類:
public class MyTestClass: ISomeInterface, ISomethingElse { }
接下來(lái)魄梯,你將會(huì)去注入 ISomeInterface
和 ISomethingElse
二者的其中之一,接下來(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è)為 IFoo
和 IBar
二者的單例照雁,但是結(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)雅的解決方案。
-
提供一個(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ù)。 -
使用工廠方法實(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ì)是更好的做法。 - 我們希望已注冊(cè)的服務(wù)(
總結(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