在使用实体框架时,存储库应该是工作单元上的属性吗?

本文关键字:单元 工作 属性 实体 框架 存储 | 更新日期: 2023-09-27 18:06:23

我在网上搜索了使用实体框架的存储库/工作单元模式的良好实现。我所遇到的一切要么在抽象的某个点上紧密耦合,要么假设工作单元和存储库使用的DbContext是共享的,并且应该为整个HTTP请求(通过依赖注入的每个请求实例)而存在。

例如,假设您正在使用来自服务层的存储库,那么服务构造函数可能如下所示:
public DirectoryService(IUnitOfWork unitOfWork, ICountryRepository countryRepo, IProvinceRepository provinceRepo)
{
    /* set member variables... */
}

工作单元构造函数可能是:

public UnitOfWork(IDbContext context)
{
    _context = context;
}

和一个存储库构造函数可能看起来像:

CountryRepository(IDbContext context)
{
    _context = context;
}

该解决方案盲目地假设依赖注入正在设置工作单元和存储库,以使用每个请求实例共享相同的IDbContext。这样的假设真的安全吗?

如果你对每个请求实例使用依赖注入,同样的IDbContext将被注入到多个工作单元中。功的单位不再是原子的了,是吗?我可能在一个服务中有挂起的更改,然后在另一个服务中提交,因为上下文是跨多个工作单元共享的。

对我来说,设置一个IDbContextFactory并获得每个工作单元的新数据库上下文似乎更有意义。

public interface IDbContextFactory
{
    IDbContext OpenContext();
}
public class UnitOfWork 
{
    private IDbContextFactory _factory;
    private IDbContext _context;
    UnitOfWork(IDbContextFactory factory)
    {
        _factory = factory;
    }
    internal IDbContext Context
    {
        get { return _context ?? (_context = _factory.OpenContext()); }
    }
}

问题就变成了,我如何使我的工作单元实现对注入的存储库可用?我不想假设每个请求都有一个实例,因为那样我就会回到我开始的同一条船上。

我能想到的唯一一件事就是遵循实体框架的领导,使存储库(IDbSet<T>)成为工作单元(DbContext)的一部分。

那么我可能有这样的功单位:

public class DirectoryUnitOfWork : IDirectoryUnitOfWork
{
    private IDbContextFactory _factory;
    private IDbContext _context;
    public DirectoryUnitOfWork(IDbContextFactory factory)
    {
        _factory = factory;
    }
    protected IDbContext Context 
    {
        get { return _context ?? (_context = _factory.OpenContext()); }
    }
    public ICountryRepository CountryRepository
    {
        get { return _countryRepo ?? (_countryRepo = new CountryRepository(Context)); }
    }
    public IProvinceRepository ProvinceRepository
    {
        get { return _provinceRepo ?? (_provinceRepo = new ProvinceRepository(Context)); }
    }
    void Commit()
    {
        Context.SaveChanges();
    }
}

然后我的目录服务开始看起来像这样

public class DirectoryService : IDirectoryService
{
    private IDirectoryUnitOfWork _unitOfWork;
    public DirectoryService(IDirectoryUnitOfWork unitOfWork)
    {
        _unitOfWork = unitOfWork;
    }
    public GetCountry(int id)
    {
        return _unitOfWork.CountryRepository.GetById(id);
    }
    public GetProvince(int id)
    {
        return _unitOfWork.ProvinceRepository.GetById(id);
    }
    public void AddProvince(Province province)
    {
        _unitOfWork.ProvinceRepository.Create(province);
        Country country = GetCountry(province.CountryId);
        country.NumberOfProvinces++; // Update aggregate count
        _unitOfWork.Commit();
    }
    /* ... and so on ... */
}

看起来工作量很大,但是使用这种方法使所有东西都松散耦合并且可以进行单元测试。我错过了一个更容易的方法,还是这是一个很好的方法来做它 if 我要抽象掉实体框架?

在使用实体框架时,存储库应该是工作单元上的属性吗?

您应该永远不要抽象ORM(它本身是一个抽象),但是您应该抽象持久性。我使用的唯一UoW是一个db事务,它是一个持久化细节。您不需要同时使用UoW和Repository。如果你真的需要这些东西,你应该考虑

就我个人而言,我默认使用存储库,因为持久化是我在应用程序中做的最后一件事。我不关心模式本身,我关心的是将我的BL或UI与DAL解耦。你的上层(除了DAL之外的所有东西,从依赖关系的角度来看,它是最低层)应该总是知道抽象,这样你就可以在DAL实现中随心所欲。

