Types d’entité détenus

EF Core vous permet de modéliser des types d’entités qui ne peuvent apparaître que sur les propriétés de navigation d’autres types d’entités. C’est ce qu’on appelle des types d’entités détenus. L’entité contenant un type d’entité détenu est son propriétaire.

Les entités détenues font essentiellement partie du propriétaire et ne peuvent pas exister sans lui, et sont conceptuellement similaires à des agrégats. Cela signifie que l’entité détenue est par définition du côté dépendant de la relation avec le propriétaire.

Configuration des types comme détenus

Par convention, dans la plupart des fournisseurs, les types d’entités ne sont jamais configurés comme détenus. Vous devez utiliser explicitement la méthode OwnsOne dans OnModelCreating ou annoter le type avec OwnedAttribute pour configurer le type comme détenu. Le fournisseur Azure Cosmos DB est une exception. Étant donné qu’Azure Cosmos DB est une base de données de documents, le fournisseur configure par défaut tous les types d’entités associés comme détenus.

Dans cet exemple, StreetAddress est un type sans propriété d’identité. Il est utilisé comme propriété du type Order pour spécifier l’adresse d’expédition d’une commande particulière.

Nous pouvons utiliser l’attribut OwnedAttribute pour le traiter comme une entité détenue lorsqu’elle est référencée à partir d’un autre type d’entité :

[Owned]
public class StreetAddress
{
    public string Street { get; set; }
    public string City { get; set; }
}
public class Order
{
    public int Id { get; set; }
    public StreetAddress ShippingAddress { get; set; }
}

Il est également possible d’utiliser la méthode OwnsOne dans OnModelCreating pour spécifier que la propriété ShippingAddress est une entité détenue du type d’entité Order, puis de configurer des facettes supplémentaires si nécessaire.

modelBuilder.Entity<Order>().OwnsOne(p => p.ShippingAddress);

Si la propriété ShippingAddress est privée dans le type Order, vous pouvez utiliser la version de chaîne de la méthode OwnsOne :

modelBuilder.Entity<Order>().OwnsOne(typeof(StreetAddress), "ShippingAddress");

Le modèle ci-dessus est mappé au schéma de base de données suivant :

Screenshot of the database model for entity containing owned reference

Pour plus de contexte, consultez l’exemple de projet complet.

Conseil

Le type d’entité détenu peut être marqué comme obligatoire. Pour en savoir plus, consultez Dépendants un-à-un requis.

Clés implicites

Les types détenus configurés avec OwnsOne ou découverts par le biais d’une navigation de référence ont toujours une relation un-à-un avec le propriétaire. Par conséquent, ils n’ont pas besoin de leurs propres valeurs de clé, car les valeurs de clé étrangère sont uniques. Dans l’exemple précédent, le type StreetAddress n’a pas besoin de définir une propriété de clé.

Pour comprendre comment EF Core effectue le suivi de ces objets, il est utile de savoir qu’une clé primaire est créée en tant que propriété cachée pour le type détenu. La valeur de la clé d’une instance du type détenu est identique à la valeur de la clé de l’instance propriétaire.

Collections de types détenus

Pour configurer une collection de types détenus, utilisez OwnsMany dans OnModelCreating.

Les types détenus ont besoin d’une clé primaire. S’il n’existe pas de bonnes propriétés candidates sur le type .NET, EF Core peut essayer d’en créer une. Toutefois, lorsque les types détenus sont définis par le biais d’une collection, il n’est pas suffisant de créer une propriété cachée pour servir à la fois de clé étrangère dans le propriétaire et de clé primaire de l’instance détenue, comme nous le faisons pour OwnsOne. Il peut y avoir plusieurs instances de type détenu pour chaque propriétaire, et la clé du propriétaire n’est donc pas suffisante pour fournir une identité unique à chaque instance détenue.

