Compartilhar via


Conversões de valor

Conversores de valor permitem que valores de propriedade sejam convertidos ao ler ou gravar no banco de dados. Essa conversão pode ser de um valor para outro do mesmo tipo (por exemplo, criptografando cadeias de caracteres) ou de um valor de um tipo para um valor de outro tipo (por exemplo, convertendo valores de enumeração de e para cadeias de caracteres no banco de dados).

Dica

Você pode executar e depurar em todo o código neste documento baixando o código de exemplo do GitHub.

Visão geral

Conversores de valor são especificados em termos de um ModelClrType e um ProviderClrType. O tipo de modelo é o tipo .NET da propriedade no tipo de entidade. O tipo de provedor é o tipo .NET compreendido pelo provedor de banco de dados. Por exemplo, para salvar enumerações como cadeias de caracteres no banco de dados, o tipo de modelo é o tipo de enumerações e o tipo de provedor é String. Esses dois tipos podem ser os mesmos.

As conversões são definidas usando duas árvores de expressão Func: uma de ModelClrType para ProviderClrType e outra de ProviderClrType para ModelClrType. As árvores de expressão são usadas para que possam ser compiladas no delegado de acesso ao banco de dados para conversões eficientes. A árvore de expressão pode conter uma chamada simples para um método de conversão para conversões complexas.

Observação

Uma propriedade que foi configurada para conversão de valor também pode precisar especificar uma ValueComparer<T>. Veja os exemplos abaixo e a documentação comparadores de valor para obter mais informações.

Configurando um conversor de valor

As conversões de valor são configuradas em DbContext.OnModelCreating. Por exemplo, considere um tipo de enumeração e entidade definido como:

public class Rider
{
    public int Id { get; set; }
    public EquineBeast Mount { get; set; }
}

public enum EquineBeast
{
    Donkey,
    Mule,
    Horse,
    Unicorn
}

As conversões podem ser configuradas em OnModelCreating para armazenar os valores de enumeração como cadeias de caracteres como "Burro", "Mula", etc. no banco de dados; você simplesmente precisa fornecer uma função que converte do ModelClrType para o ProviderClrTypee outra para a conversão oposta:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder
        .Entity<Rider>()
        .Property(e => e.Mount)
        .HasConversion(
            v => v.ToString(),
            v => (EquineBeast)Enum.Parse(typeof(EquineBeast), v));
}

Observação

Um valor null nunca será passado para um conversor de valor. Um valor nulo em uma coluna de banco de dados é sempre nulo na instância da entidade e vice-versa. Isso facilita a implementação de conversões e permite que elas sejam compartilhadas entre propriedades anuláveis e não anuláveis. Consulte o problema do GitHub nº 13850 para obter mais informações.

Configurando em massa um conversor de valor

É comum que o mesmo conversor de valor seja configurado para cada propriedade que usa o tipo CLR relevante. Em vez de fazer isso manualmente para cada propriedade, você pode usar configuração de modelo de pré-convenção para fazer isso uma vez para todo o modelo. Para fazer isso, defina o conversor de valor como uma classe:

public class CurrencyConverter : ValueConverter<Currency, decimal>
{
    public CurrencyConverter()
        : base(
            v => v.Amount,
            v => new Currency(v))
    {
    }
}

Em seguida, substitua ConfigureConventions no tipo de contexto e configure o conversor da seguinte maneira:

protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
    configurationBuilder
        .Properties<Currency>()
        .HaveConversion<CurrencyConverter>();
}

Conversões predefinidas

O EF Core contém muitas conversões predefinidas que evitam a necessidade de gravar funções de conversão manualmente. Em vez disso, o EF Core escolherá a conversão a ser usada com base no tipo de propriedade no modelo e no tipo de provedor de banco de dados solicitado.

Por exemplo, conversões de enumeração em cadeia de caracteres são usadas como um exemplo acima, mas o EF Core realmente fará isso automaticamente quando o tipo de provedor é configurado como string usando o tipo genérico de HasConversion:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder
        .Entity<Rider>()
        .Property(e => e.Mount)
        .HasConversion<string>();
}

A mesma coisa pode ser obtida especificando explicitamente o tipo de coluna de banco de dados. Por exemplo, se o tipo de entidade for definido da seguinte forma:

public class Rider2
{
    public int Id { get; set; }

    [Column(TypeName = "nvarchar(24)")]
    public EquineBeast Mount { get; set; }
}

Em seguida, os valores de enumeração serão salvos como cadeias de caracteres no banco de dados sem nenhuma configuração adicional em OnModelCreating.

A classe ValueConverter

Chamar HasConversion conforme mostrado acima criará uma instância ValueConverter<TModel,TProvider> e a definirá na propriedade. O ValueConverter pode ser criado explicitamente. Por exemplo:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    var converter = new ValueConverter<EquineBeast, string>(
        v => v.ToString(),
        v => (EquineBeast)Enum.Parse(typeof(EquineBeast), v));

    modelBuilder
        .Entity<Rider>()
        .Property(e => e.Mount)
        .HasConversion(converter);
}

Isso pode ser útil quando várias propriedades usam a mesma conversão.

Conversores internos

Conforme mencionado acima, o EF Core é fornecido com um conjunto de classes de ValueConverter<TModel,TProvider> predefinidas, encontradas no namespace Microsoft.EntityFrameworkCore.Storage.ValueConversion. Em muitos casos, o EF escolherá o conversor interno apropriado com base no tipo da propriedade no modelo e no tipo solicitado no banco de dados, conforme mostrado acima para enumerações. Por exemplo, usar .HasConversion<int>() em uma propriedade bool fará com que o EF Core converta valores bool em valores numéricos zero e um:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder
        .Entity<User>()
        .Property(e => e.IsActive)
        .HasConversion<int>();
}

Isso é funcionalmente o mesmo que criar uma instância do BoolToZeroOneConverter<TProvider> interno e defini-la explicitamente:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    var converter = new BoolToZeroOneConverter<int>();

    modelBuilder
        .Entity<User>()
        .Property(e => e.IsActive)
        .HasConversion(converter);
}

A tabela a seguir resume conversões predefinidas comumente usadas de tipos de modelo/propriedade para tipos de provedor de banco de dados. Na tabela any_numeric_type significa um dos int, short, long, byte, uint, ushort, ulong, sbyte, char, decimal, float ou double.

Tipo de modelo/propriedade Tipo de provedor/banco de dados Conversão Uso
bool any_numeric_type False/true para 0/1 .HasConversion<any_numeric_type>()
any_numeric_type False/true para qualquer dois números Use BoolToTwoValuesConverter<TProvider>.
string False/true para "N"/"Y" .HasConversion<string>()
string False/true para duas cadeias de caracteres Use BoolToStringConverter.
any_numeric_type bool 0/1 para false/true .HasConversion<bool>()
any_numeric_type Conversão simples .HasConversion<any_numeric_type>()
string O número como uma cadeia de caracteres .HasConversion<string>()
Enumeração any_numeric_type O valor numérico da enumeração .HasConversion<any_numeric_type>()
string A representação de cadeia de caracteres do valor de enumeração .HasConversion<string>()
string bool Analisa a cadeia de caracteres como um bool .HasConversion<bool>()
any_numeric_type Analisa a cadeia de caracteres como o tipo numérico fornecido .HasConversion<any_numeric_type>()
char O primeiro caractere da cadeia de caracteres .HasConversion<char>()
DateTime Analisa a cadeia de caracteres como um DateTime .HasConversion<DateTime>()
DateTimeOffset Analisa a cadeia de caracteres como um DateTimeOffset .HasConversion<DateTimeOffset>()
TimeSpan Analisa a cadeia de caracteres como um TimeSpan .HasConversion<TimeSpan>()
Guid Analisa a cadeia de caracteres como um Guid .HasConversion<Guid>()
byte[] A cadeia de caracteres como bytes UTF8 .HasConversion<byte[]>()
char string Uma única cadeia de caracteres .HasConversion<string>()
DateTime longo Data/hora codificada preservando DateTime.Kind .HasConversion<long>()
longo Tiques Use DateTimeToTicksConverter.
string Cadeia de caracteres de data/hora de cultura invariável .HasConversion<string>()
DateTimeOffset longo Data/hora codificada com deslocamento .HasConversion<long>()
string Cadeia de caracteres de data/hora de cultura invariável com deslocamento .HasConversion<string>()
TimeSpan longo Tiques .HasConversion<long>()
string Cadeia de caracteres de intervalo de tempo de cultura invariável .HasConversion<string>()
Uri string O URI como uma cadeia de caracteres .HasConversion<string>()
PhysicalAddress string O endereço como uma cadeia de caracteres .HasConversion<string>()
byte[] Bytes na ordem de rede big-endian .HasConversion<byte[]>()
IPAddress string O endereço como uma cadeia de caracteres .HasConversion<string>()
byte[] Bytes na ordem de rede big-endian .HasConversion<byte[]>()
Guid string O GUID no formato 'dddddddd-dddd-dddd-dddd-dd' .HasConversion<string>()
byte[] Bytes na ordem de serialização binária do .NET .HasConversion<byte[]>()

