Les nouvelles techniques EF Core : Convertisseurs de valeurs

Brouillon

À venir

0 commentaire

Dans cet article nous allons parler des convertisseurs de valeurs, une nouvelle fonctionnalité introduite dans la version 2.1 de Entity Framework Core (EF Core). Avant d'expliquer comment utiliser cette nouveauté et ce qu'elle apporte, nous allons d'abords partir d'une problématique rencontrée dans une application ciblant Entity Framework 6 (EF 6). Le cas utilisé pour élucider la problématique n'est qu'un exemple parmi tant d'autres.

Les différentes parties de la série Nouvelles techniques EF Core :

  1. Introduction
  2. Les convertisseurs de valeurs (cet article)
  3. en cours...

Value Conversions

Problématique rencontrée avec EF 6

Partons d'une classe exemple Person contenant les propriétés Id, FirstName, LastName et Gender. Il s'agit de notre entité EF et se présente comme suit :

public class Person
{
    public int Id { get; set; }

    public string FirstName { get; set; }

    public string LastName { get; set; }

    public Gender? Gender { get; set; }
}

Gender est un type enum et se présente comme suit :

public enum Gender
{
    Female = 1,
    Male
}

La configuration de l'entité est toute simple comme le montre le code de la classe PersonConfiguration :

internal class PersonConfiguration : EntityTypeConfiguration<Person>
{
    public PersonConfiguration()
    {
        this.Property(p => p.FirstName).IsRequired().HasMaxLength(20);
        this.Property(p => p.LastName).IsRequired().HasMaxLength(20);
    }
}

Enfin ci-dessous notre classe MyDbContext dérivant de la classe DbContext. Elle représente notre contexte EF.

public class MyDbContext : DbContext
{
    public DbSet<Person> People { get; set; }

    public MyDbContext() : base("name=MyDbContext")
    {
    }

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        modelBuilder.Configurations.Add(new PersonConfiguration());
    }
}

Après avoir activé la migration et mis à jour la base de données, voilà à quoi ressemble notre table People mappée à notre entité Person :

Les colonnes de la table People

Si nous regardons la colonne Gender de la table People nous remarquons que EF a décidé, par défaut, de tout simplement représenter le type de cette colonne en utilisant le type int de SQL Server. Cela vient du fait que EF s'est basé sur le type sous-jacent de notre type enum qui par défaut, s'il n'est pas spécifié, est un type int de C#.

Les données de la table People

Cette manière de stocker une énumération non combinable apporte son lot de problèmes dans certains cas comme :

  • lorsque nous observons les données de la table, le résultat d'une vue ou d'une requête SQL, il faudra toujours se rappeler à quoi correspondent les différentes valeurs de la colonne Gender de la table People. Pour une énumération comme dans le cas de Gender où le nombre de valeurs possibles n'est pas conséquent alors les retenir en tête est facile. Par contre imaginons une énumération contenant plusieurs valeurs alors le développeur se trouvera à jongler entre les données SQL et la définition de notre énumération dans le code C#. Cela aurait été plus simple de stocker non pas le nom du membre de l’énumération mais un code facile à retenir comme, dans notre cas exemple, utiliser un caractère tel que M pour Male et F pour Female directement dans la table.
  • lorsque nous partons d'une base de données existante où la valeur la colonne Gender est stockée en utilisant un seul caractère (M pour Male et F pour Female) alors pour rendre compatible notre entité Person avec EF nous sommes obligés d'utiliser le type string comme type pour la propriété Gender. Dans ce cas on perd tous les avantages apportés par l'utilisation d'une énumération C#.

Solution de contournement (hack ou workaround) avec EF 6

Il existe plusieurs solutions permettant de pallier les deux problèmes cités précédemment. Dans cette section nous allons parler uniquement de la solution la plus répandue vu qu'elle permet d'avoir un code simple, centralisé au niveau de l'entité concernée et surtout qu'elle permet à elle seule de résoudre les deux problèmes.

