web-dev-qa-db-fra.com

EF6 Code First avec référentiel générique et injection de dépendances et SoC

Après beaucoup de lecture et d'essais avec Entity Framework dernière version stable (6.1.1).

Je lis beaucoup de contradictions sur l'utilisation ou non des référentiels avec EF6 ou EF en général, car c'est DbContext fournit déjà un référentiel et DbSet le UoW, prêt à l'emploi.

Permettez-moi d'abord d'expliquer ce que contient ma solution en termes de projet, puis je reviendrai sur la contradiction.

Il a un projet de bibliothèque de classe et un asp.net-mvc projet. Le projet lib de classe étant l'accès aux données et où Migrations sont activés pour Code First.

Dans mon projet de bibliothèque de classes, j'ai un référentiel générique:

public interface IRepository<TEntity> where TEntity : class
{
    IEnumerable<TEntity> Get();

    TEntity GetByID(object id);

    void Insert(TEntity entity);

    void Delete(object id);

    void Update(TEntity entityToUpdate);
}

Et voici la mise en œuvre de celui-ci:

public class Repository<TEntity> where TEntity : class
{
    internal ApplicationDbContext context;
    internal DbSet<TEntity> dbSet;

    public Repository(ApplicationDbContext context)
    {
        this.context = context;
        this.dbSet = context.Set<TEntity>();
    }

    public virtual IEnumerable<TEntity> Get()
    {
        IQueryable<TEntity> query = dbSet;
        return query.ToList();
    }

    public virtual TEntity GetByID(object id)
    {
        return dbSet.Find(id);
    }

    public virtual void Insert(TEntity entity)
    {
        dbSet.Add(entity);
    }

    public virtual void Delete(object id)
    {
        TEntity entityToDelete = dbSet.Find(id);
        Delete(entityToDelete);
    }

    public virtual void Update(TEntity entityToUpdate)
    {
        dbSet.Attach(entityToUpdate);
        context.Entry(entityToUpdate).State = EntityState.Modified;
    }
}

Et voici quelques entités:

public DbSet<User> User{ get; set; }
public DbSet<Order> Orders { get; set; }
public DbSet<UserOrder> UserOrders { get; set; }
public DbSet<Shipment> Shipments { get; set; }

Je ne veux pas me répéter mais, avec EF6 vous ne passez plus de référentiels, mais le DbContext à la place. Donc pour DI j'ai mis en place ce qui suit dans le asp-net-mvc projet utilisant Ninject:

private static void RegisterServices(IKernel kernel)
{
    kernel.Bind<ApplicationDbContext>().ToSelf().InRequestScope();
}

Et cela injectera le ApplicationDbContext via l'injection de constructeur dans les classes de couches supérieures, le cas échéant.

Revenons maintenant à la contradiction.

Si nous n'avons plus besoin d'un référentiel parce que EF le fournit déjà hors de la boîte, comment faisons-nous Separation of Concern (abrégé en SoC dans le titre)?

Maintenant, corrigez-moi si je me trompe, mais il me semble que je dois juste faire toutes les logiques/calculs d'accès aux données (comme ajouter, récupérer, mettre à jour, supprimer et quelques logique/calculs personnalisés ici et là (spécifique à l'entité)) dans le asp.net-mvc projet, si je n'ajoute pas de référentiel.

Toute lumière à ce sujet est vraiment appréciée.

26
Quoter

J'espère qu'une petite explication éclaircira votre confusion. Le modèle de référentiel existe pour abstraire la connexion à la base de données et la logique d'interrogation. Les ORM (mappeurs objet-relationnels, comme EF) existent sous une forme ou une autre depuis si longtemps que beaucoup de gens ont oublié ou n'ont jamais eu l'immense joie et le plaisir de traiter avec du code spaghetti jonché de requêtes et d'instructions SQL. Il était temps que si vous vouliez interroger une base de données, vous étiez en fait responsable de choses folles comme l'initiation d'une connexion et la construction d'instructions SQL à partir d'éther. Le but du modèle de référentiel était de vous donner un endroit unique pour mettre toute cette méchanceté, loin de votre beau code d'application vierge.

Avance rapide jusqu'en 2014, Entity Framework et d'autres ORM sont votre référentiel. Toute la logique SQL est soigneusement emballée loin de vos regards indiscrets, et à la place, vous avez une belle API de programmation à utiliser dans votre code. À un égard, c'est assez d'abstraction. La seule chose qu'il ne couvre pas est la dépendance vis-à-vis de l'ORM lui-même. Si vous décidez plus tard que vous souhaitez désactiver Entity Framework pour quelque chose comme NHibernate ou même une API Web, vous devez faire une opération sur votre application pour le faire. Par conséquent, ajouter une autre couche d'abstraction est toujours une bonne idée, mais tout simplement pas un référentiel, ou du moins disons un référentiel typique.

Le référentiel dont vous disposez est un référentiel typique . Il crée simplement des proxys pour les méthodes d'API Entity Framework. Vous appelez repo.Add Et le référentiel appelle context.Add. C'est franchement ridicule, et c'est pourquoi beaucoup, y compris moi-même, disent n'utilisent pas de référentiels avec Entity Framework .