许多开发人员不知道的一个技巧是,设计模式(尤其是架构模式)应该首先被视为高级原则,其次才是技术诀窍。简单地说,最重要的事情是模式试图实现的利益(原则,更大的图景),而不是它的实际实现("低级"细节)。

问问你自己,为什么BL一开始就应该知道UoW。BL只知道处理业务对象的抽象。你永远不会一次处理整个BL,你总是在一个特定的BL环境中。你的DirectoryService似乎为自己的利益做得太多了。更新统计数据看起来与添加新省份不属于同一个上下文中。另外,为什么需要UoW进行查询?

我经常看到的一个错误是开发人员匆忙编写代码(附带设计模式),而他们没有做最重要的部分:设计本身。当您的设计不正确时,问题就会出现,您就会开始寻找解决方案。其中之一是具有Repository属性的UoW,它需要像BL这样的高层来了解业务之外的内容。现在,BL需要知道你正在使用UoW,这是一种较低级别的模式,对DAL来说很好,对BL来说就不那么好了。

顺便说一句,当你处理"读"的情况时,UoW对查询从来没有意义,UoW是只有用于"写"。我没有提到EF或任何ORM,因为它们并不重要,您的BL服务(目录服务)已经被不适当的设计所需的基础结构细节破坏了。请注意,有时您确实需要妥协以实现解决方案,但情况并非如此。

适当的设计意味着你知道你的有限上下文(是的,DDD概念,你可以应用它,不管你想做多少DDD),不要把所有可能使用相同数据的东西放在一个地方。对于用例,您有特定的上下文,计算省份(表示/报告细节)实际上是与添加省份不同的用例。

添加省份,然后发布一个事件,该事件向处理程序发出信号以更新统计数据,这是一种更优雅、更易于维护的解决方案。也不需要UoW。

你的代码看起来像这样

public class DirectoryService : IDirectoryService
{
     public DirectoryService(IProvinceRepository repo, IDispatchMessage bus) 
     { 
       //assign fields
     }
      /* other stuff */
      //maybe province is an input model which is used by the service to create a business Province?
      public void AddProvince(Province province)
     {
        _repo.Save(province);
        _bus.Publish( new ProvinceCreated(province));
     }
}
  public class StatsUpdater:ISubscribeTo<ProvinceCreated> /* and other stat trigger events */
   {
       public void Handle(ProvinceCreated evnt)
       {
              //update stats here
       }  
   }
在某种程度上,它简化了事情,在另一方面,你可能会认为它使事情复杂化。实际上,这是一种可维护的方法,因为统计更新程序可以订阅多个事件,但逻辑只保留在一个类中。而且DirectoryService只做假定它做的事情(名称AddProvince的哪个部分暗示该方法也更新统计信息?)

总之,在急于用UoW、DbContext、repository等属性使您的生活复杂化之前,您需要更好地设计BL

或假设工作单元和存储库使用的DbContext是共享的,并且应该在整个HTTP请求

中存在

这显然是错误的。假设上下文在UoW和存储库之间共享,并不意味着上下文生存期应该依赖于HTTP请求。相反,您可以创建上下文和UoW的新实例,以便随时使用它。有一个默认的 UoW存在于HTTP请求中只是为了方便,但是创建新的本地工作单元可能更方便。

另一方面,如果存储库从UoW公开:

public class UnitOfWork
{
    ...
    public IUserRepository UserRepo
    {
        get { ... } 
    }
    public IAccountRepository AccountRepo
    {
        get { ... } 
    }

然后而不是在多个repos之间共享相同的上下文可能会产生意想不到的结果:

 UoW uow = ....
 var u1 = uow.User.FirstOrDefault( u => u.ID == 5 );
 var u2 = uow.Account.FirstOrDefault( a => a.ID_USER == 5 ).User;

您肯定希望这两个查询返回id为5的用户的相同实例,更重要的是,共享相同的上下文意味着第二个查询可以从第一级缓存检索用户。另一方面,两个repos的两个不同上下文意味着得到两个不同的实例。

这也意味着这是不可能的

 var u1 = uow.User.FirstOrDefault( u => u.ID == 5 );
 var a1 = uow.Account.FirstOrDefault( a => a.ID == 177 );
 a1.User = u1; // oops!

作为混合来自不同上下文的实体只会引发异常。但是上面的场景是很常见的!

从这些观察中得出的结论是,应该在repos之间共享上下文。但是,如果你需要一个新的实例,你只需要创建一个本地的,上下文的新实例,把它注入到UoW中,从那里它被注入到repos中,然后随意处置它。