ASP.NET Core 2 中多个相同类型实例的依赖注入 [英] Dependency injection of multiple instances of same type in ASP.NET Core 2

查看:21
本文介绍了ASP.NET Core 2 中多个相同类型实例的依赖注入的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

在 ASP.NET Core 2 Web Api 中,我想使用依赖注入将 HttpClienthttpClientA 实例注入到 ControllerA,以及一个HttpClientControllerB 的实例 httpClientB.

DI 注册代码类似于:

HttpClient httpClientA = new HttpClient();httpClientA.BaseAddress = endPointA;services.AddSingleton(httpClientA);HttpClient httpClientB = new HttpClient();httpClientB.BaseAddress = endPointB;services.AddSingleton(httpClientB);

我知道我可以子类化 HttpClient 来为每个控制器创建一个唯一的类型,但这不能很好地扩展.

什么是更好的方法?

更新特别是关于 HttpClient 微软似乎有一些工作

https://github.com/aspnet/HttpClientFactory/blob/dev/samples/HttpClientFactorySample/Program.cs#L32 - 感谢@mountain-traveller (Dylan) 指出这一点.

解决方案

注意:此答案使用 HttpClientHttpClientFactory 作为示例,但很容易适用于任何其他类型的东西.特别是对于 HttpClient使用来自 的新 IHttpClientFactoryMicrosoft.Extensions.Http 是首选.


内置依赖注入容器不支持命名依赖注册,并且有没有计划此刻添加这个.

这样做的一个原因是依赖注入,没有类型安全的方法来指定你想要的命名实例类型.您肯定可以使用类似构造函数的参数属性(或属性注入的属性)之类的东西,但这将是一种不同的复杂性,可能不值得;它肯定不会得到类型系统的支持,这是依赖注入工作方式的重要部分.

通常,命名依赖项表明您没有正确设计依赖项.如果您有两个相同类型的不同依赖项,那么这应该意味着它们可以互换使用.如果情况并非如此,并且其中一个有效而另一个无效,则表明您可能违反了 Liskov 替换原则.

此外,如果您查看那些确实支持命名依赖项的依赖项注入容器,您会注意到检索这些依赖项的唯一方法不是使用依赖项注入,而是 服务定位器模式 而不是 控制反转 DI 促进.

Simple Injector,较大的依赖注入容器之一,解释他们没有像这样的命名依赖:

<块引用>

通过键解析实例是 Simple Injector 故意忽略的一个特性,因为它总是导致应用程序往往对 DI 容器本身有很多依赖的设计.要解析键控实例,您可能需要直接调用 Container 实例,这会导致 服务定位器反模式.

这并不意味着通过键解析实例永远没有用.通过键解析实例通常是特定工厂的工作,而不是 Container.这种方法使设计更加简洁,使您不必对 DI 库产生大量依赖,并支持许多 DI 容器作者根本没有考虑过的场景.


话虽如此,有时您确实想要这样的东西并且拥有大量子类型和单独的注册根本不可行.在这种情况下,有适当的方法来解决这个问题.

我能想到的一种特殊情况是 ASP.NET Core 在其框架代码中具有与此类似的内容:身份验证框架的命名配置选项.让我尝试快速解释这个概念(请耐心等待):

ASP.NET Core 中的身份验证堆栈支持注册多个相同类型的身份验证提供程序,例如您可能最终拥有多个 OpenID Connect 提供程序.但是,尽管它们都共享相同的协议技术实现,但需要有一种方法让它们独立工作并单独配置实例.

这是通过给每个身份验证方案"一个唯一名称来解决的.添加方案时,基本上是注册一个新名称并告诉注册它应该使用哪种处理程序类型.此外,您可以使用 IConfigureNamedOptions 当您实现它时,基本上会传递一个未配置的选项对象,然后配置该对象 - 如果名称匹配.因此,对于每种身份验证类型 T,最终会为 IConfigureNamedOptions 注册多个,这些注册可以为方案配置单独的选项对象.

在某些时候,特定方案的身份验证处理程序运行并需要实际配置的选项对象.为此,它取决于 IOptionsFactory默认实现 使您能够创建一个具体的选项对象,然后由所有那些 IConfigureNamedOptions<T> 处理程序进行配置.>

选项工厂的确切逻辑是您可以用来实现一种命名依赖"的方法.翻译成您的特定示例,例如可能如下所示:

//容纳客户端并为其命名的容器类型公共类 NamedHttpClient{公共字符串名称 { 获取;私人订制;}公共 HttpClient 客户端 { 获取;私人订制;}public NamedHttpClient(字符串名称,HttpClient 客户端){姓名 = 姓名;客户 = 客户;}}//工厂来检索命名的客户端公共类 HttpClientFactory{私有只读 IDictionary_客户;公共 HttpClientFactory(IEnumerable 客户端){_clients = clients.ToDictionary(n => n.Name, n => n.Client);}公共 HttpClient GetClient(字符串名称){if (_clients.TryGet(name, out var client))回访客户;//处理错误throw new ArgumentException(nameof(name));}}//注册那些命名的客户端services.AddSingleton(new NamedHttpClient(A", httpClientA));services.AddSingleton(new NamedHttpClient(B", httpClientB));

然后您将在某处注入 HttpClientFactory 并使用其 GetClient 方法来检索命名的客户端.

显然,如果你考虑一下这个实现和我之前写的内容,那么这看起来非常类似于服务定位器模式.从某种意义上说,在这种情况下它确实是一个,尽管它构建在现有的依赖注入容器之上.这是否使它变得更好?可能不是,但这是使用现有容器实现您的需求的一种方式,所以这很重要.顺便说一下,在上面的身份验证选项案例中,选项工厂是一个真正的工厂,因此它构建了实际的对象并且不使用现有的预注册实例,因此在技术上是不是那里的服务位置模式.


