Comparadores de valores
Sugerencia
El código de este documento se puede encontrar en GitHub como ejemplo ejecutable.
Fondo
Seguimiento de cambios significa que EF Core determina automáticamente qué cambios ha realizado la aplicación en una instancia de entidad cargada, de modo que esos cambios se puedan volver a guardar en la base de datos cuando se llame a SaveChanges. EF Core normalmente hace esto tomando una instantánea de la instancia cuando se carga desde la base de datos y comparando esa instantánea con la instancia que se entrega a la aplicación.
EF Core tiene lógica integrada para la creación de instantáneas y la comparación de la mayoría de los tipos estándar que se usan en las bases de datos, por lo que los usuarios no suelen tener que preocuparse por este tema. Sin embargo, cuando se asigna una propiedad a través de un convertidor de valores, EF Core debe comparar tipos de usuario arbitrarios, lo que puede ser complejo. De forma predeterminada, EF Core usa la comparación de igualdad predeterminada definida por tipos (por ejemplo, el método Equals
). Para la creación de instantáneas, se copian los tipos de valor para generar la instantánea, mientras que, para los tipos de referencia, no se copia nada y se usa la misma instancia como instantánea.
En los casos en los que el comportamiento de comparación integrado no es adecuado, los usuarios pueden proporcionar un comparador de valores, que contiene lógica para la creación de instantáneas y la comparación y el cálculo de un código hash. Por ejemplo, el siguiente código configura la conversión de valores para que la propiedad List<int>
se convierta en una cadena JSON en la base de datos, y define también un comparador de valores adecuado:
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()));
Consulte la sección Clases mutables más adelante para obtener más información.
Tenga en cuenta que los comparadores de valores también se usan para determinar si dos valores de clave son iguales cuando se resuelven relaciones. Esto se explica a continuación.
Comparación superficial frente a comparación profunda
Para los tipos de valor pequeños e inmutables, como int
, la lógica predeterminada de EF Core funciona bien: el valor se copia como está cuando se crea la instantánea y se compara por medio de la comparación de igualdad integrada del tipo. Cuando implementa su propio comparador de valores, es importante tener en cuenta si es adecuada la lógica de comparación (y de creación de instantáneas) profunda o superficial.
Considere las matrices de bytes, que pueden ser arbitrariamente grandes. Se podrían comparar:
- Por referencia, de modo que una diferencia solo se detecta si se usa una nueva matriz de bytes.
- Por comparación profunda, de modo que se detecta una mutación de los bytes de la matriz.
De forma predeterminada, EF Core usa el primero de estos enfoques para matrices de bytes que no son de claves. Es decir, solo se comparan las referencias y un cambio se detecta solamente cuando se reemplaza una matriz de bytes por una nueva. Esta es una decisión pragmática que evita copiar matrices enteras y compararlas byte a byte cuando se ejecuta SaveChanges. Esto significa que el caso común de reemplazar, por ejemplo, una imagen por otra se controla de forma eficaz.
Por otro lado, la igualdad de referencias no funcionaría cuando se usan matrices de bytes para representar claves binarias, ya que es muy improbable que una propiedad FK se establezca en la misma instancia que una propiedad PK con la que debe compararse. Por tanto, EF Core usa comparaciones profundas para matrices de bytes que actúan como claves. Es poco probable que esto tenga un gran impacto en el rendimiento, ya que las claves binarias suelen ser cortas.
Tenga en cuenta que la lógica de comparación y de creación de instantáneas elegidas deben corresponderse entre sí: la comparación profunda requiere la creación de instantáneas profunda para funcionar correctamente.
Clases inmutables simples
Piense en una propiedad que usa un convertidor de valores para asignar una clase inmutable simple.
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));
Las propiedades de este tipo no necesitan comparaciones ni instantáneas especiales porque:
- La igualdad se invalida para que las distintas instancias se comparen correctamente.
- El tipo es inmutable, por lo que no hay posibilidad de mutar un valor de instantánea.
Por tanto, en este caso, el comportamiento predeterminado de EF Core es correcto tal cual.
Estructuras inmutables simples
La asignación de estructuras simples también es sencilla y no requiere ningún comparador ni instantáneas especiales.
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 tiene compatibilidad integrada para generar comparaciones compiladas de propiedades de estructuras en términos de miembros. Esto significa que las estructuras no necesitan invalidar la igualdad para EF Core, pero puede hacerlo si lo desea por otras razones. Además, no se necesita una creación de instantáneas especial, ya que las estructuras son inmutables y, de todos modos, siempre se copian en términos de miembros. Esto ocurre también con las estructuras mutables, pero las estructuras mutables deben evitarse en general.
Clases mutables
Se recomienda usar tipos inmutables (clases o estructuras) con convertidores de valores siempre que sea posible. Esto suele ser más eficaz y tiene una semántica más limpia que el uso de un tipo mutable. Sin embargo, dicho esto, es habitual usar propiedades de tipos que la aplicación no puede cambiar. Por ejemplo, asignar una propiedad que contiene una lista de números:
public List<int> MyListProperty { get; set; }
La clase List<T>:
- Tiene igualdad de referencia, es decir, dos listas que contienen los mismos valores se tratan como diferentes.
- Es mutable. Los valores de la lista se pueden agregar y quitar.
Una conversión de valor típica en una propiedad de lista podría convertir la lista al formato JSON y viceversa:
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()));
El constructor ValueComparer<T> acepta tres expresiones:
- Una expresión para comprobar la igualdad.
- Una expresión para generar un código hash.
- Una expresión para crear una instantánea de un valor.
En este caso, la comparación se realiza comprobando si las secuencias de números son iguales.
Del mismo modo, el código hash se compila a partir de esta misma secuencia. Tenga en cuenta que se trata de un código hash sobre valores mutables y, por tanto, puede causar problemas. En su lugar, use tipos inmutables, si puede.
La instantánea se crea clonando la lista con ToList
. De nuevo, esto solo es necesario si las listas se van a mutar. En su lugar, use tipos inmutables, si puede.
Nota:
Los convertidores y comparadores de valores se construyen usando expresiones en lugar de delegados simples. Esto se debe a que EF Core inserta estas expresiones en un árbol de expresiones mucho más complejo que, después, se compila como delegado de conformador de entidad. Conceptualmente, esto es similar a la inserción del compilador. Por ejemplo, una conversión simple se puede compilar simplemente en la conversión, en lugar de llamar a otro método para realizar la conversión.
Comparadores de claves
En la sección Información previa se explica por qué las comparaciones de claves pueden requerir una semántica especial. Asegúrese de crear un comparador adecuado para las claves cuando lo establezca en una propiedad de clave principal, de entidad de servicio o externa.
Use SetKeyValueComparer en los casos poco frecuentes en los que se requiere una semántica diferente en la misma propiedad.
Nota:
SetStructuralValueComparer se ha quedado obsoleto. En su lugar, use SetKeyValueComparer.
Invalidación del comparador predeterminado
A veces, es posible que la comparación predeterminada que usa EF Core no sea adecuada. Por ejemplo, la mutación de matrices de bytes no se detecta de forma predeterminada en EF Core. Esto se puede invalidar estableciendo un comparador diferente en la propiedad :
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()));
Ahora EF Core comparará las secuencias de bytes y, por tanto, detectará mutaciones de matrices de bytes.