Tipos de entidade de propriedade

O EF Core permite que você modele tipos de entidade que só podem aparecer nas propriedades de navegação de outros tipos de entidade. Eles são chamados de tipos de entidade de propriedade. A entidade que contém um tipo de entidade de propriedade é seu proprietário.

As entidades de propriedade são essencialmente parte do proprietário e não podem existir sem ele, elas são conceitualmente semelhantes às agregações. Isso significa que a entidade de propriedade é por definição do lado dependente da relação com o proprietário.

Configurando tipos de propriedade

Na maioria dos provedores, os tipos de entidade nunca são configurados como propriedade da convenção – você deve usar explicitamente o método OwnsOne em OnModelCreating ou anotar o tipo para OwnedAttribute configurar o tipo como de propriedade. O provedor do Azure Cosmos DB é uma exceção a isso. Como o Azure Cosmos DB é um banco de dados de documento, o provedor configura todos os tipos de entidades relacionadas de propriedade por padrão.

Neste exemplo, StreetAddress é um tipo sem propriedade de identidade. Ele é usado como uma propriedade do tipo Ordem para especificar o endereço para entrega para uma ordem específica.

Podemos usá-la OwnedAttribute para tratá-la como uma entidade de propriedade quando referenciada de outro tipo de entidade:

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

Também é possível usar o método OwnsOne em OnModelCreating para especificar que a propriedade ShippingAddress é uma entidade de propriedade do tipo de entidade Order e configurar facetas adicionais, se necessário.

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

Se a propriedade ShippingAddress for privada no tipo Order, você poderá usar a versão da cadeia de caracteres do método OwnsOne:

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

O modelo acima é mapeado para o seguinte esquema de banco de dados:

Screenshot of the database model for entity containing owned reference

Consulte o projeto de exemplo completo para obter mais contexto.

Dica

O tipo de entidade de propriedade pode ser marcado como necessário, consulte dependentes um-para-um necessários para obter mais informações.

Chaves implícitas

Tipos de propriedade configurados com OwnsOne ou descobertos por meio de uma navegação de referência sempre têm uma relação um-para-um com o proprietário, portanto, eles não precisam de seus próprios valores de chave, pois os valores de chave estrangeira são exclusivos. No exemplo anterior, o tipo StreetAddress não precisa definir uma propriedade de chave.

Para entender como o EF Core rastreia esses objetos, é útil saber que uma chave primária é criada como uma propriedade de sombra para o tipo de propriedade. O valor da chave de uma instância do tipo de propriedade será o mesmo que o valor da chave da instância do proprietário.

Coleções de tipos de propriedade

Para configurar uma coleção de tipos de propriedade usados OwnsMany em OnModelCreating.

Os tipos de propriedade precisam de uma chave primária. Se não houver boas propriedades de candidatos no tipo .NET, o EF Core poderá tentar criar uma. No entanto, quando os tipos de propriedade são definidos por meio de uma coleção, não basta apenas criar uma propriedade de sombra para atuar como a chave estrangeira no proprietário e a chave primária da instância de propriedade, como fazemos para OwnsOne: pode haver várias instâncias de tipo de propriedade para cada proprietário e, portanto, a chave do proprietário não é suficiente para fornecer uma identidade exclusiva para cada instância de propriedade.

As duas soluções mais simples para isso são:

  • Definindo uma chave primária substituta em uma nova propriedade independente da chave estrangeira que aponta para o proprietário. Os valores contidos precisariam ser exclusivos em todos os proprietários (por exemplo, se Pai {1} tiver Filho {1}, então Pai {2} não pode ter Filho {1}), portanto, o valor não tem nenhum significado inerente. Como a chave estrangeira não faz parte da chave primária, seus valores podem ser alterados, portanto, você pode mover um filho de um pai para outro, no entanto, isso geralmente vai contra a semântica agregada.
  • Usando a chave estrangeira e uma propriedade adicional como uma chave composta. O valor da propriedade adicional agora só precisa ser exclusivo para um determinado pai (portanto, se o pai {1} tiver um filho {1,1}, o pai {2} ainda poderá ter filho {2,1}). Ao tornar a chave estrangeira parte da chave primária, a relação entre o proprietário e a entidade de propriedade torna-se imutável e reflete melhor a semântica de agregação. Isso é o que o EF Core faz por padrão.

Neste exemplo, usaremos a classe Distributor.

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

Por padrão, a chave primária usada para o tipo de propriedade referenciada por meio da propriedade de navegação ShippingCenters será ("DistributorId", "Id") onde "DistributorId" está o FK e "Id" é um valor int exclusivo.

Para configurar uma chamada de chave primária diferente HasKey.

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

O modelo acima é mapeado para o seguinte esquema de banco de dados:

Sceenshot of the database model for entity containing owned collection

Mapeamento de tipos de propriedade com divisão de tabela

