Author : Rahul
Last Modified : 27-Apr-2021
Complexity : Intermediate

Dependency Injection Explained - C#


To know about Dependency Injection(DI), we first need to know about the Dependency Inversion Principle(DIP) and Inversion of Controls(IoC). So we start our discussion about the Dependency Inversion Principle, after that, we will discuss Inversion of Controls and at last, we will discuss Dependency Injection. 


Dependency Inversion Principle(DIP):
This principle is related to dependencies between modules. It provides some guidelines which say how to write loosely coupled modules/classes. The definition of DIP is given by Robert C. Martin is as follows:

 

  1. High-level modules should not depend on low-level modules. Both should depend on abstractions.
  2. Abstractions should not depend on details. Details should depend on abstractions.

 

To understand this principle, we will take one example.

Suppose we have a Student class. The functionality of this class is to create a student and if there is some problem in creating a student, write an exception into the file using File_Log class. Here Student class is dependent on File_Log class. Our classes look like this:

  class File_Log
  {
    public void Log(string error)
    {
      System.IO.File.WriteAllText(@"c:\Error.txt", error);
    }
  }

  class Student
  {
    private File_Log obj = new File_Log();
    public void CreateStudent()
    {
      try
      {
        // Add code to create new student
      }
      catch (Exception ex)
      {
        obj.Log(ex.ToString());
      }
    }
  }

//In the main method

Student objstudent = new Student();
objstudent.CreateStudent();

In one go, the above class design looks good. But the above design violates the Dependency Inversion Principle(DIP). If we look at both Student and File_Log classes, we see that the high-level module (Student) is dependent on the low-level module (File_Log).
To understand the problem, we will extend the requirement. Now we want to send an email to this error message, So our class design will look like this:

  class File_Log
  {
    public void Log(string error)
    {
      System.IO.File.WriteAllText(@"c:\Error.txt", error);
    }
  }

  class Email_Log
  {
    public void Log(string error)
    {
      //Send error on email
    }
  }

  class Student
  {
    private File_Log obj = new File_Log();
    private Email_Log obj1 = new Email_Log();
    public void CreateStudent(int logType)
    {
      try
      {
        // Add code to create new student
      }
      catch (Exception ex)
      {
        if (logType == 1)
        {
          obj.Log(ex.ToString());
        }
        else
        {
          obj1.Log(ex.ToString());
        }
      }
    }
  }

//In the main method

Student objstudent = new Student();

//If we want to log the message in txt file
objstudent.CreateStudent(1);

//If we want to log the message in email
objstudent.CreateStudent(2);

Here we see that for every new log type, our student class is changed.


Inversion of Controls(IoC):
The Dependency Inversion Principle is a principle, it only says, how two modules/classes should depend on each other but it does not say which mechanism we need to follow to achieve this principle. Inversion of Controls(IoC) provides us with the actual mechanism through which we can implement the Dependency Inversion Principle and our high-level module/class will depend on abstraction instead of low-level module/class.

 

To resolve it we have created a common interface for logging and implement that interface accordingly.

  interface ILog
  {
    void Log(string error);
  }

  class File_Log : ILog
  {
    public void Log(string error)
    {
      System.IO.File.WriteAllText(@"c:\Error.txt", error);
    }
  }

  class Email_Log : ILog
  {
    public void Log(string error)
    {
      //Send error on email
    }
  }

  class Student
  {
    private ILog obj = null;
    public void CreateStudent(int logType)
    {
      try
      {
        // Add code to create new student
      }
      catch (Exception ex)
      {
        if (logType == 1)
        {
          obj = new File_Log();
        }
        else
        {
          obj = new Email_Log();
        }
        obj.Log(ex.ToString());
      }
    }
  }

//In the main method

Student objstudent = new Student();

//If we want to log the message in txt file
objstudent.CreateStudent(1);

//If we want to log the message in email
objstudent.CreateStudent(2);

The above code looks ok at this time but there is one problem with this code. This code violates the Single Responsibility Principle. Here our student class is doing two things, one is creating a student and the other is creating a logging object.
So our Student class and Logging class is not completely decoupled. Now to make these classes completely decoupled, Dependency Injection(DI) comes into the picture. So Dependency Injection(DI) is mainly used to decouple the classes/modules.


Dependency Injection(DI):
To understand dependency injection, we understand injection. In the real world, "To manually enter something into your body is called injection." 
Similarly, To remove dependencies between two modules/objects, when we inject some module/object from outside is called dependency injection.
So Dependency Injection is used to remove tight coupling between the classes/modules and develop loosely coupled classes/modules.

 

