Conversions de valeurs
Les convertisseurs de valeurs permettent de convertir les valeurs de propriété lors de la lecture ou de l’écriture dans la base de données. Cette conversion peut passer d’une valeur à une autre du même type (par exemple, chiffrement de chaînes) ou d’une valeur d’un type à une valeur d’un autre type (par exemple, conversion de valeurs d’énumération en et à partir de chaînes dans la base de données.)
Conseil
Vous pouvez exécuter et déboguer dans tout le code de ce document en téléchargeant l’exemple de code à partir de GitHub.
Vue d’ensemble
Les convertisseurs de valeurs sont spécifiés en termes de ModelClrType
et d’un ProviderClrType
. Le type de modèle est le type .NET de la propriété dans le type d’entité. Le type de fournisseur est le type .NET compris par le fournisseur de base de données. Par exemple, pour enregistrer des énumérations sous forme de chaînes dans la base de données, le type de modèle est le type de l’énumération et le type de fournisseur est String
. Ces deux types peuvent être identiques.
Les conversions sont définies à l’aide de deux arborescences d’expressions Func
: une ModelClrType
à ProviderClrType
et l’autre de ProviderClrType
à ModelClrType
. Les arborescences d’expressions sont utilisées afin qu’elles puissent être compilées dans le délégué d’accès à la base de données pour des conversions efficaces. L’arborescence d’expressions peut contenir un appel simple à une méthode de conversion pour les conversions complexes.
Remarque
Une propriété qui a été configurée pour la conversion de valeur peut également avoir besoin de spécifier une ValueComparer<T>. Pour plus d’informations, consultez les exemples ci-dessous et la documentation Comparateurs de valeurs.
Configuration d’un convertisseur de valeurs
Les conversions de valeurs sont configurées dans DbContext.OnModelCreating. Par exemple, considérez un type d’énumération et d’entité défini comme suit :
public class Rider
{
public int Id { get; set; }
public EquineBeast Mount { get; set; }
}
public enum EquineBeast
{
Donkey,
Mule,
Horse,
Unicorn
}
Les conversions peuvent être configurées dans OnModelCreating pour stocker les valeurs d’énumération en tant que chaînes telles que « Donkey », « Mule », etc. dans la base de données ; il vous suffit de fournir une fonction qui se convertit du ModelClrType
au ProviderClrType
, et une autre pour la conversion opposée :
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder
.Entity<Rider>()
.Property(e => e.Mount)
.HasConversion(
v => v.ToString(),
v => (EquineBeast)Enum.Parse(typeof(EquineBeast), v));
}
Remarque
Une valeur null
ne sera jamais passée à un convertisseur de valeur. Une valeur Null dans une colonne de base de données est toujours une valeur Null dans l’instance d’entité, et inversement. Cela facilite l’implémentation des conversions et leur permet d’être partagées entre des propriétés nullables et non nullables. Pour plus d’informations, consultez problème GitHub #13850.
Configuration en bloc d’un convertisseur de valeur
Il est courant que le convertisseur de valeur soit configuré pour chaque propriété qui utilise le type CLR approprié. Au lieu d’effectuer cette opération manuellement pour chaque propriété, vous pouvez utiliser configuration de modèle pré-convention pour effectuer cette opération une fois pour l’ensemble de votre modèle. Pour ce faire, définissez votre convertisseur de valeurs en tant que classe :
public class CurrencyConverter : ValueConverter<Currency, decimal>
{
public CurrencyConverter()
: base(
v => v.Amount,
v => new Currency(v))
{
}
}
Ensuite, remplacez ConfigureConventions dans votre type de contexte et configurez le convertisseur comme suit :
protected override void ConfigureConventions(ModelConfigurationBuilder configurationBuilder)
{
configurationBuilder
.Properties<Currency>()
.HaveConversion<CurrencyConverter>();
}
Conversions prédéfinies
EF Core contient de nombreuses conversions prédéfinies qui évitent la nécessité d’écrire manuellement des fonctions de conversion. Au lieu de cela, EF Core choisit la conversion à utiliser en fonction du type de propriété dans le modèle et du type de fournisseur de base de données demandé.
Par exemple, l’énumération en conversions de chaînes est utilisée comme exemple ci-dessus, mais EF Core le fera automatiquement lorsque le type de fournisseur est configuré comme string
à l’aide du type générique de HasConversion:
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder
.Entity<Rider>()
.Property(e => e.Mount)
.HasConversion<string>();
}
La même chose peut être obtenue en spécifiant explicitement le type de colonne de base de données. Par exemple, si le type d’entité est défini comme suit :
public class Rider2
{
public int Id { get; set; }
[Column(TypeName = "nvarchar(24)")]
public EquineBeast Mount { get; set; }
}
Ensuite, les valeurs d’énumération sont enregistrées sous forme de chaînes dans la base de données sans configuration supplémentaire dans OnModelCreating.
Classe ValueConverter
L’appel de HasConversion comme indiqué ci-dessus crée une instance de ValueConverter<TModel,TProvider> et la définit sur la propriété. Le ValueConverter
peut être créé explicitement. Par exemple :
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);
}
Cela peut être utile lorsque plusieurs propriétés utilisent la même conversion.
Convertisseurs intégrés
Comme mentionné ci-dessus, EF Core est fourni avec un ensemble de classes ValueConverter<TModel,TProvider> prédéfinies, trouvées dans l’espace de noms Microsoft.EntityFrameworkCore.Storage.ValueConversion. Dans de nombreux cas, EF choisit le convertisseur intégré approprié en fonction du type de la propriété dans le modèle et du type demandé dans la base de données, comme indiqué ci-dessus pour les énumérations. Par exemple, l’utilisation de .HasConversion<int>()
sur une propriété de bool
entraîne la conversion des valeurs bool par zéro numérique et une valeur :
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder
.Entity<User>()
.Property(e => e.IsActive)
.HasConversion<int>();
}
Cela est fonctionnellement identique à la création d’une instance de l’intégrée BoolToZeroOneConverter<TProvider> et à sa définition explicite :
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
var converter = new BoolToZeroOneConverter<int>();
modelBuilder
.Entity<User>()
.Property(e => e.IsActive)
.HasConversion(converter);
}
Le tableau suivant récapitule les conversions prédéfinis couramment utilisées des types de modèles/propriétés vers des types de fournisseurs de base de données. Dans la table any_numeric_type
signifie l’un des int
, short
, long
, byte
, uint
, ushort
, ulong
, sbyte
, char
, decimal
, float
ou double
.
Type de modèle/propriété | Type de fournisseur/de base de données | Conversion | Utilisation |
---|---|---|---|
bool | any_numeric_type | False/true sur 0/1 | .HasConversion<any_numeric_type>() |
any_numeric_type | False/true sur deux nombres | Utilisez BoolToTwoValuesConverter<TProvider>. | |
string | False/true sur "N"/"Y" | .HasConversion<string>() |
|
string | False/true sur deux chaînes | Utilisez BoolToStringConverter. | |
any_numeric_type | bool | 0/1 à false/true | .HasConversion<bool>() |
any_numeric_type | Cast simple | .HasConversion<any_numeric_type>() |
|
string | Nombre sous forme de chaîne | .HasConversion<string>() |
|
Enum | any_numeric_type | Valeur numérique de l’énumération | .HasConversion<any_numeric_type>() |
string | Représentation sous forme de chaîne de la valeur d’énumération | .HasConversion<string>() |
|
string | bool | Analyse la chaîne en tant que bool | .HasConversion<bool>() |
any_numeric_type | Analyse la chaîne en tant que type numérique donné | .HasConversion<any_numeric_type>() |
|
char | Premier caractère de la chaîne | .HasConversion<char>() |
|
Date et heure | Analyse la chaîne en tant que DateTime | .HasConversion<DateTime>() |
|
DateTimeOffset | Analyse la chaîne en tant que DateTimeOffset | .HasConversion<DateTimeOffset>() |
|
TimeSpan | Analyse la chaîne en tant que timeSpan | .HasConversion<TimeSpan>() |
|
GUID | Analyse la chaîne en tant que GUID | .HasConversion<Guid>() |
|
byte[] | Chaîne en tant qu’octets UTF8 | .HasConversion<byte[]>() |
|
char | string | Chaîne de caractères unique | .HasConversion<string>() |
Date et heure | long | Date/heure encodée préservant DateTime.Kind | .HasConversion<long>() |
long | Cycles | Utilisez DateTimeToTicksConverter. | |
string | Chaîne de date/heure de culture invariante | .HasConversion<string>() |
|
DateTimeOffset | long | Date/heure encodées avec décalage | .HasConversion<long>() |
string | Chaîne de date/heure de culture invariante avec décalage | .HasConversion<string>() |
|
TimeSpan | long | Cycles | .HasConversion<long>() |
string | Chaîne d’intervalle de temps de culture invariant | .HasConversion<string>() |
|
Uri | string | URI sous forme de chaîne | .HasConversion<string>() |
PhysicalAddress | string | Adresse sous forme de chaîne | .HasConversion<string>() |
byte[] | Octets dans l’ordre réseau big-endian | .HasConversion<byte[]>() |
|
IPAddress | string | Adresse sous forme de chaîne | .HasConversion<string>() |
byte[] | Octets dans l’ordre réseau big-endian | .HasConversion<byte[]>() |
|
GUID | string | Le GUID au format 'dddddddd-dddd-dddd-dddd-dddddddddddd' | .HasConversion<string>() |
byte[] | Octets dans l’ordre de sérialisation binaire .NET | .HasConversion<byte[]>() |
Notez que ces conversions supposent que le format de la valeur est approprié pour la conversion. Par exemple, la conversion de chaînes en nombres échoue si les valeurs de chaîne ne peuvent pas être analysées en tant que nombres.
La liste complète des convertisseurs intégrés est la suivante :
- Conversion des propriétés bool :
- BoolToStringConverter - Bool à des chaînes telles que « N » et « Y »
- BoolToTwoValuesConverter<TProvider> - Bool à deux valeurs
- BoolToZeroOneConverter<TProvider> - Bool à zéro et un
- Conversion des propriétés du tableau d’octets :
- BytesToStringConverter - Tableau d’octets en chaîne encodée en Base64
- Toute conversion qui ne nécessite qu’un cast de type
- CastingConverter<TModel,TProvider> - Conversions qui nécessitent uniquement un cast de type
- Conversion des propriétés de caractères :
- CharToStringConverter - Char en chaîne de caractères unique
- Conversion des propriétés DateTimeOffset :
- DateTimeOffsetToBinaryConverter - DateTimeOffset à la valeur 64 bits encodée en binaire
- DateTimeOffsetToBytesConverter - DateTimeOffset au tableau d’octets
- DateTimeOffsetToStringConverter - DateTimeOffset à chaîne
- Conversion des propriétés DateTime :
- DateTimeToBinaryConverter - DateTime à la valeur 64 bits, y compris DateTimeKind
- DateTimeToStringConverter - DateTime à chaîne
- DateTimeToTicksConverter - DateTime aux cycles
- Conversion des propriétés d’énumération :
- EnumToNumberConverter<TEnum,TNumber> - Énumération en nombre sous-jacent
- EnumToStringConverter<TEnum> - Énumération en chaîne
- Conversion des propriétés Guid :
- GuidToBytesConverter - Guid au tableau d’octets
- GuidToStringConverter - Guid à chaîne
- Conversion des propriétés IPAddress :
- IPAddressToBytesConverter - IPAddress au tableau d’octets
- IPAddressToStringConverter - IPAddress à chaîne
- Conversion des propriétés numériques (int, double, décimal, etc.) :
- NumberToBytesConverter<TNumber> - Toute valeur numérique en tableau d’octets
- NumberToStringConverter<TNumber> - Toute valeur numérique à chaîne
- Conversion des propriétés PhysicalAddress :
- PhysicalAddressToBytesConverter - PhysicalAddress au tableau d’octets
- PhysicalAddressToStringConverter - PhysicalAddress à chaîne
- Conversion des propriétés de chaîne :
- StringToBoolConverter - Chaînes telles que « N » et « Y » pour bool
- StringToBytesConverter - Chaîne en octets UTF8
- StringToCharConverter - Chaîne à caractère
- StringToDateTimeConverter - Chaîne à DateTime
- StringToDateTimeOffsetConverter - Chaîne à DateTimeOffset
- StringToEnumConverter<TEnum> - Chaîne à énumérer
- StringToGuidConverter - Chaîne à Guid
- StringToNumberConverter<TNumber> - Chaîne en type numérique
- StringToTimeSpanConverter - Chaîne à TimeSpan
- StringToUriConverter - Chaîne à Uri
- Conversion des propriétés TimeSpan :
- TimeSpanToStringConverter - TimeSpan à chaîne
- TimeSpanToTicksConverter - TimeSpan aux cycles
- Conversion des propriétés Uri :
- UriToStringConverter - Uri à chaîne
Notez que tous les convertisseurs intégrés sont sans état et qu’une seule instance peut être partagée en toute sécurité par plusieurs propriétés.
Facettes de colonne et indicateurs de mappage
Certains types de base de données ont des facettes qui modifient la façon dont les données sont stockées. Il s’agit notamment des paramètres suivants :
- Précision et échelle pour les décimales et les colonnes de date/heure
- Taille/longueur des colonnes binaires et de chaînes
- Unicode pour les colonnes de chaîne
Ces facettes peuvent être configurées de manière normale pour une propriété qui utilise un convertisseur de valeur et s’applique au type de base de données converti. Par exemple, lors de la conversion d’une énumération en chaînes, nous pouvons spécifier que la colonne de base de données doit être non-Unicode et stocker jusqu’à 20 caractères :
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder
.Entity<Rider>()
.Property(e => e.Mount)
.HasConversion<string>()
.HasMaxLength(20)
.IsUnicode(false);
}
Ou, lors de la création du convertisseur explicitement :
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);
}
Cela entraîne une colonne varchar(20)
lors de l’utilisation des migrations EF Core sur SQL Server :
CREATE TABLE [Rider] (
[Id] int NOT NULL IDENTITY,
[Mount] varchar(20) NOT NULL,
CONSTRAINT [PK_Rider] PRIMARY KEY ([Id]));
Toutefois, si, par défaut, toutes les colonnes EquineBeast
doivent être varchar(20)
, ces informations peuvent être fournies au convertisseur de valeurs en tant que ConverterMappingHints. Par exemple :
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);
}
À présent, chaque fois que ce convertisseur est utilisé, la colonne de base de données n’est pas unicode avec une longueur maximale de 20. Toutefois, il s’agit uniquement d’indicateurs, car ils sont substitués par toutes les facettes définies explicitement sur la propriété mappée.
Exemples
Objets de valeur simple
Cet exemple utilise un type simple pour encapsuler un type primitif. Cela peut être utile lorsque vous souhaitez que le type de votre modèle soit plus spécifique (et donc plus sûr de type) qu’un type primitif. Dans cet exemple, ce type est Dollars
, qui encapsule la primitive décimale :
public readonly struct Dollars
{
public Dollars(decimal amount)
=> Amount = amount;
public decimal Amount { get; }
public override string ToString()
=> $"${Amount}";
}
Cela peut être utilisé dans un type d’entité :
public class Order
{
public int Id { get; set; }
public Dollars Price { get; set; }
}
Converti en decimal
sous-jacent lorsqu’il est stocké dans la base de données :
modelBuilder.Entity<Order>()
.Property(e => e.Price)
.HasConversion(
v => v.Amount,
v => new Dollars(v));
Remarque
Cet objet valeur est implémenté en tant que structure en lecture seule. Cela signifie qu’EF Core peut prendre une capture instantanée et comparer des valeurs sans problème. Pour plus d’informations, consultez comparaisons de valeurs.
Objets de valeur composite
Dans l’exemple précédent, le type d’objet value ne contenait qu’une seule propriété. Il est plus courant pour un type d’objet value de composer plusieurs propriétés qui forment ensemble un concept de domaine. Par exemple, un type de Money
général qui contient à la fois le montant et la devise :
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
}
Cet objet valeur peut être utilisé dans un type d’entité comme avant :
public class Order
{
public int Id { get; set; }
public Money Price { get; set; }
}
Les convertisseurs de valeurs peuvent actuellement convertir uniquement des valeurs vers et à partir d’une seule colonne de base de données. Cette limitation signifie que toutes les valeurs de propriété de l’objet doivent être encodées en une seule valeur de colonne. Cela est généralement géré en sérialisant l’objet au fur et à mesure qu’il se trouve dans la base de données, puis le désérialisant à nouveau en sortie. Par exemple, à l’aide de System.Text.Json:
modelBuilder.Entity<Order>()
.Property(e => e.Price)
.HasConversion(
v => JsonSerializer.Serialize(v, (JsonSerializerOptions)null),
v => JsonSerializer.Deserialize<Money>(v, (JsonSerializerOptions)null));
Remarque
Nous prévoyons d’autoriser le mappage d’un objet à plusieurs colonnes dans une version ultérieure d’EF Core, en supprimant la nécessité d’utiliser la sérialisation ici. Ceci est suivi par problème GitHub #13947.
Remarque
Comme dans l’exemple précédent, cet objet valeur est implémenté en tant que struct en lecture seule. Cela signifie qu’EF Core peut prendre une capture instantanée et comparer des valeurs sans problème. Pour plus d’informations, consultez comparaisons de valeurs.
Collections de primitives
La sérialisation peut également être utilisée pour stocker une collection de valeurs primitives. Par exemple :
public class Post
{
public int Id { get; set; }
public string Title { get; set; }
public string Contents { get; set; }
public ICollection<string> Tags { get; set; }
}
Utilisation de System.Text.Json à nouveau :
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>
représente un type de référence mutable. Cela signifie qu’un ValueComparer<T> est nécessaire pour qu’EF Core puisse suivre et détecter correctement les modifications. Pour plus d’informations, consultez comparaisons de valeurs.
Collections d’objets valeur
En combinant les deux exemples précédents, nous pouvons créer une collection d’objets valeur. Par exemple, considérez un type de AnnualFinance
qui modélise les finances de blog pour une seule année :
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);
}
Ce type compose plusieurs des types Money
que nous avons créés précédemment :
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
}
Nous pouvons ensuite ajouter une collection de AnnualFinance
à notre type d’entité :
public class Blog
{
public int Id { get; set; }
public string Name { get; set; }
public IList<AnnualFinance> Finances { get; set; }
}
Utilisez à nouveau la sérialisation pour stocker ceci :
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()));
Remarque
Comme précédemment, cette conversion nécessite une ValueComparer<T>. Pour plus d’informations, consultez comparaisons de valeurs.
Objets valeur en tant que clés
Parfois, les propriétés de clé primitive peuvent être encapsulées dans des objets valeur pour ajouter un niveau supplémentaire de sécurité de type lors de l’attribution de valeurs. Par exemple, nous pourrions implémenter un type de clé pour les blogs et un type de clé pour les publications :
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; }
}
Ceux-ci peuvent ensuite être utilisés dans le modèle de domaine :
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; }
}
Notez que Blog.Id
ne peut pas être affecté accidentellement à un PostKey
, et Post.Id
ne peut pas être affecté accidentellement à un BlogKey
. De même, la propriété de clé étrangère Post.BlogId
doit être affectée à un BlogKey
.
Remarque
L’affichage de ce modèle ne signifie pas que nous le recommandons. Déterminez soigneusement si ce niveau d’abstraction aide ou entrave votre expérience de développement. Envisagez également d’utiliser des navigations et des clés générées au lieu de traiter directement les valeurs de clé.
Ces propriétés de clé peuvent ensuite être mappées à l’aide de convertisseurs de valeurs :
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);
});
}
Remarque
Les propriétés de clé avec conversions peuvent uniquement utiliser les valeurs de clé générées à partir d’EF Core 7.0.
Utiliser ulong pour timestamp/rowversion
SQL Server prend en charge la concurrence automatique optimiste à l’aide de colonnes de rowversion
/timestamp
binaires de 8 octets. Celles-ci sont toujours lues et écrites dans la base de données à l’aide d’un tableau de 8 octets. Toutefois, les tableaux d’octets sont un type de référence mutable, ce qui les rend quelque peu douloureux à traiter. Les convertisseurs de valeurs permettent au rowversion
d’être mappés à une propriété ulong
, qui est beaucoup plus appropriée et facile à utiliser que le tableau d’octets. Par exemple, considérez une entité Blog
avec un jeton d’accès concurrentiel ulong :
public class Blog
{
public int Id { get; set; }
public string Name { get; set; }
public ulong Version { get; set; }
}
Cela peut être mappé à une colonne rowversion
SQL Server à l’aide d’un convertisseur de valeurs :
modelBuilder.Entity<Blog>()
.Property(e => e.Version)
.IsRowVersion()
.HasConversion<byte[]>();
Spécifier dateTime.Kind lors de la lecture des dates
SQL Server ignore l’indicateur de DateTime.Kind lors du stockage d’un DateTime en tant que datetime
ou datetime2
. Cela signifie que les valeurs DateTime qui reviennent de la base de données ont toujours une DateTimeKind de Unspecified
.
Les convertisseurs de valeur peuvent être utilisés de deux manières pour résoudre ce problème. Tout d’abord, EF Core a un convertisseur de valeur qui crée une valeur opaque de 8 octets qui conserve l’indicateur de Kind
. Par exemple :
modelBuilder.Entity<Post>()
.Property(e => e.PostedOn)
.HasConversion<long>();
Cela permet aux valeurs DateTime avec différents indicateurs Kind
d’être mélangés dans la base de données.
Le problème avec cette approche est que la base de données n’a plus de colonnes reconnaissables datetime
ou datetime2
. Il est donc courant de toujours stocker l’heure UTC (ou, moins souvent, l’heure locale), puis d’ignorer l’indicateur Kind
ou de le définir sur la valeur appropriée à l’aide d’un convertisseur de valeurs. Par exemple, le convertisseur ci-dessous garantit que la valeur DateTime
lue à partir de la base de données aura la DateTimeKind UTC
:
modelBuilder.Entity<Post>()
.Property(e => e.LastUpdated)
.HasConversion(
v => v,
v => new DateTime(v.Ticks, DateTimeKind.Utc));
Si une combinaison de valeurs locales et UTC est définie dans des instances d’entité, le convertisseur peut être utilisé pour effectuer une conversion appropriée avant l’insertion. Par exemple :
modelBuilder.Entity<Post>()
.Property(e => e.LastUpdated)
.HasConversion(
v => v.ToUniversalTime(),
v => new DateTime(v.Ticks, DateTimeKind.Utc));
Remarque
Envisagez soigneusement d’unifier tout le code d’accès à la base de données pour utiliser l’heure UTC à tout moment, uniquement en traitant l’heure locale lors de la présentation des données aux utilisateurs.
Utiliser des clés de chaîne non sensibles à la casse
Certaines bases de données, y compris SQL Server, effectuent des comparaisons de chaînes non sensibles à la casse par défaut. .NET, d’autre part, effectue des comparaisons de chaînes sensibles à la casse par défaut. Cela signifie qu’une valeur de clé étrangère comme « DotNet » correspond à la valeur de clé primaire « dotnet » sur SQL Server, mais ne la correspondra pas dans EF Core. Un comparateur de valeurs pour les clés peut être utilisé pour forcer EF Core à des comparaisons de chaînes qui ne respectent pas la casse, comme dans la base de données. Par exemple, considérez un modèle de blog/billets avec des clés de chaîne :
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; }
}
Cela ne fonctionnera pas comme prévu si certaines des valeurs Post.BlogId
ont une casse différente. Les erreurs provoquées par cela dépendent de ce que fait l’application, mais impliquent généralement des graphiques d’objets qui ne sont pas corrigés correctement, et/ou des mises à jour qui échouent, car la valeur de la clé de domaine complet est incorrecte. Un comparateur de valeurs peut être utilisé pour corriger ce problème :
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);
});
}
Remarque
Les comparaisons de chaînes .NET et les comparaisons de chaînes de base de données peuvent différer en plus de la sensibilité de la casse. Ce modèle fonctionne pour les clés ASCII simples, mais peut échouer pour les clés avec n’importe quel type de caractères spécifiques à la culture. Pour plus d’informations, consultez classements et respect de la casse.
Gérer les chaînes de base de données de longueur fixe
L’exemple précédent n’a pas besoin d’un convertisseur de valeur. Toutefois, un convertisseur peut être utile pour les types de chaînes de base de données de longueur fixe comme char(20)
ou nchar(20)
. Les chaînes de longueur fixe sont rembourrées à leur longueur complète chaque fois qu’une valeur est insérée dans la base de données. Cela signifie qu’une valeur clé de «dotnet
» sera lue à partir de la base de données sous la forme «dotnet..............
», où .
représente un espace. Cela ne comparera pas correctement les valeurs de clé qui ne sont pas rembourrées.
Un convertisseur de valeurs peut être utilisé pour découper le remplissage lors de la lecture des valeurs de clé. Cela peut être combiné avec le comparateur de valeurs dans l’exemple précédent pour comparer correctement les clés ASCII qui ne respectent pas la casse fixe. Par exemple :
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);
});
}
Chiffrer les valeurs de propriété
Les convertisseurs de valeurs peuvent être utilisés pour chiffrer les valeurs de propriété avant de les envoyer à la base de données, puis de les déchiffrer en sortie. Par exemple, en utilisant l’inversion de chaîne comme substitut d’un algorithme de chiffrement réel :
modelBuilder.Entity<User>().Property(e => e.Password).HasConversion(
v => new string(v.Reverse().ToArray()),
v => new string(v.Reverse().ToArray()));
Remarque
Il n’existe actuellement aucun moyen d’obtenir une référence à l’état DbContext actuel ou à un autre état de session, à partir d’un convertisseur de valeurs. Cela limite les types de chiffrement qui peuvent être utilisés. Votez pour problème GitHub #11597 de supprimer cette limitation.
Avertissement
Veillez à comprendre toutes les implications si vous déployez votre propre chiffrement pour protéger les données sensibles. Envisagez plutôt d’utiliser des mécanismes de chiffrement prédéfini, tels que Always Encrypted sur SQL Server.
Limites
Il existe quelques limitations connues du système de conversion de valeur :
- Comme indiqué ci-dessus,
null
ne peut pas être convertie. Votez (👍) pour problème GitHub #13850 si c’est quelque chose dont vous avez besoin. - Il n’est pas possible d’interroger des propriétés converties en valeur, par exemple des membres de référence sur le type .NET converti par valeur dans vos requêtes LINQ. Votez (👍) pour problème GitHub #10434 si c’est quelque chose dont vous avez besoin, mais envisagez d’utiliser une colonne JSON à la place.
- Il n’existe actuellement aucun moyen de répartir une conversion d’une propriété en plusieurs colonnes ou vice versa. Votez (👍) pour problème GitHub #13947 si c’est quelque chose dont vous avez besoin.
- La génération de valeur n’est pas prise en charge pour la plupart des clés mappées par le biais de convertisseurs de valeurs. Votez (👍) pour problème GitHub #11597 si c’est quelque chose dont vous avez besoin.
- Les conversions de valeurs ne peuvent pas référencer l’instance DbContext actuelle. Votez (👍) pour problème GitHub #12205 si c’est quelque chose dont vous avez besoin.
- Les paramètres utilisant des types convertis par valeur ne peuvent pas être utilisés actuellement dans les API SQL brutes. Votez (👍) pour problème GitHub #27534 si c’est quelque chose dont vous avez besoin.
La suppression de ces limitations est envisagée pour les futures versions.