Spostamenti tra relazioni

Le relazioni di EF Core sono definite da chiavi esterne. Gli spostamenti vengono sovrapposti alle chiavi esterne per fornire una visualizzazione naturale orientata agli oggetti per la lettura e la modifica delle relazioni. Usando gli spostamenti, le applicazioni possono usare grafici di entità senza preoccuparsi di ciò che accade ai valori di chiave esterna.

Importante

Più relazioni non possono condividere gli spostamenti. Qualsiasi chiave esterna può essere associata al massimo una navigazione dall'entità a quella dipendente e al massimo uno spostamento da dipendente a entità.

Suggerimento

Non è necessario eseguire spostamenti virtuali, a meno che non vengano usati da proxy lazy-loading o change-tracking .

Spostamenti di riferimento

Gli spostamenti sono disponibili in due forme- riferimento e raccolta. Gli spostamenti di riferimento sono semplici riferimenti a oggetti a un'altra entità. Rappresentano i lati "uno" delle relazioni uno-a-molti e uno-a-uno . Ad esempio:

public Blog TheBlog { get; set; }

Gli spostamenti di riferimento devono avere un setter, anche se non è necessario essere pubblici. Gli spostamenti di riferimento non devono essere inizializzati automaticamente su un valore predefinito non Null; in questo modo equivale ad affermare che un'entità esiste quando non esiste.

Quando si usano tipi riferimento nullable C#, gli spostamenti di riferimento devono essere nullable per le relazioni facoltative:

public Blog? TheBlog { get; set; }

Gli spostamenti di riferimento per le relazioni necessarie possono essere nullable o non nullable.

Spostamenti nella raccolta

Gli spostamenti delle raccolte sono istanze di un tipo di raccolta .NET; ovvero qualsiasi tipo che implementa ICollection<T>. La raccolta contiene istanze del tipo di entità correlato, di cui può essere presente un numero qualsiasi. Rappresentano i lati "molti" delle relazioni uno-a-molti e molti-a-molti . Ad esempio:

public ICollection<Post> ThePosts { get; set; }

Non è necessario che gli spostamenti delle raccolte abbiano un setter. È comune inizializzare la raccolta inline, eliminando così la necessità di controllare se la proprietà è null. Ad esempio:

public ICollection<Post> ThePosts { get; } = new List<Post>();

Suggerimento

Non creare accidentalmente una proprietà con corpo di espressione, ad esempio public ICollection<Post> ThePosts => new List<Post>();. Verrà creata una nuova istanza di raccolta vuota ogni volta che si accede alla proprietà e pertanto sarà inutile come navigazione.

Tipi di raccolta

L'istanza di raccolta sottostante deve implementare ICollection<T>e deve avere un metodo di lavoro Add . È comune usare List<T> o HashSet<T>. List<T> è efficiente per un numero ridotto di entità correlate e mantiene un ordinamento stabile. HashSet<T> offre ricerche più efficienti per un numero elevato di entità, ma non dispone di un ordinamento stabile. È anche possibile usare la propria implementazione di raccolta personalizzata.

Importante

La raccolta deve utilizzare l'uguaglianza dei riferimenti. Quando si crea un HashSet<T> oggetto per la navigazione di una raccolta, assicurarsi di usare ReferenceEqualityComparer.

Le matrici non possono essere usate per gli spostamenti nella raccolta perché, anche se implementano ICollection<T>, il Add metodo genera un'eccezione quando viene chiamato.

Anche se l'istanza della raccolta deve essere un ICollection<T>oggetto , la raccolta non deve essere esposta come tale. Ad esempio, è comune esporre la navigazione come IEnumerable<T>, che fornisce una visualizzazione di sola lettura che non può essere modificata in modo casuale dal codice dell'applicazione. Ad esempio:

public class Blog
{
    public int Id { get; set; }
    public IEnumerable<Post> ThePosts { get; } = new List<Post>();
}

Una variante di questo modello include metodi per la manipolazione della raccolta in base alle esigenze. Ad esempio:

public class Blog
{
    private readonly List<Post> _posts = new();

    public int Id { get; set; }

    public IEnumerable<Post> Posts => _posts;

    public void AddPost(Post post) => _posts.Add(post);
}

Il codice dell'applicazione potrebbe comunque eseguire il cast della raccolta esposta in un oggetto ICollection<T> e quindi modificarlo. Se si tratta di un problema, l'entità potrebbe restituire una copia difensiva della raccolta. Ad esempio:

public class Blog
{
    private readonly List<Post> _posts = new();

    public int Id { get; set; }

    public IEnumerable<Post> Posts => _posts.ToList();

    public void AddPost(Post post) => _posts.Add(post);
}

Valutare attentamente se il valore ottenuto da questo valore è sufficientemente elevato che supera il sovraccarico di creazione di una copia della raccolta ogni volta che si accede al riquadro di spostamento.

Suggerimento

