Freigeben über


Identitätsauflösung in EF Core

Ein DbContext kann nur eine Entitätsinstanz mit einem bestimmten Primärschlüsselwert nachverfolgen. Dies bedeutet, dass mehrere Instanzen einer Entität mit demselben Schlüsselwert in eine einzelne Instanz aufgelöst werden müssen. Dies wird als „Identitätsauflösung“ bezeichnet. Die Identitätsauflösung stellt sicher, dass Entity Framework Core (EF Core) einen konsistenten Graph ohne Mehrdeutigkeiten über die Beziehungen oder Eigenschaftswerte der Entitäten nachverfolgt.

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.

Einführung

Der folgende Code fragt eine Entität ab und versucht dann, eine andere Instanz mit demselben Primärschlüsselwert anzufügen:

using var context = new BlogsContext();

var blogA = context.Blogs.Single(e => e.Id == 1);
var blogB = new Blog { Id = 1, Name = ".NET Blog (All new!)" };

try
{
    context.Update(blogB); // This will throw
}
catch (Exception e)
{
    Console.WriteLine($"{e.GetType().FullName}: {e.Message}");
}

Das Ausführen dieses Codes löst die folgende Ausnahme aus:

System.InvalidOperationException: Die Instanz des Entitätstyps „Blog“ kann nicht nachverfolgt werden, da bereits eine andere Instanz mit dem Schlüsselwert „{Id: 1}“ nachverfolgt wird. Stellen Sie beim Anfügen vorhandener Entitäten sicher, dass nur eine Entitätsinstanz mit einem bestimmten Schlüsselwert angefügt ist.

EF Core erfordert aus folgenden Gründen eine einzelne Instanz:

  • Eigenschaftswerte können zwischen mehreren Instanzen unterschiedlich sein. Beim Aktualisieren der Datenbank muss EF Core wissen, welche Eigenschaftswerte verwendet werden sollen.
  • Beziehungen mit anderen Entitäten können zwischen mehreren Instanzen unterschiedlich sein. Beispielsweise kann „blogA“ mit einer anderen Auflistung von Beiträgen als „blogB“ zusammenhängen.

Die obige Ausnahme tritt in den folgenden Situationen häufiger auf:

  • Beim Versuch, eine Entität zu aktualisieren
  • Beim Versuch, einen serialisierten Graph von Entitäten nachzuverfolgen
  • Bei Fehlern beim Festlegen eines Schlüsselwerts, der nicht automatisch generiert wird
  • Beim Wiederverwenden einer DbContext-Instanz für mehrere Arbeitseinheiten

Diese Situationen werden in den folgenden Abschnitten erläutert.

Aktualisieren einer Entität

Es gibt mehrere verschiedene Ansätze zum Aktualisieren einer Entität mit neuen Werten, wie unter Änderungsnachverfolgung in EF Core und Explizites Nachverfolgen von Entitäten behandelt. Diese Ansätze werden im Folgenden im Kontext der Identitätsauflösung beschrieben. Ein wichtiger zu beachtender Punkt ist, dass jeder der Ansätze entweder eine Abfrage oder einen Aufruf von Update oder Attach, aber nie von beiden verwendet.

Aufrufen von Aktualisierungen

Häufig stammt die zu aktualisierende Entität nicht aus einer Abfrage im DbContext, den Sie für SaveChanges verwenden. In einer Webanwendung kann beispielsweise eine Entitätsinstanz anhand der Informationen in einer POST-Anforderung erstellt werden. Die einfachste Möglichkeit hierfür stellt die Verwendung von DbContext.Update oder DbSet<TEntity>.Update dar. Beispiel:

public static void UpdateFromHttpPost1(Blog blog)
{
    using var context = new BlogsContext();

    context.Update(blog);

    context.SaveChanges();
}

In diesem Fall:

  • Es wird nur eine einzelne Instanz der Entität erstellt.
  • Die Entitätsinstanz wird im Rahmen der Aktualisierung nicht aus der Datenbank abgefragt.
  • Alle Eigenschaftswerte werden in der Datenbank aktualisiert, unabhängig davon, ob sie tatsächlich geändert wurden oder nicht.
  • Es wird ein Datenbankroundtrip durchgeführt.

Abfragen und anschließendes Anwenden von Änderungen

Normalerweise ist nicht bekannt, welche Eigenschaftswerte tatsächlich geändert wurden, wenn eine Entität aus Informationen in einer POST-Anforderung oder ähnlichem erstellt wird. Häufig ist es unproblematisch, alle Werte in der Datenbank wie im vorherigen Beispiel zu aktualisieren. Wenn die Anwendung jedoch viele Entitäten verarbeitet und nur eine kleine Anzahl dieser geändert werden, kann es hilfreich sein, die gesendeten Aktualisierungen einzuschränken. Dies kann erreicht werden, indem eine Abfrage ausgeführt wird, um die Entitäten mit ihrem derzeitigen Zustand in der Datenbank nachzuverfolgen und die Änderungen dann auf diese nachverfolgten Entitäten anzuwenden. Beispiel:

public static void UpdateFromHttpPost2(Blog blog)
{
    using var context = new BlogsContext();

    var trackedBlog = context.Blogs.Find(blog.Id);

    trackedBlog.Name = blog.Name;
    trackedBlog.Summary = blog.Summary;

    context.SaveChanges();
}

In diesem Fall:

  • Nur eine einzige Instanz der Entität wird nachverfolgt – und zwar die mit der Find-Abfrage aus der Datenbank zurückgegebene Entität.
  • Update, Attach usw. werden nicht verwendet.
  • Nur Eigenschaftswerte, die tatsächlich geändert wurden, werden in der Datenbank aktualisiert.
  • Es werden zwei Datenbankroundtrips durchgeführt.

EF Core enthält einige Hilfsprogramme zum Übertragen von Eigenschaftswerten wie dieses. PropertyValues.SetValues kopiert z. B. alle Werte aus dem angegebenen Objekt und legt sie im nachverfolgten Objekt fest:

public static void UpdateFromHttpPost3(Blog blog)
{
    using var context = new BlogsContext();

    var trackedBlog = context.Blogs.Find(blog.Id);

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

    context.SaveChanges();
}

SetValues akzeptiert verschiedene Objekttypen, einschließlich Datenübertragungsobjekte (Data Transfer Objects, DTOs) mit Eigenschaftennamen, die den Eigenschaften des Entitätstyps entsprechen. Beispiel:

public static void UpdateFromHttpPost4(BlogDto dto)
{
    using var context = new BlogsContext();

    var trackedBlog = context.Blogs.Find(dto.Id);

    context.Entry(trackedBlog).CurrentValues.SetValues(dto);

    context.SaveChanges();
}

Oder ein Wörterbuch mit Name-Wert-Einträgen für die Eigenschaftswerte:

public static void UpdateFromHttpPost5(Dictionary<string, object> propertyValues)
{
    using var context = new BlogsContext();

    var trackedBlog = context.Blogs.Find(propertyValues["Id"]);

    context.Entry(trackedBlog).CurrentValues.SetValues(propertyValues);

    context.SaveChanges();
}

Weitere Informationen zum Arbeiten mit Eigenschaftswerten finden Sie unter Zugreifen auf nachverfolgte Entitäten.

Verwenden der ursprünglichen Werte

Bisher haben die einzelnen Ansätze entweder eine Abfrage ausgeführt, bevor die Aktualisierung vorgenommen wird, oder alle Eigenschaftswerte aktualisiert, unabhängig davon, ob sie geändert wurden. Um nur Werte zu aktualisieren, die sich im Rahmen der Aktualisierung geändert haben, und ohne vorab eine Abfrage auszuführen, sind bestimmte Informationen dazu erforderlich, welche Eigenschaftswerte geändert wurden. Eine gängige Möglichkeit zum Abrufen dieser Informationen besteht darin, sowohl die aktuellen als auch die ursprünglichen Werte in der HTTP POST-Abfrage oder ähnlichem zurückzusenden. Beispiel:

public static void UpdateFromHttpPost6(Blog blog, Dictionary<string, object> originalValues)
{
    using var context = new BlogsContext();

    context.Attach(blog);
    context.Entry(blog).OriginalValues.SetValues(originalValues);

    context.SaveChanges();
}

In diesem Code wird die Entität mit geänderten Werten zunächst angefügt. Dies bewirkt, dass EF Core die Entität im Zustand Unchanged nachverfolgt, d. h. ohne Eigenschaftswerte, die als geändert markiert wurden. Das Wörterbuch der ursprünglichen Werte wird dann auf diese nachverfolgte Entität angewandt. Dadurch werden geänderte Eigenschaften mit unterschiedlichen aktuellen und ursprünglichen Werten markiert. Eigenschaften, bei denen aktueller Wert und ursprünglicher Wert identisch sind, werden nicht als geändert markiert.

In diesem Fall:

  • Mit Attach wird nur eine einzelne Instanz der Entität nachverfolgt.
  • Die Entitätsinstanz wird im Rahmen der Aktualisierung nicht aus der Datenbank abgefragt.
  • Durch das Anwenden der ursprünglichen Werte wird sichergestellt, dass nur Eigenschaftswerte, die tatsächlich geändert wurden, in der Datenbank aktualisiert werden.
  • Es wird ein Datenbankroundtrip durchgeführt.

Wie bei den Beispielen im vorherigen Abschnitt müssen die ursprünglichen Werte nicht als Wörterbuch übergeben werden. Eine Entitätsinstanz oder ein DTO funktionieren ebenfalls.

Tipp

Dieser Ansatz weist zwar interessante Merkmale auf, erfordert jedoch, dass die ursprünglichen Werte der Entität zum und vom Webclient gesendet werden. Wägen Sie sorgfältig ab, ob die Vorteile diese zusätzliche Komplexität aufwiegen. Für viele Anwendungen ist einer der einfacheren Ansätze sinnvoller.

Anfügen eines serialisierten Graphen

EF Core arbeitet mit Graphen von Entitäten, die über Fremdschlüssel und Navigationseigenschaften verbunden sind, wie unter Ändern von Fremdschlüsseln und Navigationen beschrieben. Wenn diese Diagramme außerhalb von EF Core erstellt werden (z. B. aus einer JSON-Datei), können sie mehrere Instanzen derselben Entität haben. Diese Duplikate müssen in einzelne Instanzen aufgelöst werden, bevor der Graph nachverfolgt werden kann.

Graphen ohne Duplikate

Bevor Sie fortfahren, ist es wichtig, Folgendes zu wissen:

  • Serialisierer verfügen häufig über Optionen für die Behandlung von Schleifen und doppelten Instanzen im Graph.
  • Die Auswahl des Objekts, das als Graphstamm verwendet wird, kann häufig dazu beitragen, Duplikate zu reduzieren oder zu entfernen.

Verwenden Sie nach Möglichkeit Serialisierungsoptionen, und wählen Sie Stämme aus, die nicht zu Duplikaten führen. Der folgende Code verwendet z. B. Json.NET, um eine Liste von Blogs mit den zugehörigen Beiträgen zu serialisieren:

using var context = new BlogsContext();

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

var serialized = JsonConvert.SerializeObject(
    blogs,
    new JsonSerializerSettings { ReferenceLoopHandling = ReferenceLoopHandling.Ignore, Formatting = Formatting.Indented });

Console.WriteLine(serialized);

Der aus diesem Code generierte JSON-Code lautet:

[
  {
    "Id": 1,
    "Name": ".NET Blog",
    "Summary": "Posts about .NET",
    "Posts": [
      {
        "Id": 1,
        "Title": "Announcing the Release of EF Core 5.0",
        "Content": "Announcing the release of EF Core 5.0, a full featured cross-platform...",
        "BlogId": 1
      },
      {
        "Id": 2,
        "Title": "Announcing F# 5",
        "Content": "F# 5 is the latest version of F#, the functional programming language...",
        "BlogId": 1
      }
    ]
  },
  {
    "Id": 2,
    "Name": "Visual Studio Blog",
    "Summary": "Posts about Visual Studio",
    "Posts": [
      {
        "Id": 3,
        "Title": "Disassembly improvements for optimized managed debugging",
        "Content": "If you are focused on squeezing out the last bits of performance for your .NET service or...",
        "BlogId": 2
      },
      {
        "Id": 4,
        "Title": "Database Profiling with Visual Studio",
        "Content": "Examine when database queries were executed and measure how long the take using...",
        "BlogId": 2
      }
    ]
  }
]

Beachten Sie, dass es keine doppelten Blogs oder Beiträge im JSON-Code gibt. Dies bedeutet, dass einfache Aufrufe von Update funktionieren, um diese Entitäten in der Datenbank zu aktualisieren:

public static void UpdateBlogsFromJson(string json)
{
    using var context = new BlogsContext();

    var blogs = JsonConvert.DeserializeObject<List<Blog>>(json);

    foreach (var blog in blogs)
    {
        context.Update(blog);
    }

    context.SaveChanges();
}

Umgang mit Duplikaten

Der Code im vorherigen Beispiel serialisierte jeden Blog mit den zugehörigen Beiträgen. Wenn dies geändert wird, um jeden Beitrag mit dem zugehörigen Blog zu serialisieren, werden im serialisierten JSON-Code Duplikate eingeführt. Beispiel:

using var context = new BlogsContext();

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

var serialized = JsonConvert.SerializeObject(
    posts,
    new JsonSerializerSettings { ReferenceLoopHandling = ReferenceLoopHandling.Ignore, Formatting = Formatting.Indented });

Console.WriteLine(serialized);

Der serialisierte JSON-Code sieht nun wie folgt aus:

[
  {
    "Id": 1,
    "Title": "Announcing the Release of EF Core 5.0",
    "Content": "Announcing the release of EF Core 5.0, a full featured cross-platform...",
    "BlogId": 1,
    "Blog": {
      "Id": 1,
      "Name": ".NET Blog",
      "Summary": "Posts about .NET",
      "Posts": [
        {
          "Id": 2,
          "Title": "Announcing F# 5",
          "Content": "F# 5 is the latest version of F#, the functional programming language...",
          "BlogId": 1
        }
      ]
    }
  },
  {
    "Id": 2,
    "Title": "Announcing F# 5",
    "Content": "F# 5 is the latest version of F#, the functional programming language...",
    "BlogId": 1,
    "Blog": {
      "Id": 1,
      "Name": ".NET Blog",
      "Summary": "Posts about .NET",
      "Posts": [
        {
          "Id": 1,
          "Title": "Announcing the Release of EF Core 5.0",
          "Content": "Announcing the release of EF Core 5.0, a full featured cross-platform...",
          "BlogId": 1
        }
      ]
    }
  },
  {
    "Id": 3,
    "Title": "Disassembly improvements for optimized managed debugging",
    "Content": "If you are focused on squeezing out the last bits of performance for your .NET service or...",
    "BlogId": 2,
    "Blog": {
      "Id": 2,
      "Name": "Visual Studio Blog",
      "Summary": "Posts about Visual Studio",
      "Posts": [
        {
          "Id": 4,
          "Title": "Database Profiling with Visual Studio",
          "Content": "Examine when database queries were executed and measure how long the take using...",
          "BlogId": 2
        }
      ]
    }
  },
  {
    "Id": 4,
    "Title": "Database Profiling with Visual Studio",
    "Content": "Examine when database queries were executed and measure how long the take using...",
    "BlogId": 2,
    "Blog": {
      "Id": 2,
      "Name": "Visual Studio Blog",
      "Summary": "Posts about Visual Studio",
      "Posts": [
        {
          "Id": 3,
          "Title": "Disassembly improvements for optimized managed debugging",
          "Content": "If you are focused on squeezing out the last bits of performance for your .NET service or...",
          "BlogId": 2
        }
      ]
    }
  }
]

Beachten Sie, dass der Graph jetzt mehrere Blog-Instanzen mit demselben Schlüsselwert sowie mehrere Post-Instanzen mit demselben Schlüsselwert enthält. Beim Versuch, diesen Graph wie im vorherigen Beispiel nachzuverfolgen, wird Folgendes ausgelöst:

System.InvalidOperationException: Die Instanz des Entitätstyps „Post“ kann nicht nachverfolgt werden, da bereits eine andere Instanz mit dem Schlüsselwert „{Id: 2}“ nachverfolgt wird. Stellen Sie beim Anfügen vorhandener Entitäten sicher, dass nur eine Entitätsinstanz mit einem bestimmten Schlüsselwert angefügt ist.

Sie können dies auf zwei Arten beheben:

  • Verwenden von JSON-Serialisierungsoptionen, die Verweise beibehalten
  • Ausführen der Identitätsauflösung während der Nachverfolgung des Graphen

Beibehalten von Verweisen

Json.NET bietet die PreserveReferencesHandling-Option, um dies zu behandeln. Beispiel:

var serialized = JsonConvert.SerializeObject(
    posts,
    new JsonSerializerSettings
    {
        PreserveReferencesHandling = PreserveReferencesHandling.All, Formatting = Formatting.Indented
    });

Der resultierende JSON-Code sieht nun wie folgt aus:

{
  "$id": "1",
  "$values": [
    {
      "$id": "2",
      "Id": 1,
      "Title": "Announcing the Release of EF Core 5.0",
      "Content": "Announcing the release of EF Core 5.0, a full featured cross-platform...",
      "BlogId": 1,
      "Blog": {
        "$id": "3",
        "Id": 1,
        "Name": ".NET Blog",
        "Summary": "Posts about .NET",
        "Posts": [
          {
            "$ref": "2"
          },
          {
            "$id": "4",
            "Id": 2,
            "Title": "Announcing F# 5",
            "Content": "F# 5 is the latest version of F#, the functional programming language...",
            "BlogId": 1,
            "Blog": {
              "$ref": "3"
            }
          }
        ]
      }
    },
    {
      "$ref": "4"
    },
    {
      "$id": "5",
      "Id": 3,
      "Title": "Disassembly improvements for optimized managed debugging",
      "Content": "If you are focused on squeezing out the last bits of performance for your .NET service or...",
      "BlogId": 2,
      "Blog": {
        "$id": "6",
        "Id": 2,
        "Name": "Visual Studio Blog",
        "Summary": "Posts about Visual Studio",
        "Posts": [
          {
            "$ref": "5"
          },
          {
            "$id": "7",
            "Id": 4,
            "Title": "Database Profiling with Visual Studio",
            "Content": "Examine when database queries were executed and measure how long the take using...",
            "BlogId": 2,
            "Blog": {
              "$ref": "6"
            }
          }
        ]
      }
    },
    {
      "$ref": "7"
    }
  ]
}

Beachten Sie, dass dieser JSON-Code Duplikate durch Verweise ersetzt hat, z. B. "$ref": "5", der auf die bereits vorhandene Instanz im Graph zeigt. Dieser Graph kann erneut mithilfe der oben gezeigten einfachen Aufrufe von Update nachverfolgt werden.

Die System.Text.Json-Unterstützung in den .NET-Basisklassenbibliotheken (BCL) bietet eine ähnliche Option, zum selben Ergebnis führt. Beispiel:

var serialized = JsonSerializer.Serialize(
    posts, new JsonSerializerOptions { ReferenceHandler = ReferenceHandler.Preserve, WriteIndented = true });

Auflösen von Duplikaten

Wenn es nicht möglich ist, Duplikate im Serialisierungsprozess zu bereinigen, bietet ChangeTracker.TrackGraph hierzu eine Möglichkeit. TrackGraph funktioniert wie Add, Attach und Update mit der Ausnahme, dass für jede Entitätsinstanz ein Rückruf generiert wird, bevor sie nachverfolgt wird. Dieser Rückruf kann verwendet werden, um die Entität nachzuverfolgen oder zu ignorieren. Beispiel:

public static void UpdatePostsFromJsonWithIdentityResolution(string json)
{
    using var context = new BlogsContext();

    var posts = JsonConvert.DeserializeObject<List<Post>>(json);

    foreach (var post in posts)
    {
        context.ChangeTracker.TrackGraph(
            post, node =>
            {
                var keyValue = node.Entry.Property("Id").CurrentValue;
                var entityType = node.Entry.Metadata;

                var existingEntity = node.Entry.Context.ChangeTracker.Entries()
                    .FirstOrDefault(
                        e => Equals(e.Metadata, entityType)
                             && Equals(e.Property("Id").CurrentValue, keyValue));

                if (existingEntity == null)
                {
                    Console.WriteLine($"Tracking {entityType.DisplayName()} entity with key value {keyValue}");

                    node.Entry.State = EntityState.Modified;
                }
                else
                {
                    Console.WriteLine($"Discarding duplicate {entityType.DisplayName()} entity with key value {keyValue}");
                }
            });
    }

    context.SaveChanges();
}

Für jede Entität im Graph führt der Code Folgendes aus:

  • Suchen des Entitätstyps und des Schlüsselwerts der Entität
  • Nachschlagen der Entität mit diesem Schlüssel in der Änderungsnachverfolgung
    • Wenn die Entität gefunden wird, erfolgt keine weitere Aktion, da die Entität ein Duplikat ist.
    • Wenn die Entität nicht gefunden wird, wird sie nachverfolgt, indem ihr Zustand auf Modified festgelegt wird

Die Ausgabe aus der Ausführung dieses Codes lautet:

Tracking EntityType: Post entity with key value 1
Tracking EntityType: Blog entity with key value 1
Tracking EntityType: Post entity with key value 2
Discarding duplicate EntityType: Post entity with key value 2
Tracking EntityType: Post entity with key value 3
Tracking EntityType: Blog entity with key value 2
Tracking EntityType: Post entity with key value 4
Discarding duplicate EntityType: Post entity with key value 4

Wichtig

Dieser Code geht davon aus, dass alle Duplikate identisch sind. Dadurch kann problemlos ein beliebiges Duplikat für die Nachverfolgung ausgewählt und die anderen verworfen werden. Wenn sich die Duplikate unterscheiden können, muss der Code entscheiden, wie ermittelt werden soll, welches Duplikat verwendet werden soll und wie Eigenschafts- und Navigationswerte miteinander kombiniert werden.

Hinweis

Der Einfachheit halber geht dieser Code davon aus, dass jede Entität eine Primärschlüsseleigenschaft mit dem Namen Id hat. Diese könnte in eine abstrakte Basisklasse oder Schnittstelle geschrieben werden. Alternativ kann die Primärschlüsseleigenschaft (oder -eigenschaften) aus den IEntityType-Metadaten abgerufen werden. In diesem Fall würde dieser Code mit jedem Entitätstyp funktionieren.

Fehler beim Festlegen von Schlüsselwerten

Entitätstypen werden häufig so konfiguriert, dass automatisch generierte Schlüsselwerte verwendet werden. Dies ist die Standardeinstellung für Integer- und GUID-Eigenschaften von nicht zusammengesetzten Schlüsseln. Wenn der Entitätstyp jedoch nicht für die Verwendung automatisch generierter Schlüsselwerte konfiguriert ist, muss ein expliziter Schlüsselwert festgelegt werden, bevor die Entität nachverfolgt werden kann. Gehen Sie z. B. von folgendem Entitätstyp aus:

public class Pet
{
    [DatabaseGenerated(DatabaseGeneratedOption.None)]
    public int Id { get; set; }

    public string Name { get; set; }
}

Hier sehen Sie Code, der versucht, zwei neue Entitätsinstanzen nachzuverfolgen, ohne Schlüsselwerte festzulegen:

using var context = new BlogsContext();

context.Add(new Pet { Name = "Smokey" });

try
{
    context.Add(new Pet { Name = "Clippy" }); // This will throw
}
catch (Exception e)
{
    Console.WriteLine($"{e.GetType().FullName}: {e.Message}");
}

Dieser Code löst Folgendes aus:

System.InvalidOperationException: Die Instanz des Entitätstyps „Pet“ kann nicht nachverfolgt werden, da bereits eine andere Instanz mit dem Schlüsselwert „{Id: 0}“ nachverfolgt wird. Stellen Sie beim Anfügen vorhandener Entitäten sicher, dass nur eine Entitätsinstanz mit einem bestimmten Schlüsselwert angefügt ist.

Als Korrektur können Sie die Schlüsselwerte explizit festlegen oder die Schlüsseleigenschaft so konfigurieren, dass generierte Schlüsselwerte verwendet werden. Weitere Informationen finden Sie unter Generierte Werte.

Überlasten einer einzelnen DbContext-Instanz

DbContext ist als kurzlebige Arbeitseinheit konzipiert, wie in Initialisierung und Konfiguration von DbContext beschrieben und in Änderungsnachverfolgung in EF Core genauer erläutert. Wenn Sie diese Leitfäden nicht befolgen, kann es schnell zu Situationen kommen, in denen versucht wird, mehrere Instanzen derselben Entität nachzuverfolgen. Typische Beispiele:

  • Ein solcher Fall wäre die Verwendung der gleichen DbContext-Instanz zum Einrichten des Testzustands und anschließenden Ausführen des Tests. Dies führt häufig dazu, dass DbContext weiterhin eine Entitätsinstanz aus dem Testsetup nachverfolgt und versucht, eine neue Instanz im richtigen Test anzufügen. Verwenden Sie stattdessen verschiedene DbContext-Instanzen zum Einrichten des Testzustands und des Testcodes.
  • Ein anderer Fall ist die Verwendung einer freigegebenen DbContext-Instanz in einem Repository oder ähnlichem Code. Stellen Sie stattdessen sicher, dass Ihr Repository eine einzelne DbContext-Instanz für jede Arbeitseinheit verwendet.

Identitätsauflösung und Abfragen

Die Identitätsauflösung erfolgt automatisch, wenn Entitäten aus einer Abfrage nachverfolgt werden. Wenn also bereits eine Entitätsinstanz mit einem bestimmten Schlüsselwert nachverfolgt wird, bedeutet dies, dass diese vorhandene nachverfolgte Instanz verwendet wird, anstatt eine neue Instanz zu erstellen. Dies hat wichtige Auswirkungen: Wenn die Daten in der Datenbank geändert wurden, wird dies nicht in den Ergebnissen der Abfrage widergespiegelt. Dies ist ein wichtiger Grund, eine neue DbContext-Instanz für jede Arbeitseinheit zu verwenden, wie in Initialisierung und Konfiguration von DbContext beschrieben und in Änderungsnachverfolgung in EF Core genauer erläutert.

Wichtig

Es ist wichtig zu verstehen, dass EF Core immer eine LINQ-Abfrage für ein DbSet in der Datenbank ausführt und Ergebnisse ausschließlich basierend auf dem Inhalt der Datenbank zurückgibt. Wenn jedoch bei einer Nachverfolgungsabfrage die zurückgegebenen Entitäten bereits nachverfolgt werden, werden die nachverfolgten Instanzen verwendet und keine neuen Instanzen aus den Daten in der Datenbank erstellt.

Reload() oder GetDatabaseValues() können verwendet werden, wenn nachverfolgte Entitäten mit den neuesten Daten aus der Datenbank aktualisiert werden müssen. Weitere Informationen finden Sie unter Zugreifen auf nachverfolgte Entitäten.

