1

有很多类似的问题,这在S/O,但是这其中有一个具体的问题,我还没有看到解决:Repository模式+扶养注射液+的UnitOfWork + EF

这是一个MVC应用程序。我正在使用Dependancy注射(简单注射器,虽然我认为它是无关紧要的),它会注入Per Web Request。

我遇到的主要问题是,因为我的UoW是按每个Web请求注入的,所以我无法在添加数据时失败并继续添加数据,这是我最近需要的。

下面的代码说明:

数据层

public abstract RepositoryBase<TEntity> 
{ 
    private readonly MyDbContext _context; 

    //fields set from contrstuctor injection 
    protected RepositoryBase(MyDbContext context) 
    { 
     _context = context; 
    } 

    public IList<TEntity> GetAll() 
    { 
     return _context.Set<TEntity>().ToList(); 
    } 

    public TEntity GetById(Int32 id) 
    { 
     _context.Set<TEntity>().Find(id); 
    } 

    public TEntity Insert(TEntity entity) 
    { 
     _context.Set<TEntity>().Add(entity); 
    } 
} 

public UserRepository : RepositoryBase<User>, IUserRepository 
{ 
    //constructor injection 
    public UserRepository(MyDbContext c) : base(c) {} 

    public Update(Int32 id, String name, String email, Int32 ageYears) 
    { 
     var entity = GetById(id); 
     entity.Name = name; 
     entity.Email = email; 
     entity.Age = ageYears; 
    } 

    public UpdateName(Int32 id, String name) 
    { 
     var entity = GetById(id); 
     entity.Name = name; 
    } 
} 

public AddressRepository : RepositoryBase<Address>, IAddressRepository 
{ 
    //constructor injection 
    public AddressRepository(MyDbContext c) : base(c) {} 

    public Update(Int32 id, String street, String city) 
    { 
     var entity = GetById(id); 
     entity.Street = street; 
     entity.City = city; 
    } 

    public Address GetForUser(Int32 userId) 
    { 
     return _context.Adresses.FirstOrDefault(x => x.UserId = userId); 
    } 
} 

public DocumentRepository : RepositoryBase<Document>, IDocumentRepository 
{ 
    //constructor injection 
    public DocumentRepository(MyDbContext c) : base(c) {} 

    public Update(Int32 id, String newTitle, String newContent) 
    { 
     var entity.GetById(id); 
     entity.Title = newTitle; 
     entity.Content = newContent; 
    } 

    public IList<Document> GetForUser(Int32 userId) 
    { 
     return _context.Documents.Where(x => x.UserId == userId).ToList(); 
    } 
} 

public UnitOfWork : IUnitOfWork 
{ 
    private readonly MyDbContext _context; 

    //fields set from contrstuctor injection 
    public UnitOfWork(MyDbContext context) 
    { 
     _context = context; 
    } 

    public Int32 Save() 
    { 
     return _context.SaveChanges(); 
    } 

    public ITransaction StartTransaction() 
    { 
     return new Transaction(_context.Database.BeginTransaction(IsolationLevel.ReadUncommitted)); 
    } 
} 

public Transaction : ITransaction 
{ 
    private readonly DbContextTransaction _transaction; 

    public Transaction(DbContextTransaction t) 
    { 
     _transaction = t; 
     State = TransactionState.Open; 
    } 

    public void Dispose() 
    { 
     if (_transaction != null) 
     { 
      if (State == TransactionState.Open) 
      { 
       Rollback(); 
      } 
      _transaction.Dispose(); 
     } 
    } 

    public TransactionState State { get; private set; } 

    public void Commit() 
    { 
     try 
     { 
      _transaction.Commit(); 
      State = TransactionState.Committed; 
     } 
     catch (Exception) 
     { 
      State = TransactionState.FailedCommitRolledback; 
      throw; 
     } 
    } 

    public void Rollback() 
    { 
     if (_transaction.UnderlyingTransaction.Connection != null) 
     { 
      _transaction.Rollback(); 
     } 
     State = TransactionState.Rolledback; 
    } 
} 

服务层

public DocumentService : IDocumentService 
{ 
    //fields set from contrstuctor injection 
    private readonly IDocumentRepository _docRepo; 
    private readonly IUnitOfWork _unitOfWork; 

    public void AuthorNameChangeAddendum(Int32 userId, String newAuthorName) 
    { 
     //this works ok if error thrown 
     foreach(var doc in _docRepo.GetForUser(userId)) 
     { 
      var addendum = $"\nAddendum: As of {DateTime.Now} the author will be known as {newAuthorName}."; 
      _docRepo.Update(documentId, doc.Title + "-Processed", doc.Content + addendum); 
     } 
     _unitOfWork.Save(); 
    } 
} 

