Quantcast
Channel: Active questions tagged blazor - Stack Overflow
Viewing all articles
Browse latest Browse all 4839

Using .NET 8 with EF Core and Identity. Which DbContext does UserManagerUse? Strange error with UserManager

$
0
0

I have a Blazor project that was originally written for .NET 6 and used WebAssembly.

I have migrated it to .NET 8 - still with WebAssembly, but have resolved to move a lot of my 'Admin' functions back to the server using the new 'InteractiveServer' render mode. Which will suit and works great.

I have a bunch of services that interact with the server (standard controllers) but to make this work correctly for BOTH Interactive server and webassembly, it was necessary to make the services into Interfaces and have similar (interface) services on the server that bypass the controller going direct to the data access layer.

I have made this work mostly. But one of the things I had to do was to convert the application DbContext to use the DbContextFactory. This is well described in various Blazor posts on this and other blogs. To avoid errors like this:

A second operation was started on this context instance before a previous operation completed. This is usually caused by different threads concurrently using the same instance of DbContext

I am now getting EF Core tracking errors like this:

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. Consider using 'DbContextOptionsBuilder.EnableSensitiveDataLogging' to see the conflicting key values

The strange thing is I am calling the DAL from the service as follows:

// NOW CHANGED SEE *EDIT* BELOWpublic 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}

The UpdateUser function is quite big but handles a lot of stuff:

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");}

The tracking error is happening further down in the bowels of the UserLib initially here on adding or removing roles:

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}"});            }        }

I am only using userManager here and as you can see for these functions, both the context and userMananger are passed in at the top (UpdateUser) function. The STRANGE thing is that when I call the same function from the controller. It doesn't happen and works fine.For completeness the controller code is:

        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();        }

My guess is that the fact that I am using a contextFactory in the server side Service code means somehow that it is different from the context used by the userManager.However, the context in the controllers is just the standard one created in the program.cs at startup. What am I missing here?

EDIT (OTHER EDITS REMOVED TO AVOID CONFUSION)

Ok. After looking at the advice in the answers and comments, I have tried removing the context and userManager from the calling code - so the UserService code now looks like this:

        public async Task<ApplicationUserDTO> UpdateUser(ApplicationUserDTO user)        {            //using var _dbContext = _dbContextFactory.CreateDbContext();   **COMMENTED OUT - REMOVED**            bool updatingcurrrentuser = false;            if (user.Id == CurrentUser.Id) updatingcurrrentuser = true;            InfoBool? result = new();            ApplicationUser? loggedinUser = await _userLib.GetLoggedInUser();            bool userisAdmin = false;            if((await _userLib.LoggedInUserIsAdminAsync()).Success) userisAdmin = true;            if (userisAdmin || (user.Id == loggedinUser.Id)) // If the logged in user is an admin or the person edited then allow the update            {                result = await _userLib.UpdateUser(/*_userManager, *//*_dbContext,*/ user, user.Roles, userisAdmin);            }            if (result.Success)            {... more stuff

The UpdateUser function in the DAL now looks like this:

        public async Task<InfoBool> UpdateUser(            //UserManager<ApplicationUser> userManager, **NOT PASSED IN**            //RGDbContext context,            ApplicationUserDTO user,            List<string> Roles = null,            bool userisadmin = false            )        {            using var _dbContext = _contextFactory.CreateDbContext();            //First check if its there ** NOT TRACKED **            ApplicationUser? founduser = await _dbContext.Users.AsNoTracking().Include(x => x.Address)                .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);            _dbContext.Entry(founduser).State = EntityState.Modified;            await _dbContext.SaveChangesAsync();            _dbContext.Dispose();            IList<string> currentroles = await GetRolesAsync(founduser.Id);            // First check for changed roles            List<string> newrolestoadd = Roles.Except(currentroles).ToList(); // Creates a list of the roles in Roles to add not in current roles            List<string> rolestoremove = currentroles.Except(Roles).ToList(); // Create a list of the roles in current roles to remove not in roles            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( founduser, rolestoremove);            }

Completely over the top probably (disposing of the context but to prove the point)The RemoveUserFromRoles (which is the one I am testing and have temporarily commented out the add function) - now looks like this:

    private async Task<IdentityResult> RemoveUserFromRoles(       // UserManager<ApplicationUser> userManager,        ApplicationUser User,        IList<string> RolesToRemove        )    {        try        {            IdentityResult result = IdentityResult.Failed();            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}"});        }    }

The user manager is no longer passed in but injected into the lib in the usual way.Also, just in case, I have chenged the GetRolesAsync private function in the lib to just use the userManager.

        private async Task<List<string>> GetRolesAsync(/*RGDbContext context, string userID*/ ApplicationUser user)        {            //using var _context = _contextFactory.CreateDbContext();            //List<string> roles = new List<string>();            //var rolelist = await _context.UserRoles.AsNoTracking().Where(r => r.UserId == userID).ToListAsync();            //foreach (var role in rolelist)            //{            //    var foundrole = await _context.Roles.FindAsync(role.RoleId);             //    if (foundrole != null && foundrole.Name != null) roles.Add(foundrole.Name);            //}            //return roles;            var roles = (List<string>) await _userManager.GetRolesAsync(user);            return roles;        }

For completeness the GetLoggedInUser function now looks like this:

        public async Task<ApplicationUser> GetLoggedInUser()        {            using var _context = _contextFactory.CreateDbContext();            var claimslist = _httpContextAccessor.HttpContext.User.Claims.ToList();            string userId = claimslist.Find(x => x.Type.EndsWith("nameidentifier")).Value;            ApplicationUser? user = await _context.Users.AsNoTracking().Where(u => u.Id == userId).FirstOrDefaultAsync();            if (user == null) return null;            foreach (var claim in claimslist)            {                if (claim.Type.EndsWith("role"))                {                    user.Roles.Add(claim.Value);                }            }            return user;        }

Sorry for the complex edit but I wanted to show all the changes. Suffice to say the error is still there. The only up/downside is that the error also occurs when called from the controller now as well.. so it feels like I am going backwards.


Viewing all articles
Browse latest Browse all 4839

Trending Articles



<script src="https://jsc.adskeeper.com/r/s/rssing.com.1596347.js" async> </script>