Saturday, June 30, 2018

Seeding & Extending Users and Roles with ASP.NET Core 2.1 Identity in Visual Studio 2017

In this post I shall describe the steps you need to follow if you want to add more data fields to the standard users & roles database table and seed both users and roles data. The approach being followed is code first development with Entity Framework Core. In order to proceed with this tutorial you need to have the following prerequisites:
  • You are using Visual Studio 2017 running under the Windows 10 operating system
  • You have installed ASP.NET 2.1 Core

Getting Started

In Visual Studio 2017, start a new ASP.NET Core 2.1 application by clicking on File >> New >> Project

Select ASP.NET Core Web Application and name your project with a name like IdentityCore.

Click on the OK button. On the next dialog:
  • select ASP.NET Core 2.1 from the drop-down list
  • select "Web Application (Model-View-Controller)" from the templates
  • click on the "Change Authentication" button and choose "Individual User Accounts"

Click on OK. Run the application by hitting Ctrl + F5 on your keyboard. 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 may receive a message that looks like this:

Do not be alarmed. The message simple reminds you that the Entity Framework migrations have not been applied yet. Simple click on the blue "Apply Migrations" button then refresh the page in your browser. The home page will display as shown below:


Click on Logout in the top-right corner.

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 the following properties:

FirstName
LastName
Street
City
Province
PostalCode

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

    public class ApplicationUser : IdentityUser
    {

        public ApplicationUser() : base() { }

        public string FirstName { get; set; }
        public string LastName { get; set; }
        public string Street { get; set; }
        public string City { get; set; }
        public string Province { get; set; }
        public string PostalCode { get; set; }
        public string Country { get; set; }
    }

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

Description
CreatedDate

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

    public class ApplicationRole : IdentityRole
    {

        public ApplicationRole() : base() { }

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

        public ApplicationRole(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; }

    }

Edit Data/ApplicationDbContext.cs file and get ApplicationDbContext to inherit from IdentityDbContext<ApplicationUser, ApplicationRole, string>. The ApplicationDbContext class code should look like this:

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

Let us now create some sample data for roles and users. Create class named DummyData in Data folder, then add to it the following method:

public class DummyData {
    public static async Task Initialize(ApplicationDbContext context,
                          UserManager<ApplicationUser> userManager,
                          RoleManager<ApplicationRole> roleManager)
    {
        context.Database.EnsureCreated();

        String adminId1 = "";
        String adminId2 = "";

        string role1 = "Admin";
        string desc1 = "This is the administrator role";
        
        string role2 = "Member";
        string desc2 = "This is the members role";

        string password = "P@$$w0rd";

        if (await roleManager.FindByNameAsync(role1) == null) {
            await roleManager.CreateAsync(new ApplicationRole(role1, desc1, DateTime.Now));
        }
        if (await roleManager.FindByNameAsync(role2) == null) {
            await roleManager.CreateAsync(new ApplicationRole(role2, desc2, DateTime.Now));
        }

        if (await userManager.FindByNameAsync("a") == null) {
            var user = new ApplicationUser {
                UserName = "a@a.a",
                Email = "a@a.a",
                FirstName = "Adam",
                LastName = "Aldridge",
                Street = "Fake St",
                City = "Vancouver",
                Province = "BC",
                PostalCode = "V5U K8I",
                Country = "Canada",
                PhoneNumber = "6902341234"
            };

            var result = await userManager.CreateAsync(user);
            if (result.Succeeded) {
                await userManager.AddPasswordAsync(user, password);
                await userManager.AddToRoleAsync(user, role1);
            }
            adminId1 = user.Id;
        }

        if (await userManager.FindByNameAsync("b") == null) {
            var user = new ApplicationUser {
                UserName = "b@b.b",
                Email = "b@b.b",
                FirstName = "Bob",
                LastName = "Barker",
                Street = "Vermont St",
                City = "Surrey",
                Province = "BC",
                PostalCode = "V1P I5T",
                Country = "Canada",
                PhoneNumber = "7788951456"
            };

            var result = await userManager.CreateAsync(user);
            if (result.Succeeded) {
                await userManager.AddPasswordAsync(user, password);
                await userManager.AddToRoleAsync(user, role1);
            }
            adminId2 = user.Id;
        }

        if (await userManager.FindByNameAsync("m") == null) {
            var user = new ApplicationUser {
                UserName = "m@m.m",
                Email = "m@m.m",
                FirstName = "Mike",
                LastName = "Myers",
                Street = "Yew St",
                City = "Vancouver",
                Province = "BC",
                PostalCode = "V3U E2Y",
                Country = "Canada",
                PhoneNumber = "6572136821"
            };

            var result = await userManager.CreateAsync(user);
            if (result.Succeeded) {
                await userManager.AddPasswordAsync(user, password);
                await userManager.AddToRoleAsync(user, role2);
            }
        }

        if (await userManager.FindByNameAsync("d") == null) {
            var user = new ApplicationUser {
                UserName = "d@d.d",
                Email = "d@d.d",
                FirstName = "Donald",
                LastName = "Duck",
                Street = "Well St",
                City = "Vancouver",
                Province = "BC",
                PostalCode = "V8U R9Y",
                Country = "Canada",
                PhoneNumber = "6041234567"
            };

            var result = await userManager.CreateAsync(user);
            if (result.Succeeded) {
                await userManager.AddPasswordAsync(user, password);
                await userManager.AddToRoleAsync(user, role2);
            }
        }
    }
}

In the Startup class, update the call to AddDefaultIdentity to use the new ApplicationUser and add the call to AddDefaultUI with the following code replacement:

