1. 程式人生 > >在 ABP vNext 中編寫倉儲單元測試的問題一則

在 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);
    }
}

四、參考資料

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