【问题标题】:How to implement IDisposable inheritance in a repository?如何在存储库中实现 IDisposable 继承?
【发布时间】:2013-12-03 18:26:49
【问题描述】:

我正在创建一个通用存储库,但不知道实现处置功能的正确方法是什么:

我没有使用 IoC/DI,但我会在未来重构我的代码来做到这一点,所以:

我的代码:

IUnitOfWork 接口:

namespace MyApplication.Data.Interfaces
{
    public interface IUnitOfWork
   {
       void Save();
   }
}

DatabaseContext 类:

namespace MyApplication.Data.Infra
{
    public class DatabaseContext : DbContext, IUnitOfWork
    {
        public DatabaseContext(): base("SQLDatabaseConnectionString")
        {
            Database.SetInitializer<DatabaseContext>(null);
        }

        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            // Same code.
            base.OnModelCreating(modelBuilder);
        }

        #region Entities mapping
        public DbSet<User> User { get; set; }
        // >>> A lot of tables
        #endregion

        public void Save()
        {
            base.SaveChanges();
        }
    }
}

IGenericRepository 接口:

namespace MyApplication.Data.Interfaces
{
    public interface IGenericRepository<T> where T : class
    {
        IQueryable<T> GetAll();

        IQueryable<T> Get(Expression<Func<T, bool>> predicate);

        T Find(params object[] keys);

        T GetFirstOrDefault(Expression<Func<T, bool>> predicate);

        bool Any(Expression<Func<T, bool>> predicate);

        void Insert(T entity);

        void Edit(T entity);

        void Delete(Expression<Func<T, bool>> predicate);
    }
}

GenericRepository 类:

namespace MyApplication.Data.Repositories
{
    public class GenericRepository<T> : IDisposable, IGenericRepository<T> where T  : class
    {
        private DatabaseContext _context;
        private DbSet<T> _entity;

        public GenericRepository(IUnitOfWork unitOfWork)
        {
            if (unitOfWork == null)
                throw new ArgumentNullException("unitofwork");

            _context = unitOfWork as DatabaseContext;
            _entity = _context.Set<T>();
        }

        public IQueryable<T> GetAll()
        {
            return _entity;
        }

        public IQueryable<T> Get(Expression<Func<T, bool>> predicate)
        {
            return _entity.Where(predicate).AsQueryable();
        }

        // I delete some of the code to reduce the file size.

        #region Dispose
        public void Dispose()
        {
            // HERE IS MY FIRST DOUBT: MY METHOD ITS OK?!
            // I saw implementations with GC.Suppress... and dispose in destructor, etc.

            _context.Dispose();
        }
       #endregion
    }
}

IUserRepository 接口:

namespace MyApplication.Data.Interfaces
{
    public interface IUserRepository : IGenericRepository<User> { }
}

UserRepository 类:

namespace MyApplication.Data.Repositories
{
    public class UserRepository : GenericRepository<User>, IUserRepository, IDisposable
    {
        public UserRepository(IUnitOfWork unitOfWork) : base(unitOfWork) {}
    }
}

UserController 控制器类:

namespace MyApplication.Presentation.MVCWeb.Controllers
{
    [Authorize]
    public class UserController : Controller
    {
        private IUserRepository _userRepository;
        private IProfileRepository _profileRepository;
        private IUnitOfWork _unitOfWork;

        public UserController()
        {
            this._unitOfWork = new DatabaseContext();
            this._userRepository = new UserRepository(_unitOfWork);
            this._profileRepository = new ProfileRepository(_unitOfWork);
        }

        public ActionResult List()
        {
            return View(this._userRepository.GetAll().ToList());
        }

        public ActionResult Create()
        {
            ViewBag.Profiles = new SelectList(this._profileRepository.GetAll().ToList(), "Id", "Name");

            return View(new User());
        }

        [HttpPost]
        [ValidateAntiForgeryToken]
        public ActionResult Create([Bind(Exclude = "Id, Status, CompanyId")] User model)
        {
            ViewBag.Profiles = new SelectList(this._profileRepository.GetAll().ToList(), "Id", "Name");

            if (ModelState.IsValid)
            {
                model.EmpresaId = 1;
                model.Status = Status.Active;

                _userRepository.Insert(model);
                _unitOfWork.Save();

                return RedirectToAction("List");
            }
            else
            {
                return View();
            }
        }
    }

那么,我何时以及如何处置我的控制器和/或我的存储库和上下文?

【问题讨论】:

