OnionArch - 采用DDD+CQRS+.Net 7.0實現的洋蔥架構

博主最近失業在家,找工作之余,看了一些關于洋蔥(整潔)架構的資料和項目,有感而發,自己動手寫了個洋蔥架構解決方案,起名叫OnionArch。基于最新的.Net 7.0 RC1, 數據庫采用PostgreSQL, 目前實現了包括多租戶在內的12個特性。

該架構解決方案主要參考了NorthwindTraders,?sample-dotnet-core-cqrs-api?項目,?B站上楊中科的課程代碼以及博主的一些項目經驗。

洋蔥架構的示意圖如下:

e9e1c91ca561c82c6651d0d65eada54f.png

一、OnionArch 解決方案說明

解決方案截圖如下:
c27f246c19a28b7138dbbcea6d292c61.png

可以看到,該解決方案輕量化實現了洋蔥架構,每個層都只用一個項目表示。建議將該解決方案作為單個微服務使用,不建議在領域層包含太多的領域根。

源代碼分為四個項目:

1. OnionArch.Domain

- 核心領域層,類庫項目,其主要職責實現每個領域內的業務邏輯。設計每個領域的實體(Entity),值對象、領域事件和領域服務,在領域服務中封裝業務邏輯,為應用層服務。
- 領域層也包含數據庫倉儲接口,緩存接口、工作單元接口、基礎實體、基礎領域跟實體、數據分頁實體的定義,以及自定義異常等。

2. OnionArch.Infrastructure

- 基礎架構層,類庫項目,其主要職責是實現領域層定義的各種接口適配器(Adapter)。例如數據庫倉儲接口、工作單元接口和緩存接口,以及領域層需要的其它系統集成接口。
- 基礎架構層也包含Entity Framework基礎DbConext、ORM配置的定義和數據遷移記錄。

3. OnionArch.Application

- 應用(業務用例)層,類庫項目,其主要職責是通過調用領域層服務實現業務用例。一個業務用例通過調用一個或多個領域層服務實現。不建議在本層實現業務邏輯。
- 應用(業務用例)層也包含業務用例實體(Model)、Model和Entity的映射關系定義,業務實基礎命令接口和查詢接口的定義(CQRS),包含公共MediatR管道(AOP)處理和公共Handler的處理邏輯。

4. OnionArch.GrpcService

- 界面(API)層,GRPC接口項目,用于實現GRPC接口。通過MediatR特定業務用例實體(Model)消息來調用應用層的業務用例。
- 界面(API)層也包含對領域層接口的實現,例如通過HttpContext獲取當前租戶和賬號登錄信息。

二、OnionArch已實現特性說明

1.支持多租戶(通過租戶字段)

基于Entity Framework實體過濾器和實現對租戶數據的查詢過濾

protected override void OnModelCreating(ModelBuilder modelBuilder)

{
//加載配置
modelBuilder.ApplyConfigurationsFromAssembly(typeof(TDbContext).Assembly);//為每個繼承BaseEntity實體增加租戶過濾器
// Set BaseEntity rules to all loaded entity types
foreach (var entityType in GetBaseEntityTypes(modelBuilder))
{
var method = SetGlobalQueryMethod.MakeGenericMethod(entityType);
method.Invoke(this, new object[] { modelBuilder, entityType });
}
}

在BaseDbContext文件的SaveChanges之前對實體租戶字段賦值

//為每個繼承BaseEntity的實體的Id主鍵和TenantId賦值
var baseEntities = ChangeTracker.Entries<BaseEntity>();
foreach (var entry in baseEntities)
{
switch (entry.State)
{
case EntityState.Added:
if (entry.Entity.Id == Guid.Empty)
entry.Entity.Id = Guid.NewGuid();
if (entry.Entity.TenantId == Guid.Empty)
entry.Entity.TenantId = _currentTenantService.TenantId;
break;
}
}

多租戶支持全部在底層實現,包括租戶字段的索引配置等。開發人員不用關心多租戶部分的處理邏輯,只關注業務領域邏輯也業務用例邏輯即可。

2.通用倉儲和緩存接口

