Dependency Injection in .NET Core

Pranay
Feb 20, 2021  ·  1856 views  ·  6 min read

This post explains what Dependency Injection is with detailed code examples. It also focuses on how .NET Core provides a built-in DI container and different classes associated with it.

Here is the list of contents in brief.

  1. Dependency Injection
  2. What is Dependency Injection? (with examples)
  3. Types of Dependency Injection
  4. Dependency Injection in .NET Core (with example)
  5. Additional resources

Dependency Injection

As per Wikipedia, dependency injection is a technique in which an object receives other objects that it depends on. These other objects are called dependencies.

Though it is simple to a say, it was always confusing and tricky to understand for most of the developers. Dependency Injection is a way of decreasing coupling between different classes and make each class independent of other class. Lets see what exactly does it mean with a series examples.

Why Dependency Injection?##

Lets consider a simple project with 3 classes which can represent a 3 tier architecture or may be a simple web application.

  • UserInterface
  • Business
  • DataAccess

Example 1:

Below is a .NET core console application (Dependencies) with these classes

DI examples

using System;
namespace Dependancies
{
    class Program
    {
        static void Main(string[] args)
        {
            UserInterface userInterface = new UserInterface();
            userInterface.ReadData();
        }
    }
    public class UserInterface
    {
        public void ReadData()
        {
            Console.WriteLine("Enter your username:");
            string userName = Console.ReadLine();
            Console.WriteLine("Enter your password:");
            string password = Console.ReadLine();
            Business business = new Business();
            business.Login(userName, password);

        }
    }
    public class Business
    {
        public void Login(string userName, string password)
        {
            DataAccess dataAccess = new DataAccess();
            dataAccess.GetUserInfo(userName, password);
        }
    }
    public class DataAccess
    {
        public void GetUserInfo(string userName, string password)
        {
            //Authenticate & Get user data from Database
            Console.WriteLine("Login successful! Welcome " + userName);
            Console.ReadLine();
        }
    }
}

In the above we see the UserInterface class depends on Business class and Business class depends on DataAccess class. Say if the DataAccess class has changed its implementation from MySQL DB to SQL Server, then there would be a change in the DataAccess class. This would effect the implementation of Business and UserInterface class which is not correct.

Example 2

Here is another .net core console application(ReducedCoupling) with reduced coupling.

using System;
namespace ReducedCoupling
{
    class Program
    {
        static void Main(string[] args)
        {
            IUserInterface userInterface = new UserInterface();
            userInterface.ReadData();
        }
    }
    public interface IUserInterface
    {
        void ReadData();
    }
    public interface IBusiness
    {
        void Login(string userName, string password);
    }
    public interface IDataAccess
    {
        void GetUserInfo(string userName, string password);
    }
    public class UserInterface : IUserInterface
    {
        public void ReadData()
        {
            Console.WriteLine("Enter your username:");
            string userName = Console.ReadLine();
            Console.WriteLine("Enter your password:");
            string password = Console.ReadLine();
            IBusiness business = new Business();
            business.Login(userName, password);
        }
    }
    public class Business : IBusiness
    {
        public void Login(string userName, string password)
        {
            //IDataAccess dataAccess = new DataAccess();
            IDataAccess dataAccess = new DataAccessV2();
            dataAccess.GetUserInfo(userName, password);
        }
    }
    public class DataAccess: IDataAccess
    {
        public void GetUserInfo(string userName, string password)
        {
            //Authenticate & Get user data from Database (SQL Server DB)
            Console.WriteLine("Login successful! Welcome " + userName);
            Console.ReadLine();
        }
    }
    public class DataAccessV2 : IDataAccess
    {
        public void GetUserInfo(string userName, string password)
        {
            //Authenticate & Get user data from Database (MySql DB)
            Console.WriteLine("Login successful! Welcome " + userName);
            Console.ReadLine();
        }
    }
}

In this, the coupling between the classes has been decreased by creating interfaces for each class and declaring the object variables as interface variables. So now if there is change in implementation of DataAccess, then we can simply create a new class DataAccessV2 implementing IDataAccess. This ensures, the methods and signatures in the new class would follow the guidelines of the IDataAccess interface. So if the DataAccess is changing its implementation from MySQL DB to SQL Server, we can add another class DataAccessV2 with new implementation and update the existing business class code to use the new DataAccessV2 class.

        //IDataAccess dataAccess = new DataAccess();
        IDataAccess dataAccess = new DataAccessV2();

This has reduced the coupling to some extend but not completely. We still need to update the dependent class code based on the implementation of DataAccess.

Example 3:

Here is another .net core console application (WithDI) with DI in action.

using System;
namespace WithDI
{
    class Program
    {
        static void Main(string[] args)
        {
            IDataAccess dataAccess = new DataAccess();
            IBusiness business = new Business(dataAccess);
            IUserInterface userInterface = new UserInterface(business);
            //Dependancy Injection with Contructor injection
            userInterface.ReadData();
        }
    }
    public interface IUserInterface
    {
        void ReadData();
    }
    public interface IBusiness
    {
        void Login(string userName, string password);
    }
    public interface IDataAccess
    {
        void GetUserInfo(string userName, string password);
    }
    public class UserInterface : IUserInterface
    {
        public readonly IBusiness _business;
        public UserInterface(IBusiness business)
        {
            _business = business;
        }
        public void ReadData()
        {
            Console.WriteLine("Enter your username:");
            string userName = Console.ReadLine();
            Console.WriteLine("Enter your password:");
            string password = Console.ReadLine();
            _business.Login(userName, password);
        }
    }
    public class Business : IBusiness
    {
        private readonly IDataAccess _dataAccess;
        public Business(IDataAccess dataAccess)
        {
            _dataAccess = dataAccess;
        }
        public void Login(string userName, string password)
        {
            _dataAccess.GetUserInfo(userName, password);
        }
    }
    public class DataAccess : IDataAccess
    {
        public void GetUserInfo(string userName, string password)
        {
            //Authenticate & Get user data from Database (SQL Server DB)
            Console.WriteLine("Login successful! Welcome " + userName);
            Console.ReadLine();
        }
    }
    public class DataAccessV2 : IDataAccess
    {
        public void GetUserInfo(string userName, string password)
        {
            //Authenticate & Get user data from Database (MySql DB)
            Console.WriteLine("Login successful! Welcome " + userName);
            Console.ReadLine();
        }
    }
}

