Blog ENI : Toute la veille numérique !
-25€ dès 75€ sur les livres en ligne, vidéos... avec le code FUSEE25. J'en profite !
Accès illimité 24h/24 à tous nos livres & vidéos ! 
Découvrez la Bibliothèque Numérique ENI. Cliquez ici
  1. Livres et vidéos
  2. Entity Framework Core
  3. Création d’un fournisseur de données
Extrait - Entity Framework Core Maîtrisez la solution de Mappage Objet-Relationnel de Microsoft
Extraits du livre
Entity Framework Core Maîtrisez la solution de Mappage Objet-Relationnel de Microsoft Revenir à la page d'achat du livre

Création d’un fournisseur de données

Architecture modulaire

Historiquement, la structure d’Entity Framework jusqu’à sa version 6 peut être plus ou moins assimilée à une boîte noire à laquelle on fournit des informations et qui effectue des traitements en fonction de celles-ci. Le monolithe Entity Framework n’est, par conséquent, que très peu flexible face aux contraintes techniques ou métier.

A contrario, l’architecture d’Entity Framework Core est basée sur le concept de modularité. Les éléments qui composent cette librairie sont en grande partie pensés pour être remplaçables ou extensibles. Cet objectif est atteint par l’utilisation de très nombreuses interfaces qui définissent les contrats d’implémentation nécessaires au bon fonctionnement du moteur ou d’un fournisseur. Les implémentations qui leur sont associées sont manipulées au travers d’un moteur d’injection de dépendances, faisant ainsi d’Entity Framework Core une librairie souple et extensible.

En programmation orientée objet, la notion de dépendance est définie par une liaison statique entre deux classes. Cette liaison statique correspond, plus concrètement, à l’utilisation directe d’une classe par une autre classe. L’exemple suivant montre une liaison statique...

Mise en place du fournisseur

Les fournisseurs de données sont essentiels à l’utilisation d’Entity Framework Core puisque, sans eux, la librairie est inutile. Pour plonger encore davantage dans son fonctionnement et comprendre les rouages de cet outil, nous allons écrire un fournisseur de données pour MongoDB. Comme évoqué plus tôt, Entity Framework Core est en effet conçu pour supporter les sources de données non relationnelles, dont MongoDB fait partie. Nous découvrirons donc à travers cet exemple comment des requêtes peuvent être générées et exécutées pour cette base de données NoSQL, et leurs résultats transformés en objets .NET exploitables.

L’écriture d’un fournisseur complet est un travail de très longue haleine puisqu’il existe de nombreux éléments différents qui peuvent être traités par l’utilisation d’Entity Framework Core : clés multiples, relations entre entités, projections partielles, requêtes complexes avec filtrages/tris/jointures, héritage… La nature de MongoDB augmente encore le nombre de cas d’utilisation avec la notion de documents imbriqués, ou encore la possibilité qu’a un document d’avoir des propriétés dont la valeur est un tableau.

Pour ne pas complexifier inutilement notre propos, nous nous concentrerons uniquement sur un objectif relativement simple : une fois l’ensemble du code écrit, il devra être possible d’effectuer une requête de sélection de l’ensemble des enregistrements d’une collection, et de mapper chaque propriété à un champ dont le nom est différent.

L’objectif sera considéré comme atteint lorsque l’application d’exemple décrite ci-après affichera l’ensemble des clients et des villes enregistrés dans la base de données.


