ASP.NET Core 2中相同类型的多个实例的依赖注入

Bry*_*yan 16 c# dependency-injection asp.net-core

在ASP.NET的Core 2网络API,我想使用依赖注入注入httpClientA的情况下HttpClient,以ControllerA和实例httpClientBHttpClientControllerB.

DI注册码看起来像:

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

HttpClient httpClientB = new HttpClient();
httpClientB.BaseAddress = endPointB;
services.AddSingleton<HttpClient>(httpClientB);
Run Code Online (Sandbox Code Playgroud)

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

什么是更好的方法?

更新 特别是关于HttpClient微软似乎有一些工作正在进行中

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

pok*_*oke 28

注意:这个答案使用HttpClientHttpClientFactory作为一个例子,但很容易适用于任何其他类型的事情.对于HttpClient特别是,使用新的IHttpClientFactoryMicrosoft.Extensions.Http是优选的.


内置依赖注入容器不支持命名依赖注册,目前还没有计划添加它.

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

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

此外,如果你看一下那些支持命名依赖项的依赖注入包含,你会发现检索这些依赖项的唯一方法是不使用依赖注入,而是使用服务定位器模式,这与DI促进的控制反转完全相反.

Simple Injector是一个较大的依赖注入容器,它解释了它们缺少这样的命名依赖:

通过一键解决实例是故意漏掉简单的喷油器,因为这必然导致在应用程序往往有DI容器本身在许多相关性的设计特点.要解析键控实例,您可能需要直接调用Container实例,这会导致Service Locator反模式.

这并不意味着通过密钥解析实例永远不会有用.通过密钥解析实例通常是特定工厂而不是Container的工作.这种方法使设计更加清晰,使您不必在DI库上采用多种依赖关系,并且可以实现DI容器作者根本不考虑的许多场景.


尽管如此,有时你真的想要这样的东西并拥有大量的子类型和单独的注册是不可行的.在这种情况下,有适当的方法来解决这个问题.

我可以想到一个特殊的情况,ASP.NET Core在其框架代码中有类似的东西:身份验证框架的命名配置选项.让我试着快速解释这个概念(请耐心等待):

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

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

在某些时候,特定方案的身份验证处理程序会运行,并且需要实际配置的选项对象.为此,它取决于IOptionsFactory<T>哪个默认实现使您能够创建具体选项对象,然后由所有这些IConfigureNamedOptions<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));
Run Code Online (Sandbox Code Playgroud)

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

显然,如果你考虑这个实现以及我之前写的内容,那么这看起来与服务定位器模式非常相似.在某种程度上,它在这种情况下确实是一个,尽管建立在现有的依赖注入容器之上.这会让它变得更好吗?可能不是,但它是用现有容器实现您的需求的一种方式,所以重要的是.为了充分辩护顺便说一句,在上面的身份验证选项的情况下,选择工厂是一个真正的工厂,所以它构建实际的对象,并且不使用现有的预登记的情况下,所以它在技术上存在一个服务点的模式.


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


Joh*_* Wu 6

使用命名注册

这正是命名注册的用途。

像这样注册:

container.RegisterInstance<HttpClient>(new HttpClient(), "ClientA");
container.RegisterInstance<HttpClient>(new HttpClient(), "ClientB");
Run Code Online (Sandbox Code Playgroud)

并以这种方式检索:

var clientA = container.Resolve<HttpClient>("ClientA");
var clientB = container.Resolve<HttpClient>("ClientB");
Run Code Online (Sandbox Code Playgroud)

如果您希望 ClientA 或 ClientB 自动注入另一个注册类型,请参阅此问题。例子:

container.RegisterType<ControllerA, ControllerA>(
    new InjectionConstructor(                        // Explicitly specify a constructor
        new ResolvedParameter<HttpClient>("ClientA") // Resolve parameter of type HttpClient using name "ClientA"
    )
);
container.RegisterType<ControllerB, ControllerB>(
    new InjectionConstructor(                        // Explicitly specify a constructor
        new ResolvedParameter<HttpClient>("ClientB") // Resolve parameter of type HttpClient using name "ClientB"
    )
);
Run Code Online (Sandbox Code Playgroud)

使用工厂

如果您的 IoC 容器缺乏处理命名注册的任何能力,您可以注入一个工厂并让控制器决定如何获取实例。这是一个非常简单的例子:

class HttpClientFactory : IHttpClientFactory
{
    private readonly Dictionary<string, HttpClient> _clients;

    public void Register(string name, HttpClient client)
    {
        _clients[name] = client;
    }

    public HttpClient Resolve(string name)
    {
        return _clients[name];
    }
}
Run Code Online (Sandbox Code Playgroud)

在你的控制器中:

class ControllerA
{
    private readonly HttpClient _httpClient;

    public ControllerA(IHttpClientFactory factory)
    {
        _httpClient = factory.Resolve("ClientA");
    }
}
Run Code Online (Sandbox Code Playgroud)

在你的作文根中:

var factory = new HttpClientFactory();
factory.Register("ClientA", new HttpClient());
factory.Register("ClientB", new HttpClient());
container.AddSingleton<IHttpClientFactory>(factory);
Run Code Online (Sandbox Code Playgroud)

  • Dotnetcore IServiceProvider 故意不允许命名注册。您提供的文章适用于过时的 Unity 容器。 (3认同)

Ric*_*ter 5

另一种选择是

  • use an additional generic type parameter on the interface or a new interface implementing the non generic interface,
  • implement an adapter/interceptor class to add the marker type and then
  • use the generic type as “name”

I’ve written an article with more details: Dependency Injection in .NET: A way to work around missing named registrations