Search code examples
c#repository-patternasp.net-core-2.2asp.net-core-identityef-core-2.2

unable to update user via UserManager<TUser> using ef core 2.2 in asp.net core 2.2 app


I'm trying to update the users using UserManager<TUser> but getting the following error

The instance of entity type 'UserEntity' cannot be tracked because another instance with the key value '{Id}' is already being tracked. When attaching existing entities, ensure that only one entity instance with a given key value is attached.

There are several issues in the internet with respect to above error, but none is while updating the user via UserManager.

Here is my code:

Services Configuration:

        services.AddScoped<IUserRepository, UserRepository>();
        services.AddIdentity<UserEntity, UserRoleEntity>()
                    .AddEntityFrameworkStores<GallaContext>()
                    .AddDefaultTokenProviders();

UserEntity:

    public class UserEntity : IdentityUser<Guid>
    {
        private ProfileEntity _profile;

        private UserEntity(ILazyLoader lazyLoader)
        {
            LazyLoader = lazyLoader;
        }

        private ILazyLoader LazyLoader { get; set; }

        public UserEntity()
        {

        }

        public string FirstName { get; set; }
        public string LastName { get; set; }
        public string Designation { get; set; }
        public string IdentityNumber { get; set; }
        public bool Active { get; set; }
        public DateTimeOffset CreatedAt { get; set; }
        public string CreatedBy { get; set; }
        public DateTimeOffset? ModifiedAt { get; set; }
        public string ModifiedBy { get; set; }

        public Guid ProfileId { get; set; }
        public ProfileEntity Profile { get => LazyLoader.Load(this, ref _profile); set => _profile = value; }
    }

Razor Page Handler:

        public async Task<IActionResult> OnPutAsync(
            [FromForm] UserViewModel userViewModel,
            CancellationToken ct)
        {
            try
            {
                await _user.InitializeAsync(userViewModel.Id, ct);
                var user = _mapper.Map(userViewModel, _user);

                var result = await user.SaveAsync(ct);

                return new JsonResult(new AjaxResultHelper<string>(result.Succeeded)
                {
                    Response = result.ErrorMessage ?? ResponseMessages.DataSaved
                });
            }
            catch (Exception e)
            {
                // Need to log the error

                return new JsonResult(new AjaxResultHelper<string>()
                {
                    Response = e.Message
                });
            }
        }

UserBusiness:

    public class User : CoreEntityBase
    {
        private readonly IUserRepository _userRepository;
        private readonly IMapper _mapper;

        public User(
            IUserRepository userRepository,
            IMapper mapper)
        {
            _userRepository = userRepository;
            _mapper = mapper;
        }

        public string Name => $"{FirstName} {LastName}";
        public string UserName { get; private set; }
        public string NormalizedUserName { get; private set; }
        public string Email { get; private set; }
        public string NormalizedEmail { get; private set; }
        public bool EmailConfirmed { get; private set; }
        public string PasswordHash { get; private set; }
        public string SecurityStamp { get; private set; }
        public string ConcurrencyStamp { get; private set; }
        public bool LockoutEnabled { get; private set; }

        [Sortable(Default = true)]
        [SearchableString]
        public string FirstName { get; set; }

        [Sortable]
        [SearchableString]
        public string LastName { get; set; }

        [Sortable]
        [SearchableString]
        public string Designation { get; set; }

        [Sortable]
        [SearchableString]
        public string IdentityNumber { get; set; }

        public Guid ProfileId { get; set; }

        [NestedSortable]
        [NestedSearchable]
        public Profile Profile { get; set; }

        public async Task InitializeAsync(
            Guid Id,
            CancellationToken ct)
        {
            if (Id == null)
            {
                throw new ArgumentNullException($"{nameof(Id)} cannot be null");
            }

            var user = await _userRepository.GetUserByIdAsync(Id, ct);
            _mapper.Map(user, this);
            SecurityStamp = user.SecurityStamp;
            UserName = user.UserName;
        }

        public async Task<User> GetUserByIdAsync(
            Guid userId,
            CancellationToken ct)
        {
            var user = await _userRepository.GetUserByIdAsync(userId, ct);

            return _mapper.Map<User>(user);
        }

        public async Task<(bool Succeeded, string ErrorMessage)> SaveAsync(CancellationToken ct)
        {
            if (!Validate())
            {
                return (false, Errors?.First());
            }

            //var userEntity = await _userRepository.GetUserByIdAsync(Id, ct);
            //var userEntity = _mapper.Map(this, userEntity);
            var update = await _userRepository.UpdateUserAsync(this);

            return update;
        }

        public override bool Validate()
        {
            var isValid = true;

            if (string.IsNullOrWhiteSpace(FirstName))
            {
                Errors.Add("FirstName cannot be empty");
                isValid = false;
            }

            if (string.IsNullOrWhiteSpace(LastName))
            {
                Errors.Add("LastName cannot be empty");
                isValid = false;
            }

            if (string.IsNullOrWhiteSpace(Designation))
            {
                Errors.Add("Designation cannot be empty");
                isValid = false;
            }

            return isValid;
        }
    }

UserRepository:

    public class UserRepository : IUserRepository
    {
        private readonly UserManager<UserEntity> _userManager;
        private readonly IMapper _mapper;

        public UserRepository(
            UserManager<UserEntity> userManager,
            IMapper mapper)
        {
            _userManager = userManager;
            _mapper = mapper;
        }

        public async Task<UserEntity> GetUserByIdAsync(
            Guid userId,
            CancellationToken ct)
        {
            var entity = await _userManager.Users.AsNoTracking().TagWith(nameof(GetUserIdAsync))
                .Include(u => u.Profile)
                .SingleOrDefaultAsync(x => x.Id == userId, ct);

            return entity;
        }

        public async Task<(bool Succeeded, string ErrorMessage)> UpdateUserAsync(User user)
        {
            var userEntity = _mapper.Map<UserEntity>(user);
            var result = await _userManager.UpdateAsync(userEntity);

            if (result.Succeeded)
            {
                return (true, null);
            }

            var firstError = result.Errors.FirstOrDefault()?.Description;
            return (false, firstError);
        }
    }

I'm not able to figure out where I'm wrong. I have AsNoTracking() set for _userManager.Users in GetUserByIdAsync() method; but still its not working and keep throwing the same error. Am I doing in a wrong way? Please assist on what needs to be corrected and where i'm going wrong.

Thanks in advance.


Solution

  • After some analysis figured out the issue. This is because of GetUserByIdAsync(). Though I set AsNoTracking() to _userManager.Users its being tracked by ef core internally. So changed the implementation to return user by _userManager.FindByIdAsync and that worked.

    Here is the final cleaned code. Hope this helps someone.

    Razor Page Handler:

            public async Task<IActionResult> OnPutAsync([FromForm] UserViewModel userViewModel)
            {
                try
                {
                    await _user.InitializeAsync(userViewModel.Id);
                    var user = _mapper.Map(userViewModel, _user);
    
                    var (Succeeded, ErrorMessage) = await user.SaveAsync();
    
                    return new JsonResult(new AjaxResultHelper<string>(Succeeded)
                    {
                        Response = ErrorMessage ?? ResponseMessages.DataSaved
                    });
                }
                catch (Exception ex)
                {
                    // Need to log the error
                    Console.WriteLine(ex.Message);
    
                    return new JsonResult(new AjaxResultHelper<string>()
                    {
                        Response = ResponseMessages.DataNotSaved
                    });
                }
            }
    

    User Business:

        public class User : CoreEntityBase
        {
            private readonly IUserRepository _userRepository;
            private readonly IMapper _mapper;
    
            public User()
            {
    
            }
    
            public User(
                IUserRepository userRepository,
                IMapper mapper)
            {
                _userRepository = userRepository;
                _mapper = mapper;
            }
    
            public string Name { get; set; }
    
            [Sortable(Default = true)]
            [SearchableString]
            public string FirstName { get; set; }
    
            [Sortable]
            [SearchableString]
            public string LastName { get; set; }
    
            [Sortable]
            [SearchableString]
            public string Designation { get; set; }
    
            [Sortable]
            [SearchableString]
            public string IdentityNumber { get; set; }
    
            public Guid ProfileId { get; set; }
    
            [NestedSortable]
            [NestedSearchable]
            public Profile Profile { get; set; }
    
            public async Task InitializeAsync(Guid Id)
            {
                if (Id == null)
                {
                    throw new ArgumentNullException($"{nameof(Id)} cannot be null");
                }
    
                var userEntity = await _userRepository.GetUserByIdAsync(Id);
                _mapper.Map(userEntity, this);
            }
    
            public async Task<PagedResults<User>> GetUsersAsync(
                PagingOptions pagingOptions,
                SortOptions<User, UserEntity> sortOptions,
                SearchOptions<User, UserEntity> searchOptions,
                CancellationToken ct)
            {
                var users = await _userRepository.GetUsersAsync(pagingOptions, sortOptions, searchOptions, ct);
    
                return new PagedResults<User>
                {
                    Items = _mapper.Map<User[]>(users.Items),
                    TotalSize = users.TotalSize
                };
            }
    
            public async Task<User> GetUserByIdAsync(Guid userId)
            {
                var userEntity = await _userRepository.GetUserByIdAsync(userId);
    
                return _mapper.Map<User>(userEntity);
            }
    
            public async Task<(bool Succeeded, string ErrorMessage)> SaveAsync()
            {
                if (!Validate())
                {
                    return (false, Error);
                }
    
                var userEntity = await _userRepository.GetUserByIdAsync(Id);
                userEntity = _mapper.Map(this, userEntity);
                var update = await _userRepository.UpdateUserAsync(userEntity);
    
                return update;
            }
    
            public override bool Validate()
            {
                var isValid = true;
    
                if (string.IsNullOrWhiteSpace(FirstName))
                {
                    Error = "FirstName cannot be empty";
                    isValid = false;
                }
    
                if (string.IsNullOrWhiteSpace(LastName))
                {
                    Error = "LastName cannot be empty";
                    isValid = false;
                }
    
                if (string.IsNullOrWhiteSpace(Designation))
                {
                    Error = "Designation cannot be empty";
                    isValid = false;
                }
    
                return isValid;
            }
        }
    

    User Repository:

        public class UserRepository : IUserRepository
        {
            private readonly UserManager<UserEntity> _userManager;
    
            public UserRepository(
                UserManager<UserEntity> userManager)
            {
                _userManager = userManager;
            }
    
            public async Task<UserEntity> GetUserByIdAsync(Guid userId)
            {
                var entity = await _userManager.FindByIdAsync(userId.ToString());
    
                return entity;
            }
    
            public async Task<(bool Succeeded, string ErrorMessage)> UpdateUserAsync(UserEntity userEntity)
            {
                var result = await _userManager.UpdateAsync(userEntity);
    
                if (result.Succeeded)
                {
                    return (true, null);
                }
    
                var firstError = result.Errors.FirstOrDefault()?.Description;
                return (false, firstError);
            }
        }