ASP.NET Core 依赖注入基础测试题
作为一名 ASP.NET Core 的开发者,依赖注入可以说是居家旅行开发调试的必备技能。
在这篇文章里,希望通过一些常识性测试题,来巩固学习一下依赖注入的基础知识。
作用域
请问下面这段代码的执行结果是什么?
public interface IServiceA { } class ServiceA : IServiceA { ServiceA() { Console.WriteLine("New SA"); } } public class Startup { public void ConfigureServices(IServiceCollection services) { services.AddTransient(); ... } }
结果是报错:
System.AggregateException: 'Some services are not able to be constructed' A suitable constructor for type 'AspNetCore.Services.ServiceA' could not be located. Ensure the type is concrete and services are registered for all parameters of a public constructor.
官方文档在 Constructor injection behavior 有提过,如果通过构造函数注入,构造函数必须的 public ,而如果不显示声明,默认的访问等级是 internal 。
为什么 constructor 要 public 呢?因为默认情况下的访问级别是 internal,只允许同一个 assembly 下的文件访问。依赖注入是由 ASP.NET Core 实现的,自然是无法访问 internal 级别的构造方法的。
那 class 需不需要是 public 呢?不需要,因为通过方法调用的方式已经让 DI 获取到了 class,如果是 using namespace 的情况下访问 class,才需要 class 也是 public。
初始化时间
如果没有任何服务依赖 IServiceA,但是通过 AddSingleton 注入了,IServiceA 的构造方法是否会执行? AddScoped 呢? AddTransient 呢?
public interface IServiceA { } public class ServiceA : IServiceA { public ServiceA() { Console.WriteLine("New SA"); } } public class Startup { public void ConfigureServices(IServiceCollection services) { services.AddSingleton(); ... } }
结果是都不会执行。虽然没有 public constructor 会报错,但是如果没有服务依赖 IServiceA,是不会进入 ServiceA constructor 的。具体原理可以阅读 Microsoft.Extensions.DependencyInjection 源码 学习。
生命周期
下面这段代码中,IServiceA 被 HelloController 所依赖,在项目启动之后,没有访问网页的情况下,ServiceA 会被初始化吗?
public interface IServiceA { } public class ServiceA : IServiceA { public ServiceA() { Console.WriteLine("New SA"); } } public class Startup { public void ConfigureServices(IServiceCollection services) { services.AddSingleton(); ... } } public class HelloController : ControllerBase { public WeatherForecastController(IServiceA sa) { Console.WriteLine($"Test Controller: {sa.GetType()}"); } }
ServiceA 并不会被初始化,因为 controller 只有在请求过来的时候才会被初始化:

