ConsoleAppFrameworkでC#のCLIアプリを開発する

ConsoleAppFramework は UniTask や MemoryPack などを提供されている Cysharp が開発しているライブラリの1つで、 C# でコンソールアプリを手軽に開発することができます。

今回は ConsoleAppFramework を使用して簡単なコンソールアプリを開発する方法について紹介します。


はじめに

C# のコンソールアプリを開発するには Visual Studio のコンソールアプリテンプレートを使用してシンプルに実装する、 汎用ホストを使用し ASP.NET Core と同様の DI やログ、設定ファイルの機能を活用しながら実装する、などが様々な方法が考えられます。

実装したい内容が単純なものであればこれらの手法だけで事足りますが、例えば複数の処理を実行し分ける、実行引数を扱う、などは意外と骨が折れる内容だったりします。

ConsoleAppFramework (旧 MicroBatchFramework ) は汎用ホスト上に実装されたコンソールアプリを開発するためのライブラリです。汎用ホスト上ですので ASP.NET Core と同様の環境で開発することができます。そして実行時に呼び出される処理の選択や引数のハンドリング、メインの処理の前後に別の処理を挟み込むことができるなど、かゆいところに手が届く機能が揃っています。

これらの理由から私はコンソールアプリ実装のベースとして ConsoleAppFramework を長らく利用しています。

github.com

本記事では ConsoleAppFramework を使用して簡単なコンソールアプリを開発する方法について簡単に紹介します。

なお先述の通り ConsoleAppFramework は汎用ホスト上に構成されており、 ASP.NET Core 開発時と同様の DI やロギング、設定ファイルの機能が使用できます。本記事では ConsoleAppFramework の機能のみの紹介とし、これら汎用ホストで使用できる機能には言及しないことをお断りしておきます。

試した環境

要素 バージョン
.NET SDK 7.0.400
ConsoleAppFramework 4.2.4

ConsoleAppFramework を使用する

ConsoleAppFramework を利用するには次の2点を守る必要があります。

  1. 処理を実装するクラスは ConsoleAppBase を継承する
  2. Program.csConsoleApp.Create(...).Run() をする

実のところ ConsoleAppBase は必ずしも利用する必要はなく、簡単な処理内容であれば全てを Program.cs のみで完結することも可能です。 しかし私が利用している対象では利用しないよりも利用する方がメリットが大きいため、本記事では利用する方法のみを紹介します。 利用しない方法が気になる方は README をご参照ください。

ConsoleAppFramework では help コマンドが自動的に追加され、 ビルドされた exe が実行できるコマンドの一覧が表示されます。これから紹介する内容はこのヘルプコマンドの結果も添えておきます。

# ヘルプコマンドを実行
./SampleApp.exe help
Usage: SampleApp <Command>

Commands:
  help       Display help.
  run
  version    Display version.

それではいくつかのパターンの使い方を見ていきましょう。

1つだけ処理を実装する

1つの処理のみを ConsoleAppFramework で実装するには ConsoleAppBase を継承したクラスを1つだけ用意します。

var app = ConsoleApp.Create(args);
app.AddCommands<Hoge>(); // 処理クラスをコマンドとして登録
app.Run();


// 処理クラス
class Hoge : ConsoleAppBase
{
    public void Run()
    {
        // 何か処理
    }
}

これを実行するにはメソッド名を exe 実行時の引数に渡します。

# コマンド一覧
Commands:
  help       Display help.
  run
  version    Display version.

# 実行
./SampleApp.exe run

複数の処理を実装する

複数の処理を ConsoleAppFramework で実装するには、 ConsoleAppBase を継承したクラスに複数のメソッドを実装する方法と複数の ConsoleAppBase を継承したクラスを用意する方法の2通りがあります。

まずは複数のメソッドを実装するパターンです。

var app = ConsoleApp.Create(args);
app.AddCommands<Hoge>(); // 処理クラスをコマンドとして登録
app.Run();


// 処理クラス
class Hoge : ConsoleAppBase
{
    public void Foo()
    {
        // 何か処理
    }

    public void Bar()
    {
        // 何か処理
    }
}

// ※コマンド一覧
// Commands:
//   bar
//   foo
//   help       Display help.
//   version    Display version.

コマンド一覧にメソッド名のコマンドが2個表示されました。

続いて複数の ConsoleAppBase を継承したクラスを実装するパターンです。

// CLIエントリーポイント
var app = ConsoleApp.Create(args);
app.AddAllCommandType(); // 全てのコマンドを自動登録
app.Run();


// 処理クラス
class Hoge : ConsoleAppBase
{
    public void Run()
    {
        // 何か処理
    }
}

// 処理クラス
class Fuga : ConsoleAppBase
{
    public void Run()
    {
        // 何か処理
    }
}

// ※コマンド一覧
// Commands:
// help Display help.
//   hoge run
//   fuga run
//   version     Display version.

複数の ConsoleAppBase を継承したクラスを実装すると、コマンドがメソッド名からクラス名とメソッド名のセットに変化しました。いずれの方法を使用するにせよ、複数の処理を実装することも簡単に実現できますね。

