banner
Chenh

Chenh

A Clear Explanation of Dependency Injection

Explaining Dependency Injection in One Article#

Let's start with a little self-hypnosis and remember that dependency injection is a design pattern.

Let's look at a simple example:

// 1. Strong dependency
public int MyStringTool(){
  string str = "Hello world!";
  return str.Count();
}
// 2. No dependency
string str = "Hello world!";
public int MyStringTool(string str){
  return str.Count();
}

We have two functions, one creates a local variable inside the function, and the other passes the variable through a parameter. The former has a strong dependency between the variable and the function (reflected in the fact that the local variable only works within the function). The latter passes the variable as a parameter to the function, giving the function the ability to handle it in a generalized way, without any dependency between the variable and the function.

This is the basic idea of dependency injection. The extension of this idea between classes is shown in the following example:

// 1. Strong dependency
public class MainObject{
  public Method(){
    var tool = new MyTool();
    // Use tool
  }
}

// 2. Dependency injection
public class MainObject{
  public readonly MyTool _tool;
  MainObject(MyTool tool){
    _tool = tool;
  }
  public Method(){
    _tool.function();
    // Use tool
  }
}

These are two ways to call the tool class Tool. In the first method, the tool MyTool is instantiated within the class, and there is a strong dependency between them. In the second method, the instance of the tool MyTool is passed through the constructor, achieving dependency injection, where the dependency MyTool is injected into the MainObject class through the constructor.

There is also a term called Inversion of Control, which I think is more appropriate.

Control represents the dependency relationship between the two, who controls whom/who depends on whom (exists).

In the first function example, the relationship between the variable and the function changes from the variable depends on the existence of the function/the function controls the lifecycle of the variable to the function must provide a parameter to run.

In the second class example, the relationship between the tool class and the main class changes from the tool instance is created by the main class and depends on the existence of the main class to the constructor of the main class must provide the tool class. This reflects the meaning of inversion. Of course, the class has a default constructor, so it is not necessary to provide the tool class.

Dependency Injection in ASP.NET#

The main body of ASP.NET is the WebApplication class. Its tool class is a series of Services. Here is a typical entry program for an ASP.NET application (Program.cs):

// Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
var provider = builder.Services.BuildServiceProvider();
var app = builder.Build();
app.UseAuthorization();
app.MapControllers();

app.Run()

In this sample program, Services provides services to WebApplication through dependency injection.

In this web application, there is an independent container that provides functional support for the app. These containers can obtain instances from each other through constructor injection. The following are the key classes and interfaces involved in dependency injection (DI):

  1. IServiceCollection: This is the interface for registering services. You can use its extension methods to register various service types and implementations.

  2. IServiceProvider: This is an interface for a service provider that resolves and retrieves registered service instances. It is created by the container when needed.

  3. ServiceLifetime: This is an enumeration that specifies the lifetime of registered services. Common lifetime options include Singleton, Scoped, and Transient.

  4. AddTransient, AddScoped, and AddSingleton: These are extension methods of the IServiceCollection interface used to add service types and their implementations to the service container, and specify their lifetimes.

  5. IServiceProvider.GetService<T>: This is a method of the IServiceProvider interface used to retrieve registered service instances. You can specify the specific type of service instance to retrieve by specifying the generic parameter of the service type.

  6. Constructor Injection: This is a common way of dependency injection, where dependencies are passed to the class constructor. Dependencies are automatically resolved and provided when creating class instances.

  7. Property Injection: In addition to constructor injection, you can also use property injection to inject dependencies. Define the properties that need to be injected in the class and mark them with the corresponding property injection attributes (such as [Inject], [Autowired], etc.).

Tip: Services.AddXXX() is the method for adding dependencies to the dependency container. This syntax is provided by the C# extension method feature, which allows you to add additional methods to a class without changing the class itself:

// Namespace is MyNamespace
public static int ExMethod(this String str){
  return str.Count();
}
// Usage
using MyNamespace
String str = new String("hello world!");
str.ExMethod();

In .NET, services are dependencies of an application and can be managed using dependency injection (DI). Services can be classes, interfaces, or other components defined in the application that provide specific functionality or services.

