Nota
L'accesso a questa pagina richiede l'autorizzazione. È possibile provare ad accedere o modificare le directory.
L'accesso a questa pagina richiede l'autorizzazione. È possibile provare a modificare le directory.
Suggerimento
Questo contenuto è un estratto dell'eBook, Architettura di microservizi .NET per applicazioni .NET containerizzati, disponibile in documentazione .NET o come PDF scaricabile gratuitamente leggibile offline.
Nella sezione precedente sono stati illustrati i principi e i modelli di progettazione fondamentali per la progettazione di un modello di dominio. È ora possibile esplorare i possibili modi per implementare il modello di dominio usando .NET (codice C# normale) ed EF Core. Il modello di dominio sarà composto semplicemente dal codice. Avrà solo i requisiti del modello di EF Core, ma non le dipendenze reali da Entity Framework. Non è consigliabile avere dipendenze o riferimenti rigidi a EF Core o a qualsiasi altro ORM nel modello di dominio.
Struttura del modello di dominio in una libreria .NET Standard personalizzata
L'organizzazione di cartelle usata per l'applicazione di riferimento eShopOnContainers illustra il modello DDD per l'applicazione. È possibile che un'organizzazione di cartelle diversa comunichi più chiaramente le scelte di progettazione effettuate per l'applicazione. Come si può notare nella figura 7-10, nel modello di dominio di ordinamento sono presenti due aggregazioni, l'aggregazione dell'ordine e l'aggregazione acquirente. Ogni aggregazione è un gruppo di entità di dominio e oggetti valore, anche se è possibile avere un'aggregazione composta da una singola entità di dominio (radice di aggregazione o entità radice).
Visualizzazione Esplora soluzioni per il progetto Ordering.Domain, che mostra la cartella AggregatesModel contenente le cartelle BuyerAggregate e OrderAggregate, ognuna contenente le relative classi di entità, i file oggetto valore e così via.
Figura 7-10. Struttura del modello di dominio per il microservizio di ordinamento in eShopOnContainers
Inoltre, il livello del modello di dominio include i contratti repository (interfacce) che sono i requisiti dell'infrastruttura del modello di dominio. In altre parole, queste interfacce esprimono i repository e i metodi che il livello di infrastruttura deve implementare. È fondamentale che l'implementazione dei repository venga inserita all'esterno del livello del modello di dominio, nella libreria dei livelli dell'infrastruttura, quindi il livello del modello di dominio non è "contaminata" dall'API o dalle classi dalle tecnologie dell'infrastruttura, ad esempio Entity Framework.
È anche possibile visualizzare una cartella SeedWork che contiene classi di base personalizzate che è possibile usare come base per le entità di dominio e gli oggetti valore, quindi non si dispone di codice ridondante nella classe oggetto di ogni dominio.
Aggregazioni di strutture in una libreria .NET Standard personalizzata
Un'aggregazione fa riferimento a un cluster di oggetti di dominio raggruppati in modo da corrispondere alla coerenza transazionale. Questi oggetti possono essere istanze di entità (una delle quali è la radice di aggregazione o l'entità radice) più eventuali oggetti valore aggiuntivi.
La coerenza transazionale significa che un'aggregazione deve essere coerente e aggiornata alla fine di un'azione aziendale. Ad esempio, l'aggregazione degli ordini del modello di dominio del microservizio di ordinamento di eShopOnContainers è costituita come illustrato nella figura 7-11.
Visualizzazione dettagliata della cartella OrderAggregate: Address.cs è un oggetto valore, IOrderRepository è un'interfaccia del repository, Order.cs è una radice di aggregazione, OrderItem.cs è un'entità figlio e OrderStatus.cs è una classe di enumerazione.
Figura 7-11. Aggregazione dell'ordine nella soluzione Visual Studio
Se si apre uno dei file in una cartella di aggregazione, è possibile vedere come viene contrassegnato come una classe o un'interfaccia di base personalizzata, ad esempio un oggetto entity o value, come implementato nella cartella SeedWork .
Implementare entità di dominio come classi POCO
È possibile implementare un modello di dominio in .NET creando classi POCO che implementano le entità di dominio. Nell'esempio seguente la classe Order viene definita come entità e anche come radice di aggregazione. Poiché la classe Order deriva dalla classe base Entity, può riutilizzare il codice comune correlato alle entità. Tenere presente che queste classi e interfacce di base sono definite dall'utente nel progetto del modello di dominio, quindi si tratta del codice, non del codice dell'infrastruttura da un ORM come 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
// ...
}
È importante notare che si tratta di un'entità di dominio implementata come classe POCO. Non ha alcuna dipendenza diretta da Entity Framework Core o da altri framework dell'infrastruttura. Questa implementazione è così come dovrebbe essere in DDD, solo codice C# che implementa un modello di dominio.
Inoltre, la classe è decorata con un'interfaccia denominata IAggregateRoot. Tale interfaccia è un'interfaccia vuota, talvolta denominata interfaccia marcatore, usata solo per indicare che questa classe di entità è anche una radice di aggregazione.
Un'interfaccia marcatore viene talvolta considerata un anti-pattern; Tuttavia, è anche un modo pulito per contrassegnare una classe, soprattutto quando l'interfaccia potrebbe evolversi. Un attributo può essere l'altra scelta per il marcatore, ma è più rapido vedere la classe base (Entità) accanto all'interfaccia IAggregate anziché inserire un marcatore di attributo Aggregate sopra la classe. È una questione di preferenze, in ogni caso.
La presenza di una radice di aggregazione indica che la maggior parte del codice correlato alla coerenza e alle regole business delle entità dell'aggregazione deve essere implementata come metodi nella classe radice dell'aggregazione Order, ad esempio AddOrderItem quando si aggiunge un oggetto OrderItem all'aggregazione. Non è consigliabile creare o aggiornare oggetti OrderItems in modo indipendente o diretto; La classe AggregateRoot deve mantenere il controllo e la coerenza di qualsiasi operazione di aggiornamento rispetto alle entità figlio.
Incapsulare i dati nelle entità di dominio
Un problema comune nei modelli di entità è che espongono le proprietà di navigazione della raccolta come tipi di elenco accessibili pubblicamente. Ciò consente a qualsiasi sviluppatore collaboratore di modificare il contenuto di questi tipi di raccolta, che può ignorare importanti regole business correlate alla raccolta, eventualmente lasciando l'oggetto in uno stato non valido. La soluzione consiste nell'esporre l'accesso in sola lettura alle raccolte correlate e fornire in modo esplicito metodi che definiscono i modi in cui i client possono modificarli.
Nel codice precedente si noti che molti attributi sono di sola lettura o privati e sono aggiornabili solo dai metodi della classe, pertanto qualsiasi aggiornamento considera gli invarianti del dominio aziendale e la logica specificati all'interno dei metodi della classe.
Ad esempio, seguendo i modelli DDD, non è consigliabile eseguire le operazioni seguenti da qualsiasi metodo del gestore comandi o classe del livello applicazione (in realtà, dovrebbe essere impossibile farlo):
// 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);
//...
In questo caso, il metodo Add è puramente un'operazione per aggiungere dati, con accesso diretto all'insieme OrderItems. Pertanto, la maggior parte della logica di dominio, delle regole o delle convalide correlate a tale operazione con le entità figlio verrà distribuita a livello di applicazione (gestori di comandi e controller API Web).
Se si passa alla radice di aggregazione, la radice di aggregazione non può garantire le relative invarianti, la sua validità o la relativa coerenza. Alla fine si avrà codice spaghetti o codice di script transazionale.
Per seguire i modelli DDD, le entità non devono avere setter pubblici in alcuna proprietà di entità. Le modifiche in un'entità devono essere guidate da metodi espliciti con linguaggio universale esplicito sulla modifica che stanno eseguendo nell'entità.
Inoltre, le raccolte all'interno dell'entità ,ad esempio gli elementi dell'ordine, devono essere proprietà di sola lettura (il metodo AsReadOnly illustrato più avanti). Dovrebbe essere possibile aggiornarlo solo dall'interno dei metodi della classe radice di aggregazione o dei metodi di entità figlio.
Come si può notare nel codice per la radice dell'aggregazione Order, tutti i setter devono essere privati o almeno di sola lettura esternamente, in modo che qualsiasi operazione sui dati dell'entità o le relative entità figlio debba essere eseguita tramite metodi nella classe di entità. In questo modo la coerenza viene mantenuta in modo controllato e orientato agli oggetti anziché implementare il codice script transazionale.
Il frammento di codice seguente mostra il modo corretto per codificare l'attività di aggiunta di un oggetto OrderItem all'aggregazione 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.
//...
In questo frammento di codice, la maggior parte delle convalide o della logica correlate alla creazione di un oggetto OrderItem sarà sotto il controllo della radice di aggregazione Order, nel metodo AddOrderItem, in particolare convalide e logica correlate ad altri elementi nell'aggregazione. Ad esempio, è possibile ottenere lo stesso elemento prodotto del risultato di più chiamate a AddOrderItem. In questo metodo, è possibile esaminare gli articoli del prodotto e consolidare gli stessi articoli di prodotto in un singolo oggetto OrderItem con diverse unità. Inoltre, se sono presenti importi di sconto diversi, ma l'ID prodotto è lo stesso, è probabile che si applichi lo sconto più elevato. Questo principio si applica a qualsiasi altra logica di dominio per l'oggetto OrderItem.
Inoltre, la nuova operazione OrderItem(params) verrà controllata ed eseguita anche dal metodo AddOrderItem dalla radice di aggregazione Order. Pertanto, la maggior parte della logica o delle convalide correlate a tale operazione (in particolare ciò che influisce sulla coerenza tra altre entità figlio) si troverà in un'unica posizione all'interno della radice di aggregazione. Questo è lo scopo finale del modello radice aggregato.
Quando si usa Entity Framework Core 1.1 o versione successiva, un'entità DDD può essere espressa meglio perché consente il mapping ai campi oltre alle proprietà. Ciò è utile quando si proteggono raccolte di entità figlio o oggetti valore. Con questo miglioramento, è possibile usare campi privati semplici anziché proprietà ed è possibile implementare qualsiasi aggiornamento alla raccolta di campi nei metodi pubblici e fornire l'accesso in sola lettura tramite il metodo AsReadOnly.
In DDD si vuole aggiornare l'entità solo tramite metodi nell'entità (o nel costruttore) per controllare eventuali invarianti e la coerenza dei dati, quindi le proprietà vengono definite solo con una funzione di accesso get. Le proprietà sono supportate da campi privati. È possibile accedere ai membri privati solo dall'interno della classe . Esiste tuttavia un'eccezione: EF Core deve impostare anche questi campi, in modo che possa restituire l'oggetto con i valori appropriati.
Eseguire il mapping delle proprietà con solo le funzioni di accesso get ai campi nella tabella di database
Il mapping delle proprietà alle colonne della tabella di database non è responsabilità del dominio, ma fa parte dell'infrastruttura e del livello di persistenza. Questo argomento viene menzionato qui in modo da conoscere le nuove funzionalità di EF Core 1.1 o versioni successive correlate a come modellare le entità. Altri dettagli su questo argomento sono illustrati nella sezione infrastruttura e persistenza.
Quando si usa EF Core 1.0 o versione successiva, all'interno di DbContext è necessario eseguire il mapping delle proprietà definite solo con i getter ai campi effettivi nella tabella di database. Questa operazione viene eseguita con il metodo HasField della classe PropertyBuilder.
Eseguire il mapping dei campi senza proprietà
Con la funzionalità in EF Core 1.1 o versione successiva per eseguire il mapping delle colonne ai campi, è anche possibile non usare le proprietà. È invece possibile eseguire il mapping delle colonne da una tabella ai campi. Un caso d'uso comune per questo scenario è costituito da campi privati per uno stato interno a cui non è necessario accedere dall'esterno dell'entità.
Nell'esempio di codice OrderAggregate precedente, ad esempio, sono presenti diversi campi privati, ad esempio il _paymentMethodId
campo, che non hanno proprietà correlate per un setter o un getter. Tale campo può anche essere calcolato all'interno della logica di business dell'ordine e usato dai metodi dell'ordine, ma deve essere salvato in modo permanente anche nel database. Pertanto, in EF Core (dalla versione 1.1), è possibile eseguire il mapping di un campo senza una proprietà correlata a una colonna nel database. Questo argomento è illustrato anche nella sezione Livello infrastruttura di questa guida.
Risorse aggiuntive
Vaughn Vernon. Modellazione di aggregazioni con DDD ed Entity Framework. Si noti che non si tratta di Entity Framework Core.
https://kalele.io/blog-posts/modeling-aggregates-with-ddd-and-entity-framework/Julie Lerman. Punti dati - Codifica per la progettazione di Domain-Driven: suggerimenti per sviluppatori di Data-Focused
https://learn.microsoft.com/archive/msdn-magazine/2013/august/data-points-coding-for-domain-driven-design-tips-for-data-focused-devsUdi Dahan. How to create fully encapsulated Domain Models (Come creare modelli di dominio completamente incapsulati)
https://udidahan.com/2008/02/29/how-to-create-fully-encapsulated-domain-models/Steve Smith. Qual è la differenza tra un DTO e un POCO? \ https://ardalis.com/dto-or-poco/