實現了泛型通用倉儲接口,批量更新和刪除方法基于最新的Entity Framework 7.0 RC1,為提高查詢效率,查詢方法全部返回IQueryable,包括分頁查詢,方便和其它實體連接后再篩選查詢字段。

3.領域事件自動發布和保存

在BaseDbContext文件的SaveChanges之前從實體中獲取領域事件并發布領域事件和保存領域事件通知,以備后查。

//所有包含領域事件的領域跟實體
var haveEventEntities = domainRootEntities.Where(x => x.Entity.DomainEvents != null && x.Entity.DomainEvents.Any()).ToList();
//所有的領域事件
var domainEvents = haveEventEntities
.SelectMany(x => x.Entity.DomainEvents)
.ToList();
//根據領域事件生成領域事件通知
var domainEventNotifications = new List<DomainEventNotification>();
foreach (var domainEvent in domainEvents)
{
domainEventNotifications.Add(new DomainEventNotification(nowTime, _currentUserService.UserId, domainEvent.EventType, JsonConvert.SerializeObject(domainEvent)));
}
//清除所有領域根實體的領域事件
haveEventEntities
.ForEach(entity => entity.Entity.ClearDomainEvents());
//生成領域事件任務并執行
var tasks = domainEvents
.Select(async (domainEvent) =>
{
await _mediator.Publish(domainEvent);
});
await Task.WhenAll(tasks);
//保存領域事件通知到數據表中
DomainEventNotifications.AddRange(domainEventNotifications);

領域事件發布和通知保存在底層實現。開發人員不用關心領域事件發布和保存邏輯,只關注于領域事件的定義和處理即可。

4.領域根實體審計信息自動記錄

在BaseDbContext文件的Savechanges之前對記錄領域根實體的審計信息。

//為每個繼承AggregateRootEntity領域跟的實體的AddedBy,Added,LastModifiedBy,LastModified賦值
//為刪除的實體生成實體刪除領域事件

DateTime nowTime = DateTime.UtcNow;
var domainRootEntities = ChangeTracker.Entries<AggregateRootEntity>();
foreach (var entry in domainRootEntities)
{
switch (entry.State)
{
case EntityState.Added:
entry.Entity.AddedBy = _currentUserService.UserId;
entry.Entity.Added = nowTime;
break;
case EntityState.Modified:
entry.Entity.LastModifiedBy = _currentUserService.UserId;
entry.Entity.LastModified = nowTime;
break;
case EntityState.Deleted:
EntityDeletedDomainEvent entityDeletedDomainEvent = new EntityDeletedDomainEvent(
_currentUserService.UserId,
entry.Entity.GetType().Name,
entry.Entity.Id,
JsonConvert.SerializeObject(entry.Entity)
);
entry.Entity.AddDomainEvent(entityDeletedDomainEvent);
break;
}
}

領域根實體審計信息記錄在底層實現。開發人員不用關心審計字段的處理邏輯。

5. 回收站式軟刪除

采用回收站式軟刪除而不采用刪除字段的軟刪除方式,是為了避免垃圾數據和多次刪除造成的唯一索引問題。
自動生成和發布實體刪除的領域事件,代碼如上。
通過MediatR Handler,接收實體刪除領域事件,將已刪除的實體保存到回收站中。

public class EntityDeletedDomainEventHandler : INotificationHandler<EntityDeletedDomainEvent>
{
private readonly RecycleDomainService _domainEventService;public EntityDeletedDomainEventHandler(RecycleDomainService domainEventService)
{
_domainEventService = domainEventService;
}public async Task Handle(EntityDeletedDomainEvent notification, CancellationToken cancellationToken)
{
var eventData = JsonSerializer.Serialize(notification);
RecycledEntity entity = new RecycledEntity(notification.OccurredOn, notification.OccurredBy, notification.EntityType, notification.EntityId, notification.EntityData);
await _domainEventService.AddRecycledEntity(entity);
}
}

6.CQRS(命令查詢分離)

通過MediatR IRequest 實現了ICommand接口和Iquery接口,業務用例請求命令或者查詢繼承該接口即可。

