Is it necessary to understand what dependency injection is to use .NET Core? Should you know how to use it?
If you go to the official docs for ASP.NET Core you’ll find that this topic is under the “fundamentals” area.
Is that true - is it really fundamental?
Note: This post is part of the 2018 C# Advent Calendar.
Dependency injection is baked into .NET Core. And, it’s there for a reason: it generally promotes best coding practices and offers developers tools to build maintainable, modular and testable software.
It also provides library authors with tools that can help make installation/configuration of their libraries very simple and straightforward.
This is part 3 of an ongoing series. The other editions are:
- Part 1: What I’ve Learned So Far Building Coravel (Open Source .NET Core Tooling)
- Part 2: Fluent APIs Make Developers Love Using Your .NET Libraries (Guest post on builtwithdot.net)
As you guessed, this article will go over some things I’ve learned about DI in .NET Core, along with my suggestions for what you should know. 😊
To begin, I want to explore DI for those who may not be too familiar with dependency injection. We’ll start with the basics and move toward some more advanced scenarios.
If you already know what DI is, and how to use interfaces to mock your classes and test them, etc. then you can move onto the Using Dependency Injection In .NET Core section.
Yes, this is going to be a long one. Get ready. 😎
If you aren’t familiar with DI, it pretty much just refers to passing dependencies into your objects as an external arguments.
This can be done via an object’s constructor or method.
// Argument "dep" is "injected" as a dependency.
Dependency injection then is, at a fundamental level, just passing dependencies as arguments. That’s it. That’s all.
Well… if that was really all of what DI is - I wouldn’t be writing this. 😜
Why would you want to do this? A few reasons:
- Promotes splitting logic into multiple smaller classes and/or structures
- Promotes code testability
- Promotes using abstractions that allow a more modular code structure in general
Let’s look briefly at the idea that this promotes testability (which in turn affects all the other points mentioned).
Why do we test code? To make sure our system behaves properly.
This means that you can trust your code.
With no tests, you can’t really trust your code.
I discuss this in more detail in another blog post about Refactoring Legacy Monoliths - where I discuss some refactoring techniques around this issue.
Of course, DI is more than “just passing in arguments.” Dependency injection is a mechanism where the runtime (let’s say - .NET Core) will automatically pass (inject) required dependencies into your classes.
Why would we ever need that?
Look at this simple class:
public class Car
What if, somewhere else, we needed to do this:
Car ferrari = new Car(new Engine());
Great. What if we wanted to test this
The problem is in order to test
Car you need
Engine. This is a “hard” dependency, if you will.
This means that these classes are tightly tied together. In other words, tightly coupled. Those are bad words.
We want loosely coupled classes. This makes our code more modular, generalized and easier to test (which means more trust and more flexibility).
Some common techniques when testing are to use “mocks”. A mock is just a stubbed-out class that “pretends” to be a real implementation.
We can’t mock concrete classes. But, we can mock interfaces!
Let’s change our
Car to rely on an interface instead:
public class Car
Cool! Let’s test that:
// Mock code configuration would be here.
So now we are testing the
Car class without a hard dependency on
I had mentioned that using DI allows your code to be modular. Well, it’s not really DI that does, but the technique above (relying on interfaces).
Compare these two examples:
Car ferrari = new Car(new FastEngine());
Car civic = new Car(new HondaEngine());
Since we are relying on interfaces, we have way more flexibility as to what kinds of cars we can build!
Another benefit is that you don’t need to use class inheritance.
This is something I see abused all the time. So much so that I do my best to “never” use class inheritance.
It’s hard to test, hard to understand and usually leads to building an incorrect model anyways since it’s so hard to change after-the-fact.
99% of the time there are better ways to build your code using patterns like this - which rely on abstractions rather than tightly coupled classes.
And yes - class inheritance is the most highly coupled relationship you can have in your code! (But that’s another blog post 😉)
The example above highlights why we need DI.
Dependency injection allows us to “bind” a specific type to be used globally in place of, for example, a specific interface.
At runtime we rely on the DI system to create new instances of these objects for us. All the dependencies are handled automatically.
In .NET Core, you might do something like this to tell the DI system what classes we want to use when asking for certain interfaces, etc.
// Whenever the type 'Car' is asked for we get a new instance of the 'Car' type.
Car relies on
When the DI system tries to “build” (instantiate) a new
Car it will first grab a
new HondaEngine() and then inject that into the
Whenever we need a
Car .NET Core’s DI system will automatically rig that up for us! All the dependencies will cascade.
So, In an MVC controller we might do this:
public CarController(Car car)
Alright - the car example was simple. That’s to get the basics down. Let’s look at a more realistic scenario.
Get ready. 😎
We have a use case for creating a new user in our app:
public class CreateUser
That use case needs to issue some database queries to persist new users.
In order to make this testable - and make sure that we can test our code without requiring the database as a dependency - we can use the technique already discussed:
public interface IUserRepository
And the concrete implementation that will hit the database:
public class UserRepository : IUserRepository
Using DI, we would have something like this:
Whenever we have a class that needs an instance of the
IUserRepository the DI system will automatically build a
UserRepository for us.
The same can be said for
CreateUser - a new
CreateUser will be given to us when asked (along with all of it’s dependencies already injected).
Now, in our use case we do this:
public class CreateUser
In an MVC controller, we can “ask” for the
CreateUser use case:
public class CreateUserController : Controller
The DI system will automatically:
- Try to create a new instance of
CreateUserdepends on the
IUserRepositoryinterface, the DI system will next look to see if there is a type “bound” to that interface.
- Yes - it’s the concrete
- Create a new
- Pass that into a new
CreateUseras the implementation of it’s constructor argument
Some benefits that are obvious:
- Your code is much more modular and flexible (as mentioned)
- Your controllers etc. (whatever is using DI) become way simpler and easy to read.
And the final benefit, again, we can test this without needing to hit the database.
// Some mock configuration...
This makes for:
- Fast testing (no database)
- Isolated testing (only focusing on testing the code in
Now I want to run through some of the more proper and technical terms that you should know, along with recommended pieces of knowledge around .NET Core’s DI system.
When we refer to the “DI system” we are really talking about the Service Provider.
In other frameworks or DI systems this is also called a Service Container.
This is the object that holds the configuration for all the DI stuff.
It’s also what will ultimately be “asked” to create new objects for us. And therefore, it’s what figures out what dependencies each service requires at runtime.
When we talk about binding, we just mean that type
A is mapped to type
In our example about the
Car scenario, we would say that
IEngine is bound to
When we ask for a dependency of
IEngine we are returned an instance of
Resolving refers to the process of figuring out what dependencies are required for a particular service.
Using the example above with the
CreateUser use case, when the Service Provider is asked to inject an instance of
CreateUser we would say that the provider is “resolving” that dependency.
Resolving involves figuring out the entire tree of dependencies:
CreateUserrequires an instance of
- The provider sees that
IUserRepositoryis bound to
UserRepositoryrequires an instance of
- The provider see that
ApplicationDbContextis available (and bound to the same type).
Figuring out that tree of cascading dependencies is what we call “resolving a service.”
Generally termed scopes, or otherwise called service lifetimes, this refers to whether a service is short or long living.
For example, a singleton (as the pattern is defined) is a service that will always resolve to the same instance every time.
Without understanding what scopes are you can run into some really weird errors. 😜
The .NET Core DI system has 3 different scopes:
Whenever we resolve
IAlwaysExist in an MVC controller constructor, for example, it will always be the exact same instance.
As a side note: This implies concerns around thread-safety, etc. depending on what you are doing.
Scoped is the most complicated lifetime. We’ll look at it in more detail later.
To keep it simple for now, it means that within a particular HttpRequest (in an ASP .NET Core application) the resolved instance will be the same.
Let’s say we have service
B. Both are resolved by the same controller:
public SomeController(A a, B b)
B both rely on service
C is a scoped service, and since scoped services resolve to the same instance for the same HTTP request, both
B will have the exact same instance of
However, a different
C will be instantiated for all other HTTP requests.
Transient services are always an entirely new instance when resolved.
Given this example:
public SomeController(A a, A anotherA)
Assuming that type
A was configured as a transient service, variables
anotherA would be different instances of type
Note: Given the same example, if
A was a scoped service then variables
anotherA would be the same instance. However, in the next HTTP Request, if
A was scoped then
anotherA in the next request would be different from the instances in the first request.
A was a singleton, then variables
anotherA in both HTTP requests would reference the same single instance.
There are issues that arise when using differently scoped services who are trying to depend on each other.
Just don’t do it. It doesn’t make sense 😜
public class A
A singleton, again, lives “forever”. It’s always the same instance.
Transitive services, on the other hand, are always a different instance when requested - or resolved.
So here’s an interesting question: When a singleton depends on a transitive dependency how long does the transitive dependency live?
The answer is forever. More specifically, as long as it’s parent lives.
Since the singleton lives forever so will all of it’s child objects that it references.
This isn’t necessarily bad. But it could introduce weird issues when you don’t understand what this setup implies.
Perhaps you have a transitive service - let’s call it
ListService that isn’t thread-safe.
ListService has a list of stuff and exposes methods to
Remove those items.
Now, you started using
ListService inside of a singleton as a dependency.
That singleton will be re-used everywhere. That means, on every HTTP Request. Which implies on many many different threads.
Since the singleton accesses/uses
ListService isn’t thread-safe - big problems!
Let’s assume now that
ListService is a scoped service.
If you try to inject a scoped service into a singleton what will happen?
.NET Core will blow up and tell you that you can’t do it!
Remember that scoped services live for as long as an HTTP request?
But, remember how I said it’s actually more complicated than that?…
Under the covers .NET Core’s service provider exposes a method
Note: Alternatively, you can use
IServiceScopeFactory and use the same method
CreateScope. We’ll look at this later 😉
CreateScope creates a “scope” that implements the
IDisposable interface. It would be used like this:
using(var scope = serviceProvider.CreateScope())
The service provider also exposes methods for resolving services:
The difference between them is that
GetService returns null when a service isn’t bound to the provider, and
GetRequiredService will throw an exception.
So, a scope might be used like this:
using(var scope = serviceProvider.CreateScope())
When .NET Core begins an HTTP request under the covers it’ll do something like that. It will resolve the services that your controller may need, for example, so you don’t have to worry about the low-level details.
In terms of injecting services into ASP controllers - scoped services are basically attached to the life of the HTTP request.
But, we can create our own services (which would then be a form of the Service Locator pattern - more on that later)!
So it’s not true that scoped services are only attached to an HTTP request. Other types of applications can create their own scopes within whatever lifespan or context they need.
Notice how each scope has it’s own
ServiceProvider? What’s up with that?
The DI system has multiple Service Providers. Woah 🤯
Singletons are resolved from a root service provider (which exists for the lifetime of your app). The root provider is not scoped.
Anytime you create a scope - you get a new scoped service provider! This scoped provider will still be able to resolve singleton services, but by proxy they come from the root provider as all scoped providers have access to their “parent” provider.
Here’s the rundown of what we just learned:
- Singleton services are always resolvable (from root provider or by proxy)
- Transitive service are always resolvable (from root provider or by proxy)
- Scoped services require a scope and therefore a scoped service provider that’s available
So what happens when we try to resolve a scoped service from the root provider (a non-scoped provider)?…
All that to say that scoped services require a scope to exist.
Singletons are resolved by the root provider.
Since the root provider has no scope (it’s a “global” provider in a sense) - it just doesn’t make sense to inject a scoped service into a singleton.
What about a scoped service who relies on a transitive service?
In practice it’ll work. But, for the same reasons as using a transitive service inside a singleton, it may not behave as you expect.
The transitive service that is used by the scoped service will live as long as the scoped service.
Just be sure that makes sense within your use-case.
As library authors we sometimes want to provide native-like tools. For example, with Coravel I wanted to make the library integrate seamlessly with the .NET Core DI system.
How do we do that?
As mentioned in passing, .NET Core provides a utility for creating scopes. This is useful for library authors.
Instead of grabbing an instance of
IServiceProvider, library authors probably should use
Why? Well, remember how the root service provider cannot resolve scoped services? What if your library needs to do some “magic” around scoped services? Oops!
Entity Framework Core contexts are scoped, so doing things such as performing database queries inside your library (on behalf of the user/developer) is something you may want to do.
This is something that Coravel Pro does - execute queries from the user’s EF Core context automatically under-the-covers.
As a side note, issues around capturing services in a closure to be used in a background
Task and/or resolving services from a background
Task also facilitate the need for resolving services manually (which Coravel needs to do).
In general, the service locator pattern is not a good practice. This is when we ask for a specific type from the service provider manually.
using(var scope = serviceProvider.CreateScope())
However, for cases like mentioned above, it is what we need to do - grab a scope, resolve services and do some “magic”.
This would be akin to how .NET Core prepares a DI scope and resolves services for your ASP .NET Core controllers.
It’s not bad because it’s not “user code” but “framework code” - if you will.
We looked at some reasons behind why dependency injection is a useful tool at our disposal.
It helps to promote:
- Code testability
- Code reuse through composition
- Code readability
Then we looked at how dependency injection in .NET Core is used, and some of the lower-level aspects of how it works.
In general, we found problems arise when services rely on other services who have a shorter lifetime.
- Singleton -> scoped
- Singleton -> transitive
- Scoped -> transitive
Finally we looked at how .NET Core provides library authors with some useful tools that can help integration with .NET Core’s DI system seamless.
I hoped you learned something new! As always, leave some your thoughts in the comments 👌
I also have an e-mail letter where I’ll give you tips, stories and curated links to help ambitious and passionate developers become tech leaders, along with personal updates.