Les nouvelles techniques EF Core : Constructeurs Paramétrés

Brouillon

À venir

0 commentaire

Dans cet article nous allons parler des constructeurs paramétrés, 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
  3. Les constructeurs paramétrés (cet article)
  4. en cours...
  5. en cours...
  6. en cours...

Les constructeurs paramétrés

Problématique rencontrée avec EF 6

Prenons comme cas exemple un tweet représenté en version minimaliste par la classe Tweet avec les propriétés suivantes Id, UserId et Content. Ces propriétés représentent respectivement l'identifiant, l'identifiant de l'auteur et le contenu du tweet.

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

    public int UserId { get; set; }

    public string Content { get; set; }
}

La configuration de notre entité Tweet est définie par la classe TweetConfiguration comme suit :

internal class TweetConfiguration : EntityTypeConfiguration<Tweet>
{
    public TweetConfiguration()
    {
        this.Property(p => p.Content).IsRequired().HasMaxLength(280);
    }
}

La classe MyDbContext dérivant de la classe DbContext représente notre contexte EF.

public class MyDbContext : DbContext
{
    public DbSet<Tweet> Tweets { get; set; }

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

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

Jusqu'ici rien de compliqué. Vous pouvez créer une migration et mettre à jour la base de données.

Pour insérer un tweet dans la base de données nous utiliserons tout simplement le code ci-dessous :

using (var context = new MyDbContext())
{
    var tweet = new Tweet
    {
        UserId = 42,
        Content = "Lorem ipsum dolor sit amet, ipsum noster efficiendi te sit. Mea an modus vitae mediocritatem, usu ferri vocent dissentiunt at."
    };

    context.Tweets.Add(tweet);
    context.SaveChanges();
}

Pour la lecture des tweets insérés dans la base de données voici le code permettant de le faire :

using (var context = new MyDbContext())
{
    var tweets = context.Tweets.ToList();

    foreach(var tweet in tweets)
    {
        Console.WriteLine($"Id: {tweet.Id}");
        Console.WriteLine($"User Id: {tweet.UserId}");
        Console.WriteLine($"Content: {tweet.Content}");
        Console.WriteLine("======");
    }
}

Il faudra noter les points suivants :

  • dans le code d'insertion d'un tweet, nous n'avons pas assigné une valeur à la propriété Id. La propriété Id est mappée à la colonne représentant la clé primaire de notre table Tweet. Dans notre cas exemple il s'agit d'une colonne auto-incrémentée lorsqu'on utilise le provider SQL Server. Par conséquent cette colonne comme toute clé primaire n'a aucune raison d'être modifiée. La base de données s'occupe d'assigner une valeur à notre clé primaire. Ainsi la propriété Id peut être définie comme une propriété en lecture seule dans le code C#.
  • l'auteur d'un tweet est spécifié via la propriété UserId. L'auteur ne changera jamais donc cette propriété ne sera jamais modifiée une fois le tweet créé. Normalement dans un cas réel, elle serait forcément mappée à une colonne de la table Tweet représentant une clé étrangère référençant une autre table stockant les utilisateurs. Pour notre cas exemple on n'a pas besoin d'aller aussi loin dans la modélisation pour élucider la problématique que l'article veut soulever. Bref ! Au final, cette propriété UserId, comme la propriété Id, peut aussi être définie comme une propriété en lecture seule.
  • si vous utilisez Twiiter, on sait tous sur ce réseau social (au moment de la rédaction de cet article) que le contenu d'un tweet ne peut être modifié. Une fois qu'on balance son tweet avec plein de coquilles alors la seule possibilité est la suppression du tweet et d'en écrire un autre. Suivant cette règle et éviter qu'un développeur fasse une assignation inattendue à la propriété Content de la classe Tweet nous allons aussi mettre celle-ci en lecture seule.

En faisant passer toutes les propriétés de la classe Tweet en lecture seule, cela nous oblige à définir un constructeur au travers duquel les propriétés concernées doivent être assignées.

Prenant en compte les trois points cités précédemment et ainsi éviter qu'un développeur au travers du code modifie l'identifiant, l'auteur ou le contenu d'un tweet, voilà à quoi doit ressembler notre classe Tweet :

public class Tweet
{
    public int Id { get; private set; }

