Search code examples
asp.net-mvcasp.net-coreasp.net-identity

ASP .NET Core Identity - how to update user's single detail?


I have a question regarding updating specified user's data in my ASP.NET Identity application. I have a model which defines properties to create, delete and update users and a controller for the same. In the controller I have a method public async Task<IActionResult> EditRole(string id) which edits selected user's details. The data are updating only for following situations:

-updating all data

-updating email only

-updating one at least one value with email

When I try to update something else than email there is exception comming from class CustomUserValidator which is responsible for validating user's details while account creation. The message of exception is System.NullReferenceException: „Object reference not set to an instance of an object.” Microsoft.AspNetCore.Identity.IdentityUser<TKey>.Email.get returned null. Below are needed details to have a look at:

Model UserModel.cs:

using UserApp.Models;
using System.ComponentModel.DataAnnotations;
namespace UserApp
{
public class EditUserModel
    {
        
        public string FirstName { get; set; }
        public string LastName { get; set; }
        public string Email { get; set; }
        public string Password { get; set; }
     
    }
}

Controller AdminController.cs :

using UserApp.Database;
using UserApp.Models;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Threading.Tasks;

namespace UserApp.Controllers
{
[Authorize(Roles = "Administrator")]
    public class AdminController : Controller
    {
        private readonly UserManager<UserEntity> _userManager;
        private readonly RoleManager<IdentityRole> _roleManager;

        public AdminController(UserManager<UserEntity> userManager, RoleManager<IdentityRole> roleManager)
        {
            this._userManager = userManager;
            this._roleManager = roleManager;
        }
//Views
 public IActionResult EditUser() => View();
 [HttpPost]
        public async Task<IActionResult> EditUser(EditUserModel editUserModel,string id)
        {
            
                var user = await _userManager.FindByIdAsync(id);

            if (user != null)
            {
                user.FirstName = editUserModel.FirstName;
                user.LastName = editUserModel.LastName;
                user.Email = editUserModel.Email;
                var result = await _userManager.UpdateAsync(user);


                if (result.Succeeded)
                {
                    return RedirectToAction("Account");
                }
                else
                {
                    AddErrorsFromResult(result);
                }
            }
            else
            {
                ModelState.AddModelError("", "User not existing");
            }
            return View("Account", _userManager.Users);
       
        }
}

View EditUser.cshtml :

@model EditUserModel
@{
    Layout = "_Layout";
}
<h1>User update</h1>

<div asp-validation-summary="All" class="text-danger"></div>
<div class="row-edit">
    <form asp-action="EditUser" method="post">
        <p>First Name</p>
        <input class="register form-control" asp-for="FirstName" />
        <p>Last Name</p>
        <input class="register form-control" asp-for="LastName" />
        <p>Email</p>
        <input class="register form-control" asp-for="Email" />

        <br />
        <input type="submit" value="Save" class="btn btn-primary" />
        @* @foreach (var role in Model.RoleModel.Role)
            {
                <table>
                <tr>
                    <td>@role.Name</td>
                </tr>
                </table>
            }*@
    </form>
    </div>

CustomUserValidator.cs

using UserApp.Database;
using Microsoft.AspNetCore.Identity;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;

namespace UserApp.Infrastructure
{
    public class CustomUserValidator : UserValidator<UserEntity>
    {
        public override async Task<IdentityResult> ValidateAsync(UserManager<UserEntity> manager, UserEntity user)
        {
            IdentityResult result = await base.ValidateAsync(manager, user);

            List<IdentityError> errors = result.Succeeded ? new List<IdentityError>() : result.Errors.ToList();

            List<string> domain = new();
            string[] domains = { "@tvp.pl", "@edu.co.uk"};
            domain.AddRange(domains);

            int addError = 0;

            foreach(var check in domain) if(!user.Email.ToLower().EndsWith(check)) addError++;
            
            if(addError == domain.Count)
            {
                errors.Add(new IdentityError
                {
                    Code = "EmailDomainError",
                    Description = "Invalid email domain"
                });
            }

            return errors.Count == 0 ? IdentityResult.Success : IdentityResult.Failed(errors.ToArray());
        }
    }
}


Solution

  • You're getting an error because you can't nullify the email field. What you should do is populate the view model first in your GET action for EditUser, and then pass it into the view where the form is.

    So update your EditUser() GET action to this:

    public async Task<IActionResult> EditUser()
    {
        string userId = <insert logic to get user's ID>
        var user = await _userManager.FindByIdAsync(userId);
        var viewModel = new EditUserModel
        {
            FirstName = user.FirstName,
            LastName = user.LastName,
            Email = user.Email
        };
        return View(viewModel);
    }
    

    This way the user will be able to see their current email and name, and they can choose to modify whichever field they'd like to.

    You of course can't trust that the user won't nullify the email field, so you should add server side validation by means of a data annotation in your view model:

    using UserApp.Models;
    using System.ComponentModel.DataAnnotations;
    namespace UserApp
    {
    public class EditUserModel
        {
            
            public string FirstName { get; set; }
            public string LastName { get; set; }
            [Required]
            public string Email { get; set; }
            public string Password { get; set; }
         
        }
    }
    

    Then check the model state in the POST action before doing anything else:

    if (!ModelState.IsValid)
    {
        return View();
    }
    

    If convention is a concern of yours, I recommend designating a form for updating just the email. That's what most applications/websites do. And you should probably generate a token before updating it.