If you go to the official docs for ASP.NET Core you’ll find that Dependency Injection is under the “fundamentals” area.
Is that true - is it really fundamental?
TLDR;
Dependency injection is baked into .NET Core. And, it’s there for a reason: it promotes building classes having loose coupling and gives 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.
Intro
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. 😎
What Is Dependency Injection?
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.
public MyClass(ExternalDependency dep)
{
this._dep = dep;
}
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 Should We Pass Dependencies As Arguments?
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.
What Is Dependency Injection (Revisited)
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
{
public Car(Engine engine)
{
this._engine = engine;
}
}
What if, somewhere else, we needed to do this:
Car ferrari = new Car(new Engine());
Great. What if we wanted to test this Car
class?
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).
Quick Look At Testing
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
{
public Car(IEngine engine)
{
this._engine = engine;
}
}
Cool! Let’s test that:
// Mock code configuration would be here.
// "mockEngine" is just a stubbed IEngine.
Car ferrari = new Car(mockEngine);
Assert.IsTrue(ferrari.IsFast());
So now we are testing the Car
class without a hard dependency on Engine
. 👍
A Quick Look At Modularization
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());
and
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!
Avoiding Class Inheritance
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 😉)
Using Dependency Injection In .NET Core
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.
services.AddTransient<Car,Car>();
// Whenever the type 'IEngine' is asked for we get a new instance of the concrete 'HondaEngine' type.
services.AddTransient<IEngine, HondaEngine>();
Car
relies on IEngine
.
When the DI system tries to “build” (instantiate) a new Car
it will first grab a new HondaEngine()
and then inject that into the new Car()
.
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)
{
this._car = car; // Car will already be instantiated by the DI system using the 'HondaEngine' as the engine.
}
A Real World Example
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
{
// Filled out later...
}
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
{
public Task<int> CreateUserAsync(UserModel user);
}
And the concrete implementation that will hit the database:
public class UserRepository : IUserRepository
{
private readonly ApplicationDbContext _dbContext;
public UserRepository(ApplicationDbContext dbContext) =>
this._dbContext = dbContext;
public async Task<int> CreateUserAsync(UserModel user)
{
this._dbContext.Users.Add(user);
await this._dbContext.SaveChangesAsync();
}
}
Using DI, we would have something like this:
services.AddTransient<CreateUser, CreateUser>();
services.AddTransient<IUserRepository, UserRepository>();
Whenever we have a class that needs an instance of the IUserRepository
the DI system will automatically build a new
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
{
private readonly IUserRepository _repo;
public CreateUser(IUserRepository repo) =>
this._repo = repo;
public async Task InvokeAsync(UserModel user)
{
await this._repo.CreateUserAsync(user);
}
}
In an MVC controller, we can “ask” for the CreateUser
use case:
public class CreateUserController : Controller
{
private readonly CreateUser _createUser;
public CreateUserController(CreateUser createUser) =>
this._createUser = createUser;
[HttpPost]
public async Task<ActionResult> Create(UserModel userModel)
{
await this._createUser.InvokeAsync(userModel);
return Ok();
}
}
The DI system will automatically:
- Try to create a new instance of
CreateUser
. - Since
CreateUser
depends on theIUserRepository
interface, the DI system will next look to see if there is a type “bound” to that interface. - Yes - it’s the concrete
UserRepository
. - Create a new
UserRepository
. - Pass that into a new
CreateUser
as the implementation of it’s constructor argumentIUserRepository
.
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.
Real World Testing
And the final benefit, again, we can test this without needing to hit the database.
// Some mock configuration...
var createUser = new CreateUser(mockUserRepositoryThatReturnsMockData);
int createdUserId = await createUser.InvokeAsync(dummyUserModel);
Assert.IsTrue(createdUserId == expectedCreatedUserId);
This makes for:
- Fast testing (no database)
- Isolated testing (only focusing on testing the code in
CreateUser
)
What You Should Know About Using .NET Core Dependency Injection
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.
Service Provider
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.
Binding
When we talk about binding, we just mean that type A
is mapped to type B
.
In our example about the Car
scenario, we would say that IEngine
is bound to HondaEngine
.
When we ask for a dependency of IEngine
we are returned an instance of HondaEngine
.
Resolving
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:
CreateUser
requires an instance ofIUserRepository
- The provider sees that
IUserRepository
is bound toUserRepository
UserRepository
requires an instance ofApplicationDbContext
- The provider see that
ApplicationDbContext
is available (and bound to the same type).
Figuring out that tree of cascading dependencies is what we call “resolving a service.”
Scopes
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:
Singleton
services.AddSingleton<IAlwaysExist, IAlwaysExist>();
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
services.AddScoped<IAmSharedPerRequests, IAmSharedPerRequests>();
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 A
and B
. Both are resolved by the same controller:
public SomeController(A a, B b)
{
this._a = a;
this._b = b;
}
Now imagine A
and B
both rely on service C
.
If C
is a scoped service, and since scoped services resolve to the same instance for the same HTTP request, both A
and B
will have the exact same instance of C
injected.
However, a different C
will be instantiated for all other HTTP requests.
Transient
services.AddTransient<IAmAlwaysADifferentInstance, IAmAlwaysADifferentInstance>();
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 a
and anotherA
would be different instances of type A
.
Note: Given the same example, if A
was a scoped service then variables a
and anotherA
would be the same instance. However, in the next HTTP Request, if A
was scoped then a
and anotherA
in the next request would be different from the instances in the first request.
If A
was a singleton, then variables a
and anotherA
in both HTTP requests would reference the same single instance.
Scope Issues
There are issues that arise when using differently scoped services who are trying to depend on each other.
Circular Dependencies
Just don’t do it. It doesn’t make sense 😜
public class A
{
public A(B b) { }
}
public class B
{
public B(A a){ }
}
Singletons + Transitive Services
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.
Thread-Safety
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 Add
and 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
, and ListService
isn’t thread-safe - big problems!
Be careful.
Singletons + Scoped Services
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?…
How Scoped Services Really Work
Under the covers .NET Core’s service provider exposes a method CreateScope
.
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())
{
// Do stuff...
}
The service provider also exposes methods for resolving services: GetService
and GetRequiredService
.
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())
{
var provider = scope.ServiceProvider;
var resolvedService = provider.GetRequiredService(someType);
// Use resolvedService...
}
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.
Multiple Service Providers
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)?…
Boom 🔥
Back To Our Topic
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.
Scoped + Transitive Services
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.
Dependency Injection For Library Authors
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?
IServiceScopeFactory
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 IServiceScopeFactory
.
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!
Coravel, for example, needs to resolve certain types from the service provider in certain situations (like instantiating invocable classes).
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).
David Fowler has written briefly about this here if interested.
Service Locator Pattern
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())
{
var provider = scope.ServiceProvider;
var resolvedService = provider.GetRequiredService(someType);
// Use resolvedService...
}
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.
Conclusion
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 👌
New Site!
I’ll be writing new content over at my new site/blog. Check it out!
Keep In Touch
Don’t forget to connect with me on twitter or LinkedIn!