public interface ICommand : IRequest
{
}public interface ICommand<out TResult> : IRequest<TResult>
{
}
public interface IQuery<out TResult> : IRequest<TResult>
{}
public class AddCategoryCommand : ICommand
{
public AddCategoryRequest Model { get; set; }
}

代碼中的AddCategoryCommand 增加類別命令繼承ICommand。

7.自動工作單元Commit

通過MediatR 管道實現了業務Command用例完成后自動Commit,開發人員不需要手動提交。

public class UnitOfWorkProcessor<TRequest, TResponse> : IRequestPostProcessor<TRequest, TResponse> where TRequest : IRequest<TResponse>
{
private readonly IUnitOfWork _unitOfWork;public UnitOfWorkProcessor(IUnitOfWork unitOfWork)
{
_unitOfWork = unitOfWork;
}
public async Task Process(TRequest request, TResponse response, CancellationToken cancellationToken)
{
if (request is ICommand || request is ICommand<TResponse>)
{
await _unitOfWork.CommitAsync();
}
}
}

8.GRPC Message做為業務用例實體

通過將GRPC proto文件放入Application項目,重用其生成的message作為業務用例實體(Model)。

public class AddCategoryCommand : ICommand
{
public AddCategoryRequest Model { get; set; }
}

其中AddCategoryRequest 為proto生成的message。

9.通用CURD業務用例

在應用層分別實現了CURD的Command(增改刪)和Query(查詢) Handler。

開發人員只需要在GRPC層簡單調用即可實現CURD業務。

public async override Task<AddProductReply> AddProduct(AddProductRequest request, ServerCallContext context)
{
CUDCommand<AddProductRequest> addProductCommand = new CUDCommand<AddProductRequest>();
addProductCommand.Id = Guid.NewGuid();
addProductCommand.Model = request;
addProductCommand.Operation = "C";
await _mediator.Send(addProductCommand);
return new AddProductReply()
{
Message = "Add Product sucess"
};
}

10. 業務實體驗證

通過FluentValidation和MediatR 管道實現業務實體自動驗證,并自動拋出自定義異常。

開發人員只需要定義驗證規則即可

public class AddCategoryCommandValidator : AbstractValidator<AddCategoryCommand>
{
public AddCategoryCommandValidator()
{
RuleFor(x => x.Model.CategoryName).NotEmpty().WithMessage(p => "類別名稱不能為空.");
}
}

11.請求日志和性能日志記錄

基于MediatR 管道實現請求日志和性能日志記錄。

12. 全局異常捕獲記錄

基于MediatR 異常接口實現異常捕獲。

三、相關技術如下

* .NET Core 7.0 RC1

* ASP.NET Core 7.0 RC1

* Entity Framework Core 7.0 RC1

* MediatR 10.0.1

* Npgsql.EntityFrameworkCore.PostgreSQL?7.0.0-rc.1

*?Newtonsoft.Json 13.0.1

*?Mapster?7.4.0-pre03

*?FluentValidation.AspNetCore 11.2.2

* GRPC.Core 2.46.5

四、 找工作

博主有10年以上的軟件技術實施經驗(Tech Leader),專注于軟件架構設計、軟件開發和構建,專注于微服務和云原生(K8s)架構, .Net Core\Java開發和Devops。

博主有10年以上的軟件交付管理經驗(Project Manager,Product Ower),專注于敏捷(Scrum)項目管理、軟件產品業務分析和原型設計。

博主能熟練配置和使用 Microsoft Azure 和Microsoft 365 云平臺,獲得相關微軟認證和證書。

我家在廣州,也可以去深圳工作。做架構和項目管理都可以,希望能從事穩定行業的業務數字化轉型。有工作機會推薦的朋友可以加我微信 15920128707,微信名字叫Jerry.

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/282113.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/282113.shtml
英文地址,請注明出處:http://en.pswp.cn/news/282113.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

spark寫出分布式的訓練算法_利用 Spark 和 scikit-learn 將你的模型訓練加快 100 倍...

