It depends (on you)
Learning Dependency Injection is a must
You need to know DI
I’d argue that modern Dotnet development absolutely requires that you understand how dependency injection works. About five years ago I’d have said it’s something you should be using, now I’m saying that you absolutely need to use and understand it.
DI is not new to Dotnet, there have been (and continue to be) many DI implementations. You may have interacted with one or more of these existing container implementations:
Castle Windsor
Ninject
Unity (the DI lib not the game engine, confusing; I know)
AutoFac
StructureMap
Lamar
Several others
What pushes you (the developer) into needing to know how DI works, is the fact that Microsoft uses DI as a first-class construct instead of an optional thing you can do. If you’re using Dotnet Core/5/6/+, then you’ve no doubt already had to use it.
Side note, one of my favorite books to have ever read about .NET is all about DI. You should consider having a look here.
For the balance of this article, I’m going to assume you are somewhat new to DI and I hope to easy your mind as we progress.
Inversion of Control
We need to start slow and the first concept will be to ensure code you write is compatible with DI. Let’s first look at some incompatible code with DI. In this first example we’ll “new” up a fictional object called Emailer. Emailer does nothing more than emails someone, somehow.
public class MyClass
{
public void DoSomething()
{
var emailer = new Emailer();
emailer.SendMail();
}
} If we look at Emailer, it’s trapped. There’s no way I can construct MyClass with a different Emailer. The most common reason I’d wanna swap out Emailer is so that when I write unit tests, I’m able to swap in an implementation that doesn’t send real emails. Then in production, I swap in the version of Emailer that does send emails.
The goal of inversion of control (IoC) is to write MyClass in a way that all dependencies are swappable. DI requires you to write classes with the IoC principle.
In general a dependency is an object that has logic in it. A model is typically not considered a dependency, rather you’d pass in the model as an argument to a method.
Let’s make MyClass compatible with DI by writing it like so:
public class MyClass
{
private readonly Emailer _emailer;
public MyClass(Emailer emailer)
{
_emailer = emailer;
}
public void DoSomething()
{
_emailer.SendMail();
}
} The above code is idiomatic DI code in Dotnet. Let’s break it down a bit:
We’re using the constructor to pass in any dependencies
We set a private property to the dependency
We call the dependency
At this point, MyClass doesn’t know which instance of Emailer will be passed into it — which is the point of IoC. The control of what dependency a class gets is delegated to something else.
While the above code is DI compatible, it’s not very test friendly. That’s because we’re depending on a concrete class rather than an abstraction. So in order to make a class fully compatible with DI and testing, depend on an abstraction instead. So with that in mind, let’s see one more iteration:
public class MyClass
{
private readonly IEmailer _emailer;
public MyClass(IEmailer emailer)
{
_emailer = emailer;
}
public void DoSomething()
{
_emailer.SendMail();
}
} By using an interface, this makes our code much friendlier to testing frameworks that can mock an implementation. It also leaves the door open to being able to drop in a particular concrete implementation as we see fit.
Wait, what about…
You may be thinking, ok great but how does MyClass get an IEmailer then? Welp, let’s do it the old-fashioned way, we simply instantiate MyClass with an emailer like so:
var myClass = new MyClass(new Emailer());If we had multiple implementations of IEmailer, we could also easily just choose a different one:
var myClass = new MyClass(new AwsEmailer());Believe it or not, this is dependency injection. This flavor of DI is called “Poor Man’s” DI. You could keep doing this with additional dependencies like so:
public class MyClass
{
private readonly IEmailer _emailer;
private readonly IQueueSender _queueSender;
public MyClass(IEmailer emailer, IQueueSender queueSender)
{
_emailer = emailer;
_queueSender = queueSender;
}
public void DoSomething()
{
_emailer.SendMail();
_queueSender.Publish();
}
}
...
var myClass = new MyClass(new Emailer(), new QueueSender());At some point your dependency list will get rather long and not to mention might need their own constructor args and/or need the same dependencies:
var myClass = new MyClass(
new Emailer(connectionString, true, 15),
new QueueSender(foo, bar, new SomethingElse())
);
//finally
myClass.DoSomething();In order to preserve the principle of IoC, any medium-sized-to-large app becomes unmanageable quick. You can attempt to manage those instances on your own, but it’s much better to have something else do it.
I prefer constructor injection but another valid way to inject dependencies is via parameter injection.
The Container
The container is the centerpiece of an automated DI setup (not to be confused with a Docker container). Its job is to provide an instance of a class when asked for one. The rat’s nest above becomes much simpler to instantiate a MyClass. For example (assuming we’ve done our registrations) all we have to do is get our class is the following:
var myClass = serviceProvider.GetService<MyService>();Getting an instance of MyClass in this way is known as resolving.
Registration
But in order to get a MyClass from the container, it has to know how to create each of the dependencies of MyClass first. This is done via registration:
serviceCollection.Add<IEmailer>(x => {
return new AwsEmailer("smtp stuff", true, 15);
});
serviceCollection.Add<IQueueSender>(x => {
return new QueueSender("something queue-y");
});Since we’ve told our container how to make each dependency, it will now be able to create a MyClass. Registration can be less hands-on by using a registration convention. A common registration convention is to scan assemblies on app-start and to auto-register interfaces that have exactly one implementation. For interfaces that have multiple implementations, we will have to give the container some help through our registrations to make the correct choices.
If anything depends on
MyClass, the container knows how to construct it. All possible dependency needs for any class constructable via the container, can be thought of as a map called an object graph.
The container doesn’t need much help when a dependency has a parameterless constructor or the args are all known interfaces. It definitely will need help if a primitive value is in the constructor (e.g. a string, int, bool, etc).
If a class has multiple constructors, which one is used? While it varies depending on if you’re using Unity or MS DI, typically the constructor with the most args is used.
A common pattern to use is the Factory Pattern during DI. Meaning we can define a method that takes a bit of input and returns the correct implementation.
Now that we know that we have to register our dependencies so that the container can construct other dependencies, we need to talk about life.
That’s life
When the container constructs an object, it needs to decide if it creates a new object or re-uses one. This decision is critical and these choices need careful consideration. There are typically three choices a container can make called lifetimes:
Transient - Each time
MyClassis constructed, it will be a new instance.Scoped - In the context of the web, each request will get one-and-only-one copy of
MyClassno matter how many objects use it. After the request is finished, it is reclaimed by the garbage collector. This is the lifetime you want for most webby things.Singleton - During the entire life of the application, only one of these will be constructed. In the context of the web, all request share the same instance. These are often useful for configs and shared classes.
If you don’t tell a container which to use, it will use the default lifetime. The default lifetime varies depending on which container you choose. Therefore when you register your classes with the container, make sure to choose your lifetime.
Child containers/Scope
When a DI container is used for a web application, it’s common to have a copy/clone of the container spawned per web request. It keeps things tidy and prevents cross-thread chaos. Since EntityFramework isn’t thread-safe, this is essential to understand. Each copy/clone of the container is often referred to as a child container or scope. When an object is resolved from a child container, if it is a “scoped” lifetime, it will only come from this container which is reserved for the current request only. If a “singleton” object is resolved from a child container, it will use the object from the root because there can only be one per app.
At the end of the web request, the child container/scope gets reclaimed. There is an important thing to consider with console applications and background services. Both of those application types handle scope a little differently which may not be obvious. In multi-threaded situations you will likely need to spawn your own scopes manually to keep threading from bringing the chaos.
Root of all things (and evil)
So where do we interact with the container? Registrations should only be done during app-start as one-off initialization code. This area is known as the composition root and is typically in the `program.cs` or `startup.cs` files.
The plumbing for resolving objects should also be in these two files. When done properly, nothing in your classes should know anything about the DI container.
If you pass in a reference to the container to one of your classes, you’re doing it wrong. If you have a line of code that looks like the following, you’re likely using the Service Locator anti-pattern:
var myService = serviceProvider.GetService<T>();You should avoid this type of code as it makes your code untestable and it leaks the DI container to the rest of your app.
Magic
If done right, DI looks like magic. If you’re skeptical of DI, consider web controllers. When have you ever had to instantiate a controller on your own (unless you’re doing unit tests :P)? The answer is never because something else is in charge of instantiating them for you per request.
DI is simply letting something else create your objects exactly how you told (the container) to do it. Most containers will tell you which objects couldn’t be created (and hopefully why). These errors are usually resolved (pun intended) by adjusting your registrations. The harder thing to debug about DI is run-time issues (particularly lifetimes).
It’s possible to mix more than one container type in a single app, but if you can go with one; go with the Microsoft one.
Happy injecting!

