繁体   English   中英

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

[英]Dependency injection of multiple instances of same type in ASP.NET Core 2

在ASP.NET的Core 2网络API,我想使用依赖注入注入httpClientA的实例HttpClientControllerA和实例httpClientB的的HttpClientControllerB

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) 指出这一点。

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


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

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

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

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

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

通过键解析实例是 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.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方法来检索命名的客户端。

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


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

使用命名注册

这正是命名注册的用途。

像这样注册:

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 自动注入另一个注册类型,请参阅此问题 示例:

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

另一种选择是

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

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

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

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

services.AddTransient<HttpClient, HttpClient>();

暂无
暂无

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM