通用存储库框架(通用工作单元)






4.83/5 (19投票s)
通用存储库框架(通用工作单元)
引言
在我的工作中,我遇到了在一个应用程序中访问不同类型存储库的问题。这个问题涉及到 ADO.NET 和 EF。我一直在寻找的解决方案应该能够让我在未来轻松地扩展现有功能,维护现有功能。我还希望能够在将数据存储到数据库之前验证数据。我想与您分享我的调查和结论。
我花了一些时间试图解决与附加到任何已定义框架的不同工作单元相关的问题。我找不到一种具有持久性忽略的通用方法。我也非常关注 TDD 和易于测试解决方案。总的来说,我不想去考虑存储库的类型(ADO、EF 或 WebService)。
我将在 Portal 项目上展示解决方案。Portal 允许执行一些操作,但我们将只关注 portal 用户及其联系信息。创建 Portal 时,我尝试遵循 SOLID 原则。我使用了 Autofac 进行依赖注入。该项目不展示完整的解决方案,只提供一些指导方针和小例子来支持我枯燥的演讲。在代码示例中,为了缩短代码,我省略了 null
对象验证。抱歉,这部分您需要自己完成。数据库架构如下。

服务
我将从服务开始。
服务是一组类(您可以称它们为控制器或管理器),它们将域对象来回处理到存储库。

我们有一个通用的服务接口,它提供了一个方法来验证传递的对象是否可以存储在存储库中。我们还有两个面向域对象的接口:User
和 ContactInfo
。它们是典型的 CRUD 方法。IServiceBase<T>
的实现如下:
public abstract class ServiceBase<P, U, T> : IServiceBase<T>
where T : class
where P : IUnitOfWorkProvider<U>
where U : IUnitOfWork
{
private readonly IValidator<T> _validator;
private readonly P _unitOfWorks;
private U _readOnly = default(U);
public ServiceBase(P unitOfWorks, IValidator<T> validator)
{
_validator = validator;
_unitOfWorks = unitOfWorks;
}
protected U GetTransactionalUnitOfWork()
{
var unitOfWork = _unitOfWorks.GetReadOnly();
return unitOfWork;
}
protected U GetReadOnlyUnitOfWork()
{
if (_readOnly == null)
{
_readOnly = _unitOfWorks.GetReadOnly();
}
return _readOnly;
}
protected void Validate(T entity)
{
var result = _validator.Validate(entity);
if (!result.IsValid)
{
throw new CustomValidationException(result.Errors);
}
}
public bool IsValid(T entity)
{
if (entity == null)
{
return false;
}
var result = _validator.Validate(entity);
return result.IsValid;
}
}
正如预期的那样,构造函数接受一个验证器作为参数。

除此之外,还有一个 IUnitOfWorkProvider<IUnitOfWork>
。工作单元提供程序为我们要在服务中与存储库执行的每个操作提供工作单元。


在这种情况下,这将是 IPortalUnitOfWorkProvider
。我们可以从同一个工作单元读取所有服务,但肯定需要一个新的工作单元来执行修改。工作单元可以确保所有执行的操作都包含在事务中。
PortalUnitOfWork
可能令人困惑。我将其定义为与 portal 通用的存储库部分(数据库 dbo.* 表)。如果我们平台上有商品销售,我们可以预期会有 ProductUnitOfWork
(以及可能对应的数据库架构)。


IPortalUnitOfWork
提供对可以执行事务的存储库的访问。在我们的例子中,它将是 IUserRepository
和 IContactInfoRepository
。关于工作单元的更多说明。通用工作单元允许仅在现有事务中保存更改。而其 portal 实现提供了存储库事务的关注点。工作单元根据存储库类型而有所不同。
现在我们有了工作单元,但缺少 IPortalUnitOfWokProvider
的实现。通用工作单元提供程序如下所示:
public abstract class UnitOfWorkProvider<T> : IUnitOfWorkProvider<T> where T : IUnitOfWork
{
private T _readOnly = default(T);
public T GetReadOnly()
{
if (_readOnly == null)
{
_readOnly = GetNew();
}
return _readOnly;
}
public T GetTransactional()
{
return GetNew();
}
protected abstract T GetNew();
public void Dispose()
{
if (_readOnly != null)
{
_readOnly.Dispose();
}
}
}
我的假设是,我的工作单元提供程序将在应用程序/HTTP 请求中作为单例。因此,我重用了所有对存储库的读取连接。Portal 的自定义实现只是覆盖了 GetNew()
方法。
每种存储库类型都实现自己的 UnitOfWork
,该 UnitOfWork
实现 IUnitOfWork
以及 IPortalUnitOfWork
。
现在我们可以回到服务,它们在构造函数中拥有提供 IUnitOfWork
的 IUnitOfWorkProvider
。服务类的实现对于所有类型的存储库都是相同的,并且与存储库无关。

public class UserService : ServiceBase<IUnitOfWorkProvider<IPortalUnitOfWork>, IPortalUnitOfWork, IUser>, IUserService
{
private readonly IUserRepository _readOnlyRepository;
public UserService(IPortalUnitOfWorkProvider provider, IUserValidator validator)
: base(provider, validator)
{
_readOnlyRepository = GetReadOnlyUnitOfWork().UserRepository;
}
public IUser AddOrUpdate(IUser user)
{
using (var unitOfWork = GetTransactionalUnitOfWork())
{
var result = AddOrUpdate(unitOfWork.UserRepository, user);
unitOfWork.Save();
return result;
}
}
public IUser AddOrUpdate(IUserRepository repository, IUser user)
{
Validate(user);
return repository.AddOrUpdate(user);
}
public IUser GetBy(string logon)
{
return _readOnlyRepository.GetBy(logon);
}
}
public class ContactInfoService : ServiceBase<IUnitOfWorkProvider<IPortalUnitOfWork>, IPortalUnitOfWork, IContactInfo>, IContactInfoService
{
private readonly IContactInfoRepository _readOnlyRepository;
public ContactInfoService(IPortalUnitOfWorkProvider provider, IContactInfoValidator validator)
: base(provider, validator)
{
_readOnlyRepository = GetReadOnlyUnitOfWork().ContactInfoRepository;
}
public IEnumerable<IContactInfo> GetUserContactInfosBy(string login)
{
return _readOnlyRepository.GetAllBy(login);
}
public void DeleteAllBy(IContactInfoRepository repository, IUser user)
{
repository.DeleteAllBy(user);
}
public IContactInfo AddOrUpdate(IContactInfoRepository repository, IUser user, IContactInfo contactInfo)
{
Validate(contactInfo);
return repository.AddOrUpdate(user, contactInfo);
}
public IContactInfo AddOrUpdate(IUser user, IContactInfo contactInfo)
{
using (var unitOfWork = GetTransactionalUnitOfWork())
{
var result = AddOrUpdate(_readOnlyRepository, user, contactInfo);
unitOfWork.Save();
return result;
}
}
}
构造函数中创建了 read-only
存储库。正如我所提到的,工作单元提供程序为所有读取操作保持一个连接。每次修改都在新的工作单元中完成。域模型的验证在将值传递到存储库之前完成,因此更改存储库不会损害域对象的验证。
还有一件事值得一提,那就是 UserInfo
域实体。
UserInfo
组合了用户及其联系信息以满足域需求。由于域实体组合了两个存储库模型,我们希望访问它们是透明的,并通过 UserInfoService
来处理。
public class UserInfoService : IUserInfoService
{
private readonly IContactInfoService _contactInfoService;
private readonly IUserInfoCreator _cretor;
private readonly IPortalUnitOfWorkProvider _unitOfWorks;
private readonly IUserService _userService;
public UserInfoService(IPortalUnitOfWorkProvider unitOfWorks,
IUserService userService, IContactInfoService contactInfoService, IUserInfoCreator cretor)
{
_unitOfWorks = unitOfWorks;
_userService = userService;
_cretor = cretor;
_contactInfoService = contactInfoService;
}
public IUserInfo GetBy(IUser user)
{
if (user == null)
{
return null;
}
var info = _cretor.From(user);
if (info == null)
{
return null;
}
var infos = _contactInfoService.GetUserContactInfosBy(user.Login);
if (infos != null)
{
info.PrimaryPhone = infos.Where(x => x.Type == ContactInfoType.Phone && x.IsDefault).FirstOrDefault();
info.SecondaryPhone = infos.Where(x => x.Type == ContactInfoType.Phone && !x.IsDefault).FirstOrDefault();
info.PrimaryEmail = infos.Where(x => x.Type == ContactInfoType.Email && x.IsDefault).FirstOrDefault();
info.SecondaryEmail = infos.Where(x => x.Type == ContactInfoType.Email && !x.IsDefault).FirstOrDefault();
}
return info;
}
public bool IsValid(IUserInfo userInfo)
{
if (userInfo == null)
{
return false;
}
if (!_userService.IsValid(userInfo.User))
{
return false;
}
if (!_contactInfoService.IsValid(userInfo.PrimaryEmail))
{
return false;
}
if (!_contactInfoService.IsValid(userInfo.PrimaryPhone))
{
return false;
}
return true;
}
public void Save(IUserInfo userInfo)
{
if (userInfo == null)
{
return;
}
using (var unitOfWork = _unitOfWorks.GetTransactional())
{
var user = _userService.AddOrUpdate(unitOfWork.UserRepository, userInfo.User);
_contactInfoService.DeleteAllBy(unitOfWork.ContactInfoRepository, user);
_contactInfoService.AddOrUpdate(unitOfWork.ContactInfoRepository, user, userInfo.PrimaryPhone);
_contactInfoService.AddOrUpdate(unitOfWork.ContactInfoRepository, user, userInfo.SecondaryPhone);
_contactInfoService.AddOrUpdate(unitOfWork.ContactInfoRepository, user, userInfo.PrimaryEmail);
_contactInfoService.AddOrUpdate(unitOfWork.ContactInfoRepository, user, userInfo.SecondaryEmail);
unitOfWork.Save();
}
}
}
如果我们只需要用户信息,则从存储库检索正确的数据并将其组合成域可接受的实体。我们使用现有的服务来检索用户的 User
和 ContactInfo
。通过现有服务进行操作,我们可以确保每个模型定义的逻辑都不会被破坏。
添加和更新也类似。会创建一个新的事务性工作单元,并将相应的存储库传递给服务。与上面相同,我们可以确保在处理之前进行验证是一致的。
实体框架
本节提供了有关如何添加 EntityFramework
存储库以使其成为工作解决方案的指南。
工作单元应提供 Context
并允许保存当前上下文中已进行的更改。我们的 Portal 工作单元还应提供 ContactInfoRepository
以及 UserRepository
。

首先,我们定义了一个通用 UnitOfWork
,它允许处理任何 EF DbContext
。
public abstract class UnitOfWork<T> : IUnitOfWork, IDisposable where T : DbContext
{
private readonly T _context;
private bool _disposed = false;
private DbContextTransaction _transaction = null;
protected T Context
{
get { return _context; }
}
public UnitOfWork(T entities)
{
_context = entities;
_transaction = _context.Database.BeginTransaction();
}
public void Save()
{
try
{
_context.SaveChanges();
_transaction.Commit();
}
catch
{
_transaction.Rollback();
throw;
}
finally
{
_transaction = _context.Database.BeginTransaction();
}
}
protected virtual void Dispose(bool disposing)
{
if (!this._disposed)
{
if (disposing)
{
_transaction.Dispose();
_context.Dispose();
}
}
this._disposed = true;
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
}
PortalUnitOfWorkProvider
的实现只是覆盖了 GetNew()
方法,该方法将返回 PortalUnitOfWork
。
public class PortalUnitOfWorkProvider : UnitOfWorkProvider<IPortalUnitOfWork>, IPortalUnitOfWorkProvider
{
protected override IPortalUnitOfWork GetNew()
{
return new PortalUnitOfWork();
}
}
UnitOfWork
实现了 IUnitOfWork
以及 IPortalUnitOfWork
。通用工作单元如下:
public abstract class UnitOfWork<T> : IUnitOfWork, IDisposable where T : DbContext
{
private readonly T _context;
private bool _disposed = false;
private DbContextTransaction _transaction = null;
protected T Context
{
get { return _context; }
}
public UnitOfWork(T entities)
{
_context = entities;
_transaction = _context.Database.BeginTransaction();
}
public void Save()
{
try
{
_context.SaveChanges();
_transaction.Commit();
}
catch
{
_transaction.Rollback();
throw;
}
finally
{
_transaction = _context.Database.BeginTransaction();
}
}
protected virtual void Dispose(bool disposing)
{
if (!this._disposed)
{
if (disposing)
{
_transaction.Dispose();
_context.Dispose();
}
}
this._disposed = true;
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
}
及其 Portal 版本:
public class PortalUnitOfWork : UnitOfWork<PortalEntities>, IPortalUnitOfWork
{
private IContactInfoRepository _contactInfoRepository;
private IUserRepository _userRepository;
public IContactInfoRepository ContactInfoRepository
{
get
{
if (_contactInfoRepository == null)
{
_contactInfoRepository = new ContactInfoRepository(Context);
}
return _contactInfoRepository;
}
}
public IUserRepository UserRepository
{
get
{
if (_userRepository == null)
{
_userRepository = new UserRepository(Context);
}
return _userRepository;
}
}
public PortalUnitOfWork()
: base(new PortalEntities())
{
}
}
为了展示完整的解决方案,以下是 Portal 存储库的实现。
必须记住不要允许存储库调用 _context.SaveChanges()
。这将破坏跨存储库的工作单元。SaveChanges
在事务中所有操作完成后在服务中调用。
public class UserRepository : GenericRepository<PortalEntities, User>, IUserRepository
{
public UserRepository(PortalEntities entities)
: base(entities)
{
}
public IUser GetBy(string login)
{
if (string.IsNullOrWhiteSpace(login))
{
return null;
}
var user = Get(u => u.Login.ToLower() == login.ToLower()).FirstOrDefault();
return user;
}
public IUser AddOrUpdate(IUser user)
{
if (user == null)
{
return null;
}
var current = Get(u => u.Login == user.Login).FirstOrDefault();
if (current != null)
{
user.MapUpdate<IUser, User>(current);
Update(current);
return current;
}
else
{
current = new User();
user.MapUpdate<IUser, User>(current);
base.Insert(current);
}
return current;
}
}
ADO.NET
本节提供了有关如何添加 ADO.NET 存储库以使其成为工作解决方案的指南。
工作单元应提供 Connection
和 Transaction
属性,并允许保存当前事务中已进行的更改。我们的 Portal 工作单元还应提供 ContactInfoRepository
以及 UserRepository
。

通用 UnitOfWork
类如下所示:
public abstract class UnitOfWork : IUnitOfWork, IDisposable
{
private readonly SqlConnection _connection;
private SqlTransaction _transaction;
private bool _disposed = false;
public SqlConnection Connection
{
get { return _connection; }
}
public SqlTransaction Transaction
{
get { return _transaction; }
}
public UnitOfWork(string connectionString)
{
_connection = new SqlConnection(connectionString);
_connection.Open();
_transaction = _connection.BeginTransaction();
}
public void Save()
{
try
{
_transaction.Commit();
_transaction = _connection.BeginTransaction();
}
catch
{
_transaction.Rollback();
throw;
}
finally
{
_transaction.Dispose();
_transaction = _connection.BeginTransaction();
}
}
protected virtual void Dispose(bool disposing)
{
if (!this._disposed)
{
if (disposing)
{
_transaction.Dispose();
_connection.Dispose();
}
}
this._disposed = true;
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
PortalUnitOfWorkProvider
的实现只是覆盖了 GetNew()
方法,该方法将返回 PortalUnitOfWork
。PortalUnitOfWorkProvider
接受连接 string
作为输入参数。
public class PortalUnitOfWorkProvider : UnitOfWorkProvider<IPortalUnitOfWork>, IPortalUnitOfWorkProvider
{
private readonly string _connectionString;
public PortalUnitOfWorkProvider(string connectionString)
{
_connectionString = connectionString;
}
protected override IPortalUnitOfWork GetNew()
{
return new PortalUnitOfWork(_connectionString);
}
}
UnitOfWork
实现了 IUnitOfWork
以及 IPortalUnitOfWork
。通用工作单元如下:
public abstract class UnitOfWork : IUnitOfWork, IDisposable
{
private readonly SqlConnection _connection;
private SqlTransaction _transaction;
private bool _disposed = false;
public SqlConnection Connection
{
get { return _connection; }
}
public SqlTransaction Transaction
{
get { return _transaction; }
}
public UnitOfWork(string connectionString)
{
_connection = new SqlConnection(connectionString);
_connection.Open();
_transaction = _connection.BeginTransaction();
}
public void Save()
{
try
{
_transaction.Commit();
_transaction = _connection.BeginTransaction();
}
catch
{
_transaction.Rollback();
throw;
}
finally
{
_transaction.Dispose();
_transaction = _connection.BeginTransaction();
}
}
protected virtual void Dispose(bool disposing)
{
if (!this._disposed)
{
if (disposing)
{
_transaction.Dispose();
_connection.Dispose();
}
}
this._disposed = true;
}
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
及其 Portal 版本:
public class PortalUnitOfWork : UnitOfWork, IPortalUnitOfWork
{
private IContactInfoRepository _contactInfoRepository;
private IUserRepository _userRepository;
public IContactInfoRepository ContactInfoRepository
{
get
{
if (_contactInfoRepository == null)
{
_contactInfoRepository = new ContactInfoRepository(Connection);
}
return _contactInfoRepository;
}
}
public IUserRepository UserRepository
{
get
{
if (_userRepository == null)
{
_userRepository = new UserRepository(this);
}
return _userRepository;
}
}
public PortalUnitOfWork(string connectionString)
: base(connectionString)
{
}
}
为了展示完整的解决方案,以下是 Portal 存储库的实现。
public class UserRepository : GenericRepository, IUserRepository
{
private PortalUnitOfWork _unitOfWork;
public UserRepository(PortalUnitOfWork unitOfWork)
{
_unitOfWork = unitOfWork;
}
public IUser GetBy(string login)
{
if (string.IsNullOrWhiteSpace(login))
{
return null;
}
using (var command = new SqlCommand())
{
command.Connection = _unitOfWork.Connection;
command.Transaction = _unitOfWork.Transaction;
command.CommandType = CommandType.Text;
command.CommandText = "SELECT id, login, firstname, lastname FROM [dbo].[User] WHERE login = @login ";
command.Parameters.Add("@login", SqlDbType.VarChar);
command.Parameters["@login"].Value = login;
var table = ExecuteToDataTable(command, "User");
var entity = GetEntity(table);
return entity;
}
}
public IUser AddOrUpdate(IUser user)
{
if (user == null)
{
return null;
}
using (var command = new SqlCommand())
{
command.Connection = _unitOfWork.Connection;
command.Transaction = _unitOfWork.Transaction;
command.CommandType = CommandType.Text;
command.CommandText = "MERGE [dbo].[User] AS ex USING _
(SELECT @id As ID, @login as Login, @firstname as firstname, _
@lastname as lastname) AS u ON ex.login = u.login WHEN MATCHED _
THEN UPDATE SET ex.firstname = @firstname, ex.lastname =@lastname _
WHEN NOT MATCHED THEN INSERT(login,firstname, lastname) _
VALUES(@login, @firstname, @lastname); SELECT id, login, _
firstname, lastname FROM [dbo].[User] WHERE login = @login ";
command.Parameters.Add("@id", SqlDbType.Int);
command.Parameters["@id"].Value = user.Id;
command.Parameters.Add("@login", SqlDbType.VarChar);
command.Parameters["@login"].Value = user.Login;
command.Parameters.Add("@firstname", SqlDbType.VarChar);
command.Parameters["@firstname"].Value = user.FirstName;
command.Parameters.Add("@lastname", SqlDbType.VarChar);
command.Parameters["@lastname"].Value = user.LastName;
var table = ExecuteToDataTable(command, "User");
var entity = GetEntity(table);
return entity;
}
}
public IUser GetEntity(DataTable dataTable)
{
var entities = GetEntities(dataTable);
if (entities != null)
{
return entities.FirstOrDefault();
}
return null;
}
public IEnumerable<IUser> GetEntities(DataTable dataTable)
{
if (dataTable == null || dataTable.Rows.Count == 0)
{
return null;
}
var entities = Mapper.DynamicMap<IDataReader, List<User>>(dataTable.CreateDataReader());
return entities;
}
}
工具
在接下来的几节中,您将找到 EF 和 ADO.NET 存储库的实现。为了实现这一点,我需要介绍一些我用来帮助自己的工具。
AutoMapper
如果在代码中发现任何与映射相关的内容,您可以百分之百确定使用的是 AutoMapper
。您可以在 这里 找到它。“AutoMapper
是一个简单的小型库,用于解决一个复杂的问题——消除将一个对象映射到另一个对象的代码。”——我完全同意这一点。
FluentValidation
为了以一种通用的方式验证域对象,我开始使用 FluentValidator
。您可以在 CodePlex 这里 找到它。如何使用?正如您所注意到的,所有域实体都由接口定义。所以我们的 User 验证器看起来是这样的:
public class UserValidator : BaseValidator<IUser>, IUserValidator
{
public UserValidator()
{
RuleFor(x => x.Login)
.NotEmpty()
.Length(0, 50);
RuleFor(x => x.FirstName)
.Length(0, 50);
RuleFor(x => x.LastName)
.Length(0, 50);
}
}
它已集成到 ASP.NET MVC 和 Web API 2 中。很酷 :)