System.CommandLine.Hostingを利用して、System.CommandLineでDIする
概要
System.CommandLineにはSystem.CommandLine.HostingというMicrosoft.Extensions.Hostingをサポートするライブラリがあります。
これは一言でいうと、System.CommandLineでGenericHostを利用するためのパッケージです。
厳密にいうとSystem.CommandLineの各種機能はGenericHostにより構築されますが、その実装はCommandHander
により隠蔽されています。
そのためSystem.CommandLine.Hostingは、ホストの構築のカスタマイズをサポートするためのパッケージといえます。 用途としては用意したクラスのDepencency Injection(DI)GenericHostに備わっているや設定ファイルのサポート、ログ周りの機能の利用が多いかと思います。
セットアップ
プロジェクトを作成し、System.CommandLineとSystem.CommandLine.Hostingを追加します。
dotnet add package System.CommandLine --version 2.0.0-beta1.21216.1
dotnet add package System.CommandLine.Hosting --version 0.3.0-alpha.21216.1
利用してみる
System.CommandLine.Hosting経由でDIを利用してみます。まずはコード全容を下記に示します。
using Xunit;
using System.CommandLine.Invocation;
using System.CommandLine;
using System.CommandLine.Parsing;
using System.CommandLine.Builder;
using System.CommandLine.Hosting;
using System.CommandLine.IO;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
public class CliTest
{
// 利用部
[Fact]
public async void Test1()
{
var builder = new CommandLineBuilder(new MyCommand());
builder.UseDefaults();
builder.UseHost(host =>
{
host.ConfigureServices((context, services) =>
{
services.AddTransient<MyService>();
});
host.UseCommandHandler<MyCommand, MyCommand.MyHandler>();
});
var parser = builder.Build();
var result = await parser.InvokeAsync(new string[]{
"46",
"--string-option", "MyCommand"
});
Assert.Equal(result, 46);
}
}
// コマンド定義
public class MyCommand : Command
{
public MyCommand(): base(name: "mycommand")
{
AddArgument(new Argument<int>("IntArgument"));
AddOption(new Option<string>("--string-option"));
}
public class MyHandler : ICommandHandler
{
private readonly MyService _service;
public int IntArgument { get; set; }
public string StringOption { get; set; }
public IConsole Console { get; set; }
public MyHandler(MyService service)
{
_service = service;
}
public Task<int> InvokeAsync(InvocationContext context)
{
_service.Hello(Console, StringOption);
return Task.FromResult(IntArgument);
}
}
}
// DIによってインジェクションされるサービス
public class MyService
{
public void Hello(IConsole console, string name)
{
console.Out.WriteLine($"Hello {name}");
}
}
コマンドとコマンドハンドラーの定義
コマンドの定義は下記のとおりです。
public class MyCommand : Command
{
public MyCommand(): base(name: "mycommand")
{
AddArgument(new Argument<int>("IntArgument"));
AddOption(new Option<string>("--string-option"));
}
}
mycommand
という名前でコマンドを作成しています。
このコマンドはコマンドライン引数からIntArgument
という引数を1つと、--string-option
というオプション引数を受け取ることができます。
具体的には下記のようなコマンドライン引数を受け取ります。
46 --string-option hello
~~ ~~~~~~~~~~~~~~~~~~~~~
# IntArgument --string-option
コマンドハンドラーの実装ですが、System.CommandLine.Hostingを用いない、通常のコマンドハンドラー実装の場合は下記のようにCommandHandler.Create
を利用します。
https://blog.yucchiy.com/2021/03/intro-system-commandline/
// commandはCommandクラスのインスタンス
command.Handler = CommandHandler.Create<int, string>((intArgument, stringOption) =>
{
Console.WriteLine($"The value for IntArgument is: {intArgument}");
Console.WriteLine($"The value for --string-option is: {stringOption}");
});
System.CommandLine.Hostingを用いる場合は、下記のようにICommandHandler
を実装したクラスを用意します。
コマンドハンドラーへのDI
上記の引数およびオプション引数は、このクラスのプロパティにインジェクションされます。それらを受け取るためのIntArgument
およびStringOption
を定義します。
IntArgument
プロパティでnew Argument<int>("IntArgument")
で指定した引数を、StringOption
で、new Option<string>("--string-option")
で指定したオプション引数を取得できます。
// MyCommmandのネストクラスとして実装している
// (もちろんネストしなくてもOK)
public class MyHandler : ICommandHandler
{
// コンストラクタインジェクション経由で初期化
private readonly MyService _service;
// OptionやArgumentはプロパティインジェクションされる
public int IntArgument { get; set; }
public string StringOption { get; set; }
// コマンドラインの利用するコンソール情報もプロパティインジェクションされる
public IConsole Console { get; set; }
// 続きはこのあと
}
次にMyHandler
が利用するMyService
をDIします。DIはコンストラクタ経由で行います。
public MyHandler(MyService service)
{
_service = service;
}
このようにコンストラクタ引数として指定したMyService
が自動でDIされます。
コマンドの呼び出し
では上記のコマンドを実行するところをみていきます。通常は、Command.InvokeAsync
でコマンドを実行していましたが、
System.CommandLine.Hosting
を使う場合、CommandLineBuilder
経由でパーサーを作成し、そのパーサー経由でInvokeAsync
を呼び出します。
var builder = new CommandLineBuilder(new MyCommand());
builder.UseDefaults();
builder.UseHost(host =>
{
host.ConfigureServices((context, services) =>
{
services.AddTransient<MyService>();
});
host.UseCommandHandler<MyCommand, MyCommand.MyHandler>();
});
var parser = builder.Build();
// コマンドラインの呼び出し
// argsはコマンドライン引数が格納されたstring配列
var result = await parser.InvokeAsync(args);
ここで、CommandLineBuilder
に対してDI設定や、コマンドハンドラーを設定します。
// コマンドラインの標準機能を一通り初期化
builder.UseDefaults();
// ここでホスト設定(DIや設定ファイル、ログまわりの設定)
builder.UseHost(host =>
{
host.ConfigureServices((context, services) =>
{
// ここで依存定義
// これでMyServiceをコンストラクタインジェクションできる
services.AddTransient<MyService>();
});
// いままではcommand.Handler = CommandHandler.Create
// でやってた箇所をUseCommandHanderで定義する
host.UseCommandHandler<MyCommand, MyCommand.MyHandler>();
});
UseHost
でホスト設定を行います。DIの依存関係の定義は上記のhost.ConfigureService
内で行っています。こちらのドキュメントを参考に設定します。
またUserCommandHandler
で、Command
に対してどのコマンドハンドラーを利用するかを定義します。
UserDefaults
は、コマンドラインアプリケーションで必要な設定を一通り行ってくれるので呼び出しておきます。
まとめ
System.CommandLine.Hosting
について、その説明と利用方法を説明しました。