ConsoleAppFramework(旧MicroBatchFramework)を用いたAzure Functionsの実装例と良いところ紹介


<blockquote class="twitter-tweet"><p lang="ja" dir="ltr">ConsoleAppFrameworkリリースしました！破壊的大変更でより便利に！なりました。 / “neue cc - ConsoleAppFramework - .NET Coreコンソールアプリ作成のためのマイクロフレームワーク（旧MicroBatchFramework)” <a href="https://t.co/CEXtQ4iJwp">https://t.co/CEXtQ4iJwp</a></p>&mdash; neuecc (@neuecc) <a href="https://twitter.com/neuecc/status/1215183642814664705?ref_src=twsrc%5Etfw">January 9, 2020</a></blockquote> 

とのことで記事修正しました。

#はじめに
ConsoleAppFrameworkを用いてAzure Functionsにアプリをデプロイしてみたのでその方法を紹介し、ConsoleAppFrameworkの良いところについても記載してみます。

##ConsoleAppFrameworkとは
+ [ConsoleAppFramework - .NET Coreコンソールアプリ作成のためのマイクロフレームワーク（旧MicroBatchFramework)](http://neue.cc/2020/01/09_588.html)
+ [ConsoleAppFramework — Micro-framework for console applications, for C# .NET Core](https://medium.com/@neuecc/consoleappframework-micro-framework-for-console-applications-for-c-net-core-4e69daa7e149)
+ [GitHub][ConsoleAppFramework]
+ [MicroBatchFramework – クラウドネイティブ時代のC#バッチフレームワーク][]

詳細は上記参照なのですが、GUIを作成するためのフレームワークとして[Prism](https://github.com/PrismLibrary/Prism)等があるように、CUIを作成する際にもフレームワークを用いましょうという感じです。

#ざっくりやりたいこと
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109302/261ab612-fbcb-ea55-0570-a7ac9f92a1da.png)

上図のように(定期的に)ネット上から情報取得して整形フィルタリングしてネット上のどこかに出力(通知)したいなと考えていました。
情報元の数だけコンソールアプリケーション(バッチスクリプト)を用意してcronにでも仕掛ければ実現できる内容です。

#ConsoleAppFrameworkを採用した理由
[MicroBatchFramework – クラウドネイティブ時代のC#バッチフレームワーク][]から引用
> すでにC#にはコマンドライン引数の解析ツールはたくさんあります。とはいえ、そもそもそういうツールを使う時は「コマンドライン引数の解析」がしたいわけではなくて、「パラメータバインディング」をしたいのが一般的と思われます。ということで、『MicroBatchFramework』はウェブフレームワークのようにメソッドを呼び出してくれる仕様にしました。

という点にとても共感しました。これを用いればひとつのコンソールアプリケーションで複数のユースケースを使い分けられると思いました。

##ざっくりエントリーポイントの紹介

```C#
using ConsoleAppFramework;
using Microsoft.Extensions.Hosting;

class Program
{
    public static async Task Main(string[] args)
    {
            await Host
                .CreateDefaultBuilder()
                .RunConsoleAppFrameworkAsync(args);
    }
}
```

コンソールアプリのエントリーポイントに上記内容を記述するだけで使えます。

#サーバーレスアーキテクチャへの変更
cronのためだけにサーバを起動し続けるのももったいないのでサーバーレスアーキテクチャを採用してみることにしました。
[Azure Logic Appsの繰り返しトリガー](https://docs.microsoft.com/ja-jp/azure/logic-apps/logic-apps-workflow-actions-triggers#recurrence-trigger)と[HttpトリガーのAzure Function](https://docs.microsoft.com/ja-jp/azure/azure-functions/functions-triggers-bindings)の組み合わせでcronちっくことが実現できました。
デザイナーで見ると
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/109302/833948a4-4d72-6f97-3f0d-979c7ebc3e6e.png)
こんな感じでjsonでコンソールアプリケーションに入力する内容を伝えれば済みます。

##Azure Function例
``` C#
    public static class Function1
    {
        [FunctionName("Function1")]
        public static async Task<IActionResult> Run(
            [HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] HttpRequest req,
            ILogger log)
        {
            await Program.Main(new[] { (string)req.Query["exec"] });

            return (ActionResult)new OkObjectResult("OK");
        }
    }
```

コンソールアプリケーションといってもエントリーポイントは文字列配列を引数としたstaticメソッドなので外部から呼び出せます。
jsonで取得した情報を文字列配列に置きなおしてキックすれば動きます。
これの何がすごいかって、実装やテストはコンソールアプリケーションで行ってデプロイする際は軽くラップするだけで済むってところです。
いちいちサーバ上やローカルにAzure Functionとして配置して動作確認しなくても良いのです。

さらに良かった点は[Microsoft.Extensions.Logging.ILogger][]の使い回し。
Azure Functionのエントリーポイントで渡されるILoggerをMicroBatchFrameworkに設定しなおせばデフォルトではコンソールに出力していた情報も[Application Insights](https://docs.microsoft.com/ja-jp/azure/azure-monitor/app/app-insights-overview)等に出力させることができます。
具体的には以下な感じ

```C#:ILoggerProviderを継承したクラスを実装
    public class MyLoggerProvider : ILoggerProvider
    {
        private ILogger Logger { get; }
        public MyLoggerProvider(ILogger _logger) => Logger = _logger;
        public ILogger CreateLogger(string categoryName) => Logger;
        public void Dispose() { }
    }
```

``` C#:MyLoggerProviderを生成してコンソールアプリケーションに渡す
    public static class Function1
    {
        [FunctionName("Function1")]
        public static async Task<IActionResult> Run(
            [HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] HttpRequest req,
            ILogger log)
        {
            await Program.MainFunction(new[] { (string)req.Query["exec"] }, new MyLoggerProvider(log));

            return (ActionResult)new OkObjectResult("OK");
        }
    }
```

```C#:コンソールアプリのエントリーポイントとは別に入り口を用意
        public static async Task MainFunction(string[] args, ILoggerProvider _loggerProvider)
        {
            await Host.CreateDefaultBuilder()
                .ConfigureLogging((ILoggingBuilder l) => { l.ClearProviders(); l.AddProvider(_loggerProvider); })
                .RunConsoleAppFrameworkAsync(args);
        }
```

[ConfigureLogging](https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.hosting.webhostbuilderextensions.configurelogging?view=aspnetcore-2.2)にて(既存のプロバイダを削除して)MyLoggerProviderを追加してあげればAzure Functionで指定しているログ場所へ出力されるようになります。
これでコンソールアプリの時では発生しないAzure Function固有の問題の調査も容易になる思います。

#ConsoleAppFrameworkを採用することで良くなると思うところ

##コンソールアプリ(バッチスクリプト)を使い捨てにしなくて良くなる
業務系だと

+ 黒魔術で書かれたスクリプト
+ ソースがなく挙動がブラックボックスなコンソールアプリ

みたいなのがちらほらあって、いざサーバをリプレースするぞって時に困るんですよね。(動作確認とか作り直しとか)
ConsoleAppFrameworkを採用してコンテナ化して運用すれば環境にもよらなくなるしソースもあるしひとつのアプリで済むので管理が楽になると思います。

## CUI <-> GUI の垣根がなくなる
例えばGUIで実装していた機能(ユースケース)をCUIとして提供したいというときに

+ GUIでもフレームワーク採用(Prismとか)
+ DDD
+ SOLID
+ オニオンアーキテクチャ(クリーンアーキテクチャ)

を意識してちゃんと機能(ユースケース)単位で注入できるように設計していたならばConsoleAppFrameworkに乗せ換えることは容易だと思うんですよね。
逆も然りなんですけど。
簡単なバッチ処理でもちゃんとユビキタス言語を用い業務知識として蓄えておけば、いざというとき二度手間をしなくても良くなる気がします。

#まとめ
CUI側にもフレームワークを導入することでGUIと機能の共通化が行えるのはとても魅力的です。
今後はConsoleAppFramework固有の機能についても紹介できればと思います。


[MicroBatchFramework – クラウドネイティブ時代のC#バッチフレームワーク]:http://tech.cygames.co.jp/archives/3241/
[ConsoleAppFramework]:https://github.com/Cysharp/ConsoleAppFramework
[Microsoft.Extensions.Logging.ILogger]:https://docs.microsoft.com/ja-jp/aspnet/core/fundamentals/logging/?view=aspnetcore-2.2