Skip to main content

Dependency Injection

When class A uses class B, class B is a dependency of class A.

There are two types of dependencies:

  1. Tight Coupling
  2. Loose Coupling

Tight Coupling

It's characterized by inflexible dependencies between classes.

For example, in Get() method, we use an instance of inMemoryRepository.

WeatherForecastController
public class WeatherForecastController : ControllerBase
{
private readonly ILogger<WeatherForecastController> _logger;
private readonly IRepository repository;

public WeatherForecastController(ILogger<WeatherForecastController> logger, IRepository repository)
{
_logger = logger;
this.repository = repository;
}

public WeatherForecastController(){}

public IEnumerable<WeatherForecast> Get()
{
var inMemoryRepository = new InMemoryRepository();
...
}
}

If another class creates WeatherForecastController using the empty constructor, it won't be aware of the hidden dependency on InMemoryRepository. Thus, the Get() method may return unpredictable results. For example:

    var weatherForecastController = new WeatherForecastController();
weatherForecastController.Get()

Problems with Tight Coupling:

  1. Lack of flexibility
  2. Do not know what dependency holds

Loose Coupling

Loose coupling is characterized by flexible and explicit dependencies between classes.

For example:

WeatherForecastController
public class WeatherForecastController : ControllerBase
{
private readonly IRepository repository;
public WeatherForecastController(IRepository repository)
{
this.repository = repository;
}
}
InMemoryRepository
public class InMemoryRepository : IRepository

Here, InMemoryRepository is implemented through an interface (IRepository). We can pass any class that implements IRepository to WeatherForecastController.

Advantages of Loose Coupling:

  1. More flexibility
  2. Explicit dependency

However, there's still a challenge. To initialize WeatherForecastController, you need to pass a class that implements IRepository. If that class also requires other classes as parameters, the initialization becomes cumbersome:

    var WeatherForecastController = new WeatherForecastController(new InMemoryRepository(new Logger(...)))

There is a better solution using Dependency Injection.

Dependency Injection

Dependency Injection (DI) automates the process of providing dependencies to classes, improving flexibility and testability.

AddSingleton

When a class requests an instance of IRepository, the same InMemoryRepository instance is provided for the application's lifetime, even across different HTTP requests:

builder.Services.AddSingleton<IRepository, InMemoryRepository>();  

If we want to change the implementation of IRepository later, we only need to modify it here.

AddTransient

When a class requests an instance of IRepository, a new instance of InMemoryRepository is provided each time:

builder.Services.AddTransient<IRepository, InMemoryRepository>();  

Let's say, InMemoryRepository has a list of genres:

public class InMemoryRepository : IRepository
{
private List<Genre> _genres;
public InMemoryRepository()
{
_genres = new List<Genre>()
{
new Genre(){ Id = 1, Name = "Comedy" },
new Genre(){ Id = 2, Name = "Action"},
};
}
}

Now, suppose we have two classes, Class A and Class B, that both depend on an instance of InMemoryRepository. If we register InMemoryRepository with AddTransient, each class will receive its own separate instance of the repository. This means that:

Class A and Class B will each get their own version of the InMemoryRepository and its list of genres. If Class A modifies the list of genres (e.g., adding a new genre) and Class B also modifies the list, there will be two independent copies of the genre list. The changes made in one class won't be reflected in the other. In other words, the lists in Class A and Class B are isolated from each other, and neither can access the changes made by the other.

AddScoped

Creates one instance per HTTP request. If multiple classes request the same service within a single HTTP request, they will share the same instance. If class A modifies the genre list during an HTTP request, class B will see those changes only if it runs within the same HTTP request. For a new HTTP request, a new instance will be provided, resetting the list.

builder.Services.AddScoped<IRepository, InMemoryRepository>();  

Conclusion

Dependency Injection offers:

  1. Improved flexibility by decoupling classes.
  2. Explicit dependency management, making code more readable and maintainable.
  3. Better testability by allowing easy replacement of real implementations with mocks or stubs during testing.

By using DI, we eliminate the need for manual instantiation of dependencies, leading to cleaner, more maintainable code.