有很多关于存储库模式的博文和误解,尤其是在引入OR/M库后,如Entity Framework。在这篇文章中,我们将研究为什么这种模式仍然有用,使用它的好处是什么,以及我们如何将它与Entity Framework结合使用。尽管微软将其DbContext和DbSet类定义为这种模式的替代品,但我们将看到这种模式仍然可以帮助我们使代码更加简洁。
让我们从Repository Pattern的定义开始。这种模式的最佳定义之一可以在Martin Fowler的《企业架构模式》一书中找到。
存储库在领域和数据映射层之间起中介作用, 就像一个内存中的领域对象集合。
人们可能会问,为什么这个定义这么酷。嗯,我个人喜欢它,因为它强调了存储库模式的两个非常重要的属性。第一个属性是这个模式是一个抽象的,旨在减少复杂性。第二个重要属性是,这个模式实际上是一个反映数据库表的内存集合。
因此,在这篇文章中,我们将介绍:
1.存储库模式--好处和误解
2.存储库模式概述
3.实体框架简要概述
4.实现、测试和嘲讽
1.存储库模式--好处和误解
即使我们使用了Entity Framework,我们最终可能会有很多重复的查询代码。例如,如果我们正在实现一个博客应用程序,我们想在几个地方获得浏览量最大的文章,我们可能最终会出现重复的查询逻辑,看起来就像这样:
var twentyMostViewedPosts = context.Articles
.Where(a => a.IsPublished)
.OrderBy(a => a.Views).Take(20);
我们最终可能会有更复杂的查询,这有可能是通过代码的重复。改变和维护这样的代码不是一件容易的事情。所以,我们仍然调用我们的存储库来帮助我们解决这个问题。我们可以把这个行为封装在Repository里面,然后像这样调用它:
var twentyMostViewedPosts = repository.GetTopTwentyArticles(context);

这样就干净多了,不是吗?另外,如果我们想改变这个逻辑中的某些东西,我们只需要在一个地方做,这是一个巨大的好处。这就是使用Repository Pattern的第一个好处--没有重复的查询逻辑。
第二个明显的好处是,它将应用程序代码与持久化框架分开,并与我们正在使用的数据库分开。基本上,我们可以在我们的存储库中使用不同的OR/M,或者使用完全不同的技术,例如MongoDB或PostgreSQL。
看看过去十年中数据库趋势的变化,很明显我们希望有这样的灵活性。这里的有效问题是:"是的,但你到底多久会改变数据库?"最近我做了一个项目,从SQL Server到ElasticSearch再到RavenDB。
最后,但可能是使用这种模式的主要好处之一是,它简化了单元测试。然而,人们常常有一个误解,认为这种模式可以让你以更容易的方式测试数据访问层。事实并非如此,但它正在成为测试业务逻辑的重要资产。在业务逻辑中模拟资源库的实现很容易。
2.存储库模式概述
正如我们已经提到的,存储库是一个对象的内存集合,该集合需要有一个接口,我们可以使用该接口访问集合中的元素。这就是为什么 Repository 应该公开经典的 CRUD 操作。有些人选择跳过更新操作,因为从内存集合中更新一个对象本质上就是获取它并改变它的值。我就是这样的人 🙂 当然,如果你喜欢,你也可以实现这个功能。
我们将定义一个接口IRepository ,它暴露了这些功能:
- Add - 方法,将定义的类型T的对象添加到我们的存储库中。
- Remove - 方法将从我们的存储库中移除定义类型T的对象。
- Get - 方法将从我们的资源库中获得一个定义类型T的对象。
- GetAll - 方法将从我们的资源库中获得所有的对象
- Find - 方法将从我们的资源库中找到并检索符合特定条件的对象。
注意,在我们的资源库中也没有类似保存 方法的东西。这就是另一个模式,叫做工作单元的地方。这是一个独立的组件,持有不同资源库的信息,并实现保存功能。
3.实体框架简要概述
此刻我们知道我们将使用Entity Framework,如果你对它不熟悉,你可以查看MSDN的相关文章。实质上,Entity Framework是一个对象-关系映射器(O/RM),它使.NET开发者能够使用.NET对象与数据库一起工作。它消除了开发人员通常需要编写的大部分数据访问代码的需要。简而言之,它将代码对象映射到数据库表,反之亦然。
当涉及到使用EntityFramework时,有两种方法。第一种被称为数据库优先的方法。在这种方法中,我们创建一个数据库,使用Entity Framework来创建领域对象,并在此基础上构建代码。第二种被称为代码优先的方法。这就是我们建立存储库的方法。

