Zugriff auf nachverfolgte Entitäten

Es gibt vier Haupt-APIs für den Zugriff auf Entitäten, die von einem DbContext nachverfolgt werden:

Alle werden in den nachstehenden Abschnitten ausführlicher erläutert.

Tipp

In diesem Dokument wird davon ausgegangen, dass Entitätszustände und die Grundlagen der EF Core-Änderungsnachverfolgung verstanden werden. Weitere Informationen zu diesen Themen finden Sie unter Änderungsnachverfolgung in EF Core.

Tipp

Sie können den gesamten Code in dieser Dokumentation ausführen und debuggen, indem Sie den Beispielcode von GitHub herunterladen.

Verwenden von DbContext.Entry- und EntityEntry-Instanzen

Für jede nachverfolgte Entität verfolgt Entity Framework Core (EF Core) Folgendes:

  • Den Gesamtstatus der Entität. Dieser ist Unchanged, Modified, Added oder Deleted. Weitere Informationen finden Sie unter Änderungsnachverfolgung in EF Core.
  • Die Beziehungen zwischen nachverfolgten Entitäten. Ein Beispiel wäre der Blog, zu dem ein Beitrag gehört.
  • Die „aktuellen Werte“ von Eigenschaften.
  • Die „ursprünglichen Werte“ von Eigenschaften, sofern diese Informationen verfügbar sind. Die ursprünglichen Werte sind die Eigenschaftswerte, die vorhanden waren, als die Entität aus der Datenbank abgefragt wurde.
  • Welche Eigenschaftswerte seit der Abfrage geändert wurden.
  • Andere Informationen zu Eigenschaftswerten, z. B. ob der Wert temporär ist.

Das Übergeben einer Entitätsinstanz an DbContext.Entry hat als Ergebnis eine EntityEntry<TEntity>, die Zugriff auf diese Informationen für die angegebene Entität ermöglicht. Beispiel:

using var context = new BlogsContext();

var blog = context.Blogs.Single(e => e.Id == 1);
var entityEntry = context.Entry(blog);

In den folgenden Abschnitten wird gezeigt, wie Sie mithilfe eines EntityEntry-Elements auf den Entitätsstatus und den Status der Eigenschaften und Navigationen der Entität zugreifen und diese bearbeiten.

Verwenden von Entitäten

EntityEntry<TEntity> wird am häufigsten dazu verwendet, auf den aktuellen EntityState einer Entität zuzugreifen. Beispiel:

var currentState = context.Entry(blog).State;
if (currentState == EntityState.Unchanged)
{
    context.Entry(blog).State = EntityState.Modified;
}

Die Entry-Methode kann auch für Entitäten verwendet werden, die noch nicht nachverfolgt werden. Dadurch wird nicht die Nachverfolgung der Entität gestartet. Der Status der Entität ist weiterhin Detached. Der zurückgegebene EntityEntry kann dann jedoch verwendet werden, um den Entitätsstatus zu ändern, sodass die Entität im angegebenen Zustand nachverfolgt wird. Der folgende Code startet z. B. die Nachverfolgung einer Bloginstanz als Added:

var newBlog = new Blog();
Debug.Assert(context.Entry(newBlog).State == EntityState.Detached);

context.Entry(newBlog).State = EntityState.Added;
Debug.Assert(context.Entry(newBlog).State == EntityState.Added);

Tipp

Im Gegensatz zu EF6 bewirkt das Festlegen des Status einer einzelnen Entität nicht, dass alle verbundenen Entitäten nachverfolgt werden. Dadurch wird das Festlegen des Zustands zu einem Vorgang auf niedrigerer Ebene als ein Aufruf von Add, Attach oder Update, die für den gesamten Graph von Entitäten ausgeführt werden.

In der folgenden Tabelle werden Möglichkeiten für die Verwendung eines EntityEntry für die Arbeit mit einer gesamten Entität zusammengefasst:

EntityEntry-Member Beschreibung
EntityEntry.State Ruft den EntityState der Entität ab und legt ihn fest.
EntityEntry.Entity Ruft die Entitätsinstanz ab.
EntityEntry.Context Der DbContext für die Nachverfolgung dieser Entität.
EntityEntry.Metadata IEntityType-Metadaten für den Entitätstyp.
EntityEntry.IsKeySet Gibt an, ob für die Entität ein Schlüsselwert festgelegt wurde.
EntityEntry.Reload() Überschreibt Eigenschaftswerte mit Werten, die aus der Datenbank gelesen werden.
EntityEntry.DetectChanges() Erzwingt nur die Erkennung von Änderungen für diese Entität. Weitere Informationen finden Sie unter Änderungserkennung und Benachrichtigungen.

