具有通用存储库和依赖注入和 SoC 的 EF6 Code First

Posted

技术标签:

【中文标题】具有通用存储库和依赖注入和 SoC 的 EF6 Code First【英文标题】:EF6 Code First with generic repository and Dependency Injection and SoC 【发布时间】:2014-12-17 14:24:40 【问题描述】:

经过大量阅读和尝试Entity Framework 最新稳定版本(6.1.1)。

我正在阅读很多关于是否使用 EF6EF 的存储库的矛盾,因为它的 DbContext 已经提供了一个存储库,而 DbSet 是开箱即用的 UoW .

让我先解释一下我的解决方案在项目方面包含的内容,然后我会回到矛盾之处。

它有一个类库项目和一个asp.net-mvc 项目。类库项目是数据访问,并且为 Code First 启用了 Migrations

在我的类库项目中,我有一个通用存储库:

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);

下面是它的实现:

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;
    

这里有几个实体:

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

我不想重复自己,但是使用EF6,您不再传递存储库,而是使用DbContext。所以对于DI,我使用Ninjectasp-net-mvc项目中设置了以下内容:

private static void RegisterServices(IKernel kernel)

    kernel.Bind<ApplicationDbContext>().ToSelf().InRequestScope();

这将通过构造函数注入将ApplicationDbContext注入适用的上层类。

现在回到矛盾。

如果我们不再需要存储库,因为 EF 已经提供了开箱即用的存储库,我们该怎么做 Separation of Concern(标题中缩写为 SoC)?

如果我错了,现在纠正我,但听起来我只需要执行所有数据访问逻辑/计算(如添加、获取、更新、删除和一些自定义逻辑/计算(实体具体))在asp.net-mvc项目中,如果我不添加存储库。

非常感谢您对此事的任何了解。

【问题讨论】:

这只是你能得到多抽象的问题。 EF 上下文 确实是一个存储库,但它是一个非常固执己见的存储库,具有自己的一组关系数据库特定方法。如果您也想将其抽象化,则必须使用您自己的通用存储库包装 EF。 我在网上看到的矛盾是最困扰我的。但是现在我们已经弄清楚了,我想知道要走的路是什么。克里斯普拉特用服务说。你说什么? 服务一直存在(并且由于领域驱动设计的影响而得到广泛普及),但我不认为它们是为了替换存储库。据我了解,它们只是常见操作和查询的外观,在大多数实现中,您会看到IRepository 被注入其中。最后,这一切都取决于您的项目范围。您需要问问自己,您选择的抽象级别对项目有多重要,以及从长远来看它将如何影响它,尤其是在可测试性和可扩展性方面。 您希望拥有自己的存储库接口,并依赖于它,而不是某些特定于实现的类型(如 DbContext)。 【参考方案1】:

一点解释有望消除您的困惑。存储库模式的存在是为了抽象出数据库连接和查询逻辑。 ORM(对象关系映射器,如 EF)以一种或另一种形式存在已久,以至于许多人已经忘记或从未享受过处理充斥着 SQL 查询和语句的意大利面条式代码的巨大乐趣和乐趣。时间是,如果你想查询一个数据库,你实际上要负责一些疯狂的事情,比如启动一个连接,并实际从 ether 构造 SQL 语句。存储库模式的重点是为您提供一个单一的地方来放置所有这些讨厌的东西,远离您美丽的原始应用程序代码。

快进到 2014 年,实体框架和其他 ORM 是您的存储库。所有的 SQL 逻辑都被整齐地打包,远离你的窥探,相反,你有一个很好的编程 API 可以在你的代码中使用。一方面,这已经足够抽象了。它唯一没有涵盖的是对 ORM 本身的依赖。如果您后来决定要为 NHibernate 甚至 Web API 之类的东西切换实体框架,那么您必须对您的应用程序进行手术才能这样做。因此,添加另一层抽象仍然是一个好主意,但不是存储库,或者至少可以说是一个典型的存储库。

您拥有的存储库是一个典型 存储库。它只是为实体框架 API 方法创建代理。您调用repo.Add,存储库调用context.Add。坦率地说,这很荒谬,这就是为什么包括我自己在内的许多人说不要将存储库与实体框架一起使用

那么应该你怎么办?创建服务,或者最好将其称为“类服务类”。当开始讨论与 .NET 相关的服务时,突然之间,您谈论的是与我们在此讨论的内容完全无关的各种事物。类服务类与服务类似,因为它具有返回特定数据集或对某些数据集执行非常特定功能的端点。例如,对于典型的存储库,您会发现自己在做如下事情:

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

