存储库模式 + 依赖注入 + 工作单元 + EF
Repository Pattern + Dependancy Injection + UnitOfWork + EF
S/O 上有很多与此类似的问题,但这个问题有一个我尚未解决的具体问题:
这是一个 MVC 应用程序。我正在使用依赖注入(简单注入器,虽然我认为它是无关紧要的),它注入了 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 个用户要更新,而第 3 个失败,因为名称列不够长,无法处理新名称,那么结果 3 到 100 都会有它们中的相同验证消息。解决这个问题的唯一方法是为 for 循环的每次传递使用一个新的工作单元 (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();
}
}
这感觉很奇怪,因为现在这个函数的行为与我所有其他存储库操作完全不同,并且不会服从事务。
是否有解决此问题的 UoW + EF + 存储库模式 + DI 的实现?
工作原理:
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;
}
}
Main class 是 DbFactory,它包含 只有一个 EF db context 实例。所以无论你在不同的存储库中做什么,应用程序总是使用一个上下文。
Class EntityBaseRepository 也可以在相同的数据库上下文 上工作,由 DbFactory 提供。
UnitOfWork 被传递给控制器只是为了能够使用 Commit 方法保存对数据库的所有更改,在同一个数据库上下文实例上工作。
您可能不需要您在代码中使用的事务。
完整教程在这里:
查找单词:"DbFactory" 或 "UnitOfWork" 以了解详细信息。
让 AddLastNameToAll()
立即提交每个更改的解决方案是 'immediate repository wrapper'。这让我可以重用我现有的代码,并让我在测试我的服务时继续轻松地模拟存储库行为。用户的示例即时存储库包装器如下所示:
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();
}
}
}
这对于我需要立即提交的罕见批量处理场景非常有效。
S/O 上有很多与此类似的问题,但这个问题有一个我尚未解决的具体问题:
这是一个 MVC 应用程序。我正在使用依赖注入(简单注入器,虽然我认为它是无关紧要的),它注入了 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 个用户要更新,而第 3 个失败,因为名称列不够长,无法处理新名称,那么结果 3 到 100 都会有它们中的相同验证消息。解决这个问题的唯一方法是为 for 循环的每次传递使用一个新的工作单元 (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();
}
}
这感觉很奇怪,因为现在这个函数的行为与我所有其他存储库操作完全不同,并且不会服从事务。
是否有解决此问题的 UoW + EF + 存储库模式 + DI 的实现?
工作原理:
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;
}
}
Main class 是 DbFactory,它包含 只有一个 EF db context 实例。所以无论你在不同的存储库中做什么,应用程序总是使用一个上下文。
Class EntityBaseRepository 也可以在相同的数据库上下文 上工作,由 DbFactory 提供。
UnitOfWork 被传递给控制器只是为了能够使用 Commit 方法保存对数据库的所有更改,在同一个数据库上下文实例上工作。
您可能不需要您在代码中使用的事务。
完整教程在这里:
查找单词:"DbFactory" 或 "UnitOfWork" 以了解详细信息。
让 AddLastNameToAll()
立即提交每个更改的解决方案是 'immediate repository wrapper'。这让我可以重用我现有的代码,并让我在测试我的服务时继续轻松地模拟存储库行为。用户的示例即时存储库包装器如下所示:
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();
}
}
}
这对于我需要立即提交的罕见批量处理场景非常有效。