ASP.NET Core 3.1 - User Management


Ken Haggerty
Created 02/01/2020 - Updated 04/24/2020 15:05

This article will demonstrate the implementation of user management. I will assume you have created a new ASP.NET Core 3.1 Razor Pages project and implemented the first 3 articles of the series. See Tutorial: Get started with Razor Pages in ASP.NET Core .

Users Without Identity Project and Article Series


Access to the research project source code may be purchased on KenHaggerty.Com at Manage > Assets.

A project which implements users without Identity has been published to demo.kenhaggerty.com.

I enjoy writing these articles. It often enhances and clarifies my coding. The research project is a result of a lot of refactoring and hopefully provides logical segues for the articles. Thank you for supporting my efforts.

Users can not self-register. We need an interface to create, read, update and delete users. We can scaffold Razor Pages with Entity Framework(CRUD) for the AppUser entity. Create a new folder under Admin named Users. I will create an Admin role in the next article which will restrict access to the Users folder. Right click on the new folder then Add then click New Scaffolded Item.

New Scaffolded Item
Razor Page Using Entity Framework

Select AppUser for the Model class and ApplicationDbContext for the Data context class. Leave the layout page option empty because it is set by _ViewStart.cshtml.

Razor Pages Entity Options

When complete, there will be Create, Delete, Details, Edit and Index pages in the Users folder. Add a link from the Admin Index page to the Users Index page.

<a class="btn btn-primary" asp-page="/Admin/Users/Index">Users</a>
Admin Index page

The Users Index page displays a list of all users including the user's LoginNameUppercase and PasswordHash. Remove the LoginNameUppercase and PasswordHash properties and add style to the page.

Users Index page

To create a new user we need a login name and password. I use an InputModel decorated with DataAttributes. If the model validates, check for an exisiting login name, hash the password then save the user.

Edit Admin/ Users/ Create.cshtml.cs:
public class CreateModel : PageModel
{
    private readonly ApplicationDbContext _context;
    public CreateModel(ApplicationDbContext context)
    {
        _context = context;
    }

    [BindProperty]
    public InputModel Input { get; set; }
    public class InputModel
    {
        [Required]
        [StringLength(32, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)]
        [Display(Name = "Login Name")]
        public string LoginName { get; set; }

        [Required]
        [StringLength(32, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 8)]
        public string Password { get; set; }
    }

    public IActionResult OnGet()
    {
        return Page();
    }

    public async Task<IActionResult> OnPostAsync()
    {
        if (!ModelState.IsValid)
        {
            return Page();
        }

        // Test for an existing user with the same LoginName
        int testId = await _context.AppUsers
            .Where(u => u.LoginNameUppercase == Input.LoginName.ToUpper())
            .Select(u => u.Id)
            .FirstOrDefaultAsync();

        if (testId == 0)
        {
            var passwordHasher = new CustomPasswordHasher();
            var hashedPassword = passwordHasher.HashPassword(Input.Password);
            var newUser = new AppUser
            {
                LoginName = Input.LoginName,
                LoginNameUppercase = Input.LoginName.ToUpper(),
                PasswordHash = hashedPassword
            };

            _context.AppUsers.Add(newUser);
            try
            {
                _context.SaveChanges();
            }
            catch (DbUpdateException)
            {
                throw new InvalidOperationException("DbUpdateException occurred creating a new user.");
            }
        }
        else
        {
            ModelState.AddModelError(string.Empty, "Login Name already exisits.");
            return Page();
        }

        return RedirectToPage("./Index");
    }
}

Update the page inputs and add a little style.

New User page

The edit InputModel includes the Id and does not require a password. The password hash is not updated if the InputModel password is empty. If a password is entered, it will be validated. If the model validates, check updating the login name, hash the password if updated then save the user.

Edit Admin/ Users/ Edit.cshtml.cs:
public class EditModel : PageModel
{
    private readonly ApplicationDbContext _context;
    public EditModel(ApplicationDbContext context)
    {
        _context = context;
    }

    [BindProperty]
    public InputModel Input { get; set; }
    public class InputModel
    {
        [Key]
        public int Id { get; set; }

        [Required]
        [StringLength(32, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)]
        [Display(Name = "Login Name")]
        public string LoginName { get; set; }

        [StringLength(32, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 8)]
        public string Password { get; set; }
    }

    public async Task<IActionResult> OnGetAsync(int? id)
    {
        if (id == null)
        {
            return NotFound();
        }

        var user = await _context.AppUsers
            .AsNoTracking()
            .FirstOrDefaultAsync(m => m.Id == id);

        if (user == null)
        {
            return NotFound();
        }

        Input = new InputModel
        {
            Id = user.Id,
            LoginName = user.LoginName
        };

        return Page();
    }

    public async Task<IActionResult> OnPostAsync()
    {
        if (!ModelState.IsValid)
        {
            return Page();
        }

        var user = await _context.AppUsers.FirstOrDefaultAsync(m => m.Id == Input.Id);

        if (user == null)
        {
            return NotFound();
        }

        if (user.LoginNameUppercase != Input.LoginName.ToUpper())
        {
            // Test for an existing user with the new LoginName
            int testId = await _context.AppUsers
                .Where(u => u.LoginNameUppercase == Input.LoginName.ToUpper())
                .Select(u => u.Id)
                .FirstOrDefaultAsync();

            if (testId != 0)
            {
                ModelState.AddModelError(string.Empty, "Login Name already exisits.");
                return Page();
            }
        }

        // Hash new password if provided
        if (!string.IsNullOrEmpty(Input.Password))
        {
            var passwordHasher = new CustomPasswordHasher();
            var hashedPassword = passwordHasher.HashPassword(Input.Password);
            user.PasswordHash = hashedPassword;
        }

        user.LoginName = Input.LoginName;
        user.LoginNameUppercase = Input.LoginName.ToUpper();

        _context.Attach(user).State = EntityState.Modified;

        try
        {
            await _context.SaveChangesAsync();
        }
        catch (DbUpdateConcurrencyException)
        {
            if (!AppUserExists(user.Id))
            {
                return NotFound();
            }
            else
            {
                throw;
            }
        }

        return RedirectToPage("./Index");
    }

    private bool AppUserExists(int id)
    {
        return _context.AppUsers.Any(e => e.Id == id);
    }
}

Update the page inputs and add a little style.

Edit User page

The code for details and delete doesn't need modification but I remove the LoginNameUppercase and PasswordHash properties from the pages.

User Details page
Delete User page
Update 04/24/2020

I published the User Database Service article and updated the article links. I implemented a UserService in the Users Without Identity Project at v1.0.6 and have updated the project's NuGet packages, details and download.



Comment Count = 2

Tony Albutt
Tony Albutt
Posted 03/10/2020 13:09
Member Since 09/04/2019

Hello Ken Can you use this method to manage users using Organisational accounts i.e. Work accounts Azure AD, school accounts and Office 365 If the user is authenticated, you can then create a local profile using token info from the User. Identity.Claims? Thanks and regards Tony

Ken Haggerty
Ken Haggerty *
Posted 03/10/2020 15:06
Member Since 01/04/2019

I am not sure what you want to manage but you need to start with a project which supports Windows Authentication. See Configure Windows Authentication in ASP.NET Core

Please log in to comment or follow.

Login Register
Follow to get web notifications when new comments are posted to this article.
Logged in users receive web notifications for new articles, topics and assets.
Web Notifications