DEV Community

Alex
Alex

Posted on • Edited on

.NET Learning Notes: Dependency Injection

references:

MicroSoft: dependency-injection
Youtube: YZK

Control Inversion in Real Life:

Generating your own electricity (self-controlled) vs. using the power grid (externally controlled, user just consumes).
Dependency Injection (DI) is an implementation of the Inversion of Control (IoC) principle.
It simplifies module assembly and reduces coupling between components.

The goal of IoC in code:

From “How to create an object” → to “I just need the object.”
There are two main ways to implement IoC:

Service Locator, and

Dependency Injection (DI) — the more commonly used approach.

Key Concepts of Dependency Injection (DI):

  • Dependency: An object that another object depends on to perform its function.
  • Service: The object that is requested from the framework or dependency injection container.
  • Service Registration: The process of registering service types and their implementations in the DI container.
  • Service Container: The central registry that manages all registered services and their lifetimes.
  • Service Resolution: The process of creating service instances and injecting their dependencies when needed.
  • Service Lifetimes: Transient: A new instance is created each time the service is requested. Scoped: A single instance is created per scope (e.g., per web request). Singleton: A single instance is created and shared across the entire application's lifetime.

.NET中使用DI:

根据类型来获取和注册服务。可以分别制定服务类型(service type)和实现类(implementation type)。
.NET控制反转组件取名为DependencyInjection, 它包含ServiceLocator的功能。

  • dotnet add package Microsoft.Extensions.DependencyInjection --version 9.0.1
  • using Microsoft.Extensions.DependencyInjection
  • Create a new ServiceCollection instance, then register and configure services in the ServiceCollection.
  • ServiceCollection来构造容器对象IServiceProvider。调用ServiceCollection的BuildServiceProvider创建ServiceProvider,可以用来获取BuildServiceProvider之前ServiceCollection中的对象。

What happens if a constructor contains parameters that cannot be injected?

In the default .NET Dependency Injection container, the rule is strict:

  • All parameters in the selected constructor must be resolvable by the container.
  • If any parameter cannot be resolved, the container will throw an exception (typically InvalidOperationException or a similar activation error).
  • It will not pass null or default values. This means that the container will refuse to construct the object altogether unless it can resolve every required dependency.

How to handle parameters that are not registered services?

There are three common strategies:

1.Only use injectable services in constructors

This is the recommended practice. Keep constructors clean and fully injectable.

2.Use IOptions<T> or configuration binding
For primitive types like string, int, or bool, bind them to a configuration section and inject them as a strongly typed options object.

services.Configure<MySettings>(configuration.GetSection("MySettings"));
Enter fullscreen mode Exit fullscreen mode
public MyService(IOptions<MySettings> options)
{
    var connectionString = options.Value.ConnectionString;
}
Enter fullscreen mode Exit fullscreen mode

3.Use factory-based registration

If you need to inject a combination of container-managed services and manual values:

services.AddSingleton<IMyService>(sp =>
{
    var logger = sp.GetRequiredService<ILogger<MyService>>();
    var manualValue = "hard-coded";
    return new MyService(logger, manualValue);
});
Enter fullscreen mode Exit fullscreen mode

Summary
In .NET DI, constructor injection is all-or-nothing:
If any constructor parameter can't be resolved by the container, the entire object won't be created. Use IOptions<T> or manual factories to work around this when necessary.

服务的生命周期:

  • Transient(created each time they're requested from the service container.); Scoped(For Web applications, a scoped lifetime indicates that services are created once per client request/connection); Singleton(Every subsequent request of the service implementation from the dependency injection container uses the same instance, Singleton services must be thread safe and are often used in stateless services.)
  • 给类构造函数中打印,看看不同生命周期对象创建,使用serviceProvider.CreateScope()创建scope(在scope中获取Scope相关对象,scope.ServiceProvider而不是全局的ServiceProvider), 另外在范围内部的对象不要在范围外部引用,有可能对象已经在范围内部销毁了,外部引用为空;
  • 如果一个类实现了IDisposable接口,则离开作用域之后容器会自动调用对象的Dispose方法;
  • 不要在长生命周期的对象中引用比它短的生命周期对象,在ASP.NET Core中,这样做会默认抛异常。
  • 生命周期的选择:如果类无状态,建议为Singleton;如果类有状态,则有Scope控制,建议为Scoped,因为通常这种Scope控制下的代码都会运行在同一个线程中,没有并发修改的问题;在使用Transient的时候要谨慎。
  • .NET组册服务的重载方法很多。

Any of service registration methods can be used to register multiple service instances of the same service type. the following call overrides the previous one when resolving service, and adds to the previous one when multiple services are solved via IEnumerable, Services appear in the order they were registered when resolved via IEnumerable.

IServiceProvider的服务定位器方法:

  • T GetService()如果获取不到对象,则返回null
  • object GetService(Type serviceType)
  • T GetRequiredService()如果获取不到对象,则抛异常
  • object GetRequiredService(Type serviceType)
  • IEnumerable GetServices()适用于可能有很多满足条件的服务
  • IEnumerable GetServices(Type ServiceType)

依赖注入是有“传染性”的,如果一个类的对象是通过DI创建的,那么这个类的构造函数中生命的所有服务类型的参数都会被DI赋值;但是如果一个对象是程序员手动创建的,安么这个对象就和DI没有关系,它的构造函数中的生命的服务类型参数就不会被自动赋值;
.NET的DI默认是构造函数注入

e.g. 编写一个类,连接数据库做插入操作,并且记录日志(模拟的输出),把Dao、日志都放入单独的服务类。

using Microsoft.Extensions.DependencyInjection;

ServiceCollection services = new ServiceCollection();

services.AddScoped<Controller>();
services.AddScoped<ILog, LogImpl>();
services.AddScoped<IStorage, StorageImpl>();
services.AddScoped<IConfig, ConfigImpl>();

using (var sp = services.BuildServiceProvider())

{
// 所有的成员变量,都会被框架创建并赋值
var c = sp.GetRequiredService<Controller>();

c.Test();
}


class Controller

{
private readonly ILog log;

private readonly IStorage storage;

public Controller (ILog log, IStorage storage)

{
this.log = log;

this.storage = storage;

}


public void Test()

{
this.log.Log("start upload...");
this.storage.Save("sdflkjljioweuriowueroiweu", "1.txt");

this.log.Log("finish upload....");
}
}


interface ILog

{
public void Log(String msg);

}


class LogImpl : ILog

{
public void Log(string msg)

{
Console.WriteLine($"log: {msg}");

}
}


interface IConfig

{
public string GetValue(string name);

}


class ConfigImpl : IConfig

{
public string GetValue(string name)

{
return "hello";

}
}


interface IStorage

{
public void Save(string content, string name);

}


class StorageImpl : IStorage

{
private readonly IConfig config;

public StorageImpl(IConfig config)

{
this.config = config;

}
public void Save(string content, string name)

{
string server = config.GetValue("serve");

Console.WriteLine($"upload to {server}, file name is {name}, content is {content}");

}
}
Enter fullscreen mode Exit fullscreen mode

Multiple constructor discovery rules

  • When a type defines more than one constructor, the service provider has logic for determining which constructor to use. The constructor with the most parameters where the types are DI-resolvable is selected.
  • If there's ambiguity when discovering constructors, an exception is thrown. You can avoid ambiguity by defining a constructor that accepts both DI-resolvable types instead.

总结:
关注于接口,而不是关注于实现,各个服务可以更弱耦合的协同工作。在编写代码的时候,不用知道具体服务的实现。

Top comments (0)