web-dev-qa-db-fra.com

Entity Framework 6 Code First - La mise en œuvre du référentiel est-elle bonne?

Je suis sur le point de mettre en œuvre une conception Entity Framework 6 avec un référentiel et une unité de travail.

Il y a tellement d'articles autour et je ne sais pas quel est le meilleur conseil: par exemple, j'aime vraiment le modèle implémenté ici: pour les raisons suggérées dans l'article ici

Cependant, Tom Dykstra (Senior Programming Writer on Microsoft's Web Platform & Tools Content Team) suggère que cela devrait être fait dans un autre article: ici

Je m'abonne à Pluralsight, et il est implémenté d'une manière légèrement différente à peu près chaque fois qu'il est utilisé dans un cours, donc choisir un design est difficile.

Certaines personnes semblent suggérer que l'unité de travail est déjà implémentée par DbContext comme dans ce post , donc nous ne devrions pas du tout l'implémenter.

Je me rends compte que ce type de question a déjà été posée et cela peut être subjectif mais ma question est directe:

J'aime l'approche du premier article (Code Fizzle) et je voulais savoir si elle est peut-être plus maintenable et aussi facilement testable que d'autres approches et sûre pour aller de l'avant?

Toute autre vue est plus que bienvenue.

49
davy

@Chris Hardie est correct, EF implémente UoW hors de la boîte. Cependant, de nombreuses personnes ignorent le fait qu'EF implémente également un modèle de référentiel générique prêt à l'emploi:

var repos1 = _dbContext.Set<Widget1>();
var repos2 = _dbContext.Set<Widget2>();
var reposN = _dbContext.Set<WidgetN>();

... et ceci est une très bonne implémentation générique du référentiel qui est intégrée à l'outil lui-même.

Pourquoi se donner la peine de créer une tonne d'autres interfaces et propriétés, alors que DbContext vous donne tout ce dont vous avez besoin? Si vous voulez faire abstraction du DbContext derrière les interfaces au niveau de l'application et que vous souhaitez appliquer la séparation des requêtes de commande, vous pouvez faire quelque chose d'aussi simple que ceci:

public interface IReadEntities
{
    IQueryable<TEntity> Query<TEntity>();
}

public interface IWriteEntities : IReadEntities, IUnitOfWork
{
    IQueryable<TEntity> Load<TEntity>();
    void Create<TEntity>(TEntity entity);
    void Update<TEntity>(TEntity entity);
    void Delete<TEntity>(TEntity entity);
}

public interface IUnitOfWork
{
    int SaveChanges();
}

Vous pouvez utiliser ces 3 interfaces pour tous vos accès d'entité, et ne pas avoir à vous soucier d'injecter 3 référentiels différents ou plus dans un code d'entreprise qui fonctionne avec 3 ensembles d'entités ou plus. Bien sûr, vous utiliseriez toujours l'IoC pour vous assurer qu'il n'y a qu'une seule instance DbContext par demande Web, mais les 3 de vos interfaces sont implémentées par la même classe, ce qui facilite la tâche.

public class MyDbContext : DbContext, IWriteEntities
{
    public IQueryable<TEntity> Query<TEntity>()
    {
        return Set<TEntity>().AsNoTracking(); // detach results from context
    }

    public IQueryable<TEntity> Load<TEntity>()
    {
        return Set<TEntity>();
    }

    public void Create<TEntity>(TEntity entity)
    {
        if (Entry(entity).State == EntityState.Detached)
            Set<TEntity>().Add(entity);
    }

    ...etc
}

Vous n'avez désormais plus qu'à injecter une seule interface dans votre dépendance, quel que soit le nombre d'entités différentes avec lesquelles elle doit travailler:

// NOTE: In reality I would never inject IWriteEntities into an MVC Controller.
// Instead I would inject my CQRS business layer, which consumes IWriteEntities.
// See @MikeSW's answer for more info as to why you shouldn't consume a
// generic repository like this directly by your web application layer.
// See http://www.cuttingedge.it/blogs/steven/pivot/entry.php?id=91 and
// http://www.cuttingedge.it/blogs/steven/pivot/entry.php?id=92 for more info
// on what a CQRS business layer that consumes IWriteEntities / IReadEntities
// (and is consumed by an MVC Controller) might look like.
public class RecipeController : Controller
{
    private readonly IWriteEntities _entities;

    //Using Dependency Injection 
    public RecipeController(IWriteEntities entities)
    {
        _entities = entities;
    }

    [HttpPost]
    public ActionResult Create(CreateEditRecipeViewModel model)
    {
        Mapper.CreateMap<CreateEditRecipeViewModel, Recipe>()
            .ForMember(r => r.IngredientAmounts, opt => opt.Ignore());

        Recipe recipe = Mapper.Map<CreateEditRecipeViewModel, Recipe>(model);
        _entities.Create(recipe);
        foreach(Tag t in model.Tags) {
            _entities.Create(tag);
        }
        _entities.SaveChanges();
        return RedirectToAction("CreateRecipeSuccess");
    }
}

Une de mes choses préférées à propos de cette conception est qu'elle minimise les dépendances de stockage d'entité sur le consommateur. Dans cet exemple, le RecipeController est le consommateur, mais dans une application réelle, le consommateur serait un gestionnaire de commandes. (Pour un gestionnaire de requêtes, vous consommez généralement IReadEntities uniquement parce que vous voulez simplement renvoyer des données, sans muter aucun état.) Mais pour cet exemple, utilisons simplement RecipeController comme consommateur pour examiner les implications de la dépendance:

Supposons que vous ayez un ensemble de tests unitaires écrits pour l'action ci-dessus. Dans chacun de ces tests unitaires, vous renouvelez le contrôleur, en passant une maquette dans le constructeur. Supposez ensuite que votre client décide qu'il souhaite autoriser les utilisateurs à créer un nouveau livre de recettes ou à en ajouter un lors de la création d'une nouvelle recette.

Avec un modèle d'interface référentiel par entité ou référentiel par agrégat, vous devez injecter une nouvelle instance de référentiel IRepository<Cookbook> dans le constructeur de votre contrôleur (ou en utilisant la réponse de @Chris Hardie, écrivez du code pour attacher un autre référentiel à l'instance UoW). Cela entraînerait immédiatement la rupture de tous vos autres tests unitaires, et vous devriez revenir en arrière pour modifier le code de construction dans chacun d'eux, passer une autre instance fictive et élargir votre tableau de dépendances. Cependant, avec ce qui précède, tous vos autres tests unitaires seront toujours au moins compilés. Tout ce que vous avez à faire est d'écrire des tests supplémentaires pour couvrir la nouvelle fonctionnalité du livre de recettes.