  • 您将类型与类型参数混淆了。

标签: c# repository


【解决方案1】:

更新答案:

我的控制器示例:

private IGenericRepository<Profile> _repository; 
private IUnitOfWork _unitOfWork = new DatabaseContext();
public ProfileController() 
{ 
    this._repository = new GenericRepository<Profile>(_unitOfWork); 
}

public class ProfileController : Controller 
{
    private IGenericRepository<Profile> _repository;
    private IUnitOfWork _unitOfWork = new DatabaseContext();  
    public ProfileController()  
    { 
        this._repository = new GenericRepository<Profile>(_unitOfWork);  
    }
}

使用您现在拥有的代码,最好的办法是覆盖 Controller.Dispose(bool disposing) 并在其中处理存储库。

protected override void Dispose(bool disposing)
{
    if (disposing)
    {
        IDisposable d = _repository as IDisposable;
        if (d != null)
            d.Dispose();
        GC.SupressFinalize(this);
    }
    base.Dispose(disposing);
}

一旦您开始使用 IOC 容器,所有这些处理代码都会消失。建造和处置应在集装箱层面进行。容器将是唯一知道或关心存储库和工作单元是一次性的。

但我怀疑这些类一开始都不需要是一次性的。您应该在 using 块中使用 SqlConnection。它不需要是DatabaseContext 中的类级字段。


请原谅这个答案的长度。为了使我的建议有意义,我必须建立一些基础。

S.O.L.I.D.

SOLID ... 代表面向对象编程和设计的五个基本原则。 这里关注的两个原则是IS

接口隔离原则 (ISP)

IGenericRepository&lt;T&gt; 接口上包含IDisposable 明显违反了ISP。

这样做是因为存储库的可处置性(以及正确处置对象的必要性)与其设计目的无关,即获取和存储聚合根对象。通过将接口组合在一起,您将获得一个非隔离的接口。

除了违反一些理论原则之外,为什么这很重要?我将在下面解释。但首先我必须涵盖另一个 SOLID 原则:

单一职责原则

我总是保留这篇文章,Taking the Single Responsibility Principle Seriously,当我将功能代码重构为良好的 OO 代码时,它会很方便。这不是一个容易的话题,而且文章很密集。但它作为对 SRP 的彻底解释是非常宝贵的。

了解 SRP 并忽略 99.9% 的所有 MVC 控制器中的缺陷,这些控制器采用许多 DI 构造函数参数,这里相关的一个缺陷是:

让控制器负责使用存储库和处理存储库跨越到不同的抽象级别,这违反了 SRP。

解释:

因此,您至少在存储库对象上调用了两个公共方法。一个到Get 一个对象,一个到Dispose 存储库。这没什么不好,对吧?通常不会,在存储库或任何对象上调用两个方法都没有错。

但是Dispose() 很特别。处置对象的惯例是处置后将不再有用。这个约定是使用模式建立单独代码块的原因之一:

using (var foo = new Bar())
{
    ...  // This is the code block
}

foo.DoSomething();  // <- Outside the block, this does not compile

这在技术上是合法的:

var foo = new Bar();
using (foo)
{
    ...  // This is the code block
}

foo.DoSomething();   // <- Outside the block, this will compile

但这会在对象被处理后发出使用警告。这是不正确的,这就是为什么您在 MS 文档中看不到这种用法的示例。

由于这种独特的约定,Dispose(),与构造和销毁对象的关系比与对象的其他成员的使用更密切相关,即使它被公开为一个简单的公共方法。

Construction 和 Disposal 处于相同的低抽象级别。但是因为控制器本身并没有构建存储库,所以它存在于更高的抽象层次上。在处理存储库时,它超出了其抽象级别以在不同级别处理存储库对象。这违反了 SRP。

代码现实

好的,就我的代码而言,所有这些理论都意味着什么?

考虑一下控制器代码在处理存储库本身时的样子:

public class CustomerController : Controller
{
    IGenericRepository<Customer> _customerRepo;
    IMapper<Customer, CustomerViewModel> _mapper;

    public CustomerController(
        IMapper<Customer, CustomerViewModel> customerRepository,
        IMapper<Customer, CustomerViewModel> customerMapper)
    {
        _customerRepo = customerRepository;
        _customerMapper = customerMapper;
    }

    public ActionResult Get(int id)
    {
        CustomerViewModel vm;
        using (_customerRepo)  // <- This looks fishy
        {
            Customer cust = _customerRepo.Get(id);
            vm = _customerMapper.MapToViewModel(cust);
        }
        return View(wm);
    }