services.AddIdentity<ApplicationUser, ApplicationRole>(
    options => options.Stores.MaxLengthForKeys = 128)
    .AddEntityFrameworkStores<ApplicationDbContext>()
    .AddDefaultUI()
    .AddDefaultTokenProviders();

Edit Views/Shared/_LoginPartial.cshtml and change:

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

TO


@inject SignInManager<ApplicationUser> SignInManager
@inject UserManager<ApplicationUser> UserManager

We will use the magic of dependency injection to make available to us the ApplicationDbContext, RoleManager and UserManager objects in the Configure() method in Startup.cs. Change the signature of the Configure() method by adding additional arguments (UserManager & RoleManager) so that it looks like this:

public void Configure(IApplicationBuilder app, 
  IHostingEnvironment env,
  ApplicationDbContext context,
  RoleManager<ApplicationRole> roleManager,
  UserManager<ApplicationUser> userManager
)
{ . . . . . }

Add the following to the bottom of the Configure() method in Startup.cs:

DummyData.Initialize(context, userManager, roleManager).Wait();// seed here

Note that in the Data/Migrations folder there are Entity Framework Code First migrations files that were added by the initial Visual Studio project template.


Since we changed the model for both users and roles, we need to add another migration and subsequently update the database. Therefore, execute the following commands from inside the Package Manager Console (Tools >> Nuget Package Manager >> Package Manager Console):

Add-Migration ExtendedUserRole -Context ApplicationDbContext

Update-Database -Context ApplicationDbContext

At this stage the tables are created, however data is not yet seeded. Let us run our application so that the sample roles and users get seeded. Hit CTRL + F5 on your keyboard. When the application starts, Logout if you are already logged in.

To prove that user and role data have been successfully seeded, login with one of the below credentials that were previously seeded:

Email: a@a.a    Password: P@$$w0rd
Email: b@b.b    Password: P@$$w0rd
Email: d@d.d    Password: P@$$w0rd
Email: m@m.m    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 First Name, Last Name, Street, City, Province, Postal Code and Country. ASP.NET Core 2.1 (and later) provide 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 the ConfigureServices() method in Startup.cs.

Since we need to modify the registration controller and view, we instruct the scaffolder to generate the code used for registration. To do this, right-click on the project node in the Solution Explorer pane then:  Add >> New Scaffolded Item:


On the next Add Scaffold dialog, click on Identity on the left side, highlight Identity in the middle pane then click on the Add button.

On the Add Identity dialog, enable the "Override all files" checkbox, select the ApplicationDbContext class then click on the Add button.

Many Razor view pages will be generated for you under folder Areas/Identity/Pages/Account.


Edit the code-behind file Areas\Itentity\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; }

[DataType(DataType.Text)]
[MaxLength(50)]
public string Street { get; set; }

[DataType(DataType.Text)]
[MaxLength(50)]
public string City { get; set; }

[DataType(DataType.Text)]
[MaxLength(50)]
public string Province { get; set; }

[DataType(DataType.Text)]
[MaxLength(15)]
[Display(Name = "Postal Code")]
public string PostalCode { get; set; }

[DataType(DataType.Text)]
[MaxLength(35)]
public string Country { get; set; }

In the same  \Itentity\Pages\Account\Register.cshtml.cs code-behind file, edit the code in the OnPostAsync() method so that line:

var user = new ApplicationUser { UserName = Input.Email, Email = Input.Email };

is changed to:

var user = new ApplicationUser {
  UserName = Input.Email,
  Email = Input.Email,
  FirstName = Input.FirstName,
  LastName = Input.LastName,
  Street = Input.Street,
  City = Input.City,
  Province = Input.Province,
  PostalCode = Input.PostalCode,
  Country = Input.Country
};

Next, let's update the UI. Edit the Razor view page Areas\Itentity\Pages\Account\Register.cshtml. Add the following markup to the form right before the email block:

<div class="form-group">
  <label asp-for="Input.FirstName"></label>
  <input asp-for="Input.FirstName" class="form-control" />
  <span asp-validation-for="Input.FirstName" class="text-danger"></span>
</div>
<div class="form-group">
  <label asp-for="Input.LastName"></label>
  <input asp-for="Input.LastName" class="form-control" />
  <span asp-validation-for="Input.LastName" class="text-danger"></span>
</div>
<div class="form-group">
  <label asp-for="Input.Street"></label>
  <input asp-for="Input.Street" class="form-control" />
  <span asp-validation-for="Input.Street" class="text-danger"></span>
</div>
<div class="form-group">
  <label asp-for="Input.City"></label>
  <input asp-for="Input.City" class="form-control" />
  <span asp-validation-for="Input.City" class="text-danger"></span>
</div>
<div class="form-group">
  <label asp-for="Input.Province"></label>
  <input asp-for="Input.Province" class="form-control" />
  <span asp-validation-for="Input.Province" class="text-danger"></span>
</div>
<div class="form-group">
  <label asp-for="Input.PostalCode"></label>
  <input asp-for="Input.PostalCode" class="form-control" />
  <span asp-validation-for="Input.PostalCode" class="text-danger"></span>
</div>
<div class="form-group">
  <label asp-for="Input.Country"></label>
  <input asp-for="Input.Country" class="form-control" />
  <span asp-validation-for="Input.Country" class="text-danger"></span>
</div>

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

When you click on the register button all the user data is saved in the database. You can verify that data has indeed been saved by viewing data in the AspNetUsers database table using "SQL Server Object Explorer":


We have succeeded in seeding user and role data and subsequently updating the registration page so that additional user data is captured. Thanks for coming this far in the tutorial.

References:

ASP.NET Core 2.1.0-preview1: Introducing Identity UI as a library

No comments:

Post a Comment