如何使用 .NET Generic Host for Microsoft.Extensions.Hosting

.NET Generic Host 是 .NET Core 發展出來的基礎建設,可以和其他類型的 .NET 應用程式搭配使用例如背景服務的主控台應用程式,Microsoft.Extensions.Hosting.Host.CreateDefaultBuilder 靜態方法,它來自於  Microsoft.Extensions.Hosting.dll,主要用來提供應用程式一個標準的啟動,包含注入、紀錄、組態,不同的應用程式框架 (HostService) 有不同的預設啟動設定,.NET Generic Host 讓我們的應用程式的生命週期的控制,啟動到結束的撰寫方式統一了。
 

開發環境

  • Rider 2021.3.4
  • Windows 10
  • .Net Fx 4.8 via 新版專案範本 .NET Project SDKs
  • Microsoft.Extensions.Hosting 5.0.0
    Microsoft.Extensions.Hosting 適用於 .NET Fx 4.6.1 以上
Install-Package Microsoft.Extensions.Hosting -Version 5.0.0

 

實例化 IHostBuilder

Microsoft.Extensions.Hosting.Host.CreateDefaultBuilder 靜態方法提供了應用程式預設的啟動設定,只要呼叫就能完成設定,主要的設定如下:

官方文件的重點整理再搭配原始碼,應該會更清楚的知道 Host.CreateDefaultBuilder 內部幫我們做了哪些事。

Host.CreateDefaultBuilder

  • 實例化 HostBuilder
上圖出自原始碼
上圖出自原始碼

 

  • 載入主機組態 (Host configuration):
    • 環境變數前綴為 DOTNET_
      若使用 ConfigureWebHostDefaults,ASPNETCORE_ENVIRONMENT 的值會覆寫 DOTNET_ENVIRONMENT
    • 命令列組態
上圖出自原始碼

 

  • 載入應用程式組態 (ASP.NET Core/Console App):
    • appsettings.js。
    • appsettings.{Environment}.json
    • 應用程式在 環境中執行時的 Development 祕密管理員。
    • 環境變數。
    • 命令列引數。
上圖出自原始碼
  • 新增下列記錄提供者:
    • 主控台
    • 偵錯
    • EventSource
    • EventLog (僅當在 Windows 上執行時)
上圖出自原始碼
  • 當環境為時,可啟用範圍驗證和相依性 驗證 Development 。
上圖出自原始碼

IHostBuilder 擴充方法

以下這些擴充方法用來設定應用程式其他的設定,可多次呼叫,結果會累加(後面蓋掉前面)。

更多的擴充方法請「參考

 

實例化 IHostedService

IHostedService 是用來實現來完成應用程序的"工作",當 Host 啟動後,也就是呼叫 IHostedService.StartAsync,這時它會執行 DI Container 裡已註冊的所有 BackgroundServiceIHostedService 的物件和 BackgroundService.ExecuteAsync 方法

BackgroundService 實作 IHostedServiceBackgroundService 擁有更多控制服務的能力,比如長時間的背景執行作業調用 BackgroundService.ExecuteAsync

預設自動注入以下:

LabHostedService 實作 IHostedService 並開啟 ILogger、 IHostApplicationLifetime、IHostLifetime、IHostEnvironment 注入點,範例如下:

public class LabHostedService : IHostedService
{
    private readonly ILogger _logger;

    public LabHostedService(ILogger<LabHostedService> logger,
                            IHostApplicationLifetime  appLifetime,
                            IHostLifetime             hostLifetime,
                            IHostEnvironment          hostEnvironment)
    {
        this._logger = logger;
        appLifetime.ApplicationStarted.Register(this.OnStarted);
        appLifetime.ApplicationStopping.Register(this.OnStopping);
        appLifetime.ApplicationStopped.Register(this.OnStopped);
        this._logger.LogInformation($"主機環境:"                                           +
                                    $"ApplicationName = {hostEnvironment.ApplicationName}\r\n" +
                                    $"EnvironmentName = {hostEnvironment.EnvironmentName}\r\n" +
                                    $"RootPath = {hostEnvironment.ContentRootPath}\r\n"        +
                                    $"Root File Provider = {hostEnvironment.ContentRootFileProvider}\r\n");
    }
 
    public Task StartAsync(CancellationToken cancellationToken)
    {
        this._logger.LogInformation("1. 調用 Host.StartAsync ");
        return Task.CompletedTask;
    }
    private void OnStarted()
    {
        this._logger.LogInformation("2. 調用 OnStarted");
    }
    private void OnStopping()
    {
        this._logger.LogInformation("3. 調用 OnStopping");
    }

    public Task StopAsync(CancellationToken cancellationToken)
    {
        this._logger.LogInformation("4. 調用 Host.StopAsync");
        return Task.CompletedTask;
    }
 