我们需要知道两个重要的类--DbContext和DbSet。 DbContext是Entity Framework的一个重要类。当使用Entity Framework时,我们需要有一个类在我们的系统中派生这个类。这样,Entity Framework就会知道需要创建什么。继承这个DbContext的类为你想成为模型一部分的类型暴露了DbSet属性。
4.实现、测试和嘲弄
酷,现在我们知道了我们的Repository应该是什么样子,我们可以开始为Repository类写测试了。在我们继续之前有一个小提示,在这个例子中,我使用了xUnit单元测试框架和Moq嘲弄框架。另外,本着TDD的精神,我们将先写测试,后写实现。除此之外,本例中使用的测试类型是简单的TestClass, 下面是它的样子:
public class TestClass
{
public int Id { get; set; }
}
很简单,对吗?它只有一个属性--Id。 让我们定义一下Repository的接口,这样我们就能更好的理解我们的实现:
public interface IReporitory<TEntity> where TEntity : class
{
void Add(TEntity entity);
void Remove(TEntity entity);
TEntity Get(int id);
IEnumerable<TEntity> GetAll();
IEnumerable<TEntity> Find(Expression<Func<TEntity, bool>> predicate);
}
4.1 添加方法
好的,现在让我们看看添加 方法的测试是什么样子的:
[Fact]
public void Add_TestClassObjectPassed_ProperMethodCalled()
{
// Arrange
var testObject = new TestClass();
var context = new Mock<DbContext>();
var dbSetMock = new Mock<DbSet<TestClass>>();
context.Setup(x => x.Set<TestClass>()).Returns(dbSetMock.Object);
dbSetMock.Setup(x => x.Add(It.IsAny<TestClass>())).Returns(testObject);
// Act
var repository = new Repository<TestClass>(context.Object);
repository.Add(testObject);
//Assert
context.Verify(x => x.Set<TestClass>());
dbSetMock.Verify(x => x.Add(It.Is<TestClass>(y => y == testObject)));
}
这里有很多东西需要解释。首先,假设Repository模式将依赖于DbContext类,并且它将通过构造函数来接收它。这就是为什么在测试的Arrange 部分,我们模拟了这个类。我们也创建了一个DbSet类的模拟对象。

然后我们设置了一种方式,DbSet 的Add方法返回testObject, 这只是一个TestClass的对象*,而DbContext的Set方法返回DbSet* 模拟对象。这样做是为了以后能测试这些方法是否被调用,你可以在测试的断言部分看到这一点。
总结一下,我们模拟DbContext 和DbSet,然后调用资源库的Add 方法,最后从模拟对象中验证那些被调用的方法。这个方法的实现是非常直接的。这里就是:
public class Repository<TEntity> : IReporitory<TEntity> where TEntity : class
{
protected readonly DbContext Context;
protected readonly DbSet<TEntity> Entities;
public Repository(DbContext context)
{
Context = context;
Entities = Context.Set<TEntity>();
}
public void Add(TEntity entity)
{
Entities.Add(entity);
}
}
现在你可以明白为什么我们需要模拟DbContext 的Set 方法了 , 这个方法返回一个非通用的DbSet 实例,使用它我们可以访问上下文中给定类型的实体。然后我们在方法中使用这个实例来添加另一个对象。
4.2 删除方法
这个方法的实现与添加方法的实现类似。让我们看看测试是什么样子的:
[Fact]
public void Remove_TestClassObjectPassed_ProperMethodCalled()
{
// Arrange
var testObject = new TestClass();
var context = new Mock<DbContext>();
var dbSetMock = new Mock<DbSet<TestClass>>();
context.Setup(x => x.Set<TestClass>()).Returns(dbSetMock.Object);
dbSetMock.Setup(x => x.Remove(It.IsAny<TestClass>())).Returns(testObject);
// Act
var repository = new Repository<TestClass>(context.Object);
repository.Remove(testObject);
//Assert
context.Verify(x => x.Set<TestClass>());
dbSetMock.Verify(x => x.Remove(It.Is<TestClass>(y => y == testObject)));
}

几乎和添加方法一样,我们只需要设置DbSet的Remove 方法 。 下面是我们添加Remove方法后Repository类的实现的样子。
public class Repository<TEntity> : IReporitory<TEntity> where TEntity : class
{
protected readonly DbContext Context;
protected readonly DbSet<TEntity> Entities;
public Repository(DbContext context)
{
Context = context;
Entities = Context.Set<TEntity>();
}
public void Add(TEntity entity)
{
Entities.Add(entity);
}
public void Remove(TEntity entity)
{
Entities.Remove(entity);
}
}
现在,由于在实现Add 方法的过程中,我们正确地初始化了DbContext 和DbSet,所以我们很容易添加其他方法。
4.3 Get方法
在实现Get 方法的过程中,我们也遵循同样的原则。对这个方法的测试是这样的:
[Fact]
public void Get_TestClassObjectPassed_ProperMethodCalled()
{
// Arrange
var testObject = new TestClass();
var context = new Mock<DbContext>();
var dbSetMock = new Mock<DbSet<TestClass>>();
context.Setup(x => x.Set<TestClass>()).Returns(dbSetMock.Object);
dbSetMock.Setup(x => x.Find(It.IsAny<int>())).Returns(testObject);
// Act
var repository = new Repository<TestClass>(context.Object);
repository.Get(1);
// Assert
context.Verify(x => x.Set<TestClass>());
dbSetMock.Verify(x => x.Find(It.IsAny<int>()));
}

