是否使用存储库抽象而非实体框架良好实践?

时间:2013-07-24 16:11:54

标签: .net entity-framework

我即将开始一个新项目,它将使用实体框架(EF)作为ORM与SQL Server进行链接。我一直在做大量的阅读,以了解在抽象方面使用EF的最佳方法是什么,并最终使我的代码可以测试。

问题是我做的阅读越多,我就越困惑。

我认为这是一个很好的方法:

我打算沿着存储库路线走下去,制作它们以便通过接口轻松注入并且易于模拟以进行测试,从我似乎发现的许多示例中,有许多人发誓这种方法。对于与EF交互的存储库部分,我只是要进行集成测试,因为我的Repos中没有业务逻辑,我会将其转移到服务/控制器类来处理它。

有人说存储库模式是一种无根据的抽象

http://ayende.com/blog/3955/repository-is-the-new-singleton

还有其他链接,但我不想用太多的来淹没这个问题

我得到了这个,我明白EF和它的实现本质上是一个抽象,但对我来说它似乎是一个具体的(它击中了应用程序范围之外的东西......数据库)问题对我来说,它是推动测试问题(至少在初始架构中),我可能会在我的服务中与数据上下文和工作单元进行交互,但这感觉就像是在混合数据访问登录与业务在那个级别的逻辑,然后我该怎么做单元测试呢?

我似乎已经阅读了很多,以至于我现在没有明确的方向和不可避免的编码器块已经开始。我正在寻找一种干净的方法,我可以测试。

更新 - 解决方案我将从

开始

我走了一条与simon给出的答案略有不同的路径,但感谢我读过的一篇文章,但他再次提交,所以我再次讨论了它。首先我使用Code First,因为我有一个现有的数据库,并且出于某种原因不喜欢设计器工具。我创建了一些简单的POCO对象和一些映射,为了简单起见,我只是要展示一个POCO / Mapping,什么不是。

POCO

public abstract class BaseEntity<T>
{
    [Key]
    public T Id { get; set; }
    public string Name { get; set; }
    public DateTime CreatedOn { get; set; }
}    

public class Project : BaseEntity<int>
{
    public virtual ICollection<Site> Sites { get; set; }
    public bool Active { get; set; }
}    

工作单元界面

public interface IUnitOfWork
{
    IDbSet<Project> Projects { get; }

    void Commit();
}

上下文

internal class MyContext : DbContext
{
    public MyContext(string connectionString)
        : base(connectionString)
    {

    }

    protected override void OnModelCreating(DbModelBuilder modelBuilder)
    {
        modelBuilder.Configurations.Add(new ProjectMap());
    }
}

具体单位工作

public class UnitOfWork : IUnitOfWork
{
    readonly MyContext _context;
    const string ConnectionStringName = "DBConn";

    public UnitOfWork()
    {
        var connectionString = ConfigurationManager.ConnectionStrings[ConnectionStringName].ConnectionString;
        _context = new FosilContext(connectionString);
    }

    public IDbSet<Project> Projects
    {
        get { return _context.Set<Project>(); }
    }

    public void Commit()
    {
        _context.SaveChanges();
    }
}

基本测试代码

        var _repo = new UnitOfWork().Projects;
        var projects = from p in _repo
                       select p;

        foreach (var project in projects)
        {
            Console.WriteLine(string.Format("{0} - {1} - {2} - {3})", project.Id, project.Name, project.Active.ToString(), project.CreatedOn.ToShortDateString()));
        }

        Console.Read();

UnitOfWork是自我解释的,但基于具体的上下文,我可以创建一个虚假的IUnitOfWork并将其传递到假的IDBSet进行测试。对我来说,这可能会让我在建筑上咬我更多,但我没有在这个抽象的EF之外有一个回购(我想)。正如文章解释的那样,IDbSet相当于一个Repo,所以我正在使用它。我有点隐藏了我在UoW背后的自定义环境,但我将会看到它是如何实现的。

我现在在想的是我可以在服务层中使用它,它将封装检索数据和业务规则,但应该是单元可测试的,因为我可以伪造EF特定项目。希望这会让我的控制器非常精简,但我们会看到:)

2 个答案:

答案 0 :(得分:7)

不要忘记,如果您使用的是实体框架,DbContext 类似于UnitOfWork,而DbSet 类似于存储库。我认为,这个问题引发了个人观点,可能不适合Stack Overflow,但我认为如果你打算使用Entity Framework,你也可以接受这种模式。 DbContextDbSet的进一步包装实际上是为了便于单元测试以及在某种程度上依赖注入,在具体类上给出了非常薄的抽象。

