14
Help us understand the problem. What are the problem?

posted at

updated at

.NETの汎用ホストの公式Docをやさしくしました

公式ドキュメントが難しかったので、それを易しく解説していきます。

テンプレートからコンソールアプリを作成し、そこにホストを実装します。
汎用ホストとは何でしょうか?
公式ドキュメントによると以下のようにあります。

''ホスト'' とは、次のようなアプリのリソースと有効期間機能をカプセル化するオブジェクトです。

依存関係の挿入 (DI)
ログの記録
構成
アプリのシャットダウン
IHostedService の実装

これだけではあまりピンとこない方もいると思うので、デフォルトの設定のホストをコンソールアプリに実装して実行した出力例をご覧ください。

bash-3.2$ ./ConsoleApp
info: Microsoft.Hosting.Lifetime[0]
      Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
      Hosting environment: Production
info: Microsoft.Hosting.Lifetime[0]
      Content root path: /Users/ken/repos/ConsoleApp

#----- Ctrl + Cを押すと終了 --------

^Cinfo: Microsoft.Hosting.Lifetime[0]
      Application is shutting down...

デフォルトではCtrl+Cで強制停止するまでアプリは起動し続け、特に何もすることはありません。ここに任意の処理を差し込んだり、それが終わったらアプリを終了させる処理をさせたりして目的のアプリを作っていきます。

2章ではホストを構築するためのプロジェクトの作成した、3章から本題に入ります。
4章以降で、公式ドキュメントが説明する 「IHostedService の実装」等の機能を順次解説していきます。

1. 必要なツール

1.1. 開発ツール

アプリを作成するために必要なツールになります。

ツール 用途 インストール先 メモ
.NET SDK アプリのビルド https://docs.microsoft.com/ja-jp/dotnet/core/install/windows?tabs=net60
筆者の開発環境はM1 Macなのでうまくいくのか心配でしたが、brew caskでインストールできました。(2022/8/29)
汎用ホストのアプリは以下の.NET系の環境でも動きますが、それらを統合した.NETで作っておけば間違い無いでしょう。執筆時のLTSバージョンは6でした。
  • .NET Framwark
  • .NET Core
  • Xamarin
テキストエディタ コーディング お好きのものを。
迷ったらVS Code
筆者はemacsです

1.2 実行環境

ビルドしたアプリをデプロイするマシンに以下をインストールします。

ツール 用途 インストール先 メモ
.NETランタイム アプリの実行 SDKのバージョンと同じものをインストール。
https://docs.microsoft.com/ja-jp/dotnet/core/install/windows?tabs=net60
以下の場合は必要ありません。
  • .NETランタイム無しでも実行できるアプリにビルドする場合(2.2章参照)。
  • 開発環境と同じバージョンのSDKがインストールされている場合

2. 準備

2.1. テンプレートからコンソールアプリのプロジェクトを作成

任意のシェルのコマンドラインから、コンソールアプリのテンプレートを選択してプロジェクトを作成します。

dotnet new console -o ConsoleApp

ConsoleAppはプロジェクト名です。お好きな名前で結構です。
デフォルトでは実行ファイルはこの名前で作成されます。

以下の構造のディレクトリが作成されます。

ConsoleApp
├── ConsoleApp.csproj
├── Program.cs
└── obj

2.2. ビルドアーティファクトのパッケージング

ビルドしたファイルを1つの実行ファイルにパッケージングする設定です。

2.2.1. プロジェクトファイルの変更

以下を参照してプロジェクトファイル(ConsoleApp.csproj)を変更します。

ConsoleApp.csproj
  <Project Sdk="Microsoft.NET.Sdk">

    <PropertyGroup>
      <OutputType>Exe</OutputType>
      <TargetFramework>net6.0</TargetFramework>
      <ImplicitUsings>enable</ImplicitUsings>
      <Nullable>enable</Nullable>
+     <PublishSingleFile>true</PublishSingleFile>
+     <SelfContained>true</SelfContained>
+     <DebugType>embedded</DebugType>
    </PropertyGroup>

  </Project>

SelfContainedをtrueにすればその中に.NETランタイムも含めることができて、.NETランタイムをインストールしていないマシンでも実行できるようになります。必要ない場合はfalseに。(ちょっと違う気がしますが、サーブレットコンテナとアーティファクトをまとめてパッケージングしたjarのような感じ(spring boot))

2.2.2. ビルドと実行

アプリをビルドして単一のファイルになるか確認してみます。
プロジェクトのルートディレクトリで、以下のコマンドを実行します。

dotnet publish -r <ターゲットとする OS と CPU の種類> -o <ビルドアーティファクトの出力先>

-r の引数には、ターゲットとする OS と CPU の種類を指定します。
x64CPUで動くWindows OSのマシンの場合は、win-x64です。

以下M1 Macで動く実行ファイルにビルドして、アプリを実行するまでの例です。

bash-3.2$ dotnet publish -r osx.12-arm64 -o dist
MSBuild version 17.3.0+92e077650 for .NET
  復元対象のプロジェクトを決定しています...
  復元対象のすべてのプロジェクトは最新です。
  ConsoleApp -> /Users/ken/repos/ConsoleApp/bin/Debug/net6.0/osx.12-arm64/ConsoleApp.dll
  ConsoleApp -> /Users/ken/repos/ConsoleApp/dist/
bash-3.2$ ./dist/ConsoleApp
Hello, World!

2.3 パッケージマネジャの設定をする

nuget.configを作成します。

