Enabling dependencies of multiple instances of the same type in ASP.NET Core 2

In ASP.NET Core 2 Web Api, I want to use dependency injection to insert an HttpClient instance of HttpClient into ControllerA , and an HttpClient instance from HttpClient to ControllerB .

The DI registration code will look something like this:

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

I know that I could subclass HttpClient create a unique type for each controller, but it does not scale very well.

What's better?

UPDATE In particular, regarding Microsoft's HttpClient, something seems to be working

https://github.com/aspnet/HttpClientFactory/blob/dev/samples/HttpClientFactorySample/Program.cs#L32 - thanks to @ mountain-traveler (Dylan) for pointing this out.

+5
source share
3 answers

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 dependency injection does not have a safe type to indicate which type of named instance you need. You could use something like parameter attributes for constructors (or property attributes to insert properties), but this will be another type of complexity that is probably not worth it; and this, of course, will not be supported 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 properly developing your dependencies. If you have two different dependencies of the same type, this means that they can be used interchangeably. If this is not the case, and one of them is valid where the other is not, then this is a sign that you can violate the principle of Liskov substitution.

Also, if you look at these dependency injections that support named dependencies, you will notice that the only way to get these dependencies is not using dependency injection, but a service locator pattern , which is the exact opposite of control inversion , which DI makes easier.

A simple injector, one of the large containers for dependency injection, explains their lack of named dependencies like this :

Key removal is a feature that is intentionally excluded from Simple Injector because it invariably leads to a design in which the application has many dependencies on the DI container itself. To allow an instance with a key, you will most likely need to call the container instance directly, and this will lead to an anti Locator pattern.

This does not mean that resolving instances with a key is never useful. Clearing instances with a key is usually a job for a specific factory, not a container. This approach makes the design much cleaner, eliminates the need to take a lot of dependencies on the DI library, and allows many scenarios that the authors of the DI container simply did not consider.


For all that you say, sometimes you really want something similar, and the presence of a large number of subtypes and individual registrations is simply impossible. In this case, there are suitable ways to approach this.

There is one specific situation, I can think of where ASP.NET Core has something similar to this in its code: Named configuration parameters for the authentication infrastructure. Let me try to explain the concept quickly (bear with me):

The authentication stack in ASP.NET Core supports the registration of several authentication providers of the same type, for example, you can get several OpenID Connect providers that your application can use. But, although they all use the same technical implementation of the protocol, it is necessary that they work independently and individually configure the instances.

This is accomplished by providing each “authentication scheme” with a unique name. When you add a schema, you basically register the new name and specify the registration that the handler type should use. In addition, you configure each scheme with IConfigureNamedOptions<T> , which, when you implement it, basically gets the passed object of undefined options, which is then configured if the name matches. Thus, for each type of T authentication, there will eventually be several accounts for IConfigureNamedOptions<T> that can configure a separate parameter object for the scheme.

At some point, the authentication processor for a particular scheme starts up and needs an actual configured object of options. For this, it depends on the IOptionsFactory<T> , which the default implementation gives you the opportunity to create a specific options object, which is then configured by all these IConfigureNamedOptions<T> handlers.

And this precise logic of the factory options is what you can use to achieve some sort of “named dependency”. This can be done in the following example:

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

Then you paste the HttpClientFactory somewhere and use its GetClient method to get the named client.

Obviously, if you think about this implementation and what I wrote earlier, it will be very similar to the service locator pattern. And in a way, this is truly one in this case, although it is built on top of an existing dependency injection container. Does it make it better? Probably not, but this is a way to fulfill your requirement with an existing container, so that's what counts. For complete protection, in the above authentication options, the factory parameters are real factory, therefore it builds the actual objects and does not use existing pre-registered instances, therefore it is technically not a service location template.


Obviously, another 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 can easily replace the default container for ASP.NET Core .

+8
source

Use named registrations

This is exactly what the registration name is for.

Register here:

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

And extract this path:

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

If you want ClientA or ClientB to be automatically entered 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 factory

If your IoC container is not able to handle named registrations, you can enter 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 root directory:

 var factory = new HttpClientFactory(); factory.Register("ClientA", new HttpClient()); factory.Register("ClientB", new HttpClient()); container.AddSingleton<IHttpClientFactory>(factory); 
+3
source

Indeed, the consumer of the service should not have to worry about where to use the instance that he is using. In your case, I see no reason to manually register many different HttpClient instances. You can register a type once, and any instance instance that needs an instance will receive its own instance of HttpClient . You can do this with AddTransient .

The AddTransient method is used to map abstract types to specific services, which are created separately for each object that requires it.

 services.AddTransient<HttpClient, HttpClient>(); 
0
source

Source: https://habr.com/ru/post/1272205/


All Articles