Why do We Use Dependency Injection?

Dependency Injection History

Dependency injection is no longer a buzzword nowadays, and it comes under basic software development principles that need to be followed in all software development projects. This programming best practice has been present in the industry for more than a decade. In the early days, the .net framework did not support it natively, and hence, to have dependency injection, we used to depend on an external framework added as a nugget package.

There were many options available to impelement dependency injection, some popular frameworks are Unity, Ninject, Autofac etc., but .net core has provided a built in suuport for dependency injection and you don't need external framework now to support dependency injection in .Net core.

Dependency Injection Need


Life cycle management of objects in application

The world before the introduction of dependency injection was handling the needs manually by customer solutions like static classes and singleton patterns, etc. For e.g., you need to have a single instance of a class that maintains the logs for your application or a single instance of a repository class that manages your database operation for your application. This need was addressed by having a static class that follows a singleton design pattern, which makes sure that only a single instance for that class is maintained throughout the life cycle of that application. So, instead of maintaining such custom solutions, dependency injection provides you out of a box solution and framework that can take care of managing the lifecycle of objects in the application.

For example, for a simple weather report application I created for demo purposes, here, I registered the lifecycle "WeatherService" as scoped.

  • Transient: An object of the class is created every time it is requested
  • Scoped: Created once per scope. Most of the time, scope refers to a web request. But this can also be used for any unit of work, such as the execution of an Azure Function.
  • Singleton: Created only for the first request. If a particular instance is specified at registration time, this instance will be provided to all consumers of the registration type.

For e.g. In my demo application Program.cs bootstrap class.

Program. cs

using Dependency_Injection_Demo;

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.

builder.Services.AddMvc();
builder.Services.AddScoped<IWeatherReportProvider,WeatherService>();

//other options for lifecycles
//builder.Services.AddSingleton<IWeatherReportProvider, WeatherService>();
//builder.Services.AddTransient<IWeatherReportProvider, WeatherService>();

var app = builder.Build();

// Configure the HTTP request pipeline.

app.MapGet("/", () => "Hello World!");
app.UseRouting();

app.UseEndpoints(endpoints =>
{
    endpoints.MapDefaultControllerRoute();
    endpoints.MapAreaControllerRoute("default", "default", "{controller=Home}/{action=Index}/{id?}");
});

app.Run();

Dependency Inversion Principle

Robert C. Martin’s definition of the Dependency Inversion Principle consists of two parts.

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

What the above principle states indirectly is that we should use abstraction i.e. interfaces, and concrete classes should implement those interfaces. When one concrete class needs another concrete class, they should communicate with each other using the interface in between, so you can consider this interface as a contract between two parties or two classes interacting with each other.

Handling of object instantiation

This is also one of the crucial aspects of software applications; where does it handle the dependency management and object creation needed for the application? As per the best practice suggested, it should not be all over the place, and it needs to be handled in a central place. 

The Composition Root pattern states that the entire dependency graph should be composed in a single location “as close as possible to the application’s entry point”. This could get pretty messy without the assistance of a framework. DI frameworks provide a mechanism, often referred to as an Inversion of Control (IoC) Container, for offloading the instantiation, injection, and lifetime management of dependencies to the framework. You invert the control of component instantiation from the consumers to the container, hence “Inversion of Control”.

For e.g. here in my demo application, "WeatherReporterController" and "WeatherService" are concrete implementations, and both depend on abstraction i.e. interface "IWeatherReportProvider".

Here dependency fo "WeatherService" is getting registered at very begining of the application, when it is loading and all such other dependencies needs to be registered here, refer above Program.cs bootstrap class example, also "WeatherReporterController" class does not have to worry about instantiation of "WeatherService" class, its managed by IoC contrainer, which is inbuild .net core IoC container here.

using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;

namespace Dependency_Injection_Demo
{

    [Route("api/[controller]")]
    [ApiController]
    public class WeatherReporterController : ControllerBase
    {

        IWeatherReportProvider _provider;

        public WeatherReporterController(IWeatherReportProvider weatherReportProvider) 
        {
            _provider = weatherReportProvider;
        }

        [HttpGet]
        public String GetWeartherReport(int temperature)
        {
            return _provider.GetWeaatherInformation(temperature);
        }
    }
}

namespace Dependency_Injection_Demo
{
    public class WeatherService : IWeatherReportProvider
    {
          public WeatherService() { }

        public string GetWeaatherInformation(int temperature)
        {
            if (temperature <= 15)
                return "Winter";

            else if (temperature > 15 && temperature <= 20)
                return "Spring";

            else
                return "Summer";
        }
    }

    public interface IWeatherReportProvider
    {
        string GetWeaatherInformation(int temperature);
    }
}

Other advantages of dependency injection

  1. It will make your application more testable as you can mock dependencies and focus on the core business logic in that class.
  2. You can replace dependencies easily if required.
  3. Changes in dependencies do not affect the places where it is getting used as long as the contract/interface is safeguarded.