ASP.NET MVC3 和实体框架代码优先架构
Posted
技术标签:
【中文标题】ASP.NET MVC3 和实体框架代码优先架构【英文标题】:ASP.NET MVC3 and Entity Framework Code first architecture 【发布时间】:2011-08-02 08:14:59 【问题描述】:My previous question 让我重新思考了层、存储库、依赖注入和诸如此类的架构的东西。
我的架构现在看起来像这样: 我首先使用 EF 代码,所以我只创建了 POCO 类和上下文。这会创建数据库和模型。 更高级别是业务层类(提供者)。我为每个域使用不同的提供程序...例如 MemberProvider、RoleProvider、TaskProvider 等,并且我正在每个提供程序中创建我的 DbContext 的新实例。 然后我在我的控制器中实例化这些提供程序,获取数据并将它们发送到视图。
我最初的架构包括存储库,我将其删除是因为我被告知它只会增加复杂性,所以我为什么不只使用 EF。我想这样做.. 直接从控制器使用 EF,但我必须编写测试,而且它与真实数据库有点复杂。我不得不以某种方式伪造 - 模拟数据。所以我为每个提供者制作了一个接口,并在列表中制作了带有硬编码数据的假提供者。有了这个,我又回到了一些我不确定如何正确进行的地方。
这些事情很快就开始变得过于复杂......许多方法和“模式”......它会产生太多的噪音和无用的代码。
是否有任何简单且可测试的架构用于使用实体框架创建 ASP.NET MVC3 应用程序?
【问题讨论】:
您说存储库为您的应用程序增加了“复杂性”,但我想说它们是使测试更容易的初始“开销”。模拟几个存储库比模拟整个数据上下文更容易。 是的,但在我目前的情况下,我不希望初始开销。我想快速推进申请。我已经浪费了太多时间而没有任何实际进展。添加存储库会带来诸如 IoC、DI 等之类的东西。在我第一次查看之前,我将不得不编写无数的测试。我知道这可能是正确的解决方案,但我不是在寻找“正确的解决方案”。我正在寻找简单(但仍可测试)的解决方案。 【参考方案1】:如果您想同时使用 TDD(或任何其他具有高测试覆盖率的测试方法)和 EF,您必须编写集成或端到端测试。这里的问题是,任何模拟上下文或存储库的方法都只会创建可以测试您的上层逻辑(使用这些模拟)而不是您的应用程序的测试。
简单示例:
让我们定义通用存储库:
public interface IGenericRepository<TEntity>
IQueryable<TEntity> GetQuery();
...
让我们写一些业务方法:
public IEnumerable<MyEntity> DoSomethingImportant()
var data = MyEntityRepo.GetQuery().Select((e, i) => e);
...
现在,如果您模拟存储库,您将使用 Linq-To-Objects 并且您将获得绿色测试,但如果您使用 Linq-To-Entities 运行应用程序,您将收到异常,因为不支持使用索引选择重载L2E。
这是一个简单的例子,但是在查询中使用方法和其他常见错误也会发生同样的情况。此外,这也会影响通常在存储库中公开的添加、更新、删除等方法。如果您不编写将精确模拟 EF 上下文行为和引用完整性的模拟,您将不会测试您的实现。
故事的另一部分是延迟加载问题,通过针对模拟的单元测试也很难检测到。
因此,您还应该引入集成或端到端测试,这些测试将适用于使用真实 EF 上下文和 L2E 的真实数据库。顺便提一句。需要使用端到端测试才能正确使用 TDD。对于在 ASP.NET MVC 中编写端到端测试,您可以 WatiN 并且可能还 SpecFlow 用于 BDD,但这确实会增加很多工作,但您将真正测试您的应用程序。如果您想了解更多关于 TDD 的信息,我推荐 this book(唯一的缺点是示例是用 Java 编写的)。
如果您不使用通用存储库并且将查询隐藏在某个不会公开IQueryable
但直接返回数据的类中,则集成测试是有意义的。
例子:
public interface IMyEntityRepository
MyEntity GetById(int id);
MyEntity GetByName(string name);
现在您可以编写集成测试来测试此存储库的实现,因为查询隐藏在此类中并且不会暴露给上层。但是这种类型的存储库在某种程度上被认为是与存储过程一起使用的旧实现。使用此实现,您将失去很多 ORM 功能,或者您将不得不做很多额外的工作 - 例如添加 specification pattern 以便能够在上层定义查询。
在 ASP.NET MVC 中,您可以使用控制器级别的集成测试部分替换端到端测试。
根据评论编辑:
我并不是说您需要单元测试、集成测试和端到端测试。我说制作经过测试的应用程序需要更多的努力。所需测试的数量和类型取决于您的应用程序的复杂性、应用程序的预期未来、您的技能和其他团队成员的技能。
完全无需测试就可以创建小型直截了当的项目(好吧,这不是一个好主意,但我们都这样做了,最后它奏效了)但是一旦项目通过了一些门槛,您就会发现引入新功能或维护该项目非常困难,因为您永远无法确定它是否会破坏已经起作用的东西——这就是所谓的回归。对回归的最佳防御是一套好的自动化测试。
单元测试帮助您测试方法。理想情况下,此类测试应涵盖方法中的所有执行路径。这些测试应该非常简短且易于编写 - 复杂的部分可以是设置依赖项(模拟,faktes,存根)。 集成测试可帮助您测试跨多个层的功能,通常跨多个进程(应用程序、数据库)。您不需要为所有事情都拥有它们,更多的是经验来选择它们有帮助的地方。 端到端测试类似于用例/用户故事/功能的验证。它们应该涵盖整个需求流程。不需要多次测试一个功能 - 如果您知道该功能是在端到端测试中测试的,那么您就不需要为相同的代码编写集成测试。此外,如果您知道该方法只有集成测试涵盖的单一执行路径,则无需为其编写单元测试。这在 TDD 方法中效果更好,您从大型测试(端到端或集成)开始并深入到单元测试。
根据您的开发方法,您不必从一开始就使用多种类型的测试,但您可以稍后再介绍它们,因为您的应用程序会变得更加复杂。例外情况是 TDD/BDD,您应该在编写单行其他代码之前至少开始使用端到端测试和单元测试。
所以你问错问题了。问题不是什么更简单?问题是最终什么会对您有所帮助,什么复杂性适合您的应用程序?如果您想轻松地对应用程序和业务逻辑进行单元测试,您应该将 EF 代码包装到其他一些可以模拟的类中。但同时您必须引入其他类型的测试以确保 EF 代码正常工作。
我不能告诉你哪种方法适合你的环境/项目/团队/等等。但我可以解释一下我过去项目的例子:
我和两个同事一起在这个项目上工作了大约 5-6 个月。该项目基于 ASP.NET MVC 2 + jQuery + EFv4 并以增量和迭代的方式开发。它有很多复杂的业务逻辑和很多复杂的数据库查询。我们从通用存储库和高代码覆盖率开始,通过单元测试+集成测试来验证映射(用于插入、删除、更新和选择实体的简单测试)。几个月后,我们发现我们的方法不起作用。我们有超过 1200 个单元测试,代码覆盖率大约 60%(这不是很好)和很多回归问题。更改 EF 模型中的任何内容都可能在数周未触及的部分中引入意外问题。我们发现我们缺少应用程序逻辑的集成测试或端到端测试。在另一个项目上工作的并行团队也得出了相同的结论,并且将使用集成测试作为新项目的建议。
【讨论】:
Hmm.. 所以如果我理解正确,你是说使用模拟是为了对业务逻辑进行单元测试,我需要使用真实的 ef 上下文以及端到端测试进行集成测试(我将其理解为功能/用户测试......使用 Watin 等工具)。但我不明白建筑的意义。我很高兴你给了我关于问题所在的提示,但我在这个领域没有经验,所以我不知道什么是更好的解决方案。这就是我在这里寻找的。我说的是“更容易”或“简单”的意思。 谢谢。我非常感谢您的回复和解释。我相信我现在正在使用“将 ef 代码包装到其他类”(我的 Provider 类)。只是为了在我的问题中添加一些上下文:我正在为用户创建围绕任务(在项目上下文中)管理构建的简单应用程序(+ 专家系统,它不会改变架构,因为它只是消耗数据并提供简单的输出)。这是我自己的项目(没有其他人参与),我认为它不会有任何美好的未来。 @Ladislav:静态代码分析工具是否有可能捕捉到您所描述的问题(Linq to Entities 不支持的 Linq 方法)?如果是这样,那么您可以消除一类错误,而无需为它们编写单元测试,并且更有信心在您编写的测试中模拟将“真正起作用”。它可能无法解决参照完整性问题,但正如您所说,可以通过集成测试(而不是 E2E)来解决这些问题。 一如既往的出色回答。谢谢拉迪斯拉夫。 可测试性正是我不从存储库中公开 IQueryable 的原因。我确实获得了更大的存储库,但使用的方法具有明确的职责并且更容易模拟。【参考方案2】:使用存储库模式会增加复杂性吗?在你的情况下,我不这么认为。它使 TDD 更容易,您的代码更易于管理。尝试使用通用存储库模式来实现更多分离和更清晰的代码。
如果您想了解更多关于实体框架中的 TDD 和设计模式,请查看:http://msdn.microsoft.com/en-us/ff714955.aspx
但是,您似乎正在寻找一种模拟测试实体框架的方法。一种解决方案是使用虚拟种子方法来生成数据库初始化数据。看看 Seed 部分:http://blogs.msdn.com/b/adonet/archive/2010/09/02/ef-feature-ctp4-dbcontext-and-databases.aspx
您还可以使用一些模拟框架。我知道的最著名的是:
Rhino Mocks Moq Typemock(商业)要查看更完整的 .NET 模拟框架列表,请查看:https://***.com/questions/37359/what-c-mocking-framework-to-use
另一种方法是使用内存数据库提供程序,例如SQLite。在Is there an in-memory provider for Entity Framework? 了解更多信息
最后,这里有一些关于实体框架单元测试的好链接(有些链接是指 Entity Framework 4.0。但你会明白的。):
http://social.msdn.microsoft.com/Forums/en/adodotnetentityframework/thread/678b5871-bec5-4640-a024-71bd4d5c77ff
http://mosesofegypt.net/post/Introducing-Entity-Framework-Unit-Testing-with-TypeMock-Isolator.aspx
What is the way to go to fake my database layer in a unit test?
【讨论】:
感谢您的意见,这里有一些有趣的链接。但我的问题实际上与测试和模拟无关。更多的是寻找简单、快速且没有开销的简单架构。如果您的方法确实返回字符串值,您可以使用它来快速轻松地测试和开发应用程序,而无需准备 XYZ 行代码来查看。 抱歉有点讽刺。 @dampe:好吧,与其编写接口并手动模拟数据,我建议了一些额外的解决方案,它们可以为你做很多工作。再一次,在这些情况下,我会使用通用存储库模式,并且从未觉得它增加了我的解决方案的复杂性。希望对您有所帮助。 关于通用存储库的建议,请参阅本教程:asp.net/entity-framework/tutorials/…【参考方案3】:我所做的是我使用一个简单的 ISession 和 EFSession 对象,女巫很容易在我的控制器中模拟,易于使用 Linq 访问和强类型。使用 Ninject 注入 DI。
public interface ISession : IDisposable
void CommitChanges();
void Delete<T>(Expression<Func<T, bool>> expression) where T : class, new();
void Delete<T>(T item) where T : class, new();
void DeleteAll<T>() where T : class, new();
T Single<T>(Expression<Func<T, bool>> expression) where T : class, new();
IQueryable<T> All<T>() where T : class, new();
void Add<T>(T item) where T : class, new();
void Add<T>(IEnumerable<T> items) where T : class, new();
void Update<T>(T item) where T : class, new();
public class EFSession : ISession
DbContext _context;
public EFSession(DbContext context)
_context = context;
public void CommitChanges()
_context.SaveChanges();
public void Delete<T>(System.Linq.Expressions.Expression<Func<T, bool>> expression) where T : class, new()
var query = All<T>().Where(expression);
foreach (var item in query)
Delete(item);
public void Delete<T>(T item) where T : class, new()
_context.Set<T>().Remove(item);
public void DeleteAll<T>() where T : class, new()
var query = All<T>();
foreach (var item in query)
Delete(item);
public void Dispose()
_context.Dispose();
public T Single<T>(System.Linq.Expressions.Expression<Func<T, bool>> expression) where T : class, new()
return All<T>().FirstOrDefault(expression);
public IQueryable<T> All<T>() where T : class, new()
return _context.Set<T>().AsQueryable<T>();
public void Add<T>(T item) where T : class, new()
_context.Set<T>().Add(item);
public void Add<T>(IEnumerable<T> items) where T : class, new()
foreach (var item in items)
Add(item);
/// <summary>
/// Do not use this since we use EF4, just call CommitChanges() it does not do anything
/// </summary>
/// <typeparam name="T"></typeparam>
/// <param name="item"></param>
public void Update<T>(T item) where T : class, new()
//nothing needed here
如果我想从 EF4 切换到 MongoDB,我只需要创建一个实现 ISession 的 MongoSession...
【讨论】:
谢谢。我相信我正在做类似的事情......除了通用和 Ninject 部分:)【参考方案4】:在决定 MVC 应用程序的总体设计时,我遇到了同样的问题。 This Shiju Varghese 的 CodePlex 项目提供了很多帮助。它是在 ASP.net MVC3、EF CodeFirst 中完成的,并且还利用了服务层和存储库层。依赖注入是使用 Unity 完成的。这很简单,也很容易理解。它还得到了 4 篇非常好的博客文章的支持。它值得一试。而且,不要放弃存储库..还没有。
【讨论】:
谢谢,我查看了该解决方案代码,它几乎完全符合我不想要的...所有存储库、IoC、工厂等。当有人说这不是我想象的“简单的架构”:) 我可以建议的最简单的设计(尽管不推荐)是直接从您的控制器创建 EF Context 对象,但正如您的问题所示,您已经尝试过并且已经遇到问题了..以上是关于ASP.NET MVC3 和实体框架代码优先架构的主要内容,如果未能解决你的问题,请参考以下文章
使用带有实体框架代码优先和 ASP.NET MVC 3 和 mvc miniprofiler 的 SQL Server CE 时出现问题