public UserService 
{ 
    //fields set from contrstuctor injection 
    private readonly IUserRepository _userRepo; 
    private readonly IAddressRepository _addressRepo; 
    private readonly IUnitOfWork _unitOfWork; 
    private readonly IDocumentService _documentService; 

    public void ChangeUser(Int32 userId, String newName, String newStreet, String newCity) 
    { 
     //this works ok if error thrown 
     _userRepo.UpdateName(userId, newName); 

     var address = _addressRepo.GetForUser(userId); 
     _addressRepo.Update(address.AddressId, newStreet, newCity); 

     _unitOfWork.Save(); 
    } 

    public void ChangeUserAndProcessDocs(Int32 userId, String newName, Int32) 
    { 
     //this is ok because of transaction 
     using(var transaction = _unitOfWork.StartTransaction()) 
     { 
      _documentService.AuthorNameChangeAddendum(userId, newName); //this function calls save() on uow 

      //possible exception here could leave docs with an inaccurate addendum, so transaction needed 
      var x = 1/0; 

      _userRepo.UpdateName(userId, newName); 

      _unitOfWork.Save(); 
      transaction.Commit(); 
     } 
    } 

    //THE PROBLEM: 
    public IList<String> AddLastNameToAll(String lastName) 
    { 
     var results = new List<String>(); 
     foreach(var u in _userRepo.GetAll()) 
     { 
      try 
      { 
       var newName = $"{lastName}, {u.Name}"; 
       _userRepo.UpdateName(u.UserId, newName); 
       _unitOfWork.Save(); //throws validation exception 
       results.Add($"Changed name from {u.Name} to {newName}."); 
      } 
      catch(DbValidationException e) 
      { 
       results.Add($"Error adding last name to {u.Name}: {e.Message}"); 
       //but all subsequeqnet name changes will fail because the invalid entity will be stuck in the context 
      } 
     } 
     return results; 
    } 
} 

您可以在该UOW实现处理ChangeUser()的UserService看到,和潜在的问题在ChangeUserAndProcessDocs()是通过使用显式事务来处理的。

但在AddLastNameToAll()问题是,如果我有100个用户更新和第三个失败,因为名称列不足以处理新名称,那么结果3到100都将具有相同的验证消息他们。解决此问题的唯一方法是对for循环的每次传递使用一个新的UnitOf Work(DbContext),这对于我的实现来说并不是真的可行。

我的UoW + Repo实现可以防止EF泄漏到其他层,并确实赋予其他层创建事务的能力。但是总是感到奇怪的是,如果A服务调用B服务,则B服务可以在A准备好之前调用Save()。范围交易解决了这个问题,但仍然有点奇怪。

