Tipi di entità di proprietà

EF Core consente di modellare i tipi di entità che possono essere visualizzati solo nelle proprietà di navigazione di altri tipi di entità. Questi tipi sono denominati tipi di entità di proprietà. L'entità contenente un tipo di entità di proprietà è il proprietario.

Le entità di proprietà sono essenzialmente una parte del proprietario e non possono esistere senza di essa, sono concettualmente simili alle aggregazioni. Ciò significa che l'entità di proprietà è per definizione sul lato dipendente della relazione con il proprietario.

Configurazione dei tipi come di proprietà

Nella maggior parte dei provider i tipi di entità non vengono mai configurati come di proprietà della convenzione. È necessario usare in modo esplicito il OwnsOne metodo in OnModelCreating o annotare il tipo con OwnedAttribute per configurare il tipo come di proprietà. Il provider Azure Cosmos DB è un'eccezione. Poiché Azure Cosmos DB è un database di documenti, il provider configura tutti i tipi di entità correlati come di proprietà per impostazione predefinita.

In questo esempio, StreetAddress è un tipo senza proprietà Identity. Viene usato come proprietà del tipo Order per specificare l'indirizzo di spedizione per uno specifico ordine.

È possibile usare OwnedAttribute per considerarlo come entità di proprietà quando viene fatto riferimento da un altro tipo di 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; }
}

È anche possibile usare il OwnsOne metodo in OnModelCreating per specificare che la ShippingAddress proprietà è un'entità di proprietà del Order tipo di entità e per configurare facet aggiuntivi, se necessario.

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

Se la ShippingAddress proprietà è privata nel Order tipo, è possibile usare la versione stringa del OwnsOne metodo :

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

Il modello precedente è mappato allo schema di database seguente:

Screenshot of the database model for entity containing owned reference

Per altre informazioni di contesto, vedere il progetto di esempio completo.

Suggerimento

Il tipo di entità di proprietà può essere contrassegnato come richiesto. Per altre informazioni, vedere Dipendenti uno-a-uno necessari.

Chiavi implicite

I tipi di proprietà configurati con OwnsOne o individuati tramite una navigazione di riferimento hanno sempre una relazione uno-a-uno con il proprietario, pertanto non hanno bisogno dei propri valori di chiave perché i valori di chiave esterna sono univoci. Nell'esempio precedente, il StreetAddress tipo non deve definire una proprietà chiave.

Per comprendere come EF Core tiene traccia di questi oggetti, è utile sapere che una chiave primaria viene creata come proprietà shadow per il tipo di proprietà. Il valore della chiave di un'istanza del tipo di proprietà sarà uguale al valore della chiave dell'istanza del proprietario.

Raccolte di tipi di proprietà

Per configurare una raccolta di tipi di proprietà da usare OwnsMany in OnModelCreating.

I tipi di proprietà necessitano di una chiave primaria. Se non sono presenti proprietà idonee per il tipo .NET, EF Core può provare a crearne uno. Tuttavia, quando i tipi di proprietà vengono definiti tramite una raccolta, non è sufficiente creare solo una proprietà shadow per agire sia come chiave esterna nel proprietario che nella chiave primaria dell'istanza di proprietà, come OwnsOneper : possono essere presenti più istanze di tipo di proprietà per ogni proprietario e quindi la chiave del proprietario non è sufficiente per fornire un'identità univoca per ogni istanza di proprietà.

Le due soluzioni più semplici sono:

  • Definizione di una chiave primaria surrogata in una nuova proprietà indipendente dalla chiave esterna che punta al proprietario. I valori contenuti devono essere univoci in tutti i proprietari ,ad esempio se Padre {1} ha Figlio {1}, l'elemento Padre {2} non può avere Figlio {1}, quindi il valore non ha alcun significato intrinseco. Poiché la chiave esterna non fa parte della chiave primaria, è possibile modificarne i valori, quindi è possibile spostare un elemento figlio da un elemento padre a un altro, ma questo avviene in genere rispetto alla semantica di aggregazione.
  • Uso della chiave esterna e di una proprietà aggiuntiva come chiave composita. Il valore della proprietà aggiuntivo deve ora essere univoco solo per un determinato elemento padre( quindi se Padre {1} ha Figlio {1,1} , l'elemento Padre {2} può comunque avere Figlio {2,1}). Rendendo la chiave esterna parte della chiave primaria, la relazione tra il proprietario e l'entità di proprietà diventa non modificabile e riflette meglio la semantica di aggregazione. Questo è ciò che EF Core esegue per impostazione predefinita.

In questo esempio si userà la Distributor classe .

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

Per impostazione predefinita, la chiave primaria usata per il tipo di proprietà a cui si fa riferimento tramite la ShippingCenters proprietà di navigazione sarà ("DistributorId", "Id") dove "DistributorId" è FK ed "Id" è un valore univoco int .

Per configurare una diversa chiamata HasKeydi chiave primaria.

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

Il modello precedente è mappato allo schema di database seguente:

Sceenshot of the database model for entity containing owned collection

Mapping dei tipi di proprietà con suddivisione delle tabelle

Quando si usano database relazionali, per impostazione predefinita viene eseguito il mapping dei tipi di proprietà di riferimento alla stessa tabella del proprietario. Questa operazione richiede la suddivisione della tabella in due: alcune colonne verranno usate per archiviare i dati del proprietario e alcune colonne verranno usate per archiviare i dati dell'entità di proprietà. Si tratta di una funzionalità comune nota come suddivisione delle tabelle.

Per impostazione predefinita, EF Core denomina le colonne di database per le proprietà del tipo di entità di proprietà seguendo il modello Navigation_OwnedEntityProperty. Pertanto, le StreetAddress proprietà verranno visualizzate nella tabella 'Orders' con i nomi 'ShippingAddress_Street' e 'ShippingAddress_City'.

È possibile usare il HasColumnName metodo per rinominare tali colonne.

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

Nota

La maggior parte dei normali metodi di configurazione dei tipi di entità, ad esempio Ignore , può essere chiamata nello stesso modo.

Condivisione dello stesso tipo .NET tra più tipi di proprietà

Un tipo di entità di proprietà può essere dello stesso tipo .NET di un altro tipo di entità di proprietà, pertanto il tipo .NET potrebbe non essere sufficiente per identificare un tipo di proprietà.

In questi casi, la proprietà che punta dal proprietario all'entità di proprietà diventa la struttura di navigazione del tipo di entità di proprietà. Dal punto di vista di EF Core, lo spostamento che definisce fa parte dell'identità del tipo insieme al tipo .NET.

Ad esempio, nella classe ShippingAddress seguente e BillingAddress sono entrambi dello stesso tipo .NET, StreetAddress.

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

Per comprendere in che modo EF Core distinguerà le istanze rilevate di questi oggetti, può essere utile pensare che la navigazione che definisce sia diventata parte della chiave dell'istanza insieme al valore della chiave del proprietario e del tipo .NET del tipo di proprietà.

Tipi di proprietà annidati

In questo esempio OrderDetails è proprietario di BillingAddress e ShippingAddress, che sono entrambi StreetAddress tipi. Quindi OrderDetails è di proprietà 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
}