コマンドにパラメーターを設定する

ConsoleAppFramework ではコマンドのメソッドに引数を作ると、コマンド実行時の引数を受け取ることができます。メソッド引数にデフォルト値を設定することで任意のコマンド引数とすることもできます。

// CLIエントリーポイント
var app = ConsoleApp.Create(args);
app.AddCommands<Hoge>(); // 全てのコマンドを自動登録
app.Run();


// 処理クラス
class Hoge : ConsoleAppBase
{
    public void Run(string p1, string p2 = "test")
    {
        // 何か処理
    }
}

// ※コマンド一覧
// Commands:
//   help       Display help.
//   run
//   version    Display version.

// パラメーター一覧
// ./SampleApp.exe run help
// Usage: run [options...]
//
// Options:
//   --p1 <String>     (Required)
//   --p2 <String>     (Default: test)

引数は string 以外にも intDateTime 、はたまた独自のクラスオブジェクトまで設定ができます。クラスオブジェクトの場合はコマンド引数設定時に JSON を渡します。

// CLIエントリーポイント
var app = ConsoleApp.Create(args);
app.AddCommands<Hoge>(); // 全てのコマンドを自動登録
app.Run();


// 処理クラス
class Hoge : ConsoleAppBase
{
    public void Run(string p1, int p2, DateTime p3, HogeArgument p4)
    {
        // 何か処理
    }
}

// 引数クラス
class HogeArgument
{
    public string Prop1 { get; set; }
    public int Prop2 { get; set; }
}

// ※コマンド一覧
// Commands:
//   help       Display help.
//   run
//   version    Display version.

// パラメーター一覧
// Options:
//   --p1 <String>           (Required)
//   --p2 <Int32>            (Required)
//   --p3 <DateTime>         (Required)
//   --p4 <HogeArgument>     (Required)

// 実行
// .\SampleApp.exe run --p1 test --p2 1 --p3 2023-08-27 --p4 '{ "prop1": "p1", "prop2": 10 }'

終了コードを設定する

終了コードとは処理が正常に完了したのか否かを外部(呼び出し元)に対して通知するための値です。

.NET 環境で終了コードを扱うには Environment.ExitCode に値を設定しますが、 ConsoleAppFramework ではメソッドの戻り値の型を int Task<int> または ValueTask<int> とすることで自動的にメソッドの呼び出し結果が終了コードに設定されます。

class Hoge : ConsoleAppBase
{
    public async Task<int> Run()
    {
        // 何か処理

        // 終了コードの返却
        // 一般的に0は成功、0以外は失敗とされる
        return 0;
    }
}

処理の前後で追加の処理を行う

メインの処理の実行前後でなんらかの初期化や後処理、ログ出力などの処理を挟み込みたい場面があります。 ConsoleAppFramework ではフィルターとしてサポートされており、これは ASP.NET Core のミドルウェアと同様の機能になります。

フィルターを使用するには ConsoleAppFilter を継承したクラスを作成して Invoke メソッドの中身を実装します。第2引数の next が次のフィルターやメインの処理を実行するためのデリゲートになっていますので、この next を呼び出す前後に前処理や後処理を実装します。

class SampleFilter : ConsoleAppFilter
{
    public override async ValueTask Invoke(
        ConsoleAppContext context, Func<ConsoleAppContext, ValueTask> next)
    {
        // 前処理

        // 後続のフィルターの前処理+メインの処理+後続のフィルターの後処理
        await next(context);

        // 後処理
    }
}

フィルターを使用する方法は2種類あり、1つはグローバルフィルターに設定する方法、もう1つはコマンド毎に使用するフィルターを設定する方法です。

// グローバルフィルターに設定(全てのコマンドで使用される)
var app = ConsoleApp.Create(args, options =>
{
    options.GlobalFilters = new ConsoleAppFilter[]
    {
        new SampleFilter()
    };
});

// コマンド毎に設定
[ConsoleAppFilter(typeof(SampleFilter), Order = 10)]
class Hoge : ConsoleAppBase
{
    [ConsoleAppFilter(typeof(SampleFilter), Order = 10)]
    public void Run()
    {
        // 何か処理
    }
}

フィルターは大変強力な機能ではありますが、唯一残念に思うのはフィルターからメイン処理で返却した終了コードを取得する方法が無いことです。これを取ることができれば外部に送信しているログをもっと簡単に実装できるのですが。。

おわりに

本記事では C# でコンソールアプリを開発する際に使用している ConsoleAppFramework について簡単に紹介しました。

C# でコンソールアプリを作るためのライブラリはいくつか存在しますが、使い勝手が良いとは言えないものばかりだと感じています。 ConsoleAppFramework は破壊的変更が多いという欠点はあるものの、かゆいところに手が届く機能が揃っており、使い方も直感的で、とてもお勧めできるライブラリです。

執筆担当者プロフィール
中谷 大造

中谷 大造(日本ビジネスシステムズ株式会社)

情報システム部の中谷です。社内用スクラッチアプリの開発をしています。

担当記事一覧