athome-developer’s blog

不動産情報サービスのアットホームの開発者が発信するブログ

.NET Coreの汎用ホストを使ってバッチを作ってみる

こんにちは、情報システム部の高野です。
とある方のつぶやきで初めて知ったのですが、
.NET Core 2.1で汎用ホストなるものが追加されていたのですね。
docs.microsoft.com
これでバッチ処理などのバックエンドのプログラムも
ASP.NET Coreのようにインジェクションなどの設定ができそう!
ということでサンプルを作ってみたので手順を書いておきます。

とりあえず環境

.NET Core SDK v2.1.403
VisualStudio Code v1.29.0

プロジェクトの作成

汎用ホスト用のテンプレートは今のところ無いのでconsoleで作成します。

dotnet new console -o GenericHostSample
cd GenericHostSample
dotnet restore

VS Codeデバッグ設定

VS Codeでプロジェクトを開くと下記のメッセージが表示されると思いますので
「Yes」ボタンをクリックして設定ファイルを生成します。

Required assets to build and debug are missing from 'GenericHostSample'. Add them?

これでデバッグ実行ができるようになりました。

生成されたlaunch.jsonファイルを一部変更します。

"console": "integratedTerminal"

変更しなくても特に問題はないのですが、
変更するとVS Codeのターミナルタブに結果が出力されるので
確認がしやすいと思います。

汎用ホストで実行

まずNugetパッケージを追加します。

dotnet add package Microsoft.Extensions.Hosting
dotnet restore


Mainメソッドを書き換えます。

using System;
using Microsoft.Extensions.Hosting;

namespace GenericHostSample
{
    class Program
    {
        public static void Main(string[] args)
        {
            var host = new HostBuilder().Build();

            host.Run();
        }
    }
}


デバッグ実行してもなにも変化はないですが
一応、汎用ホストで動いています。

環境変数を読み込む

現状は、実行するとコンソールに

Hosting environment: Production

が表示されていると思います。
これを設定したEnvironmentに変更します。

必要なNugetパッケージを追加します。

dotnet add package Microsoft.Extensions.Configuration.CommandLine
dotnet add package Microsoft.Extensions.Configuration.EnvironmentVariables
dotnet restore


.vscode/launch.json環境変数を追加します。

{
  "configurations": [
    {
      "name": ".NET Core Launch (console)",
      ・・・
      // 追加
      "env": {
        "DOTNETCORE_ENVIRONMENT": "Development"
      }
    }
  ]
}


Mainメソッドに環境変数を読み込む処理を追記します。

using Microsoft.Extensions.Configuration; // 追加

・・・
public static void Main(string[] args)
{
    var host = new HostBuilder()
        .ConfigureHostConfiguration(config => { // ConfigureHostConfigurationを追加
            config.AddEnvironmentVariables(prefix: "DOTNETCORE_"); // ここでプレフィックスを指定する
            config.AddCommandLine(args);
        })
        .Build();

    host.Run();
}


これで実行するとEnvironmentがDevelopmentになってるはずです。

Hosting environment: Development

バッチ処理本体を書く

ここまでの手順では、まだバッチ処理本体を記述する場所がありません。
バッチ処理本体を記述する場所を追加します。

バッチ処理を記述するクラスを追加します。

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

namespace GenericHostSample
{
    internal class BatchService : IHostedService
    {
        private readonly IApplicationLifetime appLifetime;

        public BatchService(IApplicationLifetime appLifetime)
        {
            this.appLifetime = appLifetime;
        }

        public Task StartAsync(CancellationToken cancellationToken)
        {
            this.appLifetime.ApplicationStarted.Register(OnStarted); // 最低限これだけあればバッチ処理としては成り立つ

            Console.WriteLine("StartAsync");

            return Task.CompletedTask;
        }

        public Task StopAsync(CancellationToken cancellationToken)
        {
            Console.WriteLine("StopAsync");

            return Task.CompletedTask;
        }

        private void OnStarted()
        {
            Console.WriteLine("OnStarted");
            // 実際の処理はここに書きます

            this.appLifetime.StopApplication(); // 自動でアプリケーションを終了させる
        }
    }
}


BatchServiceが実行されるようにMainメソッドに追記します。

using Microsoft.Extensions.DependencyInjection; // 追加
・・・

