かつてはコマンドライン引数の解析も自前でやる必要があったりログ出力も苦労して作ってたりするけど、今時はそういうパッケージがあるのでガンガン利用すべし。
というわけで今時のコンソールアプリを作るための初期構造。
使うパッケージ
<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
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
// 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}");
}
}
コマンドラインオプション
public class ConsoleAppOptions
{
public string Name { get; set; }
public string Value { get; set; }
}
設定ファイルマッピングクラス
public class SampleSettings
{
public int Id { get; set; }
public string Name { get; set; }
}
設定ファイル
{
"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