ついに.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.Configuration
- Microsoft.Extensions.DependencyInjection
- Microsoft.Extensions.FileProviders
- Microsoft.Extensions.FileSystemGlobbing
- Microsoft.Extensions.Hosting
- Microsoft.Extensions.Logging
- Microsoft.Extensions.Options
- Microsoft.Extensions.Primitives
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.ConfigurationExtensions
のIOptions<T>
で各クラスへ渡せます。
なので、上記Packageはセットで使うことが多いっぽい?
Packageは以下の通り。
- Microsoft.Extensions.Configuration.CommandLine
- Microsoft.Extensions.Configuration.EnvironmentVariables
- Microsoft.Extensions.Configuration.Json
- Microsoft.Extensions.Configuration.Ini
- Microsoft.Extensions.Configuration.XML
- Microsoft.Extensions.Configuration.UserSecrets
ちょっと長いですが以下に例を示します。
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に指定できたりもしますが割愛
- Microsoft.Extensions.Logging.Configuration
- Microsoft.Extensions.Logging.Console
- Microsoft.Extensions.Logging.Debug
- Microsoft.Extensions.Logging.EventLog
- Microsoft.Extensions.Logging.EventSource
- Microsoft.Extensions.Logging.TraceSource
- Microsoft.Extensions.Logging.AzureAppServicesFile
- Microsoft.Extensions.Logging.AzureAppServicesBlob
- Microsoft.Extensions.Logging.ApplicationInsights
サードパーティー製の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\
イベントビューア
Textファイル
おわり
Generic Hostは作業者によって書き方が変わってしまうような部分を吸収してくれる点や
DIをフレームワーク的に導入してくれている点が良いですね。
WebアプリだけでなくWindows Serviceも同じノリで書けるらしいので、機会があれば書いてみたいところ。
以下は宿題。
- TraceSourceの設定をXMLから制御する方法
- HostBuilderの起動方法の各メソッドの違い(Run/Start/RunConsoleAsyncなど)
以上