This page shows the source for this entry, with WebCore formatting language tags and attributes highlighted.
Title
Discussing DI, IOC, and containers
Description
I was recently allowed to observe as a team discussed the benefits and drawbacks of using an IOC container.<fn>
I was asked not to directly participate because it was a team-building exercise; the team needed to convince itself based on the merits of its own arguments. If those <i>for</i> the technology were unable to articulate their convictions sufficiently, then it wouldn't help for an outside authority to dictate the answer. I assisted in the background, with clarification and alternate explanations.
<h>Why no tests?</h>
Some team members had a reasonable hesitation to using an IOC. Why reasonable? Because they'd been hurt in the past by non-pragmatic and overly magical solutions.
The main reason that the other team members wanted to use DI and an ICO container was to improve testability. They also appreciated that a side-effect of DI is that it makes it so much easier not only to reason about your system, but to repurpose parts of it.
The disconnect arose because <b>the first group doesn't write automated tests.</b> Therefore, they never felt the pain of trying to replace an annoyingly <i>impure</i> component deep in the program logic with something else in order to test other components. If that situation doesn't comes up, then you might not see what the big deal is.
So, part of the confusion was that some of the team still had an at-best antiquated---and at-worst irresponsible and inefficient---approach to engineering because they did all of their testing manually and in an ad-hoc manner.
Another part of the confusion was terminology, where people were arguing against a technology by naming the concept. This was a missed opportunity for finding common ground and then focusing on the details, where they had different preferred approaches.
<img src="{att_link}application_components.webp" align="none" title="Application components">
<h>Clarifying terminology</h>
My colleagues were contrasting DI with what they were calling "static class trees". I think that expression is quite confusing because what they probably meant was "static <i>object</i> trees," which I think was meant to mean the untestable evil that is "bottom-up-instantiated object graphs."
Let's be clear about which kind of static class trees we think are bad.
<h level="3">Not using DI</h>
This is a static class tree rooted at <c>D</c> that does not inject any dependencies.
<code>class A {}
class B {}
class C {
A a;
B b;
C()
{
this.a = new A();
this.b = new B();
}
}
class D
{
A a;
C c;
D()
{
this.a = new A();
this.c = new C();
}
}
D d = new D();</code>
<h level="3">Using some DI</h>
You might complain that this is pathological because we've created <c>A</c> twice. OK, fine, let's pass it in to <c>C</c>.
<code>class A {}
class B {}
class C {
A a;
B b;
C(A a)
{
this.a = a;
this.b = new B();
}
}
class D
{
A a;
C c;
D()
{
this.a = new A();
this.c = new C(a);
}
}
D d = new D();</code>
Congratulations! You've injected your first dependency.
Now keep going!
<h level="3">Using DI</h>
The following code illustrates a static class tree rooted at <c>D</c> that uses dependency injection for everything. Note that this example uses primary constructors without confusing things too much, shortening the code considerably. All instantiations are under the control of the calling code.
<code>class A {}
class B {}
class C(A a, B b) {}
class D(A a, C c) {}
A a = new A();
D d = new D(a, new C(a, new B()));</code>
<h level="3">How DI and IOC are related</h>
<ul>
<b>IOC</b> is the concept. It stands for "inversion of control", which means that the control over who gets to decide which implementation backs a given interface is no longer with the <i>consumer</i> of the interface but the <i>provider</i>.
<b>DI</b> is a way of implementing IOC. (Usually rounded up to be equivalent.)
An <b>IOC Container</b> services requests for instances based on interface-to-implementation mappings.
</ul>
Looking at the examples above, I think we can all agree that DI is a good thing. That is, "dependency injection" and "inversion of control" as concepts are good things.
<h level="3">What does the IOC container do?</h>
An IOC container generally consists of two parts:
<ul>
A mapping of abstractions to implementations.
A method to resolve implementations from abstractions.
</ul>
In .NET, these are two completely separate interfaces, so that you can't register mappings when you should only be using them. You use the <c>IServiceCollection</c> in .NET to register mappings and then use <c>IServiceProvider</c> to request instances.
The service provider locates a requested service and constructs an instance where necessary. It recursively locates any parameters to the constructor of a requested service. Obviously, a directly requested service must be registered with a concrete implementation. But also, every service on which it depends must also be registered with a concrete implementation, recursively until dependencies don't have dependencies of their own.
Using an <i>IOC container</i> carries the following implications.
<ul>
✅ It reduces fragility when constructors are refactored.
⚠️ It can make it unclear which constructors are called.
✅ It can be helpful for implementing very generalized factories (where you inject the service provider into, say, a "plugin factory").
</ul>
A white paper I wrote six years ago has an extended example (in Swift, of all things): <a href="{app}/view_article.php?id=4436">Encodo White Papers: DI, IOC and Containers (2019)</a>.
As in the examples above, and in the extended one below, most of the steps in the paper do not use a container. You can do DI without a container---it just gets kind of tedious and wordy. Let the IOC container do the brain-dead stuff for you.
<h level="3">What's a DAG?</h>
Some folks might refer DAGs, which are <a href="https://en.wikipedia.org/wiki/Directed_acyclic_graph" source="Wikipedia">directed acyclic graphs</a>. This is just another way of referring to the graph of objects represented by the <i>composition root</i>, which is the single object you should create in your program's root method---often called <c>main</c>, but sometimes it's just the main file of your application---to create your application.
It's possible to build a DAG without DI. The second and third examples in the sections above also create DAGs. The reason those DAGs are hard to change or extend is because dependencies are created at lower levels rather than <i>injected</i>. We <i>want</i> a DAG ... but not like that. We want a DAG created with explicit dependencies, as illustrated by the first example.
For the most part, it's not really important what you call it, as long as you end up with testable components.
<h level="3">"Refining" your code (separating pure logic from impure)</h>
Any non-trivial application comprises <i>pure</i> and <i>impure</i> parts. The impure parts are the messy bits that communicate with the unpredictable outside world:
<ul>
Reading from the command line
Reading configuration from a file
Reading values from the environment
Reading data from files
Reading from a database
Reading user input
Calling network services
Etc.
</ul>
We don't want unpredictability in our testing application, so we'll push all of the impure stuff as far out to the edges as possible, leaving a nice, fat pile of pure logic that we can reliably and reproducibly test.
The preceding sections have hopefully convinced you that IOC is useful, and that constructor-based DI is a good way of implementing it. We've also discussed the advantages of using an IOC container to improve flexibility and reduce code-duplication.
All of these things are going to help achieve our goal of testing as much of the program logic as possible automatically.
<h level="3">The single-responsibility principle</h>
Components should not only be pure, but should have a <i>single responsibility</i>. That's the "S" in <a href="https://en.wikipedia.org/wiki/SOLID#Liskov_substitution_principle">SOLID</a>.
For functional languages, parameters serve as dependencies; for C# and many other languages, another natural "injection point" is a constructor. Where injection by parameter mixes parameter types---that is, which parameters are <i>data</i> and which are <i>tools</i> for the calculation---a constructor is more clearly a point at which to inject <i>tools</i>.
Now that we have a concept---use IOC to define dependencies as <i>abstractions</i>---and a mechanism---use DI via constructor to <i>inject</i> dependencies---we can write components that address a single responsibility. That is, we have a mechanism for ruthlessly separating concerns.
Components will stitch other dependencies together to accomplish their task (i.e., their sole responsibility). If this stitching code becomes too <i>involved</i>, then the act of stitching might be its own task!
When each component does only a single thing, it is easier to test its logic in isolation. It is, in many cases, <i>trivial</i>.
<h level="3">We can't test yet! (We need abstractions.)</h>
Do we have testable components yet, though? No. Even if we were to use a container with the examples above, we're not done yet! Our goal is to make our logic <i>testable</i> with automated tests. None of the examples above is <i>testable</i> because, although dependencies are injected, they are <i>concrete</i> dependencies.
These cannot be replaced with other implementations and thus cannot be mocked away in tests. That is, if the component <c>A</c> above accessed an external service available only in the cloud or when connected to hardware, you cannot test <c>D</c> without having access either to the cloud or hardware because <i>you can only ever pass in <c>A</c></i>.
We want all modules, high and low, to define and depend on abstractions that can be replaced. This is the "L" in <a href="https://en.wikipedia.org/wiki/SOLID#Liskov_substitution_principle">SOLID</a>. A component should <i>receive</i> configuration through an interface rather than either creating it itself or accessing a static, global, concrete instance. When all components receive all external dependencies as other, injected components, it's extremely easy to both reason about the code and to test it in isolation.
So, to be able to write automated tests, our next step will be to inject <i>abstractions</i> rather than concrete implementations. An abstraction defines a narrow interface that makes as few promises as possible while still fulfilling its task.
In C#, we typically use <i>interfaces</i> or <i>abstract classes</i>. Interfaces are way better. Just trust me.
<h>A more concrete example</h>
Instead of continuing with the toy classes defined above, let's look at how we would make a part of our program logic more testable.
Suppose you have the following code:
<code>class EmailClient
{
void Send(Email email) { ... }
}
class SubscriptionManager
{
void Notify()
{
var client = new EmailClient();
foreach (var email in _subscriptions.Select(CreateEmail))
{
client.Send(email);
}
}
Email CreateEmail(Subscription subscription) { ... }
}</code>
Now, suppose you'd like to test this code. You can't test it without an email server configured because the <c>EmailClient</c> is hard-coded. If you invert control, though, you can pass that dependency in to the <c>SubscriptionManager</c>. One way to do this is to pass the dependency directly into the method, like this:
<code>class SubscriptionManager
{
void Notify(<hl>EmailClient client</hl>)
{
foreach (var email in _subscriptions.Select(CreateEmail))
{
client.Send(email);
}
}
Email CreateEmail(Subscription subscription) { ... }
}</code>
Is this really solving anything, though? No. The callee is still in control of the type because the type of the parameter is a specific class. The caller has no choice but to pass in an <c>EmailClient</c>, which will try to sent mails to an external server over a network.
In order to support IOC, the callee needs to <i>abstract</i> its requirement. In C#, you use an <i>interface</i>.
<code><hl>interface IEmailClient
{
void Send(Email email);
}</hl>
class EmailClient <hl>: IEmailClient</hl>
{
public void Send(Email email) { ... }
}
class SubscriptionManager
{
void Notify(<hl>I</hl>EmailClient client)
{
foreach (var email in _subscriptions.Select(CreateEmail))
{
client.Send(email);
}
}
Email CreateEmail(Subscription subscription) { ... }
}</code>
We're done. We've implemented inversion of control. The caller now controls the concrete type.
We are also using <i>dependeny injection</i> but of a very manual kind: the caller is expected to provide the email-sending mechanism. This can be inconvenient and can muddy otherwise legible code because each and every caller has to have a reference the thing that the <c>SubscriptionManager</c> needs. That is, instead of coupling just the <c>SubscriptionManager</c> to an <c>IEmailClient</c>, we end up coupling any client of the <c>SubscriptionManager</c> as well.
Therefore, a common practice is to inject dependencies like this through the constructor.
<code>
class SubscriptionManager
{
<hl>private readonly IEmailClient _client;
public SubscriptionManager(IEmailClient client)
{
_client = client ?? throw new ArgumentNullException(nameof(client));
}</hl>
void Notify()
{
foreach (var email in _subscriptions.Select(CreateEmail))
{
<hl>_</hl>client.Send(email);
}
}
Email CreateEmail(Subscription subscription) { ... }
}
</code>
The code that calls <c>Notify()</c> no longer has to know anything about the dependency, thus better decoupling the <c>SubscriptionManager</c> interface from its consumers. The <c>SubscriptionManager</c> declares its dependencies in the constructor, which makes good use of that language construct. That is, we're leveraging the language to improve the clarity of our design.
At this point, we can still construct the <c>SubscriptionManager</c> manually, passing in the concrete type for <c>IEmailClient</c> but we can now also consider using an IOC <i>container</i>, as outlined above.
<h level="3">Another example: Injecting configuration</h>
Let's take a look at how we can lean on the IOC container to build our application's configuration.
<code>public class AppSettings :
ITimerSettings,
IListenerSettings,
IPathSettings,
IBroadcastSettings,
IDashBoardSettings
{
/* ... */
}</code>
At first glance, you may think this is over-engineered, but there's a good reason for it. There is a single object holding all the settings for the app. But each service only needs to know about <i>one part</i> of these settings. That is, it's convenient on the implementation side to have a single object handling all settings, but each service should only be <i>coupled</i> to the settings that it uses.
We don't want to increase coupling in the services just because of how the <i>current</i> implementation works. Therefore, we register the single implementation for all of service-settings interfaces in the IOC container, and each service uses its own settings interface.
<h>Getting the ball rolling</h>
For this section, I'm going to be referencing from two older articles I wrote about a framework I used to work on. It's fun that nothing has really changed in the last decade.
<ul>
<a href="{app}/view_article.php?id=3137">Encodo’s configuration library for Quino: part III</a> (2015)
<a href="{app}/view_article.php?id=3165">API Design: Running an Application (Part I)</a> (2015)
<a href="{app}/view_article.php?id=3166">API Design: To Generic or not Generic? (Part II)</a> (2015)
<a href="{app}/view_article.php?id=3175">Quino 2: Starting up an application, in detail</a> (2015)
<a href="{app}/view_article.php?id=3222">Mini-applications and utilities with Quino</a> (2016)
</ul>
So we can see how to construct an application out of components. We see how to stitch them together with very simple rules. We see how we can test those components.
But...now we want to <i>run</i> the application. We want it to do the thing that it does. Do we just create a <i>component root</i> and call ... um ... <c>Run()</c> on it?
<code>
IServiceCollection services = AppTools.CreateServiceCollection();
IServiceProvider provider = services.CreateServiceProvider();
IApplication application = provider.GetRequiredService<iapplication>();
application.Run();
</code>
Are we cool?
Kind of. Like, that works just fine. The articles referenced above provide a lot more background on providing exception-handling, standard logging, command-line support, etc. But the code above is what we're shooting for, for <i>real</i> applications. What's a real application?
Let's flesh this out a bit.
<ul>
How many applications do we have?
What kind of application are we running?
What even is an application?
</ul>
Any solution is going to have at least two applications: Whatever the <i>real</i> application is, and one or more test runners. An application is any way of executing part or all of your program logic.
<h level="3">"Real" applications have event loops</h>
A real application might be a console, a GUI, or a server application. These applications have one thing in common: they contain one or more <i>event loops</i> that <i>react</i> to external input.
Most applications have an event loop.
<ul>
A <b>console application</b> that "watches" for changes in the file system, updating other files base on that. It exits when the user issues a special command like <kbd>Ctrl</kbd> + <kbd>C</kbd>.
A <b>web server</b> that "listens" on specific ports, returning responses to requests. It exits when the user issues a special command like <kbd>Ctrl</kbd> + <kbd>C</kbd> or when the system terminates the service.
A <b>GUI</b> that "responds" to user input---mouse and keyboard events---and runs until the user issues a special command to exit.
</ul>
<h level="3">Run-once applications</h>
Some applications don't have an event loop They run the parameters through their flowcharts one time and then exit automatically.
<ul>
A console application or script that processes a single set of command-line parameters, like processing a file and producing a report.
A test runner that executes part of the logic in your real application.
</ul>
<h level="3">Application "actions"</h>
The example above doesn't show any detail about what the <c>IApplication</c> <i>does</i> when it runs.
As the referenced articles show in more detail, "getting the whole ball rolling" in a nontrivial application always involves several "actions" to execute during "startup". That is, the application is not just a service collection, but also a list of startup actions, a list of shutdown actions. Each action is created by the IOC container, so it can have all of the services it needs injected into it.
The basic loop in <i>Application.Run()</i> is something like this:
<code>foreach (var action in _startupActions)
{
action.Execute();
}</code>
The actual implementation ended up being more complicated than this, as noted above, to accommodate general error-handling, async startup actions, debugging comfort, and to support re-running the application, e.g., when it showed command-line help, or when it needed to run a schema-migration for a database. See the linked article for more information.
<h>I heard you like IOC containers...</h>
Another common problem with IOC containers is: how can you dynamically configure the IOC? What if you want to use a <c>AmazonS3Provider</c> by default, but allow a command-line parameter or configuration file to enable an <c>FTPProvider</c> instead?
DI and IOC are awesome, so we want to use them everywhere, right?
But we can't, can we? As soon as you get a service provider, you can no longer modify it. As soon as we request the <c>IApplication</c> service above, it's all over for modifying service registrations.
I've wrestled with this a lot in the past. The most relevant article is linked above: <a href="{app}/view_article.php?id=3175">Quino 2: Starting up an application, in detail</a>.
Basically, the answer is to <i>use two IOCs</i>.
<dl dt_class="field">
Bootstrap IOC
The first IOC is much smaller and contains registrations for services needed to configure the <i>Main IOC</i> (e.g. configuration-loader, command-line-reader, fs-location-resolver, etc.). These registrations are necessarily a small core of services that cannot be changed by configuration (files, command-line parameters, database values, etc.). That keeps things simple.
Main IOC
Includes all registrations from the <i>Bootstrap IOC</i>, plus overrides that came out of the configuration, plus anything else needed for the main app.
</dl>
As noted above, an appilcation's startup and shutdown are lists of actions (discussed in <a href="{app}view_article.php?id=3137">Encodo’s configuration library for Quino: part III</a>).
Specifically, there are actions to execute during,
<ul>
the bootstrap phase,
the main phase,
and shutdown.
</ul>
So, the application startup kind of looks like this:
<ul>
Configure services and actions for the Bootstrap IOC and Main IOC.
<ul>
Any registration in the Bootstrap IOC is made in the main IOC as well.
Crucially, singletons in the Main IOC are <i>the same</i> as those in the Bootstrap IOC.
</ul>
Seal the Bootstrap IOC (i.e., get the service provider from the service collection).
Execute application-startup actions <i>using the Bootstrap IOC</i>.
<ul>
The first few actions will be stuff like "read command line", "read configuration", etc.
These might alter the registrations in the main IOC and might add or modify actions to execute. That's OK. It's not "sealed" yet.
Any attempt to alter a registration in the bootstrap IOC results in an error.
Modifying an action in the list before the app's current position in that list will have no effect.
At some point, the "bootstrap" actions are finished, and an action executes that "seals" the main IOC from modification.
Now we're in the "classic" app startup.
</ul>
Run the main actions.
Run the event loop or application logic (e.g, fixed handling for command-line parameters).
Run the shutdown actions.
</ul>
There's more documentation but it’s no longer available because Encodo has taken down all public documentation … and we never published the source code as open source. I'm working from memory and my existing articles. 🤷
But that's the general gist of it. There are clean solutions to anything that might come up. For example, if you need a more "heavyweight" service during the bootstrap---like a database, which you also use in the main application, but which you want to keep configurable---consider making an interface like <i>IBootstrapConfigurationDatabase</i> or something like that, which will be its own singleton and not even available in the main application phase.
<hr>
<ft>This isn't the first time I've taken a run at this topic, although I only recently remember that I'd written <a href="{app}/view_article.php?id=3487">Why use an IOC? (hint: testing)</a> in April 2019.</ft>