Les deux solutions les plus simples sont les suivantes :

  • Définir une clé primaire de substitution sur une nouvelle propriété indépendante de la clé étrangère qui pointe vers le propriétaire. Les valeurs contenues doivent être uniques pour tous les propriétaires (p. ex., si le Parent {1} a l’Enfant {1}, le Parent {2} ne peut pas avoir l’Enfant {1}), de sorte que la valeur n’a aucune signification inhérente. Étant donné que la clé étrangère ne fait pas partie de la clé primaire, ses valeurs peuvent être modifiées. Vous pouvez donc déplacer un enfant d’un parent à un autre, bien que cela aille généralement à l’encontre de la sémantique d’agrégation.
  • Utiliser la clé étrangère et une propriété supplémentaire comme clé composite. La valeur de propriété supplémentaire doit alors seulement être unique pour un parent donné (donc si le Parent {1} a l’Enfant {1,1}, le Parent {2} peut toujours avoir l’Enfant {2,1}). En rendant la clé étrangère partie intégrante de la clé primaire, la relation entre le propriétaire et l’entité détenue devient immuable et reflète mieux la sémantique d’agrégation. C’est ce que fait EF Core par défaut.

Dans cet exemple, nous allons utiliser la classe Distributor.

public class Distributor
{
    public int Id { get; set; }
    public ICollection<StreetAddress> ShippingCenters { get; set; }
}

Par défaut, la clé primaire utilisée pour le type détenu référencé via la propriété de navigation ShippingCenters sera ("DistributorId", "Id"), où "DistributorId" est la clé étrangère et "Id" est une valeur int unique.

Pour configurer une autre clé primaire, appelez HasKey.

modelBuilder.Entity<Distributor>().OwnsMany(
    p => p.ShippingCenters, a =>
    {
        a.WithOwner().HasForeignKey("OwnerId");
        a.Property<int>("Id");
        a.HasKey("Id");
    });

Le modèle ci-dessus est mappé au schéma de base de données suivant :

Sceenshot of the database model for entity containing owned collection

Mappage des types détenus avec fractionnement de table

Lorsque vous utilisez des bases de données relationnelles, les types détenus de référence par défaut sont mappés à la même table que le propriétaire. Cela nécessite le fractionnement de la table en deux : certaines colonnes sont utilisées pour stocker les données du propriétaire, et d’autres sont utilisées pour stocker les données de l’entité détenue. Il s’agit d’une fonctionnalité courante appelée fractionnement de table.

Par défaut, EF Core nomme les colonnes de la base de données pour les propriétés du type d’entité détenu selon le modèle Navigation_OwnedEntityProperty. Par conséquent, les propriétés StreetAddress s’affichent dans la table 'Orders' avec les noms 'ShippingAddress_Street' et 'ShippingAddress_City'.

Vous pouvez utiliser la méthode HasColumnName pour renommer ces colonnes.

modelBuilder.Entity<Order>().OwnsOne(
    o => o.ShippingAddress,
    sa =>
    {
        sa.Property(p => p.Street).HasColumnName("ShipsToStreet");
        sa.Property(p => p.City).HasColumnName("ShipsToCity");
    });

Remarque

La plupart des méthodes normales de configuration du type d’entité, comme Ignore, peuvent être appelées de la même façon.

Partage du même type .NET entre plusieurs types détenus

Comme un type d’entité détenu peut être du même type .NET qu’un autre type d’entité détenu, le type .NET peut ne pas suffire pour identifier un type détenu.

Dans ce cas, la propriété pointant du propriétaire à l’entité détenue devient la navigation de définition du type d’entité détenu. Du point de vue d’EF Core, la navigation de définition fait partie de l’identité du type, au même titre que le type .NET.

Par exemple, dans la classe suivante, ShippingAddress et BillingAddress sont tous deux du même type .NET : StreetAddress.

public class OrderDetails
{
    public DetailedOrder Order { get; set; }
    public StreetAddress BillingAddress { get; set; }
    public StreetAddress ShippingAddress { get; set; }
}

Pour comprendre comment EF Core distingue les instances suivies de ces objets, il faut imaginer que la navigation de définition est devenue partie intégrante de la clé de l’instance, au même titre que la valeur de la clé du propriétaire et que le type .NET du type détenu.

Types détenus imbriqués

Dans cet exemple, OrderDetails est le propriétaire de BillingAddress et ShippingAddress, qui sont tous deux des types StreetAddress. OrderDetails est alors détenu par le type DetailedOrder.

public class DetailedOrder
{
    public int Id { get; set; }
    public OrderDetails OrderDetails { get; set; }
    public OrderStatus Status { get; set; }
}
public enum OrderStatus
{
    Pending,
    Shipped
}