    public int UserId { get; private set; }

    public string Content { get; private set; }

    public Tweet(int userId, string content)
    {
        if (userId <= 0) throw new ArgumentException("User Id must be greater than 0", nameof(userId));
        if (string.IsNullOrWhiteSpace(content)) throw new ArgumentNullException(nameof(content));

        this.UserId = userId;
        this.Content = content;
    }
}

Étant donné qu'il n'y a plus de constructeur par défaut, le code ne compile plus parce que ces modifications impliquent de revenir sur notre code d'insertion d'un tweet (voir plus haut). Le code d'instanciation d'un nouveau tweet doit maintenant passer par le constructeur personnalisé, celui avec des paramètres, comme dans le code ci-dessous :

using (var context = new MyDbContext())
{
    var tweet = new Tweet(
        42,
        "Lorem ipsum dolor sit amet, ipsum noster efficiendi te sit. Mea an modus vitae mediocritatem, usu ferri vocent dissentiunt at."
    );

    context.Tweets.Add(tweet);
    context.SaveChanges();
}

Aucun changement n'est nécessaire pour la lecture des tweets.

Le code compile et l'application peut être exécutée sans soucis. Cependant à l'exécution nous avons l'exception ci-dessous qui est lancée par l'application :

System.Reflection.TargetInvocationException: 'Exception has been thrown by the target of an invocation.'

Inner Exception

InvalidOperationException: The class 'CustomConstructorSamples.Models.Tweet' has no parameterless constructor.

L'exception est déclenchée lors de l'exécution de la ligne ci-dessous :

var tweets = context.Tweets.ToList();

Le problème rencontré ici est que EF 6, lors de la matérialisation des données de la table vers des instances de l'entité Tweet, a besoin qu'un constructeur par défaut (celui sans paramètre) soit présent et cela quel que soit le modificateur utilisé (le plus restrictif étant private) si celui-ci est défini par le développeur. Pour rappel, avec C#, dès lors qu'un constructeur personnalisé est défini alors il rend celui par défaut (celui sans paramètres) automatiquement indisponible tant que le développeur ne l'aura lui-même explicitement défini. En l'absence de constructeur par défaut alors l'exception dans notre cas exemple s'explique vu qu'on a ajouté un constructeur personnalisé avec paramètres dans l'entité Tweet.

Différentes solutions de contournement

La solution la plus simpliste pour contourner ce problème est de tout simplement ajouter le constructeur par défaut requis par EF 6 comme dans le code ci-dessous :

/// <summary>
/// Prevents a default instance of the <see cref="Tweet"/> class from being created.
/// </summary>
/// <remarks>This is used by EF materialization</remarks>
private Tweet()
{
}

Comme vous pouvez le voir :

  • le constructeur par défaut est défini comme privé (mot-clé private) ce qui permet d'éviter toute utilisation de celui-ci à l'extérieur de la classe. À noter que EF 6 n'aura aucun problème à l'utiliser lors de la matérialisation des entités.
  • nous avons ajouté une remarque <remarks> dans la documentation XML du constructeur pour prévenir tous les développeurs susceptibles de passer sur ce code de comprendre pourquoi ce constructeur existe et surtout pourquoi elle n'est référencée nulle part ailleurs.

Le code complet de la classe Tweet :

public class Tweet
{
    public int Id { get; private set; }

    public int UserId { get; private set; }

    public string Content { get; private set; }

    public Tweet(int userId, string content)
    {
        if (userId <= 0) throw new ArgumentException("User Id must be greater than 0", nameof(userId));
        if (string.IsNullOrWhiteSpace(content)) throw new ArgumentNullException(nameof(content));

        this.UserId = userId;
        this.Content = content;
    }

