應用程式服務
應用程式服務是一種無狀態的服務,它實作應用程式的用例,應用程式服務通常獲取和回傳dto,它由表示層使用,它使用并協調領域物件(物體、存盤庫等)來實作用例
應用程式服務的常見原則如下:
- 實作特定于當前用例的應用程式邏輯,不要在應用程式服務內部實作核心領域邏輯,我們將回到應用程式領域邏輯之間的差異
- 永遠不要為應用程式服務方法獲取或回傳物體,這打破了領域層的封裝,總是獲取和回傳dto
示例:分配問題給用戶
public class IssueAppService : ApplicationService, IIssueAppService
{
//省略了Repository和DomainService的依賴注入
[Authorize]
public async Task AssignAsync(IssueAssignDto input)
{
var issue = await _issueRepository.GetAsync(input.IssueId);
var user = await _userRepository.GetAsync(input.UserId);
await _issueManager.AssignToAsync(issue, user);
await _issueRepository.UpdateAsync(issue);
}
}
應用程式服務方法通常有三個步驟,在這里實作了
- 從資料庫中獲取相關的領域物件來實作用例
- 使用領域物件(領域服務、物體等)執行實際操作
- 更新已更改的物體到資料庫中
如果你正在使用EF Core,上面的更新是不必要的,因為它有一個更改跟蹤系統,如果你想利用這個EF Core特性,請參閱 關于資料庫無關心原則的討論部分
本例中的 IssueAssignDto 是一個簡單的DTO類:
public class IssueAssignDto
{
public Guid IssueId { get; set; }
public Guid UserId { get; set; }
}
資料傳輸物件(DTO)
DTO是一個簡單的物件,用于在應用程式層和表示層之間傳輸狀態(資料),因此,應用程式服務方法獲取和回傳dto
通用DTO原則和最佳實踐:
- 就其本質而言,DTO應該是可序列化的,因為,大多數時候它是通過網路傳輸的,因此,它應該有一個無引數(空)建構式,
- 不應該包含業務邏輯,
- 永遠不要繼承或參考物體,
輸入dto(傳遞給應用程式服務方法的dto)與輸出dto(從應用程式服務方法回傳的dto)具有不同的性質,所以,他們會被區別對待
輸入DTO最佳實踐
不要為輸入dto定義未使用的屬性
只定義用例所需的屬性! 否則,客戶端在使用Application Service方法時會感到困惑,您當然可以定義可選屬性,但是當客戶端提供它們時,它們應該影響用例的作業方式
首先,這條規則似乎沒有必要,誰會為方法定義未使用的引數(輸入DTO屬性)?但是,這種情況會發生,尤其是當您試圖重用輸入dto時,
不要復用輸入dto
為每個用例定義專門的輸入DTO(應用程式服務方法), 否則,某些屬性在某些情況下不使用,這違反了上面定義的規則:不要為輸入dto定義未使用的屬性
有時,為兩個用例重用同一個DTO類似乎很有吸引力,因為它們幾乎是相同的,即使他們現在是一樣的,他們可能會變成不同的時候,你會遇到相同的問題,代碼復制是比耦合用例更好的實踐
重用輸入dto的另一種方法是相互繼承dto,雖然這在一些罕見的情況下是有用的,但大多數情況下它會使你達到相同的目的
示例:用戶應用服務
public interface IUserAppService : IApplicationService
{
Task CreateAsync(UserDto input);
Task UpdateAsync(UserDto input);
Task ChangePasswordAsync(UserDto input);
}
IUserAppService 在所有方法(用例)中使用 UserDto 作為輸入DTO,UserDto的定義如下:
public class UserDto
{
public Guid UserId { get; set; }
public string UserName { get; set; }
public string Email { get; set; }
public string Password { get; set; }
public DateTime CreationTime { get; set; }
}
對于這個示例:
- Id 在創建中不使用,因為服務器會在創建用戶時自動生成它
- Password 沒有在更新中使用,因為我們有另一個更新密碼的方法
- CreationTime 從不被使用,因為我們不能允許客戶端發送創建時間,它應該在服務器中設定
真正的實作可以是這樣的:
public interface IUserAppService : IApplicationService
{
Task CreateAsync(UserCreationDto input);
Task UpdateAsync(UserUpdateDto input);
Task ChangePasswordAsync(UserChangePasswordDto input);
}
盡管撰寫了更多的代碼,但這是一種更易于維護的方法
例外情況:該規則可能有一些例外:如果您總是希望并行開發兩個方法,它們可能共享相同的輸入DTO(通過繼承或直接重用),例如,如果您有一個具有一些過濾器的報告頁面,并且您有多個Application Service方法(如螢屏報告、excel報告和csv報告方法)使用相同的過濾器但回傳不同的結果,您可能希望重用相同的過濾器輸入DTO來耦合這些用例,因為,在本例中,無論何時更改過濾器,您都必須對所有方法進行必要的更改,以擁有一致的報告系統,
輸入DTO驗證邏輯
- 只在DTO內部實作形式驗證,使用資料注釋驗證屬性或實作IValidatableObject 進行形式驗證,
- 不要執行領域驗證,例如,不要嘗試檢查dto中的唯一用戶名約束
示例:使用資料注釋屬性
public class UserCreationDto
{
[Required]
[StringLength(UserConsts.MaxUserNameLength)]
public string UserName { get; set; }
[Required]
[EmailAddress]
[StringLength(UserConsts.MaxEmailLength)]
public string Email { get; set; }
[Required]
[StringLength(UserConsts.MaxPasswordLength, MinimumLength = UserConsts.MaxMinPasswordLength)]
public string Password { get; set; }
}
ABP框架自動驗證輸入的dto,拋出AbpValidationException,并在無效輸入的情況下向客戶端回傳HTTP狀態400
一些開發人員認為最好將驗證規則和DTO類分開,我們認為宣告式(資料注釋)方法是實用和有用的,并且不會導致任何設計問題,但是,如果您喜歡其他方法,ABP也支持 FluentValidation集成,
有關所有驗證選項,請參 閱驗證檔案,
輸出DTO最佳實踐
- 保持輸出DTO計數最小,在可能的情況下重用(例外:不要將輸入dto重用為輸出dto)
- 輸出dto可以包含比客戶端代碼中使用的更多的屬性,
- 從創建和更新方法回傳物體DTO,
這些建議的主要目的是:
-
使客戶端代碼易于開發和擴展
- 在客戶端處理類似但不相同的dto是有問題的
- 將來在UI/客戶端上添加其他屬性是很常見的,回傳物體的所有屬性(通過考慮安全性和特權)使客戶端代碼很容易改進,而不需要接觸后端代碼
- 如果你將API開放給第三方客戶,而你不知道每個客戶的需求
-
使服務器端代碼易于開發和擴展
- 你需要理解和維護的類更少
- 您可以重用 Entity->DTO 物件映射代碼
- 從不同的方法回傳相同的型別使創建新方法變得簡單明了
示例:從不同的方法回傳不同的dto
public interface IUserAppService : IApplicationService
{
UserDto Get(Guid id);
List<UserNameAndEmailDto> GetUserNameAndEmail(Guid id);
List<string> GetRoles(Guid id);
List<UserListDto> GetList();
UserCreationResultDto Create(UserCreationDto input);
UserUpdateResultDto Update(UserUpdateDto input);
}
(我們沒有使用異步方法使示例更清晰,但在您的實際應用程式中使用異步!)
上面的示例代碼為每個方法回傳不同的DTO型別,您可以猜到,在查詢資料、將物體映射到dto時,會有很多代碼重復
上面的IUserAppService服務可以被簡化:
public interface IUserAppService : IApplicationService
{
UserDto Get(Guid id);
List<UserDto> GetList();
UserDto Create(UserCreationDto input);
UserDto Update(UserUpdateDto input);
}
統一使用單個輸出DTO:
public class UserDto
{
public Guid Id { get; set; }
public string UserName { get; set; }
public string Email { get; set; }
public DateTime CreationTime { get; set; }
public List<string> Roles { get; set; }
}
- 洗掉了GetUserNameAndEmail 和 GetRoles,因為Get方法已經回傳必要的資訊
- GetList 現在與Get回傳相同的結果
- 創建和更新也回傳相同的UserDto
如前所述,使用相同的輸出DTO有許多優點,例如,設想一個場景,您在UI上顯示一個Users資料網格,在更新用戶之后,您可以獲得回傳值并在UI上更新它,你不需要再呼叫GetList,這就是為什么我們建議回傳物體DTO(這里是UserDto)作為創建和更新操作的回傳值
討論
有些輸出DTO建議可能不適合所有場景,由于性能原因,可以忽略這些建議,特別是當回傳的資料集很大,或者當您為自己的UI創建服務時,您有太多的并發請求
在這些情況下,您可能希望創建包含最少資訊的專用輸出dto,以上建議特別適用于那些維護代碼庫比忽略不計的性能損失更重要的應用程式
物件映射
當兩個物件具有相同或相似的屬性時,自動 物件映射 是將值從一個物件復制到另一個物件的有用方法
DTO和物體類通常具有相同/相似的屬性,通常需要從物體創建DTO物件,ABP的 物件映射系統 與 AutoMapper 集成使得這些操作比手動映射更容易,
- 只對物體使用自動物件映射來輸出DTO映射
- 不要對輸入DTO到物體的映射使用自動物件映射,
有一些原因不應該使用輸入DTO來進行物體自動映射:
- 物體類通常有一個接受引數并確保有效創建物件的建構式,自動物件映射操作通常需要一個空的建構式
- 大多數物體屬性將具有私有設定器,您應該使用方法以可控的方式更改這些屬性
- 您通常需要仔細地驗證和處理用戶/客戶端輸入,而不是盲目地映射到物體屬性
雖然其中一些問題可以通過映射配置來解決(例如,AutoMapper允許定義自定義映射規則),但它使您的業務代碼隱式/隱藏,并與基礎設施緊密耦合,我們認為業務代碼應該是明確的、清晰的和容易理解的
請參閱下面的物體創建一節,以獲得本節建議的示例實作,
轉載請註明出處,本文鏈接:https://www.uj5u.com/net/495143.html
標籤:.NET Core
