在 ABP vNext 中編寫倉儲單元測試的問題一則
- 2019 年 10 月 3 日
- 筆記
一、問題
新項目是基於 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