    /// <summary>
    /// Prevents a default instance of the <see cref="Tweet"/> class from being created.
    /// </summary>
    /// <remarks>This is used by EF materialization</remarks>
    private Tweet()
    {
    }
}

L’inconvénient avec cette solution est qu'un développeur qui n'a pas pris la peine de lire la remarque du commentaire XML du constructeur pourra tout simplement le supprimer après avoir observé qu'aucune référence à ce constructeur n'existe dans le code source. Pour éviter cela nous serons obligés de mettre en place un test unitaire dont le seul but est de vérifier que l'instanciation de la classe en utilisant le constructeur par défaut est possible par réflection.

Une idée qui vient souvent à l'esprit d'un développeur qui assure sa veille technologique donc qui est au courant des nouveautés du langage C# dans sa version 6 c'est d'utiliser la fonctionnalité de propriétés automatiques en lecture seule. Étant donné que les propriétés Id, UserId et Content sont en lecture seule ({ get; private set; }) et sont assignés, en partie, via un constructeur alors C# 6 nous permet de tout simplement écrire à la place un accesseur { get; }. En appliquant cette fonctionnalité de C# 6 alors notre classe ressemblera à ceci :

public class Tweet
{
    public int Id { get; }

    public int UserId { get; }

    public string Content { get; }

    /// <summary>
    /// Prevents a default instance of the <see cref="Tweet"/> class from being created.
    /// </summary>
    /// <remarks>This is used by EF materialization</remarks>
    private Tweet()
    {
    }

    public Tweet(int userId, string content)
    {
        if (userId <= 0) throw new ArgumentException("User Id must be greater than 0", nameof(userId));
        if (string.IsNullOrWhiteSpace(content)) throw new ArgumentNullException(nameof(content));

        this.UserId = userId;
        this.Content = content;
    }
}

Cette entité utilisée telle quelle avec EF 6 ne fonctionnera pas du fait des deux points cités ci-dessous :

  • lors de la migration, EF 6 vous lancera une exception :

EntityType 'Tweet' has no key defined. Define the key for this EntityType.
Tweets: EntityType: EntitySet 'Tweets' is based on type 'Tweet' that has no keys defined.

  • du coup essayant de corriger le point précédent en changeant public Id { get; } et remettre public Id { get; private set; }, la migration marchera mais vous verrez que EF 6 aura tout simplement ignoré les propriétés Content et UserId. Vous aurez une table Tweet avec une seule colonne représentant la clé primaire Id. Cela parce que EF 6 veut que toute propriété qui est mappée à une colonne d'une table de la base de données ait défini un modificateur set et cela même si ce dernier possède la visibilité la plus restrictive à savoir private.

Avec ces deux points précédemment cités, la fonctionnalité de propriétés automatiques en lecture seule de C# 6 n'est pas une bonne option lorsque vous codez vos entités avec EF 6.

Il existe une deuxième solution de contournement permettant en plus d'éviter l’inconvénient de la solution précédente. Il faudra supprimer le constructeur personnalisé, celui avec les paramètres, et ajouter une nouvelle méthode statique nommons-la Create comme suit :

public static Tweet Create(int userId, string content)
{
    if (userId <= 0) throw new ArgumentException("User Id must be greater than 0", nameof(userId));
    if (string.IsNullOrWhiteSpace(content)) throw new ArgumentNullException(nameof(content));

    var tweet = new Tweet
    {
        UserId = userId,
        Content = content
    };

    return tweet;
}

Avec cette solution, aucun risque de suppression du constructeur par défaut par un développeur qui penserait que ce dernier n'est utilisé nulle part vu qu’il l'est maintenant à l'intérieur de la méthode statique Create. Cela n'empêche qu'il faudra toujours un test unitaire vérifiant que le constructeur est défini avec le mot-clé private et éviter qu'un développeur aimant la facilité ne le modifie, le définit avec le mot-clé public pour le rendre accessible de l'extérieur. Toute instanciation d'une nouvelle entité doit passer par la méthode statique Create.

Le code complet de la classe Tweet avec la nouvelle méthode Create :

public class Tweet
{
    public int Id { get; private set; }

    public int UserId { get; private set; }

    public string Content { get; private set; }

