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

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

问题描述

在ASP.NET Core 2 Web Api中,我想使用依赖项注入将HttpClienthttpClientA实例注入到ControllerA,并且将HttpClient的实例httpClientB注入到ControllerB./p>

DI注册代码如下:

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

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

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

有什么更好的方法?

更新 专门针对HttpClient的Microsoft似乎正在进行一些工作

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

解决方案

注意::此答案使用HttpClientHttpClientFactory作为示例,但很容易应用于其他类型的事物.特别是对于HttpClient,使用最好使用 Microsoft.Extensions.Http 中的新IHttpClientFactory .


内置的依赖项注入容器不支持命名的依赖项注册,并且有没有计划立即添加.

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

通常,命名依赖关系表明您没有正确设计依赖关系.如果您具有两个相同类型的不同依赖项,则这意味着它们可以互换使用.如果不是这种情况,并且其中一个有效,而另一个无效,则表明您可能违反了解释它们不存在像这样的命名依赖项:

通过键解析实例是故意遗漏在Simple Injector中的一项功能,因为它总是导致设计,其中应用程序倾向于对DI容器本身有很多依赖性.要解析键控实例,您可能需要直接调用 Container 实例,这会导致您的应用程序可能使用的OpenID Connect提供程序.但是,尽管它们都共享该协议的相同技术实现,但仍需要一种使它们独立工作并分别配置实例的方法.

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

有时,用于特定方案的身份验证处理程序将运行,并且需要实际配置的选项对象.为此,它取决于IOptionsFactory<T>,其中

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

然后将HttpClientFactory注入到某个位置,并使用其GetClient方法来检索命名的客户端.

很显然,如果您考虑此实现以及我之前写的内容,那么它看起来将与服务定位器模式非常相似.从某种意义上说,它确实是这种情况,尽管它是建立在现有依赖项注入容器之上的.这样会更好吗?可能不是,但这是通过现有容器实现您的要求的一种方式,这才是最重要的.顺带一提,在上面的身份验证选项中,选项工厂是一个 real 工厂,因此它构造实际对象,并且不使用现有的预注册实例,因此从技术上讲,它是不是那里的服务位置模式.


显然,另一种选择是完全忽略我上面写的内容,并在ASP.NET Core中使用其他依赖项注入容器.例如, Autofac 支持命名依赖关系,它可以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 contains 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 uses. 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> which 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.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<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天全站免登陆