続・ConsoleAppFrameworkとAWS CDKで爆速バッチ開発

こんにちは。

テックリードのTerukiです。

Advent Calendarで[ConsoleAppFrameworkとAWS CDKで爆速バッチ開発](https://qiita.com/omt_teruki/items/dae315c7e86722fe05e6)という記事を書いたのですが、作者の@neueccさんがこの記事を踏まえてConsoleAppFrameworkのv5に追加機能を入れてくださいました:sob:

https://neue.cc/2024/12/16_ConsoleAppFramewrok_v5_3_0.html

ConsoleAppFramework v5の思想も非常に共感していたのですが、上記の記事の使い方はv5のコンセプトの真逆を行く使い方だと思っていたのでIssueをオープンするなどもしていませんでした。

私の記事を見て改修を入れてくださったのは非常に感謝です。ありがとうございます！

今後もただの歯医者のシステムを作ってる会社というわけではない、コードレベルでプロダクトと向き合って行く組織にしていきたいなと思っています。

というわけで今回の記事はConsoleAppFrameworkをv5にしてみたというお話です。

# アップデート

今回は前回作ったリポジトリがあるので、それをベースにやっていこうと思います。

https://github.com/ohmyteeth/ConsoleAppFrameworkCDKBatchSample/tree/c067e176beb9cae965dba0289136c74e2ba5eb3b

まずはConsoleAppFramework本体のNuGetパッケージを更新。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3192685/746a4e92-dd80-0b93-8a4c-654dc3173724.png)


サンプルリポジトリなのでアップデート直後のエラーはこのくらいですが、実際のプロダクションコードではもうちょっと多く出ると思います。

基本的に機械的な対応をすればOKなので難しくはありません。

### Program.cs

v4では各種DI用NuGetパッケージが入っていたのですが、v5では入っていないので自分でいれる必要があります。

自分で設定をしても良いですが、かなり煩雑になるのでGeneric Hostのデフォルトの処理を使ってしまうのが楽でいいと思います。
というわけで `Microsoft.Extensions.Hosting` をインストール。

このNuGetパッケージを入れると `HostApplicationBuilder` に `ToConsoleAppBuilder` というスーパー便利メソッドが生えるのですぐ使います。

```cs
var builder = Host.CreateApplicationBuilder();

// DI登録やConfig処理をする

var app = builder.ToConsoleAppBuilder();
app.Run(args);
```

ConsoleAppFrameworkネームスペースはProgram.csでglobal usingしてしまうと便利です。

```cs
global using ConsoleAppFramework;
```

コマンド名をケバブケースにされるとちょっと面倒なので用意していただいたケバブケース変換を無効にする設定をします。

```cs
[assembly: ConsoleAppFrameworkGeneratorOptions(DisableNamingConversion = true)]
```

全体像はこんな感じ。

```cs
global using ConsoleAppFramework;
using Batch.Filters;
using Microsoft.Extensions.Hosting;

[assembly: ConsoleAppFrameworkGeneratorOptions(DisableNamingConversion = true)]

var builder = Host.CreateApplicationBuilder();
var app = builder.ToConsoleAppBuilder();

app.UseFilter<BatchFilter>(); // 後述

app.Run(args);
```
少ない。。。
実際はDIをコネコネすると思うのでもうちょっと長くはなると思いますが、ミニマムの初期化処理でこれだけで済むのは非常に良いですね。
これだけなら `CreateApplicationBuilder` を使わなくても良いかもと思うかもしれませんが、これは `IConfiguration` や `ILoggerFactory` の設定をやってくれるので特にデフォルトから弄る必要性がないなら使ってしまっていいかなと思います。

### BatchFilter

これはコマンド処理の前後に処理を挟むために使います。

ここが一番変わったのではと思うので先に全体像を載せてしまいます。

