ASP.NET Core7单元测试遇EF Core实体跟踪冲突异常
问题描述
我使用ASP.NET Core 7结合NSubstitute、Xunit和Dapper开发,业务代码与测试代码如下:
业务方法
public async Task UpdateStatus(AuthorityStatusUpdateInputDto input) { int tenantId = AbpSession.GetTenantId(); using var connection = _dapperContextBuilder.CreateConnection(); var query = _dapperContextBuilder.QueryBuilder(new {Id = input.Id, TenantId = tenantId}, "SELECT TOP 1 * FROM Authority"); Authority authority = await connection.QueryFirstOrDefaultAsync<Authority>(query.Item1, query.Item2); authority.Status = input.Status; await _authorityRepository.UpdateAsync(authority); }
IDapperContextBuilder接口
public interface IDapperContextBuilder { IDbConnection CreateConnection(); (string, DynamicParameters) QueryBuilder(object conditions, string query); }
测试代码
通过NSubstitute.DbConnection库模拟IDbConnection,使用SQLite内存数据库进行单元测试:
private readonly IDbConnection _fakedbConnection; public AppService_Tests() { _fakedbConnection = Substitute.For<IDbConnection>().SetupCommands(); } [Fact] public async Task Should_UpdateStatus_As_Edit() { // Arrange int id = 1001; _tenantId = AbpSession.TenantId ?? 0; _userId = AbpSession.UserId ?? 0; string query = "SELECT TOP 1 * FROM Authority WHERE Id = @Id AND TenantId = @TenantId AND IsDeleted = 0"; var connectionFactory = Substitute.For<IDapperContextBuilder>(); connectionFactory.CreateConnection().Returns(_fakedbConnection); UsingDbContext(async context => { CreateAuthorities(context, _tenantId, _userId); // Simulate Dapper behavior var item = context.Authority.FirstOrDefault(x=> x.Id == id); _fakedbConnection.SetupQuery(query).Returns(item); }); var dto = new AuthorityStatusUpdateInputDto() { Id = id, Status = AuthorityStatus.Edit }; await _authorityAppService.UpdateStatus(dto); //Asserts }
异常信息
测试中从DbContext获取的Authority实体被传入Dapper模拟查询返回,但调用UpdateAsync时抛出异常,WebAPI运行时无此问题:
System.InvalidOperationException : The instance of entity type 'Authority' cannot be tracked because another instance with the same key value for {'Id'} is already being tracked. When attaching existing entities, ensure that only one entity instance with a given key value is attached. Consider using 'DbContextOptionsBuilder.EnableSensitiveDataLogging' to see the conflicting key values. at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.IdentityMap`1.ThrowIdentityConflict(InternalEntityEntry entry) at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.IdentityMap`1.Add(TKey key, InternalEntityEntry entry, Boolean updateDuplicate) at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.StateManager.StartTracking(InternalEntityEntry entry) at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.InternalEntityEntry.SetEntityState(EntityState oldState, EntityState newState, Boolean acceptChanges, Boolean modifyProperties) at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.InternalEntityEntry.SetEntityState(EntityState entityState, Boolean acceptChanges, Boolean modifyProperties, Nullable`1 forceStateWhenUnknownKey, Nullable`1 fallbackState) at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.EntityGraphAttacher.PaintAction(EntityEntryGraphNode`1 node) at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.EntityEntryGraphIterator.TraverseGraph[TState](EntityEntryGraphNode`1 node, Func`2 handleNode) at Microsoft.EntityFrameworkCore.ChangeTracking.Internal.EntityGraphAttacher.AttachGraph(InternalEntityEntry rootEntry, EntityState targetState, EntityState storeGeneratedWithKeySetTargetState, Boolean forceStateWhenUnknownKey) at Microsoft.EntityFrameworkCore.Internal.InternalDbSet`1.SetEntityState(InternalEntityEntry entry, EntityState entityState) at Microsoft.EntityFrameworkCore.Internal.InternalDbSet`1.Attach(TEntity entity) at Abp.EntityFrameworkCore.Repositories.EfCoreRepositoryBase`3.AttachIfNot(TEntity entity) at Abp.EntityFrameworkCore.Repositories.EfCoreRepositoryBase`3.Update(TEntity entity) at Abp.EntityFrameworkCore.Repositories.EfCoreRepositoryBase`3.UpdateAsync(TEntity entity) at LegalRegTech.MasterData.AuthorityAppService.SetAsDraft(Authority authority, AuthorityStatusUpdateInputDto input) in c:\6clicks\src\LegalRegTech.Application\MasterData\AuthorityAppService.cs:line 967 at LegalRegTech.MasterData.AuthorityAppService.UpdateStatus(AuthorityStatusUpdateInputDto input) in c:\6clicks\src\LegalRegTech.Application\MasterData\AuthorityAppService_Tests.cs:line 639 at LegalRegTech.Tests.Authority.AuthorityAppService_Tests.Should_UpdateStatus_As_Edit() in c:\6clicks\test\LegalRegTech.Tests\Authority\AuthorityAppService_Tests.cs:line 1042 --- End of stack trace from previous location ---
解决方案
问题根源:测试中从UsingDbContext里获取的Authority实体被当前DbContext跟踪,业务代码调用_authorityRepository.UpdateAsync时,ABP的EF Core仓库会尝试将这个已被跟踪的实体再次附加到同一DbContext实例,导致跟踪冲突。
以下是三种可行的解决方式:
1. 解除实体的DbContext跟踪
修改测试中获取实体的代码,调用Detach解除跟踪后再返回给模拟查询:
UsingDbContext(async context => { CreateAuthorities(context, _tenantId, _userId); var item = context.Authority.FirstOrDefault(x=> x.Id == id); // 解除实体跟踪 context.Entry(item).State = EntityState.Detached; _fakedbConnection.SetupQuery(query).Returns(item); });
业务代码拿到的实体处于未跟踪状态,仓库执行Update时可正常附加。
2. 使用未被跟踪的实体副本
克隆一个新的实体实例返回给模拟查询,避免使用被DbContext跟踪的原实例:
UsingDbContext(async context => { CreateAuthorities(context, _tenantId, _userId); var item = context.Authority.FirstOrDefault(x=> x.Id == id); // 克隆实体(手动赋值、AutoMapper或序列化反序列化均可) var detachedItem = new Authority { Id = item.Id, TenantId = item.TenantId, Status = item.Status, IsDeleted = item.IsDeleted // 复制其他必要业务属性 }; _fakedbConnection.SetupQuery(query).Returns(detachedItem); });
副本从未被DbContext跟踪,不会触发跟踪冲突。
3. 隔离测试与业务代码的DbContext实例
确保测试中创建实体的DbContext和业务代码使用的DbContext不是同一个实例,或创建实体后立即释放上下文。这种方式需要调整测试框架的UsingDbContext实现,复杂度较高,前两种方法更直接高效。
内容的提问来源于stack exchange,提问作者San Jaisy
相关产品推荐
相关产品推荐

