Dependency Injection
Dependency Injection in .NET
A deep dive into how dependency injection works in .NET
Hi humans!
Dependency injection is a great mechanism to manage your services construction when needed. It removes the need for you manually having to create an instance with new Service(...)
and also it removes from you the responsibility of having to dispose of it and all its inner dependencies when you don’t need it anymore.
Whenever you need a new service, you just need to declare it in the class constructor, and the Dependency Injection container will take charge of creating a new instance of that class for you.
For example:
And for that to be possible, you just need to register your service with the desired lifecycle in your Program.cs
.
builder.Services.AddTransient<InjectedService>();
Or, if you injected an interface, you can just register like this:
builder.Services.AddTransient<InterfaceService, InjectedService>();
There are three scopes you can use for your dependency injection:
- Transient
- Scoped
- Singleton
Let’s dive deeper into all of them.
Transient
An instance with this scope will be constructed every time it is requested. This means that every service that has this type injected will get a different instance.
This is probably the lifecycle you’ll mostly use for services that only execute logic and don’t need to live too long.
Example
For this and the following examples, we will use a default minimal API in .NET 6. You can create it using:
dotnet new webapi -minimal
First, to prove that all services will have the same code, we will create a BaseService.cs
with just a simple random number generation in the constructor and one property to return it.
For the transient example, we will create an interface ITransientService.cs
and a class TransientService.cs
which implements the BaseService
class.
And now in the Program.cs
you can register it as Transient
with:
builder.Services.AddTransient<TransientService>();
And last, our endpoint for testing it will just inject two instances of the same service, get the random number, and then return it with another property for checking if they are equal:
Now, if you run your application and call this endpoint, you should get something like this:
Note how both first and second values are different and the isEqual
property is false
. This proves that every time an instance of TransientService
is requested, a new instance is built.
Scoped
This scope will generate an instance of this type per request. For web applications, it means that every time this type is injected, it will get the same instance during a client request.
Example
For this example, we will build an equal service as the TransientService
but called ScopedService
.
The difference will be how we inject it, which is as Scoped:
builder.Services.AddScoped<IScopedService, ScopedService>();
And to demonstrate it, we will need two endpoints to prove that an instance is the same as the current request.
If you call this endpoint, you’ll get something like:
Showing that both services are the same. But if you run it again you will get a different value:
Note how the values in the response are always different for each request.
Singleton
This scope is the same as the pattern it gets its name. It will have the same instance injected during the whole application lifetime. You’ll only get a different instance if you restart your application.
Something to keep in mind is to use Singleton the minimum as possible to avoid issues like memory leaks. Because a singleton leaves until the application restarts, you need to manage every instance inside of it manually. You need to manually dispose of connections, for example.
Example
For this example, we will have another service called SingletonService
.
Then register it with:
builder.Services.AddSingleton<ISingletonService, SingletonService>();
And for the GET
endpoint:
When you call it, you get the same values until your restart your application.
Gotchas
You need to watch out for never inject an instance with a shorter lifecycle into an instance with a longer lifecycle.
If you do that, the instance with the shorter lifecycle will automatically assume the lifecycle of the parent instance.
For example, if you inject a Transient in a Singleton, this transient instance will live until the application restarts. That is because dependency injection happens when the instance is constructed and is only disposed of once it reaches its lifecycle end, in this case, once the Singleton is disposed of.
Example
Now, to test it, we can create a class SingletonWithTransientService
and inject the TransientService
in the constructor.
Then we inject it as Singleton
in the Program.cs
with:
builder.Services.AddSingleton<ISingletonWithTransientService, SingletonWithTransientService>();
And for the endpoint:
Now, you can call this endpoint multiple times and you will get the same value.
Note that the value is the same, regardless of the number of times you call this endpoint.
This proves that the Transient
service inside the Singleton
assumed the lifecycle of that Singleton
.
Main Example
Now to demonstrate all these services at once, you can just create an endpoint like this one:
And then when you call it, you should get:
- Different values for the transient services in a request and every request.
- Same values for the scoped services in a request but new values in different requests.
- Same value for the single service for every request.
Note that the transient services are always different for every instance. The scoped values are the same for both services but change when you make a new request. And both singletons keep the same value for every request.
Multiple dependency injection
For last, there might be a case where you’d want to register multiple implementations for the same type, for example:
services.AddTransient<IService, MyService>();
services.AddTransient<IService, OtherService>();
Let’s first create an interface IService
:
It has only one method to get a name.
Now let’s create two implementations of this service, MyService.cs
and AnotherService.cs
:
To be able to work with these services, you just need to change how you inject IService
in your constructor. You should inject it with a list of IService
:
See that we have a list of IService
inject, and then we just iterate through calling the GetName
method.
The result should be:
Now, if you want to get the specific type, you just need to iterate and find the type using the GetType()
method in addition with typeof(Type)
.
Conclusion
Dependency injection is a powerful tool in .NET to simplify your code and its maintenance.
It also makes it safer because the DI container will automatically manage its lifecycle and is responsible for creating and disposing of every inner dependency your service might have.
We dove deeper into the different service lifecycles available for dependency injection. We also saw how you should be careful not to inject dependencies with a shorter lifecycle into a dependency with a longer lifecycle, as the short-lived dependency will assume the lifecycle of the longer-lived dependency.
We also could see how to work with multiple injections of the same type, and how it is fairly simple to handle it.
The code for the demos can be found here.
Hope you all enjoyed it.
Happy coding!