使用Microsoft.AspNetCore.Identity与EF Core调用UserManager.ChangePasswordAsync重置用户密码时遭遇实体跟踪冲突问题
Problem Description
I'm trying to implement password reset using
UserManager.ChangePasswordAsync, but I'm hitting this EF Core tracking error:The instance of entity type 'ApplicationUser' 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.Here's my core code:
public async Task<ActionResult> ChangePasswordAsync(ChangePwdInfo info) { var result = await _userManager.ChangePasswordAsync(CurrentUser, info.PasswordCurrent, info.Password); if (result.Succeeded) { LogInformation("Password changed successfully"); return Ok(); } var err = result.Errors.FirstOrDefault(); if (err?.Code == "PasswordMismatch") { return SystemInfo("Current Password was not correct", $"Change password called with incorrect current password"); } return SystemError($"Password change {result}: ", $"Change password failed {result.Errors.FirstOrDefault()?.Description}"); }I know the issue is that
_userManageris trying to track theApplicationUserentity, but I don't need tracking in this scenario. Unfortunately,UserManagerdoesn't have an.AsNoTracking()option. I have access to the shareddbContextthat_userManageruses, and I tried callingdbContext.ChangeTracker.Clear()afterCheckPasswordSignInAsync, but that didn't fix the problem. How can I make EF Core not track theApplicationUserin this scenario?
Solutions to Resolve the Tracking Conflict
Let's walk through a few reliable fixes for this issue:
1. Detach the Tracked CurrentUser Instance Explicitly
If CurrentUser is already being tracked by your DbContext, set its state to Detached before passing it to ChangePasswordAsync. This removes it from the tracker so UserManager can handle its own instance without conflicts:
public async Task<ActionResult> ChangePasswordAsync(ChangePwdInfo info) { // Detach the currently tracked user to avoid conflicts var userEntry = _dbContext.Entry(CurrentUser); if (userEntry.State != EntityState.Detached) { userEntry.State = EntityState.Detached; } var result = await _userManager.ChangePasswordAsync(CurrentUser, info.PasswordCurrent, info.Password); // ... rest of your existing code }
This works because CurrentUser is no longer tracked, so when UserManager internally queries for the user to validate and update, it won't hit a duplicate tracking instance.
2. Use a "Stub" User Instance Instead of the Tracked CurrentUser
Create a lightweight "stub" entity with only the user's Id (the only required property for ChangePasswordAsync). This stub won't be tracked by EF Core, eliminating overlap with existing tracked instances:
public async Task<ActionResult> ChangePasswordAsync(ChangePwdInfo info) { // Create a minimal stub user with just the Id var stubUser = new ApplicationUser { Id = CurrentUser.Id }; var result = await _userManager.ChangePasswordAsync(stubUser, info.PasswordCurrent, info.Password); // ... rest of your existing code }
UserManager's ChangePasswordAsync will internally fetch the full user record using the stub's Id, and since the stub isn't tracked, there's no conflict.
3. Fetch a Fresh Non-Tracked User Instance
Use your DbContext to get a fresh, non-tracked copy of the user and pass that to ChangePasswordAsync:
public async Task<ActionResult> ChangePasswordAsync(ChangePwdInfo info) { // Retrieve a non-tracked user instance var nonTrackedUser = await _dbContext.Users.AsNoTracking() .FirstOrDefaultAsync(u => u.Id == CurrentUser.Id); if (nonTrackedUser == null) { return NotFound("User not found"); } var result = await _userManager.ChangePasswordAsync(nonTrackedUser, info.PasswordCurrent, info.Password); // ... rest of your existing code }
This ensures the user instance passed to UserManager isn't tracked by your DbContext, so there's no conflict when UserManager processes it.
Why ChangeTracker.Clear() Might Have Failed
Calling dbContext.ChangeTracker.Clear() removes all tracked entities, but if CurrentUser was re-referenced or re-tracked after that call (e.g., from a request-scoped variable or another service), the conflict would reoccur. The solutions above target the specific user instance involved in the password change, making them more reliable for this scenario.
内容的提问来源于stack exchange,提问作者Peter Kerr

