プログラムを中心とした個人的なメモ用のブログです。 タイトルは迷走中。
内容の保証はできませんのであしからずご了承ください。

2017/09/20

.NET Core コンソールアプリケーションにおけるロギングや設定について

update2017/09/21 event_note2017/09/20 1:33

.NET Core では Dependency Injection (DI) を使って設定やロギングを行うアーキテクチャがデフォルトで用意されているようです。
しかし、ネット上にあるほとんどの例が ASP.NET Core で説明されています。
デフォルトで用意されている DI コンテナ自体は .NET Core でも使用できるようなので、もっとシンプルに理解したいと思い、コンソールアプリケーションを使ってコードを書いてみました。

尚、私はDI コンテナはおろか、DI パターンすらまともに扱ったことがありませんので、説明が間違っている可能性もあります。

環境

  • Visual Studio 2017
  • .NET Core 2.0

依存ライブラリのインストール

NuGet で以下の3つをインストールする必要があります。

  • Microsoft.Extensions.Configuration
  • Microsoft.Extensions.DependencyInjection
  • Microsoft.Extensions.Logging

実装

とりあえずソースを全部載せます。
私自身が後で見たときに分かるように、調べたことは全てコメントに書いておいたので、コードを眺めて頂くだけで大体理解していただけるのではないかと思います。
ただし、私なりの解釈も多く含んでいるので、間違っている箇所もあるかもしれません。

using System;
using System.IO;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Microsoft.Extensions.Configuration;

namespace ConfigurationAndLogging
{
    public class Program
    {
        // https://stackoverflow.com/questions/38706959/net-core-console-applicatin-configuration-xml
        static void Main(string[] args)
        {
            // DI を使った Application クラスを例として挙げる

            // IServiceCollection に対してフレームワークが提供する拡張メソッド を使いながら依存性を定義してゆき、
            // ActivatorUtilities などを使って依存関係が解決されたインスタンスを取得するのが大まかな流れ
            // add メソッドでサービスを追加すると、DI として構成される
            IServiceCollection serviceCollection = new ServiceCollection();

            // ConfigureServices で DI の準備を行う
            ConfigureServices(serviceCollection);

            // var app = new Application(serviceCollection);
            IServiceProvider serviceProvider = serviceCollection.BuildServiceProvider();

            // DI サービスコンテナから指定した型のサービスを取得する
            var app = serviceProvider.GetService<Application>();

            // 実行
            app.Run();
            Console.ReadKey();
        }

        private static void ConfigureServices(IServiceCollection services)
        {
            // ロギングの設定
            ILoggerFactory loggerFactory = new LoggerFactory()
                // コンソールに出力する
                .AddConsole()
                // Visual Studio のデバッグウィンドウに出力する
                .AddDebug();

            // DI サービスコンテナに Singleton ライフサイクルにてオブジェクトを登録する
            // Singleton ライフサイクルでは Dependency インスタンスを一つ生成し、そのインスタンスをアプリケーションで共有する
            services.AddSingleton(loggerFactory);
            // AddLogging メソッドを呼び出すことで ILoggerFactory と ILogger<T> が DI 経由で扱えるようになる
            services.AddLogging();

            // IConfigurationBuilder で設定を選択
            // IConfigurationBuilder.Build() で設定情報を確定し、IConfigurationRoot を生成する
            IConfigurationRoot configuration = new ConfigurationBuilder()
                // 基準となるパスを設定
                .SetBasePath(Directory.GetCurrentDirectory())
                // ここでどの設定元を使うか指定
                // 同じキーが設定されている場合、後にAddしたものが優先される
                .AddJsonFile($"appsettings.json", optional: true)
                // ここでは JSON より環境変数を優先している
                .AddEnvironmentVariables()
                // 上記の設定を実際に適用して構成読み込み用のオブジェクトを得る
                .Build();

            // Logger と同じく DI サービスコンテナに Singleton ライフサイクルにてオブジェクトを登録する
            services.AddSingleton(configuration);

            // オプションパターンを有効にすることで、構成ファイルに記述した階層構造データを POCO オブジェクトに読み込めるようにする
            services.AddOptions();

            // Configure<T> を使ってオプションを初期化する
            // IConfigurationRoot から GetSection 及び GetChildren で個々の設定の取り出しができる
            // ここでは "MyOptions" セクションの内容を MyOptions として登録
            services.Configure<MyOptions>(configuration.GetSection("MyOptions"));

            // Application を DI サービスコンテナに登録する
            // AddTransient はインジェクション毎にインスタンスが生成される
            services.AddTransient<Application>();
        }
    }

    // オプションを保持するためのクラス
    public class MyOptions
    {
        public string Name { get; set; }
    }

    public class Application
    {
        ILogger logger;
        MyOptions settings;

        // コンストラクタの引数として ILogger や IOptions 型の引数を定義すると、.NET Core の DI 機能によりオブジェクトが注入される
        // (コンストラクタインジェクション)
        // 設定を取得する際には IOptions<T> を経由して DI から値を受け取る
        public Application(ILogger<Application> logger, IOptions<MyOptions> settings)
        {
            this.logger = logger;
            // ここで受け取れるオブジェクトは、オブジェクト自体ではなくアクセサオブジェクトであるため、Value プロパティを参照している
            this.settings = settings.Value;
        }

        public void Run()
        {
            logger.LogCritical("Log Critical");
            logger.LogError("Log Error");
            logger.LogWarning("Log Warning");
            logger.LogInformation("Log Information");
            // 以下の2つはデフォルトでは出力されない
            logger.LogDebug("Log Debug");
            logger.LogTrace("Log Trace");

            try
            {
                logger.LogInformation($"This is a console application for {settings.Name}");
            }
            catch (Exception ex)
            {
                logger.LogError(ex.ToString());
            }
        }
    }

}

簡単な解説

浅い理解のまま解説してみますが・・・。

DI コンテナを使うには ServiceCollection を使います。
IServiceCollection に用意されている拡張メソッドを使って、注入したいオブジェクトを指定し、サービスとして登録します。
拡張メソッドにはオブジェクトのライフタイムに応じて以下のメソッドが用意されているようです。

  • AddTransient():インジェクション毎にインスタンスを生成。つまり常に新しいインスタンスを生成?
  • AddScoped():リクエスト毎にインスタンスを生成。コンソールアプリでは使用しない?
  • AddSingleton():アプリケーション内で1つのインスタンスを生成

これらで登録したサービスを使用するには、ServiceCollection.BuildServiceProvider メソッドから返される IServiceProvider を使用します。

IServiceProviderGetService などを使うことで、登録したサービスが使用できます。
尚、利用できるサービスは、BuildServiceProvider を呼び出す前に追加されたサービスだけです。

上記のコードでは GetService の型に Application を指定しており、事前に登録しておいた Application というサービスを取得しています。
そして、このとき Application のコンストラクタでオブジェクトが注入されます(コンストラクタインジェクション)。

IServiceCollection にサービスを登録しておけば、コンストラクタの引数を変えるだけで注入したいオブジェクトを変更できるので、とても楽だと思いました。

参考 URL