The UserInterface and Business class does not create objects of Business and DataAccess class respectively. Instead the objects are passed/injected as parameters to constructers. Business class need not to know who is implementing the DataAccess as it receives an object directly. So a change in DataAccess implementation does not require any change to Business class. Here the dependencies are injected through constructers hence called Constructor Injection.

Types of Dependency Injections

Constructor injection: the dependencies are provided through a class constructor.

Setter injection: the client exposes a setter method that the injector uses to inject the dependency.

Interface injection: the dependency provides an injector method that will inject the dependency into any client passed to it. Clients must implement an interface that exposes a setter method that accepts the dependency.

Uses of DI:

  1. Cleaner Code. Easier to understand the code.
  2. Better reusability. Low coupling allows reusability of modules
  3. Better unit testing. Concrete classes can be replaced by mocks.
  4. Low coupling. Concrete classes can be replaced

Implementation of DI in .NET core##

.NET Core provided a built in Dependency Injection Container and does not need any DI containers to be installed/configured. Here is the built in DI namespace - Microsoft.Extensions.DependencyInjection.

DI Container - IServiceCollection and IServiceProvider###

IServiceCollection is the DI container which holds the dependencies. So we need to create ServiceCollection and add our dependences to it.

IServiceProvider is our container to resolve dependencies in our application. ServiceProvider is responsible for creating objects based on our dependencies registered in ServiceCollection .

Here is how we can get the DataAccess object using ServiceCollection and ServiceProvider

//Create Service collection
var collection = new ServiceCollection();
collection.AddScoped<IDataAccess, DataAccess>();

//Build service provider
var provider = collection.BuildServiceProvider();
        
//Get DataAccess obj
IDataAccess dataAccess = provider.GetService<IDataAccess>();

Here is the same example in .NET core with DI implemented using built-in DI containers.

using System;
using Microsoft.Extensions.DependencyInjection;
namespace WithDIContainer
{
    class Program
    {
        static void Main(string[] args)
        {
            //Create Service collection
            var collection = new ServiceCollection();
            collection.AddScoped<IDataAccess, DataAccess>();
            collection.AddScoped<IBusiness, Business>();
            collection.AddScoped<IUserInterface, UserInterface>();

            //Build service provider
            var provider = collection.BuildServiceProvider();

            IDataAccess dataAccess = provider.GetService<IDataAccess>();
            IBusiness business = provider.GetService<IBusiness>();
            IUserInterface userInterface = provider.GetService<IUserInterface>();

            userInterface.ReadData();
        }
    }

    public interface IUserInterface
    {
        void ReadData();
    }
    public interface IBusiness
    {
        void Login(string userName, string password);
    }
    public interface IDataAccess
    {
        void GetUserInfo(string userName, string password);
    }

    public class UserInterface : IUserInterface
    {
        public readonly IBusiness _business;
        public UserInterface(IBusiness business)
        {
            _business = business;
        }
        public void ReadData()
        {
            Console.WriteLine("Enter your username:");
            string userName = Console.ReadLine();
            Console.WriteLine("Enter your password:");
            string password = Console.ReadLine();

            _business.Login(userName, password);
        }
    }
    public class Business : IBusiness
    {
        private readonly IDataAccess _dataAccess;
        public Business(IDataAccess dataAccess)
        {
            _dataAccess = dataAccess;
        }
        public void Login(string userName, string password)
        {
            _dataAccess.GetUserInfo(userName, password);
        }
    }
    public class DataAccess : IDataAccess
    {
        public void GetUserInfo(string userName, string password)
        {
            //Authenticate & Get user data from Database (SQL Server DB)
            Console.WriteLine("Login successful! Welcome " + userName);
            Console.ReadLine();
        }
    }

    public class DataAccessV2 : IDataAccess
    {
        public void GetUserInfo(string userName, string password)
        {
            //Authenticate & Get user data from Database (MySql DB)
            Console.WriteLine("Login successful! Welcome " + userName);
            Console.ReadLine();
        }
    }

}

Methods of injection in ServiceCollection

In the above example code, we have used AddScoped in ServiceCollection for injecting the dependencies. Below are the details of all the available methods of doing the same.

  • Singleton -> This will create a Singleton pattern object. So the object scope would be same for every object and every request.
  • AddScoped -> Scoped objects are the same within a request, but different across different requests.
  • AddTransient -> Transient objects are always different. A new instance is provided to every controller and every service

Additional resources

> If you like to learn through an online course from top professionals then here is a best online course I would strongly suggest.

Course: Dependency Injection in .NET 5 (.NET Core)

Hope this helps you.

AUTHOR

Pranay

A Software Engineer by profession, a part time blogger and an enthusiast programmer. You can find more about me here.


Post a comment




Thank you! You are now subscribed.

Sign up for our newsletter

Subscribe to receive updates on our latest posts.

Thank you! You are now subscribed.