C# – Ambient dependency injection through static service locator

asp.net-corecdependency-injectionservice-locator

After some googling I found some debates about whether constructor injection or property/field injection is better, but there is yet another alternative that strikes me as more beneficial.

In most programming environments, there is something called "the call context", which is information implicitly passed from caller to callee.

Sometimes this context is simply thread-local storage, sometimes, in the case of cross-thread asynchronous programming, it's more complex than that.

In any case, such contexts store information such as culture, user, or, for web applications, the current request.

These types of information are rarely carried around explicitly but accessed through global methods that are accessing said contexts.

In the use case of dependency injection, this means a static/global form of service locator (my example is in C#):

public static Sl
{
    public static Dependency Get<Dependency>();

    public static IDisposable Push<Dependency>(Dependency dependency);
}

The usage would be:

public void SomeFunction()
{
    Sl.Get<Dependency>().UseItSomehow();
}

To call SomeFunction, the dependency can be set or changed for the callees like this:

using (Sl.Push<SomeDependency>(someImplementation)
{
    SomeFunction();
}

I think this pattern superior to other types of dependency injection in those cases where a dependency is uniformly required for a large part of a larger call subtree.

For example, a repository, logger or configuration interface is rarely required only for a single function, but also for all its subsequent nested calls.

Especially with contructor injection this leads to some clutter that can be completely avoided: SomeFunction above can call other functions that will get the dependency automatically. It can also change the dependency in those cases where it actually is necessary.

I want to address some of the concerns I think I read somewhere, and my question would then be if anyone can think of others.

  • There's a dependency on Sl

    This strikes me as a fundamentalist's objection. Most software also depend on integers, strings and lists, without such things being properly abstracted away and passed around with proper respect to the pure Church of Dependency Injection. Clearly some low-level things one needs to depend on directly – hopefully they are well-designed and ideally they reside in the runtime library of the respective language. If not, they may still be the lesser evil.

  • Required dependencies should be explicit

    The only really explicit form of DI is contructor injection or passing dependencies directy in method calls. Unfortunately, this is also the most verbose and unflexible way of doing it. Dependencies that are added later change constructor or method signatures, requiring a refactoring of potentially several layers of function calls that do nothing but passing down objects verbatim. In static languages, this is tedious. In dynamic ones, it's even a source of bugs.

    Furthermore, if the dependency is required, the respective feature will fail whenever it's used anyway, making it very likely that a missing-dependency bug won't accidentally go to production.

    So ideally, yes, required dependencies should be explicit. But there's always a tradeoff, and am I really the only one who thinks that the constructor injection folk don't have their priorities straight?

  • One shouldn't rely on magic

    Call contexts and thread-statics are not magic, they are merely advanced and perhaps more technical than what is covered in your typical programming course at university.

    Also, the implementation is hidden behind the static service locator. Users don't have to know how it works to use it.

Besides this, there's also a strange hypocrisy among DI-fans. Take ASP.NET Core, for example: These guys literally force a DI-paradigm on their users. The result is a design that makes realizing existing dependencies particularly difficult. It's as if they are mocking themselves.

Consider how they allow binding certain configuration for outgoing web requests, such as authentication or setting some custom headers, to the code that is using it: You first do the configuration code (HttpClient and HttpClientFactory) and put them in a global service container. Then whereever the client is needed, it's fished out of the container. That means that if the configuration code is missing, everything will still compile and you won't notice until runtime.

And what if you have different configurations for different use points? ASP.NET Core's solution: Associate the specially configured HttpClientFactorys with magic strings and use those magic strings to fish out the correct one where it's needed!

Are you kidding me? What's going on here?

Dependencies used to be more explicit before this insanity was sweeping the minds of coders.

I'm now asked to make tedious finger excerises (ctor injection) and then throw those types all in one global container with no compile time checking for correct composability whatsoever.

So my question: Are there any legitimate, real-world reasons why DI with a static/global service locator would be bad, or am I correct in concluding that it is a sensible way to deal with this unfortunate craziness?

Best Answer

Are there any other objections? Are there any legitimate, real-world reasons why DI with a static/global service locator would be bad?

Ugh, yes.

Statics/globals are horrible. They assume that the runtime of your application is homogenous - all of your instances require all the same sort of instances all over. That is naive. They interfere with concurrency, since any state in any of these things is inherently shared state. They interfere with testing, since you can't effectively mock different instances running concurrently or even different instances down the callstack. And then there's the usual debugging issues with global state.

Hiding your problems is not fixing your problems. If you have something that everything needs, then you have widespread coupling. If you have objects that need a bunch of dependencies, then you have widespread coupling. If you have a huge object hierarchy that makes it difficult to pass things around where they need to go, then you probably have a poor design. DI does not fix these things! All you're doing is hiding it behind some magic that passes dependencies around. You're not removing the dependencies, just making them less visible and a runtime error rather than a compile time error.