A-t-on besoin d’une Business Layer avec Entity Framework ? (3)

A-t-on besoin d’une Business Layer avec Entity Framework ?
Sommaire : Partie 1 – Partie 2 – Partie 3 – Partie 4
Dans le précédent article, nous avons détaillé la configuration de base utilisée pour accéder à notre base de données à l’aide de Entity Framework. Nous allons maintenant voir comment isoler proprement la couche d’accès aux données de la couche de présentation à l’aide d’un Repository. Les clients de notre Business Layer sont les développeurs de la couche de présentation. Nous souhaitons donc leur exposer une API claire et simple à utiliser tout en masquant la complexité fonctionnelle et l’accès aux données, sans pour autant les brider dans leurs capacités créatives. Une des choses que nous souhaitons est que notre API ait un comportement homogène. Cela présente l’avantage d’accélérer son adoption, d’améliorer la productivité, et, plus égoïstement, de diminuer le volume de code à maintenir. Nous souhaitons également que notre Business Layer en profite pour devenir testable, sans adhérence avec la base de données. Nous allons développer un Repository par entité métier à manipuler : un pour les auteurs, un pour les éditeurs, un pour les BDs. Il émerge assez rapidement le besoin pour les opérations CRUD de base :
- Create
- Read
- Update
- Delete
Pour la méthode Select, Entity Framework expose en fait nativement deux méthodes : Entite GetById(int id) et IQueryable<Entité> Get(). La première sert naturellement à récupérer une entité à l’aide de son identifiant. La seconde récupère par défaut l’ensemble des entités. Mais étant donné qu’elle rend un ensemble IQueryable<Entité>, il est possible de lui appliquer toutes les méthodes d’agrégat de Linq, dont la méthode d’extension Where<TSource>(IEnumerable<TSource>, Func<TSource, Boolean>), qui nous permettra de requêter plus précisément la base de données.
Nous allons également nous laisser la porte ouverte pour développer des méthodes spécifiques à chaque entité.
Voici donc l’interface commune, que chacun de nos Repositories implémentera :
using Bedetheque.Entities; using System.Linq; namespace Bedetheque.Business { public interface IRepository<T> { void Add(T entity); void Update(T entity); void Delete(T entity); T GetById(int id); IQueryable<T> Get(); } }
Voilà pour le voeu pieu. Passons maintenant à l’implémentation de cette interface, qui elle est propre à Entity Framework :
public abstract class AbstractEntityRepository<T> : IRepository<T> where T:class { private DbSet<T> dbSet; private BedethequeContext _caveAVinContext; public AbstractEntityRepository(BedethequeContext bedethequeContext) { _bedethequeContext= bedethequeContext; dbSet = bedethequeContext.Set<T>(); } public void Add(T entity) { dbSet.Add(entity); } public void Update(T entity) { _caveAVinContext.Entry(entity).State = System.Data.EntityState.Modified; } public void Delete(T entity) { dbSet.Remove(entity); } public T GetById(int id) { return dbSet.Find(id); } public IQueryable<T> Get() { return dbSet; } }
Analysons de plus près cette classe. Commençons par les variables privées et le constructeur :
private DbSet<T> dbSet; private BedethequeContext _caveAVinContext; public AbstractEntityRepository(BedethequeContext bedethequeContext) { _bedethequeContext= bedethequeContext; dbSet = bedethequeContext.Set<T>(); }
Notre Repository porte un contexte Entity Framework, qui est l’Unit of Work d’EF. Il est donc à l’heure actuelle fortement lié à EF. Il porte aussi un DbSet, qui est le repository interne de Entity Framework. On injecte le contexte via le constructeur, et on récupère le DbSet depuis le contexte, grâce à sa méthode générique Set<T>(). En effet, BedethequeContext.Set<BD>() rend le même résultat que BedethequeContext.BDs. Le plus dur est fait. Passons maintenant aux méthodes CRUD :
public void Add(T entity) { dbSet.Add(entity); } public void Update(T entity) { _caveAVinContext.Entry(entity).State = System.Data.EntityState.Modified; } public void Delete(T entity) { dbSet.Remove(entity); } public T GetById(int id) { return dbSet.Find(id); } public IQueryable<T> Get() { return dbSet; }
Nous utilisons simplement les méthodes natives d’Entity Framework. Les méthodes Add, Delete, GetById et Get se passent de commentaire. La méthode Update demande un peu plus d’explication. L’action de mise à jour consiste à notifier à Entity que l’objet a été modifié et donc à forcer sa mise à jour au prochain appel de SaveChanges. Les choses se compliquent lorsque l’on manipule des grappes d’objets liés en mode déconnecté. Dans ce cas, il faudra mettre en oeuvre une mécanique de suivi d’état des objets. Nous verrons cela dans un article une autre fois, si vous le voulez bien. Pour le moment, nous allons partir du postulat que nous manipulons des entités atomiques, comme c’est souvent le cas dans un projet Web. L’autre extrême étant le cas d’une application client riche, dans lequel nous pouvons nous permettre de garder une connexion à la base de données ouverte plus longtemps. Dans ce cas, notre objet BedethequeContext serait crée tôt et détruit tard. Et tout ce qui se passe entre ces deux évènements est suivi automatiquement par EF. C’est uniquement dans le cas des applications en mode déconnecté manipulant des grappes d’objet que les choses se compliquent.
Et enfin, voici le code de nos vrais répositories :
public class BDEntityRepository : AbstractEntityRepository<BD> { public BDEntityRepository(BedethequeContext context) : base(context) { } } public class AuthorEntityRepository : AbstractEntityRepository<Author> { public AuthorEntityRepository(BedethequeContext context) : base(context) { } } public class PublisherEntityRepository : AbstractEntityRepository<Publisher> { public PublisherEntityRepository(BedethequeContext context) : base(context) { } }
Promesse tenue : on ne peut pas dire que le code soit lourd à maintenir. Nous pourrions même pousser le vice à coder un templace T4 pour générer ces Repositories pour chacune de nos entités. Pour le moment, chacun de ces Repositories se cantonne à de simples opérations CRUD. Si elles nécessitent des méthodes propres à leurs domaines de responsabilités respectives, c’est ici qu’elles se placeront.
Dans le prochain épisode : l’Unit of Work.
Bonjour !
Bravo pour cette initiative : parler de ce sujet en français n’est pas chose courante !
Une question, liée également à votre ouvrage “Visual Studio et TFS 2013” (dont d’ailleurs il reste des traces dans le second exemple de votre code) : je suis coincé lorsque vous indiquez “Si elles nécessitent des méthodes propres à leurs domaines de responsabilités respectives, c’est ici qu’elles se placeront”.
Pour ajouter des méthodes simples, pas de soucis, a priori : je passe l’objet concerné en argument. Mais comment dois-je faire si la méthode d’un repository doit utiliser des entités qui sont dans un autre repository ? Par exemple si je veux dans une méthode de BDEntityRepository la liste de tous les auteurs, indépendamment de la DB ?
Pouvez-vous m’éclairer sur sur point ?
MERCI !
Bonjour,
Si vous ne voulez/pouvez pas instancier un AuthorEntityRepository au niveau du client, le mieux est d’instancier dans BDEntityRepository un AuthorEntityRepository et de lui injecter le même contexte.
Nicholas