Remarque
L’accès à cette page nécessite une autorisation. Vous pouvez essayer de vous connecter ou de modifier des répertoires.
L’accès à cette page nécessite une autorisation. Vous pouvez essayer de modifier des répertoires.
Conseil / Astuce
Le code de ce document se trouve sur GitHub en tant qu’exemple exécutable.
Contexte
Le suivi des modifications signifie qu’EF Core détermine automatiquement les modifications effectuées par l’application sur une instance d’entité chargée, afin que ces modifications puissent être enregistrées dans la base de données quand elles SaveChanges sont appelées. EF Core effectue généralement cette opération en prenant un instantané de l’instance lorsqu’elle est chargée à partir de la base de données et en comparant cet instantané à l’instance transmise à l’application.
EF Core est fourni avec une logique intégrée pour prendre des instantanés et comparer la plupart des types standard utilisés dans les bases de données, donc les utilisateurs n'ont généralement pas à se préoccuper de ce sujet. Toutefois, lorsqu’une propriété est mappée via un convertisseur de valeurs, EF Core doit effectuer une comparaison sur des types d’utilisateurs arbitraires, ce qui peut être complexe. Par défaut, EF Core utilise la comparaison d’égalité par défaut définie par les types (par exemple, la méthode Equals) ; pour la capture instantanée, les types valeur sont copiés pour produire l'instantané, tandis que pour les types de référence, aucune copie ne se produit et la même instance est utilisée comme instantané.
Dans les cas où le comportement de comparaison intégré n’est pas approprié, les utilisateurs peuvent fournir un comparateur de valeurs, qui contient une logique pour la capture instantanée, la comparaison et le calcul d’un code de hachage. Par exemple, le code suivant configure la conversion de la valeur de la propriété List<int> en chaîne JSON dans la base de données, et définit également un comparateur de valeurs approprié.
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()));
Pour plus d’informations, consultez les classes mutables ci-dessous.
Notez que les comparateurs de valeurs sont également utilisés pour déterminer si deux valeurs clés sont identiques lors de la résolution des relations ; ceci est expliqué ci-dessous.
Comparaison superficielle et profonde
Pour les petits types de valeurs immuables tels que intla logique par défaut d’EF Core fonctionne bien : la valeur est copiée as-is lors de l’instantané et comparée à la comparaison d’égalité intégrée du type. Lors de l’implémentation de votre propre comparateur de valeur, il est important de déterminer si la logique de comparaison profonde ou superficielle (et de capture instantanée) est appropriée.
Considérez les tableaux d’octets, qui peuvent être volumineux de manière arbitraire. Celles-ci peuvent être comparées :
- Par référence, une différence est détectée uniquement si un nouveau tableau d’octets est utilisé
- Par comparaison approfondie, de sorte que la mutation des octets dans le tableau est détectée
Par défaut, EF Core utilise la première de ces approches pour les tableaux d’octets non clés. Autrement dit, seules les références sont comparées et une modification est détectée uniquement lorsqu’un tableau d’octets existant est remplacé par un nouveau tableau. Il s’agit d’une décision pragmatique qui évite de copier des tableaux entiers et de les comparer en octets lors de l’exécution SaveChanges. Cela signifie que le scénario courant de remplacement, par exemple, une image par une autre est gérée de manière performante.
En revanche, l’égalité des références ne fonctionne pas lorsque les tableaux d’octets sont utilisés pour représenter des clés binaires, car il est très peu probable qu’une propriété FK soit définie sur la même instance qu’une propriété PK à laquelle elle doit être comparée. Par conséquent, EF Core utilise des comparaisons approfondies pour les tableaux d’octets agissant en tant que clés ; cela est peu susceptible d'avoir un grand impact sur les performances, car les clés binaires sont généralement courtes.
Notez que la logique de comparaison et d’instantané choisie doit correspondre les unes aux autres : la comparaison approfondie nécessite une capture instantanée approfondie pour fonctionner correctement.
Classes immuables simples
Considérez une propriété qui utilise un convertisseur de valeurs pour mapper une classe simple et immuable.
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));
Les propriétés de ce type n’ont pas besoin de comparaisons ou d’instantanés spéciaux, car :
- L’égalité est remplacée afin que différentes instances soient comparées correctement
- Le type est immuable, il n’y a donc aucune possibilité de muter une valeur d’instantané
Par conséquent, dans ce cas, le comportement par défaut d’EF Core est correct, car c’est le cas.
Structs immuables simples
Le mappage pour les structs simples est également simple et ne nécessite aucun comparateur spécial ni capture instantanée.
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 prend en charge la génération de comparaisons compilées, par membre, des propriétés de structure. Cela signifie que les structures n’ont pas besoin de redéfinir l'égalité pour EF Core, mais vous pouvez toujours choisir de le faire pour d’autres raisons. En outre, une capture instantanée spéciale n’est pas nécessaire, car les structures sont immuables et sont toujours copiées membre par membre. (Cela est également vrai pour les structs mutables, mais les structs mutables doivent en général être évités.)
Classes mutables
Il est recommandé d’utiliser des types immuables (classes ou structs) avec des convertisseurs de valeurs lorsque cela est possible. Cela est généralement plus efficace et a une sémantique plus propre que l’utilisation d’un type mutable. Toutefois, cela dit, il est courant d’utiliser des propriétés de types que l’application ne peut pas modifier. Par exemple, mapper une propriété contenant une liste de nombres :
public List<int> MyListProperty { get; set; }
La classe List<T> :
- A l’égalité des références ; deux listes contenant les mêmes valeurs sont traitées comme différentes.
- Est mutable ; les valeurs de la liste peuvent être ajoutées et supprimées.
Une conversion de valeur classique sur une propriété de liste peut convertir la liste en et depuis 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()));
Le ValueComparer<T> constructeur accepte trois expressions :
- Expression pour la vérification de l’égalité
- Expression permettant de générer un code de hachage
- Expression permettant d’instantanér une valeur
Dans ce cas, la comparaison est effectuée en vérifiant si les séquences de nombres sont identiques.
De même, le code de hachage est généré à partir de cette même séquence. (Notez qu’il s’agit d’un code de hachage sur des valeurs mutables et peut donc entraîner des problèmes. Soyez immuable à la place si vous le pouvez.)
L’instantané est créé en clonant la liste avec ToList. Là encore, cela n’est nécessaire que si les listes vont être mutées. Soyez immuable à la place si vous le pouvez.
Remarque
Les convertisseurs et comparateurs de valeur sont construits à l’aide d’expressions plutôt que de délégués simples. Cela est dû au fait qu’EF Core insère ces expressions dans une arborescence d’expressions beaucoup plus complexe qui est ensuite compilée dans un délégué de façonnement d’entité. Conceptuellement, cela est similaire à l'insertion par le compilateur. Par exemple, une conversion simple peut simplement être compilée en cast, plutôt qu’un appel à une autre méthode pour effectuer la conversion.
Comparateurs de clés
La section en arrière-plan explique pourquoi les comparaisons clés peuvent nécessiter une sémantique spéciale. Veillez à créer un comparateur approprié pour les clés lors de sa définition sur une propriété de clé primaire, principale ou étrangère.
Utilisez SetKeyValueComparer dans les rares cas où différentes sémantiques sont requises sur la même propriété.
Remarque
SetStructuralValueComparer a été obsolète. Utilisez SetKeyValueComparer à la place.
Substitution du comparateur par défaut
Parfois, la comparaison par défaut utilisée par EF Core peut ne pas être appropriée. Par exemple, la mutation des tableaux d’octets n’est pas détectée par défaut dans EF Core. Cela peut être remplacé en définissant un comparateur différent pour la propriété.
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 compare désormais les séquences d’octets et détecte donc les mutations de tableau d’octets.