显然,另一种选择是完全忽略我上面写的内容,并在 ASP.NET Core 中使用不同的依赖注入容器.例如,Autofac 支持命名依赖项,它可以轻松替换 ASP.NET Core 的默认容器.

In ASP.NET Core 2 Web Api, I want to use dependency injection to inject httpClientA instance of HttpClient to ControllerA, and an instance httpClientB of the HttpClient to ControllerB.

The DI registration code would look something like:

HttpClient httpClientA = new HttpClient();
httpClientA.BaseAddress = endPointA;
services.AddSingleton<HttpClient>(httpClientA);

HttpClient httpClientB = new HttpClient();
httpClientB.BaseAddress = endPointB;
services.AddSingleton<HttpClient>(httpClientB);

I know I could subclass HttpClient to make a unique type for each controller, but that doesn't scale very well.

What is a better way?

UPDATE Specifically regarding HttpClient Microsoft seems to have something in the works

https://github.com/aspnet/HttpClientFactory/blob/dev/samples/HttpClientFactorySample/Program.cs#L32 - thanks to @mountain-traveller (Dylan) for pointing this out.

解决方案

Note: This answer uses HttpClient and a HttpClientFactory as an example but easily applies to any other kind of thing. For HttpClient in particular, using the new IHttpClientFactory from Microsoft.Extensions.Http is preferred.


The built-in dependency injection container does not support named dependency registrations, and there are no plans to add this at the moment.

One reason for this is that with dependency injection, there is no type-safe way to specify which kind of named instance you would want. You could surely use something like parameter attributes for constructors (or attributes on properties for property injection) but that would be a different kind of complexity that likely wouldn’t be worth it; and it certainly wouldn’t be backed by the type system, which is an important part of how dependency injection works.

In general, named dependencies are a sign that you are not designing your dependencies properly. If you have two different dependencies of the same type, then this should mean that they may be interchangeably used. If that’s not the case and one of them is valid where the other is not, then that’s a sign that you may be violating the Liskov substitution principle.

Furthermore, if you look at those dependency injection containers that do support named dependencies, you will notice that the only way to retrieve those dependencies is not using dependency injection but the service locator pattern instead which is the exact opposite of inversion of control that DI facilitates.

Simple Injector, one of the larger dependency injection containers, explains their absence of named dependencies like this:

Resolving instances by a key is a feature that is deliberately left out of Simple Injector, because it invariably leads to a design where the application tends to have numerous dependencies on the DI container itself. To resolve a keyed instance you will likely need to call directly into the Container instance and this leads to the Service Locator anti-pattern.

This doesn’t mean that resolving instances by a key is never useful. Resolving instances by a key is normally a job for a specific factory rather than the Container. This approach makes the design much cleaner, saves you from having to take numerous dependencies on the DI library and enables many scenarios that the DI container authors simply didn’t consider.


With all that being said, sometimes you really want something like this and having a numerous number of subtypes and separate registrations is simply not feasible. In that case, there are proper ways to approach this though.

There is one particular situation I can think of where ASP.NET Core has something similar to this in its framework code: Named configuration options for the authentication framework. Let me attempt to explain the concept quickly (bear with me):

The authentication stack in ASP.NET Core supports registering multiple authentication providers of the same type, for example you might end up having multiple OpenID Connect providers that your application may use. But although they all share the same technical implementation of the protocol, there needs to be a way for them to work independently and to configure the instances individually.

This is solved by giving each "authentication scheme" a unique name. When you add a scheme, you basically register a new name and tell the registration which handler type it should use. In addition, you configure each scheme using IConfigureNamedOptions<T> which, when you implement it, basically gets passed an unconfigured options object that then gets configured—if the name matches. So for each authentication type T, there will eventually be multiple registrations for IConfigureNamedOptions<T> that may configure an individual options object for a scheme.

At some point, an authentication handler for a specific scheme runs and needs the actual configured options object. For this, it depends on IOptionsFactory<T> whose default implementation gives you the ability to create a concrete options object that then gets configured by all those IConfigureNamedOptions<T> handlers.

And that exact logic of the options factory is what you can utilize to achieve a kind of "named dependency". Translated into your particular example, that could for example look like this:

// 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<string, HttpClient> _clients;

    public HttpClientFactory(IEnumerable<NamedHttpClient> clients)
    {
        _clients = clients.ToDictionary(n => n.Name, n => n.Client);
    }

    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<NamedHttpClient>(new NamedHttpClient("A", httpClientA));
services.AddSingleton<NamedHttpClient>(new NamedHttpClient("B", httpClientB));

You would then inject the HttpClientFactory somewhere and use its GetClient method to retrieve a named client.

Obviously, if you think about this implementation and about what I wrote earlier, then this will look very similar to a service locator pattern. And in a way, it really is one in this case, albeit built on top of the existing dependency injection container. Does this make it better? Probably not, but it’s a way to implement your requirement with the existing container, so that’s what counts. For full defense btw., in the authentication options case above, the options factory is a real factory, so it constructs actual objects and doesn’t use existing pre-registered instances, so it’s technically not a service location pattern there.


Obviously, the other alternative is to completely ignore what I wrote above and use a different dependency injection container with ASP.NET Core. For example, Autofac supports named dependencies and it can easily replace the default container for ASP.NET Core.

这篇关于ASP.NET Core 2 中多个相同类型实例的依赖注入的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