Dependency Injection

Dependency Injection in .NET

A deep dive into how dependency injection works in .NET

Andre Lopes
Geek Culture

--

Photo by Alejandro Escamilla on Unsplash

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:

Response body from GET /transient in Swagger UI

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:

Scoped GET endpoint demo

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.

Singleton GET endpoint demo

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.

Transient inside Singleton test

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!

--

--

Andre Lopes
Geek Culture

Full-stack developer | Casual gamer | Clean Architecture passionate