有很多类似的问题,这在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的实现来解决这个问题?
您是否知道EF默认实现UoW和回购模式? DbContext是一个UoW,DbSet是Repos。我的观点;给EF增加更多抽象是浪费时间。 –
我知道DbContext是一个UoW。为了避免EF渗入服务层,我只对它进行了薄包装。它周围的回购包装可以帮助我通过一些存储库操作来收集所有信息,以及警察坏联接等。如果有更好的方式来完成这些事情,我很乐意听到它。 – sheamus
AddLastNameToAll()中提出的问题实际上发生在几乎所有批量处理的相当专业化的服务中。我倾向于创建一对不使用UoW模式的专门的'ImmediateRepositories'。我认为这可能是最简单的。 – sheamus