46
danludwig

Je suis (pas) désolé de dire que le codefizzle, l'article de Dyksta et les réponses précédentes sont faux. Pour le simple fait qu'ils utilisent les entités EF comme objets de domaine (métier), ce qui est un gros WTF.

Mettre à jour: Pour une explication moins technique (en termes simples), lire Modèle de référentiel pour les nuls

En bref, N'IMPORTE QUELLE interface de référentiel ne doit pas être couplée à N'IMPORTE QUEL détail de persistance (ORM). L'interface de dépôt traite UNIQUEMENT des objets qui ont du sens pour le reste de l'application (domaine, peut-être l'interface utilisateur comme dans la présentation). BEAUCOUP de personnes (avec MS en tête du peloton, avec l'intention que je soupçonne) font l'erreur de croire qu'elles peuvent réutiliser leurs entités EF ou qui peuvent être un objet métier au-dessus d'eux.

Bien que cela peut se produire, c'est assez rare. En pratique, vous aurez beaucoup d'objets de domaine "conçus" d'après les règles de la base de données, c'est-à-dire une mauvaise modélisation. L'objectif du référentiel est de dissocier le reste de l'application (principalement la couche métier) de sa forme de persistance.

Comment le découpler lorsque votre dépôt traite des entités EF (détail de persistance) ou que ses méthodes retournent IQueryable, une abstraction qui fuit avec une sémantique erronée à cet effet (IQueryable vous permet de construire une requête, ce qui implique que vous devez connaître les détails de persistance ainsi niant le but et la fonctionnalité du référentiel)?

Un objet dominant ne devrait jamais connaître la persistance, les EF, les jointures, etc. Il ne devrait pas savoir quel moteur db vous utilisez ou si vous en utilisez un. Même chose avec le reste de l'application, si vous voulez qu'elle soit découplée des détails de persistance.

L'interface du référentiel ne connaît que ce que la couche supérieure sait. Cela signifie qu'une interface générique de référentiel de domaine ressemble à ceci