    public ActionResult Update(CustomerViewModel vm)
    {
        Customer cust = _customerMapper.MapToModel(vm);
        CustomerViewModel updatedVm;
        using(_customerRepo)  // <- Smells like 3 week old flounder, actually
        {
            Customer updatedCustomer = _customerRepo.Store(cust);
            updatedVm = _customerMapper.MapToViewModel(updatedCustomer);
        }
        return View(updatedVm);
    }
}

控制器在构造时必须接收一个有用的(未处置的)存储库。这是一个普遍的期望。但是不要在控制器中调用两个方法,否则它会中断。这个控制器是一次性的。另外,您甚至不能从另一个公共方法中调用一个公共方法。例如。 Update 方法可以在将模型存储在存储库中之后调用 Get 以返回更新的客户视图。但这会爆炸。

结论

接收存储库作为参数意味着其他东西负责创建存储库。其他东西也应该负责正确处理存储库。

当对象的生命周期和对象的可能后续使用不受直接控制时,将对象置于与使用其(其他)公共成员相同的抽象级别的替代方案是一个定时炸弹。

IDisposable 的规则是这样的:在另一个函数接口声明中继承 IDisposable 是绝对不能接受的,因为 IDisposable 从来不是一个函数问题,而只是一个实现细节。

【讨论】:

  • 很好,但我的观点仍然成立。我也不喜欢构造函数注入。不过,这可能是因为我不是在做 Web 的事情。也许在 ASP.Net MVC 场景中这会更有意义,但在我可配置的单层或多层客户端/服务器应用程序框架中却没有。我的服务通过 using (var repo = TypeResolver.Get&lt;IRepository&gt;()) 获得对 Repo 的引用,所以我不关心 SRP 或其他什么,这使我的代码更干净。
  • @HighCore 假设您的定位器提供了一个新的存储库。无论如何,OP 是关于注入的对象,而不是定位的对象。
  • @HighCore BTW,这并不是要反驳或反驳你的观点。
  • 在这种情况下,+1 以获得出色、有据可查的详细答案。
  • @HighCore 谢谢。 OP 开始积极反对我的建议的有效性。对于未来的读者来说,知道有有效的替代方案很重要。
【解决方案2】:

让您的 Interface 继承自 IDisposable

public interface IGenericRepository<T> : IDisposable where T : class
{
   //...
}

类:

public class GenericRepository<T> : IGenericRepository<T>
{
    public void Dispose()
    {
        //....
    }
}

【讨论】:

  • 我在IDisposable后面放了一个逗号,谢谢你,真的,你救了我!
  • 我不同意这个答案。 IGenericRepository 对象的“可处置性”是一个实现细节,不属于接口,而是属于类声明。存储库模式旨在向消费者隐藏实现细节,例如DbConnection 的状态。通过创建接口IDisposable,消费者现在必须对类的私有关注点负责。
  • @KeithPayne 这就是 OP 正在寻找的东西。是的,这很棒,因为您可以通过 DI 或 Service Locator 解析存储库并在其上使用 using 语句,而无需强制转换为 IDisposable。而且这不是“类的私人关注”的问题,因为类本身是一次性的,而不是私人成员或其他东西。
  • @HighCore 类是一次性的,并不是所有接口的实现都是一次性的。例如,内存存储库通常不需要是一次性的。如果想不惜一切代价避免转换为IDisposable,可以使用IDisposableGenericRepository&lt;T&gt; 接口。
  • @KeithPayne 在我的例子中,接口的所有实现都是一次性的。是的,也许这是一个更恰当的名字。
【解决方案3】:

单一职责原则

  • 如果您查看 SRP 并在 MVC 中使用它,它会告诉您,当您的事务由多个方法 (CRUDS) 组成时,这不是实现事务的正确方法。

  • 1234563 @ 构成事务方法的方法。如果所有这些 (CRUD) 方法都成功,您可以调用 Commit 方法 (SaveChanges)。当这些方法失败时,你什么都不做(=Rollback)。因此,请确保在这些方法中没有执行 Commit,而是将其委托给 Transaction 方法。
  • 通过实现第二个项目符号IDispose 不需要在存储库中,因为using 负责清理database context

  • 如果使用Connection Pooling,这是一种有效的实现方式,并且可以扩展。

您在上面设置类的方式感觉不对。因为在存储库的构造函数中,您可以设置database context 并在几种方法中使用相同的context,但是您需要实现IDispose,因为您需要自己清理连接。在存储库的许多示例中,它都是以这种方式实现的,但从 SRP 的角度来看这是不正确的,应该根据上面的第二个项目符号进行更改。

【讨论】:

    【解决方案4】:

    上下文:您询问了实现处置功能的正确方法。

    解决方案:这是这种情况和任何其他情况的替代方案。这个自动替代方案是由 Visual Studio 本身建议的(我在 VS2017 和 VS2019 中测试过):

    1. 在您的班级名称旁边添加 IDisposable 合同 => class MyClass : IDisposable
    2. 然后将光标仍在“IDisposable”这个词上,按 Alt+Enter 或 Ctrl+。启用“快速操作”
    3. 然后选择“Implement interface with Dispose pattern”
    4. 最后,您可以按照 cmets 中的建议为您的托管和非托管对象完成该代码模式。

    【讨论】:

      猜你喜欢
      • 2010-12-22
      • 2012-08-16
      • 1970-01-01
      • 1970-01-01
      • 2013-07-25
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多