Im Gegensatz zu Nachverfolgungsabfragen führen andere Abfragen keine Identitätsauflösung durch. Dies bedeutet, dass andere Abfragen als Nachverfolgungsabfragen auch Duplikate wie im zuvor beschriebenen JSON-Serialisierungsfall zurückgeben können. Dies ist in der Regel kein Problem, wenn die Abfrageergebnisse serialisiert und an den Client gesendet werden.

Tipp

Sie sollten die von anderen Abfragen als Nachverfolgungsabfragen zurückgegebenen Entitäten nicht an denselben Kontext anfügen. Diese Vorgehensweise ist zeitaufwendiger und schwieriger umzusetzen als die Verwendung einer Nachverfolgungsabfrage.

Nicht-Nachverfolgungsabfragen führen keine Identitätsauflösung durch, da sich dies auf die Leistung beim Streamen einer großen Anzahl von Entitäten aus einer Abfrage auswirkt. Dies liegt daran, dass für die Identitätsauflösung jede zurückgegebene Instanz nachverfolgt werden muss, damit sie verwendet werden kann, anstatt später ein Duplikat zu erstellen.

Für Nicht-Nachverfolgungsabfragen können Sie mit AsNoTrackingWithIdentityResolution<TEntity>(IQueryable<TEntity>) eine Identitätsauflösung erzwingen. Die Abfrage verfolgt dann die zurückgegebenen Instanzen (ohne sie auf normale Weise nachzuverfolgen) und stellt sicher, dass in den Abfrageergebnissen keine Duplikate erstellt werden.

Außerkraftsetzen der Objektgleichheit

EF Core wendet bei Vergleichen von Entitätsinstanzen Verweisgleichheit an. Dies gilt auch, wenn die Entitätstypen Object.Equals(Object) überschreiben oder die Objektgleichheit anderweitig ändern. Es gibt jedoch einen Fall, in dem sich die Außerkraftsetzung der Gleichheit auf das Verhalten von EF Core auswirken kann: wenn Auflistungsnavigationen die überschriebene Gleichheit anstelle der Verweisgleichheit verwenden und daher mehrere Instanzen als identisch melden.

Aus diesem Grund wird empfohlen, eine Außerkraftsetzung der Entitätsgleichheit zu vermeiden. Wenn sie dennoch angewandt wird, müssen Sie Auflistungsnavigationen erstellen, die Verweisgleichheit erzwingen. Erstellen Sie z. B. einen Gleichheitsvergleich, der die Verweisgleichheit nutzt:

public sealed class ReferenceEqualityComparer : IEqualityComparer<object>
{
    private ReferenceEqualityComparer()
    {
    }

    public static ReferenceEqualityComparer Instance { get; } = new ReferenceEqualityComparer();

    bool IEqualityComparer<object>.Equals(object x, object y) => x == y;

    int IEqualityComparer<object>.GetHashCode(object obj) => RuntimeHelpers.GetHashCode(obj);
}

(Ab .NET 5 ist dies in der BCL als ReferenceEqualityComparer enthalten.)

Dieser Vergleich kann dann beim Erstellen von Auflistungsnavigationen verwendet werden. Beispiel:

public ICollection<Order> Orders { get; set; }
    = new HashSet<Order>(ReferenceEqualityComparer.Instance);

Vergleichen von Schlüsseleigenschaften

Neben Vergleichen auf Gleichheit müssen auch Schlüsselwerte sortiert werden. Dies ist wichtig, um Deadlocks beim Aktualisieren mehrerer Entitäten in einem einzigen Aufruf von SaveChanges zu vermeiden. Alle Typen, die für primäre, alternative oder Fremdschlüsseleigenschaften sowie für eindeutige Indizes verwendet werden, müssen IComparable<T> und IEquatable<T> implementieren. Typen, die normalerweise als Schlüssel (int, GUID, string usw.) verwendet werden, unterstützen diese Schnittstellen bereits. Benutzerdefinierten Schlüsseltypen können diese Schnittstellen hinzugefügt werden.