public interface IStore<TDomainObject> //where TDomainObject != Ef (ORM) entity
{
   void Save(TDomainObject entity);
   TDomainObject Get(Guid id);
   void Delete(Guid id);
 }

L'implémentation résidera dans le DAL et utilisera EF pour travailler avec la base de données. Cependant, l'implémentation ressemble à ceci

public class UsersRepository:IStore<User>
 {
   public UsersRepository(DbContext db) {}


    public void Save(User entity)
    {
       //map entity to one or more ORM entities
       //use EF to save it
    }
           //.. other methods implementation ...

 }

Vous n'avez pas vraiment de référentiel générique concret . La seule utilisation d'un référentiel générique concret est lorsque TOUT objet de domaine est stocké sous forme sérialisée dans une table de type valeur-clé. Ce n'est pas le cas avec un ORM.

Qu'en est-il des requêtes?

 public interface IQueryUsers
 {
       PagedResult<UserData> GetAll(int skip, int take);
       //or
       PagedResult<UserData> Get(CriteriaObject criteria,int skip, int take); 
 }

Le UserData est le modèle de lecture/visualisation adapté à l'utilisation du contexte de requête.

Vous pouvez utiliser directement EF pour les requêtes dans un gestionnaire de requêtes si cela ne vous dérange pas que votre DAL connaisse les modèles de vue et dans ce cas, vous n'aurez pas besoin d'un dépôt de requête.

Conclusion

  • Votre objet métier ne doit pas connaître les entités EF.
  • Le référentiel utilisera un ORM , mais il n'exposera jamais l'ORM à le reste de l'application, donc l'interface de repo n'utilisera que des objets de domaine ou des modèles de vue (ou tout autre objet de contexte d'application qui n'est pas un détail de persistance)
  • Vous ne dites pas au repo comment faire son travail c'est-à-dire NE JAMAIS utiliser IQueryable avec une interface repo
  • Si vous voulez simplement utiliser la base de données de manière plus simple/cool et que vous avez affaire à une application CRUD simple où vous n'avez pas besoin (soyez sûr) de maintenir la séparation des préoccupations, alors ignorez le référentiel tout ensemble, utilisez directement EF pour toutes les données. L'application sera étroitement couplée à EF, mais au moins vous couperez l'homme du milieu et ce sera exprès, pas par erreur.

Notez que l'utilisation incorrecte du référentiel invalidera son utilisation et votre application sera toujours étroitement couplée à la persistance (ORM).

Si vous pensez que l'ORM est là pour stocker comme par magie vos objets de domaine, ce n'est pas le cas. Le but de l'ORM est de simuler un stockage OOP au-dessus des tables relationnelles. Il a tout à voir avec la persistance et rien à voir avec le domaine, donc n'utilisez pas l'ORM en dehors de la persistance.

42
MikeSW

DbContext est en effet construit avec le motif Unit of Work. Il permet à toutes ses entités de partager le même contexte que nous travaillons avec elles. Cette implémentation est interne au DbContext.

Cependant, il convient de noter que si vous instanciez deux objets DbContext, aucun d'eux ne verra les entités de l'autre qu'ils suivent chacun. Ils sont isolés les uns des autres, ce qui peut être problématique.

Lorsque je crée une application MVC, je veux m'assurer qu'au cours de la demande, tout mon code d'accès aux données fonctionne à partir d'un seul DbContext. Pour y parvenir, j'applique l'unité de travail en tant que modèle externe à DbContext.

Voici mon objet Unité de travail à partir d'une application de recette de barbecue que je construis:

public class UnitOfWork : IUnitOfWork
{
    private BarbecurianContext _context = new BarbecurianContext();
    private IRepository<Recipe> _recipeRepository;
    private IRepository<Category> _categoryRepository;
    private IRepository<Tag> _tagRepository;

    public IRepository<Recipe> RecipeRepository
    {
        get
        {
            if (_recipeRepository == null)
            {
                _recipeRepository = new RecipeRepository(_context);
            }
            return _recipeRepository;
        }
    }

    public void Save()
    {
        _context.SaveChanges();
    }
    **SNIP**

J'attache tous mes référentiels, qui sont tous injectés avec le même DbContext, à mon objet Unit of Work. Tant que tous les référentiels sont demandés à l'objet Unité de travail, nous pouvons être assurés que tous nos codes d'accès aux données seront gérés avec la même DbContext - sauce géniale!

Si je devais l'utiliser dans une application MVC, je m'assurerais que l'unité de travail est utilisée tout au long de la demande en l'instanciant dans le contrôleur et en l'utilisant tout au long de ses actions:

public class RecipeController : Controller
{
    private IUnitOfWork _unitOfWork;
    private IRepository<Recipe> _recipeService;
    private IRepository<Category> _categoryService;
    private IRepository<Tag> _tagService;