Questo modello finale funziona perché, per impostazione predefinita, Entity Framework accede alla raccolta tramite il relativo campo sottostante. Ciò significa che EF stesso aggiunge e rimuove le entità dalla raccolta effettiva, mentre le applicazioni interagiscono solo con una copia difensiva della raccolta.

Inizializzazione degli spostamenti di raccolte

Gli spostamenti della raccolta possono essere inizializzati dal tipo di entità, in modo eager:

public class Blog
{
    public ICollection<Post> Posts { get; } = new List<Post>();
}

O pigrizia:

public class Blog
{
    private ICollection<Post>? _posts;

    public ICollection<Post> Posts => _posts ??= new List<Post>();
}

Se EF deve aggiungere un'entità a una navigazione nella raccolta, ad esempio durante l'esecuzione di una query, inizializzerà la raccolta se è attualmente null. L'istanza creata dipende dal tipo esposto della struttura di spostamento.

  • Se lo spostamento viene esposto come HashSet<T>, viene creata un'istanza di HashSet<T> using ReferenceEqualityComparer .
  • In caso contrario, se lo spostamento viene esposto come tipo concreto con un costruttore senza parametri, viene creata un'istanza di tale tipo concreto. Questo vale per List<T>, ma anche per altri tipi di raccolta, inclusi i tipi di raccolta personalizzati.
  • In caso contrario, se lo spostamento viene esposto come IEnumerable<T>, o , ISet<T>viene creata un'istanza di HashSet<T> using ReferenceEqualityComparerICollection<T>.
  • In caso contrario, se lo spostamento viene esposto come IList<T>, viene creata un'istanza di List<T> .
  • In caso contrario, viene generata un'eccezione.

Nota

Se le entità di notifica, inclusi i proxy di rilevamento delle modifiche, vengono usate e ObservableHashSet<T>ObservableCollection<T> vengono usate al posto di List<T> e HashSet<T>.

Importante

Come descritto nella documentazione relativa al rilevamento delle modifiche, Entity Framework tiene traccia di una singola istanza di qualsiasi entità con un determinato valore di chiave. Ciò significa che le raccolte usate come spostamenti devono usare la semantica di uguaglianza dei riferimenti. I tipi di entità che non eseguono l'override dell'uguaglianza degli oggetti otterranno questo valore per impostazione predefinita. Assicurarsi di usare ReferenceEqualityComparer quando si crea un HashSet<T> oggetto da usare come struttura di spostamento per assicurarsi che funzioni per tutti i tipi di entità.

Configurazione degli spostamenti

Gli spostamenti sono inclusi nel modello come parte della configurazione di una relazione. Ovvero, per convenzione, o usando , HasManye così via HasOnenell'API di compilazione del modello. La maggior parte della configurazione associata agli spostamenti viene eseguita configurando la relazione stessa.

Esistono tuttavia alcuni tipi di configurazione specifici per le proprietà di navigazione stesse, anziché far parte della configurazione complessiva della relazione. Questo tipo di configurazione viene eseguito con il Navigation metodo . Ad esempio, per forzare EF ad accedere alla struttura di spostamento tramite la relativa proprietà, anziché usare il campo sottostante:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .Navigation(e => e.Posts)
        .UsePropertyAccessMode(PropertyAccessMode.Property);

    modelBuilder.Entity<Post>()
        .Navigation(e => e.Blog)
        .UsePropertyAccessMode(PropertyAccessMode.Property);
}

Nota

Impossibile utilizzare la Navigation chiamata per creare una proprietà di navigazione. Viene usato solo per configurare una proprietà di navigazione creata in precedenza definendo una relazione o da una convenzione.

Spostamenti necessari

Se la relazione è necessaria, è necessaria una navigazione da dipendente a entità, che a sua volta indica che la proprietà della chiave esterna non è nullable. Al contrario, lo spostamento è facoltativo se la chiave esterna è nullable e la relazione è pertanto facoltativa.

Gli spostamenti di riferimento dall'entità a dipendente sono diversi. Nella maggior parte dei casi, un'entità principal può esistere sempre senza entità dipendenti. Ovvero, una relazione obbligatoria non indica che ci sarà sempre almeno un'entità dipendente. Non esiste alcun modo nel modello di Entity Framework e non esiste un modo standard in un database relazionale, per garantire che un'entità sia associata a un determinato numero di dipendenti. Se necessario, deve essere implementato nella logica dell'applicazione (business).

Esiste un'eccezione a questa regola: quando i tipi principal e dipendenti condividono la stessa tabella in un database relazionale o contenuti in un documento. Ciò può verificarsi con tipi di proprietà o tipi non di proprietà che condividono la stessa tabella. In questo caso, la proprietà di navigazione dall'entità all'entità dipendente può essere contrassegnata come obbligatoria, a indicare che il dipendente deve esistere.

La configurazione della proprietà di navigazione in base alle esigenze viene eseguita usando il Navigation metodo . Ad esempio:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.Entity<Blog>()
        .Navigation(e => e.BlogHeader)
        .IsRequired();
}