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 .