When a class needs to use other services, they can be directly introduced as dependencies through constructor injection. By declaring the dependencies as constructor parameters, the DI container will automatically detect these dependencies and provide them when creating instances of the class.

Example:

public class MyService
{
    private readonly IAnotherService _anotherService;

    public MyService(IAnotherService anotherService)
    {
        _anotherService = anotherService;
    }

    public void DoSomething()
    {
        // Use the dependency
        _anotherService.SomeMethod();
    }
}

In the above example, the MyService class needs to use an implementation of the IAnotherService interface. By declaring IAnotherService as a constructor parameter, the DI container will automatically provide it when creating an instance of MyService.

For external (static) classes or methods that need to use dependencies, they can obtain them through the IServiceProvider interface. IServiceProvider is an interface for a service provider that resolves and retrieves registered service instances.

Example:

public static class MyStaticClass
{
    public static void DoSomething(IServiceProvider serviceProvider)
    {
        // Get dependencies from IServiceProvider
        var anotherService = serviceProvider.GetService<IAnotherService>();
        
        // Use the dependency
        anotherService.SomeMethod();
    }
}

In the above example, MyStaticClass is a static class and its method DoSomething needs an instance of IAnotherService. By passing IServiceProvider as a parameter to the DoSomething method, you can use the GetService method to obtain an instance of IAnotherService from IServiceProvider.

In summary, with dependency injection and IServiceProvider, we can directly introduce dependencies in the constructor and let the DI container automatically provide them. For external (static) classes or methods, we can obtain dependencies through IServiceProvider, achieving the use of dependencies and decoupling.

Dependency Lifetimes#

When we use a dependency container to manage Services and have the container provide a dependency when needed, a question arises: when should the dependency be instantiated? Should it be created when needed, persist as a single instance, or provide an instance based on the scope of the requester? This is the lifetime of a dependency.

In a dependency injection (DI) container, the creation of instances can be handled differently based on configuration and timing needs. Generally, the DI container is initialized when the application starts and pre-creates and registers some instances as needed. The specific timing and lifetimes of instance creation can be controlled through configuration.

Common instance lifetimes include:

  1. Singleton: Only one instance is created throughout the application's lifecycle and reused when needed. This instance is usually created when the application starts.
  2. Scoped: An instance is created within each scope (e.g., each request) and reused within that scope. The instance is destroyed when the scope ends.
  3. Transient: A new instance is created each time it is requested. Each request gets a new instance, which is destroyed after the request is completed.

These lifetime options allow you to manage the creation and lifetimes of instances based on the needs of your application. You can specify the desired lifetime options when registering services, such as using the AddSingleton, AddScoped, or AddTransient methods.

These lifetime options allow you to manage the creation and lifetimes of instances based on the needs of your application. You can specify the desired lifetime options when registering services, such as using the AddSingleton, AddScoped, or AddTransient methods.

Other#

In addition to the default dependency injection container provided by ASP.NET Core, you can also choose to use other third-party dependency injection containers. Here are some common dependency injection containers:

  1. Autofac: Autofac is a popular and powerful dependency injection container that provides many advanced features and extension points, suitable for various application scenarios.
  2. Unity: Unity is a lightweight dependency injection container provided by Microsoft. It has good performance and scalability, and supports interface-oriented development.
  3. Ninject: Ninject is another popular dependency injection container that focuses on simplicity and ease of use. It provides a simple and flexible way to manage dependencies between objects.
  4. StructureMap: StructureMap is a powerful dependency injection container that supports constructor injection, property injection, and method injection. It provides many advanced features such as interceptors and configuration files.

These dependency injection containers have their own characteristics and advantages. You can choose the appropriate container based on the requirements of your project and personal preferences. When using these containers, you need to install the corresponding NuGet packages and configure them properly. You also need to understand the specific syntax and usage of these containers to correctly register and resolve dependencies.

In conclusion, the default dependency injection container provided by ASP.NET Core is sufficient for most application needs. However, if you need more advanced features or specific requirements, you can consider using other dependency injection containers.

Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.