在 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 _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
{
    public TestUserRepository(IDbContextProvider dbContextProvider) : base(dbContextProvider)
    {
    }

    public override IQueryable WithDetails()
    {
        return GetQueryable().Include(x => x.Address);
    }

    public override Task UpdateAsync(TestUser entity, bool autoSave = false, CancellationToken cancellationToken = new CancellationToken())
    {
        DbContext.Attach(entity.Address).State = EntityState.Modified;
        return base.UpdateAsync(entity, autoSave, cancellationToken);
    }
}

四、参考资料

  • StackOverflow - Entity Framework disconnected graph and navigation property
  • MSDN - Disconnected entities