Ogni navigazione in un tipo di proprietà definisce un tipo di entità separato con una configurazione completamente indipendente.

Oltre ai tipi di proprietà annidati, un tipo di proprietà può fare riferimento a un'entità regolare che può essere il proprietario o un'entità diversa purché l'entità di proprietà si trova sul lato dipendente. Questa funzionalità imposta tipi di entità di proprietà diversi dai tipi complessi in EF6.

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

Configurazione dei tipi di proprietà

È possibile concatenare il OwnsOne metodo in una chiamata Fluent per configurare questo modello:

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

Si noti la WithOwner chiamata usata per definire la proprietà di navigazione che punta di nuovo al proprietario. Per definire una navigazione al tipo di entità proprietario che non fa parte della relazione WithOwner() di proprietà, è necessario chiamare senza argomenti.

È anche possibile ottenere questo risultato usando OwnedAttribute sia OrderDetails su che StreetAddresssu .

Si noti anche la Navigation chiamata. Le proprietà di navigazione ai tipi di proprietà possono essere ulteriormente configurate come per le proprietà di navigazione non di proprietà.

Il modello precedente è mappato allo schema di database seguente:

Screenshot of the database model for entity containing nested owned references

Archiviazione di tipi di proprietà in tabelle separate

A differenza dei tipi complessi EF6, i tipi di proprietà possono essere archiviati in una tabella separata dal proprietario. Per eseguire l'override della convenzione che esegue il mapping di un tipo di proprietà alla stessa tabella del proprietario, è sufficiente chiamare ToTable e specificare un nome di tabella diverso. Nell'esempio seguente viene eseguito il mapping OrderDetails e i relativi due indirizzi a una tabella separata da DetailedOrder:

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

È anche possibile usare per TableAttribute eseguire questa operazione, ma si noti che l'errore potrebbe verificarsi se sono presenti più spostamenti al tipo di proprietà, poiché in tal caso più tipi di entità verrebbero mappati alla stessa tabella.

Esecuzione di query sui tipi di proprietà

Quando si esegue una query sul proprietario, i tipi di proprietà saranno inclusi per impostazione predefinita. Non è necessario utilizzare il Include metodo , anche se i tipi di proprietà vengono archiviati in una tabella separata. In base al modello descritto in precedenza, la query seguente otterrà OrderOrderDetails e i due di proprietà StreetAddresses del database:

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

Limiti

Alcune di queste limitazioni sono fondamentali per il funzionamento dei tipi di entità di proprietà, ma altre sono restrizioni che potrebbero essere rimosse nelle versioni future:

Restrizioni per impostazione predefinita

  • Non è possibile creare un oggetto DbSet<T> per un tipo di proprietà.
  • Non è possibile chiamare Entity<T>() con un tipo di proprietà in ModelBuilder.
  • Le istanze dei tipi di entità di proprietà non possono essere condivise da più proprietari( si tratta di uno scenario noto per gli oggetti valore che non possono essere implementati usando tipi di entità di proprietà).

Carenze attuali

  • I tipi di entità di proprietà non possono avere gerarchie di ereditarietà

Carenze nelle versioni precedenti

  • Negli spostamenti di riferimento di EF Core 2.x ai tipi di entità di proprietà non possono essere Null a meno che non vengano mappati in modo esplicito a una tabella separata dal proprietario.
  • In EF Core 3.x le colonne per i tipi di entità di proprietà mappate alla stessa tabella del proprietario vengono sempre contrassegnate come nullable.