9

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

问题是我读得越多,我就越对这一切感到困惑。

我认为是一个好方法:

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

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

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 特定项目。希望这将使我的控制器非常精简,但我们会看到:)

4

2 回答 2

9

不要忘记,如果您使用的是实体框架,DbContext 类似于 UnitOfWork 并且DbSet 类似于存储库。这个问题,我认为,邀请个人意见,可能不适合 Stack Overflow,但我认为如果你要使用实体框架,你不妨接受这种模式。DbContext对 s 和s的进一步包装DbSet实际上是为了在具体类上提供一个非常薄的抽象,以便于单元测试,并且在一定程度上,依赖注入。

MSDN 上有一个主题(可测试性和实体框架 4.0)可以为您提供一个很好的起点。这里和那里有一些关于如何在实体框架的上下文中实现这种模式的建议资源。在大多数情况下,您将使用 DbContext 来执行SaveChanges()并在implementation onDbSet<T>的帮助下执行您的 CRUD 操作。您可以实现您的 IUnitOfWork 来模仿您对.IQueryable<T>DbSet<T>DbContextDbSet

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

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

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

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

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实现在程序集之外是可见的(例如,如果你想使用依赖注入)。这并不重要,实际上是设计选择的问题。因为您DbContext将通过内部实现的定义“链接”到您的 POCO,并且所有内容都将在您的配置中连接起来。

于 2013-07-24T16:23:50.783 回答
0

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

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

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

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

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

此调用将给定规范添加到当前上下文中的产品规范列表中,并将所有产品规范应用于执行的查询表达式

Repository<Product>.Query().Where(....)

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

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

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

所以如果用存储库抽象它的原因只是为了创建一个“理想的”平台独立 API,并且如果你没有构建一个通用框架来针对不同的 ORM 或数据访问接口,我认为这不是正确的方法.

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

于 2016-12-01T23:37:54.463 回答