La solution suit les étapes suivantes :

  1. configurer notre modèle de telle sorte que EF ignore notre propriété Gender. Ainsi cette dernière ne sera mappée à aucune colonne de la table.
  2. ajouter une nouvelle propriété, nommons-la GenderToString, qui sera mappée directement à la colonne Gender de la table.

Les relations entre les propriétés Gender et GenderToString sont les suivantes :

  • la lecture de la propriété Gender se fera à partir de la propriété GenderToString.
  • à chaque fois que la propriété Gender est renseignée alors la propriété GenderToString l'est automatiquement dans sa version string.

Pour l'étape n° 1 de la solution, on peut utiliser l'attribut d'annotation de données NotMapped sur la propriété concernée mais je préfère passer par une configuration fluent. Dans notre classe PersonConfiguration, il suffit d'ajouter la ligne suivante dans son constructeur par défaut :

this.Ignore(p => p.Gender);

Reste maintenant à modifier notre classe Person pour effectuer l'étape n° 2 de la solution c'est-à-dire l'ajout de la nouvelle propriété GenderToString. Les propriétés Gender et GenderToString doivent ressembler au code ci-dessous :

public string GenderToString { get; private set; }

public Gender? Gender
{
    get
    {
        switch(this.GenderToString)
        {
            case GenderFemale: return Models.Gender.Female;
            case GenderMale: return Models.Gender.Male;
            default: return null;
        }
    }
    set
    {
        switch(value)
        {
            case Models.Gender.Female:
                this.GenderToString = GenderFemale;
                break;

            case Models.Gender.Male:
                this.GenderToString = GenderMale;
                break;

            default:
                this.GenderToString = null;
                break;
        }
    }
}

GenderFemale et GenderMale sont des constantes privées définies au niveau de la classe Person. Elles contiennent respectivement les valeurs F et M.

Il faut aussi noter que GenderToString doit être mappée à une colonne de nom Gender et de type char à un seul caractère. Ce qui est facilement configurable avec la ligne suivante :

this.Property(p => p.GenderToString).HasColumnName("Gender").HasMaxLength(1).IsFixedLength();

Au final le constructeur de notre classe PersonConfiguration doit ressembler à ça :

internal class PersonConfiguration : EntityTypeConfiguration<Person>
{
    public PersonConfiguration()
    {
        this.Property(p => p.FirstName).IsRequired().HasMaxLength(20);
        this.Property(p => p.LastName).IsRequired().HasMaxLength(20);
        this.Property(p => p.GenderToString).HasColumnName("Gender").HasMaxLength(1).IsFixedLength();
        this.Ignore(p => p.Gender);
    }
}

Ci-dessous à quoi ressemble les colonnes de la table People après migration de la base de données suite aux modifications apportées :

People Columns After Modifications

Ci-dessous un exemple des données de la table People :

People Data After Modifications

Si vous avez bien suivi jusqu'ici alors le problème qu'on a essayé de résoudre tout au long de cette section consiste à pouvoir utiliser une donnée sous une forme (ex : utiliser l’énumération Gender) dans le code et pouvoir la stocker sous une autre forme (M pour Gender.Male et F pour Gender.Female) dans la base de données.

EF intègre des conversions automatiques (cela dépend du provider utilisé) entre les types simples comme int, string, byte[] etc vers les types correspondants int, varchar, image etc dans la base de données cible. Dès lors qu'on veut faire une conversion qui n'est pas dans les différents cas de conversions intégrées, EF aura besoin de l'aide du développeur comme c'est actuellement le cas avec notre exemple pour convertir les valeurs possibles de l'énumération Gender vers un seul caractère M ou F dans la table de la base de données.

Solution élégante avec les convertisseurs de valeurs de EF Core

