OnionArch 2.0

2023-02-07 18:01:03

大家好,去年我釋出了一篇 OnionArch - 採用DDD+CQRS+.Net 7.0實現的洋蔥架構 很多程式設計師都比較感興趣,給我要原始碼。這次我把OnionArch進行了升級,改進了一些特性,並放出原始碼,iamxiaozhuang/OnionArch2 (github.com)   歡迎大家研究使用。

一、自動生成和釋出領域事件

我在OninArch1.0實現了對刪除的實體自動生成和釋出領域事件,並通過訂閱這個領域事件,將刪除的實體資料備份至回收站表中,以備審計和資料恢復。

本次我改進了這個特性,對實體資料的新增,修改和刪除都會自動生成和釋出領域事件。我認為儘量不要通過修改程式碼來新增和釋出領域事件,這會導致新增的業務功能也需要修改程式碼而不是新增程式碼來實現,不符合對修改關閉和對擴充套件開放的設計原則。應該對實體資料的任何變動都自動釋出領域事件,然後在事件Handler中篩選需要的領域事件並進行處理。

我基於這個特性實現了按設定自動審計記錄和和按設定自動釋出整合事件功能。

按設定自動審計功能

我們可以通過設定的方式來實現那些實體,那種修改型別,那個欄位需要審計。請看如下設定:

"EntityChangedAuditLogsConfig": [
    {
      "EntityFullName": "OnionArch.Domain.ProductInventory.ProductInventory",
      "ChangeType": "Added"
    },
    {
      "EntityFullName": "OnionArch.Domain.ProductInventory.ProductInventory",
      "ChangeType": "Modified",
      "Properties": "InventoryAmount"
    },
    {
      "EntityFullName": "OnionArch.Domain.ProductInventory.ProductInventory",
      "ChangeType": "Deleted"
    }
  ],

可以按照實體的全名,修改型別(新增,修改,刪除),甚至是實體的修改欄位來設定是否需要進行資料審計,如過需要審計則會自動儲存審計紀錄檔到審計表,審計表包含實體的原值和當前值,修改人和修改時間等。

按設定自動釋出整合事件功能

 我們可以通過設定來實現該微服務的那些領域事件需要轉為整合事件釋出出去,供其它的微服務訂閱使用。這樣我們在微服務中新增整合事件訂閱的時候就不需要修改源微服務的程式碼,只需要在源微服務中增加設定即可。

我們需要設定Dapr的釋出訂閱名稱,事件Topic和這個Topic的釋出條件,例如,在產品倉庫實體的庫存數量被修改後釋出Topic為「ProductInventoryAmountChanged」整合事件。

"EntityChangedIntegrationEventConfig": {
    "PubsubName": "pubsub",
    "Topics": [
      {
        "TopicName": "ProductInventoryAmountChanged",
        "EntityFullName": "OnionArch.Domain.ProductInventory.ProductInventory",
        "ChangeType": "Modified",
        "Properties": "InventoryAmount"
      }
    ]
  },

然後就可通過Dapr在其它的微服務中訂閱並處理該整合事件,通過Dapr釋出整合事件的程式碼請檢視原始碼。

已釋出的整合事件也會自動儲存至整合事件記錄表中,以備對該事件進行後續執行跟蹤和重發。

二、自動生成Minimal WebApi介面

該特性我在 根據MediatR的Contract Messages自動生成Minimal WebApi介面 中做過介紹。因為OninArch通過MediatR實現了CQRS和其它AOP功能,例如業務實體驗證,例外處理、工作單元等特性。

本次將OninArch1.0的GRPC介面替換成了自動生成的WebAPI介面。並對自動生成WebAPI介面做了改進,可以指定生成的WebAPI的Http方法,地址、介紹和詳細說明。自動對介面按名稱空間分類,將Get方法引數自動對映到Query引數等。

 [MediatorWebAPIConfig(HttpMethod = HttpMethodToGenerate.Post, HttpUrl = "/productinventory", Summary = "建立產品庫存", Description = "建立產品庫存 Description")]
    public class CreateProductInventory : ICommand<Unit>
    {
        public string ProductCode { get; set; }
        public int InventoryAmount { get; set; }
    }
    [MediatorWebAPIConfig(HttpMethod = HttpMethodToGenerate.Patch, HttpUrl = "/productinventory/increase", Summary = "增加產品庫存")]
    public class IncreaseProductInventory : ICommand<Unit>
    {
        public Guid Id { get; set; }
        public int Amount { get; set; }
    }

生成的WebAPI:

 

 三、對充血模型的支援

我在OninArch1.0中並沒有刻意強調充血模型,本次按照我對充血模型的理解改進了倉儲程式碼,即,倉儲服務只實現實體的Add,Remove和Query,不實現實體的Create和Modify。實體的建立和修改必須放入實體中實現。也就是說,實體欄位的set都是私有的,只能在實體內部對實體的欄位進行修改,以保證將業務邏輯封裝到實體中,並提高系統的穩定性和業務邏輯重用性。

 public static ProductInventory Create<TModel>(TModel model)
        {
            //var entity = new ProductInventory();
            var entity = model.Adapt<ProductInventory>();
            return entity;
        }

        public ProductInventory Update<TModel>(ProductInventory entity, TModel model)
        {
            model.Adapt(entity);
            return entity;
        }

        public int InventoryAmount { get; private set; }

        public void IncreaseInventory(int amount)
        {
            this.InventoryAmount += amount;
        }

倉儲介面新增了Edit方法,以獲取實體物件,再呼叫實體物件內部方法進行實體資料的修改。