```cs
using Microsoft.Extensions.Logging;

namespace Batch.Filters;

internal class BatchFilter(ConsoleAppFilter next, ILogger<BatchFilter> Logger) : ConsoleAppFilter(next) {

    public override async Task InvokeAsync(ConsoleAppContext context, CancellationToken cancellationToken) {

        var timestamp = DateTimeOffset.UtcNow;
        var batchName = $"{context.Arguments[0]} {context.Arguments[1]}";
        Logger.LogInformation("{name} Batch started. {time}", batchName, timestamp.ToLocalTime());
        try {
            await Next.InvokeAsync(context, cancellationToken).ConfigureAwait(false);
            Logger.LogInformation("{name} Batch completed. {time} Elapsed: {elapsed}", batchName, DateTimeOffset.Now, DateTimeOffset.UtcNow - timestamp);
        } catch (Exception e) {
            Logger.LogError(e, "{name} Batch failed. {time} Elapsed: {elapsed}", batchName, DateTimeOffset.Now, DateTimeOffset.UtcNow - timestamp);
            throw;
        }
    }
}
```

contextにLoggerは含まれなくなったのでコンストラクタインジェクションで取ってきます。
contextの `Arguments` からコマンドライン引数が取得できるのでそれをLoggerに表示します。

この文章を書いていて思いましたが、バッチ名をログの引数に渡すくらいなら `ILogger<T>` の代わりに `ILoggerFactory` を取得してバッチ名のILoggerを `CreateLogger` するようにしたほうが賢いですね。。

一旦動いてるのでここではそのまま行きます:pray:

## Batch本体

v5ではConsoleAppBaseクラスがなくなったので一括でコマンドを登録する方法がありません。

その代替手段として、v5.3で `RegisterCommands` 属性を追加していただいたので早速使います。

```cs
using Batch.Attributes;

namespace Batch;

// サブコマンドとして認識させるためにバッチ名をnameofで渡す
[RegisterCommands(nameof(SampleBatch))]
public class SampleBatch {
    /// <summary>
    /// 19時に実行
    /// </summary>
    [Batch("0 10 * * *")]
    public void Run() {
        Console.WriteLine("Hello World!");
    }
}
```
`RegisterCommands`はConsoleAppFrameworkネームスペース内にありますが、global usingしているので良い感じです。
Riderだとどうか分かりませんが、私の環境のVisual StudioだとRegisterCommandsの行でAlt+Enterを押してusingを補完させようとしても何故か出てこなかったのでglobal usingしない場合はusingを手書きすることになります:innocent:

v5.3以前はProgram.csにコマンド登録コードを書かないといけませんでしたが、この属性によって1ファイルの編集で追加することができるようになりました。

ConsoleAppBaseを継承する代わりに属性を付けるだけと思えば手間はv4の時と変わってないので非常に良きです。

これで開発体験を損なわずにv5にすることができました！


# おわりに

v4の場合、.NET 6ベースのライブラリが使われているので自社のコードが.NET 8の場合若干チグハグな感じになってしまいずっと気になっていました。

ConsoleAppFrameworkのアップデートで今回の用途に限らずv5の使い勝手が非常に良くなったと思うのでv5への移行がしやすくなったのではないかなと思います。

尚、Oh my teethでは既にv5にアップデートした社内向けのプロダクションコードが既に本番稼働しています:exclamation:

厳密な測定はしていないですが、体感でも起動が早くなったと感じるのでSource Generatorすごいです。

今回分の修正もGitHubのサンプルリポジトリにpushしているので、そちらもよかったら確認してみてください。

https://github.com/ohmyteeth/ConsoleAppFrameworkCDKBatchSample

では。

# Oh my teethについて
Oh my teethでは未来の歯科体験を創るために日々活動しています。

Techチームではより良いユーザー体験を提供するべく、Webフロントエンドからバックエンド、スマホアプリに機械学習モデルなど、さまざまなプロダクトを開発しています。

一緒に未来の歯科体験を創りませんか？興味がある方は是非こちらを確認してください。

https://oh.careers/

カジュアル面談も可能なので気軽に応募してみてください！

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3192685/0138b492-e02b-852e-61fa-fc065b53d285.png" width="450" />