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

后端 未结 4 693
醉梦人生
醉梦人生 2020-12-29 23:57

In ASP.NET Core 2 Web Api, I want to use dependency injection to inject httpClientA instance of HttpClient to ControllerA, and an inst

相关标签:
4条回答
  • 2020-12-30 00:33

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

    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.

    0 讨论(0)
  • 2020-12-30 00:40

    Use named registrations

    This is exactly what named registrations are for.

    Register like this:

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

    And retrieve this way:

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

    If you want ClientA or ClientB automatically injected into another registered type, see this question. Example:

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

    Use a factory

    If your IoC container lacks any ability to handle named registrations, you could inject a factory and let the controller decide how to get the instance. Here is a really simple example:

    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];
        }
    }
    

    And in your controllers:

    class ControllerA
    {
        private readonly HttpClient _httpClient;
    
        public ControllerA(IHttpClientFactory factory)
        {
            _httpClient = factory.Resolve("ClientA");
        }
    }
    

    And in your composition root:

    var factory = new HttpClientFactory();
    factory.Register("ClientA", new HttpClient());
    factory.Register("ClientB", new HttpClient());
    container.AddSingleton<IHttpClientFactory>(factory);
    
    0 讨论(0)
  • 2020-12-30 00:51

    Really the consumer of the service should not care where about the implementation of the instance it is using. In your case I see no reason to manually register many different instances of HttpClient. You could register the type once and any consuming instance that needs an instance will get it's own instance of HttpClient. You can do that with AddTransient.

    The AddTransient method is used to map abstract types to concrete services that are instantiated separately for every object that requires it

    services.AddTransient<HttpClient, HttpClient>();
    
    0 讨论(0)
  • 2020-12-30 00:52

    Another option is to

    • 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

    0 讨论(0)
提交回复
热议问题