ConsoleApp
├── ConsoleApp.csproj
├── nuget.config     <--- 新規作成
├── Program.cs
├── bin
├── dist
│   └── ConsoleApp.exe
├── obj
└── packages
nuget.config
<?xml version="1.0" encoding="utf-8"?>
<configuration>
    <config>
        <add key="globalPackagesFolder" value="./packages" />
    </config>
    <packageRestore>
        <add key="enabled" value="True" />
        <add key="automatic" value="True" />
    </packageRestore>
    <packageSources>
        <add key="NuGet official package source" value="https://api.nuget.org/v3/index.json" />
    </packageSources>
    <packageManagement>
        <add key="format" value="1" />
        <add key="disabled" value="False" />
    </packageManagement>
</configuration>

以下の設定をしました。

  • globalPackagesFolder: nugetが依存パッケージをダウンロードするディレクトリを指定。筆者はグローバルの領域を汚したくないためこうしています。
  • packageRestore: ビルド時に依存パッケージが上記で指定したディレクトリになければ、ダウンロードするように設定します。
  • packageSources: パッケージリポジトリを指定します。
  • packageManagement: プロジェクトで使用するパッケージの指定を、プロジェクトファイル(csproj拡張子のファイル)のPackageReferenceでするように設定します

3. デフォルトの設定のホストを起動するアプリを作成

コンソールアプリでデフォルトの設定のホストを起動させます。
このページの冒頭で紹介した出力になるアプリを作成します。

テンプレートから作成したコンソールアプリから変更するのは、以下2つのファイルです。

  • ConsoleApp.csproj
  • Program.cs

3.1. 依存パッケージを解決する

ホスト構築に使うクラスなどは名前空間:Microsoft.Extensions.Hostingで定義されており、これを含むパッケージ:Microsoft.Extensions.Hostingをプロジェクトにインポートする必要があります。

PackageReferenceで、パッケージ名とバージョンを指定します。

ConsoleApp.csproj
     </PropertyGroup>

+    <ItemGroup>
+      <PackageReference Include="Microsoft.Extensions.Hosting" Version="6.0.1" />
+    </ItemGroup>

    </Project>

3.2. デフォルトのビルダーでホストを作成する

Program.csを以下に置き換えます。

Program.cs
using Microsoft.Extensions.Hosting;

//Step1 ビルダーを作成
IHostBuilder builder = Host.CreateDefaultBuilder();

//Step2 ホストをビルド
IHost host = builder.Build();

//Step3 ホストを起動
host.Run();

5以上のバージョンの.NETのC#ではmainメソッドが省略できるになったようなので、活用しています。

ホストのオブジェクト(IHostインターフェース実装)は、ビルダーのオブジェクトから生成します。
ホストに実行される処理は、ホストをビルドする前のビルダーから追加設定していきます。(4章で解説)

4. 作成したサービスをホストさせる

ホストに実行させる処理を記述するコンポーネントをサービスと言います。
デフォルトのビルダーでビルドしたホストもすでにサービスをホストしており、3章の紹介したアプリを実現しています。
本章ではサービスを新規作成し、ホストにそれを追加でホストさせます。

4.1. サービスの作成

ConsoleApp                     <--カレントディレクトリ
├── ConsoleApp.csproj
├── ExampleHostedService.cs    <----- 新規作成
├── nuget.config
├── Program.cs
├── bin
├── dist
│   └── ConsoleApp.exe
├── obj
└── packages

ExampleHostedService.csにサービスを記述します。

  1. 名前空間:Test.Servicesを定義
  2. 1の名前空間に、クラス:ExampleHostedServiceを定義
  3. 2のクラスに、実装したインターフェースの以下のメソッドを記述
ExampleHostedService.cs
using Microsoft.Extensions.Hosting;

//クラスを定義する名前空間
namespace Test.Services;

//サービスのクラス
public sealed class ExampleHostedService : IHostedService
{
    // サービスが開始するときにトリガーされるメソッド
    public Task StartAsync(CancellationToken cancellationToken)
    {
        Console.WriteLine("StartAsync has been called.");
        return Task.CompletedTask;
    }

    // サービスが終了するときにトリガーされるメソッド
    public Task StopAsync(CancellationToken cancellationToken)
    {
        Console.WriteLine("StopAsync has been called.");
        return Task.CompletedTask;
    }
}

Microsoftの公式の汎用ホストの説明の以下がクリアになったのではないでしょうか。

ホストが起動すると、サービス コンテナーのホステッド サービスのコレクションに登録されている IHostedService の各実装で IHostedService.StartAsync が呼び出されます。

公式の解説では、別途定義した他のメソッドもトリガされるようにしていますね。そのために、インターフェース: Microsoft.Extensions.Hosting.IHostApplicationLifetimeをインジェクションして、対応するプロパティにそれらのメソッドを登録しています。その他にも、文字の出力にロガーを使うなどちゃんとしてますね。本章の説明では、複雑になるので割愛しました。

4.2. ホストにサービスを追加する

ここで新たに使用するMicrosoft.Extensions.DependencyInjection.IServiceCollectionインターフェースのパッケージは、3.1章でインポートしたMicrosoft.Extensions.Hostingのパッケージの依存関係にあるので、すでにインポートされています。明示的にPackageReferenceに追加する必要はありません。

Program.cs
  using Microsoft.Extensions.Hosting;
+ using Microsoft.Extensions.DependencyInjection; //IServiceCollectionインターフェースを定義する名前空間
+ using Test.Services;  //4.1章で作成したクラスの名前空間

  IHostBuilder builder = Host.CreateDefaultBuilder();

  //ビルダーを通して、サービスコンテナ(サービスを格納するIoCコンテナ)に追加のサービスを登録します。
+ builder = builder.ConfigureServices(delegate(HostBuilderContext hostBuilderContext, IServiceCollection serviceCollection){ AddServiceToHost(hostBuilderContext,  serviceCollection); } );

  IHost host = builder.Build();
  host.Run();

  //4 ビルダーのConfigureServicesメソッドにデリゲートを使って渡すメソッド(任意の名前)
