Avanade Beef でテンプレートを使用して生成されるコードをカスタマイズする

# はじめに
[Avanade Beef](https://github.com/Avanade/Beef) (以下、Beef) は ASP.NET Core をベースとする Web API の自動生成ツールです。

Beefについての説明はこのあたりを参照。
- [Web API 開発を加速しよう - Avanade Beef のご紹介](https://speakerdeck.com/karamem0/accelerate-your-web-api-development-introducing-avanade-beef)
- [Avanade Beef のレイヤー構造を理解する](https://zenn.dev/karamem0/articles/2022_06_15_200000)


今回は、Beefでテンプレートをどのように定義するかについて記載します。

# Beefでのコード生成
Beef では、エンティティの定義に対して各レイヤーの[ボイラープレートコード](https://ja.wikipedia.org/wiki/ボイラープレートコード)を自動生成し、これにより一定の品質を保ちつつコーディングの手間を軽減することが可能です。
さらに、標準で生成されるコード以上のことをしたくなった場合には、独自のコードテンプレートを追加することで生成されるコードをカスタマイズすることが可能となります。

以降、どのように独自のテンプレートを定義して、必要な処理をどのように組み込むかについて、実験した結果を基に説明します。

## 標準のテンプレート
標準のテンプレートは、[Github](https://github.com/Avanade/Beef)から取得したソースコード内の、/tools/Beef.CodeGen.Core/Templates 内にある *.hbs ファイルです。これらは、[Handlebars.Net](https://github.com/Handlebars-Net/Handlebars.Net) を用いて処理されます。

どのようなテンプレートか、以下に EntityWebApiController_cs.hbs のごく一部を抜粋します。
```handlebars
namespace {{Root.NamespaceApi}}.Controllers
{
    /// <summary>
    /// Provides the {{{EntityNameSeeComments}}} Web API functionality.
    /// </summary>
{{#ifval WebApiAuthorize}}
    [{{{WebApiAuthorize}}}]
{{/ifval}}
{{#ifval WebApiRoutePrefix}}
    [Route("{{WebApiRoutePrefix}}")]
{{/ifval}}
    public partial class {{Name}}Controller : ControllerBase
    {
{{#each WebApiCtorParameters}}
        private readonly {{Type}} {{PrivateName}};
  {{#if @last}}

  {{/if}}
{{/each}}
```
`{{` ～ `}}` で囲まれた部分がHandleBars.Netの制御文です。抜粋した範囲では、WebApiAuthorizeやNameなどの値を条件分岐に使用したり出力したりしています。これらの値は、定義ファイルに設定した値を基に Beef の前処理で加工された内容であるため、いつどのような値が入るかは Beef のソースコードを確認する必要があります。

ただし、テンプレートにすでに定義されているものを参考にして、テンプレートを編集するのであれば、それほど難しくはありません。

## 例: Controllerの各メソッドが呼び出されたときにログを出力する

まずは、対象のテンプレートをBeefのソースコードから自分のプロジェクトにコピーします。
今回は、Controllerのテンプレートである EntityWebApiController_cs.hbs を、自分の生成されたプロジェクトの_Company_._AppName_.CodeGen/Templates ディレクトリにコピーします。

次に、Visual Studioでこのファイルに対するビルド アクションを埋め込みリソースに設定します。(Visual Studioを使用しない場合は .csproj ファイルの EmbeddedResource 要素に追加 - 例: https://blog.yucchiy.com/2020/11/csharp-embedded-resources/)

このテンプレートファイル（以下、テンプレートファイル）に対する変更を行います。結果的にどのようなコードが生成されるかを考えながらテンプレートファイルに追加します。

まず、ログを出力するためのloggerが必要となります。毎回型を名前空間から指定するのは面倒なので、using を追加します。
```csharp
using Microsoft.Extensions.Logging;
```

次に、ログを出力するためのloggerをクラス内に定義する必要があります。
Beefでは、標準でメンバーを追加し、コンストラクタから渡すコードを生成するための属性があるため、_Company_._AppName_.CodeGen/_Company_._AppName_.yaml（以下、エンティティ定義ファイル）の各エンティティに以下のようにwebApiCtorParams 属性を定義します。

```yaml
entities:
- { name: SomeEntity,
    webApiCtorParams: ["ILogger<SomeEntityController>^Logger"],
    ...
}
```

webApiCtorParams 属性に定義した値は、「型名^メンバ名」というように解釈され、コンストラクタのパラメータとなるとともにメンバー変数としても定義され、コンストラクター内でメンバー変数に値が設定されます。したがって、生成されたクラスの各メソッド内で使用することが可能です。

準備ができたので、各メソッドでログを出力するコードをテンプレートファイルに追加します。

元々のテンプレートでは、HasFromEntityPropertiesParametersが満たされない場合にメソッド本体が一つの式となるため、ラムダ式の形で生成するようになっていますが、今回はログ出力を追加するためテンプレートファイルからラムダ式の部分を削除し、さらに、元の式は常にreturnをつける必要があるので、returnを囲っているifも削除します。
そのうえで、ログを出力するコードを追加します。

```diff_handlebars
-        public IActionResult {{Name}}({{#each PagingLessParameters}}{{#unless @first}}, {{/unless}}{{#ifeq WebApiFrom 'FromEntityProperties'}}{{#each RelatedEntity.Properties}}{{#unless @first}}, {{/unless}}{{#ifne ArgumentName JsonName}}[FromQuery(Name = "{{JsonName}}")] {{/ifne}}{{{WebApiParameterType}}} {{ArgumentName}} = {{#ifval Default}}{{Default}}{{else}}default{{/ifval}}{{/each}}{{else}}{{#ifne WebApiFrom 'FromQuery'}}[{{WebApiFrom}}] {{/ifne}}{{{WebApiParameterType}}} {{ArgumentName}}{{#ifval Default}} = {{Default}}{{/ifval}}{{/ifeq}}{{/each}}){{#unless HasFromEntityPropertiesParameters}} =>{{/unless}}
+        public IActionResult {{Name}}({{#each PagingLessParameters}}{{#unless @first}}, {{/unless}}{{#ifeq WebApiFrom 'FromEntityProperties'}}{{#each RelatedEntity.Properties}}{{#unless @first}}, {{/unless}}{{#ifne ArgumentName JsonName}}[FromQuery(Name = "{{JsonName}}")] {{/ifne}}{{{WebApiParameterType}}} {{ArgumentName}} = {{#ifval Default}}{{Default}}{{else}}default{{/ifval}}{{/each}}{{else}}{{#ifne WebApiFrom 'FromQuery'}}[{{WebApiFrom}}] {{/ifne}}{{{WebApiParameterType}}} {{ArgumentName}}{{#ifval Default}} = {{Default}}{{/ifval}}{{/ifeq}}{{/each}})
-  {{#if HasFromEntityPropertiesParameters}} 
        {
-  {{/if}}
+          _logger.LogTrace($"{nameof({{Name}})}({{#each PagingLessParameters}}{{#unless @first}}, {{/unless}}{{ArgumentName}}: { {{ArgumentName}} }{{/each}})");
   ...
- {{#if HasFromEntityPropertiesParameters}}return {{/if}}new WebApiDelete{{#if HasReturnValue}}<{{OperationReturnType}}>{{/if}}(this, () => _manager. ...
+ return new WebApiDelete{{#if HasReturnValue}}<{{OperationReturnType}}>{{/if}}(this, () => _manager. ...
   ...
-  {{#if HasFromEntityPropertiesParameters}} 
        }
-  {{/if}}

```

これで、テンプレートは出来上がりです。CodeGenプロジェクトを実行してコードを生成することで、変更したテンプレートを用いてコードが生成されます。

# おわりに
今回は、Beefを使用したアプリケーションでテンプレートをカスタマイズする方法について記載しました。
本稿を参考にすることで、Beefを使用した開発が少しでも楽になれば幸いです。