    private void OnStopped()
    {
        this._logger.LogInformation("5. 調用 OnStopped");
    }
}

 

LabBackgroundService 實作 BackgroundService,多了一個 ExecuteAsync 方法

public class LabBackgroundService : BackgroundService
{
    private readonly ILogger<LabBackgroundService> _logger;
  
    public LabBackgroundService(ILogger<LabBackgroundService> logger,
                                IHostApplicationLifetime      appLifetime,
                                IHostLifetime                 hostLifetime,
                                IHostEnvironment              hostEnvironment)
    {
        this._logger = logger;
        appLifetime.ApplicationStarted.Register(this.OnStarted);
        appLifetime.ApplicationStopping.Register(this.OnStopping);
        appLifetime.ApplicationStopped.Register(this.OnStopped);
        this._logger.LogInformation($"主機環境:"                                                   +
                                    $"ApplicationName = {hostEnvironment.ApplicationName}\r\n" +
                                    $"EnvironmentName = {hostEnvironment.EnvironmentName}\r\n" +
                                    $"RootPath = {hostEnvironment.ContentRootPath}\r\n"        +
                                    $"Root File Provider = {hostEnvironment.ContentRootFileProvider}\r\n");
    }
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            this._logger.LogInformation("Worker running at: {time}", DateTimeOffset.Now);
            await Task.Delay(1000, stoppingToken);
        }
    }
    
    public Task StartAsync(CancellationToken cancellationToken)
    {
        this._logger.LogInformation("1. 調用 Host.StartAsync ");
        return Task.CompletedTask;
    }
    private void OnStarted()
    {
        this._logger.LogInformation("2. 調用 OnStarted");
    }
    private void OnStopping()
    {
        this._logger.LogInformation("3. 調用 OnStopping");
    }

    public Task StopAsync(CancellationToken cancellationToken)
    {
        this._logger.LogInformation("4. 調用 Host.StopAsync");
        return Task.CompletedTask;
    }
 
    private void OnStopped()
    {
        this._logger.LogInformation("5. 調用 OnStopped");
    }
}

 

在 Program.cs 調用以下

  • Host.CreateDefaultBuilder(args):實例化 HostBuilder
  • services.AddHostedService:注入 HostService
internal class Program
{
    private static Task Main(string[] args)
    {
        var host = CreateHostBuilder(args).Build();
        var task = host.RunAsync();
        Console.WriteLine($"{nameof(LabHostedService)} 應用程式已啟動");

        return task;
    }

    private static IHostBuilder CreateHostBuilder(string[] args)
    {
        return Host.CreateDefaultBuilder(args)
                   .ConfigureServices((hostBuilder, services) =>
                                      {
                                          services.AddHostedService<LabHostedService>();
                                          services.AddHostedService<LabBackgroundService>();

                                          Console.WriteLine("注入HostService");
                                      });
    }
}

 

IHostLifetime

IHostLifetime 實現來管理應用程序的生命週期,目前都是由微軟官方實作

runtime/IHostLifetime.cs at main · dotnet/runtime (github.com)

IHostLifetime 有以下兩種方法

public interface IHostLifetime
{
    Task WaitForStartAsync(CancellationToken cancellationToken);
    Task StopAsync(CancellationToken cancellationToken);
}
  • WaitForStartAsync 在 Generic Host 啟動時被調用,可用於啟動偵聽關閉事件或延遲應用程序的啟動,直到發生某些事件為止。
  • StopAsync Generic Host 停止時調用。

IHostLifetime 目前存在三種不同的實現:

以 Windows Service 來講就是按下服務的啟動 / 停止

ConsoleLifetime

.NET Generic Host 啟動時,預設啟動 ConsoleLifetime 來處理 Host 的生命週期。在ConsoleLifetime 內主要監聽四個事件:

  • ApplicationLifetime.ApplicationStarted
  • ApplicationLifetime.ApplicationStopping
  • AppDomain.CurrentDomain.ProcessExit
  • Console.CancelKeyPress

原始碼:runtime/ConsoleLifetime.cs at release/5.0 · dotnet/runtime (github.com)

public Task WaitForStartAsync(CancellationToken cancellationToken)
{
    ... 

    // Attach event handlers for SIGTERM and Ctrl+C
    AppDomain.CurrentDomain.ProcessExit += OnProcessExit;
    Console.CancelKeyPress += OnCancelKeyPress;

    // Console applications start immediately.
    return Task.CompletedTask;
}

 

Sequence diagram for program startup
上圖出自:Introducing IHostLifetime and untangling the Generic Host startup interactions: Exploring ASP.NET Core 3.0 - Part 5 (andrewlock.net)

 