    /// <summary>
    /// Prevents a default instance of the <see cref="Tweet"/> class from being created.
    /// </summary>
    /// <remarks>This is used by EF materialization</remarks>
    private Tweet()
    {
    }

    public static Tweet Create(int userId, string content)
    {
        if (userId <= 0) throw new ArgumentException("User Id must be greater than 0", nameof(userId));
        if (string.IsNullOrWhiteSpace(content)) throw new ArgumentNullException(nameof(content));

        var tweet = new Tweet
        {
            UserId = userId,
            Content = content
        };

        return tweet;
    }
}

Étant donné qu'il n'y a plus de constructeur personnalisé alors nous sommes obligés de repasser sur notre code permettant d'insérer des tweets dans la base de données. Il faudra maintenant faire appel à la méthode Create créée précédemment comme suit :

using (var context = new MyDbContext())
{
    var tweet = Tweet.Create(
        42,
        "Lorem ipsum dolor sit amet, ipsum noster efficiendi te sit. Mea an modus vitae mediocritatem, usu ferri vocent dissentiunt at."
    );

    context.Tweets.Add(tweet);
    context.SaveChanges();
}

Constructeurs paramétrés de EF Core

Dans la section précédente nous avons pu voir deux solutions permettant de contourner la problématique que nous avons essayé de résoudre avec EF 6 à savoir utiliser un seul et unique constructeur personnalisé pour la création de notre entité. Cela est actuellement impossible avec EF 6 (dans la version 6.2.3) parce qu'il nous impose la présence d'un constructeur par défaut même si ce dernier est défini avec la visibilité la plus restrictive avec savoir le mot-clé private.

Avec la version 2.1 de EF Core (les versions précédentes de EF Core utilise le même principe que EF 6 lors de la matérialisation) nous n'avons plus besoin d'utiliser ces solutions de contournement décrites dans la section précédente. Plus besoin du constructeur par défaut imposé par EF 6 ainsi que dans les versions antérieures à EF Core 2.1.

Ainsi notre classe Tweet peut être libérée de toutes les lignes d'instructions relatives aux solutions de contournement. Avec EF Core 2.1 notre classe ressemblera tout simplement au code ci-dessous :

public class Tweet
{
    public int Id { get; private set; }

    public int UserId { get; private set; }

    public string Content { get; private set; }

    public Tweet(int userId, string content)
    {
        if (userId <= 0) throw new ArgumentException("User Id must be greater than 0", nameof(userId));
        if (string.IsNullOrWhiteSpace(content)) throw new ArgumentNullException(nameof(content));

        this.UserId = userId;
        this.Content = content;
    }
}

La classe de configuration TweetConfiguration est :

internal class TweetConfiguration : IEntityTypeConfiguration<Tweet>
{
    public void Configure(EntityTypeBuilder<Tweet> builder)
    {
        builder.Property(p => p.Content).IsRequired().HasMaxLength(280);
    }
}

La classe MyDbContext représentant notre contexte EF Core est la suivante :

public class MyDbContext : DbContext
{
    public DbSet<Tweet> Tweets { get; set; }

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
    {
        optionsBuilder
            .UseSqlServer(@"Data Source=(localdb)\mssqllocaldb; Initial Catalog=MyDbContextDB; Integrated Security=true");
    }

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

Notre code d'ajout d'une instance de la classe Tweet se présente comme suit :

using (var context = new MyDbContext())
{
    var tweet = new Tweet(
        42,
        "Lorem ipsum dolor sit amet, ipsum noster efficiendi te sit. Mea an modus vitae mediocritatem, usu ferri vocent dissentiunt at. Pro in scripta accusamus intellegam, quo eleifend contentiones id. Eu brute periculis vis, ei quas suavitate eam, integre eruditi ad qui."
    );

    context.Tweets.Add(tweet);
    context.SaveChanges();
}

Notre code de lecture de nos instances de l'entité Tweet :

using (var context = new MyDbContext())
{
    var tweets = context.Tweets.ToList();

    foreach (var tweet in tweets)
    {
        Console.WriteLine($"Id: {tweet.Id}");
        Console.WriteLine($"User Id: {tweet.UserId}");
        Console.WriteLine($"Content: {tweet.Content}");
        Console.WriteLine("======");
    }
}

Ainsi avec la nouveauté introduite par EF Core, il est possible de créer un constructeur avec paramètres qui sera utilisé lors de la matérialisation de nos entités lorsqu'on exécute nos requêtes Linq To Entities. Alors comment ça marche ? Lors de la matérialisation de nos entités, processus qui se déroule lors la récupération du résultat d'une requête :

