天天看點

在 ABP vNext 中編寫倉儲單元測試的問題一則

一、問題

新項目是基于 ABP vNext 架構進行開發的,是以我要求為每層編寫單元測試。在同僚為某個倉儲編寫單元測試的時候,發現了一個奇怪的問題。他的對某個聚合根的 A 字段進行了更新,随後對某個導航屬性 B 也進行了變更,最後通過倉儲提供的

UpdateAsync()

方法對變更的資料進行持久化。

結果再次查出來的時候,發現聚合根的 A 字段倒是更新了,但是導航屬性 B 的内部字段沒有進行變更。例如在下面的執行個體當中,聚合根的

Name

字段變更成功,但是導航屬性的

Street

字段變更失敗了。

在 ABP vNext 中編寫倉儲單元測試的問題一則

二、原因

資料沒有更新到,說明問題肯定出在

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("琴台路");
}
           
在 ABP vNext 中編寫倉儲單元測試的問題一則

第二種方法變動則要大一些, 導航屬性沒有更新的根本原因,是因為在第二個工作單元中沒有追蹤到這個屬性,你隻需要手動附加該導航屬性即可。在下面的例子中,我們重寫了

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);
    }
}
           
在 ABP vNext 中編寫倉儲單元測試的問題一則

四、參考資料

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