應用程式服務 應用程式服務是一種無狀態的服務,它實現應用程式的用例。應用程式服務通常獲取和返回dto。它由表示層使用。它使用並協調領域對象(實體、存儲庫等)來實現用例 應用程式服務的常見原則如下: 實現特定於當前用例的應用程式邏輯。不要在應用程式服務內部實現核心領域邏輯。我們將回到應用程式領域邏輯之 ...
應用程式服務
應用程式服務是一種無狀態的服務,它實現應用程式的用例。應用程式服務通常獲取和返回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允許定義自定義映射規則),但它使您的業務代碼隱式/隱藏,並與基礎設施緊密耦合。我們認為業務代碼應該是明確的、清晰的和容易理解的
請參閱下麵的實體創建一節,以獲得本節建議的示例實現。