倉庫介面的Query方法不直接返回實體物件,而是直接返回Model物件(Dto、VO),提高資料庫查詢效能(通過Mapster的ProjectToType方法實現)。

倉儲服務程式碼如下:

using MediatR;
using OnionArch.Domain.Common.Entities;
using OnionArch.Domain.Common.Paged;
using System.Linq.Expressions;

namespace OnionArch.Domain.Common.Repositories
{
    public class RepositoryService<TEntity> where TEntity : BaseEntity
    {
        private readonly IMediator _mediator;

        public RepositoryService(IMediator mediator)
        {
            _mediator = mediator;
        }

        /// <summary>
        /// 建立單個實體
        /// </summary>
        /// <param name="entity"></param>
        /// <returns></returns>
        public async Task<TEntity> Add(TEntity entity)
        {
            return await _mediator.Send(new AddEntityRequest<TEntity>(entity));
        }
        /// <summary>
        /// 建立多個實體
        /// </summary>
        /// <param name="entities"></param>
        /// <returns></returns>
        public async Task Add(params TEntity[] entities)
        {
            await _mediator.Send(new AddEntitiesRequest<TEntity>(entities));
        }

        /// <summary>
        /// 刪除單個實體
        /// </summary>
        /// <param name="Id"></param>
        /// <returns></returns>
        public async Task<TEntity> Remove(Guid Id)
        {
            return await _mediator.Send(new RemoveEntityRequest<TEntity>(Id));
        }
        /// <summary>
        /// 刪除多個實體
        /// </summary>
        /// <param name="whereLambda"></param>
        /// <returns></returns>
        public async Task<int> Remove(Expression<Func<TEntity, bool>> whereLambda)
        {
            return await _mediator.Send(new RemoveEntitiesRequest<TEntity>(whereLambda));
        }

         /// <summary>
        /// 獲取單個實體以更新實體欄位
        /// </summary>
        /// <param name="Id"></param>
        /// <returns></returns>
        public async Task<TEntity> Edit(Guid Id)
        {
            return await _mediator.Send(new EditEntityRequest<TEntity>(Id));
        }

        public async Task<IQueryable<TEntity>> Edit(Expression<Func<TEntity, bool>> whereLambda)
        {
            return await _mediator.Send(new EditEntitiesRequest<TEntity>(whereLambda));
        }


        /// <summary>
        /// 查詢單個實體(不支援更新實體)
        /// </summary>
        /// <param name="Id"></param>
        /// <returns></returns>
        public async Task<TModel> Query<TModel>(Guid Id)
        {
            return await _mediator.Send(new QueryEntityRequest<TEntity,TModel>(Id));
        }
        /// <summary>
        /// 查詢多個實體
        /// </summary>
        public async Task<IQueryable<TModel>> Query<TModel>(Expression<Func<TEntity, bool>> whereLambda)
        {
            return await _mediator.Send(new QueryEntitiesRequest<TEntity,TModel>(whereLambda));
        }
        /// <summary>
        /// 分頁查詢多個實體
        /// </summary>
        /// <typeparam name="TOrder"></typeparam>
        /// <param name="whereLambda"></param>
        /// <param name="pageOption"></param>
        /// <param name="orderbyLambda"></param>
        /// <param name="isAsc"></param>
        /// <returns></returns>
        public async Task<PagedResult<TModel>> Query<TOrder,TModel>(Expression<Func<TEntity, bool>> whereLambda, PagedOption pagedOption, Expression<Func<TEntity, TOrder>> orderbyLambda, bool isAsc = true)
        {
            return await _mediator.Send(new QueryPagedEntitiesRequest<TEntity, TOrder,TModel>(whereLambda, pagedOption, orderbyLambda, isAsc));
        }

        /// <summary>
        /// 判斷是否有存在
        /// </summary>
        /// <param name="whereLambda"></param>
        /// <returns></returns>
        public async Task<bool> Any(Expression<Func<TEntity, bool>> whereLambda)
        {
            return await _mediator.Send(new AnyEntitiesRequest<TEntity>(whereLambda));
        }
    }
}
View Code

四、對採用MediatR代替介面的探索

如上倉庫服務程式碼,我並沒有建立倉庫介面並實現,而是完全基於MediatR直接實現倉庫服務。這個我在MediatRPC - 基於MediatR和Quic通訊實現的RPC框架,比GRPC更簡潔更低耦合,開源釋出第一版 的MediatR程式設計思想中做過介紹,本次是實現這個程式設計思想,即不通過介面和依賴注入,而是通過MediatR來實現控制反轉。如果大家不喜歡這種方式也可以修改回介面的方式。

鑑於篇幅所限,不能一一說明本次升級的所有改動,請大家下載程式碼自行研究,下面又到了找工作時間(是的,我還在找工作)。

五、找工作

▪ 博主有15年以上的軟體技術經驗(曾擔任架構師和技術 Leader),擅長雲原生、微服務和領域驅動軟體架構設計,.Net Core  開發。
▪ 博主有15年以上的專案交付經驗(曾擔任專案經理和產品經理),專注于敏捷(Scrum )專案管理,業務分析和產品設計。
▪ 博主熟練設定和使用 Microsoft Azure 和Microsoft 365雲(曾擔任微軟顧問)。
▪ 博主為人誠懇,工作認真負責,態度積極樂觀。

我家在廣州,也可以去深圳工作。做架構師、產品經理、專案經理都可以。有工作機會推薦的朋友可以加我微信 15920128707,微信名字叫Jerry。