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

ASP.NET Core API用户认证时的实体跟踪冲突问题咨询

解决ASP.NET Core认证中的实体跟踪冲突问题

问题场景

开发ASP.NET Core认证API时,调用CheckUserWithPassword方法完成用户认证时出现实体跟踪冲突错误,核心代码及配置如下:

AccountController中的Token方法

[HttpPost]
[Route("/api/login")]
[Consumes("application/json")]
public async Task<IActionResult> Token(LoginDto postModel)
{
    var userResult = await CheckUserWithPassword(postModel);
    // 其余实现逻辑
}

private async Task<int> CheckUserWithPassword(LoginDto model)
{
    var user = await _userService.GetUser(model.Username);
    if (user == null)
        throw new ClientSideException("Kullanıcı Bulunamadı");

    if (!user.IsActive)
        throw new ClientSideException("Kullanıcı aktif değil.");

    try
    {
        byte[] bytes = Convert.FromBase64String(model.Password);
        var passClean = Encoding.ASCII.GetString(bytes);
        var result = await _signInManager.CheckPasswordSignInAsync(user, passClean, false);
        // 后续认证逻辑
    }
    catch (Exception ex)
    {
        // 异常处理
    }
}

AppUserService中的GetUser方法

public async Task<AppUser> GetUser(string userName)
{
    return await _context.AppUsers
            .Include(u => u.Client)
            .Where(u => u.Email == userName)
            .AsNoTracking()
            .FirstOrDefaultAsync();
}

依赖注入配置

public interface IAppUserService : IService<AppUser>
{
    Task<AppUser> GetUser(string userName);
}

public class AppUserService : Service<AppUser>, IAppUserService
{
    // 实现细节
}

public class RepoServiceModule : Module
{
    protected override void Load(ContainerBuilder builder)
    {
        builder.RegisterGeneric(typeof(GenericRepository<>)).As(typeof(IGenericRepository<>)).InstancePerLifetimeScope();
        builder.RegisterGeneric(typeof(Service<>)).As(typeof(IService<>)).InstancePerLifetimeScope();
        builder.RegisterType<UnitOfWork>().As<IUnitOfWork>();

        var apiAssembly = Assembly.GetExecutingAssembly();
        var repoAssembly = Assembly.GetAssembly(typeof(AppDbContext));
        var serviceAssembly = Assembly.GetAssembly(typeof(MappingProfile));

        builder.RegisterAssemblyTypes(apiAssembly, repoAssembly, serviceAssembly).Where(x => x.Name.EndsWith("Repository")).AsImplementedInterfaces().InstancePerLifetimeScope();
        builder.RegisterAssemblyTypes(apiAssembly, repoAssembly, serviceAssembly).Where(x => x.Name.EndsWith("Service")).AsImplementedInterfaces().InstancePerLifetimeScope();
    }
}

// Program.cs 中的配置
builder.Services.AddScoped<DbContext,AppDbContext>();
builder.Services.AddFluentValidationAutoValidation();

builder.Host.UseServiceProviderFactory(new AutofacServiceProviderFactory());
builder.Host.ConfigureContainer<ContainerBuilder>(containerBuilder => containerBuilder.RegisterModule(new RepoServiceModule()));

错误信息

The instance of entity type 'AppUser' 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.

错误原因

冲突源于AsNoTracking()与SignInManager.CheckPasswordSignInAsync()的交互逻辑:

  1. GetUser方法通过AsNoTracking()获取了未被DbContext跟踪的AppUser实例
  2. CheckPasswordSignInAsync内部会通过Identity的UserManager查询同一用户,默认返回被跟踪的实例
  3. 同一个DbContext生命周期内,出现两个同Key的AppUser实例(一个未跟踪、一个被跟踪),触发跟踪冲突

解决方案建议

方案1:移除GetUser中的AsNoTracking()

让获取的用户实例被DbContext跟踪,这样SignInManager内部查询时会复用已跟踪的实例,避免冲突。修改GetUser方法:

public async Task<AppUser> GetUser(string userName)
{
    return await _context.AppUsers
            .Include(u => u.Client)
            .Where(u => u.Email == userName)
            // 移除AsNoTracking()
            .FirstOrDefaultAsync();
}

方案2:使用Identity的UserManager查询用户

放弃自定义的_userService.GetUser,直接用Identity组件提供的UserManager.FindByEmailAsync获取用户,确保实例与SignInManager使用的DbContext跟踪状态一致:

private async Task<int> CheckUserWithPassword(LoginDto model)
{
    // 直接用UserManager查询用户
    var user = await _userManager.FindByEmailAsync(model.Username);
    if (user == null)
        throw new ClientSideException("Kullanıcı Bulunamadı");

    if (!user.IsActive)
        throw new ClientSideException("Kullanıcı aktif değil.");

    try
    {
        byte[] bytes = Convert.FromBase64String(model.Password);
        var passClean = Encoding.ASCII.GetString(bytes);
        var result = await _signInManager.CheckPasswordSignInAsync(user, passClean, false);
        // 后续认证逻辑
    }
    catch (Exception ex)
    {
        // 异常处理
    }
}

这种方式符合ASP.NET Core Identity的设计规范,能从根源避免跟踪冲突。

方案3:手动处理实体跟踪状态(适合必须保留AsNoTracking的场景)

如果业务需求必须使用AsNoTracking(),可在调用CheckPasswordSignInAsync前,手动将未跟踪的实例附加到DbContext并设置状态:

private async Task<int> CheckUserWithPassword(LoginDto model)
{
    var user = await _userService.GetUser(model.Username);
    if (user == null)
        throw new ClientSideException("Kullanıcı Bulunamadı");

    if (!user.IsActive)
        throw new ClientSideException("Kullanıcı aktif değil.");

    // 检查DbContext是否已跟踪该用户,未跟踪则附加并设置为Unchanged
    var trackedUser = _context.ChangeTracker.Entries<AppUser>().FirstOrDefault(e => e.Entity.Id == user.Id);
    if (trackedUser == null)
    {
        _context.Attach(user).State = EntityState.Unchanged;
    }

    try
    {
        byte[] bytes = Convert.FromBase64String(model.Password);
        var passClean = Encoding.ASCII.GetString(bytes);
        var result = await _signInManager.CheckPasswordSignInAsync(user, passClean, false);
        // 后续认证逻辑
    }
    catch (Exception ex)
    {
        // 异常处理
    }
}

注意:需要将DbContext注入到控制器中才能执行这段代码。


内容的提问来源于stack exchange,提问作者Goktug

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.26 21:27:02