Teilen über


Beziehungsnavigationen

EF Core-Beziehungen werden durch Fremdschlüssel definiert. Navigationen werden über Fremdschlüsseln angeordnet, um eine natürliche objektorientierte Ansicht zum Lesen und Bearbeiten von Beziehungen bereitzustellen. Mithilfe von Navigationen können Anwendungen mit Graphen von Entitäten arbeiten, ohne sich darum kümmern zu müssen, was mit den Fremdschlüsselwerten geschieht.

Wichtig

Mehrere Beziehungen können keine gemeinsamen Navigationen nutzen. Jeder Fremdschlüssel kann höchstens einer Navigation von der Prinzipal- zur abhängigen Instanz und höchstens einer Navigation von der abhängigen zur Prinzipalinstanz zugeordnet sein.

Tipp

Es ist nicht erforderlich, Navigationen virtuell zu gestalten, es sei denn, sie werden von Proxys vom Typ lazy-loading (verzögertes Laden) oder change-tracking (Änderungsnachverfolgung) verwendet.

Verweisnavigationen

Es gibt zwei Arten von Navigationen: Verweis und Sammlung. Verweisnavigationen sind einfache Objektverweise auf eine andere Entität. Sie stellen die „1“ Seite(n) von 1:N- und 1:1-Beziehungen dar. Beispiel:

public Blog TheBlog { get; set; }

Verweisnavigationen müssen über einen Setter verfügen, obwohl sie nicht öffentlich sein müssen. Verweisnavigationen sollten nicht automatisch mit einem Standardwert initialisiert werden, der nicht Null ist. Dies ist gleichbedeutend mit der Behauptung, dass eine Entität existiert, obwohl dies nicht der Fall ist.

Bei Verwendung von C#-Verweistypen, die Nullwerte zulassen, müssen Verweisnavigationen für optionale Beziehungen Nullwerte zulassen:

public Blog? TheBlog { get; set; }

Verweisnavigationen für erforderliche Beziehungen können Nullwerte zulassen oder „Non-Nullable“ sein.

Sammlungsnavigationen

Sammlungsnavigationen sind Instanzen eines .NET-Sammlungstyps; d. h. jeder Typ, der ICollection<T> implementiert. Die Sammlung enthält Instanzen des entsprechenden Entitätstyps, von denen es beliebig viele geben kann. Sie stellen die „N“ Seite(n) von 1:N- und M:N-Beziehungen dar. Beispiel:

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

Sammlungsnavigationen müssen nicht über einen Setter verfügen. Es ist üblich, die Sammlung inline zu initialisieren und damit die Notwendigkeit zu beseitigen, jemals zu prüfen, ob die Eigenschaft null ist. Beispiel:

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

Tipp

Erstellen Sie nicht versehentlich eine Ausdruckskörpereigenschaft, z. B. public ICollection<Post> ThePosts => new List<Post>();. Dadurch wird bei jedem Zugriff auf die Eigenschaft eine neue, leere Sammlungsinstanz erstellt, sodass sie als Navigation nutzlos ist.

Auflistungstypen

Die zugrunde liegende Sammlungsinstanz muss ICollection<T> implementieren und eine funktionierende Add Methode haben. Es ist üblich, List<T> oder HashSet<T> zu verwenden. List<T> ist effizient für eine kleine Anzahl verwandter Entitäten und sorgt für eine stabile Ordnung. HashSet<T> verfügt über effizientere Lookups für große Anzahlen von Entitäten, hat aber keine stabile Ordnung. Sie können auch ihre eigene benutzerdefinierte Sammlungsimplementierung verwenden.

Wichtig

Die Sammlung muss die Verweisgleichheit verwenden. Wenn Sie ein HashSet<T> für eine Sammlungsnavigation erstellen, sollten Sie unbedingt ReferenceEqualityComparer verwenden.

Arrays können nicht für Sammlungsnavigationen verwendet werden, denn obwohl sie ICollection<T> implementieren, löst die Methode Add beim Aufruf eine Ausnahme aus.

Obwohl die Sammlungsinstanz ein ICollection<T> sein muss, muss die Sammlung nicht als solche verfügbar gemacht werden. Es ist z. B. üblich, die Navigation als IEnumerable<T> verfügbar zu machen, was eine schreibgeschützte Ansicht bietet, die nicht willkürlich durch Anwendungscode geändert werden kann. Beispiel:

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

Eine Variation dieses Musters umfasst Methoden für die bedarfsabhängige Bearbeitung der Sammlung. Beispiel:

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

Der Anwendungscode könnte die verfügbar gemachte Sammlung immer noch in ein ICollection<T> umwandeln und dann bearbeiten. Wenn dies ein Problem darstellt, könnte die Entität eine defensive Kopie der Sammlung zurückgeben. Beispiel:

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

Überlegen Sie sorgfältig, ob der sich daraus ergebende Nutzen groß genug ist, um den Aufwand aufzuwiegen, der entsteht, wenn bei jedem Zugriff auf die Navigation eine Kopie der Sammlung erstellt wird.

Tipp

Dieses letzte Muster funktioniert, da EF standardmäßig auf die Sammlung über ihr Unterstützungsfeld zugreift. Das bedeutet, dass EF selbst Entitäten aus der eigentlichen Sammlung hinzufügt und entfernt, während Anwendungen nur mit einer defensiven Kopie der Sammlung interagieren.