public static void Main(string[] args)
{
    var host = new HostBuilder()
        .ConfigureHostConfiguration(config => {
            config.AddEnvironmentVariables(prefix: "DOTNETCORE_");
            config.AddCommandLine(args);
        })
        .ConfigureServices((hostContext, services) => {  // ConfigureServicesを追加
            services.AddHostedService<BatchService>();
        })
        .Build();

    ・・・
}


これで実行すると下記のように出力されるはずです。

StartAsync
OnStarted
Application started. Press Ctrl+C to shut down.
Hosting environment: Development
Content root path: C:\_workspace\cs\GenericHostSample2\bin\Debug\netcoreapp2.1\
StopAsync

Application startedやHosting environmentが間に入ってきてしまうのは謎です。

設定ファイルから設定を読み込む

Webホストと同様にappsettings.josnファイルを作成し
そこから設定を読み込みます。

必要なNugetパッケージを追加します。

dotnet add package Microsoft.Extensions.Configuration.Json
dotnet add package Microsoft.Extensions.Options.ConfigurationExtensions
dotnet restore


設定用のクラスを追加します。

public class SampleConfig
{
    public string Name { get; set; }

    public string Value { get; set; }
}


appsettings.jsonを追加します。

{
    "Sample": {
        "Name": "hoge",
        "Value": "piyo"
    }
}


Mainメソッドに設定用のJSONファイルを読み込む処理を追加します。

using Microsoft.Extensions.Logging; // 追加
using System.IO; // 追加

・・・
public static void Main(string[] args)
{
    var host = new HostBuilder()
        .ConfigureHostConfiguration(config => {
            config.SetBasePath(Directory.GetCurrentDirectory()); // 追加
            config.AddJsonFile("appsettings.json", optional: true); // 追加
            config.AddEnvironmentVariables(prefix: "DOTNETCORE_");
            config.AddCommandLine(args);
        })
        .ConfigureServices((hostContext, services) => {
            services.Configure<SampleConfig>(hostContext.Configuration.GetSection("Sample")); // 追加
            services.AddHostedService<BatchService>();
        })
        .Build();

    ・・・
}


BatchServiceに設定クラスを注入し設定した値を出力します。

using Microsoft.Extensions.Options; // 追加
・・・
namespace GenericHostSample
{
    internal class BatchService : IHostedService
    {
        private readonly IApplicationLifetime appLifetime;

        private readonly SampleConfig config; // 追加

        public BatchService(IApplicationLifetime appLifetime, 
IOptions<SampleConfig> config) // 追加
        {
            this.appLifetime = appLifetime;
            this.config = config.Value; // 追加
        }

       ・・・

        private void OnStarted()
        {
            Console.WriteLine("OnStarted");
            Console.WriteLine(this.config.Name); // 追加
            Console.WriteLine(this.config.Value); // 追加

            ・・・
        }
    }
}


これでappsettings.jsonに登録した設定が出力できたと思います。

Environmentに合わせた設定ファイルを読み込む

appsettings.Development.jsonファイルを読み込みます。

appsettings.Development.jsonを作成します。

{
    "Sample": {
        "Name": "foo"
    }
}


MainメソッドにEnvironment名付きの設定ファイルを読み込む処理を追記します。

public static async Task Main(string[] args)
{
    var environment = Environment.GetEnvironmentVariable("DOTNETCORE_ENVIRONMENT"); // 追加

    var host = new HostBuilder()
        .ConfigureHostConfiguration(config => {
            config.SetBasePath(Directory.GetCurrentDirectory());
            config.AddJsonFile("appsettings.json", optional: true);
            config.AddJsonFile($"appsettings.{environment}.json", optional: true); // 追加
            config.AddEnvironmentVariables(prefix: "DOTNETCORE_");
            config.AddCommandLine(args);
        })
        ・・・
}

ConfigureHostConfiguration内では、環境変数を持つ引数が渡ってこないので
Environment.GetEnvironmentVariable関数にてEnvironmentを取得しています。

出力結果が、appsettings.Development.jsonで設定したものに変わっていると思います。

ログを出力する

今までは、Console.WriteLineでコンソールに出力していましたが
ちゃんとログで出力をしてみます。

Nugetパッケージを追加します。

dotnet add package Microsoft.Extensions.Logging
dotnet add package Microsoft.Extensions.Logging.Configuration
dotnet add package Microsoft.Extensions.Logging.Console
dotnet add package Microsoft.Extensions.Logging.Debug
dotnet restore