在 Ibotta&#xff0c;我們訓練了許多機器學習模型。這些模型為我們的推薦系統、搜索引擎、定價優化引擎、數據質量等提供動力。它們在與我們的移動應用程序交互時為數百萬用戶做出預測。當我們使用 Spark 進行數據處理時&#xff0c;我們首選的機器學習框架是 scikit-learn。隨…

理解LinkedHashMap

1. LinkedHashMap概述&#xff1a;LinkedHashMap是HashMap的一個子類&#xff0c;它保留插入的順序&#xff0c;如果需要輸出的順序和輸入時的相同&#xff0c;那么就選用LinkedHashMap。LinkedHashMap是Map接口的哈希表和鏈接列表實現&#xff0c;具有可預知的迭代順序。此實現…

MySQL - 鎖

一、什么是鎖 鎖是數據庫系統區別于文件系統的一個關鍵特性。鎖機制用于管理對共享資源的并發訪問。 二、MySQL 不同存儲引擎支持的鎖機制 存儲引擎支持的鎖類型Myisam表鎖Innodb行鎖、表鎖Memory表鎖BDB頁鎖、表鎖表鎖&#xff1a;直接鎖住的是一個表&#xff0c;開銷小&…

數據庫時區那些事兒 - MySQL的時區處理

原文地址 當JVM時區和數據庫時區不一致的時候&#xff0c;會發生什么&#xff1f;這個問題也許你從來沒有注意過&#xff0c;但是當把Java程序容器化的時候&#xff0c;問題就浮現出來了&#xff0c;因為目前幾乎所有的Docker Image的時區都是UTC。本文探究了MySQL及其JDBC驅動…

java_函數的重載

函數的重載&#xff08;Overload&#xff09;概念&#xff1a;在同一個類中&#xff0c;允許存在一個以上的同名函數&#xff0c;只要他們的參數個數或者參數類型不同即可。函數功能一樣&#xff0c;僅僅是參與運算的未知內同不同時&#xff0c;可以定義多函數&#xff0c;卻使…

全新升級的AOP框架Dora.Interception[2]: 基于約定的攔截器定義方式

Dora.Interception&#xff08;github地址&#xff0c;覺得不錯不妨給一顆星&#xff09;有別于其他AOP框架的最大的一個特點就是采用針對“約定”的攔截器定義方式。如果我們為攔截器定義了一個接口或者基類&#xff0c;那么攔截方法將失去任意注冊依賴服務的靈活性。除此之外…

redis watch使用場景_redis不得不會的事務玩法

我們都知道redis追求的是簡單&#xff0c;快速&#xff0c;高效&#xff0c;在這種情況下也就拒絕了支持window平臺&#xff0c;學sqlserver的時候&#xff0c;我們知道事務還算是個比較復雜的東西&#xff0c;所以這吊毛要是照搬到redis中去&#xff0c;理所當然redis就不是那…

加快Android Studio的編譯速度

從Eclipse切換到Android Studio后&#xff0c;感覺Android Studio的build速度比Eclipse慢很多&#xff0c;以下幾個方法可以提高Android Studio的編譯速度使用Gradle 2.4Gradle 2.4對執行性能有很大的優化&#xff0c;但Android Studio現在默認使用的是Gradle 2.2,所以我們需要…

開發中 MySQL 規范

一、建表規范 1、數據庫名、表名、字段名必須使用小寫字母或數字&#xff0c;并且禁止以數字開頭 示例&#xff1a;goods_category、agent_operate_201812_log 2、數據庫名、表名、字段名要做到見名識意 示例&#xff1a;goods_category&#xff0c;不能 gc 3、配置表建議以 …

PaddleOCR在 Linux下的webAPI部署方案

很多小伙伴在使用OCR時都希望能采用API的方式調用&#xff0c;這樣就可以跨端跨平臺了。本文將介紹一種基于python的PaddleOCR識別WebAPI部署方案。喜歡的可以關注公眾號&#xff0c;獲取更多內容。一、 Linux環境下部署1.環境要求操作系統&#xff1a;CenterOS7&#xff1b;主…

影響程序員生涯的三個錯誤觀念,你千萬不要犯!