Observe que essas conversões pressupõem que o formato do valor é apropriado para a conversão. Por exemplo, a conversão de cadeias de caracteres em números falhará se os valores da cadeia de caracteres não puderem ser analisados como números.

A lista completa de conversores internos é:

Observe que todos os conversores internos são sem estado e, portanto, uma única instância pode ser compartilhada com segurança por várias propriedades.

Facetas de coluna e dicas de mapeamento

Alguns tipos de banco de dados têm facetas que modificam como os dados são armazenados. Estão incluídos:

  • Precisão e escala para decimais e colunas de data/hora
  • Tamanho/comprimento para colunas binárias e de cadeia de caracteres
  • Unicode para colunas de cadeia de caracteres

Essas facetas podem ser configuradas da maneira normal para uma propriedade que usa um conversor de valor e serão aplicadas ao tipo de banco de dados convertido. Por exemplo, ao converter de uma enumeração em cadeias de caracteres, podemos especificar que a coluna de banco de dados deve ser não Unicode e armazenar até 20 caracteres:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder
        .Entity<Rider>()
        .Property(e => e.Mount)
        .HasConversion<string>()
        .HasMaxLength(20)
        .IsUnicode(false);
}

Ou, ao criar o conversor explicitamente:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    var converter = new ValueConverter<EquineBeast, string>(
        v => v.ToString(),
        v => (EquineBeast)Enum.Parse(typeof(EquineBeast), v));

    modelBuilder
        .Entity<Rider>()
        .Property(e => e.Mount)
        .HasConversion(converter)
        .HasMaxLength(20)
        .IsUnicode(false);
}

Isso resulta em uma coluna varchar(20) ao usar migrações do EF Core no SQL Server:

CREATE TABLE [Rider] (
    [Id] int NOT NULL IDENTITY,
    [Mount] varchar(20) NOT NULL,
    CONSTRAINT [PK_Rider] PRIMARY KEY ([Id]));

No entanto, se por padrão todas as colunas EquineBeast devem ser varchar(20), essas informações poderão ser fornecidas ao conversor de valor como um ConverterMappingHints. Por exemplo:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    var converter = new ValueConverter<EquineBeast, string>(
        v => v.ToString(),
        v => (EquineBeast)Enum.Parse(typeof(EquineBeast), v),
        new ConverterMappingHints(size: 20, unicode: false));

    modelBuilder
        .Entity<Rider>()
        .Property(e => e.Mount)
        .HasConversion(converter);
}

Agora, sempre que esse conversor for usado, a coluna de banco de dados não será unicode com um comprimento máximo de 20. No entanto, essas são apenas dicas, pois são substituídas por todas as facetas definidas explicitamente na propriedade mapeada.

Exemplos

Objetos de valor simples

Este exemplo usa um tipo simples para encapsular um tipo primitivo. Isso pode ser útil quando você deseja que o tipo em seu modelo seja mais específico (e, portanto, mais seguro de tipo) do que um tipo primitivo. Neste exemplo, esse tipo é Dollars, que encapsula o primitivo decimal:

public readonly struct Dollars
{
    public Dollars(decimal amount) 
        => Amount = amount;
        
    public decimal Amount { get; }

    public override string ToString() 
        => $"${Amount}";
}

Isso pode ser usado em um tipo de entidade:

public class Order
{
    public int Id { get; set; }

