🐥note.

小鳥とMicrosoft <3 なエンジニアの技術Blog📚

Generic Host(汎用ホスト)用のチートシート的なメモ

ついに.NET Core 3.0がリリースされましたね!
BackgroundServiceのテンプレート(dotnet new worker)が新規追加されましたし、今後Generic Host(汎用ホスト)を書く機会も増えてくかもしれませんね。

本エントリでは.NET CoreでGeneric Host(汎用ホスト)を書く際にどのPackageを追加すればいいのか忘れちゃう問題を解決する個人的なチートシート的なメモです.

TL;DR

超雑にまとめると

  • Microsoft.Extensions.HostingのPackageを参照とusingに追加

  • Microsoft.Extnsions.DependencyIndectionをusingに追加

ここまでがマストで以下はオプション

  • JSONの設定ファイルの読み込みを行うならMicrosoft.Extensions.Configuration.Jsonをusingに追加
    ※設定ファイルの値をIOptionsでConstructor InjectionしたいならMicrosoft.Extensions.Options.ConfigurationExtensionsを参照とusingに追加

  • LoggingするならMicrosoft.Extensions.Logging.Consoleあたりを参照とusingを追加

サンプルはこちら:GitHub - GenericHostSample

以上です。

以下、時間がある人向け。

はじめに

Generic Host書くとき、どのPackageが必要なんだっけ?ってなることが多い。
よって、よく使う Package をまとめる。

成果物

本エントリで作成したコードは以下に保存しています。

こちら:GitHub - GenericHostSample

環境は.NET Core 3.0 以降を想定しています。

Microsoft.Extensions.Hosting(大元のPackage編)

まずは大元となるMicrosoft.Extensions.Hostingを追加すること。

dotnet new console -o GenericHostSample
cd GenericHostSample
dotnet add package Microsoft.Extensions.Hosting
dotnet restore

上記Packageを追加すると以下のnamespaceをusingできるようになる。

Microsoft.Extensions.HostingをusingすることでHostBuilderが書けるようになる。

using System;
using System.Threading.Tasks;
using Microsoft.Extensions.Hosting;

namespace GenericHostSample {
    class Program {
        static async Task Main (string[] args) {
            // 何もしないHostBuilder
            var host = new HostBuilder ();
            await host.RunConsoleAsync();
        }
    }
}

Generic(汎用ホスト)って何?って人は以下がとっても参考になるかと思います。

では各もうちょっと詳しくPackageの使用方法を見ていきましょう。。

Microsoft.Extnsions.DependencyIndection(依存性の注入編)

名前の通りDIコンテナにサービスを追加するためのPackageです。

AddHostedServiceで登録するクラスで継承可能なclass/interfaceはIHostedService, BackgroundServiceがあります。

ホストの起動方法の差はよく知らないので今は割愛します。

using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

namespace DependencyInjectionsSample {
    class Program {
        static void Main (string[] args) {
            var host = new HostBuilder ();
            host.ConfigureServices ((hostContext, config) => {
                config.AddTransient<IHoge, LoudHoge> ();
                config.AddHostedService<HogeHostedService> ();
            });
            host.RunConsoleAsync();
        }
    }

    interface IHoge {
        void DoSomething ();
    }

    class LoudHoge : IHoge {
        public void DoSomething () {
            Console.WriteLine ("HOGE~~~~~~~!");
        }
    }

    class HogeHostedService : IHostedService {
        private IHoge hoge;