+ static IServiceCollection AddServiceToHost(HostBuilderContext hostBuilderContext, IServiceCollection ServiceCollection){
+     return ServiceCollection.AddHostedService<ExampleHostedService>();
+ }

これをメソッドチェーンを使って必要ない変数を省略し、デリゲートをラムダ式で記述すると、公式Docによる解説のようにかっこよくワンライナーで書けます。

Program.cs
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.DependencyInjection;
using Test.Services;

Host.CreateDefaultBuilder()
    .ConfigureServices((hostBuilderContext,serviceCollection) => serviceCollection.AddHostedService<ExampleHostedService>())
    .Build()
    .Run();

アプリを再ビルドして起動するとサービスが開始と終了中に4.1章で設定した文字列が出力されます。

bash-3.2$ dotnet publish -r osx.12-arm64 -o dist
MSBuild version 17.3.0+92e077650 for .NET
  復元対象のプロジェクトを決定しています...
  復元対象のすべてのプロジェクトは最新です。
  ConsoleApp -> /Users/ken/repos/ConsoleApp/bin/Debug/net6.0/osx.12-arm64/ConsoleApp.dll
  ConsoleApp -> /Users/ken/repos/ConsoleApp/dist/
bash-3.2$ ./dist/ConsoleApp
 StartAsync has been called.
info: Microsoft.Hosting.Lifetime[0]
      Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
      Hosting environment: Production
info: Microsoft.Hosting.Lifetime[0]
      Content root path: /Users/ken/repos/ConsoleApp
^Cinfo: Microsoft.Hosting.Lifetime[0]
      Application is shutting down...
StopAsync has been called.

5. バックグラウンドタスクのサービスをホストさせる

本章ではバックグラウンドタスクを定義してサービスコンテナに登録します。
筆者はこのようなケースで使いました。

5.1. サービスの作成

ConsoleApp                               <--カレントディレクトリ
├── ConsoleApp.csproj
├── ExampleBackgroundHostedService.cs    <----- 新規作成
├── ExampleHostedService.cs
├── nuget.config
├── Program.cs
├── bin
├── dist
│   └── ConsoleApp.exe
├── obj
└── packages

ExampleBackgroundHostedService.csにサービスを記述します。

  1. 名前空間:Test.Servicesを定義
  2. 1の名前空間に、クラス:ExampleBackgroundHostedServiceを定義
  3. 2のクラスで、継承したクラスのExecuteAsyncメソッドをオーバーライド
ExampleBackgroundHostedService.cs
using Microsoft.Extensions.Hosting;

namespace Test.Services;

public sealed class ExampleBackgroundHostedService : BackgroundService
{
    
    //BackgroundServiceのExecuteAsyncメソッドは、オーバーライドが必須。サービスが開始するときにトリガーされる
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            Console.WriteLine(DateTimeOffset.Now);
            await Task.Delay(1000, stoppingToken);
        }
    }
}

起動したアプリにCtrl+Cをインプットすると、シャットダウンをトリガーするトークンがサービスに送られます。
それを受信するまで、1秒ごとに時間を出力するプログラムです。

5.2. 作成したサービスをホストさせる

Program.cs
  using Microsoft.Extensions.Hosting;
  using Microsoft.Extensions.DependencyInjection;
  using Test.Services;

+ await
  Host.CreateDefaultBuilder()
      .ConfigureServices((hostBuilderContext,serviceCollection) => serviceCollection.AddHostedService<ExampleHostedService>())
+     .ConfigureServices((hostBuilderContext,serviceCollection) => serviceCollection.AddHostedService<ExampleBackgroundHostedService>())
      .Build()
-     .Run();
+     .RunAsync();
  • 4.2章のサービスに加えて、5.1章で作成したサービスをサービスコンテナに登録します。
  • 新たに登録したサービスは非同期処理をするので、ホストはメソッド:RunAsyncで起動します

再ビルドしてアプリを起動すると以下の出力となります。

bash-3.2$ dotnet publish -r osx.12-arm64 -o dist
MSBuild version 17.3.0+92e077650 for .NET
  復元対象のプロジェクトを決定しています...
  復元対象のすべてのプロジェクトは最新です。
  ConsoleApp -> /Users/ken/repos/ConsoleApp/bin/Debug/net6.0/osx.12-arm64/ConsoleApp.dll
  ConsoleApp -> /Users/ken/repos/ConsoleApp/dist/
bash-3.2$ ./dist/ConsoleApp
 StartAsync has been called.
2022/08/31 19:48:43 +09:00
info: Microsoft.Hosting.Lifetime[0]
      Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
      Hosting environment: Production
info: Microsoft.Hosting.Lifetime[0]
      Content root path: /Users/ken/repos/ConsoleApp
2022/08/31 19:48:44 +09:00
2022/08/31 19:48:45 +09:00
2022/08/31 19:48:46 +09:00
2022/08/31 19:48:47 +09:00
^Cinfo: Microsoft.Hosting.Lifetime[0]
      Application is shutting down...
StopAsync has been called.

5.3. worker テンプレート

2章ではコンソールアプリのテンプレートを使ってプロジェクトを作成しましたが、Workerアプリのテンプレートも用意されています。このテンプレートでプロジェクトを作成することにより、5章までの構成で始めることができます。

dotnet new worker -o "プロジェクトを展開するディレクトリパス"

なおこのテンプレートで作ったプロジェクトでは、Microsoft.Extensions.HostingやMicrosoft.Extensions.DependencyInjection等の名前空間を明示的にusingディレクティブで書かなくても、その名前空間以下のクラス等にパスが通りました。
暗黙的に宣言されているようです。

