Notes
L’accès à cette page nécessite une autorisation. Vous pouvez essayer de vous connecter ou de modifier des répertoires.
L’accès à cette page nécessite une autorisation. Vous pouvez essayer de modifier des répertoires.
Conseil / Astuce
Ce contenu est un extrait du livre électronique 'Architecture des microservices .NET pour les applications .NET conteneurisées', disponible sur .NET Docs ou en tant que PDF téléchargeable gratuitement, lisible hors ligne.
Comme indiqué dans les sections précédentes sur les entités et les agrégats, l’identité est fondamentale pour les entités. Toutefois, il existe de nombreux objets et éléments de données dans un système qui ne nécessitent pas d'identité et de suivi d'identité, tels que des objets de valeur.
Un objet valeur peut référencer d’autres entités. Par exemple, dans une application qui génère un itinéraire qui décrit comment obtenir d’un point à un autre, cet itinéraire serait un objet valeur. Il s’agirait d’un instantané de points sur une route spécifique, mais cette route suggérée n’aurait pas d’identité, même si en interne il pourrait faire référence à des entités telles que City, Road, etc.
La figure 7-13 illustre l’objet de valeur Address dans l’agrégat Order.
Figure 7-13. Objet de valeur Address dans l’agrégat Order
Comme illustré dans la figure 7-13, une entité est généralement composée de plusieurs attributs. Par exemple, l’entité Order
peut être modélisée en tant qu’entité avec une identité et composée en interne d’un ensemble d’attributs tels que OrderId, OrderDate, OrderItems, etc. Mais l’adresse, qui est simplement une valeur complexe composée de pays/région, rue, ville, etc., et qui n’a pas d’identité dans ce domaine, doit être modélisée et traitée comme un objet valeur.
Caractéristiques importantes des objets de valeur
Il existe deux caractéristiques principales pour les objets valeur :
Ils n’ont pas d’identité.
Ils sont immuables.
La première caractéristique a déjà été abordée. L’immuabilité est une exigence importante. Les valeurs d’un objet valeur doivent être immuables une fois l’objet créé. Par conséquent, lorsque l’objet est construit, vous devez fournir les valeurs requises, mais vous ne devez pas les autoriser à changer pendant la durée de vie de l’objet.
Les objets de valeur vous permettent d’effectuer certaines astuces pour les performances, grâce à leur nature immuable. Cela est particulièrement vrai dans les systèmes où il peut y avoir des milliers d’instances d’objet de valeur, dont beaucoup ont les mêmes valeurs. Leur nature immuable leur permet de les réutiliser ; ils peuvent être des objets interchangeables, car leurs valeurs sont identiques et n’ont pas d’identité. Ce type d’optimisation peut parfois faire une différence entre les logiciels qui s’exécutent lentement et les logiciels avec de bonnes performances. Bien sûr, tous ces cas dépendent de l’environnement d’application et du contexte de déploiement.
Implémentation de l’objet Value en C#
En termes d’implémentation, vous pouvez avoir une classe de base d’objet valeur qui a des méthodes utilitaires de base telles que l’égalité basée sur la comparaison entre tous les attributs (car un objet valeur ne doit pas être basé sur l’identité) et d’autres caractéristiques fondamentales. L’exemple suivant montre une classe de base d’objets value utilisée dans le microservice ordering à partir d’eShopOnContainers.
public abstract class ValueObject
{
protected static bool EqualOperator(ValueObject left, ValueObject right)
{
if (ReferenceEquals(left, null) ^ ReferenceEquals(right, null))
{
return false;
}
return ReferenceEquals(left, right) || left.Equals(right);
}
protected static bool NotEqualOperator(ValueObject left, ValueObject right)
{
return !(EqualOperator(left, right));
}
protected abstract IEnumerable<object> GetEqualityComponents();
public override bool Equals(object obj)
{
if (obj == null || obj.GetType() != GetType())
{
return false;
}
var other = (ValueObject)obj;
return this.GetEqualityComponents().SequenceEqual(other.GetEqualityComponents());
}
public override int GetHashCode()
{
return GetEqualityComponents()
.Select(x => x != null ? x.GetHashCode() : 0)
.Aggregate((x, y) => x ^ y);
}
// Other utility methods
}
Le ValueObject
est un type abstract class
, mais dans cet exemple, il ne surcharge pas les opérateurs ==
et !=
. Vous pouvez choisir de le faire, en faisant en sorte que les comparaisons soient déléguées au remplacement Equals
. Par exemple, considérez l’opérateur suivant surchargeant le type ValueObject
:
public static bool operator ==(ValueObject one, ValueObject two)
{
return EqualOperator(one, two);
}
public static bool operator !=(ValueObject one, ValueObject two)
{
return NotEqualOperator(one, two);
}
Vous pouvez utiliser cette classe lors de l’implémentation de votre objet valeur réel, comme avec l’objet Address
valeur indiqué dans l’exemple suivant :
public class Address : ValueObject
{
public String Street { get; private set; }
public String City { get; private set; }
public String State { get; private set; }
public String Country { get; private set; }
public String ZipCode { get; private set; }
public Address() { }
public Address(string street, string city, string state, string country, string zipcode)
{
Street = street;
City = city;
State = state;
Country = country;
ZipCode = zipcode;
}
protected override IEnumerable<object> GetEqualityComponents()
{
// Using a yield return statement to return each element one at a time
yield return Street;
yield return City;
yield return State;
yield return Country;
yield return ZipCode;
}
}
L’implémentation de cet objet valeur Address
n’a aucune identité et, par conséquent, aucun champ ID n’est défini pour lui, que ce soit dans la définition de la classe Address
ou dans celle de la classe ValueObject
.
L’absence de champ ID dans une classe à utiliser par Entity Framework (EF) n’était pas envisageable jusqu'à l'arrivée d’EF Core 2.0, ce qui facilite grandement l’implémentation de meilleurs objets de valeur sans ID. C’est précisément l’explication de la section suivante.
On pourrait penser que, comme ils sont immuables, les objets de valeur doivent être en lecture seule (avec des propriétés get-only). C’est exact. Toutefois, ils sont généralement sérialisés et désérialisés dans les files d’attente ; or, s’ils sont en lecture seule, le désérialiseur arrête l’affectation de valeurs. Pour des raisons pratiques, vous allez simplement les laisser définis comme private set
, ce qui constitue un niveau de lecture seule suffisant.
Sémantique de comparaison des objets de valeur
Deux instances du Address
type peuvent être comparées à l’aide de toutes les méthodes suivantes :
var one = new Address("1 Microsoft Way", "Redmond", "WA", "US", "98052");
var two = new Address("1 Microsoft Way", "Redmond", "WA", "US", "98052");
Console.WriteLine(EqualityComparer<Address>.Default.Equals(one, two)); // True
Console.WriteLine(object.Equals(one, two)); // True
Console.WriteLine(one.Equals(two)); // True
Console.WriteLine(one == two); // True
Lorsque toutes les valeurs sont identiques, les comparaisons sont correctement évaluées en tant que true
. Si vous n’avez pas choisi de surcharger les opérateurs ==
et !=
, alors la dernière comparaison de one == two
serait évaluée en tant que false
. Pour plus d’informations, consultez Les opérateurs d’égalité Overload ValueObject.
Comment conserver des objets de valeur dans la base de données avec EF Core 2.0 et versions ultérieures
Vous venez de voir comment définir un objet valeur dans votre modèle de domaine. Mais comment pouvez-vous réellement le conserver dans la base de données à l’aide d’Entity Framework Core, car il cible généralement des entités avec une identité ?
Contexte et approches plus anciennes à l’aide d’EF Core 1.1
En arrière-plan, une limitation lors de l’utilisation d’EF Core 1.0 et 1.1 était que vous ne pouviez pas utiliser de types complexes tels que définis dans EF 6.x dans le .NET Framework traditionnel. Par conséquent, si vous utilisez EF Core 1.0 ou 1.1, vous devez stocker votre objet valeur en tant qu’entité EF avec un champ d’ID. Ensuite, il semblait plus semblable à un objet valeur sans identité, vous pouvez masquer son ID afin de vous indiquer clairement que l’identité d’un objet valeur n’est pas importante dans le modèle de domaine. Vous pouvez masquer cet ID en l’utilisant comme une propriété cachée. Étant donné que cette configuration pour masquer l’ID dans le modèle est configurée au niveau de l’infrastructure EF, elle serait transparente pour votre modèle de domaine.
Dans la version initiale d’eShopOnContainers (.NET Core 1.1), l’ID masqué nécessaire par l’infrastructure EF Core a été implémenté de la manière suivante au niveau DbContext, à l’aide de l’API Fluent au niveau de l’infrastructure. Par conséquent, l’ID a été masqué du point de vue du modèle de domaine, mais toujours présent dans l’infrastructure.
// Old approach with EF Core 1.1
// Fluent API within the OrderingContext:DbContext in the Infrastructure project
void ConfigureAddress(EntityTypeBuilder<Address> addressConfiguration)
{
addressConfiguration.ToTable("address", DEFAULT_SCHEMA);
addressConfiguration.Property<int>("Id") // Id is a shadow property
.IsRequired();
addressConfiguration.HasKey("Id"); // Id is a shadow property
}
Toutefois, la persistance de cet objet valeur dans la base de données a été effectuée comme une entité régulière dans une autre table.
Avec EF Core 2.0 et versions ultérieures, il existe de nouvelles façons de conserver des objets de valeur.
Rendre les objets de valeur persistants en tant que types d’entité détenus dans EF Core 2.0 et les versions ultérieures
Même malgré certains écarts entre le modèle d’objet valeur canonique dans DDD et le type d’entité possédée dans EF Core, c'est actuellement le meilleur moyen de persister des objets de valeur avec EF Core 2.0 et versions ultérieures. Vous pouvez voir les limitations à la fin de cette section.
La fonctionnalité de type d’entité détenue a été ajoutée à EF Core depuis la version 2.0.
Un type d’entité détenu vous permet de mapper des types qui n’ont pas leur propre identité explicitement définie dans le modèle de domaine et qui sont utilisés comme propriétés, telles qu’un objet valeur, dans l’une de vos entités. Un type d’entité appartenant partage le même type CLR avec un autre type d’entité (autrement dit, il s’agit simplement d’une classe régulière). L’entité contenant la navigation de définition est l’entité propriétaire. Quand le propriétaire fait l’objet d’une interrogation, les types détenus sont inclus par défaut.
Un simple coup d’œil au modèle de domaine suffit pour constater qu’un type détenu n’a apparemment pas d’identité. Les types détenus ont toutefois une identité, mais la propriété de navigation du propriétaire fait partie de cette identité.
L’identité des instances de types détenus ne leur appartient pas entièrement. Il se compose de trois composants :
Identité du propriétaire
la propriété de navigation pointant vers les types ;
dans le cas de collections de types détenus, un composant indépendant (pris en charge dans EF Core 2.2 et les versions ultérieures).
Par exemple, dans le modèle de domaine Ordering sur eShopOnContainers, dans le cadre de l’entité Order, l’objet Valeur d’adresse est implémenté en tant que type d’entité appartenant à l’entité propriétaire, qui est l’entité Order.
Address
est un type sans propriété d’identité définie dans le modèle de domaine. Il est utilisé comme propriété du type Order pour spécifier l’adresse d’expédition d’une commande particulière.
Par convention, une clé primaire cachée est créée pour le type détenu et est mappée à la même table que le propriétaire à l’aide du fractionnement de table. Cela permet d’utiliser des types détenus de la même façon que les types complexes utilisés dans EF6 dans le .NET Framework traditionnel.
Il est important de noter que les types détenus ne sont jamais découverts par convention dans EF Core. Vous devez donc les déclarer explicitement.
Dans eShopOnContainers, dans le fichier OrderingContext.cs, dans la OnModelCreating()
méthode, plusieurs configurations d’infrastructure sont appliquées. L’une d’elles est liée à l’entité Order.
// Part of the OrderingContext.cs class at the Ordering.Infrastructure project
//
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.ApplyConfiguration(new ClientRequestEntityTypeConfiguration());
modelBuilder.ApplyConfiguration(new PaymentMethodEntityTypeConfiguration());
modelBuilder.ApplyConfiguration(new OrderEntityTypeConfiguration());
modelBuilder.ApplyConfiguration(new OrderItemEntityTypeConfiguration());
//...Additional type configurations
}
Dans le code suivant, l’infrastructure de persistance est définie pour l’entité Order :
// Part of the OrderEntityTypeConfiguration.cs class
//
public void Configure(EntityTypeBuilder<Order> orderConfiguration)
{
orderConfiguration.ToTable("orders", OrderingContext.DEFAULT_SCHEMA);
orderConfiguration.HasKey(o => o.Id);
orderConfiguration.Ignore(b => b.DomainEvents);
orderConfiguration.Property(o => o.Id)
.ForSqlServerUseSequenceHiLo("orderseq", OrderingContext.DEFAULT_SCHEMA);
//Address value object persisted as owned entity in EF Core 2.0
orderConfiguration.OwnsOne(o => o.Address);
orderConfiguration.Property<DateTime>("OrderDate").IsRequired();
//...Additional validations, constraints and code...
//...
}
Dans le code précédent, la orderConfiguration.OwnsOne(o => o.Address)
méthode spécifie que la Address
propriété est une entité appartenant au Order
type.
Par défaut, les conventions EF Core nomment les colonnes de la base de données pour les propriétés du type d’entité possédé en tant que EntityProperty_OwnedEntityProperty
. Par conséquent, les propriétés internes de Address
apparaîtront dans la table Orders
avec les noms Address_Street
, Address_City
et ainsi de suite pour State
, Country
et ZipCode
.
Vous pouvez ajouter la Property().HasColumnName()
méthode Fluent pour renommer ces colonnes. Dans le cas où Address
est une propriété publique, les mappages seraient comme ce qui suit :
orderConfiguration.OwnsOne(p => p.Address)
.Property(p=>p.Street).HasColumnName("ShippingStreet");
orderConfiguration.OwnsOne(p => p.Address)
.Property(p=>p.City).HasColumnName("ShippingCity");
Il est possible de chaîner la méthode OwnsOne
dans un mappage fluide. Dans l’exemple hypothétique suivant, OrderDetails
possède BillingAddress
et ShippingAddress
, qui sont les deux Address
types.
OrderDetails
est alors détenu par le type Order
.
orderConfiguration.OwnsOne(p => p.OrderDetails, cb =>
{
cb.OwnsOne(c => c.BillingAddress);
cb.OwnsOne(c => c.ShippingAddress);
});
//...
//...
public class Order
{
public int Id { get; set; }
public OrderDetails OrderDetails { get; set; }
}
public class OrderDetails
{
public Address BillingAddress { get; set; }
public Address ShippingAddress { get; set; }
}
public class Address
{
public string Street { get; set; }
public string City { get; set; }
}
Détails supplémentaires sur les types d’entités détenus
Les types détenus sont définis lorsque vous configurez une propriété de navigation sur un type particulier à l’aide de l’API OwnsOne Fluent.
La définition d’un type détenu dans notre modèle de métadonnées est un composite de : le type propriétaire, la propriété de navigation et le type CLR du type appartenant.
L’identité (clé) d’une instance de type appartenant à notre pile est un composite de l’identité du type propriétaire et de la définition du type appartenant.
Fonctionnalités d’entités détenues
Les types détenus peuvent référencer d’autres entités, qu’elles soient détenues (types détenus imbriqués) ou non (propriétés de navigation de référence régulières à d’autres entités).
Vous pouvez mapper le même type CLR que les différents types appartenant à la même entité propriétaire via des propriétés de navigation distinctes.
Le fractionnement de table est configuré par convention, mais vous pouvez le refuser en mappant le type détenu à une autre table à l’aide de ToTable.
Un chargement hâtif est effectué automatiquement sur les types détenus. Aussi, il est inutile d’appeler
.Include()
sur la requête.Peut être configuré avec l’attribut
[Owned]
, à l’aide d’EF Core 2.1 et versions ultérieures.Peut gérer des collections de types détenus (à l’aide de la version 2.2 et de versions ultérieures).
Limitations des entités détenues
Vous ne pouvez pas créer de
DbSet<T>
d’un type détenu (par conception).Vous ne pouvez pas appeler
ModelBuilder.Entity<T>()
sur les types détenus (actuellement par conception).Les types détenus facultatifs (acceptant une valeur Null) qui sont mappés avec le propriétaire dans la même table (à l’aide du fractionnement de table) ne sont pas pris en charge. Cela est dû au fait que le mappage est effectué pour chaque propriété, il n’existe aucune sentinelle distincte pour la valeur complexe Null dans son ensemble.
Le mappage d’héritage pour les types détenus n’est pas pris en charge, mais vous pouvez normalement mapper deux types feuilles des mêmes hiérarchies d’héritage en tant que types détenus différents. EF Core ne raisonnera pas sur le fait qu’ils font partie de la même hiérarchie.
Principales différences avec les types complexes d’EF6
- Le fractionnement de table est facultatif, c’est-à-dire que les types peuvent éventuellement être mappés à une table distincte tout en restant des types détenus.
Ressources supplémentaires
Martin Fowler. Modèle ValueObject
https://martinfowler.com/bliki/ValueObject.htmlEric Evans. conception Domain-Driven : s’attaquer à la complexité au cœur du logiciel. (Livre ; inclut une discussion sur les objets de valeur)
https://www.amazon.com/Domain-Driven-Design-Tackling-Complexity-Software/dp/0321125215/Vaughn Vernon. Mise en œuvre de la conception Domain-Driven. (Livre ; inclut une discussion sur les objets de valeur)
https://www.amazon.com/Implementing-Domain-Driven-Design-Vaughn-Vernon/dp/0321834577/Types d’entités détenus
https://learn.microsoft.com/ef/core/modeling/owned-entitiesPropriétés de l’ombre
https://learn.microsoft.com/ef/core/modeling/shadow-propertiesTypes complexes et/ou objets valeur. Discussion dans le dépôt GitHub EF Core (onglet Problèmes)
https://github.com/dotnet/efcore/issues/246ValueObject.cs. Classe d’objet valeur de base dans eShopOnContainers.
https://github.com/dotnet-architecture/eShopOnContainers/blob/dev/src/Services/Ordering/Ordering.Domain/SeedWork/ValueObject.csValueObject.cs. Classe d’objet valeur de base dans CSharpFunctionalExtensions.
https://github.com/vkhorikov/CSharpFunctionalExtensions/blob/master/CSharpFunctionalExtensions/ValueObject/ValueObject.csClasse d’adresses. Exemple de classe d’objet value dans eShopOnContainers.
https://github.com/dotnet-architecture/eShopOnContainers/blob/dev/src/Services/Ordering/Ordering.Domain/AggregatesModel/OrderAggregate/Address.cs