Nota
L'accesso a questa pagina richiede l'autorizzazione. È possibile provare ad accedere o modificare le directory.
L'accesso a questa pagina richiede l'autorizzazione. È possibile provare a modificare le directory.
Suggerimento
Il codice in questo documento è disponibile in GitHub come esempio eseguibile.
Sfondo
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 di valore non modificabili di piccole dimensioni, come int, la logica predefinita di EF Core funziona bene: il valore viene copiato as-is quando viene creato uno snapshot e confrontato con il confronto di uguaglianza intrinseco del tipo. Quando si implementa un comparatore di valori personalizzato, è importante considerare se la logica di comparazione dettagliata o superficiale e la creazione di istantanee siano appropriate.
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 interi array e confrontarli byte per 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 di snapshotting scelta deve corrispondere: il confronto approfondito richiede uno snapshotting approfondito per funzionare correttamente.
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 sovrascritta in modo che diverse istanze 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'è.
Strutture semplici non modificabili
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 creazione di confronti membro per membro compilati per le proprietà delle 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 immutabili e vengono sempre copiati membro per membro. 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; }
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.
L'istantanea viene creata clonando l'elenco con ToList. Anche in questo caso, questa operazione è necessaria solo se gli elenchi verranno modificati. Essere invece immutabile, se possibile.
Annotazioni
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.
Comparatori di chiavi
La sezione in background illustra il motivo per cui i confronti chiave possono richiedere una semantica speciale. Assicurarsi di creare un comparatore appropriato per le chiavi quando lo si imposta su una proprietà di chiave primaria, principale o esterna.
Usare SetKeyValueComparer nei rari casi in cui è necessaria una semantica diversa nella stessa proprietà.
Annotazioni
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.