    public Dollars Price { get; set; }
}

E convertido no subjacente decimal quando armazenado no banco de dados:

modelBuilder.Entity<Order>()
    .Property(e => e.Price)
    .HasConversion(
        v => v.Amount,
        v => new Dollars(v));

Observação

Esse objeto de valor é implementado como um struct readonly. Isso significa que o EF Core pode instantâneo e comparar valores sem problemas. Consulte Value Comparers para obter mais informações.

Objetos de valor composto

No exemplo anterior, o tipo de objeto de valor continha apenas uma única propriedade. É mais comum que um tipo de objeto de valor componha várias propriedades que, juntas, formam um conceito de domínio. Por exemplo, um tipo de Money geral que contém o valor e a moeda:

public readonly struct Money
{
    [JsonConstructor]
    public Money(decimal amount, Currency currency)
    {
        Amount = amount;
        Currency = currency;
    }

    public override string ToString()
        => (Currency == Currency.UsDollars ? "$" : "£") + Amount;

    public decimal Amount { get; }
    public Currency Currency { get; }
}

public enum Currency
{
    UsDollars,
    PoundsSterling
}

Esse objeto de valor pode ser usado em um tipo de entidade como antes:

public class Order
{
    public int Id { get; set; }

    public Money Price { get; set; }
}

Atualmente, os conversores de valor só podem converter valores de e para uma única coluna de banco de dados. Essa limitação significa que todos os valores de propriedade do objeto devem ser codificados em um único valor de coluna. Normalmente, isso é feito serializando o objeto à medida que ele entra no banco de dados e, em seguida, desserializando-o novamente ao sair. Por exemplo, usando System.Text.Json:

modelBuilder.Entity<Order>()
    .Property(e => e.Price)
    .HasConversion(
        v => JsonSerializer.Serialize(v, (JsonSerializerOptions)null),
        v => JsonSerializer.Deserialize<Money>(v, (JsonSerializerOptions)null));

Observação

Planejamos permitir o mapeamento de um objeto para várias colunas em uma versão futura do EF Core, removendo a necessidade de usar a serialização aqui. Isso é acompanhado pelo problema do GitHub nº 13947.

Observação

Assim como no exemplo anterior, esse objeto de valor é implementado como um struct readonly. Isso significa que o EF Core pode instantâneo e comparar valores sem problemas. Consulte Value Comparers para obter mais informações.

Coleções de primitivos

A serialização também pode ser usada para armazenar uma coleção de valores primitivos. Por exemplo:

public class Post
{
    public int Id { get; set; }
    public string Title { get; set; }
    public string Contents { get; set; }

    public ICollection<string> Tags { get; set; }
}

Usando System.Text.Json novamente:

modelBuilder.Entity<Post>()
    .Property(e => e.Tags)
    .HasConversion(
        v => JsonSerializer.Serialize(v, (JsonSerializerOptions)null),
        v => JsonSerializer.Deserialize<List<string>>(v, (JsonSerializerOptions)null),
        new ValueComparer<ICollection<string>>(
            (c1, c2) => c1.SequenceEqual(c2),
            c => c.Aggregate(0, (a, v) => HashCode.Combine(a, v.GetHashCode())),
            c => (ICollection<string>)c.ToList()));

ICollection<string> representa um tipo de referência mutável. Isso significa que um ValueComparer<T> é necessário para que o EF Core possa rastrear e detectar alterações corretamente. Consulte Value Comparers para obter mais informações.

Coleções de objetos de valor

Combinando os dois exemplos anteriores juntos, podemos criar uma coleção de objetos de valor. Por exemplo, considere um tipo AnnualFinance que modela as finanças do blog por um único ano:

public readonly struct AnnualFinance
{
    [JsonConstructor]
    public AnnualFinance(int year, Money income, Money expenses)
    {
        Year = year;
        Income = income;
        Expenses = expenses;
    }

    public int Year { get; }
    public Money Income { get; }
    public Money Expenses { get; }
    public Money Revenue => new Money(Income.Amount - Expenses.Amount, Income.Currency);
}

Esse tipo compõe vários dos tipos de Money que criamos anteriormente:

