我有一个最初为 .NET 6 编写并使用 WebAssembly 的 Blazor 项目。
我已将其迁移到 .NET 8 - 仍然使用 WebAssembly,但已决定使用新的“InteractiveServer”渲染模式将许多“管理”功能移回服务器。这很适合并且效果很好。
我有很多与服务器(标准控制器)交互的服务,但为了使交互式服务器和 WebAssembly 都能正常工作,有必要将服务放入接口中,并在服务器上拥有类似的(接口)服务来绕过控制器直接进入数据访问层。
这项工作大部分是我完成的。但我必须做的一件事是将应用程序
DbContext
转换为使用 DbContextFactory
。本博客和其他博客上的各种 Blazor 帖子对此进行了详细描述。为了避免这样的错误:
在上一个操作完成之前,在此上下文实例上启动了第二个操作。这通常是由于不同线程同时使用同一个 DbContext 实例导致的
我现在收到这样的 EF Core 跟踪错误:
无法跟踪实体类型“ApplicationUser”的实例,因为已跟踪具有相同键值 {'Id'} 的另一个实例。附加现有实体时,请确保仅附加一个具有给定键值的实体实例。考虑使用“DbContextOptionsBuilder.EnableSensitiveDataLogging”来查看冲突的键值
奇怪的是我从服务中调用 DAL,如下所示:
public async Task<ApplicationUserDTO> UpdateUser(ApplicationUserDTO user)
{
using var _dbContext = _dbContextFactory.CreateDbContext();
bool updatingcurrrentuser = false;
if (user.Id == CurrentUser.Id)
updatingcurrrentuser = true;
InfoBool? result = new();
bool userisAdmin = false;
ApplicationUser? loggedinUser = await _userLib.GetLoggedInUser(_dbContext);
if (_userLib.UserIsAdmin(loggedinUser))
{
userisAdmin = true; // This is to allow admin users to add paper tickets which can override the ticket price check
}
if (userisAdmin || (user.Id == loggedinUser.Id)) // If the logged in user is an admin or the person edited then allow the update
{
// ApplicationUser sentUser = new ApplicationUser();
// _mapper.Map(sentUserDTO, sentUser);
result = await _userLib.UpdateUser(_userManager, _dbContext, user, user.Roles, userisAdmin);
}
// ... more stuff
}
UpdateUser
函数相当大,但可以处理很多东西:
public async Task<InfoBool> UpdateUser(
UserManager<ApplicationUser> userManager,
RGDbContext context,
ApplicationUserDTO user,
List<string> Roles = null,
bool userisadmin = false)
{
// First check if its there
// ApplicationUser founduser = await userManager.FindByNameAsync(user.UserName);
ApplicationUser? founduser = await context.Users
.Include(x => x.Address)
//.Include(x => x.Player) // Don't need the player
//.ThenInclude(y => y.Coins)
.SingleOrDefaultAsync(x => x.UserName == user.UserName);
if (founduser == null)
{
return new InfoBool(false, "User to update not found");
}
if (founduser.Address != null && user.Address != null && user.Address.Id == 0)
{
// If there is an address but the client sends it with a zero id - to detect a client error
return new InfoBool(false, "Address Id mismatch Error");
}
_mapper.Map(user, founduser);
_mapper.Map(user.Address, founduser.Address);
IList<string> currentroles = await GetRolesAsync(context, founduser.Id);
// First check for changed roles
// Creates a list of the roles in Roles to add not in current roles
List<string> newrolestoadd = Roles.Except(currentroles).ToList();
// Create a list of the roles in current roles to remove not in roles
List<string> rolestoremove = currentroles.Except(Roles).ToList();
if (newrolestoadd.Count() > 0) // There are some
{
// If a player role has been added
if (!currentroles.Contains(Enums.AllowedRoles.Player.ToString())
&& Roles.Contains(Enums.AllowedRoles.Player.ToString()))
{
founduser.IsPlayer = true;
}
if (!userisadmin && newrolestoadd.Contains(Enums.AllowedRoles.Administrator.ToString()))
{
// Just a check that any user adding an adminrole is already an admin
newrolestoadd.Remove(Enums.AllowedRoles.Administrator.ToString()); //Not allowed
// TODO: add a log record here to indicate a possible security risk
}
await AddUsertoRoles(userManager, founduser, newrolestoadd);
}
// Remove roles
if (rolestoremove.Count() > 0)
{
if (!userisadmin && rolestoremove.Contains(Enums.AllowedRoles.Administrator.ToString()))
{
// Only an admin can remove an admin
rolestoremove.Remove(Enums.AllowedRoles.Administrator.ToString());
}
await RemoveUserFromRoles(userManager, founduser, rolestoremove);
}
// Now check the address (we don't check for changes IN the address, just if it exists.
try
{
context.Entry(founduser).State = EntityState.Modified;
await context.SaveChangesAsync();
}
catch (Exception e)
{
return new InfoBool(false, e.Message);
}
return new InfoBool(true, "User update successful");
}
最初在添加或删除角色时,跟踪错误发生在 UserLib 的内部:
private async Task<IdentityResult> RemoveUserFromRoles(
UserManager<ApplicationUser> userManager,
ApplicationUser User,
IList<string> RolesToRemove)
{
try
{
IdentityResult result = IdentityResult.Success;
foreach (string role in RolesToRemove)
{
if (await userManager.IsInRoleAsync(User, role))
{
result = await userManager.RemoveFromRoleAsync(User, role);
if (!result.Succeeded) //If not successful the bail with the error
{
return result;
}
}
}
return result;
}
catch (Exception ex)
{
Console.WriteLine( ex.Message );
return IdentityResult.Failed(new IdentityError() { Code="Exception", Description=$"{ex.Message}"});
}
}
我在这里仅使用 userManager,正如您在这些函数中所看到的,上下文和 userMananger 都在顶部(UpdateUser)函数中传入。奇怪的是,当我从控制器调用相同的函数时。它不会发生并且工作正常。 为了完整起见,控制器代码是:
public async Task<ActionResult<InfoBool>> UpdateUser(ApplicationUserDTO sentUserDTO)
{
if (!ModelState.IsValid)
{
return BadRequest(ModelState);
}
// Get the logged in user.
ApplicationUser loggedinUser = await _userManager.GetUserAsync(User).ConfigureAwait(false);
IList<string> roles = await _userManager.GetRolesAsync(loggedinUser);
bool userisadmin = false;
if (roles.Contains(Enums.AllowedRoles.Administrator.ToString())) userisadmin = true;
if (userisadmin || (sentUserDTO.Id == loggedinUser.Id)) // If the logged in user is an admin or the person edited then allow the update
{
return await _userlib.UpdateUser(_userManager, _context, sentUserDTO, sentUserDTO.Roles, userisadmin);
}
return Unauthorized();
}
我的猜测是,我在服务器端服务代码中使用 contextFactory 意味着它与 userManager 使用的上下文不同。 然而,控制器中的上下文只是启动时在program.cs 中创建的标准上下文。我在这里缺少什么?
编辑 添加 GetLoggedInUser 函数。我认为您不能搜索 AsNotTracked 项目,但将其分离应该可以,不是吗?
///
/// Uses the ihttpContext to get the current user without using user manager
/// DO NOT USE the value returned to update the DB as it has been detached from the context
///
public async Task<ApplicationUser> GetLoggedInUser(RGDbContext context)
{
var claimslist = _httpContextAccessor.HttpContext.User.Claims.ToList();
string userId = claimslist.Find(x => x.Type.EndsWith("nameidentifier")).Value;
ApplicationUser? user = await context.Users.FindAsync(userId);
if (user == null) return null;
foreach (var claim in claimslist)
{
if (claim.Type.EndsWith("role"))
{
user.Roles.Add(claim.Value);
}
}
context.Entry(user).State = EntityState.Detached;
return user;
}
在代码中,您在
UpdateUser
中打开一个 DbContext,它在方法的生命周期内存在。 然后你开始传递它 - _userLib.GetLoggedInUser
和 _userLib.UpdateUser
。
在
UpdateUser
内,您致电 UserManager
至 RemoveUserFromRoles
。 它想要打开当前打开的相同表并进行跟踪。 这就是你得到错误的地方。
控制器为何工作? 不知道,但没有迹象表明
_context
来自哪里。 它可能是应用程序共享的单个上下文。
如何解决。 整理一下你的逻辑。
不要传递上下文。 打开和关闭上下文作为方法内的事务。
在
UpdateUser
中打开和关闭上下文,并关闭跟踪以读取用户信息。
在您的
_userLib
方法中执行相同的操作。
在
_userLib.UpdateUser
中,在调用 UserManager
方法之前打开更新并关闭上下文。