使用Entity Framework时,存储库应该是工作单元的属性吗?

时间:2014-06-02 04:44:15

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

我已经在网上搜索了使用Entity Framework实现存储库/工作单元模式的良好实现。我所遇到的一切都要么在抽象中的某个点紧密耦合,要么假设工作单元和存储库使用的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 我将抽象出实体框架?

2 个答案:

答案 0 :(得分:6)

你应该从不抽象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 的哪一部分暗示该方法还更新了统计信息?)。

总之,你需要更好地设计BL,然后用UoW,DbContext,Repositories作为属性等来使你的生活复杂化

答案 1 :(得分:3)

  

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

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

另一方面,如果从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!

因为来自不同背景的混合因素只会引发异常。但上面的情况很常见!

这些观察结果表明,应该分享回购之间的背景。但是,如果你需要一个新的实例,你只需创建一个本地的,新的上下文实例,将其注入UoW,从那里注入到repos中,并随意处理它。