Design Pattern (5), Dependency Injection

Note: this article is published on 06/28/2024.

These will be a series of articles about Design Patterns. We start from MVC Pattern:

A - Introduction

.NET supports the dependency injection (DI) software design pattern, which is a technique for achieving Inversion of Control (IoC) between classes and their dependencies.This article will introduce the Dependency Inversion Principle first, then discuss the implementation of the principle: Dependency Injection.

  • A - Introduction
  • B - Dependency Inversion Principle 
  • C - Dependency Injection

B - Dependency Inversion Principle

The Dependency Inversion Principle states that High-level modules should not depend on low-level modules. Both should depend on abstraction. 

The direction of dependency within the application should be in the direction of abstraction, not implementation details. Most applications are written such that compile-time dependency flows in the direction of runtime execution, producing a direct dependency graph. such as,

  • Run Time: if class A calls a method of class B and class B calls a method of class C, then
  • Compile time: class A will depend on class B, and class B will depend on class C

Shown as:

Applying the dependency inversion principle allows A to call methods on an abstraction that B implements, making it possible for A to call B at run time, but for B to depend on an interface controlled by A at compile time (thus, inverting the typical compile-time dependency). At run time, the flow of program execution remains unchanged, but the introduction of interfaces means that different implementations of these interfaces can easily be plugged in.

Dependency inversion is a key part of building loosely coupled applications, since implementation details can be written to depend on and implement higher-level abstractions, rather than the other way around. The resulting applications are more testable, modular, and maintainable as a result. The practice of dependency injection is made possible by following the dependency inversion principle.

C - Dependency Injection

Dependency Injection (DI) design pattern is a implementation for achieving Inversion of Control (IoC) between classes and their dependencies.

Strong coupled classes:

dependency is an object that another object depends on. Examine the following MessageWriter class with a Write method that other classes depend on:

public class MyDependency
{
    public void WriteMessage(string message)
    {
        Console.WriteLine($"MyDependency.WriteMessage called. Message: {message}");
    }
}

A class can create an instance of the MyDependency class to make use of its WriteMessage method. In the following example, the MyDependency class is a dependency of the IndexModel class:

public class IndexModel : PageModel
{
    private readonly MyDependency _dependency = new MyDependency();

    public void OnGet()
    {
        _dependency.WriteMessage("IndexModel.OnGet created this message.");
    }
}

The class creates and directly depends on the MyDependency class.

Code dependencies, such as in the previous example, are problematic and should be avoided for the following reasons:

  • To replace MyDependency with a different implementation, the IndexModel class must be modified.
  • If MyDependency has dependencies, they must also be configured by the IndexModel class. In a large project with multiple classes depending on MyDependency, the configuration code becomes scattered across the app.
  • This implementation is difficult to unit test. The app should use a mock or stub MyDependency class, which isn't possible with this approach.

Loose coupled classes

Dependency injection addresses these problems through:

  • The use of an interface or base class to abstract the dependency implementation.
  • Registration of the dependency in a service container. ASP.NET Core provides a built-in service container, IServiceProvider. Services are typically registered in the app's Startup.ConfigureServices method.
  • Injection of the service into the constructor of the class where it's used. The framework takes on the responsibility of creating an instance of the dependency and disposing of it when it's no longer needed.

In the sample app, the IMyDependency interface defines the WriteMessage method:

public interface IMyDependency
{
    void WriteMessage(string message);
}

This interface is implemented by a concrete type, MyDependency:

The sample app registers the IMyDependency service with the concrete type MyDependency. The AddScoped method registers the service with a scoped lifetime:

In the sample app, the IMyDependency service is requested and used to call the WriteMessage method, it is injucted into the client class, Index2Model, through the constructor:

By using the DI pattern, the controller:

  • Doesn't use the concrete type MyDependency, only the IMyDependency interface it implements. That makes it easy to change the implementation that the controller uses without modifying the controller.
  • Doesn't create an instance of MyDependency, it's created by the DI container.

References