With ASP.NET Core Identity fully registered we can learn how to perform user registration actions in our project.

User registration is the process of registering users in our application by saving their credentials in the database. So, in this article, we are going to learn how to implement user registration actions in our project. Additionally, we are going to learn about different identity options that could help us in the process.

To download the source code for this project, you can visit the ASP.NET Core Identity User Registration repository.

To navigate through the entire series, visit the ASP.NET Core Identity series page.

Support Code Maze on Patreon to get rid of ads and get the best discounts on our products!
Become a patron at Patreon!

So, let’s get started.

Preparation for User Registration

Let’s begin with the UserRegistrationModel.cs class:

public class UserRegistrationModel
{
    public string FirstName { get; set; }
    public string LastName { get; set; }
    [Required(ErrorMessage = "Email is required")]
    [EmailAddress]
    public string Email { get; set; }

    [Required(ErrorMessage = "Password is required")]
    [DataType(DataType.Password)]
    public string Password { get; set; }

    [DataType(DataType.Password)]
    [Compare("Password", ErrorMessage = "The password and confirmation password do not match.")]
    public string ConfirmPassword { get; set; }
}

In this class, we have a couple of properties the user populates in the registration form. As you can see the Email and Password properties are required and the ConfirmPassword property must match the Password property.

With this in place, let’s create a new MVC controller with two actions inside:

public class AccountController : Controller
{
    [HttpGet]
    public IActionResult Register()
    {
        return View();
    }

    [HttpPost]
    [ValidateAntiForgeryToken]
    public IActionResult Register(UserRegistrationModel userModel)
    {
        return View();
    }
}

So, we have the Account controller with two Register actions (GET and POST). We are going to use the first one to show the view and the second one for the user registration logic.

With that in place, let’s create a view for the GET Register action:

@model IdentityByExamples.Models.UserRegistrationModel

<h1>Register</h1>

<h4>UserRegistrationModel</h4>
<hr />
<div class="row">
    <div class="col-md-4">
        <form asp-action="Register">
            <div asp-validation-summary="All" class="text-danger"></div>
            <div class="form-group">
                <label asp-for="FirstName" class="control-label"></label>
                <input asp-for="FirstName" class="form-control" />
                <span asp-validation-for="FirstName" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="LastName" class="control-label"></label>
                <input asp-for="LastName" class="form-control" />
                <span asp-validation-for="LastName" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Email" class="control-label"></label>
                <input asp-for="Email" class="form-control" />
                <span asp-validation-for="Email" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="Password" class="control-label"></label>
                <input asp-for="Password" class="form-control" />
                <span asp-validation-for="Password" class="text-danger"></span>
            </div>
            <div class="form-group">
                <label asp-for="ConfirmPassword" class="control-label"></label>
                <input asp-for="ConfirmPassword" class="form-control" />
                <span asp-validation-for="ConfirmPassword" class="text-danger"></span>
            </div>
            <div class="form-group">
                <input type="submit" value="Create" class="btn btn-primary" />
            </div>
        </form>
    </div>
</div>

@section Scripts {
    @{await Html.RenderPartialAsync("_ValidationScriptsPartial");}
}

So basically, we have all the input fields from our model in this view. Of course, clicking the Create button will direct us to the POST Register method with the UserRegistrationModel populated.

Now, let’s install the AutoMapper library, so we could map the UserRegistrationModel to the User class:

PM> Install-Package AutoMapper.Extensions.Microsoft.DependencyInjection

After we execute this command, AutoMapper will be installed. We are going to use it in a moment.

Finally, let’s slightly modify the _Layout view:

<div class="navbar-collapse collapse d-sm-inline-flex flex-sm-row-reverse">
    <partial name="_LoginPartial" />
    <ul class="navbar-nav flex-grow-1">
        <li class="nav-item">
            <a class="nav-link text-dark" asp-area="" asp-controller="Home" asp-action="Index">Home</a>
        </li> 
...

We’ve added a partial view, but we have to create it as well. To do that, let’s create a new _LoginPartial view file in the Shared folder:

<ul class="navbar-nav">
    <li class="nav-item">
        <a class="nav-link text-dark" asp-controller="Account" 
               asp-action="Register">Register</a>    
    </li>
</ul> 

Excellent.

If we start our application, we are going to see the Register link in the upper-right corner. After we click on it, the Register view will appear.

User Registration in Action

To start with the user registration logic, we have to inject our AutoMapper and UserManager class in the Account controller:

private readonly IMapper _mapper;
private readonly UserManager<User> _userManager;

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

The UserManager class comes from the Microsoft.AspNetCore.Identity namespace and it provides a set of helper methods to help us manage a user in our application.

Before we continue, let’s register AutoMapper in the ConfigureServices method in the Startup class:

services.AddAutoMapper(typeof(Startup));

If you are using .NET 6 or later, the code is a bit different because we have to modify the Program class:

builder.Services.AddAutoMapper(typeof(Program));

Now, we can modify the Register action:

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Register(UserRegistrationModel userModel)
{
    if(!ModelState.IsValid)
    {
        return View(userModel);
    }

    var user = _mapper.Map<User>(userModel);

    var result = await _userManager.CreateAsync(user, userModel.Password);
    if(!result.Succeeded)
    {
        foreach (var error in result.Errors)
        {
            ModelState.TryAddModelError(error.Code, error.Description);
        }

        return View(userModel);
    }

    await _userManager.AddToRoleAsync(user, "Visitor");

    return RedirectToAction(nameof(HomeController.Index), "Home");
}

As you can see, the action is async now because the UserManager’s helper methods are async as well. Inside the action, we check for the model validity and if it is invalid, we just return the same view with the invalid model.

If that check passes, we map the registration model to the user.

Additionally, we use the CreateAsync method to register the user. But this method does more for us. It hashes a password, executes an additional user checks and returns a result.

If the result is successful, we just attach the default role to the user, again with the UserManager’s help, and redirect the user to the Index page.

But if the registration fails, we loop through all the errors and add them to the ModelState.

Finally, we have to create the MappingProfile class:

public class MappingProfile : Profile
{
    public MappingProfile()
    {
        CreateMap<UserRegistrationModel, User>()
            .ForMember(u => u.UserName, opt => opt.MapFrom(x => x.Email));
    }
}

You can see that we map email to the username because we are not using the username in the registration form.

Testing

Let’s test several invalid cases:

Testing invalid User Registration

You can see the model validation is working just fine. The CreateAsync method validates the password-related errors too.

If a user enters valid values in our form, the user registration process will complete successfully and they will be redirected to the Index page:

Valid User Registration

Excellent.

Now, let’s dive a bit into the ASP.NET Core Identity configuration.

Identity Options

As you could see in the testing example, our user registration process requires a password to fulfill certain rules. These rules can be found and adjusted in the IdentityOptions settings.

Right now, the password requires at least 6 characters, upper and lower case letters, etc.

Let’s play around a bit with these values.

We are going to modify the AddIdentity method in the ConfigureServices method for .NET 5 and previous versions:

services.AddIdentity<User, IdentityRole>(opt =>
{
    opt.Password.RequiredLength = 7;
    opt.Password.RequireDigit = false;
    opt.Password.RequireUppercase = false;
})
 .AddEntityFrameworkStores<ApplicationContext>();

In .NET 6 and later, we have to modify the Program class:

builder.Services.AddIdentity<User, IdentityRole>(opt =>
{
    opt.Password.RequiredLength = 7;
    opt.Password.RequireDigit = false;
    opt.Password.RequireUppercase = false;
})
.AddEntityFrameworkStores<ApplicationContext>();

So, we override the default password configuration by setting the required password length to 7 instead of 6 characters, which is the default value. Additionally, we state that digits and upper case letters are not required.

If we test this with the „test“ password:

Wrong Password - User registration

We can see that there are no more alerts for digits and uppercase letters, and the required length is now 7 characters.

In our example, we are using an email as a user name. By default, email is not required to be unique, but we can change that:

opt.Password.RequiredLength = 7; 
opt.Password.RequireDigit = false; 
opt.Password.RequireUppercase = false; 

opt.User.RequireUniqueEmail = true;

If we try to register with an already registered email address:

User registration with taken email

Of course, pay attention that showing a message like this to a user could be a potential security risk.

That’s because, if this is a user who tries to hack our account, we have just reviled this email that already exists in our system and allowed them to focus only on the passwords.

A better solution would be to send an email message to the owner of this account, with the information that the account already exists.

By doing this, we don’t narrow down hacking possibilities for the malicious user and our regular user could proactively change the password or contact the system administrator to report a possible account breach.

Conclusion

There are a lot more settings we can play with, but for now, this is enough.

In articles to come, we are going to modify IdentityOptions settings even more. But feel free to explore it by yourself. As you can see, the names are pretty self-explanatory.

In the next article, we are going to learn about Login and Logout functionalities with ASP.NET Core Identity.

Until then…

Liked it? Take a second to support Code Maze on Patreon and get the ad free reading experience!
Become a patron at Patreon!