        public HogeHostedService (IHoge hoge) {
            this.hoge = hoge;
        }
        public Task StartAsync (CancellationToken cancellationToken) {
            hoge.DoSomething ();
            return Task.CompletedTask;
        }

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

実行結果

PS C:\Users\piyoe\DependencyInjectionsSample> dotnet run
HOGE~~~~~~~
Application started. Press Ctrl+C to shut down.
Hosting environment: Production
Content root path: C:\Users\piyoe\DependencyInjectionsSample\bin\Debug\netcoreapp3.0\

普通のDIって感じですね。

ConfigurationとOptions.ConfigurationExtensions(設定・起動引数の読み込みとパース)

Microsoft.Extensions.Configurationで起動引数・環境変数・各種設定ファイル(Ini,Json,Xmlなど),ユーザシークレットの取得が可能となります。
取得した値はMicrosoft.Extensions.Options.ConfigurationExtensionsIOptions<T>で各クラスへ渡せます。
なので、上記Packageはセットで使うことが多いっぽい?

Packageは以下の通り。

ちょっと長いですが以下に例を示します。

Configurationサンプル

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Options;

namespace ConfigurationsSample
{
    class Program
    {
        static void Main(string[] args)
        {
            var host = new HostBuilder();
            // 環境パスのDOTNETCORE_ENVIRONMENTを取得
            var environment = Environment.GetEnvironmentVariable("DOTNETCORE_ENVIRONMENT") ?? "Development";
            // 環境の設定
            host.UseEnvironment(environment);

            // ホストの構成
            host.ConfigureHostConfiguration(config =>
            {
                // Microsoft.Extensions.Configuration
                config.SetBasePath(Directory.GetCurrentDirectory());

                // Microsoft.Extensions.Configuration.EnvironmentVariables
                // 環境変数の先頭文字列"EnvironmentSettingModel_"の変数を取り込む.
                // 取り込んだ環境変数の名前は"EnvironmentSettingModel_"の部分がトリミングされる
                config.AddEnvironmentVariables(prefix: "EnvironmentSettingModel_");

                // Microsoft.Extensions.Configuration.JSON
                config.AddJsonFile($"Settings/setting.json");
                // 環境変数DOTNETCORE_ENVIRONMENTの設定値を使用する。
                // 同じ定義の設定値は後に読み込んだ方で上書きされる
                config.AddJsonFile($"Settings/setting.{environment}.json");

                // Microsoft.Extensions.Configuration.Ini
                config.AddIniFile("Settings/setting.ini");

                // Microsoft.Extensions.Configuration.XML
                config.AddXmlFile("Settings/setting.xml");

                // Microsoft.Extensions.Configuration.Memory
                config.AddInMemoryCollection(new Dictionary<string, string>()
                {
                    {"InMemoryMessage", "Hello InMemoryCollection!"}
                });

                // Microsoft.Extensions.Configuration.CommandLine
                config.AddCommandLine(args);
            });

            // アプリケーションの構成
            host.ConfigureAppConfiguration((hostContext, config) =>
            {
                var env = hostContext.HostingEnvironment;
                if (env.IsDevelopment())
                {
                    // Microsoft.Extensions.Configuration.UserSecrets
                    config.AddUserSecrets<Program>();
                }
            });

            // サービスの構成
            host.ConfigureServices((hostContext, config) =>
            {
                // Microsoft.Extensions.DependencyInjection
                config.AddHostedService<SampleService>();

                // Microsoft.Extensions.Options.ConfigurationExtensions
                config.Configure<IniSettingModel>(hostContext.Configuration.GetSection("IniSettingModel"));
                config.Configure<JsonSettingModel>(hostContext.Configuration.GetSection("JsonSettingModel"));
                config.Configure<XmlSettingModel>(hostContext.Configuration.GetSection("XmlSettingModel"));
                config.Configure<UserSecretModel>(hostContext.Configuration.GetSection("UserSecretSettingModel"));
                config.Configure<CommandLineSettingModel>(hostContext.Configuration.GetSection("CommandLineSettingModel"));

                // こういった方法でも使用可能
                var message = hostContext.Configuration.GetValue<String>("UserSecretSettingModel:Message");
                var messageModel1 = hostContext.Configuration.GetSection("UserSecretSettingModel").Get<SettingModel>();
                var messageModel2 = hostContext.Configuration.GetValue<UserSecretModel>("UserSecretSettingModel");
                var messageModel3 = new SettingModel();
                hostContext.Configuration.GetSection("UserSecretSettingModel").Bind(messageModel3);
            });

            host.Build().Run();
        }
    }

