在 ABP vNext 中编写仓储单元测试的问题一则
一、问题
新项目是基于 abp vnext 框架进行开发的,所以我要求为每层编写单元测试。在同事为某个仓储编写单元测试的时候,发现了一个奇怪的问题。他的对某个聚合根的 a 字段进行了更新,随后对某个导航属性 b 也进行了变更,最后通过仓储提供的 updateasync()
方法对变更的数据进行持久化。
结果再次查出来的时候,发现聚合根的 a 字段倒是更新了,但是导航属性 b 的内部字段没有进行变更。例如在下面的实例当中,聚合根的 name
字段变更成功,但是导航属性的 street
字段变更失败了。
二、原因
数据没有更新到,说明问题肯定出在 updateasync
方法内部,通过打断点单步步入之后,也没发现有什么奇怪的地方,是使用的 abp vnext 提供的默认仓储实现。
又在想是否跟实体追踪有关,然后看同事写得单元测试代码,发现他是先使用的 getasync()
方法获取到实体,然后手动变更了实体的属性。变更完成之后,通过仓储提供的 updateasync()
方法进行更新。
看了很久发现它们并不是公用的一个工作单元,这就导致 getasync()
和 updateasync()
方法内部得到的 dbcontext
是不一样的。在 ef core 内部针对这种情况,称之为 disconnected entities 即断开连接的实体,这个时候需要用户手动 attch 追踪导航属性。
三、解决
所以有两种解决办法,第一种方法是保证使用 getasync()
和 updateasync()
方法时,它们都处于一个工作单元下,例如下面的伪代码。
private readonly iunitofworkmanager _uowmgr; private readonly irepository<testuser, guid> _repository; [fact] public async task resolve1() { // 创建初始数据。 var entityid = guid.newguid(); await _repository.insertasync(new testuser { id = entityid, name = "张三", address = new testuseraddress { city = "成都市", street = "春熙路" } }); using (var outeruow = _uowmgr.begin()) { var entity = await _repository.getasync(entityid); entity.name = "李四"; entity.address.street = "琴台路"; await _repository.updateasync(entity); await outeruow.completeasync(); } // 最后查询街道是否成功修改。 var result = await _repository.getasync(entityid); result.name.shouldbe("李四"); result.address.street.shouldbe("琴台路"); }
第二种方法变动则要大一些, 导航属性没有更新的根本原因,是因为在第二个工作单元中没有追踪到这个属性,你只需要手动附加该导航属性即可。在下面的例子中,我们重写了 updateasync()
方法,手动跟踪导航属性,也能够达到上述效果。
public class testuserrepository : efcorerepository<xxxdbcontext,testuser,guid> { public testuserrepository(idbcontextprovider<xxxdbcontext> dbcontextprovider) : base(dbcontextprovider) { } public override iqueryable<testuser> withdetails() { return getqueryable().include(x => x.address); } public override task<testuser> updateasync(testuser entity, bool autosave = false, cancellationtoken cancellationtoken = new cancellationtoken()) { dbcontext.attach(entity.address).state = entitystate.modified; return base.updateasync(entity, autosave, cancellationtoken); } }