MSDN(Testability and Entity Framework 4.0)上有一个主题可以为您提供一个很好的起点。这里和那里有一些资源建议如何在实体框架的上下文中实现这种模式。在大多数情况下,您将使用DbContext执行SaveChanges()并使用DbSet<T>IQueryable<T>DbSet<T>工具的帮助下执行CRUD操作。您可以实施IUnitOfWork来模仿DbContext上的所需内容以及DbSet的相同内容。

这方面的实现几乎是DbContextDbSet<T>中方法的一对一映射,但您也可以实现一个实现基于内存的{I的自定义IUnitOfWork {{ 1}}(例如使用IRepository<T>)便于对Queryable组件进行查询逻辑的单元测试。存储库模式是个好主意吗?这是值得商榷的。这实际上取决于项目和您需要的灵活性以及您希望不同层执行的操作(以及您不想要的内容)。

编辑以回答评论: 您的HashSet实现不应继承UnitOfWork,而是创建实例,将其保密,并将自己的方法调用委托给上下文。您的DbContext实现可以在内部构造函数中合理地使用Repository<T>。模型优先方法的一大优势是给予装配组织的自由。我通常最终做的是将模型和DbContext分成两个单独的程序集,后者引用前者。这种情况看起来像:

1 - 您的Models.dll程序集包含您的POCO

2 - 您的Data.dll程序集包含您的接口和实现。您还可以(例如)Data.Core.dll包含您的数据访问层的接口和常用实用程序,并具有可互换的Data.Entity.dll(或Data.List.dll)供您实际实施。如果我们选择第一个选项,它可能看起来像:

DbSet<T>

在这种情况下,只有您的界面和public interface IUnitOfWork { /* Your methods */ } public interface IRepository<T> where T : class { /* Your methods */ } internal class YourDbContext : DbContext { /* Your implementation */ } public class YourDatabaseContext : IUnitOfWork { private readonly YourDbContext dbContext; public YourDatabaseContext() { // You could also go with the Lazy pattern here to defer creation dbContext = new YourDbContext(); } } internal class DbSetRepository<T> : IRepository<T> where T : class { private readonly DbSet<T> dbSet; public DbSetRepository(DbSet<T> dbSet) { // You could also use IDbSet<T> for a toned down version this.dbSet = dbSet; } } 实现在程序集外部可见(例如,如果您想使用依赖注入)。这并不重要,实际上是设计选择的问题。因为您的IUnitOfWork将通过内部实施的定义与您的POCO“链接”,所有内容都将在您的配置中连接。

答案 1 :(得分:0)

我们一直在使用我们自己的ORM抽象框架,它支持存储库,工作单元和规范模式几年,根据我们的经验,我肯定会说是的!

这种抽象并不一定能保证互换性。另一个非常重要的好处是可扩展性。

例如,对于一个项目,在发布之后,新需求是某些实体上的“已发布”属性,并且UI必须隐藏未发布的实体。对于这种情况,我们扩展了我们的框架以支持“默认规范”,这与Hibernate的“会话过滤器”非常相似,它适用于通过单个DbContext进行的所有查询。

由于我们已经有了一个“请求范围”的DbContext生命周期系统(实际上是现在基于owin和.net核心的应用程序的首选方式),我们很容易添加这样的API:

CurrentContext.AddDefaultSpec( new Spec<Product>(t => t.Published) )

此调用将给定规范添加到当前上下文中的产品规格列表中​​,并将所有产品规范应用于在

上执行的查询表达式
Repository<Product>.Query().Where(....)

当然,我们的规范系统已经能够合并来自多个lambdas和规范的表达式,但它超出了范围。

为了执行这个功能,我们开发了一个简单的ASP .NET MVC过滤器属性,在我们的大多数控制器动作中(管理部分除外)我们都放了这个属性。

一个注意事项:由于抽象同步和异步方法非常困难并且不值得付出努力,而且我们还需要一些EF特定的调用,如.Include()等,客户端代码仍然引用了EF如果我们需要用另一个ORM替换EF,则无济于事。但这不是一项要求,在实践中,大多数项目都没有提出这样的要求。

因此,如果使用存储库对其进行抽象的原因只是为了创建一个“理想的”平台无关API,并且如果您没有构建一个通用框架来定位不同的ORM或数据访问接口,我认为它不是正确的方式。

我们的主要目的是创建一个基于EF的内部框架,其中包含额外的实用程序,支持通用设计模式和事件驱动的可定制工作单元(即观察某些类型的对象添加到uow时的事件)所以它是解决手头问题的合适解决方案。虽然实施起来并不容易。