基本的なコンソールアプリケーションの構造 2021年版

かつてはコマンドライン引数の解析も自前でやる必要があったりログ出力も苦労して作ってたりするけど、今時はそういうパッケージがあるのでガンガン利用すべし。

というわけで今時のコンソールアプリを作るための初期構造。

## 使うパッケージ

```xml
  <ItemGroup>
    <PackageReference Include="DryIoc.Microsoft.DependencyInjection" Version="5.1.0" />
    <PackageReference Include="Microsoft.Extensions.Configuration" Version="5.0.0" />
    <PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="5.0.0" />
    <PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="5.0.1" />
    <PackageReference Include="Microsoft.Extensions.Logging" Version="5.0.0" />
    <PackageReference Include="Microsoft.Extensions.Options.ConfigurationExtensions" Version="5.0.0" />
    <PackageReference Include="NLog.Extensions.Logging" Version="1.7.2" />
    <PackageReference Include="System.CommandLine" Version="2.0.0-beta1.21216.1" />
  </ItemGroup>
```

* ログ出力 NLog
* DI DryIoc
* コマンドライン解析 System.CommandLine

System.CommandLine はまだプレビュー版だけれども自前で解析するよりまし。
GenericHost 使わないのはバッチ処理とかちょっとしたコマンドとかには過剰なので。

## コード

program.cs

```csharp
using DryIoc;
using DryIoc.Microsoft.DependencyInjection;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using NLog.Extensions.Logging;
using System;
using System.CommandLine;
using System.CommandLine.Invocation;
using System.IO;
using System.Threading.Tasks;

    class Program
    {
        static IServiceProvider serviceProvider;
        static async Task<int> Main(string[] args)
        {
            serviceProvider = BuildServiceProvider(BuildConfiguration());

            var rootCommand = BuildCommand();
            return await rootCommand.InvokeAsync(args);
        }
        static IConfigurationRoot BuildConfiguration()
        {
            return new ConfigurationBuilder()
                .SetBasePath(Directory.GetCurrentDirectory())
                .AddJsonFile(path: "appsettings.json")
                .Build();
        }

        static IServiceProvider BuildServiceProvider(IConfigurationRoot configuration)
        {
            var services = new ServiceCollection();
            services.Configure<SampleSettings>(configuration.GetSection("SampleSettings"));
            services.AddLogging(loggingBuilder =>
            {
                loggingBuilder.ClearProviders();
                loggingBuilder.SetMinimumLevel(Microsoft.Extensions.Logging.LogLevel.Trace);
                loggingBuilder.AddNLog();
            });

            var container = new DryIoc.Container(rules => rules.With()).WithDependencyInjectionAdapter(services);
            container.Register<ISampleService, SampleService>(reuse: Reuse.Scoped);
            return container.BuildServiceProvider();
        }
        static RootCommand BuildCommand()
        {
            var rootCommand = new RootCommand();
            rootCommand.Description = "console app sample";
            rootCommand.AddOption(new Option<string>(aliases: new string[] { "--name", "-n" }));
            rootCommand.AddOption(new Option<string>(aliases: new string[] { "--value", "-v" }));

            rootCommand.Handler = CommandHandler.Create<ConsoleAppOptions>(options => { ExecuteCommand(options); });

            return rootCommand;
        }
        static void ExecuteCommand(ConsoleAppOptions options)
        {
            using(var scope = serviceProvider.CreateScope())
            {
                var service = scope.ServiceProvider.GetRequiredService<ISampleService>();
                service.DoSomething(options);
            }
        }
        
    }
```

Service

```csharp
// interface
    interface ISampleService
    {
        void DoSomething(ConsoleAppOptions options);
    }
// implementation
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;

    public class SampleService : ISampleService
    {
        private readonly SampleSettings sampleSettings;
        private readonly ILogger<SampleService> logger;
        public SampleService(IOptions<SampleSettings> settings, ILogger<SampleService> logger)
        {
            this.sampleSettings = settings.Value;
            this.logger = logger;
        }
        public void DoSomething(ConsoleAppOptions options)
        {
            logger.LogTrace($"execute {nameof(DoSomething)}");
            logger.LogTrace($"options {nameof(options.Name)} {options.Name}");
            logger.LogTrace($"options {nameof(options.Value)} {options.Value}");
            logger.LogTrace($"settings {nameof(sampleSettings.Id)} {sampleSettings.Id}");
            logger.LogTrace($"settings {nameof(sampleSettings.Name)} {sampleSettings.Name}");
        }
    }

```

コマンドラインオプション

```csharp
    public class ConsoleAppOptions
    {
        public string Name { get; set; }
        public string Value { get; set; }
    }

```

設定ファイルマッピングクラス

```csharp
    public class SampleSettings
    {
        public int Id { get; set; }
        public string Name { get; set; }
    }
```

設定ファイル

```json
{
  "Logging": {
    "LogLevel": {
      "Default": "Error",
      "Microsoft": "Trace"
    }
  },
  "SampleSettings": {
    "Id": 1,
    "Name": "name"
  }
}
```

json 設定ファイルの読み込みやらDI設定やらコマンドライン解析やら、だいたいこんな感じ。

## 実行

```
[ConsoleAppSample] > dotnet run --name 111 --value 222
[TRACE] [1] ConsoleAppSample.SampleService.DoSomething#19 execute DoSomething
[TRACE] [1] ConsoleAppSample.SampleService.DoSomething#20 options Name 111
[TRACE] [1] ConsoleAppSample.SampleService.DoSomething#21 options Value 222
[TRACE] [1] ConsoleAppSample.SampleService.DoSomething#22 settings Id 1
[TRACE] [1] ConsoleAppSample.SampleService.DoSomething#23 settings Name name
```

設定ファイルの内容とコマンドライン引数はそれぞれのクラスにマッピングされる。

## その他

`git log' みたいなコマンドのコマンドとかRootCommandにCommandを追加すればよい。
DIはAutofacでもDryIocでも Unity Container でも何でもよい。最終的には Microsoft.Extensions.DependencyInjection を通して使う。

## 参考

System.CommandLine
https://docs.microsoft.com/ja-jp/archive/msdn-magazine/2019/march/net-parse-the-command-line-with-system-commandline

DryIoc
https://github.com/dadhi/DryIoc

NLog
https://nlog-project.org/