💻

Avanade Beef で複数のエンティティに書き込む

に公開

はじめに

Avanade Beef (以下、Beef) は ASP.NET Core をベースとした Web API の自動生成ツールです。

https://github.com/Avanade/Beef

概要については以下のスライドも参照してください。

Beef は既定でサービスインターフェイス層 (XxxController) からデータアクセス層 (XxxData) まで、1 つのエンティティを管理します。しかし、多くの場合はビジネスロジックを含み、複数のエンティティを操作する必要があります。今回は、ドメインロジック層 (XxxManager) をカスタマイズして複数のエンティティを操作する例を紹介します。

サンプルコード

https://github.com/karamem0/samples/tree/main/avanade-beef-write-multi-entities

実行手順

プロジェクトの作成

今回はデータソースに 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