【问题标题】:Dependency injection of multiple instances of same type in ASP.NET Core 2ASP.NET Core 2 中相同类型的多个实例的依赖注入
【发布时间】:2018-03-10 14:36:19
【问题描述】:

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

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 微软似乎有一些工作中

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

【问题讨论】:

标签: c# dependency-injection asp.net-core


【解决方案1】:

注意:此答案使用HttpClientHttpClientFactory 作为示例,但很容易适用于任何其他类型的事物。特别是对于HttpClient,首选来自Microsoft.Extensions.Httpusing the new IHttpClientFactory


内置的依赖注入容器不支持命名依赖注册,有no plans to add this at the moment

其中一个原因是,使用依赖注入,没有类型安全的方法可以指定您想要哪种命名实例。您当然可以为构造函数使用参数属性(或用于属性注入的属性上的属性),但这将是一种不同的复杂性,可能不值得;而且它肯定不会由类型系统支持,这是依赖注入工作方式的重要组成部分。

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

此外,如果您查看那些支持命名依赖项的依赖注入容器,您会注意到检索这些依赖项的唯一方法不是使用依赖注入,而是使用service locator pattern与 DI 促进的 inversion of control 完全相反。

Simple Injector,较大的依赖注入容器之一,explains their absence of named dependencies like this

通过键解析实例是 Simple Injector 有意遗漏的一个功能,因为它总是会导致应用程序往往对 DI 容器本身有大量依赖项的设计。要解析键控实例,您可能需要直接调用 Container 实例,这会导致 Service Locator anti-pattern

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


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

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

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

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

在某些时候,特定方案的身份验证处理程序运行并需要实际配置的选项对象。为此,它取决于IOptionsFactory&lt;T&gt;,其default implementation 使您能够创建一个具体的选项对象,然后由所有IConfigureNamedOptions&lt;T&gt; 处理程序配置。

而选项工厂的确切逻辑就是你可以利用它来实现一种“命名依赖”。翻译成您的特定示例,例如可能如下所示:

// 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));

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

显然,如果您考虑一下这个实现以及我之前写的内容,那么这看起来与服务定位器模式非常相似。在某种程度上,在这种情况下它确实是一个,尽管它建立在现有的依赖注入容器之上。这会让它变得更好吗?可能不是,但这是用现有容器实现您的要求的一种方式,所以这很重要。顺便说一句,为了全面防御,在上面的身份验证选项案例中,选项工厂是一个 real 工厂,因此它构造实际对象并且不使用现有的预注册实例,所以它在技术上是 不是那里的服务位置模式。


显然,另一种选择是完全忽略我上面写的内容,并在 ASP.NET Core 中使用不同的依赖注入容器。比如Autofac支持命名依赖,可以easily replace the default container for ASP.NET Core

【讨论】:

  • 感谢您的建议。 HttpClient 是一种尴尬的类型,我想根据要使用它的控制器将其基地址和标头参数设置为不同的值。特别是对于标头,在 startup.cs 中设置这些配置值比在控制器中尝试更容易。
  • 是的,HttpClient 有点烦人。我真的希望他们尽快前进with that new API
  • 工厂是正确的建议,但 DI 容器的一个重要方面是对象生命周期管理。上面的工厂模式将创建每个实现的实例,并且没有关于何时应处置对象的语义。
  • @poke 我喜欢你的回答,并在上面创建了一个变体,并在博客上写了here。它还解决了 Andrew 的担忧(尽管仍有一些情况可能与预期不符)。
【解决方案2】:

另一个选择是

  • 在接口上使用额外的泛型类型参数或实现非泛型接口的新接口,
  • 实现一个适配器/拦截器类来添加标记类型,然后
  • 使用泛型类型作为“名称”

我写了一篇更详细的文章:Dependency Injection in .NET: A way to work around missing named registrations

【讨论】:

    【解决方案3】:

    使用命名注册

    这正是named registrations 的用途。

    像这样注册:

    container.RegisterInstance<HttpClient>(new HttpClient(), "ClientA");
    container.RegisterInstance<HttpClient>(new HttpClient(), "ClientB");
    

    并以这种方式检索:

    var clientA = container.Resolve<HttpClient>("ClientA");
    var clientB = container.Resolve<HttpClient>("ClientB");
    

    如果您希望 ClientA 或 ClientB 自动注入另一个注册类型,请参阅this question。示例:

    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"
        )
    );
    

    使用工厂

    如果您的 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];
        }
    }
    

    在你的控制器中:

    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<IHttpClientFactory>(factory);
    

    【讨论】:

    • 我正在使用带有 IServiceCollection 的 .NET Core 2,而不是 Unity。你知道 IServiceCollection 是否有类似的工具?
    • 不确定。检查文档。如果我的答案没有,我添加了一个替代方案。
    • 您的第一个代码 sn-p 无效 C#:RegisterInstance&lt;HttpClient,new HttpClient()&gt;.
    • 已更正。谢谢。
    • Dotnetcore IServiceProvider 故意不允许命名注册。您提供的文章适用于过时的 Unity 容器。
    【解决方案4】:

    实际上,服务的使用者不应该关心它正在使用的实例的实现在哪里。在您的情况下,我认为没有理由手动注册 HttpClient 的许多不同实例。您可以注册一次类型,任何需要实例的消费实例都将获得它自己的HttpClient 实例。你可以通过AddTransient 做到这一点。

    AddTransient 方法用于将抽象类型映射到具体服务,这些服务为每个需要它的对象单独实例化

    services.AddTransient<HttpClient, HttpClient>();
    

    【讨论】:

    • 我想在启动时为每个 HttpClient 设置 HttpClient 基地址。在这种情况下,正确的控制器获得正确的 HttpClient 非常重要。
    • @Bryan - 可能。端点(endPointAendPointB 等)在哪里注册?也许有一种方法可以根据工厂模式动态返回正确的。如果您有许多可能随时间变化的端点值,您希望避免脆弱的代码,您必须将端点/ HttpClients 的注入列表与您在代码中所做的功能更改(添加控制器、添加方法等)分开)。
    • 在某些方面这是一个学术问题,我有其他方法来解决它。但我认为 IServiceCollection 必须有一种方法来区分命名实例,如 Castle、Ninject 或 Unity 可以做到的。
    猜你喜欢
    • 2021-08-25
    • 1970-01-01
    • 2016-10-23
    • 2018-04-05
    • 2020-10-13
    • 1970-01-01
    • 1970-01-01
    • 2018-08-23
    • 2018-03-23
    相关资源
    最近更新 更多