There is three-way to implement dependency injection.

  1. Constructor Injection
  2. Method Injection
  3. Property Injection

Constructor Injection

Understanding:
In this approach, we pass the object of the low-level module/class in the constructor of the high-level module/class. So we need a constructor in the high-level module/class which will take the object of the low-level module/class as a parameter and assign this low-level module/class object into the interface handle of the high-level module/class.

Implementation:
Now our classes will look like this:

 

  interface ILog
  {
    void Log(string error);
  }

  class File_Log : ILog
  {
    public void Log(string error)
    {
      System.IO.File.WriteAllText(@"c:\Error.txt", error);
    }
  }

  class Email_Log : ILog
  {
    public void Log(string error)
    {
      //Send error on email
    }
  }

  class Student
  {
    private ILog obj = null;

    public Student(ILog ilog)
    {
      obj = ilog;
    }

    public void CreateStudent()
    {
      try
      {
        // Add code to create new student
      }
      catch (Exception ex)
      {
        obj.Log(ex.ToString());
      }
    }
  }

//In the main method

Student objstudent = null;
File_Log file_log = new File_Log();
Email_Log email_log = new Email_Log();

//If we want to log the message in txt file
objstudent = new Student(file_log);

//If we want to log the message in email
objstudent = new Student(email_log);

objstudent.CreateStudent();

Disadvantage:
The main disadvantage of this approach is that a high-level module/class will use a low-level module/class object for his entire lifetime. So if we confirm that the high-level module/class object will use the same low-level module/class object for his entire lifetime, this approach is good.


Method Injection

Understanding:
In this approach, we pass the object of the low-level module/class in the method of the high-level module/class. So if we want to call different low-level modules/classes for each high-level module/class method, this is a good approach. So we need a method in the high-level module/class that will also take the object of the low-level module/class as a parameter and assign this low-level module/class object into the interface handle of the high-level module/class.

Implementation:

 

Now our classes will look like this:

 

  interface ILog
  {
    void Log(string error);
  }

  class File_Log : ILog
  {
    public void Log(string error)
    {
      System.IO.File.WriteAllText(@"c:\Error.txt", error);
    }
  }

  class Email_Log : ILog
  {
    public void Log(string error)
    {
      //Send error on email
    }
  }

  class Student
  {
    private ILog obj = null;

    public void CreateStudent(ILog ilog)
    {
      try
      {
    obj = ilog;
        // Add code to create new student
      }
      catch (Exception ex)
      {
        obj.Log(ex.ToString());
      }
    }
  }

//In the main method

Student objstudent = new Student();
File_Log file_log = new File_Log();
Email_Log email_log = new Email_Log();

//If we want to log the message in txt file
objstudent.CreateStudent(file_log);

//If we want to log the message in email
objstudent.CreateStudent(email_log);

Disadvantage:
The main disadvantage of this approach is that high-level module/class method invocation and low-level module/class object selection is done at the same place. So if we want that the high-level module/class method invocation and low-level module/class object selection are done at the same place, this approach is good.


Property Injection

Understanding:
This is also known as setter injection. In this approach, we pass the object of the low-level module/class in the property of the high-level module/class. So if we want to select the low-level module/class and invocation of high-level module/class method in different places, this is a good approach. So we need a property in the high-level module/class which will accept an object of low-level module/class as a setter property/method and assign this low-level module/class object into the interface handle of the high-level module/class.

Implementation:
Now our classes will look like this:

  interface ILog
  {
    void Log(string error);
  }

  class File_Log : ILog
  {
    public void Log(string error)
    {
      System.IO.File.WriteAllText(@"c:\Error.txt", error);
    }
  }

  class Email_Log : ILog
  {
    public void Log(string error)
    {
      //Send error on email
    }
  }

  class Student
  {
    private ILog obj = null;

    public ILog Log
    {
      get
      {
        return obj;
      }
      set
      {
        obj = value;
      }
    }

    public void CreateStudent()
    {
      try
      {
        // Add code to create new student
      }
      catch (Exception ex)
      {
        obj.Log(ex.ToString());
      }
    }
  }

//In the main method

Student objstudent = new Student();
File_Log file_log = new File_Log();
Email_Log email_log = new Email_Log();

//If we want to log the message in txt file
objstudent.Log = file_log;

//If we want to log the message in email
objstudent.Log = email_log;

objstudent.CreateStudent();

Disadvantage:
The approach is not useful for languages that do not support properties. In that case, we need a separate method that will do the same thing.