Dans la solution de contournement proposée dans la section précédente, nous voyons que la propriété GenderToString a uniquement été créée dans le but d'être uniquement utilisée pour le stockage dans la base de données. De plus nous avons déclaré son modificateur comme étant privé (la propriété est en lecture seule), cela pour empêcher toute modification de cette propriété à l'extérieur de la classe par conséquent obliger les développeurs à passer par la propriété Gender qui est définie avec un type enum. Pour rappel c'est la propriété Gender ignorée par EF, qui s'occupe de renseigner la propriété GenderToString utilisée pour le stockage dans la base de données.

L'idéal c'est de ne pas avoir à ajouter des propriétés telle que GenderToString, d'avoir notre entité Person telle qu'elle a été définie au début l'article et d'avoir une fonctionnalité intégrée à EF nous permettant d'effectuer la conversion vers les différentes formes de données. Dans EF 6 (version actuelle 6.2.0) cela n'est pas encore possible.

Avec EF Core, à partir de la version 2.1 nous disposons de la fonctionnalité de convertisseur de valeurs. Lorsque cette fonctionnalité est utilisée son but sera au moment de :

  • la sauvegarde : de convertir la donnée de la propriété de l'entité du modèle vers un autre type compris par le provider de la base de données.
  • la matérialisation : de convertir la donnée reçue de la base de données dans le type attendu par la propriété de l'entité de notre modèle.

