Nota
O acesso a esta página requer autorização. Podes tentar iniciar sessão ou mudar de diretório.
O acesso a esta página requer autorização. Podes tentar mudar de diretório.
Os conversores de valor permitem que os 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 enum de e para cadeias de caracteres no banco de dados).
Sugestão
Você pode executar e depurar todo o código neste documento baixando o código de exemplo do GitHub.
Visão geral
Os conversores de valor são especificados em termos de a ModelClrType e a ProviderClrType. O tipo de modelo é o tipo .NET da propriedade no tipo de entidade. O tipo de provedor é o tipo .NET entendido pelo provedor de banco de dados. Por exemplo, para salvar enums como cadeias de caracteres no banco de dados, o tipo de modelo é o tipo de enum, e o tipo de provedor é String. Estes dois tipos podem ser os mesmos.
As conversões são definidas usando duas Func árvores de expressão: 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ões 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 pode também precisar especificar um ValueComparer<T>. Consulte os exemplos abaixo e a documentação dos 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 enum e um tipo de entidade definidos 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 enum como strings, tais como "Burro", "Mula", etc., na base de dados; basta fornecer uma função que converta de ModelClrType para 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 null valor nunca será passado para um conversor de valor. Um nulo em uma coluna de banco de dados é sempre um 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 a edição #13850 do GitHub para obter mais informações.
Configuração em lote de 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 a configuração do modelo pré-convenção para fazer isso uma vez para todo o modelo. Para fazer isso, defina seu conversor de valor como uma classe:
public class CurrencyConverter : ValueConverter<Currency, decimal>
{
public CurrencyConverter()
: base(
v => v.Amount,
v => new Currency(v))
{
}
}
Em seguida, sobreponha o tipo de contexto ConfigureConventions e configure o conversor da seguinte maneira:
protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
configurationBuilder
.Properties<Currency>()
.HaveConversion<CurrencyConverter>();
}
Conversões pré-definidas
O EF Core contém muitas conversões predefinidas que evitam a necessidade de escrever 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, as conversões de enum para string são usadas como exemplo acima, mas o EF Core realmente fará isso automaticamente quando o tipo de provedor for 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 alcançada especificando explicitamente o tipo de coluna do banco de dados. Por exemplo, se o tipo de entidade for definido assim:
public class Rider2
{
public int Id { get; set; }
[Column(TypeName = "nvarchar(24)")]
public EquineBeast Mount { get; set; }
}
Em seguida, os valores enum serão salvos como strings no banco de dados sem qualquer configuração adicional no OnModelCreating.
Classe ValueConverter
Chamar HasConversion conforme mostrado acima criará uma instância de ValueConverter<TModel,TProvider> e a definirá na propriedade. Em vez disso, 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 integrados
Como mencionado acima, o EF Core vem com um conjunto de classes ValueConverter<TModel,TProvider> predefinidas, encontradas no Microsoft.EntityFrameworkCore.Storage.ValueConversion namespace. Em muitos casos, a EF escolherá o conversor interno apropriado com base no tipo da propriedade no modelo e no tipo solicitado no banco de dados, como mostrado acima para enums. Por exemplo, usar .HasConversion<int>() em uma bool propriedade fará com que o EF Core converta valores bool em valores zero e um numéricos.
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 built-in BoolToZeroOneConverter<TProvider> e defini-lo 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, ushortulong, , sbyte, char, , decimalfloat, ou double.
| Modelo/tipo de propriedade | Tipo de provedor/banco de dados | Conversão | Utilização |
|---|---|---|---|
| Bool | qualquer_tipo_numérico | Falso/verdadeiro para 0/1 | .HasConversion<any_numeric_type>() |
| qualquer_tipo_numérico | Falso/verdadeiro para quaisquer dois números | Utilize BoolToTwoValuesConverter<TProvider> | |
| cadeia (de caracteres) | Falso/verdadeiro para "N"/"Y" | .HasConversion<string>() |
|
| cadeia (de caracteres) | False/true para quaisquer duas cadeias de caracteres | Utilize BoolToStringConverter | |
| qualquer_tipo_numérico | Bool | 0/1 como falso/verdadeiro | .HasConversion<bool>() |
| qualquer_tipo_numérico | Transmissão simples | .HasConversion<any_numeric_type>() |
|
| cadeia (de caracteres) | O número como uma cadeia de caracteres | .HasConversion<string>() |
|
| Enum | qualquer_tipo_numérico | O valor numérico do enum | .HasConversion<any_numeric_type>() |
| cadeia (de caracteres) | A representação de cadeia de caracteres do valor enum | .HasConversion<string>() |
|
| cadeia (de caracteres) | Bool | Analisa a string como um booleano | .HasConversion<bool>() |
| qualquer_tipo_numérico | Interpreta a cadeia de caracteres como o tipo numérico fornecido | .HasConversion<any_numeric_type>() |
|
| char | O primeiro caractere da cadeia de caracteres | .HasConversion<char>() |
|
| Data e Hora | Analisa a cadeia de texto como uma data e hora | .HasConversion<DateTime>() |
|
| Desvio de Data e Hora | Analisa a cadeia de caracteres como um DateTimeOffset | .HasConversion<DateTimeOffset>() |
|
| Intervalo de Tempo | Analisa a cadeia de caracteres como um TimeSpan | .HasConversion<TimeSpan>() |
|
| Guia | Analisa a cadeia de caracteres como um Guid | .HasConversion<Guid>() |
|
| byte[] | A cadeia de caracteres como bytes UTF8 | .HasConversion<byte[]>() |
|
| char | cadeia (de caracteres) | Uma única cadeia de caracteres | .HasConversion<string>() |
| Data e Hora | longo | Data/hora codificada preservando DateTime.Kind | .HasConversion<long>() |
| longo | Carrapatos | Utilize DateTimeToTicksConverter | |
| cadeia (de caracteres) | Cadeia de cultura invariante de data/hora | .HasConversion<string>() |
|
| Desvio de Data e Hora | longo | Data/hora codificada com deslocamento | .HasConversion<long>() |
| cadeia (de caracteres) | Cadeia de data e hora de cultura invariável com desvio | .HasConversion<string>() |
|
| Intervalo de Tempo | longo | Carrapatos | .HasConversion<long>() |
| cadeia (de caracteres) | Cadeia de tempo de cultura invariante | .HasConversion<string>() |
|
| Uri | cadeia (de caracteres) | O URI como uma cadeia de caracteres | .HasConversion<string>() |
| Endereço Físico | cadeia (de caracteres) | O endereço como uma cadeia de caracteres | .HasConversion<string>() |
| byte[] | Bytes em ordem de rede big-endian | .HasConversion<byte[]>() |
|
| Endereço de IP | cadeia (de caracteres) | O endereço como uma cadeia de caracteres | .HasConversion<string>() |
| byte[] | Bytes em ordem de rede big-endian | .HasConversion<byte[]>() |
|
| Guia | cadeia (de caracteres) | O GUID no formato 'dddddddd-dddd-dddd-dddd-dddddddddddd' | .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 embutidos é:
- Convertendo propriedades booleanas:
- BoolToStringConverter - Bool para strings de texto como "N" e "Y"
- BoolToTwoValuesConverter<TProvider> - Bool que assume qualquer um de dois valores
- BoolToZeroOneConverter<TProvider> - Bool para zero e um
- Convertendo propriedades de matriz de bytes:
- BytesToStringConverter - Matriz de bytes para cadeia codificada em Base64
- Qualquer conversão que exija apenas uma conversão de tipo
- CastingConverter<TModel,TProvider> - Conversões que requerem apenas um tipo cast
- Convertendo propriedades de caracteres:
- CharToStringConverter - Char para cadeia de um único caractere
- Convertendo DateTimeOffset propriedades:
- DateTimeOffsetToBinaryConverter - DateTimeOffset para valor de 64 bits codificado em binário
- DateTimeOffsetToBytesConverter - DateTimeOffset para matriz de bytes
- DateTimeOffsetToStringConverter - DateTimeOffset para cadeia de caracteres
- Convertendo DateTime propriedades:
- DateTimeToBinaryConverter - DateTime para um valor de 64 bits que inclui DateTimeKind
- DateTimeToStringConverter - DateTime para cadeia de caracteres
- DateTimeToTicksConverter - DateTime aos carrapatos
- Convertendo propriedades de enum:
- EnumToNumberConverter<TEnum,TNumber> - Enum para o número subjacente
- EnumToStringConverter<TEnum> - Enum para string
- Convertendo Guid propriedades:
- GuidToBytesConverter - Guid para matriz de bytes
- GuidToStringConverter - Guid para cadeia de caracteres
- Convertendo IPAddress propriedades:
- IPAddressToBytesConverter - IPAddress para matriz de bytes
- IPAddressToStringConverter - IPAddress para cadeia de caracteres
- Conversão de propriedades numéricas (int, double, decimal, etc.):
- NumberToBytesConverter<TNumber> - Qualquer valor numérico para matriz de bytes
- NumberToStringConverter<TNumber> - Qualquer valor numérico para string
- Convertendo PhysicalAddress propriedades:
- PhysicalAddressToBytesConverter - PhysicalAddress para matriz de bytes
- PhysicalAddressToStringConverter - PhysicalAddress para cadeia de caracteres
- Convertendo propriedades de cadeia de caracteres:
- StringToBoolConverter - Strings como "N" e "Y" para bool
- StringToBytesConverter - String para UTF8 bytes
- StringToCharConverter - String para personagem
- StringToDateTimeConverter - Sequência para DateTime
- StringToDateTimeOffsetConverter - Sequência para DateTimeOffset
- StringToEnumConverter<TEnum> - String para enum
- StringToGuidConverter - Sequência para Guid
- StringToNumberConverter<TNumber> - String para tipo numérico
- StringToTimeSpanConverter - Sequência para TimeSpan
- StringToUriConverter - Sequência para Uri
- Convertendo TimeSpan propriedades:
- TimeSpanToStringConverter - TimeSpan para cadeia de caracteres
- TimeSpanToTicksConverter - TimeSpan aos carrapatos
- Convertendo Uri propriedades:
- UriToStringConverter - Uri para cadeia de caracteres
Observe que todos os conversores internos são sem estado, podendo, assim, uma única instância 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 a forma como os dados são armazenados. Estes são, entre outros:
- 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 texto
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 um enum para strings, podemos especificar que a coluna do 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 varchar(20) coluna 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 EquineBeast colunas devem ser varchar(20), então essa informação pode ser dada 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 do banco de dados será não-unicode com um comprimento máximo de 20. No entanto, estas são apenas dicas, uma vez que são substituídas por quaisquer facetas explicitamente definidas na propriedade mapeada.
Exemplos
Objetos de valor simples
Este exemplo utiliza 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 para tipos) do que um tipo primitivo. Neste exemplo, esse tipo é Dollars, que encapsula a primitiva 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 para o 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
Este objeto de valor é implementado como uma struct somente leitura. Isso significa que o EF Core pode criar capturas instantâneas e comparar valores sem problemas. Consulte Comparadores de valor para obter mais informações.
Objetos de valor composto
No exemplo anterior, o tipo de objeto value 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 geral Money 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
}
Este 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. Isso geralmente é tratado serializando o objeto à medida que ele entra no banco de dados e, em seguida, desserializando-o novamente na saída. 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, eliminando a necessidade de usar a serialização aqui. Isso é rastreado pela edição #13947 do GitHub.
Observação
Como no exemplo anterior, este objeto de valor é implementado como uma struct somente leitura. Isso significa que o EF Core pode criar capturas instantâneas e comparar valores sem problemas. Consulte Comparadores de valor 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 detetar alterações corretamente. Consulte Comparadores de valor para obter mais informações.
Coleções de objetos de valor
Combinando os dois exemplos anteriores, podemos criar uma coleção de objetos de valor. Por exemplo, considere um AnnualFinance tipo que modela as finanças do blog para 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);
}
Este tipo compõe vários dos Money tipos 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
}
Podemos então adicionar uma coleção de AnnualFinance à entidade do nosso tipo.
public class Blog
{
public int Id { get; set; }
public string Name { get; set; }
public IList<AnnualFinance> Finances { get; set; }
}
E novamente use a serialização para armazenar isso:
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, esta conversão requer um ValueComparer<T>. Consulte Comparadores de valor para obter mais informações.
Objetos de valor como chaves
Às vezes, as propriedades de chave primitivas 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, podemos 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; }
}
Estes podem então 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 ser atribuído acidentalmente um PostKey, e Post.Id não pode ser acidentalmente atribuído um BlogKey. Da mesma forma, a propriedade de Post.BlogId chave estrangeira deve ser atribuída a um BlogKey.
Observação
Mostrar este padrão não significa que o recomendamos. Considere cuidadosamente se esse nível de abstração está ajudando ou atrapalhando sua experiência de desenvolvimento. Além disso, considere usar elementos de navegação e chaves geradas em vez de lidar diretamente com valores de chave.
Essas propriedades principais 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 chave com conversões podem apenas usar valores de chave gerados a partir do EF Core 7.0.
Utilize ulong para timestamp/rowversion
O SQL Server oferece suporte à simultaneidade otimista automática usando colunas binárias rowversion/timestamp de 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 de lidar. Os conversores de valor permitem que o rowversion seja mapeado para uma ulong propriedade, que é muito mais apropriada e fácil de usar do que a matriz de bytes. Por exemplo, considere uma Blog entidade com um token de concorrência do tipo 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 do SQL Server rowversion usando um conversor de valor:
modelBuilder.Entity<Blog>()
.Property(e => e.Version)
.IsRowVersion()
.HasConversion<byte[]>();
Especifique o atributo DateTime.Kind ao ler datas
O SQL Server descarta o sinalizador DateTime.Kind ao armazenar um DateTime como um datetime ou datetime2. Isso significa que os valores DateTime que retornam do banco de dados sempre têm um DateTimeKind de Unspecified.
Os 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 Kind sinalizador. Por exemplo:
modelBuilder.Entity<Post>()
.Property(e => e.PostedOn)
.HasConversion<long>();
Isso permite que valores DateTime com sinalizadores diferentes Kind sejam misturados no banco de dados.
O problema com esta abordagem é que o banco de dados não tem mais colunas datetime ou datetime2 reconhecíveis. Então, em vez disso, é comum sempre armazenar a hora UTC (ou, menos comumente, sempre a hora local) e, em seguida, ignorar o Kind sinalizador ou defini-lo para o valor apropriado usando um conversor de valor. Por exemplo, o conversor abaixo garante que o DateTime valor lido do banco de dados terá o DateTimeKindUTC:
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 ao banco de dados para usar o tempo UTC o tempo todo, lidando apenas com o horário local ao apresentar dados aos usuários.
Usar chaves de cadeias 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 sensíveis a maiúsculas e 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 realizar comparações de strings sem diferenciar maiúsculas de minúsculas, como no banco de dados. Por exemplo, considere um modelo de blog/posts com teclas de string:
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á como esperado se alguns dos Post.BlogId valores tiverem caixa diferente. Os erros causados por isso dependerão do que o aplicativo está fazendo, mas normalmente envolvem gráficos de objetos que não são corrigidos corretamente e/ou atualizações que falham porque o valor FK está errado. Um comparador de valores 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
As comparações de cadeia de caracteres .NET e de cadeia de caracteres de banco de dados podem diferir em mais do que apenas diferenciação de maiúsculas e minúsculas. Esse padrão funciona para chaves ASCII simples, mas pode falhar para chaves com qualquer tipo de caracteres específicos da cultura. Consulte Agrupamentos 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). As cadeias de caracteres de comprimento fixo são acolchoadas em todo o seu comprimento sempre que um valor é inserido no banco de dados. Isso significa que um valor de chave de "dotnet" será lido de volta do banco de dados como "dotnet..............", onde . representa um caractere de espaço. Isso não será comparado corretamente com valores de chave que não são preenchidos.
Um conversor de valor pode ser usado para remover o preenchimento ao ler valores-chave. Isto pode ser combinado com o comparador de valores no exemplo anterior para comparar corretamente as chaves ASCII insensíveis a maiúsculas e minúsculas de comprimento fixo. 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);
});
}
Criptografe valores de propriedade
Os conversores de valor podem ser usados para criptografar valores de propriedade antes de enviá-los para o banco de dados e, em seguida, descriptografá-los na saída. Por exemplo, usando 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
Atualmente, não há nenhuma maneira de obter uma referência ao DbContext atual, ou outro estado de sessão, de dentro de um conversor de valor. Isso limita os tipos de criptografia que podem ser usados. Vote na questão #11597 do GitHub para remover esta limitação.
Advertência
Certifique-se de entender todas as implicações de desenvolver o seu próprio método de criptografia para proteger adequadamente os dados confidenciais. Em vez disso, considere o uso de mecanismos de criptografia pré-criados, como Always Encrypted no SQL Server.
Limitações
Existem algumas limitações atuais conhecidas do sistema de conversão de valores:
- Como mencionado acima,
nullnão pode ser convertido. Por favor, vote (👍) para o problema #13850 do GitHub se isso for algo de que precisas. - Não é possível consultar propriedades convertidas em valor, por exemplo, membros de referência no tipo .NET convertido em valor em suas consultas LINQ. Por favor, vote (👍) para o problema #10434 do GitHub se isso for algo que precisa - mas considere usar uma coluna JSON.
- Atualmente, não há como distribuir uma conversão de uma propriedade para várias colunas ou vice-versa. Por favor, vote (👍) para a edição #13947 do GitHub se isso for algo que você precisa.
- A geração de valor não é suportada para a maioria das chaves mapeadas através de conversores de valor. Por favor, vote (👍) para a edição #11597 do GitHub se isso for algo que você precisa.
- As conversões de valor não podem fazer referência à instância DbContext atual. Por favor, vote (👍) para a edição #12205 do GitHub se isso for algo que você precisa.
- Atualmente, os parâmetros que usam tipos convertidos em valor não podem ser usados em APIs SQL brutas. Por favor, vote (👍) para o pedido #27534 do GitHub se é algo de que precisa.
A remoção dessas limitações está sendo considerada para versões futuras.