    //Using Dependency Injection 
    public RecipeController(IUnitOfWork unitOfWork)
    {
        _unitOfWork = unitOfWork;
        _categoryRepository = _unitOfWork.CategoryRepository;
        _recipeRepository = _unitOfWork.RecipeRepository;
        _tagRepository = _unitOfWork.TagRepository;
    }

Maintenant dans notre action, nous pouvons être assurés que tous nos codes d'accès aux données utiliseront le même DbContext:

    [HttpPost]
    public ActionResult Create(CreateEditRecipeViewModel model)
    {
        Mapper.CreateMap<CreateEditRecipeViewModel, Recipe>().ForMember(r => r.IngredientAmounts, opt => opt.Ignore());

        Recipe recipe = Mapper.Map<CreateEditRecipeViewModel, Recipe>(model);
        _recipeRepository.Create(recipe);
        foreach(Tag t in model.Tags){
             _tagRepository.Create(tag); //I'm using the same DbContext as the recipe repo!
        }
        _unitOfWork.Save();
4
Mister Epic

En cherchant sur Internet, j'ai trouvé ceci http://www.thereformedprogrammer.net/is-the-repository-pattern-useful-with-entity-framework/ c'est un article en 2 parties sur l'utilité du modèle de référentiel par Jon Smith. La deuxième partie se concentre sur une solution. J'espère que ça aide!

3
Martin Blaustein

Le référentiel avec l'implémentation du modèle d'unité de travail est mauvais pour répondre à votre question.

Le DbContext du framework d'entité est implémenté par Microsoft selon l'unité de travail. Cela signifie que le context.SaveChanges enregistre vos modifications en une seule opération.

Le DbSet est également une implémentation du modèle de référentiel. Ne créez pas de référentiels que vous pouvez simplement faire:

void Add(Customer c)
{
   _context.Customers.Add(c);
}

Créez une méthode à une ligne pour ce que vous pouvez faire à l'intérieur du service de toute façon ???

Il n'y a aucun avantage et personne ne change EF ORM en un autre ORM de nos jours ...

Vous n'avez pas besoin de cette liberté ...

Chris Hardie fait valoir qu'il pourrait y avoir plusieurs objets de contexte instanciés mais déjà en faisant cela, vous le faites mal ...

Utilisez simplement un outil IOC que vous aimez et configurez MyContext per Http Request et tout va bien.

Prenez ninject par exemple:

kernel.Bind<ITeststepService>().To<TeststepService>().InRequestScope().WithConstructorArgument("context", c => new ITMSContext());

Le service exécutant la logique métier obtient le contexte injecté.

Gardez les choses simplement stupides :-)

2
Pascal

Vous devriez considérer les "objets de commande/requête" comme une alternative, vous pouvez trouver un tas d'articles intéressants dans ce domaine, mais en voici un bon:

https://rob.conery.io/2014/03/03/repositories-and-unitofwork-are-not-a-good-idea/

Lorsque vous avez besoin d'une transaction sur plusieurs objets de base de données, utilisez un objet de commande par commande pour éviter la complexité du modèle UOW.

Un objet de requête par requête est probablement inutile pour la plupart des projets. Au lieu de cela, vous pouvez choisir de commencer avec un objet 'FooQueries' ... par lequel je veux dire que vous pouvez commencer avec un modèle de référentiel pour READS mais le nommer "Requêtes" pour être explicite qu'il le fait pas et ne doit pas faire d'insertions/mises à jour.

Plus tard, vous pourriez-vous trouver utile de séparer des objets de requête individuels si vous souhaitez ajouter des éléments comme l'autorisation et la journalisation, vous pouvez alimenter un objet de requête dans un pipeline.

1
Darren

J'utilise toujours UoW avec le code EF en premier. Je le trouve plus performant et plus facile à gérer vos contextes, pour éviter les fuites de mémoire et autres. Vous pouvez trouver un exemple de ma solution sur mon github: http://www.github.com/stefchri dans le projet RADAR.

Si vous avez des questions à ce sujet, n'hésitez pas à les poser.

0
Gecko IT