C# MicroBatchFramework バッチクラスの定義に関する検証


## MicroBatchFramework とは

[【GitHub】MicroBatchFramework](https://github.com/Cysharp/MicroBatchFramework) より引用

> MicroBatchFramework is an infrastructure of creating CLI(Command-line interface) tools, daemon, and multiple contained batch program. Easy to bind argument to the simple method definition. It built on .NET Generic Host so you can configure Configuration, Logging, DI, etc can load by the standard way.

コンソールアプリケーション向けのフレームワークです。こう言ってしまうと省略しすぎですが、コマンドラインで指定されたメソッドやパラメーターを簡単な手順でバインドさせることができます。.NET Core の機能拡張に伴い、マイクロサービスなどコンソールアプリケーションの用途が増えていくことが予想される中で、このようなインフラストラクチャの拡充には非常に注目しています。

## 検証内容

手始めとして、バッチクラスの定義に関する検証を行いました。

* 別アセンブリに定義したバッチクラスのメソッドを起動できるか
* Command 属性を付与しないメソッドが存在してもよいか

`MicroBatchFramework` のバージョンは 1.2.0 です。

## 検証に使用したクラス

定義するアセンブリと名前空間が異なる次の4つのバッチクラスを定義しました。

| クラス | 名前空間 | 説明 |
|:--|:--|:--|
| SampleBatch1 | NetCoreApp1 | 起動アセンブリのルート名前空間に定義したバッチクラス |
| SampleBatch2 | NetCoreApp1.Batches | 起動アセンブリのルート名前空間の配下の名前空間に定義したバッチクラス |
| SampleBatch3 | NetCoreApp1 | 起動アセンブリのルート名前空間外に定義したバッチクラス |
| SampleBatch4 | BatchLibrary1 | 別アセンブリに定義したバッチクラス |

メソッドの処理内容はすべて同じであるため割愛しています。コンソールにログを出力するだけです。

```c#
// 起動アセンブリのルート名前空間に定義したバッチクラス
namespace NetCoreApp1
{
    class SampleBatch1 : BatchBase
    {
        [Command("exec1")]
        public void Execute(string name1, int repeat1 = 3)
        {
            for (int i = 0; i < repeat1; i++)
            {
                this.Context.Logger.LogInformation($"{this.GetType().Name}.Execute from {name1} ({i + 1}/{repeat})");
            }
        }
    }
}

// 起動アセンブリのルート名前空間の配下に定義したバッチクラス
namespace NetCoreApp1.Batches
{
    class SampleBatch2 : BatchBase
    {
        [Command("exec2")]
        public void Execute(string name2, int repeat2 = 3) {}
    }
}

// 起動アセンブリのルート名前空間外に定義したバッチクラス
namespace Batches
{
    class SampleBatch3 : BatchBase
    {
        [Command("exec3")]
        public void Execute(string name3, int repeat3 = 3) {}
    }
}

// 別アセンブリに定義したバッチクラス
namespace BatchLibrary1
{
    public class SampleBatch4 : BatchBase
    {
        [Command("exec4")]
        public void Execute(string name4, int repeat4 = 3) {}
    }
}
```

## 検証に使用したバッチファイル

list, help コマンドと、バッチクラスに定義したメソッドを実行します。

```console
@echo ===== list =====
@dotnet NetCoreApp1.dll list


@echo ===== help =====
@dotnet NetCoreApp1.dll help


@echo ===== exec1 =====
@dotnet NetCoreApp1.dll exec1 -name1 "test" -Repeat1 3

@echo ===== SampleBatch1.Execute =====
@dotnet NetCoreApp1.dll SampleBatch1.Execute -name1 "test" -Repeat1 3

@echo ===== exec2 =====
@dotnet NetCoreApp1.dll exec2 -name2 "test" -Repeat2 3

@echo ===== SampleBatch2.Execute =====
@dotnet NetCoreApp1.dll SampleBatch2.Execute -name2 "test" -Repeat2 3

@echo ===== exec3 =====
@dotnet NetCoreApp1.dll exec3 -name3 "test" -Repeat3 3

@echo ===== SampleBatch3.Execute =====
@dotnet NetCoreApp1.dll SampleBatch3.Execute -name3 "test" -Repeat3 3

@echo ===== exec4 =====
@dotnet NetCoreApp1.dll exec4 -name4 "test" -Repeat4 3

@echo ===== SampleBatch4.Execute =====
@dotnet NetCoreApp1.dll SampleBatch4.Execute -name4 "test" -Repeat4 3
@dotnet NetCoreApp1.dll BatchLibrary1.SampleBatch4.Execute -name4 "test" -Repeat4 3
```


## ホストの起動方法

### バッチクラスを指定せずに起動

RunBatchEngineAsync メソッドで起動します。

```c#
static async Task Main(string[] args)
{
    await BatchHost.CreateDefaultBuilder().RunBatchEngineAsync(args);
}
```

### バッチクラスの型を指定して起動

RunBatchEngineAsync&lt;T&gt; メソッドで起動します。

```c#
static async Task Main(string[] args)
{
    // 起動アセンブリのルート名前空間の配下に定義したバッチクラス
    await BatchHost.CreateDefaultBuilder().RunBatchEngineAsync<SampleBatch1>(args);
}

static async Task Main(string[] args)
{
    // 起動アセンブリのルート名前空間の配下の名前空間に定義したバッチクラス
    await BatchHost.CreateDefaultBuilder().RunBatchEngineAsync<Batches.SampleBatch2>(args);
}

static async Task Main(string[] args)
{
    // 起動アセンブリのルート名前空間外に定義したバッチクラス
    await BatchHost.CreateDefaultBuilder().RunBatchEngineAsync<global::Batches.SampleBatch3>(args);
}

static async Task Main(string[] args)
{
    // 別アセンブリに定義したバッチクラス
    await BatchHost.CreateDefaultBuilder().RunBatchEngineAsync<BatchLibrary1.SampleBatch4>(args);
}
```

## 実行結果

### list コマンド

* 起動アセンブリに定義されているバッチクラスのメソッドが列挙されます。
* 起動アセンブリ以外のアセンブリに定義されているバッチクラスを指定して起動した場合、そのアセンブリに定義されているバッチクラスも列挙対象に追加されるようです。

```console:実行コマンド
@echo ===== list =====
@dotnet NetCoreApp1.dll list
```

```console:バッチクラスを指定せずに起動
===== list =====
list of methods:
SampleBatch3.Execute
SampleBatch1.Execute
SampleBatch1.ExecuteNotCommand
SampleBatch2.Execute
```

```console:起動アセンブリのルート名前空間に定義したバッチクラスSampleBatch1を指定して起動
===== list =====
list of methods:
SampleBatch3.Execute
SampleBatch1.Execute
SampleBatch1.ExecuteNotCommand
SampleBatch2.Execute
```

```console:起動アセンブリのルート名前空間の配下の名前空間に定義したバッチクラスSampleBatch2を指定して起動
===== list =====
list of methods:
SampleBatch3.Execute
SampleBatch1.Execute
SampleBatch1.ExecuteNotCommand
SampleBatch2.Execute
```

```console:起動アセンブリのルート名前空間外に定義したバッチクラスSampleBatch3を指定して起動
===== list =====
list of methods:
SampleBatch3.Execute
SampleBatch1.Execute
SampleBatch1.ExecuteNotCommand
SampleBatch2.Execute
```

```console:別アセンブリに定義したバッチクラスSampleBatch4を指定して起動
===== list =====
list of methods:
SampleBatch3.Execute
SampleBatch1.Execute
SampleBatch1.ExecuteNotCommand
SampleBatch2.Execute
SampleBatch4.Execute
```

### helpコマンド

* RunBatchEngineAsync メソッドでホストを起動した場合
  * エラーが発生します。
  * help の後にメソッド名を指定すれば、そのメソッドのヘルプを表示することができます。メソッドに対して付与した Command 属性のコマンド名を指定した場合はエラーになります。
* RunBatchEngineAsync&lt;T&gt; メソッドでホストを起動した場合
  * T に指定したバッチクラスのメソッドのヘルプが列挙されます。

```実行コマンド
@echo ===== help =====
@dotnet NetCoreApp1.dll help
```

```console:バッチクラスを指定せずに起動
===== help =====
Type or method does not found on this Program. args: help
```

```console:バッチクラスを指定せずに起動（helpの後にメソッド名を指定）
> dotnet NetCoreApp1.dll help SampleBatch1.Execute
exec1:
-name1: String
-repeat1: [default=3]Int32
```

```console:起動アセンブリのルート名前空間に定義したバッチクラスSampleBatch1を指定して起動
===== help =====
exec1:
-name1: String
-repeat1: [default=3]Int32
```

```console:起動アセンブリのルート名前空間の配下の名前空間に定義したバッチクラスSampleBatch2を指定して起動
===== help =====
exec2:
-name2: String
-repeat2: [default=3]Int32
```

```console:起動アセンブリのルート名前空間外に定義したバッチクラスSampleBatch3を指定して起動
===== help =====
exec3:
-name3: String
-repeat3: [default=3]Int32
```

```console:別アセンブリに定義したバッチクラスSampleBatch4を指定して起動
===== help =====
exec4:
-name4: String
-repeat4: [default=3]Int32
```

### メソッド実行

* RunBatchEngineAsync メソッドでホストを起動した場合
  * 起動アセンブリに定義されているバッチクラスのメソッドを実行できます。参照アセンブリに定義されているバッチクラスは実行できません。
  * コマンドラインで指定するメソッド名には、そのメソッドのクラス名と型名（{クラス名}.{メソッド名}）を指定します。
* RunBatchEngineAsync&lt;T&gt; メソッドでホストを起動した場合
  * T で指定したバッチクラスのメソッドを実行できます。
  * コマンドラインで指定するメソッド名には、そのメソッドに対して Command 属性で設定したコマンド名を指定します。

```console:実行コマンド
@echo ===== exec1 =====
@dotnet NetCoreApp1.dll exec1 -name1 "test" -Repeat1 3

@echo ===== SampleBatch1.Execute =====
@dotnet NetCoreApp1.dll SampleBatch1.Execute -name1 "test" -Repeat1 3

@echo ===== exec2 =====
@dotnet NetCoreApp1.dll exec2 -name2 "test" -Repeat2 3

@echo ===== SampleBatch2.Execute =====
@dotnet NetCoreApp1.dll SampleBatch2.Execute -name2 "test" -Repeat2 3

@echo ===== exec3 =====
@dotnet NetCoreApp1.dll exec3 -name3 "test" -Repeat3 3

@echo ===== SampleBatch3.Execute =====
@dotnet NetCoreApp1.dll SampleBatch3.Execute -name3 "test" -Repeat3 3

@echo ===== exec4 =====
@dotnet NetCoreApp1.dll exec4 -name4 "test" -Repeat4 3

@echo ===== SampleBatch4.Execute =====
@dotnet NetCoreApp1.dll SampleBatch4.Execute -name4 "test" -Repeat4 3
@dotnet NetCoreApp1.dll BatchLibrary1.SampleBatch4.Execute -name4 "test" -Repeat4 3
```

```console:バッチクラスを指定せずに起動
===== exec1 =====
Type or method does not found on this Program. args: exec1 -name1 test -Repeat1 3

===== SampleBatch1.Execute =====
SampleBatch1.Execute from test (1/3)
SampleBatch1.Execute from test (2/3)
SampleBatch1.Execute from test (3/3)

===== exec2 =====
Type or method does not found on this Program. args: exec2 -name2 test -Repeat2 3

===== SampleBatch2.Execute =====
SampleBatch2.Execute from test (1/3)
SampleBatch2.Execute from test (2/3)
SampleBatch2.Execute from test (3/3)

===== exec3 =====
Type or method does not found on this Program. args: exec3 -name3 test -Repeat3 3

===== SampleBatch3.Execute =====
SampleBatch3.Execute from test (1/3)
SampleBatch3.Execute from test (2/3)
SampleBatch3.Execute from test (3/3)

===== exec4 =====
Type or method does not found on this Program. args: exec4 -name4 test -Repeat4 3

===== SampleBatch4.Execute =====
Type or method does not found on this Program. args: SampleBatch4.Execute -name4 test -Repeat4 3
Type or method does not found on this Program. args: BatchLibrary1.SampleBatch4.Execute -name4 test -Repeat4 3
```

```console:起動アセンブリのルート名前空間に定義したバッチクラスSampleBatch1を指定して起動
===== exec1 =====
SampleBatch1.Execute from test (1/3)
SampleBatch1.Execute from test (2/3)
SampleBatch1.Execute from test (3/3)

===== SampleBatch1.Execute =====
exec1:
-name1: String
-repeat1: [default=3]Int32

===== exec2 =====
exec1:
-name1: String
-repeat1: [default=3]Int32

===== SampleBatch2.Execute =====
exec1:
-name1: String
-repeat1: [default=3]Int32

===== exec3 =====
exec1:
-name1: String
-repeat1: [default=3]Int32

===== SampleBatch3.Execute =====
exec1:
-name1: String
-repeat1: [default=3]Int32

===== exec4 =====
exec1:
-name1: String
-repeat1: [default=3]Int32

===== SampleBatch4.Execute =====
exec1:
-name1: String
-repeat1: [default=3]Int32

exec1:
-name1: String
-repeat1: [default=3]Int32
```

```console:起動アセンブリのルート名前空間の配下の名前空間に定義したバッチクラスSampleBatch2を指定して起動
===== exec1 =====
exec2:
-name2: String
-repeat2: [default=3]Int32

===== SampleBatch1.Execute =====
exec2:
-name2: String
-repeat2: [default=3]Int32

===== exec2 =====
SampleBatch2.Execute from test (1/3)
SampleBatch2.Execute from test (2/3)
SampleBatch2.Execute from test (3/3)

===== SampleBatch2.Execute =====
exec2:
-name2: String
-repeat2: [default=3]Int32

===== exec3 =====
exec2:
-name2: String
-repeat2: [default=3]Int32

===== SampleBatch3.Execute =====
exec2:
-name2: String
-repeat2: [default=3]Int32

===== exec4 =====
exec2:
-name2: String
-repeat2: [default=3]Int32

===== SampleBatch4.Execute =====
exec2:
-name2: String
-repeat2: [default=3]Int32

exec2:
-name2: String
-repeat2: [default=3]Int32
```

```console:起動アセンブリのルート名前空間外に定義したバッチクラスSampleBatch3を指定して起動
===== exec1 =====
exec3:
-name3: String
-repeat3: [default=3]Int32

===== SampleBatch1.Execute =====
exec3:
-name3: String
-repeat3: [default=3]Int32

===== exec2 =====
exec3:
-name3: String
-repeat3: [default=3]Int32

===== SampleBatch2.Execute =====
exec3:
-name3: String
-repeat3: [default=3]Int32

===== exec3 =====
SampleBatch3.Execute from test (1/3)
SampleBatch3.Execute from test (2/3)
SampleBatch3.Execute from test (3/3)

===== SampleBatch3.Execute =====
exec3:
-name3: String
-repeat3: [default=3]Int32

===== exec4 =====
exec3:
-name3: String
-repeat3: [default=3]Int32

===== SampleBatch4.Execute =====
exec3:
-name3: String
-repeat3: [default=3]Int32

exec3:
-name3: String
-repeat3: [default=3]Int32
```

```console:別アセンブリに定義したバッチクラスSampleBatch4を指定して起動
===== exec1 =====
exec4:
-name4: String
-repeat4: [default=3]Int32

===== SampleBatch1.Execute =====
exec4:
-name4: String
-repeat4: [default=3]Int32

===== exec2 =====
exec4:
-name4: String
-repeat4: [default=3]Int32

===== SampleBatch2.Execute =====
exec4:
-name4: String
-repeat4: [default=3]Int32

===== exec3 =====
exec4:
-name4: String
-repeat4: [default=3]Int32

===== SampleBatch3.Execute =====
exec4:
-name4: String
-repeat4: [default=3]Int32

===== exec4 =====
SampleBatch4.Execute from test (1/3)
SampleBatch4.Execute from test (2/3)
SampleBatch4.Execute from test (3/3)

===== SampleBatch4.Execute =====
exec4:
-name4: String
-repeat4: [default=3]Int32

exec4:
-name4: String
-repeat4: [default=3]Int32
```

## Command 属性を付与しないメソッド

Command 属性はメソッドに対してコマンド名や説明を設定するための属性ですが、この属性が付与されていないメソッドが存在した場合、若干挙動が変わります。

```c#
namespace NetCoreApp1
{
    class SampleBatch1 : BatchBase
    {
        [Command("exec1")]
        public void Execute(string name1, int repeat1 = 3) {}
        // Command属性を付与しない
        public void ExecuteNotCommand(string name, int repeat = 3) {}
    }
}
```

### list コマンド

* Command 属性が付与されていないメソッドも列挙対象に含まれます。

### help メソッド

* RunBatchEngineAsync メソッドでホストを起動した場合
  * Command 属性が付与されていないメソッドも、help コマンドの後にメソッド名（{クラス名}.{メソッド名}）を指定すれば表示されます。
* RunBatchEngineAsync&lt;T&gt; メソッドでホストを起動し、Command 属性が付与されていないメソッドが T で指定されたバッチクラスのメソッドである場合
  * そのメソッドのヘルプも表示されますが、コマンド名には "argument list" と表示されます。コマンド名には空の string 配列が設定されているのではないかと予想されます。
* RunBatchEngineAsync&lt;T&gt; メソッドでホストを起動し、Command 属性が付与されていないメソッドが T で指定されたバッチクラスのメソッドでない場合
  * そのメソッドのヘルプは表示されません。

### メソッド実行

* RunBatchEngineAsync メソッドでホストを起動した場合
  * Command 属性が付与されていないメソッドも実行できます。
* RunBatchEngineAsync&lt;T&gt; メソッドでホストを起動し、Command 属性が付与されていないメソッドが T で指定されたバッチクラスのメソッドである場合
  * そのメソッドも、メソッド名（{クラス名}.{メソッド名}）を指定すれば実行できます。
  * T で指定された型以外のバッチクラスのメソッドを呼び出したときにコンソールに表示される内容が変わります。「コマンド名が見つからない」というエラーが表示されます。

```
Required parameter "name" not found in argument. args: exec2 -name2 test -Repeat1 3
```

* RunBatchEngineAsync&lt;T&gt; メソッドでホストを起動し、Command 属性が付与されていないメソッドが T で指定されたバッチクラスのメソッドでない場合
  * そのメソッドは実行できません。

## まとめ

一つのアプリケーションに複数のメソッドを実装する場合、一つのバッチクラスに集約するか複数のバッチクラスに分割するかによってホストの起動方法が変わります。コマンドラインの指定方法も変わります。

* 一つのバッチクラスに集約
  * RunBatchEngineAsync&lt;T&gt; メソッドでホストを起動すれば、Command 属性で設定したコマンド名で指定するこ        とができます。
  * 実行対象のバッチクラスは起動アセンブリから参照できる型であればよく、別アセンブリに定義されていても構いません。
* 複数のバッチクラスに分割
  * RunBatchEngineAsync&lt;T&gt; メソッドでは単一のバッチクラスのメソッドしか実行できないため、RunBatchEngineAsync メソッドでホストを起動することになります。
  * Command 属性で設定したコマンド名ではなく、メソッド名（{クラス名}.{メソッド名}）指定することになります。
  * 実行対象のバッチクラスは起動アセンブリに定義する必要があります。

Command 属性はコマンド名や説明を設定できますし、付与することに対するデメリットはなさそうです。必ず付与するルールにしてもよさそうです。