    class SampleService : IHostedService
    {
        private IApplicationLifetime appLifeTime;
        private IConfiguration config;
        private IniSettingModel ini;
        private JsonSettingModel json;
        private XmlSettingModel xml;
        private UserSecretModel userSecret;
        private CommandLineSettingModel commandLine;

        public SampleService(IApplicationLifetime appLifeTime,
                             IConfiguration config,
                             IOptions<JsonSettingModel> json,
                             IOptions<IniSettingModel> ini,
                             IOptions<XmlSettingModel> xml,
                             IOptions<UserSecretModel> userSecret,
                             IOptions<CommandLineSettingModel> commandLine)
        {
            this.appLifeTime = appLifeTime;
            this.config = config;
            this.ini = ini.Value;
            this.json = json.Value;
            this.xml = xml.Value;
            this.userSecret = userSecret.Value;
            this.commandLine = commandLine.Value;
        }
        public Task StartAsync(CancellationToken cancellationToken)
        {
            appLifeTime.ApplicationStarted.Register(onStarted);
            return Task.CompletedTask;
        }

        public Task StopAsync(CancellationToken cancellationToken)
        {
            return Task.CompletedTask;
        }
        private void onStarted()
        {
            // EnvironmentVariables
            Console.WriteLine($"{config.AsEnumerable().FirstOrDefault(f => f.Key == "EnvironmentMessage").Value}");
            // Json
            Console.WriteLine($"{json.Message}");
            // Ini
            Console.WriteLine($"{ini.Message}");
            // Xml
            Console.WriteLine($"{xml.Message}");
            // InMemoryCollection
            Console.WriteLine($"{config.AsEnumerable().FirstOrDefault(f => f.Key == "InMemoryMessage").Value}");
            // CommandLine
            Console.WriteLine($"{commandLine.Message}");
            // UserSecret
            Console.WriteLine($"{userSecret.Message}");
        }
    }
}

Configuration 実行結果

実行するにはUserSecretsと環境変数の設定が必要です。
詳細はGitHub上のサンプルのREADMEを参照すること。

C:\work\ConfigurationsSample>dotnet run /CommandLineSettingModel:Message="Hello CommandLine!"
Hello Environment!
Hello Development Json!
Hello Ini!
Hello XML!
Hello InMemoryCollection!
Hello CommandLine!
Hello UserSecret!
Application started. Press Ctrl+C to shut down.
Hosting environment: Development
Content root path: C:\work\ConfigurationsSample\bin\Debug\netcoreapp3.0\

Logging関連

Loggingの保存先に応じてPackageの種類が存在する。
※TraceSourceで出力先をConsoleに指定できたりもしますが割愛

サードパーティー製のLoggerもちらほらリリースされてます。 詳しくは以下を参照

よく使うであろうConsole, Deub, EventLog, TraceSourceあたりのサンプルは以下の通り。
※TraceSourceはXMLに設定書いてそちらで制御するのがよくあるパターンなんだろうけど、Generic Hostだとどうやって書けばいのか分かりません。。
なので、個人的にはGenericHostに対応したSeriLogやNLogなんかを使用してLogのファイル出力を実現しています。

Loggingサンプル

using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

namespace LoggingsSample {
    class Program {
        static void Main (string[] args) {
            var host = new HostBuilder ();

            host.ConfigureAppConfiguration ((hostContext, config) => {
                config.AddJsonFile ("appsettings.json");
            });

            // Loggingの設定
            host.ConfigureLogging ((hostContext, config) => {
                // config系の設定を初期化
                config.ClearProviders ();
                // appsettings.jsonに記載した設定を反映
                config.AddConfiguration (hostContext.Configuration.GetSection ("Logging"));

                // 今回は上記ファイルで指定してるのでコメントアウト.
                // 出力するLogの最低レベルを指定
                // config.SetMinimumLevel (LogLevel.Trace);
                // Microsofot系のLogはInformation以下のレベルは出力しない
                // config.AddFilter ("Microsoft", LogLevel.Information);

                // Microsoft.Extensions.Logging.Console
                config.AddConsole ();

                // Microsoft.Extensions.Logging.Debug
                config.AddDebug ();

                // Microsoft.Extensions.Logging.EventLog
                config.AddEventLog ();

                // Microsoft.Extensions.Logging.EventSource
                // これはEvent Tracing for Windows (ETW)によるLoggin.
                config.AddEventSourceLogger ();

                //Microsoft.Extensions.Logging.TraceSource
                Trace.AutoFlush = true;
                var sourceSwitch = new SourceSwitch ("sample") { Level = SourceLevels.All };
                var listener = new TextWriterTraceListener ("Trace.log");
                config.AddTraceSource (sourceSwitch, listener);
            });

            host.ConfigureServices ((hostContext, config) => {
                config.AddHostedService<SampleHostedService> ();
            });
            host.Build ().Run ();
        }
    }

