dav*_*avy 49 entity-framework unit-of-work repository-pattern
我即将实现一个带有存储库和工作单元的Entity Framework 6设计.
有太多的文章,我不知道最好的建议是什么:例如我真的喜欢这里实现的模式:由于这里的文章建议的原因
但是,Tom Dykstra (Senior Programming Writer on Microsoft's Web Platform & Tools Content Team)建议它应该在另一篇文章中完成:这里
我订阅了Pluralsight,并且它在每次在课程中使用时都以稍微不同的方式实现,因此选择设计很困难.
有些人似乎认为工作单元已经DbContext在这篇文章中实现,所以我们根本不需要实现它.
我知道之前已经提出过这类问题,这可能是主观的,但我的问题是直接的:
我喜欢第一篇(Code Fizzle)文章中的方法,并想知道它是否可能更易于维护,并且可以像其他方法一样容易测试并且可以安全地继续使用?
任何其他观点都非常受欢迎.
dan*_*wig 46
@Chris Hardie是正确的,EF实现UoW开箱即用.然而,许多人忽略了EF也开箱即用的通用存储库模式这一事实:
var repos1 = _dbContext.Set<Widget1>();
var repos2 = _dbContext.Set<Widget2>();
var reposN = _dbContext.Set<WidgetN>();
Run Code Online (Sandbox Code Playgroud)
...这是一个非常好的通用存储库实现,内置于工具本身.
当DbContext为您提供所需的一切时,为什么要经历创建大量其他接口和属性的麻烦?如果要在应用程序级接口后面抽象DbContext,并且想要应用命令查询隔离,则可以执行以下简单操作:
public interface IReadEntities
{
IQueryable<TEntity> Query<TEntity>();
}
public interface IWriteEntities : IReadEntities, IUnitOfWork
{
IQueryable<TEntity> Load<TEntity>();
void Create<TEntity>(TEntity entity);
void Update<TEntity>(TEntity entity);
void Delete<TEntity>(TEntity entity);
}
public interface IUnitOfWork
{
int SaveChanges();
}
Run Code Online (Sandbox Code Playgroud)
您可以将这3个接口用于所有实体访问,而不必担心将3个或更多不同的存储库注入到与3个或更多实体集一起使用的业务代码中.当然,您仍然会使用IoC来确保每个Web请求只有1个DbContext实例,但是所有3个接口都由同一个类实现,这使得它更容易.
public class MyDbContext : DbContext, IWriteEntities
{
public IQueryable<TEntity> Query<TEntity>()
{
return Set<TEntity>().AsNoTracking(); // detach results from context
}
public IQueryable<TEntity> Load<TEntity>()
{
return Set<TEntity>();
}
public void Create<TEntity>(TEntity entity)
{
if (Entry(entity).State == EntityState.Detached)
Set<TEntity>().Add(entity);
}
...etc
}
Run Code Online (Sandbox Code Playgroud)
您现在只需要在依赖项中注入一个接口,无论它需要使用多少个不同的实体:
// NOTE: In reality I would never inject IWriteEntities into an MVC Controller.
// Instead I would inject my CQRS business layer, which consumes IWriteEntities.
// See @MikeSW's answer for more info as to why you shouldn't consume a
// generic repository like this directly by your web application layer.
// See http://www.cuttingedge.it/blogs/steven/pivot/entry.php?id=91 and
// http://www.cuttingedge.it/blogs/steven/pivot/entry.php?id=92 for more info
// on what a CQRS business layer that consumes IWriteEntities / IReadEntities
// (and is consumed by an MVC Controller) might look like.
public class RecipeController : Controller
{
private readonly IWriteEntities _entities;
//Using Dependency Injection
public RecipeController(IWriteEntities entities)
{
_entities = entities;
}
[HttpPost]
public ActionResult Create(CreateEditRecipeViewModel model)
{
Mapper.CreateMap<CreateEditRecipeViewModel, Recipe>()
.ForMember(r => r.IngredientAmounts, opt => opt.Ignore());
Recipe recipe = Mapper.Map<CreateEditRecipeViewModel, Recipe>(model);
_entities.Create(recipe);
foreach(Tag t in model.Tags) {
_entities.Create(tag);
}
_entities.SaveChanges();
return RedirectToAction("CreateRecipeSuccess");
}
}
Run Code Online (Sandbox Code Playgroud)
我最喜欢这个设计的一个方面是它最大限度地减少了对消费者的实体存储依赖性.在这个例子中,RecipeController它是消费者,但在实际的应用程序中,消费者将是一个命令处理程序.(对于查询处理程序,您通常IReadEntities只会因为您只想返回数据而消耗,而不是改变任何状态.)但是对于此示例,我们只是RecipeController用作消费者来检查依赖性含义:
假设您为上述操作编写了一组单元测试.在每个单元测试中,您新建Controller,将模拟传递给构造函数.然后,假设您的客户决定在创建新配方时允许人们创建新的Cookbook或添加到现有的Cookbook.
使用每个实体的存储库或每个聚合的存储库接口模式,您必须将新的存储库实例IRepository<Cookbook>注入到控制器构造函数中(或者使用@Chris Hardie的答案,编写代码以将另一个存储库附加到UoW实例).这将立即使所有其他单元测试中断,您将不得不返回修改所有这些中的构造代码,传递另一个模拟实例,并扩展您的依赖项数组.但是,如上所述,所有其他单元测试仍然至少可以编译.您所要做的就是编写额外的测试以涵盖新的烹饪书功能.
Mik*_*eSW 42
我(不)抱歉说代码失败,Dyksta的文章和以前的答案是错误的.简单来说,他们使用EF实体作为域(业务)对象,这是一个很大的WTF.
更新:对于技术性较低的解释(用简单的话说)阅读 Repository Pattern for Dummies
简而言之,任何存储库接口都不应与任何持久性(ORM)细节相关联.repo界面仅处理对应用程序的其余部分有意义的对象(域,可能是UI,如演示文稿).很多人(MS领导包装,我怀疑的意图)犯了错误,认为他们可以重用他们的EF实体或者可以作为业务对象.
虽然它可能发生,但这种情况非常罕见.实际上,在数据库规则之后,你会设计很多域对象,即糟糕的建模.存储库的目的是将应用程序的其余部分(主要是业务层)与其持久性形式分离.
当你的repo处理EF实体(持久性细节)或者它的方法返回IQueryable时,你如何将它解耦?这是一个带有错误语义的泄漏抽象(IQueryable允许你构建一个查询,因此暗示你需要知道持久性细节因此否定存储库的目的和功能)?
绝对对象永远不应该知道持久性,EF,连接等.它不应该知道你正在使用什么数据库引擎或者你是否正在使用它.与应用程序的其余部分相同,如果您希望它与持久性详细信息分离.
存储库接口只知道更高层知道的内容.这意味着,通用域存储库接口看起来像这样
public interface IStore<TDomainObject> //where TDomainObject != Ef (ORM) entity
{
void Save(TDomainObject entity);
TDomainObject Get(Guid id);
void Delete(Guid id);
}
Run Code Online (Sandbox Code Playgroud)
该实施将驻留在DAL和将使用EF与DB工作.但是实现看起来像这样
public class UsersRepository:IStore<User>
{
public UsersRepository(DbContext db) {}
public void Save(User entity)
{
//map entity to one or more ORM entities
//use EF to save it
}
//.. other methods implementation ...
}
Run Code Online (Sandbox Code Playgroud)
您实际上没有具体的通用存储库.具体通用存储库的唯一用法是当任何域对象以序列化形式存储在类似键的值中时.ORM不是这种情况.
查询怎么样?
public interface IQueryUsers
{
PagedResult<UserData> GetAll(int skip, int take);
//or
PagedResult<UserData> Get(CriteriaObject criteria,int skip, int take);
}
Run Code Online (Sandbox Code Playgroud)
该的UserData是查询上下文用法读/视图模型的拟合.
如果您不介意DAL知道视图模型,那么您可以直接使用EF在查询处理程序中进行查询,在这种情况下,您将不需要任何查询存储库.
结论
请注意,以错误的方式使用存储库会使其使用无效,您的应用程序仍将与持久性(ORM)紧密耦合.
如果你认为ORM可以神奇地存储你的域对象,那就不是了.ORM的目的是在关系表之上模拟OOP存储.它与持久性有关,与域无关,因此不要使用ORM外部持久性.
DbContext确实是用工作单元模式构建的。它允许它的所有实体在我们使用它们时共享相同的上下文。这个实现是内部的DbContext。
但是,应该注意的是,如果您实例化两个DbContext对象,则它们都不会看到各自跟踪的另一个实体。它们彼此绝缘,这可能是有问题的。
当我构建一个 MVC 应用程序时,我想确保在请求过程中,我所有的数据访问代码都在一个DbContext. 为了实现这一点,我将工作单元作为DbContext.
这是我正在构建的烧烤食谱应用程序中的工作单元对象:
public class UnitOfWork : IUnitOfWork
{
private BarbecurianContext _context = new BarbecurianContext();
private IRepository<Recipe> _recipeRepository;
private IRepository<Category> _categoryRepository;
private IRepository<Tag> _tagRepository;
public IRepository<Recipe> RecipeRepository
{
get
{
if (_recipeRepository == null)
{
_recipeRepository = new RecipeRepository(_context);
}
return _recipeRepository;
}
}
public void Save()
{
_context.SaveChanges();
}
**SNIP**
Run Code Online (Sandbox Code Playgroud)
我将所有注入了相同 的存储库附加DbContext到我的工作单元对象。只要从工作单元对象请求任何存储库,我们就可以确保我们所有的数据访问代码都将使用相同的DbContext- 很棒的酱汁进行管理!
如果我要在 MVC 应用程序中使用它,我将通过在控制器中实例化它并在整个操作中使用它来确保在整个请求中使用工作单元:
public class RecipeController : Controller
{
private IUnitOfWork _unitOfWork;
private IRepository<Recipe> _recipeService;
private IRepository<Category> _categoryService;
private IRepository<Tag> _tagService;
//Using Dependency Injection
public RecipeController(IUnitOfWork unitOfWork)
{
_unitOfWork = unitOfWork;
_categoryRepository = _unitOfWork.CategoryRepository;
_recipeRepository = _unitOfWork.RecipeRepository;
_tagRepository = _unitOfWork.TagRepository;
}
Run Code Online (Sandbox Code Playgroud)
现在在我们的行动中,我们可以确信我们所有的数据访问代码都将使用相同的DbContext:
[HttpPost]
public ActionResult Create(CreateEditRecipeViewModel model)
{
Mapper.CreateMap<CreateEditRecipeViewModel, Recipe>().ForMember(r => r.IngredientAmounts, opt => opt.Ignore());
Recipe recipe = Mapper.Map<CreateEditRecipeViewModel, Recipe>(model);
_recipeRepository.Create(recipe);
foreach(Tag t in model.Tags){
_tagRepository.Create(tag); //I'm using the same DbContext as the recipe repo!
}
_unitOfWork.Save();
Run Code Online (Sandbox Code Playgroud)
| 归档时间: |
|
| 查看次数: |
35286 次 |
| 最近记录: |