38

ASP.NET Core 依赖注入基础测试题

 3 years ago
source link: https://blog.callmewhy.com/post/aspnet-core-yi-lai-zhu-ru-ji-chu-ce-shi-ti/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

作为一名 ASP.NET Core 的开发者,依赖注入可以说是居家旅行开发调试的必备技能。

在这篇文章里,希望通过一些常识性测试题,来巩固学习一下依赖注入的基础知识。

作用域

请问下面这段代码的执行结果是什么?

public interface IServiceA { }

class ServiceA : IServiceA
{
    ServiceA()
    {
        Console.WriteLine("New SA");
    }
}

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddTransient<IServiceA, ServiceA>();
        ...
    }
}

结果是报错:

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<IServiceA, ServiceA>();
        ...
    }
}

结果是都不会执行。虽然没有 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<IServiceA, ServiceA>();
        ...
    }
}
public class HelloController : ControllerBase
{
    public WeatherForecastController(IServiceA sa)
    {
        Console.WriteLine($"Test Controller: {sa.GetType()}");
    }
}

ServiceA 并不会被初始化,因为 controller 只有在请求过来的时候才会被初始化:

Nb2EfeF.png!web

如果访问了三次 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<IServiceA, ServiceA>();
        services.AddSingleton<IServiceB, ServiceBz>();
        ...
    }
}
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<IServiceA, ServiceA>();
        services.AddSingleton<IServiceB, ServiceB>();
        ...
    }
}

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<IServiceB, ServiceB>();
    services.AddScoped<IServiceA, ServiceA>();
}

输出结果:

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<IServiceA, ServiceA>();
        services.AddSingleton<IServiceA, ServiceB>();
        ...
    }
}

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<IServiceA, ServiceB>();
        services.AddSingleton<IServiceB, ServiceB>();
        ...
    }
}

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

参考资料:


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK