Värdejämförare

Tips/Råd

Koden i det här dokumentet finns på GitHub som ett körbart exempel.

Bakgrund

Ändringsspårning innebär att EF Core automatiskt avgör vilka ändringar som utförts av programmet på en inläst entitetsinstans, så att dessa ändringar kan sparas tillbaka till databasen när SaveChanges anropas. EF Core utför vanligtvis detta genom att ta en ögonblicksbild av instansen när den läses in från databasen och jämföra ögonblicksbilden med den instans som delas ut till programmet.

EF Core levereras med inbyggd logik för ögonblicksbilder och jämförelse av de flesta standardtyper som används i databaser, så användarna behöver vanligtvis inte bekymra sig om det här ämnet. Men när en egenskap mappas via en värdekonverterare måste EF Core göra en jämförelse av godtyckliga användartyper, vilket kan vara komplext. SOM standard använder EF Core den standardjämlikhetsjämförelse som definieras av typer (t.ex. Equals metoden); för ögonblicksbilder kopieras värdetyper för att producera ögonblicksbilden, medan ingen kopiering sker för referenstyper och samma instans används som ögonblicksbilden.

I fall där det inbyggda jämförelsebeteendet inte är lämpligt kan användarna tillhandahålla en värdejämförare som innehåller logik för ögonblicksbilder, jämförelse och beräkning av en hashkod. Följande ställer till exempel in värdekonvertering för List<int> egenskap som ska konverteras till en JSON-sträng i databasen och definierar även en lämplig värdejämförare:

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

Mer information finns i föränderliga klasser nedan.

Observera att värdejämförarna också används för att avgöra om två nyckelvärden är samma vid matchning av relationer. Detta förklaras nedan.

Ytlig kontra djup jämförelse

För små, oföränderliga värdetyper som intfungerar EF Cores standardlogik bra: värdet kopieras as-is när det ögonblicksbilderas och jämförs med typens inbyggda likhetsjämförelse. När du implementerar din egen värdejämförare är det viktigt att överväga om djup eller ytlig jämförelselogik (och ögonblicksbilder) är lämplig.

Överväg bytematriser, som kan vara godtyckligt stora. Dessa kan jämföras:

  • Som referens identifieras en skillnad endast om en ny bytematris används
  • Vid en djup jämförelse identifieras mutationer av bytes i arrayen

Som standard använder EF Core den första av dessa metoder för bytematriser som inte är viktiga. Det vill: endast referenser jämförs och en ändring identifieras endast när en befintlig bytematris ersätts med en ny. Det här är ett pragmatiskt beslut som undviker att kopiera hela arrayer och jämföra dem byte-för-byte när SaveChanges utförs. Det innebär att det vanliga scenariot att ersätta, till exempel, en bild med en annan hanteras på ett högpresterande sätt.

Å andra sidan fungerar inte referensjämlikhet när bytematriser används för att representera binära nycklar, eftersom det är mycket osannolikt att en FK-egenskap är inställd på samma instans som en PK-egenskap som den behöver jämföras med. Ef Core använder därför djupa jämförelser för bytematriser som fungerar som nycklar. Detta kommer sannolikt inte att ha en stor prestandaträff eftersom binära nycklar vanligtvis är korta.

Observera att den valda jämförelse- och ögonblicksbildslogiken måste motsvara varandra: djupjämförelse kräver djup ögonblicksbildstagning för att fungera korrekt.

Enkla oföränderliga klasser

Överväg en egenskap som använder en värdekonverterare för att mappa en enkel, oföränderlig klass.

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

Egenskaper av den här typen behöver inte särskilda jämförelser eller ögonblicksbilder eftersom:

  • Likhet åsidosätts så att olika instanser jämförs korrekt
  • Typen är oföränderlig, så det finns ingen chans att mutera ett ögonblicksbildsvärde

Så i det här fallet är standardbeteendet för EF Core bra som det är.

Enkla oföränderliga strukturer

Mappningen för enkla structs är också enkel och kräver inga särskilda jämförelser eller ögonblicksbilder.

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 har inbyggt stöd för att generera kompilerade, medlemsbaserade jämförelser av struct-egenskaper. Det innebär att strukturer inte behöver åsidosätta likhet för EF Core, men du kan fortfarande välja att göra detta av andra skäl. Dessutom behövs ingen särskild ögonblicksbild eftersom strukturer är oföränderliga och ändå alltid kopieras medlemsvis. (Detta gäller även för mutable structs, men mutable structs bör i allmänhet undvikas.)

Föränderliga klasser

Vi rekommenderar att du använder oföränderliga typer (klasser eller structs) med värdekonverterare när det är möjligt. Detta är vanligtvis effektivare och har renare semantik än att använda en föränderlig typ. Med detta sagt är det dock vanligt att använda egenskaper för typer som programmet inte kan ändra. Du kan till exempel mappa en egenskap som innehåller en lista med tal:

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

List<T>-klassen:

  • Har referensjämlikhet; två listor som innehåller samma värden behandlas som olika.
  • Är föränderlig; värden i listan kan läggas till och tas bort.

En typisk värdekonvertering på en listegenskap kan konvertera listan till och från JSON:

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

Konstruktorn ValueComparer<T> accepterar tre uttryck:

  • Ett uttryck för att kontrollera likhet
  • Ett uttryck för att generera en hash-kod
  • Ett uttryck för att ögonblicksbilda ett värde

I det här fallet görs jämförelsen genom att kontrollera om talsekvenserna är desamma.

På samma sätt skapas hash-koden från samma sekvens. (Observera att detta är en hash-kod över föränderliga värden och därför kan orsaka problem. Var oföränderlig i stället om du kan.)

Ögonblicksbilden skapas genom att listan klonas med ToList. Återigen behövs detta bara om listorna ska muteras. Var oföränderlig i stället om du kan.

Anmärkning

Värdekonverterare och jämförare konstrueras med hjälp av uttryck i stället för enkla delegater. Det beror på att EF Core infogar dessa uttryck i ett mycket mer komplext uttrycksträd som sedan kompileras till ett entitetsformningsdelegat. Konceptuellt liknar detta inlinning i kompilatorer. En enkel konvertering kan till exempel bara vara en kompilerad i cast i stället för ett anrop till en annan metod för att utföra konverteringen.

Viktiga jämförelseverktyg

I bakgrundsavsnittet beskrivs varför viktiga jämförelser kan kräva särskilda semantik. Säkerställ att du skapar en jämförelse som är lämplig för nycklar när du ställer in den på en primär-, huvud- eller utländsk nyckelegenskap.

Använd SetKeyValueComparer i sällsynta fall där olika semantik krävs på samma egenskap.

Anmärkning

SetStructuralValueComparer har föråldrats. Använd SetKeyValueComparer i stället.

Åsidosätta standardkomparatorn

Ibland är standardjämförelsen som används av EF Core kanske inte lämplig. Till exempel identifieras inte mutation av bytematriser som standard i EF Core. Detta kan åsidosättas genom att ange en annan jämförelse för egenskapen:

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 jämför nu bytesekvenser och identifierar därför bytematrismutationer.