程序員在社會上&#xff0c;到底是怎樣一個生活群體&#xff1f;是否能找到自己方向&#xff1f;其實&#xff0c;路一直都在那里&#xff0c;只是你看不到而已&#xff01; 當初的你&#xff0c;可能一直被一些技術牽著鼻子走&#xff0c;并不是自己在做著自己想做的&#xff…

心電圖計算心率公式_心電圖到底能反應啥問題,看過之后你也能當“醫生”

只要是經歷過健康體檢的健康人&#xff0c;或者做過手術的患者&#xff0c;基本都做過心電圖檢查。都說久病成醫&#xff0c;所以有些人對血、尿常規等各項檢查的結果都門清兒得很&#xff0c;最起碼看一眼也能說出個大概齊。偏偏心電圖這種常做的檢查&#xff0c;不但老病號如…

獲取正在運行的服務

手機上安裝的App&#xff0c;在后臺運行著很多不同功能的服務&#xff0c;最常見的例如消息推送相關的服務。如何查看這些服務&#xff1f;如何判斷某個服務是否正在運行&#xff1f;如何停止某一個服務呢&#xff1f;請看下面的方法&#xff1a; package com.example.servicel…

openstack的vnc啟動ssl

1、制作ssl證書# cd /etc/pki/tls/certs [rootwww certs]# make vnc.key Enter pass phrase:# 輸入密碼 Verifying - Enter pass phrase:#確認# 從private key 中刪除密碼# openssl rsa -in vnc.key -out vnc.key # make vnc.csr Country Name (2 letter code) [XX]:CN# 國家 S…

開發composer包

一、初始化&#xff08;生成composer.json文件&#xff09; composer init#輸入你要創建的composer包項目命名空間 Package name (<vendor>/<name>) [root/tiny-laravel]: #haveyb/tiny-laravel #輸入composer包的描述 Description []:#this is a tiny laravel h…

Linux本地yum源配置以及使用yum源安裝gcc編譯環境

本文檔是圖文安裝本地yum源的教程&#xff0c;以安裝gcc編譯環境為例。 適用范圍&#xff1a;所有的cetos,紅帽,fedroa版本 適用人群&#xff1a;有一點linux基礎的小白 范例系統版本&#xff1a;CentOS Linux release 7.3.1611 (Core) 范例環境&#xff1a;vmware 虛擬機 安裝…

word如何設置上標形式_如何在word中設置特殊頁碼

獲取更多業界資訊和深度好文● 點擊藍字關注我們 ●在日常工作中&#xff0c;我們編輯的word文檔經常需要設置頁碼&#xff0c;但有時文檔的第一頁是封面&#xff0c;第二頁才是正文&#xff0c;或者第二頁是目錄&#xff0c;第三頁才是正文&#xff0c;如下圖所示&#xff0c;…

[cf797c]Minimal string(貪心+模擬)

題意&#xff1a; 給出了字符串s的內容&#xff0c;字符串t&#xff0c;u初始默認為空&#xff0c;允許做兩種操作&#xff1a; 1、把s字符串第一個字符轉移到t字符串最后 2、把t字符串最后一個字符轉移到u字符串最后 最后要求s、t字符串都為空&#xff0c;問u字符串字典序最小…

發布composer包到 Packagist,并設置自動同步(從github到Packagist)

一、發布composer包 1、將我們寫好的項目包發布到github上 這一步不贅述&#xff0c;應該都會。 但是需要注意的是&#xff0c;我們一定要為我們的項目包打上tag之后再提交&#xff0c;否則 我們composer require時可能會報錯 Could not find a version of package。 # 設置…

教你在CorelDRAW中導入位圖

在CorelDRAW軟件中不能直接打開位圖圖像&#xff0c;在實際操作中&#xff0c;用戶需要使用導入位圖圖像的方法進行操作。導入位圖圖像時&#xff0c;可以導入整幅圖像&#xff0c;也可以在導入的過程中對圖像進行裁剪&#xff0c;或重新取樣圖像&#xff0c;導入整幅位圖圖像時…