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 源码 ,看完源码之后,很多疑惑和猜想便会自然得到解答。

参考资料: