【问题标题】:Repository Pattern + Dependancy Injection + UnitOfWork + EF存储库模式 + 依赖注入 + 工作单元 + EF
【发布时间】:2017-04-08 06:16:09
【问题描述】:

在 S/O 上有很多与此类似的问题,但这个问题有一个我没有解决的具体问题:

这是一个 MVC 应用程序。我正在使用依赖注入(Simple Injector,虽然我认为它无关紧要),它会注入 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;
    }
}

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

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

我的 UoW+Repo 实现可防止将 EF 泄漏到其他层,并且确实使其他层能够创建事务。但是一直觉得奇怪的是,如果 A Service 调用 B Service,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();
        }
    }

这感觉很奇怪,因为现在这个函数的行为与我所有其他存储库操作完全不同,并且不会服从事务。

是否有解决此问题的 UoW + EF + Repository Pattern + DI 的实现?

【问题讨论】:

  • 你知道EF默认实现了UoW和Repo模式吗? DbContext 是 UoW,DbSet 是 Repos。我的观点;向 EF 添加更多抽象是浪费时间。
  • 我知道 DbContext 是一个 UoW。我只对其进行薄包装以避免将 EF 泄漏到服务层中。围绕它的 repo 包装器可以帮助我通过一些存储库操作来汇集我的所有内容,并监督不良连接等。如果有更好的方法来完成这些事情,我愿意听到它。
  • AddLastNameToAll() 中出现的问题实际上发生在一个非常专业的服务中,该服务几乎执行所有批量处理。我倾向于只创建几个不使用 UoW 模式的专门的“ImmediateRepositories”。我认为这可能是最简单的。

标签: c# entity-framework dependency-injection repository-pattern unit-of-work


【解决方案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 db 上下文实例。所以无论你在不同的存储库中做什么,应用程序总是使用一个上下文。

EntityBaseRepository 类也可以在相同的数据库上下文中工作,由 DbFactory 提供。

UnitOfWork 被传递给控制器​​只是为了能够使用 Commit 方法保存对数据库的所有更改,并在数据库上下文的同一实例上工作。

您可能不需要您在代码中使用的事务。

完整教程在这里:

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

查找词:“DbFactory”或“UnitOfWork”以了解详细信息。

【讨论】:

  • 非常确定您希望每个工作单元有一个数据库上下文,而不是整个应用程序的一个上下文。如果连接中断会怎样?
  • 不是每个应用程序,而是每个用户/请求。应该由 IOC 容器来完成。
  • 这不是你说的 "...DbFactory,它只包含一个 EF db 上下文实例...应用程序总是使用一个上下文" 和注入的@987654324 @ 总是会通过 DbFactory.Init 返回相同的实例。你需要展示工厂的生命周期是如何管理的,然后我可能不会称它为工厂。查看youtube.com/watch?v=rtXpYpZdOzM
  • 好的:),你是对的。所以它需要一些改变。每个用户/请求需要一个实例。这将由 IOC 和每个请求的对象定义来完成。
  • 那是我朋友的票 :)
【解决方案2】:

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();
        }
    }
}

这对于罕见的批量处理场景非常有效,我需要立即提交。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2023-03-23
    • 1970-01-01
    • 1970-01-01
    • 2014-10-28
    • 2010-12-10
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多