如果访问了三次 HelloController 中的路径,运行结果会是什么?
New SA Test Controller: AspNetCore.Services.ServiceA Test Controller: AspNetCore.Services.ServiceA Test Controller: AspNetCore.Services.ServiceA
可以看到,singleton 是延时加载的,只有在调用时发现没有实例的情况下才会初始化。
如果我们用 AddScoped 或者 AddTrancient,每次访问 API 都会看到 ServiceA 被初始化了:
New SA Test Controller: AspNetCore.Services.ServiceA New SA Test Controller: AspNetCore.Services.ServiceA New SA Test Controller: AspNetCore.Services.ServiceA
依赖后的生命周期
如果 ServiceA 是 transient 的,ServiceB 是 singleton 的,ServiceB 和 controller 都依赖 ServiceA,请问第一次访问 controller 的路由,ServiceA 会被初始化几次?第二次访问呢?
public interface IServiceA { } public class ServiceA : IServiceA { public ServiceA() { Console.WriteLine("New SA"); } } public interface IServiceB { } public class ServiceB : IServiceB { public ServiceB(IServiceA sa) { Console.WriteLine("New SB"); } } public class Startup { public void ConfigureServices(IServiceCollection services) { services.AddTransient(); services.AddSingleton(); ... } } public class HelloController : ControllerBase { public WeatherForecastController(IServiceA sa, IServiceB sb) { Console.WriteLine($"Test Controller: {sa.GetType()} {sb.GetType()}"); } }
第一次访问输出结果:
New SA New SA New SB Test Controller: AspNetCore.Services.ServiceA AspNetCore.Services.ServiceB New SA Test Controller: AspNetCore.Services.ServiceA AspNetCore.Services.ServiceB
可以看到,ServiceA 因为是 transient 的,所以每次请求都会被初始化一次。而 ServiceB 是 singleton 的,虽然它依赖一个 transient 的 ServiceA,但是初始化之后就不会再传入新的 ServiceA 了,在 singleton 的 ServiceB 中的 ServiceA 也是 singleton 的。
如果在 transient 的 ServiceA 中依赖一个 singleton 的 ServiceB 呢?
New SB New SA Test Controller: AspNetCore.Services.ServiceA AspNetCore.Services.ServiceB New SA Test Controller: AspNetCore.Services.ServiceA AspNetCore.Services.ServiceB
singleton 的 ServiceB 不管在哪里取出,都是 singleton 的,虽然 ServiceA 和 controller 在多个请求中做了多次初始化,但是传入的都是同一个 ServiceB 实例。
多个依赖的初始化顺序
如果注册的时候是先 A 后 B,constructor 里是先 B 后 A,哪个会先被初始化?
public interface IServiceA { } public class ServiceA : IServiceA { public ServiceA() { Console.WriteLine("New SA"); } } public interface IServiceB { } public class ServiceB : IServiceB { public ServiceB() { Console.WriteLine("New SB"); } } public class Startup { public void ConfigureServices(IServiceCollection services) { services.AddSingleton(); services.AddSingleton(); ... } } public class HelloController : ControllerBase { public WeatherForecastController(IServiceB sb, IServiceA sa) { Console.WriteLine($"Test Controller: {sa.GetType()}"); } }
输出结果:
New SB New SA Test Controller: AspNetCore.Services.ServiceA AspNetCore.Services.ServiceB
虽然注入依赖的顺序是 AB ,但是因为调用顺序是 BA,所以会先初始化 B 再初始化 A
如果 B 的构造函数依赖了 A 呢?
public class ServiceB : IServiceB { public ServiceB(IServiceA sa) { Console.WriteLine($"New SB with sa:{sa.GetType()}"); } }
输出结果:
New SA New SB with sa:AspNetCore.Services.ServiceA Test Controller: AspNetCore.Services.ServiceA AspNetCore.Services.ServiceB
此时会先把被依赖的 ServiceA 初始化完成再继续初始化 ServiceB。
如果依赖注入的时候是先注入 B 再注入 A 呢?
public void ConfigureServices(IServiceCollection services) { services.AddScoped(); services.AddScoped(); }
输出结果:
New SA New SB with sa:AspNetCore.Services.ServiceA Test Controller: AspNetCore.Services.ServiceA AspNetCore.Services.ServiceB
可以看到,依赖注入的声明顺序并不重要,DI Container 会存储一个类似 Key Value 的实现关系,在初始化的时候会根据依赖关系妥善处理。
一个接口多种实现
如果一个 interface 有多个实现类,并且都进行了注入,在 constructor 取出这个 interface 的时候会取到哪一个?多个实现类是否都会被初始化?
public interface IServiceA { } public class ServiceA : IServiceA { public ServiceA() { Console.WriteLine("New SA"); } } public class ServiceB : IServiceA { public ServiceB() { Console.WriteLine("New SB"); } } public class Startup { public void ConfigureServices(IServiceCollection services) { services.AddSingleton(); services.AddSingleton(); ... } } public class HelloController : ControllerBase { public WeatherForecastController(IServiceA sa) { Console.WriteLine($"Test Controller: {sa.GetType()}"); } }
输出结果:
New SB Test Controller: AspNetCore.Services.ServiceB
一个接口多个实现,只会取出最后的一个实现来构造实例。其他实现类的构造方法不会被调用。DI Container 在存好接口和实现的映射关系后,如果有新的实现就会覆盖掉前面的映射。
多个接口一个实现
如果一个接口有多个实现,并且都进行了单例的依赖注入,在取出实例的时候会被初始化几次?
public interface IServiceA { } public interface IServiceB { } public class ServiceB : IServiceA, IServiceB { public ServiceB() { Console.WriteLine("New SB"); } } public class Startup { public void ConfigureServices(IServiceCollection services) { services.AddSingleton(); services.AddSingleton(); ... } } public class HelloController : ControllerBase { public WeatherForecastController(IServiceA sa, IServiceB sb) { Console.WriteLine($"Test Controller: {sa.GetType()} {sb.GetType()}"); } }
输出结果:
New SB New SB Test Controller: AspNetCore.Services.ServiceB AspNetCore.Services.ServiceB
可以看到,AddSingleton 是针对 interface 的单例,而不是实现类的单例。对于 DI 来说,ServiceB 是对两种 interface 的实现类,会分别进行初始化。
后续
这些问题都是比较基础的依赖注入问题,其中的一些理解分析也只是个人观点,如果有错误的地方欢迎指出。
如果希望深入的学习 ASP.NET Core 的依赖注入,推荐阅读 Microsoft.Extensions.DependencyInjection 源码 ,看完源码之后,很多疑惑和猜想便会自然得到解答。
参考资料: