乘风破浪,遇见最佳跨平台跨终端框架.Net Core/.Net生态 - 贯穿ASP.NET Core整个架构的依赖注入框架(Dependency Injection)

为什么需要依赖注入框架

image

  • 借助依赖注入框架,可以轻松管理类之间的依赖,帮助我们在构建应用时遵循设计原则,确保代码的可维护性和可扩展性。
  • ASP.NET Core的整个架构中,依赖注入框架提供了对象创建和生命周期管理的核心能力,各个组件相互协作,也是依靠依赖注入框架的能力来实现的。

组件包

它采用接口实现分离模式,其中Microsoft.Extensions.DependencyInjection.Abstractions是抽象包,Microsoft.Extensions.DependencyInjection是具体实现。

意味着后续我们可以用第三方包来替代默认实现。

核心类型

  • IServiceCollection,负责服务的注册。
  • ServiceDescriptor,每一个服务注册时的信息。
  • IServiceProvider,具体的容器,也是由ServiceCollection Build出来的。
  • IServiceScope,表示一个容器的子容器的生命周期。

生命周期(Sevice Lifetime)

  • 单例(Singleton),指在整个根容器的生命周期内,都是单例,不管你是根容器还是子容器。
  • 作用域(Scoped),在容器的生存周期内,或者子容器的生存周期内,如果我的容器释放掉,意味着我的对象也会释放掉,在这个范围内我们得到的是个单例模式。
  • 瞬时(暂时)Transient,指我们每一次从容器中获取对象时,都可以得到一个全新的对象。

单例和作用域的区别是,单例是全局的单例,作用域是范围内的单例。

实践理解

https://github.com/TaylorShi/HelloDependencyInjection

创建项目

dotnet new sln -o HelloDependencyInjection
cd .\HelloDependencyInjection\
dotnet new webapi -o demoForDI60 -f net6.0
dotnet sln add .\demoForDI60\demoForDI60.csproj
dotnet new webapi -o demoForDI31 -f netcoreapp3.1
dotnet sln add .\demoForDI31\demoForDI31.csproj
code .
explorer.exe .

image

准备一些代码

public interface IMyScopedService { }

public class MyScopedService : IMyScopedService
{

}
public interface IMySingletonService { }

public class MySingletonService : IMySingletonService
{

}
public interface IMyTransientService { }

public class MyTransientService : IMyTransientService
{

}

注册不同服务

// demoForDI31\Startup.cs

public void ConfigureServices(IServiceCollection services)
{
    #region 注册服务不同生命周期的服务

    services.AddSingleton<IMySingletonService, MySingletonService>();
    services.AddScoped<IMyScopedService, MyScopedService>();
    services.AddTransient<IMyTransientService, MyTransientService>();

    #endregion

    services.AddControllers();
}

这里将IMySingletonService组注册成单例模式,将IMyScopedService组注册成作用域模式,将IMyTransientService组注册成瞬时模式。

然后在WeatherForecastController.cs中添加一个检验的方法,新增一个GetServices接口来打印从容器中读取的六个实例的HashCode值。

[HttpGet("GetServices")]
public int GetServices
(
    [FromServices] IMySingletonService singleton1,
    [FromServices] IMySingletonService singleton2,
    [FromServices] IMyScopedService scoped1,
    [FromServices] IMyScopedService scoped2,
    [FromServices] IMyTransientService transient1,
    [FromServices] IMyTransientService transient2
)
{
    Console.WriteLine($"请求开始");

    Console.WriteLine($"singleton1:{singleton1.GetHashCode()}");
    Console.WriteLine($"singleton2:{singleton2.GetHashCode()}");

    Console.WriteLine($"scoped1:{scoped1.GetHashCode()}");
    Console.WriteLine($"scoped2:{scoped2.GetHashCode()}");

    Console.WriteLine($"transient1:{transient1.GetHashCode()}");
    Console.WriteLine($"transient2:{transient2.GetHashCode()}");

    Console.WriteLine($"请求结束");
    return 1;
}

这里使用了[FromServices]代表从容器中获取实例。

分析结果

demoForDI31\Properties\launchSettings.jsondemoForDI31节点下的launchUrl改成:weatherforecast/GetServices

运行后,我们看下结果。

请求开始
singleton1:687191
singleton2:687191
scoped1:49385318
scoped2:49385318
transient1:13062350
transient2:10366524
请求结束

在运行一次,看下结果

请求开始
singleton1:687191
singleton2:687191
scoped1:26847985
scoped2:26847985
transient1:199777
transient2:8990007
请求结束

从这两组数据可以看出,注册为单例的实例在两次运行中HashCode是相同的,两个瞬时实例得到的HashCode是完全不同的。对作用域实例来说,单次请求得到的HashCode是相同的,但是两次请求的值是不同的。

花式注册

1. 直接注入实例

public static IServiceCollection AddSingleton<TService>(this IServiceCollection services, TService implementationInstance) where TService : class;
services.AddSingleton<IOrderService>(new OrderService());