Initialisierung von Sammlungsnavigationen

Sammlungsnavigationen können vom Entitätstyp initialisiert werden, entweder vorzeitig:

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

Oder verzögert:

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

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

Wenn EF eine Entität zu einer Sammlungsnavigation hinzufügen muss, z. B. während der Ausführung einer Abfrage, dann initialisiert es die Sammlung, wenn sie derzeit null ist. Die erstellte Instanz hängt vom verfügbar gemachten Navigationstyp ab.

  • Wenn die Navigation als HashSet<T> verfügbar gemacht wird, wird eine Instanz von HashSet<T> mit ReferenceEqualityComparer erstellt.
  • Andernfalls wird eine Instanz dieses konkreten Typs erstellt, wenn die Navigation als konkreter Typ mit einem parameterlosen Konstruktor verfügbar gemacht wird. Dies gilt für List<T>, aber auch für andere Sammlungstypen, einschließlich benutzerdefinierter Sammlungstypen.
  • Andernfalls wird eine Instanz von HashSet<T> mithilfe von ReferenceEqualityComparer erstellt, wenn die Navigation als IEnumerable<T>, ICollection<T> oder ISet<T> verfügbar gemacht wird.
  • Andernfalls wird eine Instanz von List<T>erstellt, wenn die Navigation als IList<T> verfügbar gemacht wird.
  • Andernfalls wird eine Ausnahme ausgelöst.

Hinweis

Wenn Benachrichtigungsentitäten, einschließlich Proxys vom Typ change-tracking (Änderungsnachverfolgung), verwendet werden, werden ObservableCollection<T> und ObservableHashSet<T> anstelle von List<T> und HashSet<T> verwendet.

Wichtig

Wie in der Dokumentation zur Änderungsnachverfolgungbeschrieben, verfolgt EF nur eine einzelne Instanz einer Entität mit einem bestimmten Schlüsselwert nach. Dies bedeutet, dass Sammlungen, die als Navigationen verwendet werden, Verweisgleichheitssemantik verwenden müssen. Entitätstypen, die die Objektgleichheit nicht außer Kraft setzen, erhalten dies standardmäßig. Stellen Sie sicher, dass Sie ReferenceEqualityComparer beim Erstellen von HashSet<T> für die Verwendung als Navigation verwenden, um sicherzustellen, dass es für alle Entitätstypen funktioniert.

Konfigurieren von Navigationen

Navigationen werden als Teil der Konfiguration einer Beziehung in das Modell aufgenommen. Das heißt, gemäß Konvention oder mithilfe von HasOne, HasMany usw. in der Modellerstellungs-API. Der Großteil der zu Navigationen zugeordneten Konfiguration erfolgt durch Konfigurieren der Beziehung selbst.

Es gibt jedoch einige Arten von Konfigurationen, die sich auf die Navigationseigenschaften selbst beziehen und nicht Teil der allgemeinen Beziehungskonfiguration sind. Diese Art von Konfiguration erfolgt mit der Navigation-Methode. Wenn Sie z. B. erzwingen möchten, dass EF auf die Navigation über ihre Eigenschaft zugreift, anstatt das Unterstützungsfeld zu verwenden:

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

Hinweis

Der Aufruf Navigation kann nicht verwendet werden, um eine Navigationseigenschaft zu erstellen. Er wird nur verwendet, um eine Navigationseigenschaft zu konfigurieren, die zuvor durch die Definition einer Beziehung oder durch eine Konvention erstellt wurde.

Erforderliche Navigationen

Eine Navigation vom abhängigen zum Prinzipalende ist erforderlich, wenn die Beziehung notwendig ist, was wiederum bedeutet, dass die Fremdschlüsseleigenschaft „Non-Nullable“ ist. Umgekehrt ist die Navigation optional, wenn der Fremdschlüssel Nullwerte zulässt und die Beziehung daher optional ist.

Verweisnavigationen vom Prinzipalende zum abhängigen Ende sind unterschiedlich. In den meisten Fällen kann eine Prinzipalentität immer ohne abhängige Entitäten existieren. Das heißt, eine erforderliche Beziehung bedeutet nicht, dass es immer mindestens eine abhängige Entität geben wird. Es gibt keine Möglichkeit im EF-Modell und auch keine Standardmethode in einer relationalen Datenbank, um sicherzustellen, dass ein Prinzipal einer bestimmten Anzahl von abhängigen Entitäten zugeordnet ist. Wenn dies erforderlich ist, muss es in der Anwendungslogik (Geschäftslogik) implementiert werden.

Es gibt eine Ausnahme von dieser Regel, wenn der Prinzipal- und der abhängige Typ dieselbe Tabelle in einer relationalen Datenbank teilen oder in einem Dokument enthalten sind. Dies kann mit eigenen Typen oder nicht eigenen Typen geschehen, die sich dieselbe Tabelle teilen. In diesem Fall kann die Navigationseigenschaft von der Prinzipal- zur abhängigen Entität als erforderlich markiert werden, was bedeutet, dass die abhängige Entität existieren muss.

Die erforderliche Konfiguration der Navigationseigenschaft erfolgt mithilfe der Methode Navigation. Beispiel:

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