共用方式為


在領域模型層中設計驗證

小提示

此內容是適用於容器化 .NET 應用程式的電子書.NET 微服務架構摘錄,可在 .NET Docs 或免費下載的 PDF 中取得,可脫機讀取。

.NET 微服務架構的容器化 .NET 應用程式電子書封面縮圖。

在 DDD 中,驗證規則可以視為不變。 聚合的主要責任是維護該聚合內所有實體在狀態變更過程中的不變性。

網域實體一律應該是有效的實體。 對於一個物件,必須一直保持為真的某些不變性。 例如,訂單項目的物件需有一個正整數的數量、品項名稱和價格。 因此,不變量的強制是領域實體的責任(尤其是聚合根),而且實體對象如果無效就不應存在。 不可變的規則只會以合約表示,而且違反合約時會引發例外狀況或通知。

其背後的原因是許多錯誤都因為對象處於他們不應該進入的狀態而發生。

假設我們現在有一個 SendUserCreationEmailService,它採用 UserProfile ...如何在該服務中確認 Name 不是 null? 我們再檢查一次嗎? 或者更有可能的是... 你懶得去檢查,只是抱著僥倖心理—希望有人在把它傳送給你之前就已經驗證過了。 當然,在使用測試驅動開發(TDD)時,我們應該撰寫的第一個測試之一是,如果我傳送一個名稱為空值的客戶,它應該引發錯誤。 但是,一旦我們開始反覆撰寫這類測試,我們就會意識到...“如果我們從未允許名稱變成 null,該怎麼辦?我們不會進行所有這些測試!

在領域模型層中實作驗證

驗證通常會在網域實體建構函式或可更新實體的方法中實作。 有多種方式可以實作驗證,例如驗證數據,並在驗證失敗時引發例外狀況。 還有更進階的設計模式,例如使用規格設計模式來進行驗證,以及使用通知設計模式來傳回錯誤集合,而不是每次驗證時都回傳例外。

驗證條件並擲回例外狀況

下列程式代碼範例示範透過引發例外狀況,在網域實體中驗證最簡單的方法。 在本節結尾的參考數據表中,您可以根據我們先前討論的模式,查看更進階實作的連結。

public void SetAddress(Address address)
{
    _shippingAddress = address?? throw new ArgumentNullException(nameof(address));
}

更好的範例應展示需要確保內部狀態沒有改變,或方法的所有變更已經完成。 例如,下列實作會讓對象處於無效狀態:

public void SetAddress(string line1, string line2,
    string city, string state, int zip)
{
    _shippingAddress.line1 = line1 ?? throw new ...
    _shippingAddress.line2 = line2;
    _shippingAddress.city = city ?? throw new ...
    _shippingAddress.state = (IsValid(state) ? state : throw new …);
}

如果狀態的值無效,那麼第一行地址和城市已經被更改。 這可能會使地址無效。

類似的方法可用於實體的建構函式中,引發例外狀況,以確保實體在建立后有效。

根據數據批注在模型中使用驗證屬性

資料註解,例如 Required 或 MaxLength 屬性,可以用來設定 EF Core 資料庫欄位屬性,正如 數據表對應 一節中詳細說明的那樣,但 它們不再適用於 EF Core 中的實體驗證 (方法 IValidatableObject.Validate 也同樣無法使用),這與自 .NET Framework 中的 EF 4.x 以來的情況不同。

資料註釋和 IValidatableObject 介面仍可在模型綁定期間用於模型驗證,和往常一樣,會在控制器動作被調用之前進行,但該模型應是 ViewModel 或 DTO,而這是 MVC 或 API 的考量,而不是領域模型的考量。

已理清概念上的差異,您仍然可以在實體類別中使用資料註解和IValidatableObject進行驗證,如果您的操作接收了實體類別對象參數,但這是不被推薦的。 在此情況下,驗證會發生在模型系結時,就在叫用動作之前,而且您可以檢查控制器的 ModelState.IsValid 屬性來檢查結果,但同樣地,它會發生在控制器中,而不是在 DbContext 中保存實體物件之前,如同 EF 4.x 之後所做的一樣。

您仍然可以藉由覆寫 DbContext 的 SaveChanges 方法,使用數據批注和 IValidatableObject.Validate 方法,在實體類別中實作自定義驗證。

您可以在 IValidatableObject中看到驗證實體的範例實作。 該範例不會執行以屬性為基礎的驗證,但它們應該很容易在相同的覆寫中使用反映來實作。

不過,從 DDD 的觀點來看,定義域模型最好保持精簡,以在實體的行為方法中使用例外狀況,或藉由實作規格和通知模式來強制執行驗證規則。

在應用層的 ViewModel 類別中使用資料註釋(而不是網域實體)來接受輸入,這樣可以在 UI 層進行模型驗證,這是合理的。 不過,不應該排除在領域模型中的驗證。

實作規格模式和通知模式來驗證實體

最後,在領域模型中實作驗證的更詳細方法,是結合通知模式來實作規格模式,如稍後所列的一些其他資源所述。

值得一提的是,您也可以只使用這些模式之一,例如,使用控件語句手動驗證,但使用通知模式來堆棧並傳回驗證錯誤清單。

在網域中使用延後驗證

處理網域中延遲驗證的方法有很多種。 在《 實施 Domain-Driven 設計》一書中,沃恩·弗農在驗證一節中討論這些內容。

雙步驟驗證

也請考慮雙步驟驗證。 對命令數據傳輸物件(DTO)使用欄位層級驗證,並在實體內使用領域層級驗證。 您可以藉由傳回結果物件而非例外狀況來達成此目的,以便更輕鬆地處理驗證錯誤。

例如,使用欄位驗證搭配數據批註,您不會複製驗證定義。 不過,在 DTO 的情況下,執行可以在伺服器端或用戶端進行(例如命令和 ViewModels)。

其他資源