2. 工程方式注册

public static IServiceCollection AddSingleton<TService>(this IServiceCollection services, Func<IServiceProvider, TService> implementationFactory) where TService : class;
services.AddSingleton<IOrderService>(serviceProvider =>
{
    return new OrderService();
});
services.AddScoped<IOrderService>(serviceProvider =>
{
    return new OrderService();
});
services.AddTransient<IOrderService>(serviceProvider =>
{
    return new OrderService();
});

3. 尝试注册(接口维度)

尝试注册意思是,注册的接口对应的实现已经注册过了,就不重复注册了。

services.AddSingleton<IOrderService>(new OrderService());
services.TryAddSingleton<IOrderService, OrderServiceEx>();

然后我们新增一个GetServiceList路由节点来打印从容器中获取到的IOrderService到底有多少个。

[HttpGet("GetServiceList")]
public int GetServiceList([FromServices] IEnumerable<IOrderService> orderServices)
{
    foreach (var orderService in orderServices)
    {
        Console.WriteLine($"{orderService}:{orderService.GetHashCode()}");
    }

    return 1;
}

执行结果来看。

demoForDI31.Services.OrderService:38196344

只有一个,说明第二次试图再次注册IOrderService失败了。

如果注册两个实例会怎么样?

services.AddSingleton<IOrderService>(new OrderService());
services.AddSingleton<IOrderService, OrderServiceEx>();

这时候输出是

demoForDI31.Services.OrderService:31523018
demoForDI31.Services.OrderServiceEx:17375337

那么当IOrderService存在两个注册实例的时候,默认会取哪一个呢?

[HttpGet("GetService")]
public int GetService([FromServices]IOrderService orderService)
{
    Console.WriteLine($"{orderService}:{orderService.GetHashCode()}");
    return 1;
}

运行结果是:

demoForDI31.Services.OrderServiceEx:50833863

我们会看到,它默认会取最后注册的那一个,也就是说,同一个服务注册多次,如果取服务实例的时候,默认取的是最后一个。

4. 尝试注册(实现维度)

这里有个特殊的用法,就是通过TryAddEnumerable方法可以实现,实现不同就可以注册进去,如果实现相同就不注册进去。

services.AddSingleton<IOrderService>(new OrderService());
services.TryAddEnumerable(ServiceDescriptor.Singleton<IOrderService, OrderService>());

得到的结果只有一个,因为后面注册的接口实现类和之前一样,所以注册不进去。

demoForDI31.Services.OrderService:24250448
services.AddSingleton<IOrderService>(new OrderService());
services.TryAddEnumerable(ServiceDescriptor.Singleton<IOrderService, OrderServiceEx>());

得到的结果是

demoForDI31.Services.OrderService:24250448
demoForDI31.Services.OrderServiceEx:40098280

这时候发现,OrderServiceOrderServiceEx实现不一样,所以两个成功注册进去了。

这样能解决的那种,同一个接口,不希望被注册多次,但是不同实现可以被注册的场景。

替换注册

通过Replace可以替换掉之前注册的服务为新注册的服务。

services.AddSingleton<IOrderService>(new OrderService());
services.Replace(ServiceDescriptor.Singleton<IOrderService, OrderServiceEx>());

运行结果

demoForDI31.Services.OrderServiceEx:24250448

可以看到前面注册的OrderService被替换成OrderServiceEx实例了。

移除注册

services.RemoveAll<IOrderService>();

运行结果


实际为空,也就是没有注册实例可打印了。

注册泛型模板

准备泛型模板代码

public interface IGenericService<T> { }

public class GenericService<T> : IGenericService<T>
{
    public T Data { get; private set; }

    public GenericService(T data)
    {
        this.Data = data;
    }
}

注册泛型模板

services.AddSingleton(typeof(IGenericService<>), typeof(GenericService<>));

需要用typeof来注册。

使用泛型模板

public WeatherForecastController(ILogger<WeatherForecastController> logger, IGenericService<IOrderService> genericService)
{
    _logger = logger;
}

运行之后,我们可以看到genericService的Data是demoForDI31.Services.OrderService

image

依赖注入实例的两种方法

1. 通过构造函数方式

public WeatherForecastController(ILogger<WeatherForecastController> logger, IOrderService orderService)
{
    _logger = logger;
}

适合场景:当我们定义一个Controller的时候,它的服务是大部分接口都需要使用的情况下,推荐从构造函数方式来注入。

2. 使用FromServices方式

[HttpGet("GetService")]
public int GetService([FromServices]IOrderService orderService)
{
    Console.WriteLine($"{orderService}:{orderService.GetHashCode()}");
    return 1;
}

适合场景:当我们一个服务仅仅是在某一个接口场景下使用,推荐使用FromServices的方式来注入。

实现IDisposable接口类型的释放

