Docs Hub
Documentation/Languages/C#/Advanced

Dependency Injection | Part 1

Start developing with C#

What is "Dependency Injection" or "DI" for short?

Dependency Injection is a abstract concept that is used in software development.
It is not special to C# or .NET, it's a common design pattern used in many programming languages.

General

  • It is a technique in which an object receives its dependencies from an external source rather than creating them itself.
  • This is typically done through a constructor, a factory method, or a service locator.
  • The main goal of Dependency Injection is to decouple the code, making it easier to test and maintain.
  • It allows for better separation of concerns, as the code that uses a dependency does not need to know how to create it or manage its lifecycle.
  • This makes it easier to swap out implementations, mock dependencies for testing, and manage the lifecycle of objects.

Then what is Dependency Inversion Principle or "DIP" within the context of Dependency Injection?

  • "DIP" states that:
    • High-level modules should not depend on low-level modules.
    • Both should depend on abstractions (for example an Interface).
    • Abstractions should not depend on details.
    • Details (an implementation) should depend on abstractions.

Now, how do we use it?

Without Dependency injection:

public class Client
{
    private DataType _dataTypeInstance;

    Client()
    {
        _dataTypeInstance = new DataType();
    }
}

What is happening here?

  1. We have a class called Client.
  2. Inside the Client class, we have a private variable called "dataTypeInstance", which is of type DataType.
  3. In the constructor of the Client class, we are creating a new instance of DataType and assigning it to the "dataTypeInstance" variable.

Note: You will probably see _dataTypeInstance or this.dataTypeInstance in the constructor body, they do the exact same thing but some people prefer to use this.dataTypeInstance to make it clear that we are referring to the class instance itself and not a local variable somewhere else in the class. It is a matter of preference but using _dataTypeInstance is modern C# and is the most common way to do it.

So... what is wrong with this?

The problem with this approach is that the Client class is tightly coupled to the Client class.
Every time we create a new instance of Client, it will also create a new instance of DataType. This is fine for small applications, but as the application grows, it can become a problem.

For example, if we want to change the implementation of DataType or if we want to use a different implementation of DataType, we would have to modify the Client class. This can lead to a lot of code duplication and make it difficult to maintain the code.

So... ? What is the solution?

The solution is to decouple the Client class from the DataType class. This can be done by using Dependency Injection.

With Dependency injection:

public class Client
{
    private DataType _dataTypeInstance;

    Client(DataType dataTypeInstance)
    {
        _dataTypeInstance = dataTypeInstance;
    }
}

What is happening here?

Here, we first create an instance (the private field) of DataType and call it "_dataTypeInstance".
In our Client constructor, we pass a type of DataType called "dataTypeInstance".
Within the constructor body, we set the Client class instance of "_dataTypeInstance" to the "dataTypeInstance", which is what is passed to the constructor when someone wants to create a new Client.

So... what is the difference?

The difference is that we are no longer creating a new instance of DataType inside the Client class.
We will not go through every variation of how an instance of DataType can be created right now.
There are multiple ways to do it, for example, the instance we are passing could be singleton or transient or scoped. The important part is that we are passing an instance of DataType to the Client class through the constructor and Dependency Injection Container is managing the lifecycle of that instance.

This means that the Client class is no longer tightly coupled to the DataType class and can work with any implementation of DataType. This makes life easier to change the implementation of DataType or to use a different implementation of DataType without having to modify the entire Client class. It will be prominent when we start using interfaces and mocking classes for unit testing.

But ok... now let's imagine this without Dependency Injection:

You can see that we have a lot of classes that are being instantiated within the Client class.
It means every time we create a new Client, we are also creating a new instance of DataType, DataTypeTwo, DataTypeThree, DataTypeFour and DataTypeFive.

public class Client
{
    private DataType _dataTypeInstance;
    private DataTypeTwo _dataTypeInstanceTwo;
    private DataTypeThree _dataTypeInstanceThree;
    private DataTypeFour _dataTypeInstanceFour;
    private DataTypeFive _dataTypeInstanceFive;

    Client()
    {
        _dataTypeInstance = new DataType();
        _dataTypeInstanceTwo = new DataTypeTwo();
        _dataTypeInstanceThree = new DataTypeThree();
        _dataTypeInstanceFour = new DataTypeFour();
        _dataTypeInstanceFive = new DataTypeFive();
    }
}

What is wrong with this?

The problem with this approach is that the Client class is tightly coupled to all of the classes that it depends on.

It will be more prominent when we start using interfaces and mocking classes for unit testing.
This can lead to a lot of code duplication and make it difficult to maintain the code.