Il faut noter qu'on peut avoir des données de même type des deux côtés (modèle et provider). Par exemple stocker dans la base de données SQL Server une donnée cryptée de type nvarchar (le provider s'attend donc à manipuler un type string contenant la donnée cryptée) et dans notre entité du modèle EF avoir toujours à manipuler la donnée avec un type string contenant la version décryptée. Tout cela sans ajouter une logique au sein de la classe (de l'entité) de notre modèle. Une simple configuration sera nécessaire.

Pour dire à EF Core que nous souhaitons utiliser un convertisseur de valeurs pour une des propriétés de notre entité, nous devons passer par l'une des surcharges de la méthode HasConversion définies dans la classe PropertyBuilder. Nous disposons actuellement de ces 4 surcharges ci-dessous :

public virtual PropertyBuilder<TProperty> HasConversion(ValueConverter converter);
public virtual PropertyBuilder<TProperty> HasConversion<TProvider>();
public virtual PropertyBuilder<TProperty> HasConversion(Type providerClrType);
public virtual PropertyBuilder<TProperty> HasConversion<TProvider>(ValueConverter<TProperty, TProvider> converter);
public virtual PropertyBuilder<TProperty> HasConversion<TProvider>(
    Expression<Func<TProperty, TProvider>> convertToProviderExpression, 
    Expression<Func<TProvider, TProperty>> convertFromProviderExpression
);

Utilisation des convertisseurs intégrés de EF Core

Ce qu'il faut savoir et ainsi s'éviter de réinventer la roue, c'est qu'avec cette nouvelle fonctionnalité, EF Core nous fournit un lot de convertisseurs intégrés, prêts à l'emploi, qui ne demandent qu'à être utilisés. Nous allons voir dans ce qui suit comment appliquer l'un de ses convertisseurs intégrés.

Revenons à notre cas exemple. L'entité Person et l’énumération Gender doivent rester telles qu'elles ont été définies au début de l'article à savoir comme dans le code ci-dessous :

public class Person
{
    public int Id { get; set; }

    public string FirstName { get; set; }

    public string LastName { get; set; }

    public Gender? Gender { get; set; }
}

public enum Gender
{
    Female = 1,
    Male
}

Avec EF Core voici à quoi ressemble notre classe de configuration pour l'entité Person :

internal class PersonConfiguration : IEntityTypeConfiguration<Person>
{
    public void Configure(EntityTypeBuilder<Person> builder)
    {
        builder.Property(p => p.FirstName).IsRequired().HasMaxLength(20);
        builder.Property(p => p.LastName).IsRequired().HasMaxLength(20);
    }
}

La problématique définie au début de l'article est de pouvoir stocker la valeur de notre énumération comme chaîne de caractères dans la base de données. Le convertisseur intégré à EF Core permettant d'effectuer cette tâche facilement est la classe générique EnumToStringConverter<T>. Le template T doit être un type représentant une énumération (un type enum).

Pour appliquer ce convertisseur à la propriété Gender de notre entité Person on utilisera la surcharge ci-dessous, celle recevant une instance d'une classe dérivée (en l’occurrence EnumToStringConverter<T>) de la classe abstraite ValueConverter :

public virtual PropertyBuilder<TProperty> HasConversion(ValueConverter converter);

Ainsi dans la méthode Configure de notre classe de configuration PersonConfiguration il faut ajouter la ligne ci-dessous :

builder.Property(p => p.Gender).HasConversion(new EnumToStringConverter<Gender>());

Étant donné que le type attendu par le provider EF (ici SQL Server) est un type string, celui-ci va appliquer ses propres conventions donc utiliser nvarchar(max) par défaut pour la colonne Gender. Pour éviter cela, nous pouvons spécifier juste après que notre propriété doit être mappée à une colonne de taille maximum définie à 6 caractères par exemple et qu'elle ne contiendra aucun caractère Unicode. Nous aurons besoin des méthodes HasMaxLength et IsUnicode. Ainsi la ligne précédente deviendra :

builder.Property(p => p.Gender).HasConversion(new EnumToStringConverter<Gender>()).HasMaxLength(6).IsUnicode(false);

Au final, après ajout d'une migration et mise à jour de la base de données, nous aurons notre table People qui ressemblera à ça :

EF Core - People Columns

Les données de la table People seront comme suit :

EF Core - People Data

Je sais que la problématique initiale c'est de stocker les valeurs F pour Female et M pour Male dans la table. Je montrerai comment résoudre cela plus loin lorsque nous aborderons les convertisseurs personnalisés.

La classe EnumToStringConverter<Gender> n'est qu'un convertisseur intégré à EF Core parmi tant d'autres. On peut citer un autre convertisseur comme BytesToStringConverter. Ce dernier permet de travailler avec un tableau de type byte[] dans votre entité EF Core (côté modèle) et de de stocker une chaîne de caractères encodée en base 64 dans la base de données (côté provider). Par exemple au lieu de créer physiquement le fichier puis de stocker le chemin associé à la photo de profil d'un membre, on récupère l'image fournie par l'utilisateur sous forme d'un tableau byte[] manipulé en C# et la stocker en base 64 dans la base de données. La conversion est gérée automatiquement par EF Core à travers ce convertisseur. Encore une fois aucun code personnalisé n'est nécessaire.

Vous pouvez trouver la liste complète des convertisseurs actuellement disponible dans la version 2.1 de EF Core ici.

Pour ses convertisseurs intégrés déjà fournis, EF Core nous permet de faire une conversion avec beaucoup plus de simplicité. Dans notre cas exemple nous avons spécifié que nous voulons utiliser le convertisseur EnumToStringConverter<T>. Parmi les surcharges de la méthode HasConversion citées plus haut, nous avons les deux méthodes ci-dessous :

public virtual PropertyBuilder<TProperty> HasConversion<TProvider>();
public virtual PropertyBuilder<TProperty> HasConversion(Type providerClrType);

Avec ces deux surcharges, il suffit de spécifier le type supporté donc compris par le provider pour que EF Core puisse détecter de façon automatique le convertisseur idéal pour la conversion de la donnée récupérée du modèle vers la donnée cible attendue par le provider. Avec les deux informations renseignées dans les surcharges ci-dessus à savoir TProperty (côté modèle) vers TProvider ou providerClrType (côté provider), EF Core saura déterminer, en choisissant dans son lot de convertisseurs intégrés, celui répondant le mieux entre les deux types. Il est important de noter que le procédé du choix automatique du convertisseur par EF Core est possible si et seulement il s'agit un convertisseur intégré (fourni avec la libraire EF Core) existe entre le TProperty vers TProvider ou providerClrType.

Dans notre cas exemple où nous avons essayé de faire la conversion de l’énumération Gender vers le type string, nous pouvons remplacer la méthode HasConversion(new EnumToStringConverter<Gender>()) par celle-ci HasConversion<string>(). Ainsi nous aurons :

builder.Property(p => p.Gender).HasConversion<string>().HasMaxLength(6).IsUnicode(false);

à la place de :

builder.Property(p => p.Gender).HasConversion(new EnumToStringConverter<Gender>()).HasMaxLength(6).IsUnicode(false);

Nous n'avons pas utilisé la surcharge non générique HasConversion(Type providerClrType) (ce qui amènerait à écrire HasConversion(typeof(string))) parce que tout simple nous avons connaissance, au moment de la compilation, du type que nous allons manipuler alors autant passer par la méthode générique.

Jusqu'ici nous avons utilisé la configuration fluent pour configurer notre convertisseur, soit en faisant une instanciation concrète de la classe, soit en utilisant la détection automatiquement du convertisseur. Dans le dernier cas, EF Core nous permet aussi de passer par un attribut d'annotation de données. Ainsi au lieu d'utiliser HasConversion<string>() nous pouvons décorer notre propriété Gender avec l'attribut Column en spécifiant le nom du type attendu dans la base données à travers la propriété TypeName. Le code HasConversion<string>().HasMaxLength(6).IsUnicode(false) peut entièrement être remplacé par l'utilisation du code suivant :

[Column(TyepName = "varchar(6)")]
public Gender? Gender { get; set; }

Vu que je préfère toujours utiliser la configuration fluent, je n'utiliserai pas les attributs d'annotation de données dans la version finale de mon code. Je trouve que les attributs d'annotation tel que [Column(TyepName = "varchar(6)")] non seulement polluent la classe mais surtout empêchent de séparer l'entité de sa configuration liée à la base de données et cela peut être évité avec les classes de configuration telle que PersonConfiguration. De plus utiliser cette manière de faire pour spécifier un convertisseur n'est pas vraiment explicite sur ce que le développeur essaie de faire alors que la solution utilisant la méthode HasConversion montre l'intention du développeur qui est de vouloir explicitement effectuer une conversion pour la propriété concernée.

Voilà au final à quoi ressemble la méthode Configure de notre classe PersonConfiguration :

public void Configure(EntityTypeBuilder<Person> builder)
{
    builder.Property(p => p.FirstName).IsRequired().HasMaxLength(20);
    builder.Property(p => p.LastName).IsRequired().HasMaxLength(20);
    builder.Property(p => p.Gender).HasConversion<string>().HasMaxLength(6).IsUnicode(false);
}

Mise en place d'un convertisseur de valeurs personnalisé

La section précédente a expliqué comment utiliser les convertisseurs de valeurs intégrés à EF Core à travers l'utilisation de la classe EnumToStringConverter<T>. Ce convertisseur de valeurs utilisé permet, tout simplement, de stocker le membre de l’énumération en tant que chaîne de caractères en lieu et place de l'entier du type sous-jacent. La chaîne de caractères ne correspond à rien d'autre qu'au nom du membre de l’énumération. Ce qui explique pourquoi nous avons Female au lieu de F et Male à la place de M si nous observons les données de la table People. Ainsi pour résoudre la problématique initialement posée par l'article, la solution est de passer par les convertisseurs de valeurs personnalisés que nous allons écrire dans si peu.

La solution la plus simple est d'utiliser la surcharge suivante de la méthode HasConversion :

public virtual PropertyBuilder<TProperty> HasConversion<TProvider>(
    Expression<Func<TProperty, TProvider>> convertToProviderExpression, 
    Expression<Func<TProvider, TProperty>> convertFromProviderExpression
);

Cette surcharge attend deux paramètres. Tous les deux étant des expressions Lambda :

  • le premier paramètre convertToProviderExpression s'attend à une expression recevant en entrée la valeur de la propriété du modèle pour la convertir en une autre valeur dont le type sera en adéquation avec le provider.
  • le deuxième paramètre convertFromProviderExpression s'occupe de faire le sens inverse de ce qui est décrit pour le premier paramètre.

Ainsi pour notre cas exemple, on va pouvoir écrire le code suivant :

builder.Property(p => p.Gender)
    .HasConversion(
        modelValue => ToProvider(modelValue),
        providerValue => FromProvider(providerValue)
    )
    .HasMaxLength(1)
    .IsFixedLength(true)
    .IsUnicode(false);

Les méthodes ToProvider et FromProvider sont des méthodes statiques définies au niveau de la classe de configuration PersonConfiguration. Leurs implémentions sont les suivantes :

private static string ToProvider(Gender? modelValue)
{
    if (!modelValue.HasValue) return null;
    switch (modelValue.Value)
    {
        case Gender.Female: return GenderFemale;
        case Gender.Male: return GenderMale;
        default: return null;
    }
}

private static Gender? FromProvider(string providerValue)
{
    if (string.IsNullOrWhiteSpace(providerValue)) return null;
    switch (providerValue)
    {
        case GenderFemale: return Gender.Female;
        case GenderMale: return Gender.Male;
        default: return null;
    }
}

Si nous regardons les implémentations de ces méthodes elles ressemblent aux logiques qu'on avait définies dans l'accesseur et le modificateur de la propriété Gender de notre solution initialement proposée pour une application ciblant EF 6. Grâce à la nouvelle fonctionnalité de EF Core, ces logiques sont maintenant absentes de notre classe Person et sont déplacées à l'endroit où elles doivent être c'est-à-dire dans la classe PersonConfiguration.

Nous en avons profité pour spécifier que la colonne mappée à la propriété Gender est un champ fixe, de type char et ne contient pas de caractère Unicode respectivement grâce aux méthodes IsFixedLength(true), HasMaxLength(1) et IsUnicode(false). Le schéma et les données de la table People sont maintenant identiques à celui et celle résultant de la version finale utilisée pour une application ciblant EF 6.

Avec cette solution simpliste la logique de notre convertisseur n'est pas réutilisable. Imaginons que cette façon de stocker notre énumération soit commune à plusieurs projets alors il serait bien de pouvoir fournir une classe qui contient les logiques des deux méthodes précédentes dans une librairie que les différents projets pourraient utiliser à travers un package Nuget local par exemple. Il est possible de créer une telle classe et de l'utiliser comme ça a été le cas avec la classe EnumToStringConverter<T> en prenant soin de la faire dériver de l'une des deux classes suivantes :

  • ValueConverter : il s'agit d'une classe abstraite. En héritant de cette classe, le développeur aura à implémenter pas mal de membres abstraits ce qui peut être fastidieux.
  • ValueConverter<TModel, TProvider> : il s'agit d'une classe générique dérivée de la classe abstraite ValueConverter. Tous les membres abstraits de cette dernière y sont implémentés. Par exemple la classe EnumToStringConverter<T> dérive de cette classe générique. Grosso modo nous n'avons pas grand-chose à faire mis à part fournir les logiques de conversion nécessaires à l'utilisation du convertisseur.

Vous l'aurez compris nous allons partir d'une classe, nommons-la GenderToStringConverter, dérivant de la classe générique ValueConverter<Gender?, string>. Le code de la classe de notre convertisseur personnalisé GenderToStringConverter est le suivant :

public class GenderToStringConverter : ValueConverter<Gender?, string>
{
    private const string GenderFemale = "F";
    private const string GenderMale = "M";

    public GenderToStringConverter() : base(ToProviderExpression(), FromProviderExpression())
    {
    }

    private static Expression<Func<Gender?, string>> ToProviderExpression() => modelValue => ToProvider(modelValue);

    private static Expression<Func<string, Gender?>> FromProviderExpression() => providerValue => FromProvider(providerValue);

    private static string ToProvider(Gender? modelValue)
    {
        if (!modelValue.HasValue) return null;
        switch (modelValue.Value)
        {
            case Gender.Female: return GenderFemale;
            case Gender.Male: return GenderMale;
            default: return null;
        }
    }

    private static Gender? FromProvider(string providerValue)
    {
        if (string.IsNullOrWhiteSpace(providerValue)) return null;
        switch (providerValue)
        {
            case GenderFemale: return Gender.Female;
            case GenderMale: return Gender.Male;
            default: return null;
        }
    }
}

Les logiques de conversions pour notre énumération Gender sont bien présentes dans les méthodes statiques ToProvider et FromProvider. Étant donné que le constructeur de la classe de base doit recevoir des expressions Lambda alors nous avons créé deux méthodes ToProviderExpression et FromProviderExpression. Le constructeur de notre classe GenderToStringConverter ne fait que délégué tout le processus de conversion de la valeur à la classe de base à travers l'appel au constructeur en utilisant l'instruction ci-dessous :

base(ToProviderExpression(), FromProviderExpression())

Pour utiliser notre nouvelle classe rien de plus simple. Il suffit juste de faire appel à la méthode HasConversion et de lui passer une instance de la classe GenderToStringConverter comme suit :

builder.Property(p => p.Gender).HasConversion(new GenderToStringConverter());

Ci-dessous le code complet de la classe PersonConfiguration sans les logiques de conversion maintenant présentes dans la classe GenderToStringConverter :

internal class PersonConfiguration : IEntityTypeConfiguration<Person>
{
    public void Configure(EntityTypeBuilder<Person> builder)
    {
        builder.Property(p => p.FirstName).IsRequired().HasMaxLength(20);
        builder.Property(p => p.LastName).IsRequired().HasMaxLength(20);
        builder.Property(p => p.Gender)
            .HasConversion(new GenderToStringConverter())
            .HasMaxLength(1)
            .IsFixedLength(true)
            .IsUnicode(false);
    }
}

Remarques utiles

D'après la documentation de EF Core, il existe actuellement certaines limitations liées à l'utilisation des convertisseurs de valeurs dans la version 2.1. Ces limitations sont les suivantes :

  • Il faut savoir qu'aucune valeur null ne subit une transformation, une conversion. Le convertisseur ne sera tout simplement pas exécuté si EF Core trouve que la donnée à convertir correspond à une référence null. Dans le convertisseur personnalisé de notre cas exemple nous avons pris le soin d'appliquer un code défensif donc de bien prendre en compte les valeurs null en entrée au cas où dans les prochaines versions cette limitation serait levée.
  • On note l'absence d'une configuration globale. Actuellement les convertisseurs de valeurs s'appliquent directement sur la propriété pour laquelle on veut faire des conversions de valeurs. Dans notre cas exemple il s'agit de la propriété Gender de l'entité Person. Si dans notre modèle EF Core nous disposons de plusieurs entités ayant des propriétés de ce type qui nécessitent ce genre de conversions nous devons faire la configuration pour chacune de ses propriétés. Vous conviendrez qu'avoir une configuration globale permettant d'appliquer un convertisseur de valeurs spécifique à un ensemble de propriétés similaires est vraiment nécessaire dans ces cas-là. Nous allons proposer une solution pour y pallier sous peu en attendant que EF Core, dans ses prochaines versions, nous fournisse une alternative complètement intégrée.
  • EF Core pourrait rencontrer quelques complications pour effectuer correctement la transformation SQL de certaines requêtes LINQ où des convertisseurs de valeurs sont impliqués. Dans ces cas précis EF Core émettra des avertissements vous les signalant au moment de la compilation. Pour éviter que les développeurs ignorent ces avertissements nous pouvons configurer la build pour qu'elle compile notre application en considérant tous les avertissements (ou plus spécifiquement certains d'entre eux) comme des erreurs de compilation.