Chaque navigation vers un type détenu définit un type d’entité distinct avec une configuration totalement indépendante.

En plus des types détenus imbriqués, un type détenu peut référencer une entité régulière qui peut être le propriétaire ou une entité distincte, tant que l’entité détenue est du côté dépendant. Cette capacité distingue les types d’entités détenus des types complexes dans EF6.

public class OrderDetails
{
    public DetailedOrder Order { get; set; }
    public StreetAddress BillingAddress { get; set; }
    public StreetAddress ShippingAddress { get; set; }
}

Configuration des types détenus

Il est possible de chaîner la méthode OwnsOne dans un appel Fluent pour configurer ce modèle :

modelBuilder.Entity<DetailedOrder>().OwnsOne(
    p => p.OrderDetails, od =>
    {
        od.WithOwner(d => d.Order);
        od.Navigation(d => d.Order).UsePropertyAccessMode(PropertyAccessMode.Property);
        od.OwnsOne(c => c.BillingAddress);
        od.OwnsOne(c => c.ShippingAddress);
    });

Notez que l’appel WithOwner utilisé pour définir la propriété de navigation pointe vers le propriétaire. Pour définir une navigation vers le type d’entité propriétaire ne faisant pas partie de la relation de propriété, WithOwner() doit être appelé sans argument.

Il est également possible d’obtenir ce résultat en utilisant OwnedAttribute sur à la fois OrderDetails et StreetAddress.

Notez en outre l’appel Navigation. Les propriétés de navigation vers des types détenus peuvent être configurées plus en avant, comme pour les propriétés de navigation non détenues.

Le modèle ci-dessus est mappé au schéma de base de données suivant :

Screenshot of the database model for entity containing nested owned references

Stockage de types détenus dans des tables distinctes

Par ailleurs, et contrairement aux types complexes EF6, les types détenus peuvent être stockés dans une table distincte du propriétaire. Pour remplacer la convention qui mappe un type détenu à la même table que le propriétaire, vous pouvez simplement appeler ToTable et fournir un nom de table différent. L’exemple suivant mappe OrderDetails et ses deux adresses à une table distincte de DetailedOrder :

modelBuilder.Entity<DetailedOrder>().OwnsOne(p => p.OrderDetails, od => { od.ToTable("OrderDetails"); });

Il est également possible d’utiliser l’attribut TableAttribute pour effectuer cette opération, mais notez que cela échouera s’il existe plusieurs navigations vers le type détenu, car cela signifierait que plusieurs types d’entités sont mappés à la même table.

Interrogation des types détenus

Quand le propriétaire fait l’objet d’une interrogation, les types détenus sont inclus par défaut. Il n’est pas nécessaire d’utiliser la méthode Include, même si les types détenus sont stockés dans une table distincte. En fonction du modèle décrit précédemment, la requête suivante obtient Order, OrderDetails et les deux types détenus StreetAddresses depuis la base de données :

var order = context.DetailedOrders.First(o => o.Status == OrderStatus.Pending);
Console.WriteLine($"First pending order will ship to: {order.OrderDetails.ShippingAddress.City}");

Limites

Certaines de ces limitations sont fondamentales pour le fonctionnement des types d’entités détenus, mais d’autres constituent des restrictions que nous pourrions supprimer dans les futures versions :

Restrictions de conception

  • Vous ne pouvez pas créer de DbSet<T> pour un type détenu.
  • Vous ne pouvez pas appeler Entity<T>() avec un type détenu sur ModelBuilder.
  • Les instances de types d’entités détenus ne peuvent pas être partagées par plusieurs propriétaires (il s’agit d’un scénario bien connu pour les objets de valeur qui ne peuvent pas être implémentés à l’aide de types d’entités détenus).

Lacunes actuelles

  • Les types d’entités détenus ne peuvent pas avoir de hiérarchies d’héritage

Lacunes dans les précédentes versions

  • Dans EF Core 2.x, les navigations de référence vers des types d’entités détenus ne peuvent pas être définies sur la valeur Null, sauf si elles sont explicitement mappées à une table distincte du propriétaire.
  • Dans EF Core 3.x, les colonnes pour les types d’entités détenus mappés à la même table que le propriétaire sont toujours marquées comme pouvant accepter la valeur Null.