将 .NET 8 与 EF Core 和 Identity 结合使用。 UserManager 使用哪个 DbContext? UserManager 出现奇怪的错误

问题描述 投票:0回答:1

我有一个最初为 .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;
        }
c# .net entity-framework-core blazor
1个回答
0
投票

在代码中,您在

UpdateUser
中打开一个 DbContext,它在方法的生命周期内存在。 然后你开始传递它 -
_userLib.GetLoggedInUser
_userLib.UpdateUser

UpdateUser
内,您致电
UserManager
RemoveUserFromRoles
。 它想要打开当前打开的相同表并进行跟踪。 这就是你得到错误的地方。

控制器为何工作? 不知道,但没有迹象表明

_context
来自哪里。 它可能是应用程序共享的单个上下文。

如何解决。 整理一下你的逻辑。

不要传递上下文。 打开和关闭上下文作为方法内的事务。

UpdateUser
中打开和关闭上下文,并关闭跟踪以读取用户信息。

在您的

_userLib
方法中执行相同的操作。

_userLib.UpdateUser
中,在调用
UserManager
方法之前打开更新并关闭上下文。

© www.soinside.com 2019 - 2024. All rights reserved.