Application globale d'un convertisseur de valeurs

Comme noter dans le deuxième point de la section précédente, EF Core ne nous permet pas d'appliquer un convertisseur de valeurs de façon globale sur plusieurs propriétés identiques. La version actuelle nous oblige à configurer le convertisseur de valeurs en y allant propriété par propriété.

Pour pallier ce manque, nous pouvons dans la méthode OnModelCreating de notre contexte EF Core parcourir toutes les entités de notre modèle et détecter les propriétés concernées puis y appliquer les convertisseurs de valeurs voulus. Ci-dessous le code de la méthode OnModelCreating :

protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    modelBuilder.ApplyConfiguration(new PersonConfiguration());

    foreach (var entity in modelBuilder.Model.GetEntityTypes())
    {
        foreach (var property in entity.GetProperties().Where(p => p.ClrType == typeof(Gender) || p.ClrType == typeof(Gender?)))
        {
            property.SetValueConverter(new GenderToStringConverter());
            property.SetMaxLength(1);
            property.IsUnicode(false);
            property.SqlServer().ColumnType = "char";
        }
    }
}

Nous parcourons la liste des entités et pour chaque entité nous détectons la présence d'une propriété de type Gender ou Gender? pour y appliquer :

  • bien sûr notre convertisseur via property.SetValueConverter(new GenderToStringConverter()).
  • fixer la taille du champ à 1 seul caractère avec l'instruction property.SetMaxLength(1).
  • spécifier qu'on utilisera aucun caractère Unicode avec l'instruction property.IsUnicode(false).
  • par défaut le provider SQL choisit varchar comme type pour les chaînes de caractères non-Unicode, alors nous forçons l'utilisation du type char grâce à property.SqlServer().ColumnType = "char".

