Implementera en mikrotjänstdomänmodell med .NET
Dricks
Det här innehållet är ett utdrag från eBook, .NET Microservices Architecture for Containerized .NET Applications, tillgängligt på .NET Docs eller som en kostnadsfri nedladdningsbar PDF som kan läsas offline.
I föregående avsnitt förklarades de grundläggande designprinciperna och mönstren för att utforma en domänmodell. Nu är det dags att utforska möjliga sätt att implementera domänmodellen med hjälp av .NET (vanlig C#-kod) och EF Core. Domänmodellen består helt enkelt av din kod. Det kommer bara att ha EF Core-modellkraven, men inte verkliga beroenden för EF. Du bör inte ha hårda beroenden eller referenser till EF Core eller någon annan ORM i din domänmodell.
Domänmodellstruktur i ett anpassat .NET Standard-bibliotek
Mapporganisationen som används för referensprogrammet eShopOnContainers visar DDD-modellen för programmet. Du kanske upptäcker att en annan mapporganisation tydligare kommunicerar de designval som gjorts för ditt program. Som du ser i bild 7–10 finns det två aggregeringar i domänmodellen för beställning, orderaggregatet och köparens aggregering. Varje aggregering är en grupp med domänentiteter och värdeobjekt, även om du kan ha en aggregering som består av en enskild domänentitet (den aggregerade rotentiteten eller rotentiteten).
Solution Explorer-vyn för projektet Ordering.Domain, som visar mappen AggregatesModel som innehåller mapparna BuyerAggregate och OrderAggregate, var och en som innehåller dess entitetsklasser, värdeobjektfiler och så vidare.
Bild 7-10. Domänmodellstruktur för beställning av mikrotjänst i eShopOnContainers
Dessutom innehåller domänmodelllagret lagringsplatsens kontrakt (gränssnitt) som är infrastrukturkraven för din domänmodell. Med andra ord uttrycker dessa gränssnitt vilka lagringsplatser och de metoder som infrastrukturskiktet måste implementera. Det är viktigt att implementeringen av lagringsplatserna placeras utanför domänmodelllagret, i infrastrukturlagerbiblioteket, så att domänmodelllagret inte "kontamineras" av API eller klasser från infrastrukturtekniker, till exempel Entity Framework.
Du kan också se en SeedWork-mapp som innehåller anpassade basklasser som du kan använda som bas för dina domänentiteter och värdeobjekt, så att du inte har redundant kod i varje domäns objektklass.
Strukturera aggregeringar i ett anpassat .NET Standard-bibliotek
En aggregering refererar till ett kluster med domänobjekt grupperade tillsammans för att matcha transaktionskonsekvens. Dessa objekt kan vara instanser av entiteter (varav en är den aggregerade roten eller rotentiteten) plus eventuella ytterligare värdeobjekt.
Transaktionskonsekvens innebär att en aggregering garanteras vara konsekvent och uppdaterad i slutet av en affärsåtgärd. Till exempel består orderaggregatet från eShopOnContainers som beställer mikrotjänstdomänmodellen enligt bild 7–11.
En detaljerad vy över mappen OrderAggregate: Address.cs är ett värdeobjekt är IOrderRepository ett lagringsplatsgränssnitt, Order.cs är en aggregerad rot, OrderItem.cs är en underordnad entitet och OrderStatus.cs är en uppräkningsklass.
Bild 7-11. Beställningsaggregatet i Visual Studio-lösningen
Om du öppnar någon av filerna i en samlingsmapp kan du se hur den markeras som antingen en anpassad basklass eller ett anpassat basgränssnitt, till exempel entitet eller värdeobjekt, som implementerat i mappen SeedWork .
Implementera domänentiteter som POCO-klasser
Du implementerar en domänmodell i .NET genom att skapa POCO-klasser som implementerar dina domänentiteter. I följande exempel definieras klassen Order som en entitet och även som en aggregerad rot. Eftersom klassen Order härleds från entitetsbasklassen kan den återanvända vanlig kod som är relaterad till entiteter. Tänk på att dessa basklasser och gränssnitt definieras av dig i domänmodellprojektet, så det är din kod, inte infrastrukturkod från en ORM som EF.
// 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
// ...
}
Det är viktigt att observera att det här är en domänentitet som implementeras som en POCO-klass. Den har inget direkt beroende av Entity Framework Core eller något annat infrastrukturramverk. Den här implementeringen är som den ska vara i DDD, bara C#-kod som implementerar en domänmodell.
Dessutom är klassen dekorerad med ett gränssnitt med namnet IAggregateRoot. Det gränssnittet är ett tomt gränssnitt, som ibland kallas för ett markörgränssnitt, som används bara för att indikera att den här entitetsklassen också är en aggregerad rot.
Ett markörgränssnitt betraktas ibland som ett antimönster. Men det är också ett rent sätt att markera en klass, särskilt när gränssnittet kan utvecklas. Ett attribut kan vara det andra valet för markören, men det går snabbare att se basklassen (entitet) bredvid IAggregate-gränssnittet i stället för att placera en markör för aggregerade attribut ovanför klassen. Det handlar i alla fall om preferenser.
Att ha en aggregerad rot innebär att merparten av koden som är relaterad till konsekvens- och affärsregler för aggregerade entiteter ska implementeras som metoder i rotklassen Order aggregate (till exempel AddOrderItem när du lägger till ett OrderItem-objekt i sammanställningen). Du bör inte skapa eller uppdatera OrderItems-objekt oberoende eller direkt. Klassen AggregateRoot måste ha kontroll och konsekvens för alla uppdateringsåtgärder mot dess underordnade entiteter.
Kapsla in data i domänentiteterna
Ett vanligt problem i entitetsmodeller är att de exponerar samlingsnavigeringsegenskaper som offentligt tillgängliga listtyper. På så sätt kan alla medarbetares utvecklare ändra innehållet i dessa samlingstyper, vilket kan kringgå viktiga affärsregler som är relaterade till samlingen, vilket möjligen lämnar objektet i ett ogiltigt tillstånd. Lösningen på detta är att exponera skrivskyddad åtkomst till relaterade samlingar och uttryckligen tillhandahålla metoder som definierar hur klienter kan manipulera dem.
I föregående kod bör du observera att många attribut är skrivskyddade eller privata och endast kan uppdateras av klassmetoderna, så alla uppdateringar tar hänsyn till affärsdomänens invarianter och logik som anges i klassmetoderna.
Om du till exempel följer DDD-mönster bör du inte göra följande från någon kommandohanterarmetod eller programlagerklass (det bör faktiskt vara omöjligt för dig att göra det):
// 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);
//...
I det här fallet är metoden Lägg till enbart en åtgärd för att lägga till data, med direkt åtkomst till Samlingen OrderItems. Därför sprids de flesta av domänlogiken, reglerna eller valideringarna som är relaterade till den åtgärden med de underordnade entiteterna över programskiktet (kommandohanterare och webb-API-kontrollanter).
Om du går runt den aggregerade roten kan den aggregerade roten inte garantera dess invarianter, dess giltighet eller dess konsekvens. Så småningom har du spaghettikod eller transaktionsskriptkod.
Om du vill följa DDD-mönster får entiteter inte ha offentliga setters i någon entitetsegenskap. Ändringar i en entitet bör styras av explicita metoder med explicit allmänt förekommande språk om den ändring som de utför i entiteten.
Dessutom bör samlingar i entiteten (t.ex. orderobjekten) vara skrivskyddade egenskaper (metoden AsReadOnly som beskrivs senare). Du bör bara kunna uppdatera den från de aggregerade rotklassmetoderna eller de underordnade entitetsmetoderna.
Som du ser i koden för orderaggregatroten bör alla setters vara privata eller minst skrivskyddade externt, så att alla åtgärder mot entitetens data eller dess underordnade entiteter måste utföras via metoder i entitetsklassen. Detta upprätthåller konsekvens på ett kontrollerat och objektorienterat sätt i stället för att implementera transaktionsskriptkod.
Följande kodfragment visar rätt sätt att koda uppgiften att lägga till ett OrderItem-objekt i orderaggregatet.
// 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.
//...
I det här kodfragmentet kommer de flesta valideringar eller logik som är relaterade till skapandet av ett OrderItem-objekt att kontrolleras av orderaggregatroten , i metoden AddOrderItem, särskilt valideringar och logik som är relaterade till andra element i aggregerade element. Du kan till exempel få samma produktartikel som resultatet av flera anrop till AddOrderItem. I den metoden kan du undersöka produktartiklarna och konsolidera samma produktobjekt till ett enda OrderItem-objekt med flera enheter. Om det finns olika rabattbelopp men produkt-ID:t är detsamma skulle du förmodligen tillämpa den högre rabatten. Den här principen gäller för all annan domänlogik för OrderItem-objektet.
Dessutom styrs och utförs den nya åtgärden OrderItem(params) av metoden AddOrderItem från orderaggregatroten. Därför kommer de flesta av logiken eller valideringarna som är relaterade till den åtgärden (särskilt allt som påverkar konsekvensen mellan andra underordnade entiteter) att finnas på en enda plats i den aggregerade roten. Det är det slutliga syftet med det aggregerade rotmönstret.
När du använder Entity Framework Core 1.1 eller senare kan en DDD-entitet uttryckas bättre eftersom den tillåter mappning till fält utöver egenskaper. Detta är användbart när du skyddar samlingar med underordnade entiteter eller värdeobjekt. Med den här förbättringen kan du använda enkla privata fält i stället för egenskaper och du kan implementera alla uppdateringar av fältsamlingen i offentliga metoder och ge skrivskyddad åtkomst via metoden AsReadOnly.
I DDD vill du uppdatera entiteten endast via metoder i entiteten (eller konstruktorn) för att styra alla invarianta och konsekvensen i data, så egenskaper definieras endast med en get-accessor. Egenskaperna backas upp av privata fält. Privata medlemmar kan endast nås inifrån klassen. Det finns dock ett undantag: EF Core måste också ange dessa fält (så att objektet kan returneras med rätt värden).
Mappa egenskaper med endast komma åt fälten i databastabellen
Att mappa egenskaper till databastabellkolumner är inte ett domänansvar utan en del av infrastruktur- och beständighetsskiktet. Vi nämner detta här bara så att du är medveten om de nya funktionerna i EF Core 1.1 eller senare som rör hur du kan modellera entiteter. Mer information om det här avsnittet beskrivs i avsnittet infrastruktur och beständighet.
När du använder EF Core 1.0 eller senare måste du i DbContext mappa de egenskaper som endast definieras med getter till de faktiska fälten i databastabellen. Detta görs med metoden HasField för klassen PropertyBuilder.
Mappa fält utan egenskaper
Med funktionen i EF Core 1.1 eller senare för att mappa kolumner till fält går det också att inte använda egenskaper. I stället kan du bara mappa kolumner från en tabell till fält. Ett vanligt användningsfall för detta är privata fält för ett internt tillstånd som inte behöver nås utanför entiteten.
I exemplet med OrderAggregate-kod ovan finns det till exempel flera privata fält, till exempel _paymentMethodId
fältet, som inte har någon relaterad egenskap för antingen en setter eller getter. Det fältet kan också beräknas inom ordningens affärslogik och användas från ordningens metoder, men det måste också sparas i databasen. Så i EF Core (sedan v1.1) finns det ett sätt att mappa ett fält utan en relaterad egenskap till en kolumn i databasen. Detta förklaras också i avsnittet Infrastrukturskikt i den här guiden.
Ytterligare resurser
Vaughn Vernon. Modelleringsaggregeringar med DDD och Entity Framework. Observera att detta inte är Entity Framework Core.
https://kalele.io/blog-posts/modeling-aggregates-with-ddd-and-entity-framework/Julie Lerman. Datapunkter – Kodning för domändriven design: Tips för datafokuserade devs
https://learn.microsoft.com/archive/msdn-magazine/2013/august/data-points-coding-for-domain-driven-design-tips-for-data-focused-devsUdi Dahan. Så här skapar du helt inkapslade domänmodeller
https://udidahan.com/2008/02/29/how-to-create-fully-encapsulated-domain-models/Steve Smith. Vad är skillnaden mellan en DTO och en POCO? \ https://ardalis.com/dto-or-poco/