Introduction
I used to see the word "DI Container" everywhere back when I was a beginner in C#. I am finally getting used to using dependency injection and it is a good to time to revisit DI Containers.
Dependency Injection
Dependency injection is a very important technique to achive loose-coupling in a software.
Instead of writing something below, we could remove tight coupling between classes.
public class Cat
{
public void CheckFood()
{
Food catfood = new Food("cat food");
Console.WriteLine("I am eating " + catfood.Name );
}
}
public class Food
{
public string Name {get;set;}
public Food(string foodName)
{
this.Name = foodName;
}
}
In this case, the class Cat depends on Food.
We make the dependency better by injecting dependency from constructor.
public class Cat
{
private Food food;
public Cat(Food food)
{
this.food = food;
}
public void CheckFood()
{
Console.WriteLine("I am eating " + food.Name );
}
}
public class Food
{
public string Name {get;set;}
public Food(string foodName)
{
this.Name = foodName;
}
}
The difference is that the food instance is not instantiated in the cat class, the food instance is injected through constructor.
Dependency Injection Container
We can simply keep using this dependency injection technique, however the instantiation of a class can be cumbersome. DI Container frameworks can help us with this work.
Simple implementation of interface through DI Container
Let's consider an IAnimal interface with dog and cat classes. They make different noises and we want to listen to them.
No DI Container
public interface IAnimal
{
void noise();
}
public class Cat : IAnimal
{
public void noise()
{
Console.WriteLine("meow");
}
}
public class Dog : IAnimal
{
public void noise()
{
Console.WriteLine("bark!");
}
}
It is very easy to instantiate them even without the container.
IAnimal cat = new Cat();
IAnimal dog = new Dog();
cat.noise();
dog.noise();
That's it!
With DI Container
container.RegisterType<IAnimal, Cat>();
container.RegisterType<IAnimal, Dog>();
IAnimal cat = container.Resolve<Cat>();
cat.noise();
IAnimal dog = container.Resolve<Dog>();
dog.noise();
We can register the relationship between IAnimal and Cat. We can do the same for the dog class as well.
Since the relationship is registered to the container, we can create the object by calling Resolve method.
Somewhat complicated case
The example above is too easy and it does not really show the usefulness of the DI container.
Without DI Container
What if we have a object that depends on 9 containers and we want to use DI to implement it.
The constructor would look like this.
ParentObj parentObj = new ParentObj(new Obj1(new Obj4(), new Obj5(new Obj8())), new Obj2(new Obj6(new Obj8())), new Obj3(new Obj7(new Obj9())));
This is fine if we only need to use this object once, but we may need to use something like this over and over.
With DI Container
With a DI container, we can make the instantiation part simple after properly wire them up.
The wiring process can be tedious, but we only need to do it once.
container.RegisterType<Obj8>();
//setup obj1
container.RegisterType<Obj4>();
container.RegisterType<Obj5>(new InjectionConstructor(container.Resolve<Obj8>()));
container.RegisterType<Obj1>(new InjectionConstructor(container.Resolve<Obj4>(), container.Resolve<Obj5>()));
//setup obj2
container.RegisterType<Obj6>(new InjectionConstructor(container.Resolve<Obj8>()));
container.RegisterType<Obj2>(new InjectionConstructor(container.Resolve<Obj6>()));
//setup obj3
container.RegisterType<Obj9>();
container.RegisterType<Obj7>(new InjectionConstructor(container.Resolve<Obj9>()));
container.RegisterType<Obj3>(new InjectionConstructor(container.Resolve<Obj7>()));
container.RegisterType<ParentObj>(new InjectionConstructor(container.Resolve<Obj1>(), container.Resolve<Obj2>(), container.Resolve<Obj3>()));
Now we can generate the parent object.
ParentObj containerParentObj = container.Resolve<ParentObj>();
I have used simple objects in this example, but in reality each object can be complicated and this could greatly reduce the pain of low readablity.
Where should I put the container
I had some questions about placing the container. I understood that the container can perform a lot of work for us, once all the classes are wired properly. Once we have the container, do we need to pass the containers around??
When we pass the container around classes then it becomes the service locater pattern and it is often seen as an anti-pattern.
The container should never leave the composition root and all the instances should come from the composition root.
Conclusion
I have looked into the basic usage of DI Containers in C#. I did not go over the details of how we can use the DI Container, such as checking instance life cycles. However, it is a good starting point to understand the DI container itself.
Top comments (1)
It's important to mention the package Unity in this article.
To add the Unity to your project you must use the follow command:
"dotnet add package Unity"
More info:
unitycontainer.org