ASP.NET Core 2

时间:2017-09-28 18:34:42

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

在ASP.NET Core 2 Web Api中,我想使用依赖注入将httpClientA HttpClient实例注入ControllerA,并httpClientB实例HttpClient {1}}至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-traveler(Dylan)指出这一点。

4 个答案:

答案 0 :(得分:24)

  

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

内置依赖注入容器不支持命名依赖注册,并且有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<T>配置每个方案,当您实现它时,基本上会传递一个未配置的选项对象,然后进行配置 - 如果名称匹配。因此,对于每种身份验证类型T,最终将为IConfigureNamedOptions<T>进行多个注册,这些注册可以为方案配置单个选项对象。

在某些时候,特定方案的身份验证处理程序会运行,并且需要实际配置的选项对象。为此,它取决于IOptionsFactory<T>default implementation使您能够创建具体选项对象,然后由所有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));

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

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

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

答案 1 :(得分:4)

使用命名注册

这正是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);

答案 2 :(得分:1)

另一种选择是

  • 在接口上使用附加的通用类型参数,或者在实现非通用接口的新接口上使用
  • 实现适配器/拦截器类以添加标记类型,然后添加
  • 使用通用类型作为“名称”

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

答案 3 :(得分:0)

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

执行此操作
  

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

services.AddTransient<HttpClient, HttpClient>();