はじめに
Avanade Beef (以下、Beef) は ASP.NET Core をベースとする Web API の自動生成ツールです。
Beefについての説明はこのあたりを参照。
今回は、Beefでテンプレートをどのように定義するかについて記載します。
Beefでのコード生成
Beef では、エンティティの定義に対して各レイヤーのボイラープレートコードを自動生成し、これにより一定の品質を保ちつつコーディングの手間を軽減することが可能です。
さらに、標準で生成されるコード以上のことをしたくなった場合には、独自のコードテンプレートを追加することで生成されるコードをカスタマイズすることが可能となります。
以降、どのように独自のテンプレートを定義して、必要な処理をどのように組み込むかについて、実験した結果を基に説明します。
標準のテンプレート
標準のテンプレートは、Githubから取得したソースコード内の、/tools/Beef.CodeGen.Core/Templates 内にある *.hbs ファイルです。これらは、Handlebars.Net を用いて処理されます。
どのようなテンプレートか、以下に EntityWebApiController_cs.hbs のごく一部を抜粋します。
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 を追加します。
using Microsoft.Extensions.Logging;
次に、ログを出力するためのloggerをクラス内に定義する必要があります。
Beefでは、標準でメンバーを追加し、コンストラクタから渡すコードを生成するための属性があるため、Company.AppName.CodeGen/Company.AppName.yaml(以下、エンティティ定義ファイル)の各エンティティに以下のようにwebApiCtorParams 属性を定義します。
entities:
- { name: SomeEntity,
webApiCtorParams: ["ILogger<SomeEntityController>^Logger"],
...
}
webApiCtorParams 属性に定義した値は、「型名^メンバ名」というように解釈され、コンストラクタのパラメータとなるとともにメンバー変数としても定義され、コンストラクター内でメンバー変数に値が設定されます。したがって、生成されたクラスの各メソッド内で使用することが可能です。
準備ができたので、各メソッドでログを出力するコードをテンプレートファイルに追加します。
元々のテンプレートでは、HasFromEntityPropertiesParametersが満たされない場合にメソッド本体が一つの式となるため、ラムダ式の形で生成するようになっていますが、今回はログ出力を追加するためテンプレートファイルからラムダ式の部分を削除し、さらに、元の式は常にreturnをつける必要があるので、returnを囲っているifも削除します。
そのうえで、ログを出力するコードを追加します。
- 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を使用した開発が少しでも楽になれば幸いです。