我想到了废除UoW模式,并且让我所有的存储库操作都立即提交,但是这留下了更新两种不同物质类型并让第二次更新失败的问题,但成功的第一次更新没有意义现在(见ChangeUserAndProcessDocs()就是一个例子。

所以我留下使得上忽略了注入的情况下,并创建它自己的UserRepository UpdateNameImmediately()特殊UpdateName()功能。

public void UpdateNameImmediately(Int32 id, String newName) 
    { 
     using(var mySingleUseContext = new MyDbContext()) 
     { 
      var u = mySingleUseContext.Users.Find(id); 
      u.Name = newName; 
      mySingleUseContext.SaveChanges(); 
     } 
    } 

这种感觉奇怪,因为现在这函数的行为q不同于我所有其他存储库操作,并且不会遵守交易。

有没有UoW + EF + Repository Pattern + DI的实现来解决这个问题?

+1

您是否知道EF默认实现UoW和回购模式? DbContext是一个UoW,DbSet是Repos。我的观点;给EF增加更多抽象是浪费时间。 –

+0

我知道DbContext是一个UoW。为了避免EF渗入服务层,我只对它进行了薄包装。它周围的回购包装可以帮助我通过一些存储库操作来收集所有信息,以及警察坏联接等。如果有更好的方式来完成这些事情,我很乐意听到它。 – sheamus

+0

AddLastNameToAll()中提出的问题实际上发生在几乎所有批量处理的相当专业化的服务中。我倾向于创建一对不使用UoW模式的专门的'ImmediateRepositories'。我认为这可能是最简单的。 – sheamus

回答

0

AddLastNameToAll()立即提交每个更改的解决方案是“立即存储库包装器”。这使我可以重用我现有的代码,并允许我在测试服务时继续轻松地模拟存储库行为。为用户的一个例子立即仓库的包装看起来是这样的:

public interface IUserImmediateRepository 
{ 
    void UpdateName(Int32 id, String newName); 
} 

public class UserImmediateRepository : IUserImmediateRepository 
{ 
    public UserImmediateRepository() 
    { 
    } 

    public void UpdateName(Int32 id, String newName) 
    { 
     using(var singleUseContext = new MyDbContext()) 
     { 
      var repo = new UserRepository(singleUseContext); 
      repo.UpdateName(id, newName); 
      singleUseContext.SaveChanges(); 
     } 
    } 
} 

这工作得很好批量处理的罕见场景,在这里我需要立即提交。

1

工作原理:

public class DbFactory : Disposable, IDbFactory 
    { 
     HomeCinemaContext dbContext; 



public HomeCinemaContext Init() 
    { 
     return dbContext ?? (dbContext = new HomeCinemaContext()); 
    } 

    protected override void DisposeCore() 
    { 
     if (dbContext != null) 
      dbContext.Dispose(); 
    } 
} 

public class UnitOfWork : IUnitOfWork 
    { 
     private readonly IDbFactory dbFactory; 
     private HomeCinemaContext dbContext; 

     public UnitOfWork(IDbFactory dbFactory) 
     { 
      this.dbFactory = dbFactory; 
     } 

     public HomeCinemaContext DbContext 
     { 
      get { return dbContext ?? (dbContext = dbFactory.Init()); } 
     } 

     public void Commit() 
     { 
      DbContext.Commit(); 
     } 
    } 

public class EntityBaseRepository<T> : IEntityBaseRepository<T> 
      where T : class, IEntityBase, new() 
    { 

     private HomeCinemaContext dataContext; 

     #region Properties 
     protected IDbFactory DbFactory 
     { 
      get; 
      private set; 
     } 

     protected HomeCinemaContext DbContext 
     { 
      get { return dataContext ?? (dataContext = DbFactory.Init()); } 
     } 
     public EntityBaseRepository(IDbFactory dbFactory) 
     { 
      DbFactory = dbFactory; 
     } 
     #endregion 
     public virtual IQueryable<T> GetAll() 
     { 
      return DbContext.Set<T>(); 
     } 
     public virtual IQueryable<T> All 
     { 
      get 
      { 
       return GetAll(); 
      } 
     } 
     public virtual IQueryable<T> AllIncluding(params Expression<Func<T, object>>[] includeProperties) 
     { 
      IQueryable<T> query = DbContext.Set<T>(); 
      foreach (var includeProperty in includeProperties) 
      { 
       query = query.Include(includeProperty); 
      } 
      return query; 
     } 
     public T GetSingle(int id) 
     { 
      return GetAll().FirstOrDefault(x => x.ID == id); 
     } 
     public virtual IQueryable<T> FindBy(Expression<Func<T, bool>> predicate) 
     { 
      return DbContext.Set<T>().Where(predicate); 
     } 

     public virtual void Add(T entity) 
     { 
      DbEntityEntry dbEntityEntry = DbContext.Entry<T>(entity); 
      DbContext.Set<T>().Add(entity); 
     } 
     public virtual void Edit(T entity) 
     { 
      DbEntityEntry dbEntityEntry = DbContext.Entry<T>(entity); 
      dbEntityEntry.State = EntityState.Modified; 
     } 
     public virtual void Delete(T entity) 
     { 
      DbEntityEntry dbEntityEntry = DbContext.Entry<T>(entity); 
      dbEntityEntry.State = EntityState.Deleted; 
     } 
    } 

主类是DbFactory,它包含EF分贝范围内只有一个实例。所以,你在不同的仓库中做了什么,应用程序总是使用ONE上下文。

Class EntityBaseRepository也可以在由DbFactory提供的相同db上下文上工作

UnitOfWork仅传递给控制器​​,以便能够使用方法Commit将所有对数据库的更改保存到数据库上,并在db上下文的同一个实例上运行。

您probalby不需要在您的代码中使用的交易。

完全教程是在这里:

https://chsakell.com/2015/08/23/building-single-page-applications-using-web-api-and-angularjs-free-e-book/#architecture \

查找的话: “DbFactory” 或 “的UnitOfWork” 进入细节。

+0

很确定你希望每个**工作单元有一个数据库上下文** _不是一个上下文为整个应用程序。如果连接中断,会发生什么? – MickyD

+0

不是每个应用程序,而是每个用户/请求。它应该由IOC容器完成。 –

+1

这不是你说的_“... DbFactory,它只包含EF db上下文的一个实例...应用程序总是使用ONE上下文”_并且注入的IDbFactory总是要通过DbFactory返回相同的实例.Init'。你需要展示工厂的寿命是如何管理的,然后我可能不会把它称为_factory_。看看https://www.youtube.com/watch?v=rtXpYpZdOzM – MickyD