💻

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

2022/08/02に公開

はじめに

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

テーブル定義

今回は ProductProductLog という 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 については operationsCreate および 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