Verwenden einer einzelnen Eigenschaft

Mehrere Überladungen von EntityEntry<TEntity>.Property ermöglichen den Zugriff auf Informationen zu einer einzelnen Eigenschaft einer Entität. Sie können z. B. eine stark typisierte, fluent-ähnliche API verwenden:

PropertyEntry<Blog, string> propertyEntry = context.Entry(blog).Property(e => e.Name);

Der Eigenschaftenname kann stattdessen als Zeichenfolge übergeben werden. Beispiel:

PropertyEntry<Blog, string> propertyEntry = context.Entry(blog).Property<string>("Name");

Die zurückgegebene PropertyEntry<TEntity,TProperty> kann dann verwendet werden, um auf Informationen zur Eigenschaft zuzugreifen. Sie kann beispielsweise verwendet werden, um den aktuellen Wert der Eigenschaft für diese Entität abzurufen und festzulegen:

string currentValue = context.Entry(blog).Property(e => e.Name).CurrentValue;
context.Entry(blog).Property(e => e.Name).CurrentValue = "1unicorn2";

Beide oben verwendeten Property-Methoden geben eine stark typisierte, generische PropertyEntry<TEntity,TProperty>-Instanz zurück. Die Verwendung dieses generischen Typs wird bevorzugt, da er den Zugriff auf Eigenschaftswerte ohne Boxing von Werttypen ermöglicht. Wenn der Typ der Entität oder Eigenschaft zur Kompilierzeit jedoch nicht bekannt ist, kann stattdessen eine nicht generische PropertyEntry abgerufen werden:

PropertyEntry propertyEntry = context.Entry(blog).Property("Name");

Dies ermöglicht den Zugriff auf Eigenschaftsinformationen für jede Eigenschaft unabhängig von deren Typ, aber mit dem Mehraufwand des Boxings von Werttypen. Beispiel:

object blog = context.Blogs.Single(e => e.Id == 1);

object currentValue = context.Entry(blog).Property("Name").CurrentValue;
context.Entry(blog).Property("Name").CurrentValue = "1unicorn2";

In der folgenden Tabelle sind Eigenschafteninformationen zusammengefasst, die von PropertyEntry verfügbar gemacht werden:

PropertyEntry-Member Beschreibung
PropertyEntry<TEntity,TProperty>.CurrentValue Ruft den aktuellen Wert der Eigenschaft ab und legt ihn fest.
PropertyEntry<TEntity,TProperty>.OriginalValue Ruft den ursprünglichen Wert der Eigenschaft ab (sofern vorhanden) und legt ihn fest.
PropertyEntry<TEntity,TProperty>.EntityEntry Ein Rückverweis auf die EntityEntry<TEntity> der Entität.
PropertyEntry.Metadata IProperty-Metadaten für die Eigenschaft.
PropertyEntry.IsModified Gibt an, ob diese Eigenschaft als geändert markiert ist, und ermöglicht das Ändern dieses Zustands.
PropertyEntry.IsTemporary Gibt an, ob diese Eigenschaft als temporär markiert ist, und ermöglicht das Ändern dieses Zustands.

Hinweise:

  • Der ursprüngliche Wert einer Eigenschaft ist der Wert, den die Eigenschaft hatte, als die Entität aus der Datenbank abgefragt wurde. Ursprüngliche Werte sind jedoch nicht verfügbar, wenn die Entität getrennt und dann explizit an einen anderen DbContext angefügt wurde, z. B. mit Attach oder Update. In diesem Fall entspricht der zurückgegebene ursprüngliche Wert dem aktuellen Wert.
  • SaveChanges aktualisiert nur Eigenschaften, die als geändert markiert sind. Legen Sie IsModified auf TRUE fest, um zu erzwingen, dass EF Core einen bestimmten Eigenschaftswert aktualisiert, oder legen Sie FALSE fest, um zu verhindern, dass EF Core den Eigenschaftswert aktualisiert.
  • Temporäre Werte werden in der Regel von EF Core-Wertgeneratoren generiert. Durch Festlegen des aktuellen Werts einer Eigenschaft wird der temporäre Wert durch den angegebenen Wert ersetzt und die Eigenschaft als nicht temporär markiert. Legen Sie IsTemporary auf TRUE fest, um zu erzwingen, dass ein Wert auch nach dem expliziten Festlegen temporär ist.

