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 ProviderClrType
e 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 é:
- Convertendo propriedades bool:
- BoolToStringConverter - Bool para cadeias de caracteres como "N" e "Y"
- BoolToTwoValuesConverter<TProvider> - Bool para qualquer dois valores
- BoolToZeroOneConverter<TProvider> - Bool a zero e um
- Convertendo propriedades de matriz de bytes:
- BytesToStringConverter - Matriz de bytes para cadeia de caracteres codificada em Base64
- Qualquer conversão que exija apenas uma conversão de tipo
- CastingConverter<TModel,TProvider> - Conversões que exigem apenas uma conversão de tipo
- Convertendo propriedades char:
- CharToStringConverter - Caractere para cadeia de caracteres único
- Convertendo propriedades de DateTimeOffset:
- DateTimeOffsetToBinaryConverter - DateTimeOffset para o valor de 64 bits codificado em binário
- DateTimeOffsetToBytesConverter - DateTimeOffset para byte array
- DateTimeOffsetToStringConverter - DateTimeOffset para cadeia de caracteres
- Convertendo propriedades de DateTime:
- DateTimeToBinaryConverter - DateTime para o valor de 64 bits, incluindo DateTimeKind
- DateTimeToStringConverter - DateTime para cadeia de caracteres
- DateTimeToTicksConverter - DateTime para tiques
- Convertendo propriedades de enumeração:
- EnumToNumberConverter<TEnum,TNumber> - Enumerar para o número subjacente
- EnumToStringConverter<TEnum> - Enumerar para cadeia de caracteres
- Convertendo propriedades de Guid:
- GuidToBytesConverter - Guid para byte array
- GuidToStringConverter - Guid para cadeia de caracteres
- Convertendo propriedades de IPAddress:
- IPAddressToBytesConverter - IPAddress para byte array
- IPAddressToStringConverter - IPAddress para cadeia de caracteres
- Convertendo propriedades numéricas (int, double, decimal, etc.):
- NumberToBytesConverter<TNumber> - Qualquer valor numérico para matriz de bytes
- NumberToStringConverter<TNumber> - Qualquer valor numérico para cadeia de caracteres
- Convertendo propriedades de PhysicalAddress:
- PhysicalAddressToBytesConverter - PhysicalAddress para byte array
- PhysicalAddressToStringConverter - PhysicalAddress para cadeia de caracteres
- Convertendo propriedades de cadeia de caracteres:
- StringToBoolConverter - Cadeias de caracteres como "N" e "Y" para bool
- StringToBytesConverter - Cadeia de caracteres para bytes UTF8
- StringToCharConverter - Cadeia de caracteres para caractere
- StringToDateTimeConverter - Cadeia de caracteres para DateTime
- StringToDateTimeOffsetConverter - Cadeia de caracteres para DateTimeOffset
- StringToEnumConverter<TEnum> - Cadeia de caracteres para enumeração
- StringToGuidConverter - Cadeia de caracteres para Guid
- StringToNumberConverter<TNumber> - Cadeia de caracteres para tipo numérico
- StringToTimeSpanConverter - Cadeia de caracteres para TimeSpan
- StringToUriConverter - Cadeia de caracteres para Uri
- Convertendo propriedades de TimeSpan:
- TimeSpanToStringConverter - TimeSpan para cadeia de caracteres
- TimeSpanToTicksConverter - TimeSpan para tiques
- Convertendo propriedades de Uri:
- UriToStringConverter - Uri para cadeia de caracteres
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
/timestamp
8 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.