Comparateurs de valeurs
Conseil
Le code fourni dans ce document se trouve sur GitHub en tant qu’exemple exécutable.
Arrière-plan
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 lorsque SaveChanges est appelé. 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 l’instantané et la comparaison de la plupart des types standard utilisés dans les bases de données, de sorte que les utilisateurs n’ont généralement pas besoin de se soucier de cette question. Toutefois, lorsqu’une propriété est mappée via un convertisseur de valeur, EF Core doit effectuer une comparaison sur des types d’utilisateurs arbitraires, ce qui peut s’avérer 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 l’instantané, les types de valeur sont copiés pour produire l’instantané, tandis que pour les types de référence, aucune copie n’est effectuée et la même instance est utilisée comme instantanée.
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 permettant de prendre des instantanés, de les comparer et de calculer un code de hachage. Par exemple, le code suivant configure la conversion de valeur pour une propriété List<int>
à convertir 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 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. Cela est expliqué ci-dessous.
Comparaison superficielle et approfondie
Pour les petits types de valeurs immuables tels que int
, la logique par défaut d’EF Core fonctionne bien : la valeur est copiée comme c’est le cas lors de l’instantané, puis comparée à la comparaison d’égalité intégrée du type. Lors de l’implémentation de votre propre comparateur de valeurs, il est important de déterminer si la logique de comparaison approfondie ou superficielle (et d’instantanée) est appropriée.
Considérez les tableaux d’octets, qui peuvent être arbitrairement volumineux. Ceux-ci peuvent être comparés :
- 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 octet par octet lors de l’exécution de SaveChanges. Cela signifie que le scénario courant de remplacement, par exemple, d’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 en tant que 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. Il est peu probable que cela ait un impact important sur les performances, car les clés binaires sont généralement courtes.
Notez que la logique de comparaison et d’instantané choisie doivent correspondre les unes aux autres : la comparaison approfondie nécessite un instantané approfondi 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.
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 assure la prise en charge intégrée de la génération de comparaisons par membre compilées des propriétés de struct. Cela signifie que les structs n’ont pas besoin d’avoir l’égalité remplacée pour EF Core, mais vous pouvez toujours choisir de le faire pour d’autres raisons. En outre, la capture instantanée spéciale n’est pas nécessaire, car les structs sont immuables et sont toujours copiés dans le sens des membres. (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, le mappage d’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 constructeur ValueComparer<T> accepte trois expressions :
- Une expression pour la vérification de l’égalité
- Une expression permettant de générer un code de hachage
- Une expression permettant de générer l’instantané d’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 provoquer 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 valeurs 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é du modélisateur d’entité. Conceptuellement, cela est similaire à l’incorporation du 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 valeurs
La section en arrière-plan explique pourquoi les comparaisons de 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.
Remplacement 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. Il est possible de déroger à cette règle en définissant un autre comparateur sur 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.