Verwenden einer einzelnen Navigation

Mehrere Überladungen von EntityEntry<TEntity>.Reference, EntityEntry<TEntity>.Collection und EntityEntry.Navigation ermöglichen den Zugriff auf Informationen zu einer einzelnen Navigation.

Auf Referenznavigationen zu einer einzelnen verwandten Entität wird über die Reference-Methoden zugegriffen. Referenznavigationen zeigen auf die 1-Seiten von 1:n-Beziehungen und beide Seiten von 1:1-Beziehungen. Beispiel:

ReferenceEntry<Post, Blog> referenceEntry1 = context.Entry(post).Reference(e => e.Blog);
ReferenceEntry<Post, Blog> referenceEntry2 = context.Entry(post).Reference<Blog>("Blog");
ReferenceEntry referenceEntry3 = context.Entry(post).Reference("Blog");

Navigationen können auch Auflistungen verwandter Entitäten sein, wenn sie für die n-Seiten von 1:n- und m:n-Beziehungen verwendet werden. Die Collection-Methoden werden für den Zugriff auf Auflistungsnavigationen verwendet. Beispiel:

CollectionEntry<Blog, Post> collectionEntry1 = context.Entry(blog).Collection(e => e.Posts);
CollectionEntry<Blog, Post> collectionEntry2 = context.Entry(blog).Collection<Post>("Posts");
CollectionEntry collectionEntry3 = context.Entry(blog).Collection("Posts");

Einige Vorgänge sind für alle Navigationen gleich. Auf diese kann sowohl für Referenz- als auch Auflistungsnavigationen mithilfe der EntityEntry.Navigation-Methode zugegriffen werden. Beachten Sie, dass beim gemeinsamen Zugriff auf alle Navigationen nur nicht generischer Zugriff verfügbar ist. Beispiel:

NavigationEntry navigationEntry = context.Entry(blog).Navigation("Posts");

In der folgenden Tabelle sind die Möglichkeiten für die Verwendung von ReferenceEntry<TEntity,TProperty>, CollectionEntry<TEntity,TRelatedEntity> und NavigationEntry aufgeführt:

NavigationEntry-Member Beschreibung
MemberEntry.CurrentValue Ruft den aktuellen Wert der Navigation ab und legt ihn fest. Dies ist die gesamte Auflistung für Auflistungsnavigationen.
NavigationEntry.Metadata INavigationBase-Metadaten für die Navigation.
NavigationEntry.IsLoaded Ruft einen Wert ab, der angibt, ob die zugehörige Entität oder Auflistung vollständig aus der Datenbank geladen wurde, oder legt ihn fest.
NavigationEntry.Load() Lädt die verwandte Entität oder Auflistung aus der Datenbank ab. Weitere Informationen finden Sie unter Explizites Laden verwandter Daten.
NavigationEntry.Query() Die Abfrage, die EF Core verwendet, um diese Navigation als eine IQueryable zu laden, die dann weiter zusammengesetzt werden kann. Weitere Informationen finden Sie unter Explizites Laden verwandter Daten.

Verwenden aller Eigenschaften einer Entität

EntityEntry.Properties gibt eine IEnumerable<T> der PropertyEntry für jede Eigenschaft der Entität zurück. Damit können Sie eine Aktion für jede Eigenschaft der Entität ausführen. Sie können beispielsweise eine beliebige DateTime-Eigenschaft auf DateTime.Now festlegen:

foreach (var propertyEntry in context.Entry(blog).Properties)
{
    if (propertyEntry.Metadata.ClrType == typeof(DateTime))
    {
        propertyEntry.CurrentValue = DateTime.Now;
    }
}

Darüber hinaus enthält EntityEntry mehrere Methoden, um alle Eigenschaftswerte gleichzeitig abzurufen und festzulegen. Diese Methoden verwenden die PropertyValues-Klasse, die eine Auflistung von Eigenschaften und deren Werten darstellt. PropertyValues können für aktuelle oder ursprüngliche Werte oder für die Werte abgerufen werden, die aktuell in der Datenbank gespeichert sind. Beispiel:

var currentValues = context.Entry(blog).CurrentValues;
var originalValues = context.Entry(blog).OriginalValues;
var databaseValues = context.Entry(blog).GetDatabaseValues();

Diese PropertyValues-Objekte sind selbst nicht sehr nützlich. Sie können jedoch kombiniert werden, um gängige Vorgänge auszuführen, die beim Bearbeiten von Entitäten erforderlich sind. Dies ist hilfreich beim Arbeiten mit Datenübertragungsobjekten und beim Auflösen optimistischer Nebenläufigkeitskonflikte. Die folgenden Abschnitte enthalten einige Beispiele.

Festlegen aktueller oder ursprünglicher Werte aus einer Entität oder einem DTO

Die aktuellen oder ursprünglichen Werte einer Entität können aktualisiert werden, indem Werte aus einem anderen Objekt kopiert werden. Betrachten Sie z. B. ein BlogDto-DTO (Data Transfer Object, Datenübertragungsobjekt) mit denselben Eigenschaften wie der Entitätstyp:

public class BlogDto
{
    public int Id { get; set; }
    public string Name { get; set; }
}

Auf diese Weise können Sie die aktuellen Werte einer nachverfolgten Entität mithilfe von PropertyValues.SetValues festlegen:

var blogDto = new BlogDto { Id = 1, Name = "1unicorn2" };

context.Entry(blog).CurrentValues.SetValues(blogDto);

Diese Technik wird manchmal verwendet, wenn eine Entität mit Werten aktualisiert wird, die über einen Dienstaufruf oder von einem Client in einer n-schichtigen Anwendung abgerufen wurden. Beachten Sie, dass das verwendete Objekt nicht denselben Typ wie die Entität aufweisen muss, solange es Eigenschaften aufweist, deren Namen mit denen der Entität übereinstimmen. Im obigen Beispiel wird eine Instanz des BlogDto-DTO verwendet, um die aktuellen Werte einer nachverfolgten Blog-Entität festzulegen.

Beachten Sie, dass Eigenschaften nur dann als geändert markiert werden, wenn sich der festgelegte Wert vom aktuellen Wert unterscheidet.

Festlegen aktueller oder ursprünglicher Werte aus einem Wörterbuch

Im vorherigen Beispiel werden Werte aus einer Entität oder DTO-Instanz festgelegt. Dasselbe ist auch möglich, wenn Eigenschaftswerte als Name-Wert-Paare in einem Wörterbuch gespeichert sind. Beispiel:

var blogDictionary = new Dictionary<string, object> { ["Id"] = 1, ["Name"] = "1unicorn2" };

context.Entry(blog).CurrentValues.SetValues(blogDictionary);

Festlegen aktueller oder ursprünglicher Werte aus der Datenbank

Die aktuellen oder ursprünglichen Werte einer Entität können mit den neuesten Werten aus der Datenbank aktualisiert werden. Rufen Sie dazu GetDatabaseValues() oder GetDatabaseValuesAsync auf, und verwenden Sie das zurückgegebene Objekt, um aktuelle und/oder ursprüngliche Werte festzulegen. Beispiel:

var databaseValues = context.Entry(blog).GetDatabaseValues();
context.Entry(blog).CurrentValues.SetValues(databaseValues);
context.Entry(blog).OriginalValues.SetValues(databaseValues);

Erstellen eines geklonten Objekts mit aktuellen, ursprünglichen oder Datenbankwerten

Das PropertyValues-Objekt, das von CurrentValues, OriginalValues oder GetDatabaseValues zurückgegeben wird, kann zum Erstellen eines Klons der Entität mit PropertyValues.ToObject() verwendet werden. Beispiel:

var clonedBlog = context.Entry(blog).GetDatabaseValues().ToObject();

Beachten Sie, dass ToObject eine neue Instanz zurückgibt, die nicht vom DbContext nachverfolgt wird. Das zurückgegebene Objekt verfügt auch nicht über Beziehungen, die auf andere Entitäten festgelegt sind.

