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, floatou 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 :

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 DateTimeKindUTC:

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.