Dependency Injection in .NET
Dependency Injection (DI) is one of the most common design patterns in OOP, and in .NET. I'm not entirely sure if it should be called a design pattern, but it is a common pattern, used in many projects, so I guess we could call it that. ๐
Dependencies of a classโ
In C# we can use the new
operator to create objects from classes. We call these objects instances of a class, and the process of creating them instantiation.
This is an easy way to handle our dependencies, whenever we need an instance of a class, we can just create it. Lets see how this happens when we're playing with our pet dog:
class Owner
{
public void PlayWithDog()
{
var dog = new Dog();
Console.WriteLine("Playing with Dog!");
dog.Bark();
}
}
class Dog
{
public void Bark()
{
Console.WriteLine("Woof!");
}
}
var owner = new Owner();
owner.PlayWithDog();
This is all good, but not all people have dogs, some people have cats, other people have elephants... It's a matter of personal taste... I'm not judging.
Instead of Dogs, we could say that people have pets, and the pets can talk. To model this in OOP, we can introduce an interface: IPet
.
interface IPet
{
public void Talk();
}
With this abstraction, our Dog
becomes:
class Dog : IPet
{
public void Talk()
{
Console.WriteLine("Woof!");
}
}
What happens with our owner?
Take a look at the earlier Owner
class. An issue here is that our Owner
has its pet hardcoded as a Dog
.
This is easy to solve by passing the Dog
as a parameter to the Owner
. One place for it could be in the constructor of the class.
I'll use a constructor with parameters here, but if you want to follow along, you can also use
a primary constructor.
We can also change the dog into an IPet
, just to make cat people happy ๐.
class Owner
{
private readonly IPet _pet;
public Owner(IPet pet)
{
_pet = pet;
}
public void PlayWithPet()
{
Console.WriteLine("Playing with Pet!");
_pet.Talk();
}
}
var owner = new Owner(new Dog());
owner.PlayWithPet();
Now, our Owner
class doesn't know about the type of pet anymore, it can work with any kind of pet... dogs, cats, elephants, snakes or spiders... anything could work!
The disadvantage of this?
Every time we want to instantiate an Owner
class, we need to pass it an IPet
too.
This wouldn't be a big deal in our small app with 2 classes, but imagine that our project grows over time...
our Dog
would need toys, food, and grooming too...
var owner = new Owner(new Dog(new DogFood(), new ChewToy(), new Groomer()));
In a real world project this would get out of hand pretty quickly...
But there are also a couple of advantages to this approach:
- The dependencies of our class are very explicit. It's very easy to see what a class depends on by looking at its constructor.
- The classes are less coupled. Our
Owner
class can work with any kind of pet. (Don't forget the cat people! ๐)
Are these advantages worth it? Is that ugly code above that instantiates many classes on one line... is that what we're going to do everywhere from now on?
Dependency Injectionโ
What if there was a way that we could keep the advantages of the constructor approach, but remove the disadvantages? What if something could do all those instantiations for us automatically?
Well... this is exactly what Dependency Injection can do!
The idea behind dependency injection is that it takes control of instantiating our classes. The only way it can instantiate our classes is, of course, calling the constructors we defined, so it also has to provide all the parameters for them! (in other words: it has to inject them).
So, to sum up: We moved the dependencies of our classes into their constructor as parameters, and we allow dependency injection to inject them into the constructor! That's how we arrive at the name: Dependency Injection.
Using constructor parameters is just one way to do DI. It is the most common one, and it's officially called constructor injection.
So, going back to our pets...
how does DI know which implementation of IPet
it can inject into our Owner
class?
IPet
could be a Dog
or a Cat
or something entirely different... how does it know which one to choose?
It turns out that it doesn't. We need to write some setup code for it. Let's see how that looks nowadays in the .NET world...
.NET DIโ
For a long time .NET had no out-of-the-box solution for dependency injection. Nowadays the one offered by Microsoft is called Microsoft.Extensions.DependencyInjection
.
I wish Microsoft had thought of a better name for it... but let's just call it .NET DI for now.
(Note that there still are lots of really good libraries for DI. Many of them have features that .NET DI doesn't, so they might be worth checking out sometime... ๐)
Lets install .NET DI in a project with the following command:
dotnet package add Microsoft.Extensions.DependencyInjection
We can also install it by using the NuGet Package Mananger UI in our IDE of choice, but the command line version is universal.
You probably won't see it used directly in a real-world project, but the .NET DI library comes with two main classes: ServiceCollection
and ServiceProvider
.
using Microsoft.Extensions.DependencyInjection;
var serviceCollection = new ServiceCollection();
var serviceProvider = serviceCollection.BuildServiceProvider();
Console.WriteLine("We have a service provider!" + serviceProvider);
Remember that setup part I told you about? In .NET DI we set up the ServiceCollection
.
Based on the setup we did, we build the ServiceProvider
which is responsible for instantiating our classes, and injecting their dependencies into them.
Adding our classesโ
Now we can add to our ServiceCollection
.
using Microsoft.Extensions.DependencyInjection;
var serviceCollection = new ServiceCollection();
serviceCollection.AddSingleton<IPet, Dog>();
serviceCollection.AddSingleton<Owner>();
var serviceProvider = serviceCollection.BuildServiceProvider();
var owner = serviceProvider.GetRequiredService<Owner>();
owner.PlayWithPet();
Playing with Pet!
Woof!
What just happened?
- We told the
ServiceCollection
about what classes and interfaces we have:AddSingleton<IPet,Dog>()
tells it that we have anIPet
implemented byDog
.AddSingleton<Owner>()
tells it that we have anOwner
class.- We also told it about the lifetimes of our classes. This is what Singleton refers to. We'll talk about the lifetimes later on in a bit more detail.
- From the
ServiceCollection
we built aServiceProvider
. - We asked the
ServiceProvider
to give us an instance of theOwner
class.- The
ServiceProvider
internally took care of injecting the constructor parameters into theOwner
class, and instantiating it. - The
ServiceProvider
will do this recursively for each class we need. We don't need to instantiate everything ourselves.
- The
Basically the ServiceProvider
is able to inject any classes that has been added to the ServiceCollection
into the constructor of any other class.
Changing implementationsโ
This also allows us to easily replace IPet
s implementation with any other one... For example... for all the cat people out there: a cat! ๐บ
class Cat : IPet
{
public void Talk()
{
Console.WriteLine("Miau!");
}
}
And that allows us to quickly transition from being a dog person, to being a cat person.
using Microsoft.Extensions.DependencyInjection;
var serviceCollection = new ServiceCollection();
serviceCollection.AddSingleton<IPet, Cat>();
serviceCollection.AddSingleton<Owner>();
var serviceProvider = serviceCollection.BuildServiceProvider();
var owner = serviceProvider.GetRequiredService<Owner>();
owner.PlayWithPet();
Playing with Pet!
Miau!
Lifetimesโ
There is one problem though. Now that DI handles instantiation with new
for us, we can't control the lifetime of our objects.
That lifetime can be pretty important especially when using resources that need to be closed after a while.
In .NET land, when something needs to be closed, or uses resources that need to be closed, it usually implements an interface named IDisposable
and a Dispose
method.
In this Dispose
method the class is supposed to clean up all resources it used. As an example, we can change our classes to implement this Dispose
method.
A simple thing we can do is just log some messages, and count the number of instances of each class. This way we know which instance logs which message.
class Owner : IDisposable
{
private static int instanceCounter = 0;
private readonly int _instanceNumber = 0;
private readonly IPet _pet;
public Owner(IPet pet)
{
_instanceNumber = instanceCounter++;
Console.WriteLine($"[{nameof(Owner)} {_instanceNumber}]: In constructor");
_pet = pet;
}
public void PlayWithPet()
{
Console.WriteLine($"[{nameof(Owner)} {_instanceNumber}]: Playing with Pet!");
_pet.Talk();
}
public void Dispose()
{
Console.WriteLine($"[{nameof(Owner)} {_instanceNumber}]: In {nameof(Dispose)}");
}
}
class Dog : IPet, IDisposable
{
private static int instanceCounter = 0;
private readonly int _instanceNumber = 0;
public Dog()
{
_instanceNumber = instanceCounter++;
Console.WriteLine($"[{nameof(Dog)} {_instanceNumber}]: In constructor");
}
public void Talk()
{
Console.WriteLine($"[{nameof(Dog)} {_instanceNumber}]: Woof!");
}
public void Dispose()
{
Console.WriteLine($"[{nameof(Dog)} {_instanceNumber}]: In {nameof(Dispose)}");
}
}
To illustrate all lifetimes using this code, there's one more thing we need to do: Dispose our ServiceProvider
too in our Program.cs
:
// The using statement can be used together with IDisposable.
// It calls Dispose automatically at the end of the current method.
using var serviceProvider = serviceCollection.BuildServiceProvider();
Now that we have the code to illustrate the lifetime of our objects, we can go back to the types of lifetimes in .NET DI.
.NET DI defines three lifetimes for our object instances.
Singleton lifetimeโ
This one is the simplest. Singleton means that there is one, and only one instance of the class throughout the lifetime of the application.
Singleton can be considered a design pattern by itself and implemented using a .GetInstance()
method...
but it's easier with DI. We can just define the lifetime of our class as Singleton and the DI library takes care of the rest!
using Microsoft.Extensions.DependencyInjection;
var serviceCollection = new ServiceCollection();
serviceCollection.AddSingleton<IPet, Dog>();
serviceCollection.AddSingleton<Owner>();
using var serviceProvider = serviceCollection.BuildServiceProvider();
var owner = serviceProvider.GetRequiredService<Owner>();
var owner2 = serviceProvider.GetRequiredService<Owner>();
Console.WriteLine("The two owners are " + (owner == owner2 ? "equal" : "not equal"));
owner.PlayWithPet();
[Dog 0]: In constructor
[Owner 0]: In constructor
The two owners are equal
[Owner 0]: Playing with Pet!
[Dog 0]: Woof!
[Owner 0]: In Dispose
[Dog 0]: In Dispose
In this case both we can see that even if we requested Owner
twice, there was only one Owner
instance and only one Dog
instance too.
Transient lifetimeโ
Transient means that each time we ask for an instance of a class, we will get a new instance.
using Microsoft.Extensions.DependencyInjection;
var serviceCollection = new ServiceCollection();
serviceCollection.AddTransient<IPet, Dog>();
serviceCollection.AddTransient<Owner>();
using var serviceProvider = serviceCollection.BuildServiceProvider();
var owner = serviceProvider.GetRequiredService<Owner>();
var owner2 = serviceProvider.GetRequiredService<Owner>();
Console.WriteLine("The two owners are " + (owner == owner2 ? "equal" : "not equal"));
owner.PlayWithPet();
[Dog 0]: In constructor
[Owner 0]: In constructor
[Dog 1]: In constructor
[Owner 1]: In constructor
The two owners are not equal
[Owner 0]: Playing with Pet!
[Dog 0]: Woof!
[Owner 1]: In Dispose
[Dog 1]: In Dispose
[Owner 0]: In Dispose
[Dog 0]: In Dispose
With AddTransient
we have two instances for each of our classes.
What happens if we mix it up a little?
Sometimes a dog can have two owners. The two owners each take care of the dog, but there is only one dog.
We can implement this by changing our Dog
to a singleton!
serviceCollection.AddSingleton<IPet, Dog>();
serviceCollection.AddTransient<Owner>();
[Dog 0]: In constructor
[Owner 0]: In constructor
[Owner 1]: In constructor
The two owners are not equal
[Owner 0]: Playing with Pet!
[Dog 0]: Woof!
[Owner 1]: In Dispose
[Owner 0]: In Dispose
[Dog 0]: In Dispose
Scoped lifetimeโ
Scoped lifetime is a bit more complex.
In our application we can define specific parts that will share scoped instances between them, we call this a scope.
We can use the CreateScope
method to create a scope.
Each class with scoped lifetime will have one instance for each scope.
This is a bit more complex to illustrate, so lets go back to our dog with two owners! ๐ถ
Jack and Jill have a dog. John and Ava also have a dog. If our dog would have singleton lifetime, they all would have the same dog, but if our dog would have transient lifetime, each of them would have a separate dog!
Scoped lifetime can help with this situation. Each owner can be part of a scope. Each scope has a dog.
using Microsoft.Extensions.DependencyInjection;
var serviceCollection = new ServiceCollection();
serviceCollection.AddScoped<IPet, Dog>();
serviceCollection.AddTransient<Owner>();
using var serviceProvider = serviceCollection.BuildServiceProvider();
Console.WriteLine("Creating scope1");
using var scope1 = serviceProvider.CreateScope();
var jack = scope1.ServiceProvider.GetRequiredService<Owner>();
var jill = scope1.ServiceProvider.GetRequiredService<Owner>();
jack.PlayWithPet();
Console.WriteLine("Creating scope2");
using var scope2 = serviceProvider.CreateScope();
var john = scope2.ServiceProvider.GetRequiredService<Owner>();
var ava = scope2.ServiceProvider.GetRequiredService<Owner>();
john.PlayWithPet();
Creating scope1
[Dog 0]: In constructor
[Owner 0]: In constructor
[Owner 1]: In constructor
[Owner 0]: Playing with Pet!
[Dog 0]: Woof!
Creating scope2
[Dog 1]: In constructor
[Owner 2]: In constructor
[Owner 3]: In constructor
[Owner 2]: Playing with Pet!
[Dog 1]: Woof!
[Owner 3]: In Dispose
[Owner 2]: In Dispose
[Dog 1]: In Dispose
[Owner 1]: In Dispose
[Owner 0]: In Dispose
[Dog 0]: In Dispose
This way the two owners in scope1
share a dog, and the other two owners in scope2
share a different dog.
One interesting aspect in this is that objects in a scope are disposed when the scope is disposed. To illustrate this, we can change our program a bit, to use shorter lifetimes.
using Microsoft.Extensions.DependencyInjection;
var serviceCollection = new ServiceCollection();
serviceCollection.AddScoped<IPet, Dog>();
serviceCollection.AddTransient<Owner>();
using var serviceProvider = serviceCollection.BuildServiceProvider();
Console.WriteLine("Creating scope1");
using (var scope1 = serviceProvider.CreateScope())
{
var jack = scope1.ServiceProvider.GetRequiredService<Owner>();
var jill = scope1.ServiceProvider.GetRequiredService<Owner>();
jack.PlayWithPet();
} // scope1 is disposed here
Console.WriteLine("Creating scope2");
using (var scope2 = serviceProvider.CreateScope())
{
var john = scope2.ServiceProvider.GetRequiredService<Owner>();
var ava = scope2.ServiceProvider.GetRequiredService<Owner>();
john.PlayWithPet();
} // scope2 is disposed here
Creating scope1
[Dog 0]: In constructor
[Owner 0]: In constructor
[Owner 1]: In constructor
[Owner 0]: Playing with Pet!
[Dog 0]: Woof!
[Owner 1]: In Dispose
[Owner 0]: In Dispose
[Dog 0]: In Dispose
Creating scope2
[Dog 1]: In constructor
[Owner 2]: In constructor
[Owner 3]: In constructor
[Owner 2]: Playing with Pet!
[Dog 1]: Woof!
[Owner 3]: In Dispose
[Owner 2]: In Dispose
[Dog 1]: In Dispose
Everything inside of the scope is disposed when the scope is disposed.
In a real application, we could create a scope for a specific operation.
Once the operation has ended we dispose the scope,
and .NET DI takes care of disposing everything that was inside the scope,
by calling Dispose
automatically on all implementations of IDisposable
.
By doing this, it can help avoiding memory or resource leaks. ๐
Scoped and Singleton working togetherโ
What about a mix of singleton and scoped lifetimes? To illustrate this, we will need to change our example a little.
In this example we only have two people, who have one dog. This special dog needs 24h attention, so they take turns in playing with the dog. Jack plays with the dog at night, while Jill plays with the dog during daytime.
using Microsoft.Extensions.DependencyInjection;
var serviceCollection = new ServiceCollection();
serviceCollection.AddSingleton<IPet, Dog>();
serviceCollection.AddScoped<Owner>();
using var serviceProvider = serviceCollection.BuildServiceProvider();
Console.WriteLine("Creating nightScope");
using (var nightScope = serviceProvider.CreateScope())
{
var jack = nightScope.ServiceProvider.GetRequiredService<Owner>();
jack.PlayWithPet();
}
Console.WriteLine("Creating dayScope");
using (var dayScope = serviceProvider.CreateScope())
{
var jill = dayScope.ServiceProvider.GetRequiredService<Owner>();
jill.PlayWithPet();
}
Creating nightScope
[Dog 0]: In constructor
[Owner 0]: In constructor
[Owner 0]: Playing with Pet!
[Dog 0]: Woof!
[Owner 0]: In Dispose
Creating dayScope
[Owner 1]: In constructor
[Owner 1]: Playing with Pet!
[Dog 0]: Woof!
[Owner 1]: In Dispose
[Dog 0]: In Dispose
At the end of nightScope
, the Dog
is not disposed.
Scopes will respect that Dog
is a singleton, and that there can be one and only one Dog in our application. In other words singletons are truly singletons and the scope doesn't change this.
What about the reverse? Using a scoped class inside a singleton?
This is not supported and it has an undefined behavior.
In practice it might work, but it's a bad idea to rely on it. It might lead to errors, or simply break next time you update .NET DI.
Instead, a singleton class can inject an IServiceProvider
, and use it to create a new scope when needed.
When to use Scopes?โ
Normally in your application code you won't really create scopes. These are usually created by the framework. For example ASP.NET Core creates a new scope for each request, so if you are looking for something like a request lifetime, use scoped. Another example could be MassTransit, a messaging framework, which creates a new scope for each message received.
One example where you might want to create a scope is when doing some sort of background operation using BackgroundService
.
If you want to do something like this, see this link.
Which lifetime to use and when?
As always, it depends. The classes can be very application specific, but here are some general guidelines:
-
You can use Singleton in the following scenarios:
- The class does not depend on anything that is scoped. Singleton classes can not depend on scoped classes.
- The class is stateless. For example utility classes can have singleton lifetimes without an issue.
- Use Singleton for anything that should be shared across the application. For example we can consider a cache,
IMemoryCache
. This can only work well, when the rest of the app can read from the cache. In other words it should be shared with the whole application, so the singleton lifetime is appropriate. - The class is thread-safe. This is a bit environment specific, but for example in ASP.NET Core multiple requests can run in parallel. When an object is a singleton, this means that multiple threads will access it at the same time.
- You can also use singleton for classes that are expensive to create as a performance optimization. When changing a class to singleton for this reason, make sure to double check the other criteria too. And remember the famous words of Donald Knuth "premature optimization is the root of all evil" ๐
-
You can use Scoped when:
- The class depends on something that is scoped. For example Entity Framework uses Scoped lifetime by default. To use a
DbContext
, your class should also have at least a Scoped lifetime. - The class is scope-specific. This sounds a bit vague, but what it means is application specific. For example in ASP.NET Core, since we have one scope per request, and we implement a class that somehow reads a user's session from the request... well that session information is specific to the request, and as such it could have a scoped lifetime.
- The class is expensive to create, but Singleton can not be used. In this case maybe a second option is to use Scoped lifetime. Another option could be refactoring the class. ๐
- The class depends on something that is scoped. For example Entity Framework uses Scoped lifetime by default. To use a
-
You can use Transient when:
- You have considered the other lifetimes first, but you are unsure. In this case use Transient. It's the safest choice in most cases, as it doesn't need thread safety or handling shared state.
- Use transient when the instance can not be shared with any other class. For example when it has instance specific state inside.
Ultimately it's up to you to choose the right lifetimes for your specific scenarios. You can experiment with different lifetimes, different combinations to find the right one.
Conclusionsโ
To sum up:
- We talked about Dependency Injection, how it's used in OOP languages, and why.
- It has some advantages:
- It makes our dependencies very easy to see, very explicit. On a project using it, we can simply look in the constructor of a class to see it's dependencies.
- It helps decoupling our classes.
- When used appropriately it can help cleaning up resources automatically.
- We also talked about .NET DI, how it works, and the instance lifetimes it knows about.
- Singleton, Transient and Scoped lifetimes.
- Creating scopes.
This was a bit longer than I expected... and was not even what I had in mind in the beggining... that one will be in the next blog post! ๐