  • si EF Core trouve un constructeur avec paramètres et que ces derniers correspondent aux noms (la casse est ignorée) et aux types des propriétés mappées à nos colonnes en base de données alors l'ORM choisit automatiquement ce constructeur et y passera les différentes valeurs récupérées de la base de données.
  • si le point précédent n'est pas satisfait alors EF Core continuera à effectuer tout simplement le même fonctionnement comme dans EF 6 à savoir utiliser le constructeur par défaut et puis assigner unitairement les différentes propriétés mappées à nos colonnes en base de données.

Le constructeur personnalisé peut utiliser la visibilité la plus restrictive private si on le veut. On n'est pas obligé d'avoir un constructeur avec toutes les propriétés mappées à nos colonnes en tant que paramètres de notre constructeur personnalisé. Dans notre cas exemple nous voyons que la propriété Id ne correspond à aucun paramètre de notre constructeur. Aussi il n'est pas obligatoire qu'elle soit une propriété représentant la clé primaire pour ne pas la passer en paramètre du constructeur. La présence ou non d'une propriété mappée dans le constructeur est valable quelle que soit le rôle de la propriété mappée dans la base de données. Les propriétés mappées ne correspondant à aucun paramètre du constructeur seront tout bonnement assignées par EF Core, via réflection, en faisant appel à leur modificateur set.

Avec EF Core 2.1 nous venons de résoudre la problématique présentée dans la section précédente avec EF 6. Nous pouvons aller plus loin avec EF Core à savoir nous pouvons maintenant utiliser la fonctionnalité C# 6 des propriétés automatiques en lecture seule vu que toutes les propriétés (à l'exception de la propriété Id) sont en lecture seule { get; private set; } et qu'elles sont uniquement assignées uniquement via notre constructeur paramétré. Ces propriétés peuvent utiliser la nouveauté C# 6 et être définies avec { get; } mais cela nécessite que toutes les propriétés concernées soient explicitement configurées en tant que propriétés mappées à des colonnes de la base de données. Cela a pour but de permettre à EF Core de savoir différencier les propriétés calculées des propriétés mappées. Ces propriétés calculées sont généralement en lecture seule donc définies avec { get; } et elles ne sont pas assignées dans aucun constructeur. Alors nous avons les deux étapes suivantes à suivre pour pouvoir utiliser la fonctionnalité de propriétés automatiques en lecture seule de C# 6 :

  1. utiliser { get; } au lieu de { get; private set; } toutes les propriétés assignées via le constructeur participant au mapping avec la base de données.
  2. configurer notre entité de façon explicite à savoir spécifier les propriétés modifiées dans l'étape 1 pour que EF Core prenne conscience que ces propriétés ne doivent pas être considérées comme des propriétés calculées donc ne pas les ignorer.

Pour l'étape 1, rien de plus simple. Notre classe Tweet ressemblera au code ci-dessous.

public class Tweet
{
    public int Id { get; private set; }

    public int UserId { get; }

    public string Content { get; }

