Partilhar via


Comparadores de valor

Sugestão

O código neste documento pode ser encontrado no GitHub como um exemplo executável.

Contexto geral

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 possui uma lógica integrada para a criação e comparação de instantâneos da maioria dos tipos padrão usados em bancos de dados, de modo que os utilizadores geralmente não precisem se preocupar com esse tópico. No entanto, quando uma propriedade é mapeada por meio de um conversor de valor, o EF Core precisa realizar a comparação em tipos de usuários arbitrários, o que pode ser complexo. Por padrão, o EF Core usa a comparação de igualdade padrão definida pelos tipos (por exemplo, o método Equals); na criação de snapshots, os tipos de valor são copiados para produzir o snapshot, enquanto para os tipos de referência nenhuma cópia ocorre, e as mesmas instâncias são usadas como snapshot.

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 instantâneo, comparação e cálculo de um código hash. Por exemplo, o seguinte configura a conversão de valor para a propriedade List<int> ser convertida em uma string JSON no banco de dados, e define também um comparador de valor apropriado:

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 as classes mutáveis abaixo para obter mais detalhes.

Observe que os comparadores de valor também são usados para determinar se dois valores-chave são os mesmos ao resolver relações; Isto é 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 é tirado um instantâneo e comparado usando a igualdade inerente do tipo. Ao implementar o seu próprio comparador de valor, é importante considerar se a lógica de comparação profunda ou superficial (e captura de instantâneos) é apropriada.

Considere matrizes de bytes, que podem ser arbitrariamente grandes. Estes podem ser comparados:

  • Por referência, de modo que uma diferença só seja detetada se uma nova matriz de bytes for usada
  • Através de uma comparação profunda, de forma a detetar a mutação dos bytes na matriz.

Por padrão, o EF Core usa a primeira dessas abordagens para matrizes de bytes não chave. Ou seja, apenas as referências são comparadas e uma alteração é detetada apenas quando uma matriz de bytes existente é substituída por uma nova. Esta é uma decisão pragmática que evita copiar matrizes inteiras e compará-las byte-a-byte durante a execução SaveChanges. Isso significa que o cenário comum de substituir, digamos, uma imagem por outra é tratado de forma performante.

Por outro lado, a igualdade de referência não funcionaria quando matrizes de bytes são usadas para representar chaves binárias, uma vez que é muito improvável que uma propriedade FK seja definida para a mesma instância que uma propriedade PK com a qual 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 impacto no desempenho, uma vez que as teclas binárias são geralmente curtas.

Note que a comparação escolhida e a lógica de criação de instantâneos devem corresponder uma à outra: a comparação profunda requer a criação de instantâneos profundos 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));

Propriedades desse tipo não precisam de comparações ou instantâneos especiais porque:

  • A igualdade é anulada para que diferentes instâncias sejam comparadas corretamente
  • O tipo é imutável, portanto, não há chance de mutar um valor de instantâneo

Então, neste caso, o comportamento padrão do EF Core é bom como está.

Estruturas simples e imutáveis

O mapeamento para estruturas simples também é simples e não requer comparadores especiais nem captura de estado.

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 integrado para gerar comparações compiladas de propriedades struct por membros. Isso significa que as estruturas 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 são imutáveis e são sempre copiadas membro a membro de qualquer forma. (Isso também é verdade para estruturas mutáveis, mas estruturas mutáveis devem, em geral, ser evitadas.)

Classes mutáveis

É recomendável usar 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 contendo os mesmos valores são tratadas como diferentes.
  • É mutável; Os valores na lista podem ser adicionados e removidos.

Uma conversão de valor típica numa propriedade de lista pode converter a lista para e de 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()));

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

Neste caso, a comparação é feita verificando se as sequências de números são as mesmas.

Da mesma forma, o código hash é construído a partir dessa mesma sequência. (Observe que este é um código hash sobre valores mutáveis e, portanto, pode causar problemas. Seja imutável em vez disso, se puder.)

O instantâneo é criado clonando a lista com ToList. Mais uma vez, isso só é 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 simples delegados. Isso ocorre porque o EF Core insere essas expressões numa árvore de expressões bem mais complexa, que é então compilada num delegado de formatação de entidades. Conceitualmente, isso é semelhante ao inlining do compilador. Por exemplo, uma conversão simples pode ser apenas um compilado em cast, em vez de uma chamada para outro método para fazer a conversão.

Principais comparadores

A seção de plano de fundo 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 raros casos em que semânticas diferentes são necessárias na mesma propriedade.

Observação

SetStructuralValueComparer tornou-se obsoleta. Utilize SetKeyValueComparer em substituição.

Sobrescrevendo 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, detetada 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á sequências de bytes e, portanto, detetará mutações na matriz de bytes.