Skip to content

Instantly share code, notes, and snippets.

@mindplay-dk
Last active July 16, 2022 14:45
Show Gist options
  • Save mindplay-dk/a86a301c17c9341e135e3b61e0d42379 to your computer and use it in GitHub Desktop.
Save mindplay-dk/a86a301c17c9341e135e3b61e0d42379 to your computer and use it in GitHub Desktop.
Named registrations in dependency injection containers

In response to this answer:

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.

I'm not sure what is meant by "type-safe" here?

The way service providers are provisioned in .NET today (via service providers and descriptors) is only "type-safe" in the sense that e.g. constructors specify the required types of dependencies that should be injected - this approach is not "type-safe" when it comes to ensuring that required services actually exist: you get run-time errors if they don't.

Having the ability to specify a specific, named dependency of a given type also wouldn't be "type-safe" in that sense - but (assuming registrations would still include a type and a name) it is just as "type-safe" as what is currently possible with service providers.

I would not suggest using parameter attributes or property injection. This does in deed add complexity. But you don't need any of those things to achieve named dependency registrations - you just need a way to specify, for a given dependency, which named instance you would want to inject, which you would do in your service provider, not in your application code.

(Note that "named dependencies", to my understanding, implies the naming of dependencies of a specific type. That is, an HttpClient named A does collide with a DatabaseConnection named A - the internal service registry is something like a Dictionary with a (Type, string?) tuple for a key, where a null name for the string? refers to a default (unnamed) service registration. If you had names instead of types, that would be bad, as no service lookups would be "type-safe".)

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.

Named dependencies are not a sign that "you are not designing your dependencies properly" - they are a straight forward requirement, and they definitely do not violate LSP.

I will give two real-world examples, one for abstract (interface) types, and one for concrete (class) types:

  1. An abstract ILogger type: in some parts of our system, we want an implementation that logs to a local disk - in other areas, we want an implementation that logs to a remote service. LSP is about types: an implementation of ILogger can't violate LSP, if it compiles. In practice, you can reconfigure your system at the provider level, at any time, swapping out the implementations you provide to different services, without breaking anything.

  2. A concrete Database instance: in some parts of our system, we want an implementation that connects to database A - in other areas, we want an implementation that connects to database B. This cannot not violate LSP. In practice, you can reconfigure your to move databases between servers, merge several databases into one, split a single database into several databases, and so on - all without affect any type relationships.

The need for named dependencies arises from the need to have more than one instance of something - which is a real and practical need. The only alternative is e.g. LoggerLocator or DatabaseLocator services, which you could query for a named instance - but, unless your services need to dynamically log to multiple sources, or dynamically access different databases, these would just be service locators: you're not doing dependency injection anymore.

Furthermore, if you look at those dependency injection containers 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.

This is not an issue at the provider level - the point of doing dependency injection, is the bootstrapping of your application happens in a single location (your program/main) and those concerns don't leak into your business code.

Using the service locator in your container provisioning code is expected - that's what it's for.

To illustrate my point, here is an example of a hand-written service locator doing dependency injection:

https://dotnetfiddle.net/un5Bld

As long as the ApplicationContext service locator is not a dependency anywhere outside of Main, you are doing dependency injection.

It's also worth noting that, in the example provided here, all services are in fact "named services" - each of them are behind a property-name, and this is all completely type-safe - it's actually more type-safe than what you could achieve with most dependency injection containers, in the sense that this pattern will guarantee (at compile-time) that services have actually been defined.

As for the comments on Simple Injector:

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 remark implies you're creating depedencies on the DI container. You wouldn't need to do that - you would just need some (declarative) means of specifying a named instance, in the bootstrap code, in the provider; or some means of accessing the container instance (service locator) from a callback, in the provider.

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.

As I interpret this remark, the "specific factory" in question is the e.g. LoggerLocator or DatabaseLocator described above - this does not "make the design cleaner", it just introduces a service locator for a particular type of service in your domain code. If the only thing your service does with this "specific factory" is look up another specific service instance, that is the definition of the service locator anti-pattern.

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. [...] You would then inject the HttpClientFactory somewhere and use its GetClient method to retrieve a named client.