    public Tweet(int userId, string content)
    {
        if (userId <= 0) throw new ArgumentException("User Id must be greater than 0", nameof(userId));
        if (string.IsNullOrWhiteSpace(content)) throw new ArgumentNullException(nameof(content));

        this.UserId = userId;
        this.Content = content;
    }
}

Pour l'étape 2, notre classe TweetConfiguration ressemblera à ceci :

internal class TweetConfiguration : IEntityTypeConfiguration<Tweet>
{
    public void Configure(EntityTypeBuilder<Tweet> builder)
    {
        builder.Property(p => p.UserId);
        builder.Property(p => p.Content).IsRequired().HasMaxLength(280);
    }
}

Et c'est tout ! Nous avons réussi à utiliser la fonctionnalité de propriétés automatiques en lecture seule de C# 6 avec EF Core 2.1 ce que nous n'avons pas réussi avec EF 6 (idem dans les versions antérieures à EF Core 2.1).

Si vous observez le code de la classe Tweet vous verrez que la propriété Id (mappée par défaut à la clé primaire de notre table) utilise toujours dans sa définition { get; private set; } et non { get }. Il faut tout d'abord savoir, comme cela a été dit lors de l'étude de la problématique, que la propriété Id est en lecture seule et n'est pas assignée via un constructeur parce qu'elle est de type int et que par défaut EF Core avec le provider SQL Server générera une colonne comme clé primaire dans la table concernée et de plus les valeurs de cette clé seront auto-générées par le serveur de base de données. Donc il n'y a aucun sens à assigner la valeur de la propriété Id via un constructeur étant donné qu'on ne pouvons savoir au moment de la création de l'instance qu'elle sera la valeur de cette propriété. Alors si nous utilisons les deux étapes citées précédemment pour l'utilisation de la fonctionnalité de C# 6 avec cette propriété Id et qu'aucun constructeur ne permet de recevoir sa valeur alors EF Core vous le signalera avec l'erreur ci-dessous lors de l'ajout d'une migration :

Field 'k__BackingField' of entity type 'Tweet' is readonly and so cannot be set.

Le problème est qu'en mettant juste l'accesseur { get; } pour la propriété Id et qu'elle n'est pas assignée dans aucun constructeur le champ associé généré par le compilateur <Id>k__BackingField (donc pas visible dans votre code ) est défini en lecture seule (mot-clé readonly). EF Core a besoin a besoin que ce champ soit en lecture et écriture pour l'utiliser pour le stockage des valeurs recépées de la base de données. Avec les propriétés Content et UserId cela fonctionne parce que, du fait de l'assignation de ces propriétés, les champs associés générés et utilisés pour le stockage par EF Core ne seront pas uniquement en lecture seule donc pas définis avec le mot-clé readonly.

Vous avez deux solutions et toutes deux dépendent du contexte de votre projet :

  • soit vous vous dîtes que vous aurez besoin d'accéder à la propriété Id à l'extérieur de la classe Tweet. Dans ce cas la définition de propriété avec { get; private set; } suffit largement et pas besoin de vouloir tout passé en mode utilisation d'une fonctionnalité de C# 6. Par contre il faudra veiller à mettre en commentaire XML de la propriété concernée une remarque explicite expliquant le pourquoi et une validation minutieuse des pull-requests. Cela pour éviter que les développeurs voulant tout passé en mode fonctionnalité C# 6 ne modifient le code sans savoir les conséquences.
  • soit vous vous dîtes que n'aurez jamais besoin de lire la valeur de la propriété Id en dehors de classe Tweet. Pour éviter aux développeurs de passer la définition de la propriété Id de { get; private set; } vers { get; } alors nous pouvons tout simplement supprimer la propriété et la remplacer par un champ et tirer bénéfice de la fonctionnalité champ de stockage (backing fields) de EF Core.

Pour notre cas exemple et dans le code final nous allons supposer que nous n'aurons jamais besoin de lire la valeur de propriété Id en dehors de la classe Tweet du coup nous devons suivre les deux étapes ci-dessous :

  1. supprimer la propriété Id et la remplacer par un champ _id.
  2. configurer notre modèle pour qu'elle utilise le champ _id comme champ de stockage.

Pour l'étape 1, rien de plus simple, nous aurons à la place de la propriété Id la définition du champ _id comme suit :

private int _id;

Pour l'étape 2, il suffit juste d'ajouter la ligne suivante dans la méthode Configure de notre classe TweetConfiguration comme suit :

builder.HasKey("_id");

Avec la fonctionnalité de constructeurs paramétrés ajoutée dans la version 2.1 de EF Core, le code utilisé pour élucider la problématique avec EF 6 ressemblera, au final, au code ci-dessous pour la classe Tweet :

public class Tweet
{
    private int _id;

