Share via


Comparer di valori

Suggerimento

Il codice in questo documento è disponibile in GitHub come esempio eseguibile.

Background

Il rilevamento delle modifiche indica che EF Core determina automaticamente le modifiche eseguite dall'applicazione in un'istanza di entità caricata, in modo che tali modifiche possano essere salvate nel database quando SaveChanges viene chiamato. EF Core esegue in genere questa operazione eseguendo uno snapshot dell'istanza quando viene caricata dal database e confrontando tale snapshot con l'istanza distribuita all'applicazione.

EF Core è dotato di logica predefinita per la creazione di snapshot e il confronto della maggior parte dei tipi standard usati nei database, pertanto gli utenti in genere non devono preoccuparsi di questo argomento. Tuttavia, quando una proprietà viene mappata tramite un convertitore di valori, EF Core deve eseguire un confronto su tipi di utente arbitrari, che possono essere complessi. Per impostazione predefinita, EF Core usa il confronto di uguaglianza predefinito definito dai tipi ,ad esempio il Equals metodo ; per la creazione di snapshot, i tipi valore vengono copiati per produrre lo snapshot, mentre per i tipi di riferimento non viene eseguita alcuna copia e la stessa istanza viene usata come snapshot.

Nei casi in cui il comportamento di confronto predefinito non è appropriato, gli utenti possono fornire un operatore di confronto di valori, che contiene la logica per la creazione di snapshot, il confronto e il calcolo di un codice hash. Ad esempio, il codice seguente configura la conversione di valori per la List<int> proprietà da convertire in una stringa JSON nel database e definisce anche un operatore di confronto di valori appropriato:

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

Per altri dettagli, vedere classi modificabili di seguito.

Si noti che i comparer di valori vengono usati anche per determinare se due valori chiave sono uguali durante la risoluzione delle relazioni; questo è spiegato di seguito.

Confronto superficiale e profondo

Per i tipi valore non modificabili di piccole dimensioni, intad esempio , la logica predefinita di EF Core funziona bene: il valore viene copiato così com'è quando viene snapshotzzato e confrontato con il confronto di uguaglianza predefinito del tipo. Quando si implementa un operatore di confronto di valori personalizzato, è importante considerare se la logica di confronto profondo o superficiale (e snapshot) è appropriata.

Si considerino matrici di byte, che possono essere arbitrariamente grandi. Questi valori possono essere confrontati:

  • Per riferimento, in modo che venga rilevata una differenza solo se viene usata una nuova matrice di byte
  • Per un confronto approfondito, tale che venga rilevata la mutazione dei byte nella matrice

Per impostazione predefinita, EF Core usa il primo di questi approcci per le matrici di byte non chiave. Ovvero, vengono confrontati solo i riferimenti e viene rilevata una modifica solo quando una matrice di byte esistente viene sostituita con una nuova. Si tratta di una decisione pragmatica che evita di copiare intere matrici e confrontarle con byte durante l'esecuzione di SaveChanges. Significa che lo scenario comune di sostituzione, ad esempio, un'immagine con un'altra viene gestita in modo efficiente.

D'altra parte, l'uguaglianza dei riferimenti non funziona quando vengono usate matrici di byte per rappresentare chiavi binarie, poiché è molto improbabile che una proprietà FK sia impostata sulla stessa istanza di una proprietà PK a cui deve essere confrontata. Ef Core usa quindi confronti approfonditi per le matrici di byte che fungono da chiavi; è improbabile che si verifichi un grande successo in quanto le chiavi binarie sono in genere brevi.

Si noti che la logica di confronto e snapshot scelta deve corrispondere l'una all'altra: il confronto approfondito richiede la corretta esecuzione dello snapshot.

Classi non modificabili semplici

Si consideri una proprietà che usa un convertitore di valori per eseguire il mapping di una classe semplice e non modificabile.

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

Le proprietà di questo tipo non richiedono confronti o snapshot speciali perché:

  • L'uguaglianza viene sottoposta a override in modo che istanze diverse vengano confrontate correttamente
  • Il tipo non è modificabile, quindi non è possibile modificare un valore di snapshot

In questo caso, quindi, il comportamento predefinito di EF Core è corretto così com'è.

Struct non modificabili semplici

Il mapping per gli struct semplici è anche semplice e non richiede strumenti di confronto o snapshot speciali.

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 include il supporto predefinito per la generazione di confronti compilati e membri per le proprietà di struct. Ciò significa che gli struct non devono avere override di uguaglianza per EF Core, ma è comunque possibile scegliere di eseguire questa operazione per altri motivi. Inoltre, la creazione di snapshot speciali non è necessaria perché gli struct sono non modificabili e vengono sempre copiati in modo membro comunque. Questo vale anche per gli struct modificabili, ma gli struct modificabili devono essere evitati in generale.

Classi modificabili

È consigliabile usare tipi non modificabili (classi o struct) con convertitori di valori quando possibile. Questo è in genere più efficiente e ha una semantica più pulita rispetto all'uso di un tipo modificabile. Tuttavia, detto questo, è comune usare proprietà di tipi che l'applicazione non può modificare. Ad esempio, eseguire il mapping di una proprietà contenente un elenco di numeri:

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

La classe List<T>:

  • Ha l'uguaglianza dei riferimenti; due elenchi contenenti gli stessi valori vengono considerati diversi.
  • È modificabile; è possibile aggiungere e rimuovere valori nell'elenco.

Una conversione di valori tipica in una proprietà di elenco potrebbe convertire l'elenco in e da 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()));

Il ValueComparer<T> costruttore accetta tre espressioni:

  • Espressione per verificare l'uguaglianza
  • Espressione per la generazione di un codice hash
  • Espressione per creare uno snapshot di un valore

In questo caso, il confronto viene eseguito controllando se le sequenze di numeri sono uguali.

Analogamente, il codice hash viene compilato da questa stessa sequenza. Si noti che si tratta di un codice hash su valori modificabili e quindi può causare problemi. Essere invece immutabile, se possibile.

Lo snapshot viene creato clonando l'elenco con ToList. Anche in questo caso, questa operazione è necessaria solo se gli elenchi verranno modificati. Essere invece immutabile, se possibile.

Nota

I convertitori di valori e gli strumenti di confronto vengono costruiti usando espressioni anziché delegati semplici. Questo perché EF Core inserisce queste espressioni in un albero delle espressioni molto più complesso che viene quindi compilato in un delegato di entity shaper. Concettualmente, questo aspetto è simile all'inlining del compilatore. Ad esempio, una semplice conversione può essere semplicemente compilata nel cast, anziché una chiamata a un altro metodo per eseguire la conversione.

Operatore di confronto delle chiavi

La sezione in background illustra il motivo per cui i confronti chiave possono richiedere una semantica speciale. Assicurarsi di creare un operatore di confronto appropriato per le chiavi quando lo si imposta su una proprietà primaria, principale o di chiave esterna.

Usare SetKeyValueComparer nei rari casi in cui è necessaria una semantica diversa nella stessa proprietà.

Nota

SetStructuralValueComparer è obsoleto. Utilizzare invece SetKeyValueComparer.

Override dell'operatore di confronto predefinito

A volte il confronto predefinito usato da EF Core potrebbe non essere appropriato. Ad esempio, la mutazione delle matrici di byte non è, per impostazione predefinita, rilevata in EF Core. È possibile eseguire l'override impostando un operatore di confronto diverso sulla proprietà :

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 confronta ora le sequenze di byte e rileverà quindi le mutazioni delle matrici di byte.