C# 工作单位+;存储库模式:业务事务概念的衰落

C# 工作单位+;存储库模式:业务事务概念的衰落,c#,java,architecture,repository-pattern,unit-of-work,C#,Java,Architecture,Repository Pattern,Unit Of Work,将工作单元和存储库模式相结合是当今应用相当广泛的一种模式。正如Martin Fowler所说,使用UoW的目的是形成业务事务,同时对存储库的实际工作方式一无所知(持续无知)。我已经回顾了许多实现;忽略具体细节(具体/抽象类、接口等),它们或多或少类似于以下内容: public class RepositoryBase<T> { private UoW _uow; public RepositoryBase(UoW uow) // injecting UoW insta

工作单元
存储库模式
相结合是当今应用相当广泛的一种模式。正如Martin Fowler所说,使用
UoW
的目的是形成业务事务,同时对存储库的实际工作方式一无所知(持续无知)。我已经回顾了许多实现;忽略具体细节(具体/抽象类、接口等),它们或多或少类似于以下内容:

public class RepositoryBase<T>
{
    private UoW _uow;
    public RepositoryBase(UoW uow) // injecting UoW instance via constructor
    {
       _uow = uow;
    }
    public void Add(T entity)
    {
       // Add logic here
    }
    // +other CRUD methods
}

public class UoW
{
    // Holding one repository per domain entity

    public RepositoryBase<Order> OrderRep { get; set; }
    public RepositoryBase<Customer> CustomerRep { get; set; }
    // +other repositories

    public void Commit()
    {
       // Psedudo code: 
       For all the contained repositories do:
           store repository changes.
    }
}
我认为这是不允许的。考虑到
UoW
(业务事务)的目的,方法
Commit
应仅向启动业务事务的人公开,例如业务层。令我惊讶的是,我找不到任何关于这个问题的文章。在所有这些协议中,
Commit
可以被任何正在注入的回购协议调用


PS:我知道我可以告诉我的开发人员不要在
存储库中调用
Commit
,但是受信任的体系结构比受信任的开发人员更可靠

在.NET中,数据访问组件通常会自动登记到环境事务中。因此,以事务内方式保存更改与通过提交事务来保存更改是分开的

换言之,如果您创建一个事务范围,您可以让开发人员尽可能多地节省。直到事务被提交,数据库的可观察状态才会被更新(好的,什么是可观察的取决于事务隔离级别)

这显示了如何在c#中创建事务作用域:

使用(TransactionScope范围=新TransactionScope())
{
//这里是您的逻辑。在事务内部保存您想要的内容。

scope.Complete();//我同意您的担忧。我更喜欢使用环境工作单元,其中打开工作单元的最外层函数是决定是否提交或中止的函数。调用的函数可以打开一个工作单元作用域,该工作单元作用域在有环境UoW时自动登记,如果没有,则创建一个新的环境UoW

我使用的
UnitOfWorkScope
的实现很大程度上受到了
TransactionScope
工作原理的启发。使用环境/范围方法也消除了依赖注入的需要

执行查询的方法如下所示:

public static Entities.Car GetCar(int id)
{
    using (var uow = new UnitOfWorkScope<CarsContext>(UnitOfWorkScopePurpose.Reading))
    {
        return uow.DbContext.Cars.Single(c => c.CarId == id);
    }
}
using (var uow = new UnitOfWorkScope<CarsContext>(UnitOfWorkScopePurpose.Writing))
{
    Car c = SharedQueries.GetCar(carId);
    c.Color = "White";
    uow.SaveChanges();
}
// ...
var uow = new MyUnitOfWork();

repo1.Add(entity1, uow);
repo2.Add(entity2, uow);
uow.Commit();
publicstaticenties.cargetcar(int-id)
{
使用(var uow=新的UnitOfWorkScope(UnitOfWorkScopePurpose.Reading))
{
返回uow.DbContext.Cars.Single(c=>c.CarId==id);
}
}
写入的方法如下所示:

public static Entities.Car GetCar(int id)
{
    using (var uow = new UnitOfWorkScope<CarsContext>(UnitOfWorkScopePurpose.Reading))
    {
        return uow.DbContext.Cars.Single(c => c.CarId == id);
    }
}
using (var uow = new UnitOfWorkScope<CarsContext>(UnitOfWorkScopePurpose.Writing))
{
    Car c = SharedQueries.GetCar(carId);
    c.Color = "White";
    uow.SaveChanges();
}
// ...
var uow = new MyUnitOfWork();

repo1.Add(entity1, uow);
repo2.Add(entity2, uow);
uow.Commit();
使用(var-uow=newunitofworkscope(UnitOfWorkScopePurpose.Writing))
{
carc=sharedquerys.GetCar(carId);
c、 Color=“白色”;
uow.SaveChanges();
}
请注意,
uow.SaveChanges()
调用只会在根(otermost)作用域的情况下实际保存到数据库。否则,将被解释为允许根作用域保存更改的“同意投票”


UnitOfWorkScope
的整个实现可在以下位置获得:

不要传入
UnitOfWork
,传入具有所需方法的接口。如果需要,您仍然可以在原始具体的
UnitOfWork
实现中实现该接口:

public interface IDbContext
{
   void Add<T>(T entity);
}

public interface IUnitOfWork
{
   void Commit();
}

public class UnitOfWork : IDbContext, IUnitOfWork
{
   public void Add<T>(T entity);
   public void Commit();
}

public class RepositoryBase<T>
{
    private IDbContext _c;

    public RepositoryBase(IDbContext c) 
    {
       _c = c;
    }

    public void Add(T entity)
    {
       _c.Add(entity)
    }
}
公共接口IDbContext
{
无效添加(T实体);
}
公共接口工作单元
{
无效提交();
}
公共类UnitOfWork:IDbContext,IUnitOfWork
{
公共无效添加(T实体);
公共无效提交();
}
公共类RepositoryBase
{
私人环境(c);
公共存储库(IDBC)
{
_c=c;
}
公共无效添加(T实体)
{
_c、 添加(实体)
}
}
编辑

发布后,我重新思考了一下。在
UnitOfWork
实现中公开Add方法意味着它是这两种模式的组合

我在自己的代码中使用实体框架,其中使用的框架被描述为“工作单元和存储库模式的组合”

我认为最好将两者分开,这意味着我需要在
DbContext
周围使用两个包装器,一个用于工作单元位,另一个用于存储库位。我在
RepositoryBase
中进行存储库包装

关键的区别在于,我没有将
工作单元
传递给存储库,而是传递了
DbContext
。这意味着
BaseRepository
可以访问
DbContext
上的
SaveChanges
。由于目的是自定义存储库应该继承
BaseRepository
,因此他们也可以访问
DbContext
。因此,开发人员可能会在使用该
DbContext
的自定义存储库中添加代码。所以我想我的“包装器”有点泄漏

那么,是否值得为
DbContext
创建另一个包装器,该包装器可以传递给存储库构造函数以关闭它呢?不确定它是否

传递DbContext的示例:


是的,这个问题是我关心的,下面是我如何处理它的

首先,在我的理解中,域模型不应该知道工作单元。域模型由接口(或抽象类)组成,并不意味着事务存储的存在。事实上,它根本不知道任何存储的存在。因此,术语域模型

工作单元存在于域模型实现层中。我想这是我的术语,我指的是通过合并数据访问层来实现域模型接口的层。通常,我使用ORM作为DAL,因此它带有内置UoW(实体框架SaveChanges或SubmitChanges方法来提交挂起的更改)。但是,该方法属于DAL
public interface ITable1 : IGenericRepository<table1>
{
}
public class Table1Repository : GenericRepository<table1>, ITable1
{
  private MyDatabase _context;

  public Table1Repository(MyDatabase context) : base(context)
  {
    _context = context;
  }
} 
public interface IUnitOfWork
{
  ITable1 table1 {get;}
  ...
  ...
  list all other repository interfaces here.

  void SaveChanges();
} 

public class UnitOfWork : IUnitOfWork
{
  private readonly MyDatabase _context;
  public ITable1 Table1 {get; private set;}

  public UnitOfWork(MyDatabase context)
  {
    _context = context; 

    // Initialize all of your repositories here
    Table1 = new Table1Repository(_context);
    ...
    ...
  }

  public void SaveChanges()
  {
    _context.SaveChanges();
  }
}
public class DefaultController : Controller
{
  protected IUnitOfWork UoW;

  protected override void OnActionExecuting(ActionExecutingContext filterContext)
  {
    UoW = new UnitOfWork(new MyDatabase());
  }

  protected override void OnActionExecuted(ActionExecutedContext filterContext) 
  {
    UoW.SaveChanges();
  }
}
interface IEntity
{
    int Id {get;set;}
}

interface IUnitOfWork()
{
    void RegisterNew(IRepsitory repository, IEntity entity);
    void RegisterDirty(IRepository respository, IEntity entity);
    //etc.
    bool Commit();
    bool Rollback();
}

interface IRepository<T>() : where T : IEntity;
{
    void Add(IEntity entity, IUnitOfWork uow);
    //etc.
    bool CanCommit(IUnitOfWork uow);
    void Commit(IUnitOfWork uow);
    void Rollback(IUnitOfWork uow);
}
// ...
var uow = new MyUnitOfWork();

repo1.Add(entity1, uow);
repo2.Add(entity2, uow);
uow.Commit();
public class CustomerController : Controller
{
    private readonly CustomerContext context; // injected

    [HttpPost]
    public IActionResult Update(CustomerUpdateDetails viewmodel)
    {
        // [Repository] acting like an in-memory domain object collection
        var person = context.Person.Find(viewmodel.Id);

        // [UnitOfWork] keeps track of everything you do during a business transaction
        person.Name = viewmodel.NewName;
        person.AnotherComplexOperationWithBusinessRequirements();

        // [UnitOfWork] figures out everything that needs to be done to alter the database
        context.SaveChanges();
    }
}
public class PersonRepository
{
    private readonly PersonDbContext context;
    public Person GetClosestTo(GeoCoordinate location) {} // redacted
}
public class PersonController
{
    private readonly PersonRepository repository;
    private readonly PersonDbContext context; // requires to Equals repository.context

    public IActionResult Action()
    {
        var person = repository.GetClosestTo(new GeoCoordinate());
        person.DoSomething();
        context.SaveChanges();
        // repository.SaveChanges(); would save the injection of the DbContext
    }
}
public class PersonEntity
{
    [Key]
    public int Id { get; set; }
    public string Name { get; set; }
    public bool IsValid { get; set; }
    public ICollection<AddressEntity> Addresses { get; set; }
}

public class AddressEntity
{
    [Key]
    public int Id { get; set; }
    public string Value { get; set; }
    public DateTime Since { get; set; }
    public PersonEntity Person { get; set; }
}

public class Person
{
    public int Id { get; set; }
    public string Name { get; set; }
    public string CurrentAddressValue { get; private set; }
}
public class Person
{
    public void EnsureEnrollable(IPersonRepository repository)
    {
        if(!repository.IsEnrollable(this))
        {
            throw new BusinessException<PersonError>(PersonError.CannotEnroll);
        }
    }
}
public class PersonRepository : IPersonRepository
{
    private readonly PersonDbContext context;

    public IEnumerable<Person> GetAll()
    {
        return context.Persons.AsNoTracking()
            .Where(person => person.Active)
            .ProjectTo<Person>().ToList();
    }

    public Person Enroll(Person person)
    {
        person.EnsureEnrollable(this);
        context.Persons.Find(person.Id).Active = true;
        context.SaveChanges(); // UPDATE statement
        return person;
    }

    public bool IsEnrollable(Person person)
    {
        return context.Persons.Any(entity => entity.Id == person.Id && !entity.Active);
    }
}