Ao usar bancos de dados relacionais, por padrão, os tipos de propriedade de referência são mapeados para a mesma tabela que o proprietário. Isso requer a divisão da tabela em duas: algumas colunas serão usadas para armazenar os dados do proprietário e algumas colunas serão usadas para armazenar dados da entidade de propriedade. Esse é um recurso comum conhecido como divisão de tabela.

Por padrão, o EF Core nomeará as colunas de banco de dados para as propriedades do tipo de entidade de propriedade seguindo o padrão Navigation_OwnedEntityProperty. Portanto, as propriedades StreetAddress serão exibidas na tabela 'Orders' com os nomes 'ShippingAddress_Street' e 'ShippingAddress_City'.

Você pode usar o método HasColumnName para renomear essas colunas.

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

Observação

A maioria dos métodos normais de configuração de tipo de entidade, como Ignore, pode ser chamada da mesma maneira.

Compartilhando o mesmo tipo .NET entre vários tipos de propriedade

Um tipo de entidade de propriedade pode ser do mesmo tipo .NET que outro tipo de entidade de propriedade, portanto, o tipo .NET pode não ser suficiente para identificar um tipo de propriedade.

Nesses casos, a propriedade que aponta do proprietário para a entidade de propriedade torna-se a navegação definidora do tipo de entidade de propriedade. Da perspectiva do EF Core, a definição de navegação faz parte da identidade do tipo junto com o tipo .NET.

Por exemplo, na classe a seguir ambos ShippingAddress e BillingAddress são do mesmo tipo .NET. StreetAddress

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

Para entender como o EF Core distinguirá instâncias controladas desses objetos, pode ser útil pensar que a definição de navegação se tornou parte da chave da instância juntamente com o valor da chave do proprietário e o tipo .NET do tipo de propriedade.

Tipos aninhados de propriedade

Neste exemplo OrderDetails, possui BillingAddress e ShippingAddress, que são ambos StreetAddress tipos. Então OrderDetails pertence ao 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 navegação para um tipo de propriedade define um tipo de entidade separado com configuração completamente independente.

Além dos tipos aninhados, um tipo de propriedade pode referenciar uma entidade regular que pode ser o proprietário ou uma entidade diferente, desde que a entidade de propriedade esteja no lado dependente. Essa funcionalidade define tipos de entidade de propriedade separados de tipos complexos no EF6.

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

Configurando tipos de propriedade

É possível encadear o método OwnsOne em uma chamada fluente 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 a chamada WithOwner usada para definir a propriedade de navegação apontando de volta para o proprietário. Para definir uma navegação para o tipo de entidade de proprietário que não faz parte da relação de propriedade WithOwner() deve ser chamada sem argumentos.

Também é possível obter esse resultado usando OwnedAttribute em ambos OrderDetails e StreetAddress.

Além disso, observe a chamada Navigation. As propriedades de navegação para tipos de propriedade podem ser configuradas ainda mais como para propriedades de navegação não pertencentes.

O modelo acima é mapeado para o seguinte esquema de banco de dados:

Screenshot of the database model for entity containing nested owned references

Armazenando tipos de propriedade em tabelas separadas

Além disso, ao contrário dos tipos complexos EF6, os tipos de propriedade podem ser armazenados em uma tabela separada do proprietário. Para substituir a convenção que mapeia um tipo de propriedade para a mesma tabela que o proprietário, você pode simplesmente chamar ToTable e fornecer um nome de tabela diferente. O exemplo a seguir mapeará OrderDetails e seus dois endereços para uma tabela separada de DetailedOrder:

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

Também é possível usar TableAttribute para fazer isso, mas observe que isso falhará se houver várias navegações para o tipo de propriedade, pois nesse caso vários tipos de entidade seriam mapeados para a mesma tabela.

Consultando tipos de propriedade

Ao consultar o proprietário, os tipos próprios serão incluídos por padrão. Não é necessário usar o método Include, mesmo que os tipos de propriedade sejam armazenados em uma tabela separada. Com base no modelo descrito anteriormente, a consulta a seguir obterá Order, OrderDetails e os dois pertencentes StreetAddresses do banco de dados:

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

Limitações

Algumas dessas limitações são fundamentais para como os tipos de entidade de propriedade funcionam, mas algumas outras são restrições que podemos remover em versões futuras:

Restrições de by-design

  • Você não pode criar um tipo de propriedade DbSet<T>.
  • Não é possível chamar Entity<T>() com um tipo próprio. ModelBuilder
  • Instâncias de tipos de entidade de propriedade não podem ser compartilhadas por vários proprietários (este é um cenário bem conhecido para objetos de valor que não podem ser implementados usando tipos de entidade de propriedade).

Deficiências atuais

  • Tipos de entidade de propriedade não podem ter hierarquias de herança

Deficiências em versões anteriores

  • No EF Core 2.x, as navegações de referência para tipos de entidade de propriedade não podem ser nulas, a menos que sejam explicitamente mapeadas para uma tabela separada do proprietário.
  • No EF Core 3.x, as colunas para tipos de entidade de propriedade mapeadas para a mesma tabela que o proprietário são sempre marcadas como anuláveis.