public readonly struct Money
{
    [JsonConstructor]
    public Money(decimal amount, Currency currency)
    {
        Amount = amount;
        Currency = currency;
    }

    public override string ToString()
        => (Currency == Currency.UsDollars ? "$" : "£") + Amount;

    public decimal Amount { get; }
    public Currency Currency { get; }
}

public enum Currency
{
    UsDollars,
    PoundsSterling
}

Em seguida, podemos adicionar uma coleção de AnnualFinance ao nosso tipo de entidade:

public class Blog
{
    public int Id { get; set; }
    public string Name { get; set; }

    public IList<AnnualFinance> Finances { get; set; }
}

E use novamente a serialização para armazenar este:

modelBuilder.Entity<Blog>()
    .Property(e => e.Finances)
    .HasConversion(
        v => JsonSerializer.Serialize(v, (JsonSerializerOptions)null),
        v => JsonSerializer.Deserialize<List<AnnualFinance>>(v, (JsonSerializerOptions)null),
        new ValueComparer<IList<AnnualFinance>>(
            (c1, c2) => c1.SequenceEqual(c2),
            c => c.Aggregate(0, (a, v) => HashCode.Combine(a, v.GetHashCode())),
            c => (IList<AnnualFinance>)c.ToList()));

Observação

Como antes, essa conversão requer um ValueComparer<T>. Consulte Value Comparers para obter mais informações.

Objetos de valor como chaves

Às vezes, as propriedades de chave primitiva podem ser encapsuladas em objetos de valor para adicionar um nível adicional de segurança de tipo na atribuição de valores. Por exemplo, poderíamos implementar um tipo de chave para blogs e um tipo de chave para postagens:

public readonly struct BlogKey
{
    public BlogKey(int id) => Id = id;
    public int Id { get; }
}

public readonly struct PostKey
{
    public PostKey(int id) => Id = id;
    public int Id { get; }
}

Em seguida, eles podem ser usados no modelo de domínio:

public class Blog
{
    public BlogKey Id { get; set; }
    public string Name { get; set; }

    public ICollection<Post> Posts { get; set; }
}

public class Post
{
    public PostKey Id { get; set; }

    public string Title { get; set; }
    public string Content { get; set; }

    public BlogKey? BlogId { get; set; }
    public Blog Blog { get; set; }
}

Observe que Blog.Id não pode receber acidentalmente um PostKey e Post.Id não pode receber acidentalmente um BlogKey. Da mesma forma, a propriedade de chave estrangeira Post.BlogId deve ser atribuída a um BlogKey.

Observação

Mostrar esse padrão não significa que o recomendamos. Considere cuidadosamente se esse nível de abstração está ajudando ou dificultando sua experiência de desenvolvimento. Além disso, considere o uso de navegações e chaves geradas em vez de lidar diretamente com os valores das chaves.

Essas propriedades de chave podem ser mapeadas usando conversores de valor:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    var blogKeyConverter = new ValueConverter<BlogKey, int>(
        v => v.Id,
        v => new BlogKey(v));

    modelBuilder.Entity<Blog>().Property(e => e.Id).HasConversion(blogKeyConverter);

    modelBuilder.Entity<Post>(
        b =>
        {
            b.Property(e => e.Id).HasConversion(v => v.Id, v => new PostKey(v));
            b.Property(e => e.BlogId).HasConversion(blogKeyConverter);
        });
}

Observação

As propriedades de chave com conversões só podem usar valores de chave gerados a partir do EF Core 7.0.

Usar ulong para timestamp/rowversion

O SQL Server oferece suporte à simultaneidade otimista automática usando colunas binárias de rowversion/timestamp8 bytes. Eles são sempre lidos e gravados no banco de dados usando uma matriz de 8 bytes. No entanto, as matrizes de bytes são um tipo de referência mutável, o que as torna um pouco dolorosas para lidar. Conversores de valor permitem que o rowversion seja mapeado para uma propriedade ulong, que é muito mais apropriada e fácil de usar do que a matriz de bytes. Por exemplo, considere uma entidade Blog com um token de simultaneidade ulong:

public class Blog
{
    public int Id { get; set; }
    public string Name { get; set; }
    public ulong Version { get; set; }
}

Isso pode ser mapeado para uma coluna de rowversion do SQL Server usando um conversor de valor:

modelBuilder.Entity<Blog>()
    .Property(e => e.Version)
    .IsRowVersion()
    .HasConversion<byte[]>();

Especificar o DateTime.Kind ao ler datas

O SQL Server descarta o sinalizador de DateTime.Kind ao armazenar um DateTime como um datetime ou datetime2. Isso significa que os valores datetime que voltam do banco de dados sempre têm uma DateTimeKind de Unspecified.

Conversores de valor podem ser usados de duas maneiras para lidar com isso. Primeiro, o EF Core tem um conversor de valor que cria um valor opaco de 8 bytes que preserva o sinalizador Kind. Por exemplo:

modelBuilder.Entity<Post>()
    .Property(e => e.PostedOn)
    .HasConversion<long>();

Isso permite que valores DateTime com diferentes sinalizadores de Kind sejam misturados no banco de dados.

O problema com essa abordagem é que o banco de dados não tem mais colunas datetime ou datetime2 reconhecíveis. Portanto, em vez disso, é comum sempre armazenar a hora UTC (ou, menos comumente, sempre hora local) e, em seguida, ignorar o sinalizador Kind ou defini-lo para o valor apropriado usando um conversor de valor. Por exemplo, o conversor a seguir garante que o valor DateTime lido do banco de dados terá o DateTimeKind UTC:

modelBuilder.Entity<Post>()
    .Property(e => e.LastUpdated)
    .HasConversion(
        v => v,
        v => new DateTime(v.Ticks, DateTimeKind.Utc));

Se uma combinação de valores locais e UTC estiver sendo definida em instâncias de entidade, o conversor poderá ser usado para converter adequadamente antes de inserir. Por exemplo:

modelBuilder.Entity<Post>()
    .Property(e => e.LastUpdated)
    .HasConversion(
        v => v.ToUniversalTime(),
        v => new DateTime(v.Ticks, DateTimeKind.Utc));

Observação

Considere cuidadosamente unificar todo o código de acesso de banco de dados para usar o tempo UTC o tempo todo, apenas lidando com a hora local ao apresentar dados aos usuários.

Usar chaves de cadeia de caracteres que não diferenciam maiúsculas de minúsculas

Alguns bancos de dados, incluindo o SQL Server, executam comparações de cadeia de caracteres que não diferenciam maiúsculas de minúsculas por padrão. O .NET, por outro lado, executa comparações de cadeia de caracteres que diferenciam maiúsculas de minúsculas por padrão. Isso significa que um valor de chave estrangeira como "DotNet" corresponderá ao valor da chave primária "dotnet" no SQL Server, mas não o corresponderá no EF Core. Um comparador de valor para chaves pode ser usado para forçar o EF Core a comparações de cadeia de caracteres que não diferenciam maiúsculas de minúsculas, como no banco de dados. Por exemplo, considere um modelo de blog/postagens com chaves de cadeia de caracteres:

public class Blog
{
    public string Id { get; set; }
    public string Name { get; set; }

    public ICollection<Post> Posts { get; set; }
}

public class Post
{
    public string Id { get; set; }
    public string Title { get; set; }
    public string Content { get; set; }

    public string BlogId { get; set; }
    public Blog Blog { get; set; }
}

Isso não funcionará conforme o esperado se alguns dos valores de Post.BlogId tiverem maiúsculas e minúsculas diferentes. Os erros causados por isso dependerão do que o aplicativo está fazendo, mas normalmente envolvem grafos de objetos que não são corrigidos corretamente e/ou atualizações que falham porque o valor do FK está errado. Um comparador de valor pode ser usado para corrigir isso:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    var comparer = new ValueComparer<string>(
        (l, r) => string.Equals(l, r, StringComparison.OrdinalIgnoreCase),
        v => v.ToUpper().GetHashCode(),
        v => v);

    modelBuilder.Entity<Blog>()
        .Property(e => e.Id)
        .Metadata.SetValueComparer(comparer);

    modelBuilder.Entity<Post>(
        b =>
        {
            b.Property(e => e.Id).Metadata.SetValueComparer(comparer);
            b.Property(e => e.BlogId).Metadata.SetValueComparer(comparer);
        });
}

Observação

