Conversiones de valores
Los convertidores de valores permiten convertir los valores de propiedad al leer o escribir en la base de datos. Esta conversión puede ser de un valor a otro del mismo tipo (por ejemplo, cifrar cadenas) o de un valor de un tipo a un valor de otro tipo (por ejemplo, convertir valores de enumeración a cadenas y desde estas en la base de datos).
Sugerencia
Puede ejecutar y depurar en todo el código de este documento descargando el código de ejemplo de GitHub.
Información general
Los convertidores de valores se especifican en términos de ModelClrType
y ProviderClrType
. El tipo de modelo es el tipo de .NET de la propiedad en el tipo de entidad. El tipo de proveedor es el tipo de .NET que comprende el proveedor de la base de datos. Por ejemplo, para guardar enumeraciones como cadenas en la base de datos, el tipo de modelo es el tipo de enumeración y el tipo de proveedor es String
. Estos dos tipos pueden ser el mismo.
Las conversiones se definen mediante dos árboles de expresión Func
: uno de ModelClrType
a ProviderClrType
y el otro de ProviderClrType
a ModelClrType
. Los árboles de expresión se usan para que se puedan compilar en el delegado de acceso a la base de datos para realizar conversiones eficaces. El árbol de expresión puede contener una llamada simple a un método de conversión para conversiones complejas.
Nota:
Es posible que una propiedad que se haya configurado para la conversión de valores también tenga que especificar ValueComparer<T>. Consulte los ejemplos siguientes y la documentación de Comparadores de valores para obtener más información.
Configuración de un convertidor de valores
Las conversiones de valores se configuran en DbContext.OnModelCreating. Por ejemplo, considere una enumeración y un tipo de entidad definidos como:
public class Rider
{
public int Id { get; set; }
public EquineBeast Mount { get; set; }
}
public enum EquineBeast
{
Donkey,
Mule,
Horse,
Unicorn
}
Las conversiones se pueden configurar en OnModelCreating para almacenar los valores de enumeración como cadenas como "Burro", "Mula", etc. en la base de datos; simplemente necesita proporcionar una función que convierta de ModelClrType
a ProviderClrType
y otra para la conversión opuesta:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder
.Entity<Rider>()
.Property(e => e.Mount)
.HasConversion(
v => v.ToString(),
v => (EquineBeast)Enum.Parse(typeof(EquineBeast), v));
}
Nota:
Un valor null
nunca se pasará a un convertidor de valores. Un valor NULL en una columna de base de datos siempre es NULL en la instancia de entidad y viceversa. Esto facilita la implementación de conversiones y permite compartirlas entre propiedades que aceptan valores NULL y que no aceptan valores NULL. Para obtener más información, consulte el problema de GitHub n.º 13850.
Configuración masiva de un convertidor de valores
Es habitual que el mismo convertidor de valores se configure para cada propiedad que use el tipo CLR pertinente. En lugar de hacerlo manualmente para cada propiedad, puede usar la configuración del modelo anterior a la convención para hacerlo una vez para todo el modelo. Para ello, defina el convertidor de valores como una clase:
public class CurrencyConverter : ValueConverter<Currency, decimal>
{
public CurrencyConverter()
: base(
v => v.Amount,
v => new Currency(v))
{
}
}
A continuación, invalide ConfigureConventions en el tipo de contexto y configure el convertidor de la siguiente manera:
protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
configurationBuilder
.Properties<Currency>()
.HaveConversion<CurrencyConverter>();
}
Conversiones definidas previamente
EF Core contiene muchas conversiones definidas previamente que evitan la necesidad de escribir las funciones de conversión manualmente. En su lugar, EF Core elegirá la conversión que se usará en función del tipo de propiedad en el modelo y el tipo de proveedor de base de datos solicitado.
Por ejemplo, las conversiones de enumeración a cadenas se usan como ejemplo más arriba, pero EF Core lo hará automáticamente cuando el tipo de proveedor esté configurado como string
mediante el tipo genérico de HasConversion:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder
.Entity<Rider>()
.Property(e => e.Mount)
.HasConversion<string>();
}
Se puede lograr lo mismo especificando explícitamente el tipo de columna de base de datos. Por ejemplo, si el tipo de entidad se define de la siguiente manera:
public class Rider2
{
public int Id { get; set; }
[Column(TypeName = "nvarchar(24)")]
public EquineBeast Mount { get; set; }
}
A continuación, los valores de enumeración se guardarán como cadenas en la base de datos sin ninguna configuración adicional en OnModelCreating.
La clase ValueConverter
Al llamar a HasConversion como se muestra anteriormente, se creará una instancia ValueConverter<TModel,TProvider> y se establecerá en la propiedad. En su lugar, ValueConverter
se puede crear explícitamente. Por ejemplo:
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);
}
Esto puede ser útil cuando varias propiedades usan la misma conversión.
Convertidores integrados
Como se mencionó anteriormente, EF Core se incluye con un conjunto de clases ValueConverter<TModel,TProvider> definidas previamente, que se encuentran en el espacio de nombres Microsoft.EntityFrameworkCore.Storage.ValueConversion. En muchos casos, EF elegirá el convertidor integrado adecuado en función del tipo de la propiedad en el modelo y el tipo solicitado en la base de datos, como se muestra anteriormente para las enumeraciones. Por ejemplo, el uso de .HasConversion<int>()
en una propiedad bool
hará que EF Core convierta los valores bool en valores numéricos cero y uno:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder
.Entity<User>()
.Property(e => e.IsActive)
.HasConversion<int>();
}
Esto es funcionalmente igual que crear una instancia de BoolToZeroOneConverter<TProvider> integrado y establecerlo explícitamente:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
var converter = new BoolToZeroOneConverter<int>();
modelBuilder
.Entity<User>()
.Property(e => e.IsActive)
.HasConversion(converter);
}
En la tabla siguiente se resumen las conversiones definidas previamente usadas habitualmente de tipos de modelo o propiedad a tipos de proveedor de base de datos. En la tabla any_numeric_type
significa uno de int
, short
, long
, byte
, uint
, ushort
, ulong
, sbyte
, char
, decimal
, float
o double
.
Tipo de modelo o propiedad | Tipo de proveedor o base de datos | Conversión | Uso |
---|---|---|---|
bool | any_numeric_type | False/true a 0/1 | .HasConversion<any_numeric_type>() |
any_numeric_type | False/true a dos números cualquiera | Use BoolToTwoValuesConverter<TProvider> | |
cadena | False/true a "N"/"Y" | .HasConversion<string>() |
|
cadena | False/true a dos cadenas cualquiera | Use BoolToStringConverter | |
any_numeric_type | bool | 0/1 a false/true | .HasConversion<bool>() |
any_numeric_type | Conversión simple | .HasConversion<any_numeric_type>() |
|
cadena | Número como una cadena | .HasConversion<string>() |
|
Enum | any_numeric_type | Valor numérico de la enumeración | .HasConversion<any_numeric_type>() |
cadena | Representación de cadena del valor de enumeración | .HasConversion<string>() |
|
cadena | bool | Analiza la cadena como un bool | .HasConversion<bool>() |
any_numeric_type | Analiza la cadena como el tipo numérico especificado | .HasConversion<any_numeric_type>() |
|
char | Primer carácter de la cadena | .HasConversion<char>() |
|
DateTime | Analiza la cadena como dateTime | .HasConversion<DateTime>() |
|
DateTimeOffset | Analiza la cadena como DateTimeOffset | .HasConversion<DateTimeOffset>() |
|
TimeSpan | Analiza la cadena como timeSpan | .HasConversion<TimeSpan>() |
|
Guid | Analiza la cadena como un GUID | .HasConversion<Guid>() |
|
byte[] | Cadena como bytes UTF8 | .HasConversion<byte[]>() |
|
char | cadena | Una cadena de caracteres únicos | .HasConversion<string>() |
DateTime | long | Fecha y hora codificadas conservando DateTime.Kind | .HasConversion<long>() |
long | Tics | Use DateTimeToTicksConverter | |
cadena | Cadena de fecha y hora de referencia cultural invariable | .HasConversion<string>() |
|
DateTimeOffset | long | Fecha y hora codificadas con desplazamiento | .HasConversion<long>() |
cadena | Cadena de fecha y hora de referencia cultural invariable con desplazamiento | .HasConversion<string>() |
|
TimeSpan | long | Tics | .HasConversion<long>() |
cadena | Cadena de intervalo de tiempo de referencia cultural invariable | .HasConversion<string>() |
|
Uri | cadena | URI como una cadena | .HasConversion<string>() |
PhysicalAddress | cadena | La dirección como una cadena | .HasConversion<string>() |
byte[] | Bytes en orden de red big-endian | .HasConversion<byte[]>() |
|
IPAddress | cadena | La dirección como una cadena | .HasConversion<string>() |
byte[] | Bytes en orden de red big-endian | .HasConversion<byte[]>() |
|
Guid | cadena | GUID en formato “dddddddd-dddd-dddd-dddd-dddddddddddd” | .HasConversion<string>() |
byte[] | Bytes en orden de serialización binaria de .NET | .HasConversion<byte[]>() |
Tenga en cuenta que estas conversiones suponen que el formato del valor es adecuado para la conversión. Por ejemplo, la conversión de cadenas en números producirá un error si los valores de la cadena no se pueden analizar como números.
La lista completa de convertidores integrados es:
- Conversión de propiedades bool:
- BoolToStringConverter: bool a cadenas como "N" e "Y"
- BoolToTwoValuesConverter<TProvider>: bool a dos valores cualquiera
- BoolToZeroOneConverter<TProvider>: bool a cero y uno
- Conversión de propiedades de matriz de bytes:
- BytesToStringConverter: matriz de bytes a cadena codificada en Base64
- Cualquier conversión que requiera solo una conversión de tipos
- CastingConverter<TModel,TProvider>: conversiones que requieren solo una conversión de tipos
- Conversión de propiedades char:
- CharToStringConverter: char a una cadena de caracteres únicos
- Conversión de propiedades DateTimeOffset:
- DateTimeOffsetToBinaryConverter - DateTimeOffset a un valor de 64 bits con codificación binaria
- DateTimeOffsetToBytesConverter - DateTimeOffset a una matriz de bytes
- DateTimeOffsetToStringConverter - DateTimeOffset a una cadena
- Conversión de propiedades DateTime:
- DateTimeToBinaryConverter - DateTime a un valor de 64 bits, incluido DateTimeKind
- DateTimeToStringConverter - DateTime a una cadena
- DateTimeToTicksConverter - DateTime a ticks
- Conversión de propiedades de enumeración:
- EnumToNumberConverter<TEnum,TNumber>: enumeración al número subyacente
- EnumToStringConverter<TEnum>: enumeración a cadena
- Conversión de propiedades Guid:
- GuidToBytesConverter - Guid a una matriz de bytes
- GuidToStringConverter - Guid a una cadena
- Conversión de propiedades IPAddress:
- IPAddressToBytesConverter - IPAddress a una matriz de bytes
- IPAddressToStringConverter - IPAddress a una cadena
- Convertir propiedades numéricas (int, doble, decimal, etc.):
- NumberToBytesConverter<TNumber>: cualquier valor numérico a una matriz de bytes
- NumberToStringConverter<TNumber>: cualquier valor numérico a una cadena
- Conversión de propiedades PhysicalAddress:
- PhysicalAddressToBytesConverter - PhysicalAddress a una matriz de bytes
- PhysicalAddressToStringConverter - PhysicalAddress a una cadena
- Conversión de propiedades de cadena:
- StringToBoolConverter: cadenas como "N" e "Y" a bool
- StringToBytesConverter: cadena a bytes UTF8
- StringToCharConverter: cadena a carácter
- StringToDateTimeConverter: cadena a DateTime
- StringToDateTimeOffsetConverter: cadena a DateTimeOffset
- StringToEnumConverter<TEnum>: cadena a enumeración
- StringToGuidConverter: cadena a Guid
- StringToNumberConverter<TNumber>: cadena a tipo numérico
- StringToTimeSpanConverter: cadena a TimeSpan
- StringToUriConverter: cadena a Uri
- Conversión de propiedades TimeSpan:
- TimeSpanToStringConverter - TimeSpan a una cadena
- TimeSpanToTicksConverter - TimeSpan para ticks
- Conversión de propiedades Uri:
- UriToStringConverter - Uri a una cadena
Tenga en cuenta que todos los convertidores integrados no tienen estado y, por tanto, varias propiedades pueden compartir de forma segura una sola instancia.
Facetas de columna y sugerencias de asignación
Algunos tipos de base de datos tienen facetas que modifican cómo se almacenan los datos. Entre ellas se incluyen las siguientes:
- Precisión y escala de las columnas decimales y de fecha y hora
- Tamaño y longitud de las columnas binarias y de cadena
- Unicode para columnas de cadena
Estas facetas se pueden configurar de la manera normal para una propiedad que usa un convertidor de valores y se aplicará al tipo de base de datos convertido. Por ejemplo, al convertir de una enumeración a cadenas, podemos especificar que la columna de base de datos no debe ser Unicode y almacenar hasta 20 caracteres:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder
.Entity<Rider>()
.Property(e => e.Mount)
.HasConversion<string>()
.HasMaxLength(20)
.IsUnicode(false);
}
O bien, al crear el convertidor explícitamente:
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);
}
Esto da como resultado una columna varchar(20)
cuando se usan migraciones de EF Core en SQL Server:
CREATE TABLE [Rider] (
[Id] int NOT NULL IDENTITY,
[Mount] varchar(20) NOT NULL,
CONSTRAINT [PK_Rider] PRIMARY KEY ([Id]));
Sin embargo, si de manera predeterminada todas las columnas EquineBeast
deben ser varchar(20)
, esta información se puede proporcionar al convertidor de valores como ConverterMappingHints. Por ejemplo:
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);
}
Ahora, siempre que se use este convertidor, la columna de base de datos no será Unicode con una longitud máxima de 20. Sin embargo, estas son solo sugerencias, ya que las facetas establecidas de manera explícita en la propiedad asignada las pueden invalidar.
Ejemplos
Objetos de valor simple
En este ejemplo se usa un tipo simple para ajustar un tipo primitivo. Esto puede ser útil cuando desea que el tipo del modelo sea más específico (y, por tanto, con más seguridad de tipos) que un tipo primitivo. En este ejemplo, ese tipo es Dollars
, que ajusta el primitivo decimal:
public readonly struct Dollars
{
public Dollars(decimal amount)
=> Amount = amount;
public decimal Amount { get; }
public override string ToString()
=> $"${Amount}";
}
Esto se puede usar en un tipo de entidad:
public class Order
{
public int Id { get; set; }
public Dollars Price { get; set; }
}
Y se convierte en el decimal
subyacente cuando se almacena en la base de datos:
modelBuilder.Entity<Order>()
.Property(e => e.Price)
.HasConversion(
v => v.Amount,
v => new Dollars(v));
Nota:
Este objeto de valor se implementa como una estructura de solo lectura. Esto significa que EF Core puede obtener instantáneas y comparar valores sin problema. Consulte Comparadores de valores para obtener más información.
Objetos de valor compuesto
En el ejemplo anterior, el tipo de objeto del valor contenía solo una propiedad única. Es más común que un tipo de objeto de valor componga varias propiedades que juntas forman un concepto de dominio. Por ejemplo, un tipo Money
general que contiene el importe y la moneda:
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 se puede usar en un tipo de entidad como antes:
public class Order
{
public int Id { get; set; }
public Money Price { get; set; }
}
Actualmente, los convertidores de valores solo pueden convertir valores en una sola columna de base de datos y desde ella. Esta limitación significa que todos los valores de propiedad del objeto deben codificarse en un solo valor de columna. Normalmente, esto se controla serializando el objeto a medida que entra en la base de datos y, a continuación, deserializándolo de nuevo al salir. Por ejemplo, al usar System.Text.Json:
modelBuilder.Entity<Order>()
.Property(e => e.Price)
.HasConversion(
v => JsonSerializer.Serialize(v, (JsonSerializerOptions)null),
v => JsonSerializer.Deserialize<Money>(v, (JsonSerializerOptions)null));
Nota:
Tenemos previsto permitir la asignación de un objeto a varias columnas en una versión futura de EF Core, lo que eliminaría la necesidad de usar la serialización aquí. El seguimiento lo realiza el problema de GitHub n.º 13947.
Nota:
Al igual que con el ejemplo anterior, este objeto de valor se implementa como una estructura de solo lectura. Esto significa que EF Core puede obtener instantáneas y comparar valores sin problema. Consulte Comparadores de valores para obtener más información.
Colecciones de primitivos
La serialización también se puede usar para almacenar una colección de valores primitivos. Por ejemplo:
public class Post
{
public int Id { get; set; }
public string Title { get; set; }
public string Contents { get; set; }
public ICollection<string> Tags { get; set; }
}
Uso de System.Text.Json de nuevo:
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 un tipo de referencia mutable. Esto significa que ValueComparer<T> es necesario para que EF Core pueda realizar un seguimiento y detectar los cambios correctamente. Consulte Comparadores de valores para obtener más información.
Colecciones de objetos de valor
Al combinar los dos ejemplos anteriores, podemos crear una colección de objetos de valor. Por ejemplo, considere un tipo AnnualFinance
que modele las finanzas de un blog durante un solo año:
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 compone varios de los tipos Money
que hemos creado 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
}
A continuación, podemos agregar una colección de AnnualFinance
a nuestro tipo de entidad:
public class Blog
{
public int Id { get; set; }
public string Name { get; set; }
public IList<AnnualFinance> Finances { get; set; }
}
Y, de nuevo, use la serialización para almacenar esto:
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()));
Nota:
Como antes, esta conversión requiere ValueComparer<T>. Consulte Comparadores de valores para obtener más información.
Objetos de valor como claves
A veces, las propiedades de clave primitivas se pueden ajustar en objetos de valor para agregar un nivel adicional de seguridad de tipos en la asignación de valores. Por ejemplo, podríamos implementar un tipo de clave para los blogs y un tipo de clave para las publicaciones:
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; }
}
A continuación, se pueden usar en el modelo de dominio:
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; }
}
Tenga en cuenta que Blog.Id
no se puede asignar accidentalmente a PostKey
y Post.Id
no se puede asignar accidentalmente a BlogKey
. Del mismo modo, a la propiedad de clave externa Post.BlogId
se le debe asignar un BlogKey
.
Nota:
Mostrar este patrón no significa que se recomiende. Considere detenidamente si este nivel de abstracción ayuda o dificulta su experiencia de desarrollo. Además, considere la posibilidad de usar las navegaciones y las claves generadas en lugar de tratar directamente con los valores de clave.
Estas propiedades clave se pueden asignar mediante convertidores de valores:
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);
});
}
Nota:
Las propiedades clave con conversiones solo pueden usar valores de clave generados a partir de EF Core 7.0.
Usar ulong para timestamp/rowversion
SQL Server admite la simultaneidad optimista automática mediante columnas rowversion
/timestamp
binarias de 8 bytes. Siempre se leen y escriben en la base de datos mediante una matriz de 8 bytes. Sin embargo, las matrices de bytes son un tipo de referencia mutable, lo que hace que sean algo difícil de tratar. Los convertidores de valores permiten que rowversion
se asigne en su lugar a una propiedad ulong
, que es mucho más adecuada y fácil de usar que la matriz de bytes. Por ejemplo, considere una entidad Blog
con un token de simultaneidad de ulong:
public class Blog
{
public int Id { get; set; }
public string Name { get; set; }
public ulong Version { get; set; }
}
Esto se puede asignar a una columna rowversion
de SQL Server mediante un convertidor de valores:
modelBuilder.Entity<Blog>()
.Property(e => e.Version)
.IsRowVersion()
.HasConversion<byte[]>();
Especificar dateTime.Kind al leer fechas
SQL Server descarta la marca DateTime.Kind al almacenar DateTime como datetime
o datetime2
. Esto significa que los valores DateTime que vuelven de la base de datos siempre tienen un DateTimeKind de Unspecified
.
Los convertidores de valores se pueden usar de dos maneras para tratar esto. En primer lugar, EF Core tiene un convertidor de valores que crea un valor opaco de 8 bytes que conserva la marca Kind
. Por ejemplo:
modelBuilder.Entity<Post>()
.Property(e => e.PostedOn)
.HasConversion<long>();
Esto permite mezclar valores DateTime con marcas Kind
diferentes en la base de datos.
El problema con este enfoque es que la base de datos ya no tiene columnas datetime
o datetime2
reconocibles. Por lo tanto, es habitual almacenar siempre la hora UTC (o, menos comúnmente, siempre la hora local) y, a continuación, omitir la marca Kind
o establecerla en el valor adecuado mediante un convertidor de valores. Por ejemplo, el convertidor siguiente garantiza que el valor DateTime
leído de la base de datos tenga DateTimeKindUTC
:
modelBuilder.Entity<Post>()
.Property(e => e.LastUpdated)
.HasConversion(
v => v,
v => new DateTime(v.Ticks, DateTimeKind.Utc));
Si se establece una combinación de valores locales y UTC en las instancias de entidad, el convertidor se puede usar para convertir adecuadamente antes de la inserción. Por ejemplo:
modelBuilder.Entity<Post>()
.Property(e => e.LastUpdated)
.HasConversion(
v => v.ToUniversalTime(),
v => new DateTime(v.Ticks, DateTimeKind.Utc));
Nota:
Considere cuidadosamente la posibilidad de unificar todo el código de acceso a la base de datos para usar la hora UTC todo el tiempo y solo tratar con la hora local al presentar datos a los usuarios.
Usar claves de cadena que no distinguen mayúsculas de minúsculas
Algunas bases de datos, como SQL Server, realizan comparaciones de cadenas que no distinguen mayúsculas de minúsculas de manera predeterminada. Por otro lado, .NET realiza comparaciones de cadenas que distinguen mayúsculas de minúsculas de manera predeterminada. Esto significa que un valor de clave externa como "DotNet" coincidirá con el valor de clave principal "dotnet" en SQL Server, pero no coincidirá en EF Core. Se puede usar un comparador de valores para las claves para forzar en EF Core las comparaciones de cadenas que no distinguen mayúsculas de minúsculas, como en la base de datos. Por ejemplo, considere un modelo de blog o publicaciones con claves de cadena:
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; }
}
Esto no funcionará según lo esperado si algunos de los valores Post.BlogId
tienen mayúsculas y minúsculas diferentes. Los errores causados por esto dependerán de lo que hace la aplicación, pero normalmente implican gráficos de objetos que no están corregidos correctamente o actualizaciones que producen un error porque el valor de FK es incorrecto. Se puede usar un comparador de valores para corregirlo:
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);
});
}
Nota:
Las comparaciones de cadenas de .NET y las comparaciones de cadenas de la base de datos pueden diferir en más que solo la distinción de mayúsculas y minúsculas. Este patrón funciona para claves ASCII simples, pero puede producir un error en las claves con cualquier tipo de caracteres específicos de la referencia cultural. Consulte Intercalaciones y distinción de mayúsculas y minúsculas para obtener más información.
Control de cadenas de base de datos de longitud fija
El ejemplo anterior no necesitaba un convertidor de valores. Sin embargo, un convertidor puede ser útil para los tipos de cadena de base de datos de longitud fija como char(20)
o nchar(20)
. Las cadenas de longitud fija se rellenan en su longitud completa cada vez que se inserta un valor en la base de datos. Esto significa que un valor de clave de "dotnet
" se leerá de nuevo de la base de datos como "dotnet..............
", donde .
representa un carácter de espacio. Esto no se comparará correctamente con los valores de clave que no se rellenan.
Se puede usar un convertidor de valores para recortar el relleno al leer los valores de clave. Esto se puede combinar con el comparador de valores del ejemplo anterior para comparar correctamente las claves ASCII sin distinción de mayúsculas y minúsculas de longitud fija. Por ejemplo:
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);
});
}
Cifrar valores de propiedad
Los convertidores de valores se pueden usar para cifrar los valores de propiedad antes de enviarlos a la base de datos y, a continuación, descifrarlos al salir. Por ejemplo, el uso de la inversión de cadenas como sustituto de un algoritmo de cifrado real:
modelBuilder.Entity<User>().Property(e => e.Password).HasConversion(
v => new string(v.Reverse().ToArray()),
v => new string(v.Reverse().ToArray()));
Nota:
Actualmente no hay ninguna manera de obtener una referencia al DbContext actual, u otro estado de sesión, desde dentro de un convertidor de valores. Esto limita los tipos de cifrado que se pueden usar. Vote por el problema de GitHub n.º 11597 para que se quite esta limitación.
Advertencia
Asegúrese de comprender todas las implicaciones si implementa su propio cifrado para proteger los datos confidenciales. Considere la posibilidad de usar mecanismos de cifrado compilados previamente, como Always Encrypted en SQL Server.
Limitaciones
Existen algunas limitaciones actuales conocidas del sistema de conversión de valores:
- Como se indicó anteriormente,
null
no se puede convertir. Vote (👍) para el problema de GitHub n.º 13850 si esto es algo que necesita. - No es posible consultar en propiedades convertidas a valores, por ejemplo, miembros de referencia en el tipo .NET convertido en valor en las consultas LINQ. Vote (👍) para el problema de GitHub n.º 10434 si esto es algo que necesita, pero considere la posibilidad de usar una columna JSON en su lugar.
- Actualmente no hay ninguna manera de distribuir una conversión de una propiedad a varias columnas o viceversa. Vote (👍) para el problema de GitHub n.º 13947 si esto es algo que necesita.
- No se admite la generación de valores para la mayoría de las claves asignadas a través de convertidores de valores. Vote (👍) para el problema de GitHub n.º 11597 si esto es algo que necesita.
- Las conversiones de valores no pueden hacer referencia a la instancia de DbContext actual. Vote (👍) para el problema de GitHub n.º 12205 si esto es algo que necesita.
- Los parámetros que usan tipos convertidos a valores no se pueden usar actualmente sin procesar en las API de SQL. Vote (👍) para el problema de GitHub n.º 27534 si esto es algo que necesita.
La eliminación de estas limitaciones se está considerando para futuras versiones.