Das geklonte Objekt kann nützlich sein, um Probleme im Zusammenhang mit parallelen Aktualisierungen der Datenbank zu beheben, insbesondere bei der Datenbindung an Objekte eines bestimmten Typs. Weitere Informationen finden Sie unter optimistische Nebenläufigkeit.

Verwenden aller Navigationen einer Entität

EntityEntry.Navigations gibt eine IEnumerable<T> von NavigationEntry für jede Navigation der Entität zurück. EntityEntry.References und EntityEntry.Collections führen Sie dasselbe aus, aber beschränkt auf Referenz- oder Auflistungsnavigationen. Damit können Sie eine Aktion für jede Navigation der Entität ausführen. Sie können beispielsweise das Laden aller verwandten Entitäten erzwingen:

foreach (var navigationEntry in context.Entry(blog).Navigations)
{
    navigationEntry.Load();
}

Verwenden aller Member einer Entität

Reguläre Eigenschaften und Navigationseigenschaften weisen unterschiedliche Zustände und Verhalten auf. Es ist daher üblich, Navigationen und Nicht-Navigationen separat zu verarbeiten, wie in den Abschnitten oben gezeigt. Manchmal kann es jedoch hilfreich sein, Aktionen mit einem beliebigen Member der Entität auszuführen, unabhängig davon, ob es sich um eine normale Eigenschaft oder eine Navigation handelt. Zu diesem Zweck stehen EntityEntry.Member und EntityEntry.Members zur Verfügung. Beispiel:

foreach (var memberEntry in context.Entry(blog).Members)
{
    Console.WriteLine(
        $"Member {memberEntry.Metadata.Name} is of type {memberEntry.Metadata.ClrType.ShortDisplayName()} and has value {memberEntry.CurrentValue}");
}

Wenn Sie diesen Code in einem Blog aus dem Beispiel ausführen, wird die folgende Ausgabe generiert:

