一、問題 新項目是基於 ABP vNext 框架進行開發的,所以我要求為每層編寫單元測試。在同事為某個倉儲編寫單元測試的時候,發現了一個奇怪的問題。他的對某個聚合根的 A 欄位進行了更新,隨後對某個導航屬性 B 也進行了變更,最後通過倉儲提供的 方法對變更的數據進行持久化。 結果再次查出來的時候,發 ...
一、問題
新項目是基於 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