Ainsi dans la classe PersonConfiguration on peut retirer la configuration de la propriété Gender de notre entité Person et au final avoir le code ci-dessous :

internal class PersonConfiguration : IEntityTypeConfiguration<Person>
{
    public void Configure(EntityTypeBuilder<Person> builder)
    {
        builder.Property(p => p.FirstName).IsRequired().HasMaxLength(20);
        builder.Property(p => p.LastName).IsRequired().HasMaxLength(20);
    }
}

Conclusion

Dans cet article nous avons vu, qu'avant la venue de EF Core (plus précisément dans sa version 2.1), qu'il était nécessaire de bidouiller donc compliquer d'utiliser dans notre modèle un type spécifique pour notre propriété et d'utiliser un autre type voire transformer la donnée avant de la stocker dans la base de données.

Avec les convertisseurs de valeurs, nous n'avons plus besoin de polluer nos entités juste parce que nous voulons être en adéquation avec la façon dont nos données sont stockées dans nos tables. Le code de nos entités peuvent utiliser tous les avantages des types de la plateforme et cela tant que nous fournissons à EF Core les convertisseurs de valeurs nécessaires.

Code source

Vous trouverez le code source de l'article dans le dépôt GitHub ici. Vous y trouverez les dossiers suivants :

  • EF6Samples : contient une solution VS avec le projet ValueConversionSamples pour la version ciblant EF 6.
  • EFCoreSamples : contient une solution VS avec le projet ValueConversionSamples pour la version ciblant EF Core.

Liens utiles

.

Holty Samba Sow

Titulaire d'un Master en Document Électronique et Flux d'informations de l'université Paris X - Nanterre, Holty a rejoint SoftFluent en 2012 avec 3 ans d'expérience. Ancien modérateur Développez.com, il est passionné par les applications Web, du Front-end au Back-end.

Profil de l'auteur