    public int UserId { get; }

    public string Content { get; }

    public Tweet(int userId, string content)
    {
        if (userId <= 0) throw new ArgumentException("User Id must be greater than 0", nameof(userId));
        if (string.IsNullOrWhiteSpace(content)) throw new ArgumentNullException(nameof(content));

        this.UserId = userId;
        this.Content = content;
    }
}

La classe TweetConfiguration à ceci :

public void Configure(EntityTypeBuilder<Tweet> builder)
{
    builder.HasKey("_id");
    builder.Property(p => p.UserId);
    builder.Property(p => p.Content).IsRequired().HasMaxLength(280);
}

Les lignes de code pour lire notre table ressemblera à ça :

using (var context = new MyDbContext())
{
    var tweet = new Tweet(
        42,
        "Lorem ipsum dolor sit amet, ipsum noster efficiendi te sit. Mea an modus vitae mediocritatem, usu ferri vocent dissentiunt at. Pro in scripta accusamus intellegam, quo eleifend contentiones id. Eu brute periculis vis, ei quas suavitate eam, integre eruditi ad qui."
    );

    context.Tweets.Add(tweet);
    context.SaveChanges();
}

Les instructions permettant de sauvegarder une instance de notre entité Tweet se présentent comme suit :

using (var context = new MyDbContext())
{
    var tweets = context.Tweets.ToList();

    foreach (var tweet in tweets)
    {
        Console.WriteLine($"User Id: {tweet.UserId}");
        Console.WriteLine($"Content: {tweet.Content}");
        Console.WriteLine("======");
    }
}

Remarques utiles

Comme dit plus haut, le choix du constructeur personnalisé de l'entité se fait actuellement par convention. Si EF Core détecte plusieurs constructeurs respectant la convention alors vous serez dans l’impossibilité de générer une migration parce vous obtiendrez une erreur lors de l'utilisation de la commande add-migration MesSuperModifications. D'après la documentation de EF Core, dans les prochaines versions, nous aurons la possibilité de spécifier, via une configuration, le constructeur sur lequel l'ORM devra se baser pour la construction de notre entité lors de la matérialisation.

Jusque-là nous avons précisé que cette fonctionnalité ne marche qu'avec des propriétés mappées à des colonnes des tables de la base de données donc pour le coup les propriétés de navigation ne peuvent bénéficier de cet avantage. Essayer de passer la valeur d'une propriété de navigation en tant que paramètre d'un constructeur personnalisé utilisé par EF Core ne marchera pas.

Même si nous ne pouvons pas utiliser les propriétés de navigation, nous ne sommes cependant pas limités à l'utilisation des propriétés mappées à nos colonnes de tables de base de données comme paramètres de notre constructeur. EF Core permet, à travers cette fonctionnalité, l'injection de services dont il a connaissance comme par exemple injecter une instance du contexte dans l'entité ou d'autres services listés ici. Si vous êtes comme moi et que vous aimez préserver l'aspect POCO (Plain Old CLR Object) de vos entités alors je n'utiliserai jamais l'injection de services dans mes entités.

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 impossible de spécifier quel constructeur personnalisé utilisé lors de la matérialisation des données de la base de données suite à l'exécution d'une requête Linq To Entities par EF Core. Cela était impossible parce que EF 6 ainsi que les versions antérieures à la version 2.1 de EF Core nous imposaient la présence obligatoire du constructeur par défaut et cela même s'il est défini avec la visibilité la plus restrictive private.

Avec la fonctionnalité de constructeurs paramétrés définis au sein de nos entités dans EF Core 2.1, cette problématique est résolue tout simplement grâce à des conventions mises en place. Dans les prochaines versions, EF Core nous permettra de pouvoir spécifier par l'intermédiaire d'une configuration le constructeur spécifique à utiliser dans le cas où notre entité en contiendra plusieurs.

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 CustomConstructorSamples pour la version ciblant EF 6.
  • EFCoreSamples : contient une solution VS avec le projet CustomConstructorSamples 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