Avanade Beef で複数のエンティティに書き込む
はじめに
Avanade Beef (以下、Beef) は ASP.NET Core をベースとする Web API の自動生成ツールです。
概要については以下のスライドもご覧ください。
Beef は既定でサービス インターフェイス層 (XxxController) からデータ アクセス層 (XxxData) まで 1 つのエンティティに対して管理します。しかし、多くの場合はビジネス ロジックを含み、複数のエンティティに対して操作を行います。今回は、ドメイン ロジック層 (XxxManager) をカスタマイズして複数のエンティティを操作する例を見てみたいと思います。
サンプル コード
実行手順
プロジェクトの作成
今回はデータ ソースを EntityFramework
としてプロジェクトを作成します。
dotnet new beef --company Karamem0 --appname SampleApplication --datasource EntityFramework
テーブル定義
今回は Product
と ProductLog
という 2 つのテーブルを作成します。Product
が追加または更新されたときにその情報を ProductLog
にログとして書き込むようにします。
コードの修正
database.beef.yaml
ストアド プロシージャおよび Entity Framework のモデルを定義します。
schema: SampleApplication
eventOutbox: false
entityScope: Autonomous
tables:
- name: Product
efModel: true
storedProcedures:
- name: Get
type: Get
- name: GetColl
type: GetColl
- name: Create
type: Create
- name: Update
type: Update
- name: Delete
type: Delete
- name: ProductLog
efModel: true
storedProcedures:
- name: Get
type: Get
- name: Create
type: Create
コードを自動生成します。
dotnet run all
entity.beef.yaml
エンティティを定義します。Product
については operations
に Create
および Update
を定義し managerCustom: true
とすることでドメイン ロジック層のカスタマイズを有効にします。また ProductLog
を参照するために managerCtorParams
でパラメーターを追加します。ProductLog
についてはサービス インターフェイス層やドメイン ロジック層が不要なため作成しないようにします。
entityScope: Autonomous
eventOutbox: None
appBasedAgentArgs: true
webApiAutoLocation: true
refDataText: true
databaseSchema: SampleApplication
entities:
- name: Product
webApiRoutePrefix: products
get: true
getAll: true
delete: true
collection: true
collectionResult: true
validator: ProductValidator
dataModel: true
autoImplement: Database
managerCtorParams:
- IProductLogDataSvc^logDataService
properties:
- name: ProductId
type: Guid
uniqueKey: true
identifierGenerator: IGuidIdentifierGenerator
- name: ProductName
type: string
- name: Price
type: decimal
- name: ChangeLog
type: ChangeLog
operations:
- name: Create
type: Create
managerCustom: true
- name: Update
type: Update
uniqueKey: true
managerCustom: true
- name: ProductLog
dataModel: true
autoImplement: Database
properties:
- name: LogId
type: Guid
uniqueKey: true
identifierGenerator: IGuidIdentifierGenerator
- name: ProductId
type: Guid
- name: ProductName
type: string
- name: Price
type: decimal
- name: ChangeLog
type: ChangeLog
operations:
- name: Create
type: Create
excludeWebApi: true
excludeWebApiAgent: true
excludeIManager: true
excludeManager: true
コードを自動生成します。
dotnet run all
Business/ProductManager.cs
managerCustom: true
としたので自動生成される ProductManager.cs の実装は以下のようになっています。
/// <summary>
/// Creates a new <see cref="Product"/>.
/// </summary>
/// <param name="value">The <see cref="Product"/>.</param>
/// <returns>The created <see cref="Product"/>.</returns>
public Task<Product> CreateAsync(Product value) => ManagerInvoker.Current.InvokeAsync(this, async () =>
{
await value.Validate().Mandatory().RunAsync(throwOnError: true).ConfigureAwait(false);
value.ProductId = await _guidIdentifierGenerator.GenerateIdentifierAsync<Product>().ConfigureAwait(false);
return Cleaner.Clean(await CreateOnImplementationAsync(value).ConfigureAwait(false));
}, BusinessInvokerArgs.Create);
/// <summary>
/// Updates an existing <see cref="Product"/>.
/// </summary>
/// <param name="value">The <see cref="Product"/>.</param>
/// <param name="productId">The Product Id.</param>
/// <returns>The updated <see cref="Product"/>.</returns>
public Task<Product> UpdateAsync(Product value, Guid productId) => ManagerInvoker.Current.InvokeAsync(this, async () =>
{
await value.Validate().Mandatory().RunAsync(throwOnError: true).ConfigureAwait(false);
value.ProductId = productId;
return Cleaner.Clean(await UpdateOnImplementationAsync(value, productId).ConfigureAwait(false));
}, BusinessInvokerArgs.Update);
ここで呼び出される CreateOnImplementationAsync
および UpdateOnImplementationAsync
メソッドを自前で実装する必要があります。そこで ProductManager.cs をパーシャル クラスとして追加します (Generated フォルダーにある ProductManager.cs は直接修正しないでください)。
namespace Karamem0.SampleApplication.Business
{
/// <summary>
/// Provides the <see cref="Product"/> business functionality.
/// </summary>
public partial class ProductManager : IProductManager
{
/// <summary>
/// Creates a new <see cref="Product"/>.
/// </summary>
/// <param name="value">The <see cref="Product"/>.</param>
/// <returns>The created <see cref="Product"/>.</returns>
public async Task<Product> CreateOnImplementationAsync(Product value)
{
var result = await _dataService.CreateAsync(value).ConfigureAwait(false);
await _logDataService.CreateAsync(new ProductLog()
{
LogId = await _guidIdentifierGenerator.GenerateIdentifierAsync(),
ProductId = value.ProductId,
ProductName = value.ProductName,
Price = value.Price,
}).ConfigureAwait(false);
return result;
}
/// <summary>
/// Updates an existing <see cref="Product"/>.
/// </summary>
/// <param name="value">The <see cref="Product"/>.</param>
/// <param name="productId">The Product Id.</param>
/// <returns>The updated <see cref="Product"/>.</returns>
public async Task<Product> UpdateOnImplementationAsync(Product value, Guid productId)
{
var result = await _dataService.UpdateAsync(value).ConfigureAwait(false);
await _logDataService.CreateAsync(new ProductLog()
{
LogId = await _guidIdentifierGenerator.GenerateIdentifierAsync(),
ProductId = value.ProductId,
ProductName = value.ProductName,
Price = value.Price,
}).ConfigureAwait(false);
return result;
}
}
}
ProductLog
にデータを詰め込むところは AutoMapper を使うともっとスマートにできますが今回は割愛します。
実行結果
プロジェクトをデバッグ実行します。ブラウザーで http://localhost:5000/swagger
を起動し /product
に POST や PUT を実行すると ProductLog
テーブルにログが追加されていくことを確認します。
おわりに
Beef では、特定のレイヤーをの動作を変更したり、そもそも生成しないようすることが簡単にできます。これらの組み合わせで Web API を柔軟にカスタマイズすることができます。
Discussion