Tipos de entidad en propiedad

EF Core permite modelar tipos de entidad que solo pueden aparecer en las propiedades de navegación de otros tipos de entidad. Se denominan tipos de entidad en propiedad. La entidad que contiene un tipo de entidad en propiedad es su propietario.

Las entidades en propiedad son esencialmente parte del propietario y no pueden existir sin él; son conceptualmente similares a losagregados. Esto significa que la entidad en propiedad está, por definición, en el lado dependiente de la relación con el propietario.

Configuración de tipos como en propiedad

En la mayoría de los proveedores, por convención los tipos de entidad nunca se configuran como en propiedad; debe usar explícitamente el método OwnsOne en OnModelCreating, o bien anotar el tipo con OwnedAttribute para configurarlo como en propiedad. El proveedor Azure Cosmos DB es una excepción. Como Azure Cosmos DB es una base de datos de documentos, el proveedor configura todos los tipos de entidad relacionados como en propiedad de forma predeterminada.

En este ejemplo, StreetAddress es un tipo sin ninguna propiedad de identidad. Se usa como propiedad del tipo Order para especificar la dirección de envío de un pedido en concreto.

Se puede usar OwnedAttribute para tratarlo como una entidad en propiedad cuando se le hace referencia desde otro tipo de entidad:

[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; }
}

También es posible usar el método OwnsOne en OnModelCreating para especificar que la propiedad ShippingAddress es una entidad en propiedad del tipo de entidad Order y configurar facetas adicionales si es necesario.

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

Si la propiedad ShippingAddress es privada en el tipo Order, puede usar la versión de cadena del método OwnsOne:

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

El modelo anterior se asigna al siguiente esquema de base de datos:

Screenshot of the database model for entity containing owned reference

Vea el proyecto de ejemplo completo para obtener más contexto.

Sugerencia

El tipo de entidad en propiedad se puede marcar como obligatorio; vea Dependencias de uno a uno obligatorias para más información.

Claves implícitas

Los tipos en propiedad configurados con OwnsOne o detectados mediante una navegación de referencia siempre tienen una relación uno a uno con el propietario, por lo que no necesitan sus propios valores de clave, ya que los valores de clave externa son únicos. En el ejemplo anterior, el tipo StreetAddress no necesita definir una propiedad de clave.

Para comprender cómo EF Core realiza el seguimiento de estos objetos, resulta útil saber que se crea una clave principal como una propiedad reemplazada para el tipo de propiedad. El valor de la clave de una instancia del tipo en propiedad será el mismo que el valor de la clave de la instancia del propietario.

Colecciones de tipos en propiedad

Para configurar una colección de tipos en propiedad, use OwnsMany en OnModelCreating.

Los tipos en propiedad necesitan una clave principal. Si no hay propiedades adecuadas candidatas en el tipo de .NET, EF Core puede intentar crear una. Pero cuando los tipos en propiedad se definen mediante una colección, no basta con crear una propiedad reemplazada para que actúe como la clave externa en el propietario y la clave principal de la instancia en propiedad, como se hace para OwnsOne; puede haber varias instancias de tipo en propiedad para cada propietario y, por tanto, la clave del propietario no es suficiente para proporcionar una identidad única para cada instancia en propiedad.

Las dos soluciones más sencillas para esto son las siguientes:

  • Definir una clave principal suplente en una nueva propiedad independiente de la clave externa que apunta al propietario. Los valores contenidos tendrían que ser únicos en todos los propietarios (por ejemplo, si Parent {1} tiene Child {1}, Parent {2} no puede tener Child {1}), por lo que el valor no tiene ningún significado inherente. Como la clave externa no forma parte de la clave principal, sus valores se pueden cambiar, por lo que puede mover un elemento secundario de un elemento primario a otro, pero esto suele ir en contra de la semántica de agregados.
  • Usar la clave externa y una propiedad adicional como clave compuesta. El valor de propiedad adicional ahora solo debe ser único para un elemento primario determinado (por lo que si Parent {1} tiene Child {1,1}, Parent {2} todavía puede tener Child {2,1}). Al convertir la clave externa en parte de la clave principal, la relación entre el propietario y la entidad en propiedad se convierte en inmutable y refleja mejor la semántica agregada. Esto es lo que EF Core hace de forma predeterminada.

En este ejemplo se usará la clase Distributor.

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

De manera predeterminada, la clave principal usada para el tipo en propiedad al que se hace referencia con la propiedad de navegación ShippingCenters será ("DistributorId", "Id"), donde "DistributorId" es la clave externa y "Id" es un valor int único.

Para configurar otra clave principal, llame a HasKey.

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

El modelo anterior se asigna al siguiente esquema de base de datos:

Sceenshot of the database model for entity containing owned collection

Asignación de tipos en propiedad con división de tablas

Cuando se usan bases de datos relacionales, de forma predeterminada los tipos en propiedad de referencia se asignan a la misma tabla que el propietario. Para esto es necesario dividir la tabla en dos: algunas columnas se usarán para almacenar los datos del propietario y otras para almacenar datos de la entidad en propiedad. Se trata de una característica común conocida como división de tablas.

