【5min+】后台任务的积木。.NetCore中的IHostedService

系列介绍

【五分钟的dotnet】是一个利用您的碎片化时间来学习和丰富.net知识的博文系列。它所包含了.net体系中可能会涉及到的方方面面,比如C#的小细节,AspnetCore,微服务中的.net知识等等。
5min+不是超过5分钟的意思,"+"是知识的增加。so,它是让您花费5分钟以下的时间来提升您的知识储备量。

前言

这次终于可以给大家分享一些AspNet Core方面的东西了😀。虽然本次提及的内容是.NET Core通用,但将以AspNet Core为例作为介绍。

正文

咱们开发应用的时候,有时候可能需要建立一些独立于应用逻辑体本身的后台任务。比如:定时发送邮件、定时执行脚本这类持续运行的任务,也有验证数据库是否创建等只伴随应用启动而执行一次的任务。

在.NET Core 2.0 之后,官方为我们提供了一个叫做 IHostedService 的接口,它可以便于我们更好的实现托管服务。

在微软《.NET 微服务 - 体系结构》教程中,就有提及到关于该接口的描述:

x

那么今天咱们就来扒一扒 IHostedService 到底是一个怎样的东西,我们可以在什么情况下使用它。

前方车速够快,请抓好扶手。
x

IHostService

请注意 IHostedService 是从 .NET Core 提出的,所以可以看到它并不是专门只针对于 AspNet Core。 从.NetCore 3.x 之后,当大家创建一个新的AspNetCore应用的时候,打开默认的 Program.cs 文件,就会发现它和以往的版本已经不一样了。

//现在
public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .ConfigureWebHostDefaults(webBuilder =>
            {
                webBuilder.UseStartup<Startup>();
            });

//过去
public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
        new WebHostBuilder()
            .UseContentRoot(Directory.GetCurrentDirectory())
            .UseStartup<Startup>()
            .UseKestrel()
            .UseIISIntegration();

可以很明显的看出应用程序由原来的 IWebHostBuilder 更改为了 IHostBuilder。这就告诉我们,.NET Core进行了更高层次的抽象,也就意味着现在能支持更多不同托管主机的创建方式,未来也将支持更多的类型。果然是一盘很大的棋啊🤫

回到今天的主题 IHostedService 。 从命名上来看,就可以看出一些文章。 很明显,它是伴随主机一同启动的任务。因此来看看该接口的签名:

public interface IHostedService
{
    Task StartAsync(CancellationToken cancellationToken);
    Task StopAsync(CancellationToken cancellationToken);
}

确实,很直观。只有两个方法,一个是启动,一个是停止。也就是说在 Host 启动的时候,就会调用 StartAsync 方法。在 Host 停止的时候就会调用 StopAsync 方法。

在AspNet Core中的作用

那么如果是咱们要在AspNet Core中使用它,该如何操作呢? 首先,咱们先来建立一个实现该接口的类:

public class DemoHostService : IHostedService
{
    public async Task StartAsync(CancellationToken cancellationToken)
    {
        await Task.Delay(100);
    }

    public Task StopAsync(CancellationToken cancellationToken)
    {
        return Task.CompletedTask;
    }
}

然后还需要在 Startup.cs 中将它进行注册:

services.AddHostedService<DemoHostService>();

OK,就完了。然后应用就会在启动的时候执行 StartAsync 方法。 咱们可以来断点试一试,看一看它的启动顺序。 经过断点之后我们发现基础的AspNet Core 应用会在执行完成 ConfigureServices 方法之后 再执行 DemoHostServiceStartAsync 方法,最后再执行 Configure 方法:

// startup.cs

//第一步执行
public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers();
    services.AddHostedService<DemoHostService>();
}

// 中间执行DemoHostService的StartAsync

// 最后执行
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    app.UseRouting();
    app.UseEndpoints();
}

就如同下面的示意图一样,中间的部分就是咱们自定义的 HostService :

x

这就好玩了,说明在应用加载完成所有服务之后,就会在启动的时候开启所有的IHostedService 。 那么是否意味着我们可以在自定义的 IHostedService 使用DI容器中的服务呢,或者说在自定义任务中注入其它类。 答案是:肯定的。

