きなこもち.net

.NET Framework × UiPath,Orchestrator × Azure × AWS × Angularなどの忘備録

Dotnet Core × Console Application × Dependency Injection

Purpose

dotnet core / Console Applicationの起動方法についてまとめる。

シンプルな起動

慣れ親しんだ起動方法。サンプルを作るときとかにサクっと作れて便利。

    class Program
    {
        static void Main(string[] args) => 
            Console.WriteLine($"Worker running at: {DateTimeOffset.Now}");
    }

StaticなMainを使わない方法

シンプルな起動方法では、Programクラス内に別の処理メソッドを記載するときにすべてStatic修飾子をつけないといけなくなる。これを避ける目的で、Programクラスをインスタンス化した後に、Mainメソッドを呼び出すようにする方法。
これをすることで、Mainメソッド以外のメソッドを定義するとき、Staticをつける手間を省くことができる。

    class Program
    {

        static void Main(string[] args) =>
            new Program().MainCore(args);

        private void MainCore(string[] args)=>
            this.Write($"Worker running at: {DateTimeOffset.Now}");

        private void Write(string message) =>
            Console.WriteLine(message);
    }

Dependency Injectionを使う方法1

込み入ったコンソールアプリケーションを開発する場合、いろいろなクラスに処理を分けて実装したくなる。そんな時はテストの容易性を考え、DIを使いたい
ただ、ASP.net Coreのテンプレートとは違い、コンソールアプリケーションのテンプレートには、初めからDIの仕組みが入っていないため、追加する必要がある。
追加する方法は、以下の通りだが、メインとなる処理のエントリポイントを指定するところが問題となる。調べたところ、追加方法が2つ見つかった。以下は、そのうちの一つ目。AddHostedServiceでエントリポイントとなるクラスを指定する。ここでは、Workerクラスがそれにあたる。Workerクラスは、BackgroundServiceを継承している。この方式は、一度実行されたら、キャンセルされるまで永遠と処理を繰り返す処理が必要な時に利用される。Windows Serviceで定義するようなものに向いている

    class Program
    {
        static Task Main(string[] args) =>
            CreateHostBuilder(args).Build().RunAsync();

        static IHostBuilder CreateHostBuilder(string[] args) =>
            Host.CreateDefaultBuilder(args)
                .ConfigureServices((_, services) =>
                {
                    //ここ
                    services.AddHostedService<Worker>();
                    services.AddTransient<IMessageWriter, MessageWriter>();
                });
    }

    public class Worker : BackgroundService
    {
        private readonly IMessageWriter _messageWriter;

        public Worker(IMessageWriter messageWriter) =>
            _messageWriter = messageWriter;

        protected override async Task ExecuteAsync(CancellationToken stoppingToken) =>
            this._messageWriter.Write($"Worker running at: {DateTimeOffset.Now}");
    }

    public interface IMessageWriter
    {
        void Write(string message);
    }

    public class MessageWriter : IMessageWriter
    {
        public void Write(string message) =>
            Console.WriteLine($"MessageWriter.Write(message: \"{message}\")");
    }

Dependency Injectionを使う方法2

コマンドのような。実行したら処理を一回だけ実行するようなものを開発するのであれば、ActivatorUtilitiesを利用し、エントリポイントとしたいクラスをインスタンス化する方法もある。

    class Program
    {
        static void Main(string[] args)
        {
            var host = Host.CreateDefaultBuilder()
                .ConfigureServices((context, services) =>
                {
                    services.AddTransient<IMessageWriter, MessageWriter>();
                })
                .Build();
            //起動したいサービスを狙い撃ち
            var svc = ActivatorUtilities.CreateInstance<MessageWriter>(host.Services);
            svc.Write($"Worker running at: {DateTimeOffset.Now}");
        }
    }

    public interface IMessageWriter
    {
        void Write(string message);
    }

    public class MessageWriter : IMessageWriter
    {
        public void Write(string message) =>
            Console.WriteLine($"MessageWriter.Write(message: \"{message}\")");
    }

まとめ

上記を踏まえて、以下の要望をまとめたサンプルコードをまとめとして残しておく。 - コンソールアプリケーションでDIを設定したい - appsettings.jsonで設定を追加したい - Loggingを設定したい

    class Program
    {
        static void Main(string[] args)
        {
            var host = Host.CreateDefaultBuilder()
                .ConfigureServices((context, services) =>
                {
                    services.AddTransient<IMessageWriter, MessageWriter>();
                })
                .Build();
            var svc = ActivatorUtilities.CreateInstance<MessageWriter>(host.Services);
            svc.Write();
        }
    }

    public interface IMessageWriter
    {
        void Write();
    }

    public class MessageWriter : IMessageWriter
    {
        private readonly ILogger<MessageWriter> logger;
        private readonly IConfiguration appsettings;

        public MessageWriter(ILogger<MessageWriter> logger, IConfiguration appsettings)
        {
            this.logger = logger;
            this.appsettings = appsettings;
        }

        public void Write() =>
            this.logger.LogInformation($"{appsettings.GetValue<string>("LogMessage")}");
    }

-- appsettings.json
{
  "LogMessage": "Sample"
}

追記

いただいたコメントを参考にさせていただきました。 コメントありがとうございました。 最小の実装・・・ということでもないですが、シンプルにスタートするには十分なテンプレートができたと思います。

    class Program
    {
        static void Main(string[] args)
        {
            Host.CreateDefaultBuilder(args)
                .ConfigureServices((context, services) =>
                {
                    services.AddTransient<Program>();
                })
                .Build().Services
                .GetRequiredService<Program>()
                .Run(args);
        }

        public void Run(string[] args)
        {
            //anything....
        }
    }

このような実装をこの上なく簡単に実現するためのフレームワークがありました。ConsoleAppFrameworkです。これを導入することで、上記のような実装間で、ヘルプ機能までついてくる高機能なコマンドラインを実装することができます。Githubこちら