【5min+】後臺任務的積木。.NetCore中的IHostedService

系列介紹

【五分鐘的dotnet】是一個利用您的碎片化時間來學習和豐富.net知識的博文系列。它所包含了.net體系中可能會涉及到的方方面面,好比C#的小細節,AspnetCore,微服務中的.net知識等等。
5min+不是超過5分鐘的意思,"+"是知識的增長。so,它是讓您花費5分鐘如下的時間來提高您的知識儲備量。git

前言

此次終於能夠給你們分享一些AspNet Core方面的東西了😀。雖然本次說起的內容是.NET Core通用,但將以AspNet Core爲例做爲介紹。github

正文

我們開發應用的時候,有時候可能須要創建一些獨立於應用邏輯體自己的後臺任務。好比:定時發送郵件、定時執行腳本這類持續運行的任務,也有驗證數據庫是否建立等只伴隨應用啓動而執行一次的任務。web

在.NET Core 2.0 以後,官方爲咱們提供了一個叫作 IHostedService 的接口,它能夠便於咱們更好的實現託管服務。數據庫

在微軟《.NET 微服務 - 體系結構》教程中,就有說起到關於該接口的描述:c#

x

那麼今天我們就來扒一扒 IHostedService 究竟是一個怎樣的東西,咱們能夠在什麼狀況下使用它。app

前方車速夠快,請抓好扶手。
x框架

IHostService

請注意 IHostedService 是從 .NET Core 提出的,因此能夠看到它並非專門只針對於 AspNet Core。 從.NetCore 3.x 以後,當你們建立一個新的AspNetCore應用的時候,打開默認的 Program.cs 文件,就會發現它和以往的版本已經不同了。async

//如今
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進行了更高層次的抽象,也就意味着如今能支持更多不一樣託管主機的建立方式,將來也將支持更多的類型。果真是一盤很大的棋啊🤫ide

回到今天的主題 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

相關文章
相關標籤/搜索