Alors, que devriez-vous faire? Créez des services, ou peut-être est-il préférable de parler de "classes de type service". Lorsque les services commencent à être discutés en relation avec .NET, tout à coup, vous parlez de toutes sortes de choses qui ne sont absolument pas pertinentes pour ce dont nous discutons ici. Une classe de type service est comme un service en ce sens qu'elle a des points de terminaison qui renvoient un ensemble particulier de données ou exécutent une fonction très spécifique sur un ensemble de données. Par exemple, alors qu'avec un référentiel typique, vous trouverez vous-même des choses comme:

articleRepo.Get().Where(m => m.Status == PublishStatus.Published && m.PublishDate <= DateTime.Now).OrderByDescending(o => o.PublishDate)

Votre classe de service fonctionnerait comme:

service.GetPublishedArticles();

Vous voyez, toute la logique de ce qui est considéré comme un "article publié" est soigneusement contenue dans la méthode du point final. De plus, avec un référentiel, vous exposez toujours l'API sous-jacente. Il est plus facile de passer à autre chose car le magasin de données de base est abstrait, mais si l'API pour interroger ce magasin change, vous êtes toujours dans un ruisseau .

MISE À JOUR

La configuration serait très similaire; la différence réside principalement dans la façon dont vous utilisez un service par rapport à un référentiel. À savoir, je ne le rendrais même pas dépendant de l'entité. En d'autres termes, vous auriez essentiellement un service par contexte, pas par entité.

Comme toujours, commencez par une interface:

public interface IService
{
    IEnumerable<Article> GetPublishedArticles();

    ...
}

Ensuite, votre implémentation:

public class EntityFrameworkService<TContext> : IService
    where TContext : DbContext
{
    protected readonly TContext context;

    public EntityFrameworkService(TContext context)
    {
        this.context = context;
    }

    public IEnumerable<Article> GetPublishedArticles()
    {
        ...
    }
}

Ensuite, les choses commencent à devenir un peu velues. Dans l'exemple de méthode, vous pouvez simplement référencer directement le DbSet, c'est-à-dire context.Articles, Mais cela implique une connaissance des noms DbSet dans le contexte. Il vaut mieux utiliser context.Set<TEntity>(), pour plus de flexibilité. Avant de sauter trop de trains, je tiens à souligner pourquoi j'ai nommé ce EntityFrameworkService. Dans votre code, vous ne feriez référence qu'à votre interface IService. Ensuite, via votre conteneur d'injection de dépendances, vous pouvez remplacer EntityFrameworkService<YourContext> Par cela. Cela ouvre la possibilité de créer d'autres fournisseurs de services comme peut-être WebApiService, etc.

Maintenant, j'aime utiliser une seule méthode protégée qui renvoie une requête que toutes mes méthodes de service peuvent utiliser. Cela supprime une grande partie de la cruauté comme avoir à initialiser une instance DbSet à chaque fois via var dbSet = context.Set<YourEntity>();. Cela ressemblerait un peu à:

protected virtual IQueryable<TEntity> GetQueryable<TEntity>(
    Expression<Func<TEntity, bool>> filter = null,
    Func<IQueryable<TEntity>, IOrderedQueryable<TEntity>> orderBy = null,
    string includeProperties = null,
    int? skip = null,
    int? take = null)
    where TEntity : class
{
    includeProperties = includeProperties ?? string.Empty;
    IQueryable<TEntity> query = context.Set<TEntity>();

    if (filter != null)
    {
        query = query.Where(filter);
    }

    foreach (var includeProperty in includeProperties.Split
        (new char[] { ',' }, StringSplitOptions.RemoveEmptyEntries))
    {
        query = query.Include(includeProperty);
    }

    if (orderBy != null)
    {
        query = orderBy(query);
    }

    if (skip.HasValue)
    {
        query = query.Skip(skip.Value);
    }

    if (take.HasValue)
    {
        query = query.Take(take.Value);
    }

    return query;
}

Notez que cette méthode est, tout d'abord, protégée. Les sous-classes peuvent l'utiliser, mais cela ne devrait certainement pas faire partie de l'API publique. L'intérêt de cet exercice est de ne pas exposer les requêtes. Deuxièmement, c'est générique. En d'autres termes, il peut gérer n'importe quel type que vous lui lancez tant qu'il y a quelque chose dans le contexte.

Ensuite, dans notre petit exemple de méthode, vous finiriez par faire quelque chose comme:

public IEnumerable<Article> GetPublishedArticles()
{
    return GetQueryable<Article>(
        m => m.Status == PublishStatus.Published && m.PublishDate <= DateTime.Now,
        m => m.OrderByDescending(o => o.PublishDate)
    ).ToList();
}

Une autre astuce intéressante à cette approche est la possibilité d'avoir des méthodes de service génériques utilisant des interfaces. Disons que je voulais pouvoir avoir une méthode pour faire publier n'importe quoi . Je pourrais avoir une interface comme:

public interface IPublishable
{
    PublishStatus Status { get; set; }
    DateTime PublishDate { get; set; }
}

Ensuite, toute entité publiable implémenterait simplement cette interface. Avec cela en place, vous pouvez maintenant faire:

public IEnumerable<TEntity> GetPublished<TEntity>()
    where TEntity : IPublishable
{
    return GetQueryable<TEntity>(
        m => m.Status == PublishStatus.Published && m.PublishDate <= DateTime.Now,
        m => m.OrderByDescending(o => o.PublishDate)
    ).ToList();
}

Et puis dans votre code d'application:

service.GetPublished<Article>();
40
Chris Pratt