Sunday, October 8, 2023

Extending Users and Roles with ASP.NET Identity in VS Code

In this tutorial, I will demo how to add more data fields to the standard users & roles database. In order to proceed with this tutorial, you need to have the following prerequisites:

  • VS Code
  • You have installed .NET 8.0
  • You have installed the dotnet-ef tool
  • You have installed the dotnet-aspnet-codegenerator tool

Companion Video: https://youtu.be/xo4usBberVA

Getting Started

Download the source code for an application that seeds some sample users and roles into an SQLite database from this GitHub repo:

git clone https://github.com/medhatelmasry/Code1stUsersRoles
cd Code1stUsersRoles

When you run this app, you will be able to access the privacy page (/privacy) with the following credentials:

EmailPasswordRolePage
aa@aa.aa P@$$w0rd Admin /privacy
mm@mm.mm P@$$w0rd Member /

This is because the PrivacyModel class in Pages/Privacy.cshtml.cs is annotated with the following:

[Authorize (Roles = "Member, Admin")]

Click on the Register link on the top-right side of your keyboard to add a new user. 

When you click on the Register button, you will receive a page that looks like this:


Click on Logout in the top-right corner.

Open the application folder in VS Code.

Suppose we want to capture more data about the user, in addition to email and password. Let us assume we want to extend user data with FirstName & LastName.

An easy way to do this is to create a new class that extends IdentityUser and adds the above properties. Create a Models folder and add a new class named CustomUser to it with the following class code: 

public class CustomUser : IdentityUser {
  public CustomUser() : base() { }

  public string? FirstName { get; set; }
  public string? LastName { get; set; }
}

We may also wish to extend the standard roles table with these properties:

Description
CreatedDate

Just as we did with users, we will also create another class for roles that inherits from IdentityRole. In the Models folder, create another class named CustomRole and add to it the following code:

public class CustomRole : IdentityRole {

  public CustomRole() : base() { }

  public CustomRole(string roleName) : base(roleName) { }

  public CustomRole(string roleName, string description,
    DateTime createdDate)
    : base(roleName) {
    base.Name = roleName;

    this.Description = description;
    this.CreatedDate = createdDate;
  }

  public string? Description { get; set; }
  public DateTime CreatedDate { get; set; }
}

Add the following to Pages/ _ViewImports.cshtml:

@using Code1stUsersRoles.Models

Edit Data/ApplicationDbContext.cs file and make ApplicationDbContext  inherit from IdentityDbContext<CustomUser, CustomRole, string>. The ApplicationDbContext class code should look like this:

public class ApplicationDbContext : IdentityDbContext<CustomUser, CustomRole, string> {
    public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)
        : base(options) { }
}

Modify Data/ModelBuilderExtensions.cs so that it uses CustomUser instead of IdentityUser & CustomRole instead of IdentityRole
  • When creating a role, add data for Description and CreatedDate.
  • When creating a user, add data for FirstName & LastName.
In the Program.cs class, replace IdentityUser with ApplicationUser and IdentityRole with ApplicationRole. The builder.Services.AddIdentity… statement will look like this:

builder.Services.AddIdentity<CustomUser, CustomRole>(
options => {
    options.Stores.MaxLengthForKeys = 128;
})
.AddEntityFrameworkStores<ApplicationDbContext>()
.AddDefaultUI()
.AddDefaultTokenProviders()
.AddRoles<CustomRole>();

Edit Pages/Shared/_LoginPartial.cshtml and change:

@inject SignInManager<IdentityUser> SignInManager
@inject UserManager<IdentityUser> UserManager

TO

@inject SignInManager<CustomUser> SignInManager
@inject UserManager<CustomUser> UserManager

Let us start with a clean database and migration. Therefore, delete app.db and the Data/Migrations folder. 

Then, execute the following commands from within a terminal window in the root folder of the application:

dotnet ef migrations add M1 -o Data/Migrations
dotnet ef database update

At this stage, all the database tables are created and seeded. Let us run our application.


To prove that user and role data are successfully seeded, login with any of the below credentials that were previously seeded:

Email: aa@aa.aa    Password: P@$$w0rd
Email: mm@mm.mm    Password: P@$$w0rd

The next task we need to accomplish is to modify the registration page so that the application can capture extended data such as FirstName & LastName. ASP.NET provides ASP.NET Core Identity as a Razor Class Library. This means that the registration UI is baked into the assemblies and is surfaced with the .AddDefaultUI() option with the services.AddIdentity() command in Program.cs.

We need to add some additional packages so that we can scaffold the view for account registration. From within a terminal window at the root of your application, run the following commands: 

dotnet add package Microsoft.VisualStudio.Web.CodeGeneration.Design
dotnet add package Microsoft.EntityFrameworkCore.Design
dotnet add package Microsoft.EntityFrameworkCore.SqlServer


If you do not already have the .NET code-generation (scaffolding) tool, execute the following command from within a terminal window:

dotnet tool install -g dotnet-aspnet-codegenerator

Here are some useful commands pertaining to the code-generation (scaffolding) tool:

Help with the tool dotnet aspnet-codegenerator identity -h
List all the views that can be scaffolded dotnet aspnet-codegenerator identity --listFiles
Scaffold three views dotnet aspnet-codegenerator identity --files "Account.Register;Account.Login;Account.RegisterConfirmation"
Expose all files dotnet aspnet-codegenerator identity

Since we need to modify the registration controller and view, we instruct the scaffolder to surface the code used for registration. To do this, we will scaffold three pages that pertain to account registration and login. Run the following command from within a terminal window:

dotnet aspnet-codegenerator identity --files "Account.Register;Account.Login;Account.RegisterConfirmation" -dc ApplicationDbContext

NOTE: If you encounter an error, temporarily comment out the statement "builder.Seed();" in ApplicationDbContext.cs and try the above command again.

The above command generates a handful of razor view pages under folder Areas/Identity/Pages/Account.


Edit the code-behind file Areas/Identity/Pages/Account/Register.cshtml.cs

Add the following properties to the InputModel class: 

[Required]
[DataType(DataType.Text)]
[StringLength(50, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 2)]
[Display(Name ="First Name")]
public string FirstName { get; set; }

[Required]
[DataType(DataType.Text)]
[StringLength(50, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 2)]
[Display(Name = "Last Name")]
public string LastName { get; set; }

In the same file, edit the code in the OnPostAsync() method so that line:

var user = CreateUser();

is changed to: 

var user = new CustomUser {
  UserName = Input.Email,
  Email = Input.Email,
  FirstName = Input.FirstName,
  LastName = Input.LastName
};

Next, let us update the UI. Edit razor page Areas/Identity/Pages/Account/Register.cshtml. Add the following markup to the form right before the email/username block: 

<div class="form-floating mb-3">
  <input asp-for="Input.FirstName" class="form-control" autocomplete="firstname" aria-required="true" placeholder="First Name"/>
  <label asp-for="Input.FirstName"></label>
  <span asp-validation-for="Input.FirstName" class="text-danger"></span>
</div>
<div class="form-floating mb-3">
  <input asp-for="Input.LastName" class="form-control" autocomplete="lastname" aria-required="true" placeholder="Last Name"/>
  <label asp-for="Input.LastName"></label>
  <span asp-validation-for="Input.LastName" class="text-danger"></span>
</div>

The code generator added some unnecessary code to Program.cs around line 13. Find the following code in Program.cs and comment it out or delete it:

builder.Services.AddDefaultIdentity<CustomUser>(options => options.SignIn.RequireConfirmedAccount = true).AddEntityFrameworkStores<ApplicationDbContext>();

Run the web application and click on the Register button on the top-right side.


When you click on Register, all user data will be saved in the database. 


We have succeeded in updating the registration page so that additional user data is stored. Thanks for coming this far in this tutorial.

No comments:

Post a Comment