Condividi tramite


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à che contiene un tipo di entità posseduto è il suo 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 posseduti dalla convenzione; è necessario usare esplicitamente il metodo OwnsOne in OnModelCreating oppure annotare il tipo con OwnedAttribute per configurare il tipo come posseduto. 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à posseduta di tipo Order e per configurare facetti 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 del modello di database per l'entità contenente il riferimento posseduto

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

Suggerimento

Il tipo di entità di proprietà può essere contrassegnato come necessario. Per ulteriori informazioni, vedere Dipendenti uno-a-uno richiesti.

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à posseduto. 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 posseduti, utilizza 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à possedute sono definiti tramite una raccolta, non è sufficiente creare solo una proprietà ombra per agire sia come chiave esterna verso il proprietario sia come chiave primaria dell'istanza posseduta, come nel caso di OwnsOne: possono esistere più istanze di tipi di proprietà possedute per ogni proprietario, e quindi la chiave del proprietario non è sufficiente per fornire un'identità univoca a ciascuna istanza posseduta.

Le due soluzioni più semplici sono:

  • Definizione di una chiave primaria surrogata in una nuova proprietà indipendente dalla chiave esterna che fa riferimento 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 potresti spostare un figlio da un padre a un altro, tuttavia questo di solito va contro la 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 dato Padre (quindi se Padre {1} ha Figlio {1,1}, allora 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 chiave primaria diversa, chiamare HasKey.

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:

Screenshot del modello di database dell'entità che contiene la raccolta posseduta

Mappatura dei tipi di proprietà con suddivisione delle tabelle

Quando si usano database relazionali, per impostazione predefinita viene eseguito il mapping dei tipi di riferimento di proprietà 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 proprietari

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 navigazione definente del tipo di entità di proprietà. Dal punto di vista di EF Core, la navigazione definita è parte integrante dell'identità del tipo accanto 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 come EF Core distingua le istanze tracciate di questi oggetti, può essere utile pensare che la navigazione determinante sia diventata parte della chiave dell'istanza, insieme al valore della chiave del proprietario e al tipo .NET del tipo posseduto.

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 posseduti

È 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 rimanda al proprietario. Per definire una navigazione al tipo di entità proprietario che non fa parte della relazione di proprietà, è necessario chiamare `WithOwner()` senza argomenti.

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

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

Il modello precedente è mappato allo schema di database seguente:

Screenshot del modello di database per l'entità contenente riferimenti di proprietà annidati

Archiviazione di tipi posseduti in tabelle separate

A differenza dei tipi complessi EF6, i tipi posseduti possono essere archiviati in una tabella separata dal proprietario. Per ignorare la convenzione che mappa un tipo di proprietà alla stessa tabella del proprietario, è sufficiente chiamare ToTable e fornire un nome di tabella diverso. Nell'esempio seguente, OrderDetails e i suoi due indirizzi vengono mappati a una tabella separata da DetailedOrder.

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

È anche possibile usare il TableAttribute per eseguire questa operazione, ma si noti che ciò fallirebbe se ci sono più navigazioni al tipo posseduto, 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 recupererà Order, OrderDetails e i due StreetAddresses posseduti dal database.

var order = await context.DetailedOrders.FirstAsync(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 intenzionalmente progettate

  • Non è possibile creare un DbSet<T> per un tipo posseduto.
  • Non è possibile chiamare Entity<T>() con un tipo posseduto su 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

  • Nelle navigazioni di riferimento di EF Core 2.x verso tipi di entità di proprietà non possono essere nulli a meno che non vengano mappati esplicitamente su una tabella separata dal proprietario.
  • In EF Core 3.x, le colonne dei tipi di entità di proprietà mappati alla stessa tabella a cui appartiene il proprietario sono sempre contrassegnate come annullabili.