Comparadores de Valor

Dica

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

Tela de fundo

Controle de alterações significa que o Entity Framework Core determina automaticamente quais alterações foram executadas pelo aplicativo em uma instância de entidade carregada, de modo que essas alterações possam ser salvas de volta no banco de dados quando SaveChanges for chamado. O Entity Framework 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 Entity Framework Core vem com logica interna para instantâneos e comparação da maioria dos tipos padrão utilizados em bancos de dados, portanto, os usuários normalmente não precisam se preocupar com esse tópico. No entanto, quando uma propriedade é mapeada por meio de um conversor de valor, o Entity Framework Core precisa executar a comparação em tipos de usuários arbitrários, o que pode ser complexo. Por padrão, o Entity Framework Core usa a comparação de igualdade padrão definida pelos tipos (por exemplo, o método Equals); para instantâneos, os tipo de valor são copiados para produzir o instantâneo, enquanto para os tipo de referência não ocorre cópia e a mesma instância é utilizada como instantâneo.

Nos casos em que o comportamento de comparação interno não seja apropriado, os usuários podem fornecer um comparador de valores, que contém a logica para instantâneo, comparação e cálculo de um código hash. Por exemplo, a seguir, você configura a conversão de valores para que a propriedade List<int> seja convertida em valor para uma cadeia de caracteres JSON no banco de dados e também define um comparador de valores 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 valores também são utilizados para determinar se dois valores de chave são os mesmos ao resolver relacionamentos; isso será explicado abaixo.

Comparação entre superficial e profundidade

Para tipos de valores pequenos e imutáveis, como int, a logica padrão do Entity Framework Core funciona bem: o valor é copiado no estado em que se encontra quando é feito o instantâneo e comparado com a comparação de igualdade interna do tipo. Ao implementar seu próprio comparador de valores, é importante considerar se a lógica de comparação profunda ou superficial (e de instantâneo) é apropriada.

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

  • Por referência, de modo que uma diferença só é detectada se uma nova matriz de bytes for utilizada
  • Por comparação profunda, de modo que a mutação dos bytes na matriz seja detectada

Por padrão, o Entity Framework Core utiliza a primeira dessas abordagens para matrizes de bytes sem chave. Ou seja, apenas 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 byte a byte ao executar SaveChanges. Isso significa que o cenário comum de substituir, por exemplo, uma imagem por outra é tratado de maneira eficiente.

Por outro lado, a igualdade de referência não funcionaria quando matrizes de bytes fossem utilizadas para representar chaves binárias, pois é muito improvável que uma propriedade FK seja definida para a mesma instância como uma propriedade PK com a qual ela precisa ser comparada. Portanto, o Entity Framework Core utiliza comparações profundas para matrizes de bytes que atuam como chaves; é improvável que isso tenha um grande impacto no desempenho, pois as chaves binárias geralmente são curtas.

Observe que a comparação escolhida e a logica de instantâneo devem corresponder uma à outra: a comparação profunda exige que o instantâneo profundo funcione corretamente.

Classes imutáveis simples

Considere uma propriedade que utiliza 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 especiais ou instantâneos porque:

  • A igualdade é substituída para que instâncias diferentes sejam comparadas corretamente
  • O tipo é imutável, portanto, não existe a possibilidade de alterar um valor de instantâneo

Portanto, nesse caso, o comportamento padrão do Entity Framework Core é bom como está.

Estruturas simples imutáveis

O mapeamento de structs simples também é simples e não exige comparadores ou instantâneos especiais.

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 Entity Framework Core dá suporte interno à geração de comparações compiladas de propriedades de struct memberwise. Isso significa que os structs não precisam ter a igualdade substituída no Entity Framework Core, mas você ainda pode optar por fazer isso por outros motivos. Também não é necessário um instantâneo especial, pois os structs são imutáveis e, de qualquer forma, são sempre copiados como membros. (Isso também é verdadeiro para structs mutáveis, mas as structs mutáveis devem ser evitadas em geral.)

Classes mutáveis

Recomenda-se que você deva usar tipos imutáveis (classes ou structs) com conversores de valor sempre que possível. Isso geralmente é mais eficiente e tem uma semântica mais limpa ao invés de utilizar um tipo mutável. No entanto, dito isso, é comum utilizar propriedades de tipos que o aplicativo não pode alterar. Por exemplo, mapeamentos de 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 da lista podem ser adicionados e removidos.

Uma conversão de valor típica em uma propriedade de lista pode converter a lista de e para 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 construtor ValueComparer<T> 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 o instantâneo de um valor

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

Do mesmo modo, 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. Se possível, seja imutável).

O instantâneo é criado pela clonagem da lista com ToList. Novamente, isso só será necessário se as listas sofrerem mutação. Se possível, seja imutável.

Observação

Os conversores e comparadores de valor são construídos utilizando expressões ao invés de delegados simples. Isso ocorre porque o Entity Framework Core insere essas expressões em uma árvore de expressões muito mais complexa que é compilada em um representante do modelador de entidades. Conceitualmente, isso é semelhante ao inlining do compilador. Por exemplo, uma conversão simples pode ser apenas uma conversão compilada, ao invés de uma chamada para outro método para fazer a conversão.

Principais comparadores

A seção em segundo plano explica por que as principais comparações podem exigir uma semântica especial. Verifique se é necessário 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 uma semântica diferente é obrigatória na mesma propriedade.

Observação

SetStructuralValueComparer ficou obsoleto. Use o SetKeyValueComparer em vez disso.

Substituição do comparador padrão

Às vezes, a comparação padrão utilizada pelo Entity Framework Core pode não ser apropriada. Por exemplo, a mutação de matrizes de bytes não é, por padrão, detectada no Entity Framework Core. Isso pode ser substituído pela definição de 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 Entity Framework Core agora comparará as sequências de bytes e, portanto, detectará as mutações de matrizes de bytes.