4. 容器中的服务创建与释放

我们使用了 IoC 容器之后,服务实例的创建和销毁的工作就交给了容器去处理,前面也讲到了服务的生命周期,那三种生命周期中对象的创建和销毁分别在什么时候呢。以下面的例子演示以下:

首先是新增三个类,用于注册三种不同的生命周期:

public class Service1
{
    public Service1()
    {
        Console.WriteLine("Service1 Created");
    }
}
public class Service2
{
    public Service2()
    {
        Console.WriteLine("Service2 Created");
    }
}
public class Service3
{
    public Service3()
    {
        Console.WriteLine("Service3 Created");
    }
}

接下来是演示场景,为了简单起见,就用后台服务程序吧

IHost host = Host.CreateDefaultBuilder(args)
    .ConfigureServices(services =>
    {
        services.AddHostedService<Worker>();
        services.AddSingleton<Service1>();
        services.AddScoped<Service2>();
        services.AddTransient<Service3>();
    })
    .Build();

await host.RunAsync();

public class Worker : BackgroundService
{
    private readonly ILogger<Worker> _logger;
    private readonly IServiceProvider _serviceProvid
    public Worker(ILogger<Worker> logger, IServiceProvider serviceProvider)
    {
        _logger = logger;
        _serviceProvider = serviceProvider;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        #region 生命周期实例创建
        Console.WriteLine("Service1 第一次调用");
        var service11 = _serviceProvider.GetService<Service1>();
        Console.WriteLine("Service1 第二次调用");
        var service12 = _serviceProvider.GetService<Service1>();

        // 创建作用域,与 Web 应用中的一次请求一样
        using (var scope = _serviceProvider.CreateScope())
        {
            Console.WriteLine("Service2 第一次调用");
            var service31 = scope.ServiceProvider.GetService<Service2>();
            Console.WriteLine("Service2 第二次调用");
            var service32 = scope.ServiceProvider.GetService<Service2>();

            using (var scope1 = _serviceProvider.CreateScope())
            {
                Console.WriteLine("Service2 第三次调用");
                var service33 = scope1.ServiceProvider.GetService<Service2>();
            }
        }
        {
            Console.WriteLine("Service3 第一次调用");
            var service41 = _serviceProvider.GetService<Service3>();

            Console.WriteLine("Service3 第二次调用");
            var service42 = _serviceProvider.GetService<Service3>();
            }
            #endregion
        }
    }
}

最终的输出如下:

image

通过输出,我们可以单例生命周期服务在第一次使用的时候创建,之后一直是一个实例,作用域生命周期服务在一个作用域中第一次使用的时候创建实例,之后在同一个实例中只保持一个,但在其他作用域中则会重新创建,而瞬时生命周期服务每次都会创建一个新实例。

看完创建,我们再看实例销毁的时机。

若服务实现了IDisposable接口,并且该服务是由DI容器创建的,则我们不应该手动去Dispose,DI容器会对服务自动进行释放。这里由两个关键点,一个是要实现 Idisposable 接口,一个是由容器创建。这里再增加多两个类,用于演示,并且为了避免干扰将之前演示创建过程的代码注释。

public class Service1 : IDisposable
{
    public Service1()
    {
        Console.WriteLine("Service1 Created");
  
    public void Dispose()
    {
        Console.WriteLine("Service1 Dispose");
    }
}

public class Service2 : IDisposable
{
    public Service2()
    {
        Console.WriteLine("Service2 Created");

    public void Dispose()
    {
        Console.WriteLine("Service2 Dispose");
    }
}

public class Service3 : IDisposable
{
    public Service3()
    {
        Console.WriteLine("Service3 Created");
    }

    public void Dispose()
    {
        Console.WriteLine("Service3 Dispose");
    }
}

public class Service4 : IDisposable
{
    public void Dispose()
    {
        Console.WriteLine("Service4 Dispose");
    }
}

public class Service5 : IDisposable
{
    public void Dispose()
    {
        Console.WriteLine("Service5 Dispose");
    }
}

之后后台服务程序也做一些修改

IHost host = Host.CreateDefaultBuilder(args)
    .ConfigureServices(services =>
    {
        services.AddHostedService<Worker>();
        services.AddSingleton<Service1>();
        services.AddScoped<Service2>();
        services.AddTransient<Service3>();
        // 这种方式依旧由容器创建实例,只不过提供了工厂方法
        services.AddSingleton<Service4>(provider => new Service4());
        // 这种方式是用外部创建实例,只有单例生命周期可用
        services.AddSingleton<Service5>(new Service5());
    })
    .Build();

await host.RunAsync();

public class Worker : BackgroundService
{
    private readonly ILogger<Worker> _logger;
    private readonly IServiceProvider _serviceProvid
    public Worker(ILogger<Worker> logger, IServiceProvider serviceProvider)
    {
        _logger = logger;
        _serviceProvider = serviceProvider;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        #region 生命周期实
        Console.WriteLine("Service1 调用");
        var service1 = _serviceProvider.GetService<Service1>();

        // 创建作用域,与 Web 应用中的一次请求一样
        using (var scope = _serviceProvider.CreateScope())
        {
            Console.WriteLine("Service2 调用");
            var service2 = scope.ServiceProvider.GetService<Service2>();
            Console.WriteLine("即将结束作用域
            Console.WriteLine("Service3 调用");
            var service3 = scope.ServiceProvider.GetService<Service3>();
        }

        Console.WriteLine("Service4 调用");
        var service4 = _serviceProvider.GetService<Service4>();
        Console.WriteLine("Service5 调用");
        var service5 = _serviceProvider.GetService<Service5>();

        #endregion
    }
}

这样要直接用命令启动应用,不能够通过vs调试,之后Ctrl+C停止应用的时候,输出如下:

image

通过输出可以看得到,瞬时生命周期服务和作用域生命周期服务在超出作用范围就会被释放,而单例生命周期服务则在应用关闭时才释放,同为单例生命周期的Service5没有被释放。

这里要提一下的是,在解析瞬时生命周期服务Service3的时候,示例代码中是放到一个单独的作用域中的,这是因为在通过 services.AddHostedService<Worker>(); 注入Worker的时候是注入为单例生命周期的,而在单例生命周期对象中解析其他生命周期的对象是会有问题的,这也是服务注入、解析需要注意的一个关键点。

一定要注意服务解析范围,不要在 Singleton 中解析 Transient 或 Scoped 服务,这可能导致服务状态错误(如导致服务实例生命周期提升为单例,因为单例生命周期的服务对象只会在应用停止的时候释放,而单例对象都没释放,它的依赖项肯定不会释放)。允许的方式有:

  • 在 Scoped 或 Transient 服务中解析 Singleton 服务
  • 在 Scoped 或 Transient 服务中解析 Scoped 服务(不能和前面的Scoped服务相同)

如果要在单例生命周期示例中临时解析作用域、瞬时生命周期的服务,可以通过创建一个子作用域的方式。对子作用域 IServiceScope 的工作方式感兴趣的,可阅读一下这篇文章:细聊.Net Core中IServiceScope的工作方式

参考文章:
ASP.NET Core 依赖注入 | Microsoft Learn
理解ASP.NET Core - 依赖注入(Dependency Injection)

ASP.NET Core 系列:

目录:ASP.NET Core 系列总结
上一篇:ASP.NET Core - 依赖注入(二)