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