Nota
El acceso a esta página requiere autorización. Puede intentar iniciar sesión o cambiar directorios.
El acceso a esta página requiere autorización. Puede intentar cambiar los directorios.
Sugerencia
El código de este documento se puede encontrar en GitHub como ejemplo ejecutable.
Contexto
El seguimiento de cambios significa que EF Core determina automáticamente qué cambios realizó la aplicación en una instancia de entidad cargada, de modo que esos cambios puedan guardarse nuevamente en la base de datos cuando se llama a SaveChanges. EF Core normalmente realiza 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 incluye 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 una propiedad se asigna a través de un convertidor de valores, EF Core debe realizar una comparación en 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 Equals
método); para la creación de instantáneas, los tipos de valor se copian para generar la instantánea, mientras que para los tipos de referencia no se produce ninguna copia y se usa la misma instancia que la 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, la comparación y el cálculo de un código hash. Por ejemplo, lo siguiente configura la conversión del valor de la propiedad List<int>
a una cadena JSON para almacenarla en la base de datos, y también define 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 las clases mutables siguientes para obtener más información.
Tenga en cuenta que los comparadores de valores también se usan al determinar si dos valores de clave son los mismos al resolver relaciones; esto se explica a continuación.
Comparación superficial frente a profunda
Para tipos de valor pequeños e inmutables, como int
, la lógica predeterminada de EF Core funciona bien: el valor se copia as-is al tomar una instantánea y se compara con la comparación de igualdad integrada del tipo. Al implementar su propio comparador de valores, es importante tener en cuenta si la lógica de comparación profunda o superficial, así como la creación de instantáneas, es adecuada.
Considere las matrices de bytes, que pueden ser arbitrariamente grandes. Estos podrían compararse:
- Por referencia, de modo que solo se detecta una diferencia si se usa una nueva matriz de bytes
- Mediante una comparación profunda, se detecta cualquier mutación de los bytes en la matriz.
De forma predeterminada, EF Core usa el primero de estos enfoques para matrices de bytes que no son clave. Es decir, solo se comparan las referencias y solo se detecta un cambio cuando se reemplaza una matriz de bytes existente por una nueva. Se trata de una decisión pragmática que evita copiar matrices completas y compararlas byte a byte al ejecutar SaveChanges. Esto significa que el escenario común de reemplazar, por ejemplo, una imagen con otra se controla de forma eficaz.
Por otro lado, la igualdad de referencia 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 a la que se debe comparar. Por lo tanto, EF Core usa comparaciones profundas para matrices de bytes que actúan como claves; Esto es poco probable que 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 coincidir: la comparación profunda requiere la creación de instantáneas profundas para funcionar correctamente.
Clases inmutables simples
Considere una propiedad que usa un convertidor de valores para asignar una clase simple e inmutable.
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 captura.
Por lo 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 especial ni capturas.
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 a nivel de miembros de propiedades de structs. Esto significa que las estructuras no necesitan invalidar la igualdad para EF Core, pero puede optar por hacerlo por otras razones. Además, no se necesita una captura especial, ya que las estructuras son inmutables y siempre se copian por cada miembro de cualquier manera. (Esto también es cierto para 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 semántica más limpia que usar un tipo mutable. Sin embargo, dicho esto es habitual usar propiedades de tipos que la aplicación no puede cambiar. Por ejemplo, mapear una propiedad que contiene una lista de números:
public List<int> MyListProperty { get; set; }
La clase List<T>:
- Tiene igualdad de referencia; 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 en y desde 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()));
El ValueComparer<T> constructor acepta tres expresiones:
- Expresión para comprobar la igualdad
- Expresión para generar un código hash
- 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 las mismas.
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, sea inmutable si puede).
La instantánea se crea mediante la clonación de la lista con ToList
. De nuevo, esto solo es necesario si las listas se van a mutar. Sé inmutable si puedes.
Nota:
Los convertidores de valores y los comparadores se construyen mediante 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 se compila luego en un delegado de modelador de entidad. Conceptualmente, esto es similar al inlining del compilador. Por ejemplo, una conversión sencilla puede ser simplemente un tipo de conversión integrado, en lugar de invocar otro método para realizar la conversión.
Comparadores de claves
En la sección en segundo plano se explica por qué las comparaciones clave pueden requerir semántica especial. Asegúrese de crear un comparador adecuado para las claves al establecerlo en una propiedad de clave primaria, principal o foránea.
Use SetKeyValueComparer en los casos poco frecuentes en los que se requiere una semántica diferente en la misma propiedad.
Nota:
SetStructuralValueComparer ha quedado obsoleto. En su lugar, use SetKeyValueComparer.
Invalidación del comparador predeterminado
A veces, es posible que la comparación predeterminada usada por EF Core no sea adecuada. Por ejemplo, la mutación de matrices de bytes no es, de forma predeterminada, detectada en EF Core. Esto se puede anular 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()));
EF Core ahora comparará las secuencias de bytes y, por tanto, detectará mutaciones de matriz de bytes.