Member Id is of type int and has value 1
Member Name is of type string and has value .NET Blog
Member Posts is of type IList<Post> and has value System.Collections.Generic.List`1[Post]

Tipp

Die Debugansicht der Änderungsnachverfolgung zeigt Informationen wie die folgenden an. Die Debugansicht für die gesamte Änderungsnachverfolgung wird aus der einzelnen EntityEntry.DebugView der verschiedenen nachverfolgten Entitäten generiert.

Find und FindAsync

DbContext.Find, DbContext.FindAsync, DbSet<TEntity>.Find und DbSet<TEntity>.FindAsync sind für eine effiziente Suche einer einzelnen Entität konzipiert, wenn deren Primärschlüssel bekannt ist. Find überprüft zunächst, ob die Entität bereits nachverfolgt wird, und gibt in diesem Fall die Entität sofort zurück. Eine Datenbankabfrage erfolgt nur, wenn die Entität nicht lokal nachverfolgt wird. Betrachten Sie beispielsweise folgenden Code, der Find zweimal für dieselbe Entität aufruft:

using var context = new BlogsContext();

Console.WriteLine("First call to Find...");
var blog1 = context.Blogs.Find(1);

Console.WriteLine($"...found blog {blog1.Name}");

Console.WriteLine();
Console.WriteLine("Second call to Find...");
var blog2 = context.Blogs.Find(1);
Debug.Assert(blog1 == blog2);

Console.WriteLine("...returned the same instance without executing a query.");

Die Ausgabe dieses Codes (einschließlich EF Core-Protokollierung) lautet bei Verwendung von SQLite:

First call to Find...
info: 12/29/2020 07:45:53.682 RelationalEventId.CommandExecuted[20101] (Microsoft.EntityFrameworkCore.Database.Command)
      Executed DbCommand (1ms) [Parameters=[@__p_0='1' (DbType = String)], CommandType='Text', CommandTimeout='30']
      SELECT "b"."Id", "b"."Name"
      FROM "Blogs" AS "b"
      WHERE "b"."Id" = @__p_0
      LIMIT 1
...found blog .NET Blog

Second call to Find...
...returned the same instance without executing a query.

Beachten Sie, dass der erste Aufruf die Entität nicht lokal findet und daher eine Datenbankabfrage ausführt. Umgekehrt gibt der zweite Aufruf dieselbe Instanz zurück, ohne die Datenbank abfragen zu müssen, da sie bereits nachverfolgt wird.

Find gibt NULL zurück, wenn eine Entität mit dem angegebenen Schlüssel nicht lokal nachverfolgt wird und in der Datenbank nicht vorhanden ist.

Zusammengesetzte Schlüssel

Find kann auch mit zusammengesetzten Schlüsseln verwendet werden. Betrachten Sie beispielsweise eine OrderLine-Entität mit einem zusammengesetzten Schlüssel, der aus der Bestell-ID und der Produkt-ID besteht:

public class OrderLine
{
    public int OrderId { get; set; }
    public int ProductId { get; set; }

    //...
}

Der zusammengesetzte Schlüssel muss in DbContext.OnModelCreating so konfiguriert werden, dass die Schlüsselteile und deren Reihenfolge definiert werden. Beispiel:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder
        .Entity<OrderLine>()
        .HasKey(e => new { e.OrderId, e.ProductId });
}

Beachten Sie, dass OrderId der erste Teil des Schlüssels ist und ProductId der zweite Teil. Diese Reihenfolge muss beim Übergeben von Schlüsselwerten an Find verwendet werden. Beispiel:

var orderline = context.OrderLines.Find(orderId, productId);

Verwenden von ChangeTracker.Entries für den Zugriff auf alle nachverfolgten Entitäten

Bisher haben Sie jeweils nur auf einen einzelnen EntityEntry zugegriffen. ChangeTracker.Entries() gibt einen EntityEntry für jede Entität zurück, die derzeit vom DbContext nachverfolgt wird. Beispiel:

using var context = new BlogsContext();
var blogs = context.Blogs.Include(e => e.Posts).ToList();

foreach (var entityEntry in context.ChangeTracker.Entries())
{
    Console.WriteLine($"Found {entityEntry.Metadata.Name} entity with ID {entityEntry.Property("Id").CurrentValue}");
}

Dieser Code generiert die folgende Ausgabe:

Found Blog entity with ID 1
Found Post entity with ID 1
Found Post entity with ID 2

Beachten Sie, dass Einträge für Blogs und Beiträge zurückgegeben werden. Die Ergebnisse können stattdessen auch mithilfe der generischen Überladung von ChangeTracker.Entries<TEntity>() nach einem bestimmten Entitätstyp gefiltert werden:

foreach (var entityEntry in context.ChangeTracker.Entries<Post>())
{
    Console.WriteLine(
        $"Found {entityEntry.Metadata.Name} entity with ID {entityEntry.Property(e => e.Id).CurrentValue}");
}

Die Ausgabe dieses Codes zeigt, dass nur Beiträge zurückgegeben werden:

Found Post entity with ID 1
Found Post entity with ID 2

Außerdem werden bei Verwendung der generischen Überladung auch generische EntityEntry<TEntity>-Instanzen zurückgegeben. Dies ermöglicht den fluent-ähnlichen Zugriff auf die Id-Eigenschaft in diesem Beispiel.

Der generische Typ, der für die Filterung verwendet wird, muss kein zugeordneter Entitätstyp sein. Es kann stattdessen auch ein nicht zugeordneter Basistyp oder eine nicht zugeordnete Schnittstelle verwendet werden. Angenommen, alle Entitätstypen im Modell implementieren eine Schnittstelle, die ihre Schlüsseleigenschaft definiert:

public interface IEntityWithKey
{
    int Id { get; set; }
}

Diese Schnittstelle kann verwendet werden, um mit dem Schlüssel jeder nachverfolgten Entität auf stark typisierte Weise zu arbeiten. Beispiel:

foreach (var entityEntry in context.ChangeTracker.Entries<IEntityWithKey>())
{
    Console.WriteLine(
        $"Found {entityEntry.Metadata.Name} entity with ID {entityEntry.Property(e => e.Id).CurrentValue}");
}

Verwenden von DbSet.Local zum Abfragen nachverfolgter Entitäten

EF Core-Abfragen werden immer in der Datenbank ausgeführt.Sie geben nur Entitäten zurück, die in der Datenbank gespeichert wurden. DbSet<TEntity>.Local stellt einen Mechanismus zum Abfragen des DbContext für lokale, nachverfolgte Entitäten bereit.

Da DbSet.Local zum Abfragen nachverfolgter Entitäten verwendet wird, ist es üblich, Entitäten in den DbContext zu laden und dann mit diesen geladenen Entitäten zu arbeiten. Dies gilt insbesondere für die Datenbindung, ist aber auch in anderen Situationen nützlich. Im folgenden Code wird die Datenbank beispielsweise zuerst für alle Blogs und Beiträge abgefragt. Die Erweiterungsmethode Load wird verwendet, um diese Abfrage mit den Ergebnissen auszuführen, die vom Kontext nachverfolgt werden, ohne direkt an die Anwendung zurückgegeben zu werden. (Die Verwendung von ToList oder ähnlichen Methoden hat den gleichen Effekt, aber mit dem Mehraufwand für das Erstellen der zurückgegebenen Liste, die hier nicht benötigt wird.) Anschließend wird im Beispiel DbSet.Local verwendet, um auf die lokal nachverfolgten Entitäten zuzugreifen:

using var context = new BlogsContext();

context.Blogs.Include(e => e.Posts).Load();

foreach (var blog in context.Blogs.Local)
{
    Console.WriteLine($"Blog: {blog.Name}");
}

foreach (var post in context.Posts.Local)
{
    Console.WriteLine($"Post: {post.Title}");
}

Beachten Sie, dass DbSet.Local im Gegensatz zu ChangeTracker.Entries() Entitätsinstanzen direkt zurückgibt. Natürlich kann immer durch Aufrufen von DbContext.Entry ein EntityEntry für die zurückgegebene Entität abgerufen werden.

Die lokale Ansicht

DbSet<TEntity>.Local gibt eine Ansicht der lokal nachverfolgten Entitäten mit den aktuellen EntityState dieser Entitäten zurück. Dies bedeutet insbesondere Folgendes:

  • Added-Entitäten sind enthalten. Beachten Sie, dass dies bei normalen EF Core-Abfragen nicht der Fall ist, da Added-Entitäten noch nicht in der Datenbank vorhanden sind und daher nie von einer Datenbankabfrage zurückgegeben werden.
  • Deleted-Entitäten sind ausgeschlossen. Beachten Sie, dass dies bei normalen EF Core-Abfragen nicht der Fall ist, da Deleted-Entitäten weiterhin in der Datenbank vorhanden sind und daher bei Datenbankabfragen zurückgegeben werden.

All dies bedeutet, dass DbSet.Local eine Ansicht der Daten bietet, die den aktuellen konzeptionellen Zustand des Entitätsgraphen widerspiegelt, wobei Added-Entitäten eingeschlossen und Deleted-Entitäten ausgeschlossen sind. Dies stimmt mit dem Datenbankstatus überein, der nach dem Aufruf von SaveChanges erwartet wird.

Dies ist in der Regel die ideale Ansicht für die Datenbindung, da die Daten Benutzer*innen basierend auf den von der Anwendung vorgenommenen Änderungen angezeigt werden.

Der folgende Code veranschaulicht dies durch Markieren eines Beitrags als Deleted und das anschließende Hinzufügen eines neuen Beitrags, der als Added markiert ist:

using var context = new BlogsContext();

var posts = context.Posts.Include(e => e.Blog).ToList();

Console.WriteLine("Local view after loading posts:");

foreach (var post in context.Posts.Local)
{
    Console.WriteLine($"  Post: {post.Title}");
}

context.Remove(posts[1]);

context.Add(
    new Post
    {
        Title = "What’s next for System.Text.Json?",
        Content = ".NET 5.0 was released recently and has come with many...",
        Blog = posts[0].Blog
    });

Console.WriteLine("Local view after adding and deleting posts:");

foreach (var post in context.Posts.Local)
{
    Console.WriteLine($"  Post: {post.Title}");
}

Die Ausgabe dieses Codes lautet:

Local view after loading posts:
  Post: Announcing the Release of EF Core 5.0
  Post: Announcing F# 5
  Post: Announcing .NET 5.0
Local view after adding and deleting posts:
  Post: What’s next for System.Text.Json?
  Post: Announcing the Release of EF Core 5.0
  Post: Announcing .NET 5.0

Beachten Sie, dass der gelöschte Beitrag aus der lokalen Ansicht entfernt wird und der hinzugefügte Beitrag enthalten ist.

Verwenden von „Local“ zum Hinzufügen und Entfernen von Entitäten

DbSet<TEntity>.Local gibt eine Instanz von LocalView<TEntity> zurück. Dies ist eine Implementierung von ICollection<T>, die Benachrichtigungen generiert, wenn Entitäten hinzugefügt und aus der Auflistung entfernt werden, und darauf reagiert. (Das Konzept ist mit ObservableCollection<T> identisch, aber eine Projektion über vorhandene EF Core-Änderungsnachverfolgungseinträge und nicht als unabhängige Auflistung implementiert.)

Die Benachrichtigungen der lokalen Ansicht werden in die DbContext-Änderungsnachverfolgung eingebunden, sodass die lokale Ansicht mit dem DbContext synchron bleibt. Speziell:

  • Das Hinzufügen einer neuen Entität zu DbSet.Local bewirkt, dass sie vom DbContext nachverfolgt wird, in der Regel mit dem Zustand Added. (Wenn die Entität bereits über einen generierten Schlüsselwert verfügt, wird sie stattdessen als Unchanged nachverfolgt.)
  • Wenn Sie eine Entität aus DbSet.Local entfernen, wird sie als Deleted gekennzeichnet.
  • Eine Entität, die vom DbContext nachverfolgt wird, ist automatisch in der DbSet.Local-Auflistung enthalten. Wenn Sie beispielsweise eine Abfrage ausführen, um weitere Entitäten einzubringen, wird die lokale Ansicht automatisch aktualisiert.
  • Eine Entität, die als Deleted gekennzeichnet ist, wird automatisch aus der lokalen Auflistung entfernt.

Dies bedeutet, dass die lokale Ansicht verwendet werden kann, um nachverfolgte Entitäten einfach durch Hinzufügen und Entfernen aus der Auflistung zu bearbeiten. Sie können beispielsweise den vorherigen Beispielcode ändern, um Beiträge aus der lokalen Auflistung hinzuzufügen und daraus zu entfernen:

using var context = new BlogsContext();

var posts = context.Posts.Include(e => e.Blog).ToList();

Console.WriteLine("Local view after loading posts:");

foreach (var post in context.Posts.Local)
{
    Console.WriteLine($"  Post: {post.Title}");
}

context.Posts.Local.Remove(posts[1]);

context.Posts.Local.Add(
    new Post
    {
        Title = "What’s next for System.Text.Json?",
        Content = ".NET 5.0 was released recently and has come with many...",
        Blog = posts[0].Blog
    });

Console.WriteLine("Local view after adding and deleting posts:");

foreach (var post in context.Posts.Local)
{
    Console.WriteLine($"  Post: {post.Title}");
}

Die Ausgabe bleibt gegenüber dem vorherigen Beispiel unverändert, da an der lokalen Ansicht vorgenommene Änderungen mit dem DbContext synchronisiert werden.

Verwenden der lokalen Ansicht für die Windows Forms- oder WPF-Datenbindung

DbSet<TEntity>.Local bildet die Grundlage für die Datenbindung an EF Core-Entitäten. Sowohl Windows Forms als auch WPF funktionieren jedoch am besten, wenn sie mit dem spezifischen Typ der Benachrichtigungsauflistung verwendet werden, die sie erwarten. Die lokale Ansicht unterstützt das Erstellen dieser spezifischen Auflistungstypen:

Beispiel:

ObservableCollection<Post> observableCollection = context.Posts.Local.ToObservableCollection();
BindingList<Post> bindingList = context.Posts.Local.ToBindingList();

Weitere Informationen zur WPF-Datenbindung mit EF Core finden Sie unter Erste Schritte mit WPF, und unter Erste Schritte mit Windows Forms finden Sie weitere Informationen zur Windows Forms-Datenbindung mit EF Core.

Tipp

Die lokale Ansicht für eine bestimmte DbSet-Instanz wird beim ersten Zugriff verzögert erstellt und anschließend zwischengespeichert. Die Erstellung von LocalView selbst ist schnell und verbraucht nur unerheblichen Arbeitsspeicher. Sie ruft jedoch DetectChanges auf, was bei einer großen Anzahl von Entitäten langsam sein kann. Die mit ToObservableCollection und ToBindingList erstellten Auflistungen werden ebenfalls verzögert erstellt und dann zwischengespeichert. Beide Methoden erstellen neue Auflistungen, die langsam sein können und viel Arbeitsspeicher verbrauchen können, wenn Tausende von Entitäten beteiligt sind.