第6课:作用域与对象释放行为

学习分享 丨作者 / 郑 子 铭 丨公众号 / DotNet NB / CloudNative NB

作用域主要由 IServiceScope 这个接口来承载

对于实现 IDisposable 类的实例的对象,容器会负责对其生命周期进行管理,使用完毕之后,他会释放这些对象

实现 IDisposable 接口类型的释放:

  • 1、容器只会负责由其创建的对象,如果这个对象是自己创建出来并放到容器里的,容器不负责释放这个对象

  • 2、在容器和子容器释放时,容器才会去释放这些对象,也就是说容器的生命周期与其创建的对象的生命周期是有对应关系的

两点建议:

  • 1、在根容器,最好不要创建实现了 IDisposable 瞬时服务

  • 2、避免手动创建实现了 IDisposable 对象,然后塞到容器里面,应该尽可能地使用容器来管理我们对象的创建和释放

先看一下服务

namespace DependencyInjectionScopeAndDisposableDemo.Services
{

    public interface IOrderService
    {

    }

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

首先定义 IOrderService

接着定义 IOrderService 的实现 DisposableOrderService,并实现了 IDisposable 这个接口

在释放的时候打印释放信息,并输出对象的 HashCode

接着是服务注册(Startup)

services.AddTransient<IOrderService,DisposableOrderService>();

这里先注册一个瞬时服务,将 IOrderService 注册进去

注释默认 Get 方法

//[HttpGet]
//public IEnumerable<WeatherForecast> Get()
//{
//    var rng = new Random();
//    return Enumerable.Range(1, 5).Select(index => new WeatherForecast
//    {
//        Date = DateTime.Now.AddDays(index),
//        TemperatureC = rng.Next(-20, 55),
//        Summary = Summaries[rng.Next(Summaries.Length)]
//    })
//    .ToArray();
//}

然后看一下控制器(WeatherForecastController)

[HttpGet]
public int Get([FromServices] IOrderService orderService1,
    [FromServices] IOrderService orderService2)
{
    return 1;
}

这里 FromServices 获取了两次 IOrderService

这里不需要写任何代码对它进行操作,因为整个生命周期是由容器去管理的

启动程序,输出如下:

DisposableOrderService Disposed:10579059
DisposableOrderService Disposed:47945396

可以看出,执行完毕之后,DisposableOrderService 会被释放掉,并且两个对象都会被释放掉

两个对象的 HashCode 不同

瞬时服务在每一次获取的时候都会获得一个新的对象

接着,添加一行代码表示服务

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

输出一下,表示我们的接口已经访问完毕,看一下释放时机在哪里

启动程序,输出如下:

接口请求处理结束
DisposableOrderService Disposed:35023218
DisposableOrderService Disposed:13943705

由此看出,接口请求处理结束后,才释放对象

接下来看一下 Scoped 模式

服务注册

services.AddScoped<IOrderService>(p => new DisposableOrderService());

控制器

[HttpGet]
public int Get([FromServices] IOrderService orderService,
    [FromServices] IOrderService orderService2)
{
    Console.WriteLine("=======1==========");
    // HttpContext.RequestServices
    // 是当前请求的一个根容器
    // 应用程序根容器的一个子容器
    // 每个请求会创建一个容器
    using (IServiceScope scope = HttpContext.RequestServices.CreateScope())
    {
        // 在这个子容器下面再创建一个子容器来获取服务
        var service = scope.ServiceProvider.GetService<IOrderService>();
    }
    Console.WriteLine("=======2==========");

    Console.WriteLine("接口请求处理结束");

    return 1;
}

启动程序,输出如下:

=======1==========
DisposableOrderService Disposed:31307802
=======2==========
接口请求处理结束
DisposableOrderService Disposed:31614998

每次请求会获得两个释放,意味着每创建一个 Scoped 的作用域,每个作用域内可以是单例的

接下来,把服务切换为单例模式,通过工厂的方式

services.AddSingleton<IOrderService>(p => new DisposableOrderService());

启动程序,输出如下:

=======1==========
=======2==========
接口请求处理结束

可以看到代码实际上不会被释放

如果切换为瞬时模式,通过工厂的方式

services.AddTransient<IOrderService>(p => new DisposableOrderService());

启动程序,输出如下:

=======1==========
DisposableOrderService Disposed:12021664
=======2==========
接口请求处理结束
DisposableOrderService Disposed:3165221
DisposableOrderService Disposed:13048313

这里可以看到,获取三个服务并且释放掉

接下来把服务调整为自己创建,并注册进去

var service = new DisposableOrderService();
services.AddSingleton<IOrderService>(service);

同样我们也不会得到释放的输出

也就是说,通过这种方式注册,容器不会管理对象的生命周期

如何识别这个区别呢?

在控制器中注入 IHostApplicationLifetime 接口

这个接口的作用是用来管理整个应用程序的生命周期

它有一个方法 StopApplication

也就是说它可以把整个应用程序关掉

接着,通过手工关掉的方式看一下应用程序关闭时会不会把单例对象释放掉

[HttpGet]
public int Get([FromServices] IOrderService orderService,
    [FromServices] IOrderService orderService2,
    [FromServices]IHostApplicationLifetime hostApplicationLifetime,
    [FromQuery]bool stop = false)
{
    Console.WriteLine("=======1==========");
    // HttpContext.RequestServices 是当前请求的一个根容器,应用程序根容器的一个子容器,每个请求会创建一个容器
    using (IServiceScope scope = HttpContext.RequestServices.CreateScope())
    {
        // 在这个子容器下面再创建一个子容器来获取服务
        var service = scope.ServiceProvider.GetService<IOrderService>();
        var service2 = scope.ServiceProvider.GetService<IOrderService>();
    }
    Console.WriteLine("=======2==========");

    if (stop)
    {
        hostApplicationLifetime.StopApplication();
    }

    Console.WriteLine("接口请求处理结束");

    return 1;
}

首先用自己创建对象的方式

var service = new DisposableOrderService();
services.AddSingleton<IOrderService>(service);

启动程序

输入 ?stop=true

https://localhost:5001/weatherforecast?stop=true

输出如下:

...
DependencyInjectionScopeAndDisposableDemo.exe (进程 16884)已退出,代码为 0。
要在调试停止时自动关闭控制台,请启用“工具”->“选项”->“调试”->“调试停止时自动关闭控制台”。
按任意键关闭此窗口. . .

如果单例由容器来管理,切换回普通注册方式

services.AddSingleton<IOrderService, DisposableOrderService>();

启动程序

输入 ?stop=true

https://localhost:5001/weatherforecast?stop=true

输出如下:

Application is shutting down...
接口请求处理结束
DisposableOrderService Disposed:23399238

对象释放,应用程序退出

这里说明单例的服务都是注册在根容器里面

根容器的释放意味着需要在整个应用程序退出时释放

这个时候它会释放自己所管理的所有的 IDisposable 的对象

这里面有一个非常需要注意的坑:

假如把服务注册成瞬时的

services.AddTransient<IOrderService, DisposableOrderService>();

然后又在根容器里面去获取这个对象

var s = app.ApplicationServices.GetService<IOrderService>();

这意味着在根容器去持续的创建 IOrderService,但是由于根容器只会在应用程序整个退出时回收,也就意味着这些对象会一直积累在应用程序内

调整控制器,不获取 IOrderService

[HttpGet]
public int Get(
    [FromServices]IHostApplicationLifetime hostApplicationLifetime,
    [FromQuery]bool stop = false)
{

    if (stop)
    {
        hostApplicationLifetime.StopApplication();
    }

    return 1;
}

仅仅在根容器获取一次

var s = app.ApplicationServices.GetService<IOrderService>();

这样运行起来,每次请求(点击刷新)的话,整个输出是不会有内容的,因为我们没有在子容器里面去获取对象

也就是说,实现了 IDisposable 接口的服务,如果时注册瞬时的,又在根容器去做操作,它会一直保持到应用程序退出的时候,才能够被回收掉

GitHub源码链接:https://github.com/MingsonZheng/DotNetCoreDevelopmentActualCombat/tree/main/DependencyInjectionScopeAndDisposableDemo

Last updated