6. appsettings.jsonの設定をホストに反映する

3章以降はデフォルトの構成でビルダーを作成しました。
予め設定した内容でビルダーを作成する方法の1つは、プロジェクトのルートディレクトリに配置したappsettings.jsonに設定を記述することです。

ConsoleApp                               <--カレントディレクトリ
├── appsettings.json                      <----- 新規作成
├── ConsoleApp.csproj
├── ExampleBackgroundHostedService.cs
├── ExampleHostedService.cs
├── nuget.config
├── Program.cs
├── bin
├── dist
│   └── ConsoleApp.exe
├── obj
└── packages

6.1 任意のkey-value形式のパラメータを渡す

6.1.1 appsettings.jsonへの設定

appsettings.json
{
  "Parameters": {
    "param1":  "りんご",
    "param2":  "ゴリラ",
    "param3":  "ラッパ"
  }
}

6.1.2 設定したパラメータを参照する

サービスでappsettings.jsonで指定したパラメータを参照するようにします。

ここで新たに使用する以下2つのインターフェースのパッケージは、3.1章でインポートしたMicrosoft.Extensions.Hostingのパッケージの依存関係にあるので、すでにインポートされています。明示的にPackageReferenceに追加する必要はありません。

ExampleHostedService.cs
  using Microsoft.Extensions.Hosting;
+ using Microsoft.Extensions.Configuration;
  
  namespace Test.Services;
  
  public sealed class ExampleHostedService : IHostedService{

+    private readonly IConfiguration _configuration;

     //IConfigurationインターフェースをコンストラクタでインジェクションする
     //ここにappsettings.jsonで指定したパラメータが入っている
+    public  ExampleHostedService(IConfiguration configuration){
+       _configuration = configuration;
+    }
     public Task StartAsync(CancellationToken cancellationToken)
     {
        Console.WriteLine("StartAsync has been called.");

        //パラメータを参照。jsonのセクションを指定します。
+       IConfigurationSection parameters = _configuration.GetSection("Parameters");
+       Console.WriteLine(parameters.GetValue<String>("param1"));
+       Console.WriteLine(parameters.GetValue<String>("param2"));
+       Console.WriteLine(parameters.GetValue<String>("param3"));
        return Task.CompletedTask;
     }
  
      public Task StopAsync(CancellationToken cancellationToken){
         Console.WriteLine("StopAsync has been called.");
         return Task.CompletedTask;
      }
  }

本筋からはそれますが、コンソールの出力のうち5章で作成したバックグラウンドタスクのサービスのものが邪魔なので無効にしておきます。

Program.cs
  using Microsoft.Extensions.Hosting;
  using Microsoft.Extensions.DependencyInjection;
  using Test.Services;

  await
  Host.CreateDefaultBuilder()
      .ConfigureServices((hostBuilderContext,serviceCollection) => serviceCollection.AddHostedService<ExampleHostedService>())
-     .ConfigureServices((hostBuilderContext,serviceCollection) => serviceCollection.AddHostedService<ExampleBackgroundHostedService>())
      .Build()
      .RunAsync();

以下再ビルドしてアプリを実行したときの出力です。

bash-3.2$ dotnet publish -r osx.12-arm64 -o dist
MSBuild version 17.3.0+92e077650 for .NET
  復元対象のプロジェクトを決定しています...
  復元対象のすべてのプロジェクトは最新です。
  ConsoleApp -> /Users/ken/repos/ConsoleApp/bin/Debug/net6.0/osx.12-arm64/ConsoleApp.dll
  ConsoleApp -> /Users/ken/repos/ConsoleApp/dist/
bash-3.2$ ./dist/ConsoleApp
StartAsync has been called.
りんご
ゴリラ
ラッパ
2022/08/31 20:00:43 +09:00
info: Microsoft.Hosting.Lifetime[0]
      Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
      Hosting environment: Production
info: Microsoft.Hosting.Lifetime[0]
      Content root path: /Users/ken/repos/ConsoleApp
^Cinfo: Microsoft.Hosting.Lifetime[0]
      Application is shutting down...
StopAsync has been called.

6.2 デフォルトのロガーの設定を変更する

5章までのアプリの実行結果からコンソールに出力されたログは、infoレベルのみです。
デフォルト設定のホストのロガでinfoレベル以上を出力する設定なのか、debugレベルのログがないのか?

6.2.1 サービスにデバッグログを追加する

Debugレベルのログをサービスに追加します。

ここで新たに使用する以下のインターフェースのパッケージは、3.1章でインポートしたMicrosoft.Extensions.Hostingのパッケージの依存関係にあるので、すでにインポートされています。明示的にPackageReferenceに追加する必要はありません。

ExampleHostedService.cs
  using Microsoft.Extensions.Hosting;
  using Microsoft.Extensions.Configuration;
