Wertvergleiche

Tipp

Den Code in diesem Dokument finden Sie auf GitHub als ausführbares Beispiel.

Hintergrund

Änderungsnachverfolgung bedeutet, dass EF Core automatisch bestimmt, welche Änderungen von der Anwendung an einer geladenen Entitätsinstanz ausgeführt wurden, sodass diese Änderungen beim Aufrufen von SaveChanges wieder in der Datenbank gespeichert werden können. EF Core führt dies in der Regel durch Erstellen einer Momentaufnahme der Instanz aus, wenn sie aus der Datenbank geladen wird, und vergleicht diese Momentaufnahme mit der an die Anwendung übergebenen Instanz.

EF Core verfügt über integrierte Logik zum Erstellen von Momentaufnahmen und Vergleichen der meisten in Datenbanken verwendeten Standardtypen, sodass Benutzer*innen sich in der Regel über dieses Thema keine Gedanken machen müssen. Wenn eine Eigenschaft jedoch über einen Wertkonverterzugeordnet wird, muss EF Core einen Vergleich für beliebige Benutzertypen durchführen, der möglicherweise komplex sein kann. Standardmäßig verwendet EF Core den standardmäßigen, durch Typen definierten Gleichheitsvergleich (z. B. die Equals-Methode); für das Erstellen von Momentaufnahmen werden Werttypen kopiert, um die Momentaufnahme zu erzeugen, während für Verweistypen kein Kopieren ausgeführt und dieselbe Instanz wie für die Momentaufnahme verwendet wird.

In Fällen, in denen das integrierte Vergleichsverhalten nicht geeignet ist, können Benutzer*innen einen Wertvergleich bereitstellen, der Logik für das Erstellen von Momentaufnahmen, das Vergleichen und Berechnen eines Hashcodes enthält. Im Folgenden wird beispielsweise die Wertkonvertierung für die List<int>-Eigenschaft eingerichtet, für die eine Wertkonvertierung in eine JSON-Zeichenfolge in der Datenbank durchgeführt werden soll, und außerdem ein geeigneter Wertvergleich definiert:

modelBuilder
    .Entity<EntityType>()
    .Property(e => e.MyListProperty)
    .HasConversion(
        v => JsonSerializer.Serialize(v, (JsonSerializerOptions)null),
        v => JsonSerializer.Deserialize<List<int>>(v, (JsonSerializerOptions)null),
        new ValueComparer<List<int>>(
            (c1, c2) => c1.SequenceEqual(c2),
            c => c.Aggregate(0, (a, v) => HashCode.Combine(a, v.GetHashCode())),
            c => c.ToList()));

Weitere Informationen finden Sie unter veränderliche Klassen weiter unten.

Beachten Sie, dass Wertvergleiche auch verwendet werden, wenn ermittelt wird, ob zwei Schlüsselwerte beim Auflösen von Beziehungen identisch sind; dies wird unten erläutert.

Flacher Vergleich und tiefer Vergleich

Bei kleinen, unveränderlichen Werttypen wie int funktioniert die Standardlogik von EF Core gut: Der Wert wird bei Momentaufnahmen kopiert und mit dem integrierten Gleichheitsvergleich des Typs verglichen. Bei der Implementierung Ihres eigenen Wertvergleichs müssen Sie unbedingt berücksichtigen, ob eine tiefe oder flache Logik für den Vergleich (und das Erstellen von Momentaufnahmen) geeignet ist.

Berücksichtigen Sie Bytearrays, die beliebig groß sein können. Diese könnten verglichen werden:

  • Anhand eines Verweises, sodass nur dann ein Unterschied erkannt wird, wenn ein neues Bytearray verwendet wird.
  • Im tiefen Vergleich, sodass diese Mutation der Bytes im Array erkannt wird.

Standardmäßig verwendet EF Core die ersten dieser Ansätze für Nicht-Schlüsselbyte-Arrays. Das heißt, nur Verweise werden verglichen, und eine Änderung wird nur erkannt, wenn ein vorhandenes Bytearray durch ein neues ersetzt wird. Dies ist eine pragmatische Entscheidung, die das Kopieren ganzer Arrays und deren Byte-zu-Byte-Vergleichen beim Ausführen von SaveChanges vermeidet. Dies bedeutet, dass das häufige Szenario des Ersetzens eines Images durch ein anderes effizient behandelt wird.

Auf der anderen Seite würde die Verweisgleichheit nicht funktionieren, wenn Bytearrays verwendet werden, um binäre Schlüssel darzustellen, da es sehr unwahrscheinlich ist, dass eine FK-Eigenschaft auf die gleiche Instanz als PK-Eigenschaft festgelegt wird, mit der sie verglichen werden muss. Daher verwendet EF Core tiefe Vergleiche für Byte-Arrays, die als Schlüssel fungieren; dies wird wahrscheinlich keine großen Leistungseinbußen mit sich bringen, da binäre Schlüssel in der Regel kurz sind.

Beachten Sie, dass die ausgewählte Vergleichs- und Momentaufnahmenlogik einander entsprechen müssen: Ein tiefer Vergleich erfordert tiefe Momentaufnahmen, um ordnungsgemäß zu funktionieren.

Einfache unveränderliche Klassen

Betrachten Sie eine Eigenschaft, die einen Wertkonverter verwendet, um eine einfache, unveränderliche Klasse zuzuordnen.

public sealed class ImmutableClass
{
    public ImmutableClass(int value)
    {
        Value = value;
    }

    public int Value { get; }

    private bool Equals(ImmutableClass other)
        => Value == other.Value;

    public override bool Equals(object obj)
        => ReferenceEquals(this, obj) || obj is ImmutableClass other && Equals(other);

    public override int GetHashCode()
        => Value.GetHashCode();
}
modelBuilder
    .Entity<MyEntityType>()
    .Property(e => e.MyProperty)
    .HasConversion(
        v => v.Value,
        v => new ImmutableClass(v));

Eigenschaften dieses Typs benötigen aus folgenden Gründen keine speziellen Vergleiche oder Momentaufnahmen:

  • Gleichheit wird außer Kraft gesetzt, sodass verschiedene Instanzen ordnungsgemäß verglichen werden.
  • Der Typ ist unveränderlich, sodass keine Möglichkeit besteht, einen Momentaufnahmenwert zu ändern.

In diesem Fall ist das Standardverhalten von EF Core also einwandfrei.

Einfache unveränderliche Strukturen

Die Zuordnung für einfache Strukturen ist auch einfach und erfordert keine speziellen Vergleichsfunktionen oder Momentaufnahmen.

public readonly struct ImmutableStruct
{
    public ImmutableStruct(int value)
    {
        Value = value;
    }

    public int Value { get; }
}
modelBuilder
    .Entity<EntityType>()
    .Property(e => e.MyProperty)
    .HasConversion(
        v => v.Value,
        v => new ImmutableStruct(v));

EF Core verfügt über integrierte Unterstützung für die Generierung kompilierter, memberweiser Vergleiche von Struktureigenschaften. Dies bedeutet, dass Strukturen keine Gleichheitsüberschreibung für EF Core benötigen, aber Sie können dies dennoch aus anderen Gründen tun. Außerdem ist eine spezielle Momentaufnahme nicht erforderlich, da Strukturen unveränderlich sind und ohnehin memberweise kopiert werden. (Dies gilt auch für veränderliche Strukturen, aber veränderliche Strukturen sollten im Allgemeinen vermieden werden.)

Veränderliche Klassen

Sie sollten nach Möglichkeit unveränderliche Typen (Klassen oder Strukturen) mit Wertkonvertern verwenden. Dies ist in der Regel effizienter und bietet eine klarere Semantik als die Verwendung eines veränderlichen Typs. Allerdings ist es üblich, Eigenschaften von Typen zu verwenden, die die Anwendung nicht ändern kann. Beispiel: Zuordnen einer Eigenschaft, die eine Liste von Zahlen enthält:

public List<int> MyListProperty { get; set; }

Die List<T>-Klasse:

  • Hat Verweisgleichheit; zwei Listen, die dieselben Werte enthalten, werden als unterschiedlich behandelt.
  • Ist veränderlich; Werte in der Liste können hinzugefügt und entfernt werden.

Eine typische Wertkonvertierung für eine Listeneigenschaft kann die Liste in und aus JSON konvertieren:

modelBuilder
    .Entity<EntityType>()
    .Property(e => e.MyListProperty)
    .HasConversion(
        v => JsonSerializer.Serialize(v, (JsonSerializerOptions)null),
        v => JsonSerializer.Deserialize<List<int>>(v, (JsonSerializerOptions)null),
        new ValueComparer<List<int>>(
            (c1, c2) => c1.SequenceEqual(c2),
            c => c.Aggregate(0, (a, v) => HashCode.Combine(a, v.GetHashCode())),
            c => c.ToList()));

Der ValueComparer<T>-Konstruktor akzeptiert drei Ausdrücke:

  • Einen Ausdruck zur Überprüfung der Gleichheit
  • Einen Ausdruck zum Generieren eines Hashcodes
  • Einen Ausdruck zum Erstellen der Momentaufnahme eines Werts

In diesem Fall wird der Vergleich durchgeführt, indem überprüft wird, ob die Sequenzen von Zahlen identisch sind.

Ebenso wird der Hashcode aus derselben Sequenz erstellt. (Beachten Sie, dass dies ein Hashcode über veränderliche Werte ist und daher Probleme verursachen kann. Bevorzugen Sie stattdessen nach Möglichkeit die Unveränderlichkeit.)

Die Momentaufnahme wird durch Klonen der Liste mit ToList erstellt. Noch einmal, dies ist nur erforderlich, wenn die Listen geändert werden sollen. Bevorzugen Sie stattdessen nach Möglichkeit die Unveränderlichkeit.

Hinweis

Wertkonverter und -vergleiche werden mithilfe von Ausdrücken anstatt einfacher Delegaten erstellt. Dies liegt daran, dass EF Core diese Ausdrücke in eine viel komplexere Ausdrucksstruktur einfügt, die dann in einen Entitätsshaperdelegaten kompiliert wird. Konzeptionell ähnelt dies dem Compilerinlining. Beispielsweise handelt es sich bei einer einfachen Konvertierung vielleicht nur um eine einkompilierte Umwandlung und nicht um einen Aufruf einer anderen Methode zur Konvertierung.

Schlüsselvergleiche

Im Hintergrundabschnitt wird erläutert, warum Schlüsselvergleiche möglicherweise eine spezielle Semantik erfordern. Erstellen Sie unbedingt einen Vergleich, der für Schlüssel geeignet ist, wenn Sie ihn für eine Primär-, Prinzipal- oder Fremdschlüsseleigenschaft festlegen.

Verwenden Sie SetKeyValueComparer in den seltenen Fällen, in denen für dieselbe Eigenschaft unterschiedliche Semantiken erforderlich sind.

Hinweis

SetStructuralValueComparer wurde als veraltet erklärt. Verwenden Sie stattdessen SetKeyValueComparer.

Überschreiben des Standardvergleichs

Manchmal ist der von EF Core verwendete Standardvergleich möglicherweise nicht geeignet. Beispielsweise wird die Mutation von Bytearrays nicht standardmäßig in EF Core erkannt. Dies kann durch Festlegen eines anderen Vergleichs für die Eigenschaft außer Kraft gesetzt werden:

modelBuilder
    .Entity<EntityType>()
    .Property(e => e.MyBytes)
    .Metadata
    .SetValueComparer(
        new ValueComparer<byte[]>(
            (c1, c2) => c1.SequenceEqual(c2),
            c => c.Aggregate(0, (a, v) => HashCode.Combine(a, v.GetHashCode())),
            c => c.ToArray()));

EF Core vergleicht nun Bytesequenzen und wird daher Bytearraymutationen erkennen.