Comparteix a través de


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 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:

Captura de pantalla del modelo de base de datos para entidad que incluye una referencia propia

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 de propiedad configurados con OwnsOne o descubiertos a través de 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 en sombra 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 propios, use OwnsMany en OnModelCreating.

Los tipos propios 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 sombra que actúe como la clave foránea en el propietario y como 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 primaria sustituta 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 una entidad hija de un padre a otro, pero esto suele ir en contra de la semántica de agregación.
  • 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:

Captura de pantalla del modelo de base de datos para una entidad que contiene una colección propia

Asignación de tipos propios 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 la navegación definitoria 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 rastreadas de estos objetos, puede resultar útil pensar que la navegación definitoria 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 de 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 de propiedad define un tipo de entidad distinto con una configuración completamente independiente.

Además de los tipos de propiedad anidados, un tipo de propiedad puede hacer referencia a una entidad regular, ya sea el propietario u otra entidad distinta, siempre y cuando la entidad de propiedad sea 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 de 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 propietaria 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 poseídos se pueden configurar aún más de la misma manera que las propiedades de navegación no poseídas.

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

Captura de pantalla del modelo de base de datos para una entidad que contiene referencias propias anidadas

Almacenamiento de tipos propios 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 sobrescribir la convención que asigna un tipo de propiedad a la misma tabla que el propietario, simplemente puede llamar a ToTable y especificar un nombre de tabla diferente. 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 lograr esto, pero tenga en cuenta que esto fallaría si hay múltiples accesos al tipo propiedad, ya que en ese caso se mapearían varios tipos de entidad a la misma tabla.

Consulta de tipos asociados

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 asociados a la base de datos.

var order = await context.DetailedOrders.FirstAsync(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 un DbSet<T> para un tipo 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 EF Core 2.x, las navegaciones de referencia a tipos de entidad poseídos no pueden ser null a menos que se asignen explícitamente 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.