Les modèles avec Entity Framework Core
Introduction
Depuis toujours, le développeur a besoin de lire les données d’une base, écrire des enregistrements, supprimer des lignes, gérer des transactions, mettre à jour des données utilisateurs… Le framework .NET dispose depuis ses tout débuts d’une API permettant de communiquer avec une base de données et d’envoyer des requêtes à cette dernière. Cependant, cela peut vite devenir fastidieux et non maintenable. En effet, les erreurs de frappe sont courantes, et la gestion de toutes les requêtes SQL peut vite devenir une usine à gaz.
Microsoft développe depuis des années maintenant une brique logicielle permettant de faciliter l’accès à une base de données : Entity Framework. Ce framework est un ORM (Object-Relational Mapping), c’est-à-dire un outil qui permet de travailler avec des objets C# plutôt qu’avec des requêtes en dur. Le développeur va ainsi gérer, requêter, ajouter ou supprimer des objets via l’ORM, et ce dernier va automatiquement s’occuper de traduire les opérations via des requêtes SQL.
La dernière version d’Entity Framework, la version Core, est une réécriture complète des anciennes versions. Ce choix a été opéré dans le but d’améliorer tout d’abord...
Les différents providers de base de données
Tout d’abord, avant d’utiliser un ORM, il est important de savoir quel type de base de données il supporte. Entity Framework Core étant tout jeune, il ne supporte que les bases SQL Server et SQLite pour l’instant (avril 2016). Il supporte également les bases dites in-memory, c’est-à-dire stockées en mémoire, mais ce genre de base de données est plutôt utilisé pour effectuer des tests. Le but du projet est de couvrir à terme un maximum de bases de données. Dans le cadre des providers de base de données, les namespaces importants sont :
-
Microsoft.EntityFrameworkCore.SqlServer : ensemble de classes pour gérer l’accès à une base de données MySQL.
-
Microsoft.EntityFrameworkCore.SqlServer.Design : classes utilitaires pour SQL Server.
-
Microsoft.EntityFrameworkCore.Sqlite : ensemble de classes permettant de gérer une base SQLite.
-
Microsoft.EntityFrameworkCore.Sqlite.Designer : classes utilitaires pour SQLite.
-
Microsoft.EntityFrameworkCore.InMemory : ensemble de classes permettant de gérer une base en mémoire.
L’accès aux bases de données est devenu bien plus modulaire qu’avant, et permet ainsi d’embarquer, dans les applications web, uniquement ce dont le développeur a besoin. De plus, avec une architecture découplée, il sera bien plus facile par la suite d’ajouter de nouveaux providers pour d’autres types de bases de données (MySQL, Oracle…), ce qui était...
Les migrations
Avant de parler de migration, il est important de comprendre pourquoi Entity Framework Core ne supporte plus les Database Project et les EDMX. Ce type de projet permettait de construire un modèle relationnel, dans Visual Studio, en fonction des besoins du projet. Ce modèle était ensuite appliqué à la base de données, et dans un même temps les classes C# constituant les modèles étaient générées et tout de suite utilisables par le développeur.
Sur le papier, ce processus semble profitable à la propreté du projet, mais il n’en est rien. Il devient même contre-productif dans le cadre d’un projet très conséquent (lorsqu’on atteint la centaine de modèles). En effet, ce genre de procédé utilise un fichier XML comme référence afin de gérer le modèle de base de données, et son utilisation devient problématique quand il est partagé dans une équipe de développement car les modifications deviennent trop lourdes à gérer pour le gestionnaire de code source. Le projet est ainsi susceptible de perdre des informations sur le modèle de données, ce qui n’est pas acceptable dans un contexte professionnel.
La solution choisie par l’équipe de Microsoft est d’arrêter purement et simplement le support de ce type de projet. À la place, les experts de l’équipe préconisent l’utilisation d’une approche Code First. Déjà intégrée dans le framework depuis la version 4.1, cette approche est centrée autour des classes C# que les développeurs auront créées dans le projet. Le but de ces classes est de représenter la base de données sous forme de modèle objet compréhensible ensuite par l’ORM, qui sera capable d’interpréter automatiquement les mappings, les clés étrangères… L’utilisation d’attributs est également possible afin d’aider l’ORM à mieux s’en sortir, ou pour spécifier des mappings bien spécifiques, mais ce n’est pas l’objet de cette section.
L’avantage de cette approche est sa simplicité de mise en œuvre et de maintenance...
L’API Fluent
L’approche Code First est un excellent moyen de concevoir un modèle de données à la fois maintenable et flexible. Cependant, il existe forcément des cas dans un projet où le mapping doit être explicite plutôt qu’implicite.
Entity Framework Core applique de base un mapping par convention, c’est-à-dire qu’une entité Blog va donner une table "Blog" en base de données. Il en est de même pour les propriétés : un champ "Name" donnera une colonne Name, et ainsi de suite. L’intérêt est que le développeur peut ainsi construire le mapping entre un modèle et une base de données très rapidement.
Il existe deux possibilités pour déclarer un mapping spécifique :
-
les annotations ;
-
l’API Fluent.
Nous allons nous concentrer uniquement sur l’API Fluent dans ce chapitre car c’est la partie d’Entity Framework qui a subi le plus de changements.
L’API Fluent permet de coder les relations entre les entités. Cela veut dire que le développeur va utiliser des méthodes d’extensions, fournies par le framework, pour mapper le modèle à la base de données et définir les clés primaires, clés étrangères... L’API est contenue dans le namespace EntityFrameworkCore.Relational.
Prenons un exemple simple qui consiste à mapper une entité à une table qui ne porte pas le même nom. Dans un premier temps, il faut savoir que le mapping s’effectue dans un DbContext, et précisément dans la méthode OnModelCreating. Cette méthode reçoit un objet de type ModelBuilder, et c’est cet objet qui va permettre d’agir sur le mapping.
class MyContext : DbContext
{
public DbSet<Blog> Blogs { get; set; }
protected override void OnModelCreating(ModelBuilder
modelBuilder)
{
modelBuilder.Entity<Blog>()
.ToTable("blogs");
}
} ...
L’intégration d’Entity Framework Core
Entity Framework Core offre au développeur un système de configuration poussé lui permettant de personnaliser ses DbContext de manière extensible. En effet, ce sont les différents providers qui vont venir rajouter leurs propres options au système de configuration de base. Dans l’exemple de SQLite, c’est le namespace Microsoft.EntityFrameworkCore.Sqlite qui va venir rajouter la méthode UseSqlite afin de configurer l’accès à la base SQLite.
services.AddEntityFramework()
.AddSqlite()
.AddDbContext<BloggingContext>(options =>
options.UseSqlite("Filename=./blog.db"));
L’appel de cette méthode se fait dans la classe Startup, qui va ainsi contenir toute la configuration du contexte d’accès à la base de données pour chacun des appels. Ensuite, la méthode AddDbContext va simplement enregistrer le DbContext comme étant un service utilisable dans toute l’application via l’injection de dépendances.
Les contrôleurs sont un bon exemple d’utilisation du DbContext. L’injection de dépendances permet d’injecter ce service dans le constructeur, et ainsi rendre disponible l’accès à la base de données dans toutes les actions du contrôleur.
public MyController(BloggingContext context)
Cette technique s’avère utile dans la plupart des cas, mais n’est pas vraiment une bonne pratique. Injecter le DbContext directement dans le contrôleur ne le rend pas vraiment testable, et garder le code métier au sein du contrôleur n’est pas une bonne solution. Le mieux est d’exporter le DbContext dans des classes de "services"...
La validation des modèles
Lorsqu’un utilisateur manipule l’application web afin de créer ou modifier un enregistrement, le formulaire utilisé va indiquer à l’utilisateur certaines informations de formatage afin d’assurer le bon traitement des données créées ou modifiées. Les exemples sont nombreux :
-
le formatage correct d’un e-mail ou d’une date ;
-
le caractère obligatoire de la donnée ;
-
l’application de certaines contraintes de taille (pour une chaîne de caractères), de complexité (pour un mot de passe) et bien d’autres.
Ces contraintes doivent être respectées pour prévenir des potentielles failles de sécurité et vérifier que les données respectent bien certaines règles avant d’être insérées en base de données. Avec un modèle MVC, la validation peut se faire à la fois côté client (avec jQuery par exemple) et côté serveur (avec du C#).
Le framework .NET a abstrait toute cette mécanique de validation au travers d’attributs de validation. Ces derniers s’appliquent sur les modèles objets C# de l’application, ils embarquent de la logique métier afin de réduire la duplication de code et sont interprétables côté client et serveur pour valider les données. Les contraintes peuvent varier, allant de règles simples de champ requis, jusqu’à des patterns de validation plus complexes, comme pour les cartes de crédit, les numéros de téléphone ou les e-mails.
L’exemple ci-dessous montre l’application de quelques-uns de ces attributs provenant du namespace System.ComponentModel.DataAnnotations.
public class Movie
{
public int Id { get; set; }
[Required]
[StringLength(100)]
public string Title { get; set; }
[Required]
[Range(0, 999.99)]
public float Price { get; set; }
}
Les attributs sont très parlants, et permettent d’avoir...
Les patterns et antipatterns
Afin de bien utiliser Entity Framework, il est important de comprendre comment fonctionne l’ORM, mais aussi de bien l’utiliser. Il existe bon nombre de moyens de configurer Entity Framework au sein d’un projet ASP.NET Core, et nous allons expliciter dans cette section quelques bonnes et mauvaises pratiques pour aider le développeur dans son quotidien.
Tout d’abord, il faut savoir que le DbContext n’est pas thread safe. Dans le cadre d’une application web ASP.NET Core, cela veut dire qu’il ne faut pas partager le DbContext entre plusieurs requêtes HTTP, puisqu’une requête équivaut à avoir un nouveau thread. De base, nous allons donc privilégier un service DbContext en mode Scoped au maximum, pas de singleton. Le mode Transient est acceptable. D’ailleurs, lorsque le développeur écrit AddDbContext, c’est exactement ce que le framework fait : il rajoute le DbContext en tant que Scoped (voir chapitre Les nouveaux mécanismes d’ASP NET Core, section L’injection de dépendances pour le détail sur les cycles de vie Scoped et Transient). Avec cette bonne pratique, le développeur s’assure d’avoir un seul DbContext par thread et n’a pas besoin de se préoccuper de la gestion du cycle de vie de cet objet.
Au sein de l’écosystème Entity Framework, on peut souvent entendre parler de pattern Repository et de pattern Unit of Work. Concernant le premier, nous avons déjà démontré dans ce chapitre comment implémenter un dépôt abstrait, souple, réutilisable et hautement testable.
Je vous recommande d’utiliser ce type de pattern qui permet d’abstraire facilement les relations entre votre code métier et l’accès aux données. Pour les tests, il est toujours plus facile d’implémenter un dépôt en mémoire via une abstraction propre (le repository) plutôt que de monter une base en mémoire via Entity Framework. L’abstraction ici est la clé pour passer d’un mode à un autre selon la situation.
Le concept du pattern Unit of Work est simple : synchroniser et gérer le cycle de vie des objets d’accès à la base de données tout en fournissant...