    class SampleHostedService : IHostedService {
        private IApplicationLifetime appLifeTime;
        private ILogger<SampleHostedService> logger;

        public SampleHostedService (IApplicationLifetime appLifeTime, ILogger<SampleHostedService> logger) {
            this.appLifeTime = appLifeTime;
            this.logger = logger;
        }

        public Task StartAsync (CancellationToken cancellationToken) {
            appLifeTime.ApplicationStarted.Register (onStared);
            appLifetime.ApplicationStopping.Register(onStopping);
            appLifetime.ApplicationStopped.Register(onStopped);
            return Task.CompletedTask;
        }

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

        private void onStared () {
            logger.Log (logLevel: LogLevel.None, eventId: 1, message: "This is onStarted Log.");
            logger.LogTrace (eventId: 2, "This is onStarted TraceLog.");
            logger.LogDebug (eventId: 3, "This is onStarted DebugLog.");
            logger.LogInformation (eventId: 4, "This is onStarted InformationLog.");
            logger.LogWarning (eventId: 5, "This is onStarted WarningLog.");
            logger.LogError (eventId: 6, "This is onStarted ErrorLog.");
            logger.LogCritical (eventId: 7, "This is onStarted CriticalLog.");

            using (logger.BeginScope ("Logging Scope")) {
                logger.LogInformation (8, "Log Meesage 1");
                logger.LogInformation (9, "Log Meesage 2");
                logger.LogInformation (10, "Log Meesage 3");
            }
        }

        private void onStopping () {
        }

        private void onStopped () {
        }

    }
}

Logging 実行結果

ConsoleとイベントビューアーとTextファイルそれぞれこんな感じになります。

なお、各出力先によって出力されるLogの内容が異なるのはappsettings.jsonで出力するLogLevelの値を制御しているためです。
コードの全体像はGitHubにアップしたコードをご参照ください。

Console
PS C:\Users\piyoe\LoggingsSample> dotnet run
info: LoggingsSample.SampleHostedService[4]
      This is onStarted InformationLog.
warn: LoggingsSample.SampleHostedService[5]
      This is onStarted WarningLog.
fail: LoggingsSample.SampleHostedService[6]
      This is onStarted ErrorLog.
crit: LoggingsSample.SampleHostedService[7]
      This is onStarted CriticalLog.
Application started. Press Ctrl+C to shut down.
Hosting environment: Production
Content root path: C:\Users\piyoe\LoggingsSample\bin\Debug\netcoreapp3.0\ 
イベントビューア

f:id:piyo_esq:20190923101002p:plain
イベントビューア

Textファイル

f:id:piyo_esq:20190923101035p:plain
Textファイル

おわり

Generic Hostは作業者によって書き方が変わってしまうような部分を吸収してくれる点や DIをフレームワーク的に導入してくれている点が良いですね。
WebアプリだけでなくWindows Serviceも同じノリで書けるらしいので、機会があれば書いてみたいところ。

以下は宿題。

  • TraceSourceの設定をXMLから制御する方法
  • HostBuilderの起動方法の各メソッドの違い(Run/Start/RunConsoleAsyncなど)

以上