De manera predeterminada, EF Core asignará un nombre a las columnas de base de datos para las propiedades del tipo de entidad en propiedad siguiendo el patrón Navegación_TipoDeEntidadEnPropiedad. Por tanto, las propiedades StreetAddress aparecerán en la tabla "Orders" con los nombres "ShippingAddress_Street" y "ShippingAddress_City".

Puede usar el método HasColumnName para cambiar el nombre de esas columnas.

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

Nota:

La mayoría de los métodos de configuración de tipos de entidad normales, como Ignore se pueden llamar de la misma manera.

Uso compartido del mismo tipo de .NET entre varios tipos en propiedad

Un tipo de entidad en propiedad puede ser del mismo tipo de .NET que otro, por lo que es posible que el tipo de .NET no sea suficiente para identificar un tipo en propiedad.

En esos casos, la propiedad que apunta del propietario a la entidad en propiedad se convierte en el navegación de definición del tipo de entidad en propiedad. Desde la perspectiva de EF Core, la navegación de definición forma parte de la identidad del tipo junto con el tipo de .NET.

Por ejemplo, en la siguiente clase ShippingAddress y BillingAddress son del mismo tipo de .NET, StreetAddress.

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

Para comprender cómo EF Core distinguirá las instancias que se siguen de estos objetos, puede resultar útil pensar que la navegación de definición se ha convertido en parte de la clave de la instancia junto con el valor de la clave del propietario y el tipo de .NET del tipo en propiedad.

Tipos en propiedad anidados

En este ejemplo OrderDetails posee BillingAddress y ShippingAddress, que son tipos StreetAddress. Luego, OrderDetails es propiedad del tipo DetailedOrder.

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

Cada navegación a un tipo en propiedad define un tipo de entidad distinto con una configuración completamente independiente.

Además de los tipos en propiedad anidados, un tipo en propiedad puede hacer referencia a una entidad regular que puede ser el propietario o una entidad diferente, siempre que la entidad en propiedad esté en el lado dependiente. Esta funcionalidad diferencia a los tipos de entidad en propiedad de los tipos complejos de EF 6.

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

Configuración de tipos en propiedad

Es posible encadenar el método OwnsOne en una llamada fluida para configurar este modelo:

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);
    });

Observe la llamada a WithOwner que se usa para definir la propiedad de navegación que apunta al propietario. Para definir una navegación al tipo de entidad en propiedad que no forma parte de la relación de propiedad, se debe llamar a WithOwner() sin argumentos.

También es posible lograr este resultado mediante OwnedAttribute tanto en OrderDetails como en StreetAddress.

Además, observe la llamada a Navigation. Las propiedades de navegación a tipos en propiedad se pueden configurar aún más como para propiedades de navegación que no son en propiedad.

El modelo anterior se asigna al siguiente esquema de base de datos:

Screenshot of the database model for entity containing nested owned references

Almacenamiento de tipos en propiedad en tablas independientes

Además, a diferencia de los tipos complejos de EF6, los tipos en propiedad se pueden almacenar en una tabla independiente del propietario. Para invalidar la convención que asigna un tipo en propiedad a la misma tabla que el propietario, simplemente puede llamar a ToTable y proporcionar otro nombre de tabla. En el ejemplo siguiente se asignarán OrderDetails y sus dos direcciones a una tabla independiente de DetailedOrder:

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

También es posible usar TableAttribute para lograrlo, pero tenga en cuenta que esto produciría un error si hay varias navegaciones al tipo en propiedad, ya que en ese caso se asignarían varios tipos de entidad a la misma tabla.

Consulta de tipos en propiedad

Al consultar al propietario, los tipos de propiedad se incluyen de forma predeterminada. No es necesario usar el método Include, incluso si los tipos en propiedad se almacenan en una tabla independiente. En función del modelo descrito antes, la consulta siguiente obtendrá Order, OrderDetails y los dos StreetAddresses en propiedad de la base de datos:

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

Limitaciones

Algunas de estas limitaciones son fundamentales para el funcionamiento de los tipos de entidad en propiedad, pero otras son restricciones que se pueden quitar en futuras versiones:

Restricciones por diseño

  • No se puede crear una instancia de DbSet<T> para un tipo en propiedad.
  • No se puede llamar a Entity<T>() con un tipo en propiedad en ModelBuilder.
  • Las instancias de tipos de entidad en propiedad no se pueden compartir con varios propietarios (se trata de un escenario conocido para objetos de valor que no se pueden implementar mediante tipos de entidad en propiedad).

Deficiencias actuales

  • Los tipos de entidad en propiedad no pueden tener jerarquías de herencia

Deficiencias en versiones anteriores

  • En las navegaciones de referencia de EF Core 2.x a tipos de entidad en propiedad no pueden ser null a menos que se asignen de forma explícita a una tabla independiente del propietario.
  • En EF Core 3.x, las columnas de los tipos de entidad en propiedad asignadas a la misma tabla que el propietario siempre se marcan como que admiten un valor NULL.