+ using Microsoft.Extensions.Logging;
  
  namespace Test.Services;
  
  public sealed class ExampleHostedService : IHostedService{

      private readonly IConfiguration _configuration;

-     public  ExampleHostedService(IConfiguration configuration){
      //ILoggerインターフェースをコンストラクでインジェクションする
+     public  ExampleHostedService(IConfiguration configuration, ILogger<ExampleHostedService> logger){
          _configuration = configuration;
+         _logger = logger;
      }

      public Task StartAsync(CancellationToken cancellationToken){
-         Console.WriteLine("StartAsync has been called.");
          //上記の出力をログとしての出力に変更する。
+         _logger.LogDebug("StartAsync has been called.");
          IConfigurationSection parameters = _configuration.GetSection("Parameters");
          Console.WriteLine(parameters.GetValue<String>("param1"));
          Console.WriteLine(parameters.GetValue<String>("param2"));
          Console.WriteLine(parameters.GetValue<String>("param3"));
          return Task.CompletedTask;
     }
  
      public Task StopAsync(CancellationToken cancellationToken){
-         Console.WriteLine("StopAsync has been called.");
          //上記の出力をログとしての出力に変更する。
+         _logger.LogDebug("StopAsync has been called.");
          return Task.CompletedTask;
      }
  }

アプリを再ビルドして、実行してみます。

bash-3.2$ dotnet publish -r osx.12-arm64 -o dist
MSBuild version 17.3.0+92e077650 for .NET
  復元対象のプロジェクトを決定しています...
  復元対象のすべてのプロジェクトは最新です。
  ConsoleApp -> /Users/ken/repos/ConsoleApp/bin/Debug/net6.0/osx.12-arm64/ConsoleApp.dll
  ConsoleApp -> /Users/ken/repos/ConsoleApp/dist/
bash-3.2$ ./dist/ConsoleApp
りんご
ゴリラ
ラッパ
info: Microsoft.Hosting.Lifetime[0]
      Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
      Hosting environment: Production
info: Microsoft.Hosting.Lifetime[0]
      Content root path: /Users/ken/repos/ConsoleApp
^Cinfo: Microsoft.Hosting.Lifetime[0]
      Application is shutting down...

おや、、追加したdebugレベルのログがコンソールに出力されませんね。
デフォルトの設定のホストのロガーは、出力をinfo以上のログに制限していたようです。

6.2.2 ロガーの設定を変更する

設定されているログプロバイダーに定義されているすべてのロガーの出力レベルをDebug以上に変更します。

appsettings.json
  {
+   "Logging": {
+     "LogLevel": {
+       "Default": "Debug",
+     }
+   },
    "Parameters": {
      "param1":  "りんご",
      "param2":  "ゴリラ",
      "param3":  "ラッパ"
    }
  }

アプリを再ビルドして起動すると、コンソールへDebugレベルのログが出力されます。
デフォルトの設定のホストにはDebugレベルのログが無いわけではなかったようですね。
6.1章で書いたものに加えて、Microsoft.Extensions.Hosting.Internal.Hostのdebugログが確認できます。

bash-3.2$ dotnet publish -r osx.12-arm64 -o dist
MSBuild version 17.3.0+92e077650 for .NET
  復元対象のプロジェクトを決定しています...
  復元対象のすべてのプロジェクトは最新です。
  ConsoleApp -> /Users/ken/repos/ConsoleApp/bin/Debug/net6.0/osx.12-arm64/ConsoleApp.dll
  ConsoleApp -> /Users/ken/repos/ConsoleApp/dist/
bash-3.2$ ./dist/ConsoleApp
dbug: Microsoft.Extensions.Hosting.Internal.Host[1]
      Hosting starting
dbug: Test.Services.ExampleHostedService[0]
      StartAsync has been called.
りんご
ゴリラ
ラッパ
info: Microsoft.Hosting.Lifetime[0]
      Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
      Hosting environment: Production
info: Microsoft.Hosting.Lifetime[0]
      Content root path: /Users/ken/repos/ConsoleApp
dbug: Microsoft.Extensions.Hosting.Internal.Host[2]
      Hosting started
^Cinfo: Microsoft.Hosting.Lifetime[0]
      Application is shutting down...
dbug: Microsoft.Extensions.Hosting.Internal.Host[3]
      Hosting stopping
dbug: Test.Services.ExampleHostedService[0]
      StopAsync has been called.
dbug: Microsoft.Extensions.Hosting.Internal.Host[4]
      Hosting stopped

7. 任意のjsonファイルの設定をホストに反映する

6章ではappsettings.jsonにホストへの設定を記述しましたが、追加で設定ファイルを読み込ませることもできます。

7.1 任意のjsonファイルの設定をホストに反映する

任意の名前で設定ファイルを追加します。

ConsoleApp                                <--カレントディレクトリ
├── appsettings.json
├── appsetting2.json                      <----- 新規作成
├── ConsoleApp.csproj
├── ExampleBackgroundHostedService.cs
├── ExampleHostedService.cs
├── nuget.config
├── Program.cs
├── bin
├── dist
│   └── ConsoleApp.exe
├── obj
└── packages
appsetting2.json
{
  "Parameters": {
    "param3":  "ライオン"
  }
}

ホストに作成した追加の設定ファイルを読み込ませます。

Program.cs
  using Microsoft.Extensions.Hosting;
  using Microsoft.Extensions.DependencyInjection;
+ using Microsoft.Extensions.Configuration;
  using Test.Services;

  await
  Host.CreateDefaultBuilder()
+     .ConfigureAppConfiguration((hostContext, config) =>
+     {
+          config.AddJsonFile("appsetting2.json", optional: true);
+     })
      .ConfigureServices(( hostBuilderContext,serviceCollection) =>  serviceCollection.AddHostedService<ExampleHostedService>())
      .Build()
      .RunAsync();

再ビルドしてアプリを実行してみます。
ラッパがライオンに変わりました。appsettings.jsonの内容が上書きされています。

bash-3.2$ dotnet publish -r osx.12-arm64 -o dist
MSBuild version 17.3.0+92e077650 for .NET
  復元対象のプロジェクトを決定しています...
  復元対象のすべてのプロジェクトは最新です。
  ConsoleApp -> /Users/ken/repos/ConsoleApp/bin/Debug/net6.0/osx.12-arm64/ConsoleApp.dll
  ConsoleApp -> /Users/ken/repos/ConsoleApp/dist/
bash-3.2$ ./dist/ConsoleApp
dbug: Microsoft.Extensions.Hosting.Internal.Host[1]
      Hosting starting
dbug: Test.Services.ExampleHostedService[0]
      StartAsync has been called.
りんご
ゴリラ
ライオン
info: Microsoft.Hosting.Lifetime[0]
      Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
      Hosting environment: Production
info: Microsoft.Hosting.Lifetime[0]
      Content root path: /Users/ken/repos/ConsoleApp
dbug: Microsoft.Extensions.Hosting.Internal.Host[2]
      Hosting started
^Cinfo: Microsoft.Hosting.Lifetime[0]
      Application is shutting down...
dbug: Microsoft.Extensions.Hosting.Internal.Host[3]
      Hosting stopping
dbug: Test.Services.ExampleHostedService[0]
      StopAsync has been called.
dbug: Microsoft.Extensions.Hosting.Internal.Host[4]
      Hosting stopped

7.2 jsonファイルの配置先を変更する

appsetting.jsonなどのホストの設定ファイルは、デフォルトのホストではアプリを起動したユーザーのカレントディレクトリのものが読まれるようになっています。

ディレクトリを移動すると、appsetting.json等が読み込まれず6章以前の設定に戻っていしまいます。

bash-3.2$ cd dist/
bash-3.2$ pwd
/Users/ken/repos/ConsoleApp/dist
bash-3.2$ ./ConsoleApp
bash-3.2$ ./ConsoleApp



info: Microsoft.Hosting.Lifetime[0]
      Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
      Hosting environment: Production
info: Microsoft.Hosting.Lifetime[0]
      Content root path: /Users/ken/repos/ConsoleApp/dist
^Cinfo: Microsoft.Hosting.Lifetime[0]
      Application is shutting down...

空行はappsettings.jsonが読み込まれないがため、ExampleHostedService.csでnullを出力しているためです。

実行ファイルと同じディレクトリに配置したものが読まれるようにしておくと便利なのではないでしょうか?このように配置して運用することが多いと思われます。

PS C:\ProgramData\ConsoleApp> dir

   ディレクトリ: C:\ProgramData\ConsoleApp

Mode       LastWriteTime     Length    Name
------     -------------     ------    ----
-a----     2022/09/01 14:00  69228308  ConsoleApp.exe
-a----     2022/09/01 14:00  69228308  appsettings.json

ホストに設定ファイルの配置先を設定します。

Program.cs
  using Microsoft.Extensions.Hosting;
  using Microsoft.Extensions.DependencyInjection;
  using Microsoft.Extensions.Configuration;
  using Test.Services;
+ using System.Diagnostics;

  await
  Host.CreateDefaultBuilder()
      .ConfigureAppConfiguration((hostContext, config) =>
      {
+          config.SetBasePath(Path.GetDirectoryName(Process.GetCurrentProcess().MainModule.FileName));
           config.AddJsonFile("appsetting2.json", optional: true);
      })
      .ConfigureServices(( hostBuilderContext,serviceCollection) =>  serviceCollection.AddHostedService<ExampleHostedService>())
      .Build()
      .RunAsync();

設定ファイルを移動します。

ConsoleApp                    
├── ConsoleApp.csproj
├── ExampleBackgroundHostedService.cs
├── ExampleHostedService.cs
├── nuget.config
├── Program.cs
├── bin
├── dist
│   ├── appsettings.json   <--移動
│   ├── appsetting2.json   <--移動
│   └── ConsoleApp.exe
├── obj
└── packages

何処のディレクトリでアプリを実行しても7章と同じ出力になります。

8. ログプロバイダーを変更する

今までのアプリのコンソール出力を見てきたとおり、ホストにはすでに組み込みのログプロバイダがあります。しかし、このログプロバイダはファイルに出力できません。このままでは使いにくいので変更します。

こちらにサードパーティのログプロバイダがいくつか紹介されていました。
このうちのNLogの実装を紹介していきます。

8.1 デフォルトのログプロバイダを無効にする

組み込みのログプロバイダーが必要ない場合は無効にします。

Program.cs
  using Microsoft.Extensions.Hosting;
  using Microsoft.Extensions.DependencyInjection;
  using Microsoft.Extensions.Configuration;
+ using Microsoft.Extensions.Logging;
  using Test.Services;
  using System.Diagnostics;

  await
  Host.CreateDefaultBuilder()
      .ConfigureAppConfiguration((hostContext, config) =>
      {
           config.SetBasePath(Path.GetDirectoryName(Process.GetCurrentProcess().MainModule.FileName));
           config.AddJsonFile("appsetting2.json", optional: true);
      })
+     .ConfigureLogging((hostCotext,logging) =>
+     {
+          logging.ClearProviders();
+     } )
      .ConfigureServices(( hostBuilderContext,serviceCollection) =>  serviceCollection.AddHostedService<ExampleHostedService>())
      .Build()
      .RunAsync();

再ビルドしてアプリを実行すると、コンソールにログがすべて出力されなくなっています。

bash-3.2$ dotnet publish -r osx.12-arm64 -o dist
MSBuild version 17.3.0+92e077650 for .NET
  復元対象のプロジェクトを決定しています...
  復元対象のすべてのプロジェクトは最新です。
  ConsoleApp -> /Users/ken/repos/ConsoleApp/bin/Debug/net6.0/osx.12-arm64/ConsoleApp.dll
  ConsoleApp -> /Users/ken/repos/ConsoleApp/dist/
bash-3.2$ ./dist/ConsoleApp
りんご
ゴリラ
ライオン
^C

8.2. NLogをホストのログプロバイダに設定する

8.2.1. NLogの設定ファイルを作成する

以下の出力を設定します。

  • すべてのロガーのInfoレベル以上のログをコンソールに出力
  • すべてのロガーのDebugレベル以上のログを指定したファイルに出力

nlog.jsonを作成します。

ConsoleApp                    
├── ConsoleApp.csproj
├── ExampleBackgroundHostedService.cs
├── ExampleHostedService.cs
├── nuget.config
├── Program.cs
├── bin
├── dist
│   ├── appsettings.json        
│   ├── appsetting2.json                
│   ├── nlog.json                <--新規作成
│   └── ConsoleApp.exe
├── obj
└── packages
nlog.json
{
  "NLog": {
    "throwConfigExceptions": true,

    "targets": {
      "logconsole": {
        "type": "Console"
      },
      "logfile": {
        "type": "File",
        "fileName": "./log/ConsoleApp_${shortdate}.log"
      }
    },

    "rules": [
      {
        "logger": "*",
        "minLevel": "Info",
        "writeTo": "logconsole"
      },
      {
        "logger": "*",
        "minLevel": "debug",
        "writeTo": "logfile"
      }
    ]
  }
}

8.2.2. 依存パッケージを解決する

Nlogのクラスなどは名前空間:NLog.Extensions.Loggingで定義されており、これを含むパッケージ:NLog.Extensions.Loggingをプロジェクトにインポートする必要があります。

PackageReferenceでパッケージ名とバージョンを指定して、インポートします。

ConsoleApp.csproj
     </PropertyGroup>

     <ItemGroup>
        <PackageReference Include="Microsoft.Extensions.Hosting" Version="6.0.1" />
+        <PackageReference Include="NLog.Extensions.Logging" Version="5.0.4" />
     </ItemGroup>

    </Project>

8.2.3 ホストにNLogの設定ファイルを読み込ませる

Program.csを変更します。

Program.cs
  using Microsoft.Extensions.Hosting;
  using Microsoft.Extensions.DependencyInjection;
  using Microsoft.Extensions.Configuration;
  using Microsoft.Extensions.Logging;
+ using NLog.Extensions.Logging;
  using Test.Services;
  using System.Diagnostics;

  await
  Host.CreateDefaultBuilder()
      .ConfigureAppConfiguration((hostContext, config) =>
      {
           config.SetBasePath(Path.GetDirectoryName(Process.GetCurrentProcess().MainModule.FileName));
           config.AddJsonFile("appsetting2.json", optional: true)
           //ホストの設定にnlog.jsonを追加します
+          config.AddJsonFile("nlog.json", optional: true);
      })
      .ConfigureLogging((hostCotext,logging) =>
      {
           logging.ClearProviders();
           //ホストが起動したとき、設定ファイルのNLogセクションの内容を指定してNLogを初期化します
+          logging.AddNLog(new NLogLoggingConfiguration(hostCotext.Configuration.GetSection("NLog")));
      } )
      .ConfigureServices(( hostBuilderContext,serviceCollection) =>  serviceCollection.AddHostedService<ExampleHostedService>())
      .Build()
      .RunAsync();

本章での内容でアプリを再ビルドして、実行してみます。
コンソールへの出力です。

bash-3.2$ dotnet publish -r osx.12-arm64 -o dist
MSBuild version 17.3.0+92e077650 for .NET
  復元対象のプロジェクトを決定しています...
  復元対象のすべてのプロジェクトは最新です。
  ConsoleApp -> /Users/ken/repos/ConsoleApp/bin/Debug/net6.0/osx.12-arm64/ConsoleApp.dll
  ConsoleApp -> /Users/ken/repos/ConsoleApp/dist/
bash-3.2$ ./dist/ConsoleApp
りんご
ゴリラ
ライオン
2022-09-02 14:14:45.9419|INFO|Microsoft.Hosting.Lifetime|Application started. Press Ctrl+C to shut down.
2022-09-02 14:14:45.9453|INFO|Microsoft.Hosting.Lifetime|Hosting environment: Production
2022-09-02 14:14:45.9461|INFO|Microsoft.Hosting.Lifetime|Content root path: /Users/ken/repos/ConsoleApp
^C2022-09-02 14:14:47.3169|INFO|Microsoft.Hosting.Lifetime|Application is shutting down...

ログファイルへの出力です。

2022-09-02 14:14:45.6962|DEBUG|Microsoft.Extensions.Hosting.Internal.Host|Hosting starting
2022-09-02 14:14:45.9371|DEBUG|Test.Services.ExampleHostedService|StartAsync has been called.
2022-09-02 14:14:45.9419|INFO|Microsoft.Hosting.Lifetime|Application started. Press Ctrl+C to shut down.
2022-09-02 14:14:45.9453|INFO|Microsoft.Hosting.Lifetime|Hosting environment: Production
2022-09-02 14:14:45.9461|INFO|Microsoft.Hosting.Lifetime|Content root path: /Users/ken/repos/ConsoleApp
2022-09-02 14:14:45.9461|DEBUG|Microsoft.Extensions.Hosting.Internal.Host|Hosting started
2022-09-02 14:14:47.3169|INFO|Microsoft.Hosting.Lifetime|Application is shutting down...
2022-09-02 14:14:47.3220|DEBUG|Microsoft.Extensions.Hosting.Internal.Host|Hosting stopping
2022-09-02 14:14:47.3220|DEBUG|Test.Services.ExampleHostedService|StopAsync has been called.
2022-09-02 14:14:47.3225|DEBUG|Microsoft.Extensions.Hosting.Internal.Host|Hosting stopped

9. ホストにアプリをシャットダウンさせる

8章まではユーザーによる入力(Ctrl+C)でアプリを終了させましたが、本章ではホストにアプリを終了させます。5章で作成したバックグラウンドタスクのサービスを、10回のループした後アプリをシャットダウンする処理にします。

ExampleBackgroundHostedService.csを変更します。

ExampleBackgroundHostedService.cs
  using Microsoft.Extensions.Hosting;

  namespace Test.Services;

  public sealed class ExampleBackgroundHostedService : BackgroundService{
+     private readonly IHostApplicationLifetime _applicationLifetime;

      //アプリをシャットダウンさせるメソッドを持つ、IHostApplicationLifetimeインターフェースをコンストラクタインジェクションします。
+     public ExampleBackgroundHostedService(IHostApplicationLifetime applicationLifetime){
+         _applicationLifetime = applicationLifetime;
+     }

      protected override async Task ExecuteAsync(CancellationToken stoppingToken){
          while (!stoppingToken.IsCancellationRequested){
-             Console.WriteLine(DateTimeOffset.Now);
-             await Task.Delay(1000, stoppingToken);
+             for(int i = 1; i <= 10; i++){
+                 Console.WriteLine(i);
+                 if(i == 10){
                      //アプリをシャットダウンさせます         
+                     _applicationLifetime.StopApplication();
+                 }
+                 await Task.Delay(1000, stoppingToken);
+             }
          }
      }
  }

7章で上記サービスは無効にしていたので、ホストにサービスを追加し直します。

Program.csを変更します

Program.cs
  using Microsoft.Extensions.Hosting;
  using Microsoft.Extensions.DependencyInjection;
  using Microsoft.Extensions.Configuration;
  using Microsoft.Extensions.Logging;
  using NLog.Extensions.Logging;
  using Test.Services;
  using System.Diagnostics;

  await
  Host.CreateDefaultBuilder()
      .ConfigureAppConfiguration((hostContext, config) =>
      {
           config.SetBasePath(Path.GetDirectoryName(Process.GetCurrentProcess().MainModule.FileName));
           config.AddJsonFile("appsetting2.json", optional: true)
           config.AddJsonFile("nlog.json", optional: true);
      })
      .ConfigureLogging((hostCotext,logging) =>
      {
           logging.ClearProviders();
           logging.AddNLog(new NLogLoggingConfiguration(hostCotext.Configuration.GetSection("NLog")));
      } )
-     .ConfigureServices(( hostBuilderContext,serviceCollection) =>  serviceCollection.AddHostedService<ExampleHostedService>())
+     .ConfigureServices(( hostBuilderContext,serviceCollection) =>  serviceCollection.AddHostedService<ExampleBackgroundHostedService>())
      .Build()
      .RunAsync();

アプリを再ビルドして実行します。
10秒たつとCtrl+Cを入力せずとも、アプリがシャットダウンします。

bash-3.2$ dotnet publish -r osx.12-arm64 -o dist
MSBuild version 17.3.0+92e077650 for .NET
  復元対象のプロジェクトを決定しています...
  復元対象のすべてのプロジェクトは最新です。
  ConsoleApp -> /Users/ken/repos/ConsoleApp/bin/Debug/net6.0/osx.12-arm64/ConsoleApp.dll
  ConsoleApp -> /Users/ken/repos/ConsoleApp/dist/
bash-3.2$ ./dist/ConsoleApp
1
2022-09-02 20:33:20.3657|INFO|Microsoft.Hosting.Lifetime|Application started. Press Ctrl+C to shut down.
2022-09-02 20:33:20.3693|INFO|Microsoft.Hosting.Lifetime|Hosting environment: Production
2022-09-02 20:33:20.3693|INFO|Microsoft.Hosting.Lifetime|Content root path: /Users/ken/repos/ConsoleApp
2
3
4
5
6
7
8
9
10
2022-09-02 20:33:29.3946|INFO|Microsoft.Hosting.Lifetime|Application is shutting down...

10. Widowsサービスとして起動できるようにする

10.1. 依存パッケージを解決する

Microsoft.Extensions.Hosting.WindowsServicesのパッケージが必要になります。

PackageReferenceに追加します。

ConsoleApp.csproj
     </PropertyGroup>

     <ItemGroup>
        <PackageReference Include="Microsoft.Extensions.Hosting" Version="6.0.1" />
+        <PackageReference Include="Microsoft.Extensions.Hosting.WindowsServices" Version="6.0.0" />
         <PackageReference Include="NLog.Extensions.Logging" Version="5.0.4" />
     </ItemGroup>

    </Project>

10.2. ホストへの設定

Program.csを変更します

Program.cs
  //UseWindowsServiceメソッドのために別途パッケージをインストールしたが、IHostBuilderインターフェースの拡張メソッドなのでこの名前空間には含まれる
  using Microsoft.Extensions.Hosting;
  using Microsoft.Extensions.DependencyInjection;
  using Microsoft.Extensions.Configuration;
  using Microsoft.Extensions.Logging;
  using NLog.Extensions.Logging;
  using Test.Services;
  using System.Diagnostics;

  await
  Host.CreateDefaultBuilder()
      .ConfigureAppConfiguration((hostContext, config) =>
      {
           config.SetBasePath(Path.GetDirectoryName(Process.GetCurrentProcess().MainModule.FileName));
           config.AddJsonFile("appsetting2.json", optional: true)
           config.AddJsonFile("nlog.json", optional: true);
      })
      .ConfigureLogging((hostCotext,logging) =>
      {
           logging.ClearProviders();
           logging.AddNLog(new NLogLoggingConfiguration(hostCotext.Configuration.GetSection("NLog")));
      } )
      .ConfigureServices(( hostBuilderContext,serviceCollection) =>  serviceCollection.AddHostedService<ExampleBackgroundHostedService>())
+     .UseWindowsService() //Windowsサービスとして利用するための設定
      .Build()
      .RunAsync();

Register as a new user and use Qiita more conveniently

  1. You can follow users and tags
  2. you can stock useful information
  3. You can make editorial suggestions for articles
What you can do with signing up
14
Help us understand the problem. What are the problem?