您的服务类的工作方式如下:

service.GetPublishedArticles();

看,符合“已发表文章”的所有逻辑都巧妙地包含在端点方法中。此外,使用存储库,您仍在公开底层 API。 更容易切换到其他东西,因为基本数据存储是抽象的,但是如果用于查询该数据存储的 API 发生变化,你仍然是一条小河。

更新

设置将非常相似;区别主要在于您使用服务与存储库的方式。也就是说,我什至不会让它依赖于实体。换句话说,你基本上每个上下文都有一个服务,而不是每个实体。

一如既往,从界面开始:

public interface IService

    IEnumerable<Article> GetPublishedArticles();

    ...

然后,你的实现:

public class EntityFrameworkService<TContext> : IService
    where TContext : DbContext

    protected readonly TContext context;

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

    public IEnumerable<Article> GetPublishedArticles()
    
        ...
    

然后,事情开始变得有点棘手。在示例方法中,您可以简单地直接引用DbSet,即context.Articles,但这意味着了解上下文中的DbSet 名称。最好使用context.Set&lt;TEntity&gt;(),以获得更大的灵活性。不过,在我跳火车太多之前,我想指出为什么我将其命名为EntityFrameworkService。在您的代码中,您只会引用您的 IService 接口。然后,通过你的依赖注入容器,你可以用 EntityFrameworkService&lt;YourContext&gt; 代替它。这开启了创建其他服务提供者的能力,例如 WebApiService 等。

现在,我喜欢使用一个受保护的方法,它返回一个我所有服务方法都可以使用的可查询对象。这消除了很多麻烦,比如每次都必须通过var dbSet = context.Set&lt;YourEntity&gt;(); 初始化一个DbSet 实例。看起来有点像:

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;

请注意,此方法首先是受保护的。子类可以使用它,但这应该绝对不是公共 API 的一部分。这个练习的重点是不暴露可查询对象。其次,它是通用的。换句话说,它可以处理你扔给它的任何类型,只要它的上下文中有东西。

然后,在我们的小示例方法中,您最终会执行以下操作:

public IEnumerable<Article> GetPublishedArticles()

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

这种方法的另一个巧妙技巧是能够使用接口提供通用服务方法。假设我希望能够有一种方法来获取已发布的anything。我可以有这样的界面:

public interface IPublishable

    PublishStatus Status  get; set; 
    DateTime PublishDate  get; set; 

然后,任何可发布的实体都会实现这个接口。有了这些,您现在可以执行以下操作:

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();

然后在您的应用程序代码中:

service.GetPublished<Article>();

【讨论】:

非常感谢您的详细解释。看了几遍,还是有一些疑问。我将如何创建/使用 service 对象?我希望一切都是松散耦合的,没有依赖关系。一个示例代码/项目会很好。心想,看了你的帖子后一直在找,只是找不到不使用存储库的。 再次感谢,非常好的和详细的解释。我使用DINinject 的方式,在你的例子中也是这样吗? 是的。这将涵盖上下文的注入。然后你只需要kernel.Bind&lt;IService&gt;().To&lt;EntityFrameworkService&lt;ApplicationDbContext&gt;&gt;().InRequestScope() 来覆盖你的控制器依赖。 @ChrisPratt 我有点困惑,因为这是我第一次尝试实现这一点。您的 EntityFrameworkService 中有任何和所有服务方法吗? IE。如果您要获取所有在职员工或办公地点列表,您会将他们都放在同一个服务中吗? 是的。这个想法是你会有一个类抽象你的上下文。通过使用泛型方法和接口,您可以放弃许多过度专业化的方法。在您确实需要特定于某种实体类型的方法的情况下,您可以利用部分类的概念来保​​持代码分离和有序,同时仍然只使用一个类。

以上是关于具有通用存储库和依赖注入和 SoC 的 EF6 Code First的主要内容,如果未能解决你的问题,请参考以下文章

如何在C#中使用具有存储库模式的依赖注入(AutoFac)

静态库与动态库注入

Angular2.3 自定义库和依赖注入

EF6(代码优先)、MVC、Unity 和没有存储库的服务层

如何添加通用依赖注入[重复]

如何将通用存储库(或业务逻辑层)注入 ViewModel