添加了Get方法后的存储库类看起来像这样:
public class Repository<TEntity> : IReporitory<TEntity> where TEntity : class
{
protected readonly DbContext Context;
protected readonly DbSet<TEntity> Entities;
public Repository(DbContext context)
{
Context = context;
Entities = Context.Set<TEntity>();
}
public void Add(TEntity entity)
{
Entities.Add(entity);
}
public void Remove(TEntity entity)
{
Entities.Remove(entity);
}
public TEntity Get(int id)
{
return Entities.Find(id);
}
}
4.4 GetAll方法
对于GetAll方法,我们需要对其进行一些改变。这个方法需要返回对象的列表。这意味着我们需要创建一个TestClass 对象的列表,并通过DbSet 返回 。 实际上,这意味着我们需要模拟DbSet 的一部分,在测试中实现IQueryableinterface。下面是它是如何完成的。
[Fact]
public void GetAll_TestClassObjectPassed_ProperMethodCalled()
{
// Arrange
var testObject = new TestClass() { Id = 1 };
var testList = new List<TestClass>() { testObject };
var dbSetMock = new Mock<DbSet<TestClass>>();
dbSetMock.As<IQueryable<TestClass>>().Setup(x => x.Provider).Returns(testList.AsQueryable().Provider);
dbSetMock.As<IQueryable<TestClass>>().Setup(x => x.Expression).Returns(testList.AsQueryable().Expression);
dbSetMock.As<IQueryable<TestClass>>().Setup(x => x.ElementType).Returns(testList.AsQueryable().ElementType);
dbSetMock.As<IQueryable<TestClass>>().Setup(x => x.GetEnumerator()).Returns(testList.AsQueryable().GetEnumerator());
var context = new Mock<DbContext>();
context.Setup(x => x.Set<TestClass>()).Returns(dbSetMock.Object);
// Act
var repository = new Repository<TestClass>(context.Object);
var result = repository.GetAll();
// Assert
Assert.Equal(testList, result.ToList());
}

我们需要用创建的测试列表中的属性来模拟Provider、Expression、ElementType 和 GetEnumerator() 。 事实证明,为这个方法写一个测试比写实现本身更有挑战性。下面是我们的Repository 类扩展了GetAll方法。
public class Repository<TEntity> : IReporitory<TEntity> where TEntity : class
{
protected readonly DbContext Context;
protected readonly DbSet<TEntity> Entities;
public Repository(DbContext context)
{
Context = context;
Entities = Context.Set<TEntity>();
}
public void Add(TEntity entity)
{
Entities.Add(entity);
}
public void Remove(TEntity entity)
{
Entities.Remove(entity);
}
public TEntity Get(int id)
{
return Entities.Find(id);
}
public IEnumerable<TEntity> GetAll()
{
return Entities.ToList();
}
}
4.5 查找方法
在前面的例子中学习了如何模拟IQueryable之后,为Find 方法编写测试就容易多了。我们遵循我们用于GetAll方法的相同原则。测试看起来像这样:
[Fact]
public void Find_TestClassObjectPassed_ProperMethodCalled()
{
var testObject = new TestClass(){Id = 1};
var testList = new List<TestClass>() {testObject};
var dbSetMock = new Mock<DbSet<TestClass>>();
dbSetMock.As<IQueryable<TestClass>>().Setup(x => x.Provider).Returns(testList.AsQueryable().Provider);
dbSetMock.As<IQueryable<TestClass>>().Setup(x => x.Expression).Returns(testList.AsQueryable().Expression);
dbSetMock.As<IQueryable<TestClass>>().Setup(x => x.ElementType).Returns(testList.AsQueryable().ElementType);
dbSetMock.As<IQueryable<TestClass>>().Setup(x => x.GetEnumerator()).Returns(testList.AsQueryable().GetEnumerator());
var context = new Mock<DbContext>();
context.Setup(x => x.Set<TestClass>()).Returns(dbSetMock.Object);
var repository = new Repository<TestClass>(context.Object);
var result = repository.Find(x => x.Id == 1);
Assert.Equal(testList, result.ToList());
}
最后,Repository类的完整实现看起来是这样的:
public class Repository<TEntity> : IReporitory<TEntity> where TEntity : class
{
protected readonly DbContext Context;
protected readonly DbSet<TEntity> Entities;
public Repository(DbContext context)
{
Context = context;
Entities = Context.Set<TEntity>();
}
public void Add(TEntity entity)
{
Entities.Add(entity);
}
public void Remove(TEntity entity)
{
Entities.Remove(entity);
}
public TEntity Get(int id)
{
return Entities.Find(id);
}
public IEnumerable<TEntity> GetAll()
{
return Entities.ToList();
}
public IEnumerable<TEntity> Find(Expression<Func<TEntity, bool>> predicate)
{
return Entities.Where(predicate);
}
}
结论
很多人认为,如果我们要使用DbContext和DbSet,就不需要实现Repository Pattern和Unit of Work。如果你问我是不是这样,我会说,这取决于问题的类型。在这篇文章中,你有机会看到如何使用Entity Framework构建和测试一个通用的Repository。如果你选择使用DbContext和DbSet,本文中仍有一些有用的提示,比如如何模拟这些类。 谢谢你的阅读!