Comparações de cadeia de caracteres do .NET e comparações de cadeia de caracteres de banco de dados podem diferir em mais do que apenas diferenciação de maiúsculas de minúsculas. Esse padrão funciona para chaves ASCII simples, mas pode falhar em chaves com qualquer tipo de caractere específico à cultura. Consulte Ordenações e Diferenciação de Maiúsculas e Minúsculas para obter mais informações.

Manipular cadeias de caracteres de banco de dados de comprimento fixo

O exemplo anterior não precisava de um conversor de valor. No entanto, um conversor pode ser útil para tipos de cadeia de caracteres de banco de dados de comprimento fixo como char(20) ou nchar(20). Cadeias de caracteres de comprimento fixo são adicionadas ao comprimento completo sempre que um valor é inserido no banco de dados. Isso significa que um valor chave de "dotnet" será lido novamente do banco de dados como "dotnet..............", em que . representa um caractere de espaço. Em seguida, isso não será comparado corretamente com valores de chave que não são adicionados.

Um conversor de valor pode ser usado para cortar o preenchimento ao ler valores de chave. Isso pode ser combinado com o comparador de valor no exemplo anterior para comparar corretamente as chaves ASCII que não diferenciam maiúsculas de minúsculas. Por exemplo:

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    var converter = new ValueConverter<string, string>(
        v => v,
        v => v.Trim());

    var comparer = new ValueComparer<string>(
        (l, r) => string.Equals(l, r, StringComparison.OrdinalIgnoreCase),
        v => v.ToUpper().GetHashCode(),
        v => v);

    modelBuilder.Entity<Blog>()
        .Property(e => e.Id)
        .HasColumnType("char(20)")
        .HasConversion(converter, comparer);

    modelBuilder.Entity<Post>(
        b =>
        {
            b.Property(e => e.Id).HasColumnType("char(20)").HasConversion(converter, comparer);
            b.Property(e => e.BlogId).HasColumnType("char(20)").HasConversion(converter, comparer);
        });
}

Criptografar valores de propriedade

Conversores de valor podem ser usados para criptografar valores de propriedade antes de enviá-los para o banco de dados e descriptografá-los na saída. Por exemplo, usar a reversão de cadeia de caracteres como um substituto para um algoritmo de criptografia real:

modelBuilder.Entity<User>().Property(e => e.Password).HasConversion(
    v => new string(v.Reverse().ToArray()),
    v => new string(v.Reverse().ToArray()));

Observação

No momento, não há como obter uma referência ao DbContext atual ou a outro estado de sessão de dentro de um conversor de valor. Isso limita os tipos de criptografia que podem ser usados. Vote no problema do GitHub nº 11597 para que essa limitação seja removida.

Aviso

Certifique-se de entender todas as implicações se você rolar sua própria criptografia para proteger dados confidenciais. Considere, em vez disso, usar mecanismos de criptografia pré-criados, como o Always Encrypted no SQL Server.

Limitações

Há algumas limitações atuais conhecidas do sistema de conversão de valor:

  • Conforme observado acima, null não é possível converter. Vote (👍) para o problema do GitHub nº 13850 se isso for algo de que você precisa.
  • Não é possível consultar em propriedades convertidas em valor, por exemplo, referenciar membros no tipo .NET convertido em valor em suas consultas LINQ. Vote (👍) para o problema do GitHub nº 10434 se isso for algo necessário, mas considerando usar uma coluna JSON em vez disso.
  • No momento, não há como espalhar uma conversão de uma propriedade em várias colunas ou vice-versa. Vote (👍) para o problema do GitHub nº 13947 se isso for algo de que você precisa.
  • Não há suporte para a geração de valor para a maioria das chaves mapeadas por meio de conversores de valor. Vote (👍) para o problema do GitHub nº 11597 se isso for algo necessário.
  • Conversões de valor não podem referenciar a instância DbContext atual. Vote (👍) para o problema do GitHub nº 12205 se isso for algo necessário.
  • Os parâmetros que usam tipos convertidos por valor não podem ser usados atualmente em APIs SQL brutas. Vote (👍) para o problema do GitHub nº 27534 se isso for algo necessário.

A remoção dessas limitações está sendo considerada para versões futuras.