基本原则

  • DependencyInjection只负责释放由其创建的对象实例。
  • DependencyInjection在容器或者子容器释放时,才会去释放由其创建的对象实例。

建议

  • 避免在根容器创建实现了IDisposable接口的瞬时服务。
  • 避免手动创建对象然后塞到容器里面,而应该尽量使用容器来管理对象的创建和释放。

实践理解

准备代码

public interface IOrderService { }

public class DisposableOrderService : IOrderService, IDisposable
{
    public void Dispose()
    {
        Console.WriteLine($"DisposableOrderService Disposed:{this.GetHashCode()}");
    }
}

验证释放(瞬时模式)

public void ConfigureServices(IServiceCollection services)
{
    services.AddTransient<IOrderService, DisposableOrderService>();

    services.AddControllers();
}
[HttpGet]
public int Get([FromServices] IOrderService orderService1, [FromServices] IOrderService orderService2)
{
    Console.WriteLine($"接口请求处理结束");
    return 1;
}

执行结果是

接口请求处理结束
DisposableOrderService Disposed:15401461
DisposableOrderService Disposed:25712189

可见这两个实例对象是在整个请求结束之后,才会去触发释放。

但是仍然有一种情况很危险,那就是实现了IDisposable的瞬时服务,如果是在根容器去获取和创建,那么它会一直保持到整个程序生命周期结束才会被释放。

public void ConfigureServices(IServiceCollection services)
{
    services.AddTransient<IOrderService, DisposableOrderService>();

    services.AddControllers();
}

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    app.ApplicationServices.GetService<IOrderService>();
}

这里定义了瞬时的IOrderService服务,但是接下来在根容器中获取并创建了它。

[HttpGet]
public int Get
(
    [FromServices] IHostApplicationLifetime hostApplicationLifetime,
    [FromQuery] bool stop = false
)
{
    Console.WriteLine($"接口请求处理结束");
    if (stop)
    {
        hostApplicationLifetime.StopApplication();
    }
    return 1;
}

正常运行结果:

接口请求处理结束

没有之前的释放信息。

传入关闭动作:

接口请求处理结束
info: Microsoft.Hosting.Lifetime[0]
      Application is shutting down...
DisposableOrderService Disposed:20074041

这时候这个瞬时服务实例才被真正释放掉。

验证释放(作用域模式)

public void ConfigureServices(IServiceCollection services)
{
    services.AddScoped<IOrderService, DisposableOrderService>();

    services.AddControllers();
}
[HttpGet]
public int Get([FromServices] IOrderService orderService1, [FromServices] IOrderService orderService2)
{
    Console.WriteLine($"子容器开始");

    using (IServiceScope scope = HttpContext.RequestServices.CreateScope())
    {
        var service1 = scope.ServiceProvider.GetService<IOrderService>();
        var service2 = scope.ServiceProvider.GetService<IOrderService>();
    }

    Console.WriteLine($"子容器结束");

    Console.WriteLine($"接口请求处理结束");
    return 1;
}

我们来看看结果

子容器开始
DisposableOrderService Disposed:20281500
子容器结束
接口请求处理结束
DisposableOrderService Disposed:54516368

我们发现,子容器中那个作用域对象是在子容器生命周期结束时就释放了,而外面那个作用域是在整个请求结束后释放的。

验证释放(单例模式)

public void ConfigureServices(IServiceCollection services)
{
    services.AddSingleton<IOrderService, DisposableOrderService>();

    services.AddControllers();
}
[HttpGet]
public int Get([FromServices] IOrderService orderService1, [FromServices] IOrderService orderService2)
{
    Console.WriteLine($"接口请求处理结束");
    return 1;
}
接口请求处理结束

可以看到整个接口请求结束之后,这个单例的实例还没有被释放,那么单例实例会在什么时候释放呢?

[HttpGet]
public int Get
(
    [FromServices] IOrderService orderService1,
    [FromServices] IOrderService orderService2,
    [FromServices] IHostApplicationLifetime hostApplicationLifetime,
    [FromQuery] bool stop = false
)
{
    Console.WriteLine($"接口请求处理结束");
    if (stop)
    {
        hostApplicationLifetime.StopApplication();
    }
    return 1;
}
接口请求处理结束
info: Microsoft.Hosting.Lifetime[0]
      Application is shutting down...
DisposableOrderService Disposed:48950176

这里我们看到,当使用IHostApplicationLifetime接口中StopApplication方法,强行停止整个程序的时候,这时候这个单例实例终于被释放了。

但是单例模式下,如果我们手动创建实例对象

public void ConfigureServices(IServiceCollection services)
{
    services.AddSingleton<IOrderService>(new DisposableOrderService());

    services.AddControllers();
}

即使是手动关闭程序,你会发现结果是

接口请求处理结束
info: Microsoft.Hosting.Lifetime[0]
      Application is shutting down...

这种手动创建的单例服务对象,并不会在根容器生命周期结束的时候被释放。

参考

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章