appsettings.jsonにログ設定を追記します。

{
    "Sample": {
        ・・・
    },
    "Logging": {  // 追加
        "LogLevel": {
            "Default": "Warning"
        }
    }
}


Mainメソッドにログを利用する処理を追記します。

public static void Main(string[] args)
{
    var environment = Environment.GetEnvironmentVariable("DOTNETCORE_ENVIRONMENT");

    var host = new HostBuilder()
        .ConfigureHostConfiguration(config => {
            ・・・
        })
        .ConfigureServices((hostContext, services) => {
            ・・・
        })
        .ConfigureLogging((hostContext, config) => // ConfigureLoggingを追加
        {
            config.AddConsole();
            config.AddDebug();
        })
        .Build();

    ・・・
}


BatchServiceクラスにログ出力処理を追記します。

・・・
using Microsoft.Extensions.Logging; // 追加
・・・

namespace GenericHostSample
{
    internal class BatchService : IHostedService
    {
        ・・・
        private readonly ILogger<BatchService> logger; // 追加

        public BatchService(IApplicationLifetime appLifetime,
            IOptions<SampleConfig> config,
            ILogger<BatchService> logger) // 追加
        {
            ・・・
            this.logger = logger; // 追加
        }

        public Task StartAsync(CancellationToken cancellationToken)
        {
            ・・・
            this.logger.LogInformation("StartAsync"); // Console.WriteLineをthis.logger.LogInformationに変更
            ・・・
        }
        // 以下もConsole.WriteLineをthis.logger.LogInformationに変更
    }
}


出力結果抜粋

info: GenericHostSample2.BatchService[0]
      OnStarted
info: GenericHostSample2.BatchService[0]
      foo

ログ形式で出力されています。

自分で作成したクラスをインジェクションする

インジェクションするクラスを作成します。

using Microsoft.Extensions.Logging;

namespace GenericHostSample
{
    public interface ISampleRepository
    {
        object FindById(int id);
    }

    public class SampleRepository : ISampleRepository
    {
        ILogger<SampleRepository> logger;

        public SampleRepository(ILogger<SampleRepository> logger)
        {
            this.logger = logger;
        }
        public object FindById(int id)
        {
            this.logger.LogInformation($"{nameof(FindById)}:{id}");
            return null;
        }
    }
}


BatchServiceクラスでISampleRepositoryを受け取り利用する処理を追記します。

internal class BatchService : IHostedService
{
    ・・・
    private readonly ISampleRepository repository; // 追加

    public BatchService(IApplicationLifetime appLifetime,
        IOptions<SampleConfig> config,
        ILogger<BatchService> logger,
        ISampleRepository repository) // 追加
    {
        ・・・
        this.repository = repository; // 追加
    }

    ・・・

    private void OnStarted()
    {
        ・・・
        this.repository.FindById(100); // 追加

        ・・・
    }
}


Mainメソッドにインジェクションの設定をします。

public static void Main(string[] args)
{
    ・・・

    var host = new HostBuilder()
        .ConfigureHostConfiguration(config => {
            ・・・
        })
        .ConfigureServices((hostContext, services) => {
            ・・・
            services.AddScoped<ISampleRepository, SampleRepository>(); // 追加
        })
        .ConfigureLogging((hostContext, config) =>
        {
            ・・・
        })
        .Build();

    ・・・
}

Publishの設定をする

このままだとdotnet publish時にappsetings.jsonが付いていきません。
設定を追加してdotnet publishに対応させます。

csprojファイルにコピーの設定を追記します。

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

  ・・・
  <ItemGroup>
    <None Update="appsettings.json">
      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
    </None>
    <None Update="appsettings.Development.json">
      <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
    </None>
  </ItemGroup>

</Project>
まとめ

今まではWebアプリとバッチ処理系のConsoleアプリで作り方が異なり
バッチ側があまりきれいに書けなかったのですが
これで基本的にほぼWebホスト同様の書き方ができるので色々と捗りそうです。
まだサンプルを書いた程度なので実際に使ってみないとなんとも言えませんが
これからバッチプログラムはこの書き方で行こうかなっと思っています。


弊社ではエンジニアを募集しています。
C#以外にもいろいろなシステムが有りますので
ご興味がある方は下記からエントリーをお願いします。
athome-inc.jp