共用方式為


使用 .NET 實作微服務領域模型

小提示

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

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

在上一節中,會說明設計領域模型的基本設計原則和模式。 現在是時候探索使用 .NET (純 C# 程式代碼) 和 EF Core 實作領域模型可能的方法。 您的領域模型只會由您的程式代碼組成。 它只會有 EF Core 模型需求,但不具有 EF 的實際相依性。 您不應該在網域模型中有 EF Core 或任何其他 ORM 的硬式相依性或參考。

自定義 .NET Standard 連結庫中的定義域模型結構

用於 eShopOnContainers 參考應用程式的資料夾組織會示範應用程式的 DDD 模型。 您可能會發現不同的資料夾組織會更清楚地傳達為您的應用程式所做的設計選擇。 如圖 7-10 所示,在訂購領域模型中,有兩個匯總:訂單匯總和購買者匯總。 每個匯總都是一組定義域實體和值對象,不過您也可以有由單一定義域實體(匯總根或根實體)所組成的匯總。

方案總管中 Ordering.Domain 專案的螢幕快照。

Ordering.Domain 專案的 [方案總管] 檢視,其中顯示包含 BuyerAggregate 和 OrderAggregate 資料夾的 AggregatesModel 資料夾,每個資料夾都包含其實體類別、值物件檔等等。

圖 7-10。 eShopOnContainers 中訂購微服務的領域模型結構

此外,領域模型層也包含存放庫合約(介面),這些合約是領域模型的基礎結構需求。 換句話說,這些介面會表達基礎結構層必須實作哪些存放庫和方法。 請務必將存放庫的實作放在基礎結構層連結庫中的領域模型層之外,因此領域模型層不會受到 API 或基礎結構技術類別的「污染」,例如 Entity Framework。

您也可以看到 SeedWork 資料夾,其中包含自訂基類,您可以做為定義域實體和值物件的基底,因此您在每個網域的物件類別中沒有多餘的程式代碼。

自定義 .NET Standard 程式庫中的結構聚合

彙總是指群組在一起以符合交易一致性的網域物件叢集。 這些物件可以是實體的實例(其中一個是匯總根或根實體),再加上任何其他值物件。

交易一致性表示匯總保證在商務動作結束時保持一致且最新。 例如,eShopOnContainers 訂購微服務領域模型中的訂單匯總是由以下部分組成的,如圖 7-11 所示。

OrderAggregate 資料夾及其類別的螢幕快照。

OrderAggregate 資料夾的詳細檢視:Address.cs是值物件,IOrderRepository 是存放庫介面,Order.cs是匯總根目錄,OrderItem.cs是子實體,OrderStatus.cs是列舉類別。

圖 7-11。 Visual Studio 方案中的順序匯總

如果您在集合資料夾中開啟任何檔案,您可以看到它被標示為自定義父類或介面,例如實體或值物件,如 SeedWork 資料夾中所實作。

將網域實體實作為POCO類別

您可以在 .NET 中實作定義域模型,方法是建立實作網域實體的 POCO 類別。 在下列範例中,Order 類別會定義為實體,也會定義為匯總根目錄。 由於 Order 類別衍生自 Entity 基類,因此可以重複使用與實體相關的常見程序代碼。 請記住,這些基類和介面是由您在領域模型項目中定義,因此它是您的程序代碼,而不是來自EF等 ORM 的基礎結構程序代碼。

// COMPATIBLE WITH ENTITY FRAMEWORK CORE 5.0
// Entity is a custom base class with the ID
public class Order : Entity, IAggregateRoot
{
    private DateTime _orderDate;
    public Address Address { get; private set; }
    private int? _buyerId;

    public OrderStatus OrderStatus { get; private set; }
    private int _orderStatusId;

    private string _description;
    private int? _paymentMethodId;

    private readonly List<OrderItem> _orderItems;
    public IReadOnlyCollection<OrderItem> OrderItems => _orderItems;

    public Order(string userId, Address address, int cardTypeId, string cardNumber, string cardSecurityNumber,
            string cardHolderName, DateTime cardExpiration, int? buyerId = null, int? paymentMethodId = null)
    {
        _orderItems = new List<OrderItem>();
        _buyerId = buyerId;
        _paymentMethodId = paymentMethodId;
        _orderStatusId = OrderStatus.Submitted.Id;
        _orderDate = DateTime.UtcNow;
        Address = address;

        // ...Additional code ...
    }

    public void AddOrderItem(int productId, string productName,
                            decimal unitPrice, decimal discount,
                            string pictureUrl, int units = 1)
    {
        //...
        // Domain rules/logic for adding the OrderItem to the order
        // ...

        var orderItem = new OrderItem(productId, productName, unitPrice, discount, pictureUrl, units);

        _orderItems.Add(orderItem);

    }
    // ...
    // Additional methods with domain rules/logic related to the Order aggregate
    // ...
}

請務必注意,這是實作為POCO類別的網域實體。 它與 Entity Framework Core 或任何其他基礎結構架構沒有任何直接相依性。 此實作就像在 DDD 中一樣,只是實作領域模型的 C# 程序代碼。

此外,類別會以名為IAggregateRoot的介面裝飾。 該介面是空的介面,有時稱為 標記介面,只是用來指出此實體類別也是匯總根目錄。

標記介面有時被視為反模式;不過,標記類別也是一種乾淨的方法,尤其是在該介面可能正在演進時。 屬性可以是標記的另一個選項,但查看 IAggregate 介面旁的基類 (Entity) 會更快,而不是將 Aggregate 屬性標記放在類別上方。 無論如何,這是喜好設定的問題。

擁有匯總根表示,與匯總實體之一致性和商務規則相關的大部分程式代碼都應該實作為 Order aggregate 根類別中的方法(例如,將 OrderItem 物件新增至匯總時,AddOrderItem)。 您不應該獨立或直接建立或更新 OrderItems 物件;AggregateRoot 類別必須針對其子實體保留任何更新作業的控制權和一致性。

在網域實體中封裝數據

實體模型中的常見問題是,它們會將集合導覽屬性公開為可公開存取的清單類型。 這允許任何合作的開發人員操作這些集合類型的內容,這可能會繞過與集合相關的重要業務規則,可能使對象處於無效狀態。 此解決方案是公開相關集合的唯讀存取權,並明確提供客戶端操作這些集合的方法。

在先前的程式代碼中,請注意,許多屬性都是只讀或私用的,而且只能由類別方法更新,因此任何更新都會考慮在類別方法內指定的商務域不變異和邏輯。

例如,遵循 DDD 模式,不應該從任何命令處理程式方法或應用層類別執行下列動作(實際上,您不可能這麼做):

// WRONG ACCORDING TO DDD PATTERNS – CODE AT THE APPLICATION LAYER OR
// COMMAND HANDLERS
// Code in command handler methods or Web API controllers
//... (WRONG) Some code with business logic out of the domain classes ...
OrderItem myNewOrderItem = new OrderItem(orderId, productId, productName,
    pictureUrl, unitPrice, discount, units);

//... (WRONG) Accessing the OrderItems collection directly from the application layer // or command handlers
myOrder.OrderItems.Add(myNewOrderItem);
//...

在此情況下,Add 方法純粹是新增數據的作業,可直接存取 OrderItems 集合。 因此,與子實體相關的大部分領域邏輯、規則或驗證都會分散到應用層(命令處理程式和 Web API 控制器)。

如果繞開聚合根,聚合根無法保證其不變性、有效性或一致性。 最終,您將會有義大利麵條式代碼或交易腳本代碼。

若要遵循 DDD 模式,實體在任何實體屬性中不得有公用 setter。 實體中的變更應該由明確方法所驅動,並使用明確且普遍的語言來說明他們在實體中執行的變更。

此外,實體內的集合(例如訂單項目)應該是只讀屬性(稍後說明的 AsReadOnly 方法)。 您應該只能從匯總根類別方法或子實體方法內更新它。

如您在 Order 匯總根目錄的程式代碼中所見,所有 setter 都應該是私用或至少是唯讀的,因此實體數據或其子實體的任何作業都必須透過實體類別中的方法執行。 這會以受控和面向物件的方式維護一致性,而不是實作交易式腳本程序代碼。

下列代碼段顯示將 OrderItem 物件新增至 Order 匯總之工作的正確程式代碼。

// RIGHT ACCORDING TO DDD--CODE AT THE APPLICATION LAYER OR COMMAND HANDLERS
// The code in command handlers or WebAPI controllers, related only to application stuff
// There is NO code here related to OrderItem object's business logic
myOrder.AddOrderItem(productId, productName, pictureUrl, unitPrice, discount, units);

// The code related to OrderItem params validations or domain rules should
// be WITHIN the AddOrderItem method.

//...

在此代碼段中,與建立 OrderItem 物件相關的大部分驗證或邏輯都會在 AddOrderItem 方法中的 Order 匯總根目錄控制下,特別是與匯總中其他元素相關的驗證和邏輯。 例如,您可能會收到與多次呼叫 AddOrderItem 返回的結果相同的產品項目。 在該方法中,您可以檢查產品項目,並將相同的產品合併成一個包含多個單位的 OrderItem 物件。 此外,如果有不同的折扣金額,但產品標識符相同,您可能會套用較高的折扣。 此原則適用於 OrderItem 物件的任何其他領域邏輯。

此外,來自 Order 匯總根目錄的 AddOrderItem 方法也會控制及執行新的 OrderItem(params) 作業。 因此,與該操作相關的大部分邏輯或驗證(尤其是影響其他子實體之間一致性的任何項目)都會集中在聚合根中的單一位置。 這是匯總根模式的最終用途。

當您使用 Entity Framework Core 1.1 或更新版本時,DDD 實體可以更清楚地表示,因為它除了屬性之外,還允許 對應至欄位 。 保護子實體或值物件的集合時,這非常有用。 透過這項增強功能,您可以使用簡單的私用欄位,而不是屬性,而且您可以在公用方法中實作欄位集合的任何更新,並透過 AsReadOnly 方法提供只讀存取權。

在 DDD 中,您想要只透過實體 (或建構函式) 中的方法來更新實體,以控制任何不因變數和數據一致性,因此屬性只會使用 get 存取子來定義。 屬性是由私有欄位所支持。 私人成員只能從類別內部存取。 不過,有一個例外狀況:EF Core 也必須設定這些欄位(因此可以傳回具有適當值的物件)。

僅將具有 get 存取子的屬性對應至資料庫數據表中的欄位

將屬性對應至資料庫資料表欄位不是網域責任,而是基礎結構和持久層的一部分。 我們在這裡提到這一點,只是讓您知道 EF Core 1.1 或更新版本中的新功能與如何建立實體模型有關。 本主題的其他詳細數據會在基礎結構和持續性一節中說明。

當您使用 EF Core 1.0 或更新版本時,您必須在 DbContext 內,將只使用 getter 定義的屬性對應至資料庫數據表中的實際欄位。 這是使用 PropertyBuilder 類別的 HasField 方法完成的。

對應欄位,但不包含屬性

使用 EF Core 1.1 或更新版本的功能,將資料行對應到欄位,也可以不使用屬性。 相反地,您可以只將數據表中的數據行對應至欄位。 常見的使用案例是內部狀態的私人字段,不需要從實體外部存取。

例如,在上述 OrderAggregate 程式代碼範例中,有數個私用欄位,例如 _paymentMethodId 字段,沒有 setter 或 getter 的相關屬性。 該欄位也可以在訂單的業務邏輯中計算,並通過訂單的方法來使用,但也需要保存到資料庫中。 因此,在 EF Core 中(自 v1.1 起),有一種方式可以在資料庫中對應沒有相關屬性的欄位至表格的欄位。 本指南的 基礎結構層 一節也會說明這一點。

其他資源