try 
{ 
    using (var context = new MongoDbContext()) 
    { 
        Console.WriteLine("Tentative de chargement des données 
de la collection Customer"); 
  
        var data = context.Customers.ToList(); 
  
      ...

Éléments de base

Démarrons en douceur… Nous savons que le contexte de données défini dans l’application de test n’est pas configuré, et ce pour une bonne raison : aucune méthode UseMongoDb n’est pour l’instant disponible.

Les méthodes UseX implémentées par chaque fournisseur sont définies en tant que méthodes d’extension sur le type DbContextOptionsBuilder.

 Créez le fichier /MongoDbContextOptionsBuilderExtensions.cs.

C’est ce fichier qui contient la méthode d’extension UseMongoDb. Celle-ci accepte trois paramètres, en plus de this DbContextOptionsBuilder, qui sont :

  • La chaîne de connexion à utiliser pour les accès à la source de données.

  • Le nom de la base de données à utiliser au niveau de la source de données.

  • Une action de configuration complémentaire.

La chaîne de connexion et le nom de base de données sont enregistrés par cette méthode au sein d’un objet qui implémente l’interface Infrastructure.IDbContextOptionsBuilderExtension. Ici, ce type se nomme MongoDbOptionsExtension. Ce traitement est suivi par l’invocation de la méthode de configuration complémentaire, si elle est valorisée.


public static class MongoDbContextOptionsBuilderExtensions 
{ 
    public static DbContextOptionsBuilder UseMongoDb( 
        [NotNull] this DbContextOptionsBuilder optionsBuilder, 
        [NotNull] string connectionString, 
        [NotNull] string databaseName, 
        [CanBeNull] Action<DbContextOptionsBuilder> 
mongoDbOptionsAction = null) 
    { 
        Check.NotNull(optionsBuilder, nameof(optionsBuilder)); 
        Check.NotEmpty(connectionString,  
                       nameof(connectionString)); 
        Check.NotEmpty(databaseName, nameof(databaseName)); 
        //Création de l'extension MongoDB 
        var extension = GetOrCreateExtension(optionsBuilder); 
        extension.ConnectionString = connectionString; ...

Services requis pour la sélection de données

Le moteur d’Entity Framework Core utilise de nombreux services, dont une grande majorité existe sous une forme basique au niveau de la librairie Microsoft.EntityFrameworkCore. Certains éléments doivent toutefois être implémentés obligatoirement par les fournisseurs de services car ils ont pour objectif d’exécuter des traitements spécifiques à chaque source de données.

L’implémentation dans MongoDbDatabaseProviderServices des membres abstraits de sa classe mère montre ces services indispensables au bon fonctionnement du système. Pour profiter de l’ensemble des fonctionnalités d’Entity Framework, il convient donc de remplacer les déclenchements d’exceptions par des implémentations concrètes de services. Commençons par le service qui a déclenché une NotImplementedException à la section précédente.

1. QueryContextFactory

Le type QueryContextFactory a, comme son nom l’indique, pour objectif unique de construire des objets dont le type est QueryContext.

Le type Query.QueryContext est un service utilisé lors de la phase d’exécution d’une requête LINQ. Il maintient un ensemble d’éléments qui définissent le contexte d’exécution de cette requête, de manière à simplifier leur gestion et leur utilisation.

Le constructeur de la classe de base QueryContext accepte plusieurs paramètres correspondant aux différents services qui composent le contexte d’exécution, ainsi qu’à des données tierces, sans lien direct avec la notion de services. Pour tirer parti du mécanisme d’injection de dépendances, qui résout l’ensemble des dépendances passées à un type via son constructeur, il faut ruser. L’injection est en effet incompatible avec la présence de paramètres de type "données" au niveau du constructeur, puisqu’elles ne font pas partie par définition des services. Pour parer (notamment) à ce problème, le patron de conception (design pattern, pour les anglophones) Factory est très utilisé au sein d’Entity Framework Core.

Pour le cas présent, Entity...

Entity Framework et arbres d’expressions

L’exécution de requêtes LINQ, dans le contexte d’Entity Framework, produit en premier lieu un arbre d’expressions. Celui-ci décrit l’ensemble des opérations formulées lors de l’écriture d’une requête afin de la rendre exploitable par les fournisseurs de données.

Cet arbre d’expressions est difficile d’accès car relativement complexe. C’est pour cette raison que le projet Entity Framework Core utilise la librairie tierce re-linq qui a pour objectif de simplifier l’arborescence d’expressions exposée aux fournisseurs de données. Sa forme est ainsi plus proche de celle d’une requête LINQ (ou SQL) : clauses From, expressions de sélection, identification claire des sous-requêtes… Ces données sont ensuite manipulées au sein d’Entity Framework par le biais du type Remotion.Linq.QueryModel, que nous avons déjà aperçu comme paramètre des fonctions IDatabase.CompileQuery et IDatabase.CompileQueryAsync.

Le code source du projet re-linq est disponible sous licence Apache Software License 2.0 sur Github : https://github.com/re-motion/Relinq. Il manque en revanche cruellement de documentation à l’heure de l’écriture de ces lignes.

L’arbre d’expressions traité au sein d’Entity...

Gestion des annotations

Avant d’injecter ces éléments au niveau du type MongoDbEntityQueryableExpressionVisitor, il faut créer la structure des éléments utilisés pour la gestion des annotations.

 Créez le fichier /Metadata/IMongoDbEntityTypeAnnotations.cs.


public interface IMongoDbEntityTypeAnnotations 
{ 
    string CollectionName { get; } 
}
 

 Créez le fichier /Metadata/IMongoDbKeyAnnotations.cs.


public interface IMongoDbKeyAnnotations 
{ 
    string Name { get; } 
}
 

 Créez le fichier /Metadata/IMongoDbModelAnnotations.cs.


public interface IMongoDbModelAnnotations 
{ 
    string DatabaseName { get; } 
}
 

 Créez le fichier /Metadata/IMongoDbPropertyAnnotations.cs.


public interface IMongoDbPropertyAnnotations 
{ 
    string FieldName { get; } 
    string FieldType { get; } 
    object DefaultValue { get; } 
}
 

 Créez le fichier /Metadata/IMongoDbAnnotationsProvider.cs.


public interface IMongoDbAnnotationsProvider 
{ 
    IMongoDbEntityTypeAnnotations For( 
                              [NotNull] IEntityType entityType); 
 
    IMongoDbKeyAnnotations For([NotNull] IKey key); 
 
    IMongoDbModelAnnotations For([NotNull] IModel model); 
 
    IMongoDbPropertyAnnotations For( 
                              [NotNull] IProperty property); 
}
 

Ces interfaces n’ayant que peu d’utilité par elles-mêmes, il faut maintenant créer des classes qui les implémentent. Elles auront pour but de faire le pont entre le conteneur d’annotations et le provider que nous venons d’écrire.

 Créez le fichier /Metadata/MongoDbEntityTypeAnnotations.cs.


public class MongoDbEntityTypeAnnotations : IMongoDbEntityTypeAnnotations 
{ 
    private readonly MongoDbFullAnnotationsNames FullAnnotationNames; 
  
    public MongoDbEntityTypeAnnotations( 
        [NotNull] IEntityType entityType, ...

Expressions de description de requête MongoDB

Le but de la méthode VisitEntityQueryable est de renvoyer une expression de type Call, soit un appel de méthode, qui pourra être exécutée par encapsulation dans une expression lambda. Pour atteindre cet objectif, la logique générale est la suivante :

  • Création d’une expression représentant la requête à générer.

  • Ajout de traitements pour :

  • Transformer l’expression précédente en une requête compréhensible par la source de données.

  • Exécuter la requête sur la source de données et récupérer son résultat brut.

  • Transformer le résultat brut en entités et renvoyer la collection associée.

  • Renvoi d’une expression d’appel de ces traitements.

Dans le monde de MongoDB, on utilise généralement le terme "Find" pour parler d’une requête de lecture d’enregistrements. Aussi, il semble logique de nommer l’expression qui la représente FindExpression. Elle est directement associée à une expression représentant une collection (CollectionExpression), mais elle doit également maintenir la liste des champs concernés par la requête sous la forme, encore une fois, d’expressions (FieldExpression). Les objets FindExpression sont créés par une factory afin de pouvoir tirer parti du moteur d’injection de dépendances si nécessaire.

 Créez le fichier /Query/Expressions/CollectionExpression.cs.

Le type CollectionExpression est associé à un nom de collection ainsi qu’à un type d’entité.


public class CollectionExpression : Expression 
{ 
    public CollectionExpression( 
        [NotNull] string collection, 
        [NotNull] Type entityType) 
    { 
        Check.NotEmpty(collection, nameof(collection));  
        Check.NotNull(entityType, nameof(entityType)); 
 
        Name = collection; 
        EntityType = entityType; 
    } 
  
    public virtual string Name { get; } 
  
    public virtual...

Shaping (façonnage d’entités)

Un certain nombre de concepts ont été vus jusque-là, tous relativement indépendants. Ici, en revanche, les éléments sont plus complexes puisque plus imbriqués. Avant de plonger dans la description et l’écriture de chaque composant utilisé par le traitement, une description générale du processus peut aider à éclaircir les choses.

L’expression qui doit être retournée par la méthode VisitEntityQueryable représente l’appel d’une fonction qui déclenche l’ensemble des traitements décrits ici. En fin d’exécution, elle retourne une séquence d’entités matérialisées. Pour cela, elle énumère les valeurs retournées par le type QueryingEnumerable, ce qui déclenche différentes actions qui s’enchaînent :

  • Génération d’une commande exécutable sur la source de données.

  • Exécution de la commande.

  • Transformation des données retournées pour chaque enregistrement ValueBuffer (tableau d’objets).

  • Matérialisation des entités à partir des ValueBuffer.

Le schéma suivant montre la logique qui va être implémentée de manière simplifiée.

images/figurech5.PNG

1. Command builder

Commençons par ce qui n’est pas du tout le point d’entrée : l’interface IBsonCommandBuilder et son implémentation. L’objectif de ce type est la génération d’une commande à partir de données qui lui sont fournies unitairement.

 Créez le fichier /Storage/IBsonCommandBuilder.cs.


public interface IBsonCommandBuilder 
{ 
    void AddCollection(string name, Type collectionEntityType); 
  
    void AddField(string name); 
  
    IMongoDbFindCommand Build(); 
}
 

La méthode Build renvoie un objet de type IMongoDbFindCommand qui est la commande exécutable. Pour éviter d’accumuler les erreurs de compilation, ajoutez rapidement cette interface, vide, au projet.

 Créez le fichier /Storage/IMongoDbFindCommand.cs.


public interface IMongoDbFindCommand 
{ 
 
}
 

L’implémentation du type...

ValueGeneratorCache

À ce stade, l’exécution du programme de test devrait déclencher une exception indiquant que la propriété MongoDbDatabaseProviderServices.ValueGeneratorCache n’est pas implémentée. Ce service a pour rôle de mettre en cache des objets ValueGenerator.

Lorsqu’une propriété est configurée pour que sa valeur soit générée à l’ajout et éventuellement lors de la mise à jour, un objet ValueGenerator associé au type de la propriété est instancié afin de répondre au besoin exprimé par la configuration. Cet objet est ensuite placé dans un cache local afin d’éviter la dégradation de performances qui pourrait être constatée si de nombreuses générations de valeurs devaient être effectuées simultanément. Cette opération est exécutée par un objet dont le type hérite de la classe abstraite ValueGeneratorCache.

Cette classe de base implémente la fonctionnalité de manière élémentaire, sans membre abstrait, ce qui permet de résoudre le problème de manière très simple : il suffit d’ajouter au projet une classe dérivée de ValueGeneratorCache, mais ne possédant aucune implémentation....

Mappage des champs

Une nouvelle exécution du programme de test fait encore une fois apparaître une exception. Le message qui lui est associé est explicite, mais peu clair quant à la cause du problème.

images/05-200.png

Il s’agit en fait d’une problématique d’association entre la propriété Customer.Id et le champ correspondant au niveau de la source de donnée : _id. La convention qui spécifie le lien entre ces deux noms n’est pas respectée, donc la propriété Id ne peut être valorisée. La clé primaire de l’objet est nulle, ce qui génère une exception.

Pour vérifier que la cause de ce problème est bien le manque de configuration sur la propriété Id, on peut écrire la méthode OnModelCreating de manière qu’elle ajoute directement les annotations de correspondances avec la source de données sur les propriétés concernées.


public class MongoDbContext : DbContext 
{ 
    private string _connectionString = "mongodb://
eni:eni@ds050539.mlab.com:50539/eni_demo"; 
    private string _databaseName = "eni_demo"; 
  
    protected override void OnConfiguring(DbContextOptionsBuilder 
optionsBuilder) 
    { 
        optionsBuilder.UseMongoDb(this._connectionString, 
this._databaseName); 
    } 
  
  
    protected override void OnModelCreating(ModelBuilder modelBuilder) 
    { 
        modelBuilder.Entity<Customer>() 
                    .Property(c => c.Id) 
                    .HasAnnotation("MongoDB:FieldName", "_id"); 
 
        modelBuilder.Entity<City>() 
         .Property(c => c.Id) 
         .HasAnnotation("MongoDB:FieldName", "_id"); 
    } 
  
    public DbSet<Customer> Customers { get; set; } 
  
    public DbSet<City> Cities { get; set; } 
}
 

L’exécution du programme de test devrait maintenant afficher...