public class DemoHostService : IHostedService
{
    private IMyServiceDemo serviceDemo;
    public DemoHostService(IMyServiceDemo IServiceDemo)
    {
        serviceDemo = IServiceDemo;
    }

    public async Task StartAsync(CancellationToken cancellationToken)
    {
        await Task.Delay(100);
    }
}

就如同上面一样,我们使用了注入的IMyServiceDemo类。但是,请注意!!!!IHostedService 的生命周期为单例级别。所以只能在构造函数中注入同为单例级别的服务。而且就算 IHostedService 的周期为其它级别,比如(Scoped),它其实也无法直接在构造函数中注入非单例级别的服务。

理由是,HostService既然在Configure之前,就证明它目前所在的范围作用域还是在 “根” 级别上,所以当您注入一个非单例级别的类会提示您“无法在根范围获取一个对象”。

所以如果咱们需要获取其它生命周期类型服务的时候,就要使用另外一种方法:

public DemoHostService(IServiceProvider provider)
{
    var serviceDemo = provider.CreateScope()
                              .ServiceProvider
                              .GetService<IMyScpoedService>();
}

上方只是个快捷写法,您在使用过程中一定要注意释放Scope。

在知道了IHostedService 之后,我们可以来想一想我们能够在伴随 Host 启动时,做一些什么事情呢? 比如,我们在应用启动时,可以对EFCore进行自动迁移和播种种子数据等:

public async Task StartAsync(CancellationToken cancellationToken)
{
    using (var scope = _provider.CreateScope())
    {
        var efContext = scope.ServiceProvider.GetService<MyDbCotext>();
        efContext.Database.EnsureCreated();

        // Look for any students.
        if (efContext.Students.Any())
        {
            return; // DB has been seeded
        }
        else
        {
            SeedData(efContext);
        }
    }
}

持续运行的后台服务

那么如果我们要定义一个持续运行的后台任务呢? 比如定时发送邮件等,是否直接在 IHostedServiceStartAsync 中写个死循环呢? 好吧,答案是否定的。 如果这样咱们的Host就启动不起来。 通过查看 .NET Core Host的源代码就知道,它在最后启动的时候做了这样的事情:

_hostedServices = Services.GetService<IEnumerable<IHostedService>>();

foreach (var hostedService in _hostedServices)
{
    // Fire IHostedService.Start
    await hostedService.StartAsync(combinedCancellationToken).ConfigureAwait(false);
}

是的,它用了await关键字,也就是说如果直接写while死循环的话,就会导致一直等待而无法进行下面的操作。所以,我们可以在 IHostedServiceStartAsync 中单独开一个线程来进行循环:

public Task StartAsync(CancellationToken cancellationToken)
{
    new Task(() =>
    {
        while (true)
        {
            // doing
        }
    });
    return Task.CompletedTask;
}

当然,.NET Core 早就想到了这一点,所以为我们提供了一个叫做 BackgroundService 的抽象类,我们只需要在 ExecuteAsync 方法中执行特有的逻辑就可以了:

public class MyBackgroundJob : BackgroundService
{
    protected override Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            SendEmail();
        }
    }
}

总结

IHostedService 接口为在 ASP.NET Core Web 应用程序(在 .NET Core 2.0 及更高版本中)或任何进程/主机(从使用 IHost 的 .NET Core 2.1 开始)中启动后台任务提供了一种便捷方式。 其主要优势在于,当主机本身将要关闭时,可以有机会进行正常取消以清理后台任务的代码。

其实关于后台定时任务,您可能会想到一些成熟的框架,比如Hangfire等。当然,它也为.NET Core版本提供了 IHostedService 的实现,您可以从这里看到它的实现

偷偷告诉您,其实咱们的AspNetCore在启动时进行初始化Configure 等操作也是通过扩展一个IHostedService来实现的,它的具体实现类叫做:GenericWebHostService

所以可以看出 IHostedService 为咱们提供了非常便利的操作,我们可以像累积木一样,往 Host 主机添加我们需要的任务项。就像下面的图一样:

x

好吧,这次废话好像多了些。最后,偷偷说一句:创作不易,点个推荐吧.....

x

【5min+】后台任务的积木。.NetCore中的IHostedService

全文结束