You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.13 23:18:10