If a given service depends on HttpClientFactory solely for the purpose of looking up it's actual dependency, the HttpClient, using a single call to GetClient, the HttpClientFactory in this role is a service locator.

Obviously, if you think about this implementation and about what I wrote earlier, then this will look very similar to a service locator pattern.

It not only looks similar, it is the same thing.

Think about testing your services, for one - you now need to mock an HttpClientFactory solely for the purpose of constructing the service you want to test. All this wrapping and unwrapping of the actual HttpClient dependency while testing is usually a strong indicator that you're not doing dependency injection.

Your only alternative to named dependencies (perhaps short of introducing arbitrary generic types to use as "markers") is more service locators - so a dependency injection container should support named dependencies.

That being said...

There is one way to bootstrap your service locators in such a way that they do not become dependencies of other classes - there is no way around them, but we can make sure they are implementation details, rather than dependencies.

For example, say you have many services, some of which depend on an ILogger that logs to disk, and others depend on an ILogger that logs to a remote service - you can set this up in your main program, where the simplest sort of service locator you can use (short of writing one) is Lazy<T>.

Maybe not the most obvious solution, but fairly simple:

// service locators for your loggers:

var consoleLogger = new Lazy<ILogger>(() => LoggerFactory.Create(builder => builder.AddConsole()).CreateLogger<Program>());

var remoteLogger = new Lazy<ILogger>(/* ... */);

// service registrations

builder.Services.AddSingleton<UserService>(s =>
    new UserService(consoleLogger.Value, s.GetRequiredService<UserRepository>()));

builder.Services.AddSingleton<ProductService>(s =>
    new ProductService(remoteLogger.Value, s.GetRequiredService<UserRepository>()));

This is maybe not ideal, as you're going outside the automatic dependency injection, and manually writing code to look up service dependencies - but under the circumstances, this does give you the control you need in order to inject the right instance, and it's a lot better than having your UserService and ProductService depend on some sort of service locator.

Note that this example is a bit simplified for brevity - in practice, you would most likely want those Lazy<ILogger> instances available somewhere, so you can unit-test the factory functions, rather than creating them inline, as I'm doing here.

Here the service locator example from the accepted answer on Stack Overflow, corrected to use dependency injection:

public class ClientLocator
{
    public readonly Lazy<HttpClient> ClientA;
    public readonly Lazy<HttpClient> ClientB;

    ClientLocator()
    {
        ClientA = setupClient("https://service-A.com/");
        ClientB = setupClient("https://service-B.com/");
    }

    private Lazy<HttpClient> setupClient(string baseAddress)
    {
        return new Lazy<HttpClient>(() => {				
            var client = new HttpClient();

            client.BaseAddress = new Uri(baseAddress);

            return client;
        });
    }
}

Usage for this in dependency injection would be something like this:

var clientLocator = new ClientLocator();

builder.Services.AddSingleton<ServiceA>(s => new ServiceA(clientLocator.ClientA.Value));

builder.Services.AddSingleton<ServiceB>(s => new ServiceB(clientLocator.ClientB.Value));

In scenarios where the dependency itself depends on something from the container, things get worse. In that case, you would most likely need individual service locators for the individual instances, which gets really clunky, e.g.:

class FooServiceLocator
{
  public readonly Lazy<FooService> fooService;
  
  public FooServiceLocator(ServiceA serviceA)
  {
    fooService = new Lazy<FooService>(() => new FooService(service A, "some other dependency"));
  }
}

You can now register FooServiceLocator as a regular singleton, and then manually look up the dependency using a closure:

builder.Services.AddSingleton<FooServiceLocator>();

builder.Services.AddSingleton<ProductService>(s =>
    new ProductService(s.GetRequiredService<FooServiceLocator>().fooService.Value));

This does have the drawback of actually making FooServiceLocator available for dependency injection, so it's not ideal.

If you can think of a way around that, please post a comment!

Either way, all of this would be much simpler if we had named dependencies.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment