Observação
O acesso a essa página exige autorização. Você pode tentar entrar ou alterar diretórios.
O acesso a essa página exige autorização. Você pode tentar alterar os diretórios.
Dica
O código neste documento pode ser encontrado no GitHub como um exemplo executável.
Contexto
O controle de alterações significa que o EF Core determina automaticamente quais alterações foram executadas pelo aplicativo em uma instância de entidade carregada, para que essas alterações possam ser salvas de volta no banco de dados quando SaveChanges forem chamadas. O EF Core geralmente executa isso tirando um instantâneo da instância quando ela é carregada do banco de dados e comparando esse instantâneo com a instância entregue ao aplicativo.
O EF Core vem com lógica incorporada para criação de instantâneos e comparação da maioria dos tipos padrão usados em bancos de dados, portanto, os usuários geralmente não precisam se preocupar com este assunto. No entanto, quando uma propriedade é mapeada por meio de um conversor de valor, o EF Core precisa executar a comparação em tipos de usuário arbitrários, o que pode ser complexo. Por padrão, o EF Core utiliza a comparação de igualdade padrão definida pelos tipos (por exemplo, o Equals método). Para criar um instantâneo, os tipos de valor são copiados, enquanto que para tipos de referência, nenhuma cópia é realizada e a mesma instância é usada como instantâneo.
Nos casos em que o comportamento de comparação interno não é apropriado, os usuários podem fornecer um comparador de valor, que contém lógica para captura de estado, comparação e cálculo de um código hash. Por exemplo, o seguinte configura a conversão do valor da propriedade List<int> para uma cadeia de caracteres JSON no banco de dados e também define um comparador adequado para os valores.
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()));
Veja as classes mutáveis abaixo para obter mais detalhes.
Observe que os comparadores de valor também são usados ao determinar se dois valores de chave são iguais ao resolver relações; isso é explicado abaixo.
Comparação superficial versus profunda
Para tipos de valor pequenos e imutáveis, como int, a lógica padrão do EF Core funciona bem: o valor é copiado as-is quando ocorre o instantâneo e então comparado com a comparação interna de igualdade do tipo. Ao implementar seu próprio comparador de valores, é importante considerar se a lógica de comparação profunda ou superficial e captura instantânea é apropriada.
Considere matrizes de bytes, que podem ser arbitrariamente grandes. Isso pode ser comparado:
- Por referência, de modo que uma diferença só seja detectada se uma nova matriz de bytes for usada
- Por comparação profunda, tal mutação dos bytes na matriz é detectada
Por padrão, o EF Core usa a primeira dessas abordagens para matrizes de bytes não chave. Ou seja, somente as referências são comparadas e uma alteração é detectada somente quando uma matriz de bytes existente é substituída por uma nova. Essa é uma decisão pragmática que evita copiar matrizes inteiras e compará-las bytes a bytes durante a execução SaveChanges. Isso significa que o cenário comum de substituir, digamos, uma imagem por outra é tratado de maneira performante.
Por outro lado, a igualdade de referência não funcionaria quando matrizes de bytes são usadas para representar chaves binárias, pois é muito improvável que uma propriedade FK seja definida como a mesma instância de uma propriedade PK à qual ela precisa ser comparada. Portanto, o EF Core usa comparações profundas para matrizes de bytes que atuam como chaves; é improvável que isso tenha um grande desempenho, pois as chaves binárias geralmente são curtas.
Observe que a lógica de comparação e instantaneização escolhida deve corresponder uma à outra: uma comparação profunda requer uma instantaneização profunda para funcionar corretamente.
Classes imutáveis simples
Considere uma propriedade que usa um conversor de valor para mapear uma classe simples e imutável.
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));
As propriedades desse tipo não precisam de comparações específicas ou capturas de tela porque:
- A igualdade é substituída para que instâncias diferentes sejam comparadas corretamente
- O tipo é imutável, portanto, não há nenhuma chance de alterar um valor de instantâneo
Portanto, nesse caso, o comportamento padrão do EF Core é bom como está.
Structs simples imutáveis
O mapeamento de structs simples também é simples e não requer comparadores especiais ou criação de snapshots.
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));
O EF Core tem suporte interno para gerar comparações compiladas e memberwise de propriedades de struct. Isso significa que os structs não precisam ter a igualdade substituída para o EF Core, mas você ainda pode optar por fazer isso por outros motivos. Além disso, instantâneos especiais não são necessários, pois as estruturas (structs) são imutáveis e são sempre copiadas por seus membros de qualquer forma. (Isso também é verdadeiro para structs mutáveis, mas structs mutáveis devem, em geral, ser evitados.)
Classes mutáveis
É recomendável que você use tipos imutáveis (classes ou structs) com conversores de valor quando possível. Isso geralmente é mais eficiente e tem semântica mais limpa do que usar um tipo mutável. No entanto, dito isso, é comum usar propriedades de tipos que o aplicativo não pode alterar. Por exemplo, mapeando uma propriedade que contém uma lista de números:
public List<int> MyListProperty { get; set; }
A classe List<T>:
- Tem igualdade de referência; duas listas que contêm os mesmos valores são tratadas como diferentes.
- É mutável; os valores na lista podem ser adicionados e removidos.
Uma conversão típica de valores em uma propriedade de lista pode transformar a lista em JSON e vice-versa.
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()));
O ValueComparer<T> construtor aceita três expressões:
- Uma expressão para verificar a igualdade
- Uma expressão para gerar um código hash
- Uma expressão para capturar um valor
Nesse caso, a comparação é feita verificando se as sequências de números são as mesmas.
Da mesma forma, o código hash é criado a partir dessa mesma sequência. (Observe que esse é um código hash sobre valores mutáveis e, portanto, pode causar problemas. Seja imutável se puder.)
O instantâneo é criado através da clonagem da lista com ToList. Novamente, isso só será necessário se as listas forem alteradas. Seja imutável se puder.
Observação
Conversores de valor e comparadores são construídos usando expressões em vez de delegados simples. Isso ocorre porque o EF Core insere essas expressões em uma árvore de expressão muito mais complexa que, em seguida, é compilada em um delegado de formatação de entidade. Conceitualmente, isso é semelhante à inserção do compilador. Por exemplo, uma conversão simples pode ser apenas uma compilação em conversão, em vez de uma chamada para outro método para fazer a conversão.
Comparadores de chave
A seção em segundo plano aborda por que as principais comparações podem exigir semântica especial. Certifique-se de criar um comparador apropriado para chaves ao defini-lo em uma propriedade de chave primária, principal ou estrangeira.
Use SetKeyValueComparer nos casos raros em que a semântica diferente é necessária na mesma propriedade.
Observação
SetStructuralValueComparer foi descontinuado. Use SetKeyValueComparer em seu lugar.
Substituindo o comparador padrão
Às vezes, a comparação padrão usada pelo EF Core pode não ser apropriada. Por exemplo, a mutação de matrizes de bytes não é, por padrão, detectada no EF Core. Isso pode ser substituído definindo um comparador diferente na propriedade:
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()));
O EF Core agora comparará as sequências de bytes e, portanto, detectará mutações de matriz de bytes.