GenericWebHostService

ASP.NET Core 3.1 之後則是使用 GenericWebHostService,預設使用 IHostBuilder.ConfigureWebHostDefaults 擴充方法建立 Host

public class Program
{
    public static void Main(string[] args)
    {
        CreateHostBuilder(args).Build().Run();
    }

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

.NET Generic Host 啟動後,GenericWebHostService.StartAsync 方法會啟動 IServer. StartAsync() 聆聽Http封包,並通過 HostingApplication 管道處理接著往下處理

IHostBuilder.ConfigureWebHostDefaults 擴充方法裡面再去幫我們註冊 GenericWebHostService 到 DI Container

public static IHostBuilder ConfigureWebHost(this IHostBuilder builder, Action<IWebHostBuilder> configure, Action<WebHostBuilderOptions> configureWebHostBuilder)
{
    if (configure is null)
    {
        throw new ArgumentNullException(nameof(configure));
    }

    if (configureWebHostBuilder is null)
    {
        throw new ArgumentNullException(nameof(configureWebHostBuilder));
    }

    var webHostBuilderOptions = new WebHostBuilderOptions();
    configureWebHostBuilder(webHostBuilderOptions);
    var webhostBuilder = new GenericWebHostBuilder(builder, webHostBuilderOptions);
    configure(webhostBuilder);
    builder.ConfigureServices((context, services) => services.AddHostedService<GenericWebHostService>());
    return builder;
}

 

啟動循序圖

Sequence diagram for Host.StartAsync()
上圖出自:Introducing IHostLifetime and untangling the Generic Host startup interactions: Exploring ASP.NET Core 3.0 - Part 5 (andrewlock.net)

關閉循序圖

Sequence diagram for application shut down when Ctrl+C is clicked
上圖出自:Introducing IHostLifetime and untangling the Generic Host startup interactions: Exploring ASP.NET Core 3.0 - Part 5 (andrewlock.net)

 

IHostApplicationLifetime

當主機啟動後,由開發人員撰寫的控制點,有以下控制點

註冊寫法如下:

public LabHostedService(ILogger<LabHostedService> logger,
                        IHostApplicationLifetime  appLifetime,
                        IHostLifetime             hostLifetime,
                        IHostEnvironment          hostEnvironment)
{
    this._logger = logger;
    appLifetime.ApplicationStarted.Register(this.OnStarted);
}
private void OnStarted()
{
    this._logger.LogInformation("2. 調用 OnStarted");
}

 

IHostEnvironment

主機環境資訊

Host Configuration

用來設定 IHostEnvironment 物件的屬性,透過 ConfigureAppConfiguration 設定

當呼叫 ConfigureAppConfiguration 方法時,configureDelegate 參數(定義為 Action<HostBuilderContext, IConfigurationBuilder>)

呼叫 ConfigureAppConfiguration 之後,將覆蓋原本的 HostBuilderContext.Configuration

 

IHostEnvironment.EnvironmentName 為例,預設是使用 DOTNET_ 開頭的環境變數 DOTNET_ENVIRONMENT,取 DOTNET_ 前綴詞得到 ENVIRONMENT,ENVIRONMENT 則是 Configuration Key,它代表著 IHostEnvironment.EnvironmentName 的結果。

在 ASP.NET Core 會被改成以 ASPDOTNET_ 開頭的環境變數,將會覆蓋 DOTNET_ 開頭的環境變數

 

在 appsettings.json增加 Environment 節點

{
  "ConnectionStrings": {
    "DefaultConnectionString": "Server=(localdb)\\mssqllocaldb;Database=EFGetStarted.ConsoleApp.NewDb;Trusted_Connection=True;"
  },
  "Player": {
    "AppId": "player1",
    "Key": "1234567890"
  },
  "Environment": "Development"
}

 

替換主機組態,範例如下:

[TestMethod]
public void 設定主機組態()
{
    var builder = Host.CreateDefaultBuilder()
                      .ConfigureHostConfiguration(config=>
                        {
                            config.AddJsonFile("appsettings.json", false, true);
                        })
                      ;          
    
    var host            = builder.Build();
    var environment       = host.Services.GetRequiredService<IHostEnvironment>();
    Console.WriteLine($"EnvironmentName={environment.EnvironmentName}");
}

 

執行結果如下:

EnvironmentName = Development

 

範例位置

sample.dotblog/Host/Lab.MsHost/ConsoleAppNetFx48 at master · yaochangyu/sample.dotblog (github.com)

若有謬誤,煩請告知,新手發帖請多包涵


Microsoft MVP Award 2010~2017 C# 第四季
Microsoft MVP Award 2018~2022 .NET

Image result for microsoft+mvp+logo