在ASP.NET Core 2中dependency injection相同类型的多个实例
在ASP.NET Core 2 Web Api中,我想使用dependency injection将HttpClient
httpClientA
实例注入ControllerA
,并将HttpClient
的实例httpClientB
ControllerB
。
DI注册码看起来像:
HttpClient httpClientA = new HttpClient(); httpClientA.BaseAddress = endPointA; services.AddSingleton(httpClientA); HttpClient httpClientB = new HttpClient(); httpClientB.BaseAddress = endPointB; services.AddSingleton(httpClientB);
我知道我可以将HttpClient
子类HttpClient
每个控制器创建一个唯一类型,但这不能很好地扩展。
什么是更好的方法?
更新特别针对HttpClient,微软似乎正在开发一些工作
https://github.com/aspnet/HttpClientFactory/blob/dev/samples/HttpClientFactorySample/Program.cs#L32 – 感谢@ mountain-traveler(Dylan)指出这一点。
注意:这个答案使用
HttpClient
和HttpClientFactory
作为示例,但很容易适用于任何其他类型的事情。 特别是对于HttpClient
,首选使用Microsoft.Extensions.Http
的新IHttpClientFactory
。
内置dependency injection容器不支持命名依赖注册,目前还没有计划添加它 。
这样做的一个原因是,通过dependency injection,没有类型安全的方法来指定您想要的命名实例。 你肯定可以使用像构造函数的参数属性(或属性注入属性的属性),但这将是一种不同的复杂性,可能不值得; 它肯定不会受到类型系统的支持,类型系统是dependency injection如何工作的重要部分。
通常,命名依赖项表示您没有正确设计依赖项。 如果您有两个相同类型的不同依赖项,那么这应该意味着它们可以互换使用。 如果情况并非如此,并且其中一个在另一个不存在的情况下有效,那么这表明您可能违反Liskov替代原则 。
此外,如果你看一下那些支持命名依赖项的dependency injection包含,你会注意到检索这些依赖项的唯一方法是不使用dependency injection,而是使用服务定位器模式 ,这与DI促进的控制反转完全相反。
Simple Injector是一个较大的dependency injection容器,它解释了它们缺少这样的命名依赖 :
通过密钥解析实例是故意忽略Simple Injector的一个特性,因为它总是会导致应用程序倾向于对DI容器本身有很多依赖性的设计。 要解析键控实例,您可能需要直接调用Container实例,这会导致Service Locator反模式 。
这并不意味着通过密钥解析实例永远不会有用。 通过密钥解析实例通常是特定工厂而不是Container的工作 。 这种方法使设计更加清晰,使您不必在DI库上采用多种依赖关系,并且可以实现DI容器作者根本不考虑的许多场景。
尽管如此,有时候你真的想要这样的东西并拥有大量的子类型和单独的注册根本不可行。 在这种情况下,有适当的方法来解决这个问题。
我可以想到一个特殊的情况,ASP.NET Core在其框架代码中有类似的东西:身份validation框架的命名配置选项。 让我试着快速解释这个概念(请耐心等待):
ASP.NET Core中的身份validation堆栈支持注册相同类型的多个身份validation提供程序,例如,您可能最终拥有应用程序可能使用的多个OpenID Connect提供程序。 但是,虽然它们都共享协议的相同技术实现,但是需要有一种方法让它们独立工作并单独配置实例。
这通过给每个“认证方案”赋予唯一名称来解决。 添加方案时,基本上注册一个新名称并告诉注册它应该使用哪种处理程序类型。 此外,您使用IConfigureNamedOptions
配置每个方案,当您实现它时,基本上会传递一个未配置的选项对象,然后配置 – 如果名称匹配。 因此,对于每种身份validation类型T
, IConfigureNamedOptions
最终会有多个注册,可以为方案配置单个选项对象。
在某些时候,特定方案的身份validation处理程序运行并需要实际配置的选项对象。 为此,它依赖于IOptionsFactory
, 默认实现使您能够创建具体的选项对象,然后由所有这些IConfigureNamedOptions
处理程序配置。
选项工厂的确切逻辑就是你可以用来实现一种“命名依赖”。 翻译成您的特定示例,例如,如下所示:
// container type to hold the client and give it a name public class NamedHttpClient { public string Name { get; private set; } public HttpClient Client { get; private set; } public NamedHttpClient (string name, HttpClient client) { Name = name; Client = client; } } // factory to retrieve the named clients public class HttpClientFactory { private readonly IDictionary _clients; public HttpClientFactory(IEnumerable clients) { _clients = clients.ToDictionary(n => n.Key, n => n.Value); } public HttpClient GetClient(string name) { if (_clients.TryGet(name, out var client)) return client; // handle error throw new ArgumentException(nameof(name)); } } // register those named clients services.AddSingleton (new NamedHttpClient("A", httpClientA)); services.AddSingleton (new NamedHttpClient("B", httpClientB));
然后,您可以在某处注入HttpClientFactory
并使用其GetClient
方法来检索命名客户端。
显然,如果你考虑这个实现以及我之前写的内容,那么这看起来与服务定位器模式非常相似。 在某种程度上,在这种情况下它确实是一个,尽管是在现有的dependency injection容器之上构建的。 这会让它变得更好吗? 可能不是,但它是用现有容器实现您的需求的一种方式,所以重要的是。 对于完全防御btw。,在上面的身份validation选项案例中,options工厂是一个真正的工厂,因此它构造实际对象并且不使用现有的预先注册的实例,因此从技术上讲它不是那里的服务位置模式。
显然,另一种选择是完全忽略我上面写的内容并使用与ASP.NET Core不同的dependency injection容器。 例如, Autofac支持命名依赖项,它可以轻松替换ASP.NET Core的默认容器 。
使用命名注册
这正是命名注册的目的。
像这样注册:
container.RegisterInstance(new HttpClient(), "ClientA"); container.RegisterInstance (new HttpClient(), "ClientB");
并检索这种方式:
var clientA = container.Resolve("ClientA"); var clientB = container.Resolve ("ClientB");
如果您希望ClientA或ClientB自动注入另一个注册类型,请参阅此问题 。 例:
container.RegisterType( new InjectionConstructor( // Explicitly specify a constructor new ResolvedParameter("ClientA") // Resolve parameter of type HttpClient using name "ClientA" ) ); container.RegisterType( new InjectionConstructor( // Explicitly specify a constructor new ResolvedParameter ("ClientB") // Resolve parameter of type HttpClient using name "ClientB" ) );
使用工厂
如果您的IoC容器缺乏处理命名注册的能力,您可以注入工厂并让控制器决定如何获取实例。 这是一个非常简单的例子:
class HttpClientFactory : IHttpClientFactory { private readonly Dictionary _clients; public void Register(string name, HttpClient client) { _clients[name] = client; } public HttpClient Resolve(string name) { return _clients[name]; } }
在您的控制器中:
class ControllerA { private readonly HttpClient _httpClient; public ControllerA(IHttpClientFactory factory) { _httpClient = factory.Resolve("ClientA"); } }
在你的作文根:
var factory = new HttpClientFactory(); factory.Register("ClientA", new HttpClient()); factory.Register("ClientB", new HttpClient()); container.AddSingleton(factory);
实际上,服务的使用者不应该关心它正在使用的实例的实现。 在您的情况下,我认为没有理由手动注册许多不同的HttpClient
实例。 您可以注册一次类型,任何需要实例的消费实例都将获得它自己的HttpClient
实例。 你可以用AddTransient
做到这AddTransient
。
AddTransient方法用于将抽象类型映射到为需要它的每个对象单独实例化的具体服务
services.AddTransient();