Skip to content
  • iImagine
  • Register
  • Log In

Web Development School

Learning made easy.

  • Books
    • Beginning Web Development with ASP.Net Core & Client-Side Technologies
      • TOC
      • Part 1
        • Chapter 1: Static HTML – Designing the landing page
      • Part 2
        • Chapter 2: ASP.Net Core – Let’s talk Dynamic
        • Chapter 3: Introduction to ASP.Net Core MVC
          [ASP.Net Core v9]
      • Part 4
        • Chapter 7: Using Server Side & Client Side technologies together
          [ASP.Net Core v7 & Angular 15]
  • Environment Setup
    • Installing Angular
    • Installing Visual Studio 2022
    • Installing SQL Server 2022 Express
    • Installing Postman
    • Installing Git for Windows
  • Blog
  • iImagine WebSolutions
  • Events
  • Learning Videos
  • Toggle search form

Security & Administration

Now that all of the main features of the application are working, we need to tighten down the privileges of who is allowed to do what. This comes down to two concepts known as authentication and authorization.

Authentication is the process of verifying the identity of a user.

Authentication is the process of allowing a user to do something once their identity has been verified.

Both of these processes can be accomplished using ASP.Net Core identity. And that is what we are going to learn about in this module.

Table Of Contents
  1. ASP.Net Core Identity
    • Add the Identity Package
    • Create a Custom Identity User Object
    • Create the Identity Core DbContext
      • Register the Identity Context
      • Define the Identity Context Connection String
    • Create the Identity Core Migration
    • Apply the Identity Core Migration
  2. Areas: Structure the Admin Dashboard
    • Create the folder structure
    • Configure Routing
  3. Locking Down Controllers and Actions with Authorize
  4. Identity Seeding: 1st user -> admin
  5. Build a Login: Authenticate & Authorize
    • Create the Login Model
    • Create the Account controller
    • Create the Login view
  6. Create an Admin Page
  7. Build a Profile Control panel
    • Create the View Component
      • Create a View Model for the component
      • Create the View Component class
      • Create the View Component view
      • Call the ProfileControl component in layout
  8. Use the Authorize Roles policy
    • Create the Access Denied page
  9. Identity Seeding: 1st role -> Administrator
  10. Add Profile Information to the Control Panel
  11. Unit Test Fix: Can_Update_Vehicle
  12. Add an Admin Button
  13. Build out the Roles Admin pages
    • Roles Index/List feature
      • Create the Roles Repository
        • Create the Roles Repo Interface
        • Create the Roles Repo Implementation
        • Register the Roles Repository
      • Create the Roles List View Model
      • Update _ViewImports file for Admin Area
      • Update the Roles Controller
      • Update the Roles View
    • The Create Role feature
      • Update the Roles Repository
        • Modify the Roles Repo Interface
        • Modify the Roles Repo Implementation
      • Update the Roles Controller
      • Update the Admin Area _ViewImports file
      • Add the Roles Create view
      • Add a Create New Link to the Roles Index page
    • The Delete Role feature
      • Update Roles Repository (2)
        • Modify the Roles Repo Interface (2)
        • Modify the Roles Repo Implementation (2)
      • Update the Roles Controller (2)
      • Add the Roles Delete view
      • Add a Delete link for each Role result
    • The Edit Role feature
      • Update Roles Repository (3)
      • Update the Roles Controller (3)
      • Add the Roles Edit view
      • Add an Edit link for each Role result
  14. Build out the Users Admin Pages
    • Users Index/List feature
      • Create the UserRoles View Model
      • Update the Users Controller (1)
      • Update the Users Index View
    • The Create User feature
      • Add the Create User view model
      • Update the Users Controller
      • Add the Users Create view
      • Add the jQuery validation scripts to the Admin Area
      • Add the Create New link to the Users/Index view
    • The Edit User Feature
      • Add the Users Edit view Model
      • Update the Users controller (2)
      • Add the Users Edit View
      • Add Edit button icon to Users Index page
    • The User Details Feature
      • Create UserDetails view model
      • Update the Users controller (3)
      • Create the User Details view
      • Add Details link to Users on Users Index page
    • The Delete User Feature

ASP.Net Core Identity

ASP.Net Core Identity is a Microsoft API with features to manage users, roles, and login functionality in an application.

Users can create a login account stored in Identity or use logins from social media accounts such as Facebook, Google, Twitter, or a Microsoft account.

Add the Identity Package

ASP.Net Core Identity uses EntityFramework Core to store users and roles to an SQL Server database. The two packages needed to use Identity Core are:

  • Microsoft.AspNetCore.Identity.EntityFrameworkCore: EF Core store implementation
  • Microsoft.EntityFrameworkCore.SqlServer: EF Core Sql Provider package

We already have the Microsoft.EntityFrameworkCore.SqlServer package installed since we have already been using EF Core and SQL Server for all of the database functionality up to this point in this chapter.

So, we only need to install the Microsoft.AspNetCore.Identity.EntityFrameworkCore package.

Open a console window, point it to the FredsCars directory and run the following command to install the package.

dotnet add package Microsoft.AspNetCore.Identity.EntityFrameworkCore --version 9.0.9

Create a Custom Identity User Object

In Identity Core, a user is represented by the default entity class, IdentityUser, in the Identity system.

It comes from the Microsoft.AspNetCore.Identity namespace and provides all the basic properties you typically need to handle authentication and user management, such as usernames, email addresses, hashed passwords, and security stamps.

We can add properties to the User object by creating our own custom user class.


Create a new folder named Identity in the FredsCars/Models folder. In the new folder create an object named ApplicationUser and fill it with the code below.

FredsCars\Models\Identity\ApplicationUser.cs

using Microsoft.AspNetCore.Identity;

namespace FredsCars.Models.Identity
{
    public class ApplicationUser : IdentityUser
    {
        public string? FirstName { get; set; } = null!;
        public string? LastName { get; set; } = null!;
    }
}

NOTE: Make sure to make the strings nullable here (string?). The first super admin we create with seeding will not have a first or last name.

In the next section we are going to create a separate DbContext for Identity Core. When using IdentityDbContext, the IdentityUser maps to the AspNetUsers table in the database.

Create the Identity Core DbContext

We are going to create a separate DbContext and database for the Identity Core system. This way the business tables and Identity user/role management tables won’t be mixed all together in one Database. In my opinion that would look pretty messy. (Although, it is possible to have only one DbContext with business tables and Identity tables in one DbContext and Db.)

In the FredsCars/Data folder, create a new class named FCMvcIdentityCoreDbContext and fill it with the code below.

FredsCars\Data\FCMvcIdentityCoreDbContext.cs

using FredsCars.Models.Identity;
using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore;

namespace FredsCars.Data
{
    public class FCMvcIdentityCoreDbContext : IdentityDbContext<ApplicationUser>
    {
        public FCMvcIdentityCoreDbContext(DbContextOptions<FCMvcIdentityCoreDbContext> options)
            : base(options) { }
    }
}

Register the Identity Context

Next we need to register the Identity Core DbContext (FCMvcIdentityCoreDbContext) in the same way we registered the ApplicationDbContext (FredsCarsDbContext) in Program.cs

Modify Program.cs with the code shown below.

FredsCars\Program.cs

FredsCars.Data;
using FredsCars.Models;
using FredsCars.Models.Identity;
using FredsCars.Models.Repositories;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;
using Serilog;
using Serilog.Events;
using Serilog.Sinks.MSSqlServer;

... existing code ...

// Add Services
builder.Services.AddControllersWithViews();

if (!builder.Environment.IsEnvironment("Test"))
{
   // Application DbContext    
   builder.Services.AddDbContext<FredsCarsDbContext>(opts =>
    {
        opts.UseSqlServer(
            builder.Configuration["ConnectionStrings:FredsCarsMvcConnection"]
        );
    });

    // Identity Core DbContext
   builder.Services.AddDbContext<FCMvcIdentityCoreDbContext>(opts =>
   {
       opts.UseSqlServer(
           builder.Configuration["ConnectionStrings:FCMvcIdentityConnection"]
       );
   });

   builder.Services.AddIdentity<ApplicationUser, IdentityRole>()
        .AddEntityFrameworkStores<FCMvcIdentityCoreDbContext>()
        .AddDefaultTokenProviders();
}

... existing code ...

In the code above we register the Identity DbContext, FCMvcIdentityCoreDbContext, the same way we do as the application DbContext, with the AddDbContext method.

Next, the AddIdentity method above registers the Identity DbContext while adding the default Identity system configuration for the specified user and role types:

  • Identity User type: ApplicationUser -> inherits from IdentityUser
  • Identity Role type: IdentityRole

Define the Identity Context Connection String

Next we need to define the connection string for the Identity DbContext. For development we will define it in the same place we did for the Application DbContext, in User Secrets. (Remember you can get to the User Secrets file by right clicking on the FredsCars project and selecting Manage User Secrets.)

Make the following modification to the secrets.json file.

{
  "ConnectionStrings": {
    "FredsCarsMvcConnection": "Server=(localdb)\\MSSQLLocalDB;Database=FredsCarsMvc;Trusted_Connection=True;MultipleActiveResultSets=true",
    "FCMvcIdentityConnection": "Server=(localdb)\\MSSQLLocalDB;Database=FCMvcIdentity;Trusted_Connection=True;MultipleActiveResultSets=true"
  }
}

Create the Identity Core Migration

The next step we need to perform is to create the initial migration for the Identity DbContext. In a console window point the command line to the FredsCars project and run the following command.

dotnet ef migrations add InitialFCMvcIdentityMigration -c FCMvcIdentityCoreDbContext -o Migrations/Identity

Now that we have two DbContexts in our application we need to specify which context the migration is for with the -c switch, which here is specifying to create this migration for the FCMvcIdenityCoreDbContext. The -o switch specifies what location to place the migration files. We are instructing EF Core to create a subfolder called Identity in the Migrations folder just for Identity migrations to separate out business migrations from Identity migrations.

Apply the Identity Core Migration

Now we need to create the Identity Core database by applying the migration. Run the following command in the FredsCars project folder.

dotnet ef database update -c FCMvcIdentityCoreDbContext

Note we again use the -c switch to target the correct Identity context.

The new localDb should appear in your user directory in Windows Explorer.

And you can see the Identity tables in SSOX.

Areas: Structure the Admin Dashboard

We are going to create an Administration Dashboard where Administrators can manage users and roles. We are going to structure the admin area with an ASP.Net Core feature called Areas.

An Area helps you logically group related controllers, views, and routes — perfect for something like an Admin dashboard. An MVC area can have its own controllers, views, and routes.

The functionality for users and roles will actually be quite similar to that of vehicles. Administrators should be able to get back a list of users or roles, as well as create, edit, and delete them, and view details of them. In other words we will implement full CRUD capability for users and roles just as we did for vehicles.

The URL schema will look like the following.

/Admin/Users
/Admin/Users/Create
/Admin/Users/Edit
/Admin/Users/Delete
/Admin/Users/Details
/Admin/Roles
/Admin/Roles/Create
/Admin/Roles/Edit
/Admin/Roles/Delete
/Admin/Roles/Details

Create the folder structure

Create a folder called Areas in the root of the FredsCars project. In the Areas folder create a subfolder called Admin. In the Admin folder create two subfolders called Controllers and Views.

In the Controllers folder create two controllers; one named RolesController and one named UsersController.

In the Views folder create two subfolders named Roles and Users. Now create a Razor View file named Index.cshtml in both the Views/Roles and Views/Users folders.

Now the folder structure should look like the following.

Fill the UsersController and RolesController with the following code.

FredsCars\Areas\Controllers\UsersController.cs

using Microsoft.AspNetCore.Mvc;

namespace FredsCars.Areas.Admin.Controllers
{
    [Area("Admin")]
    public class UsersController : Controller
    {
        public IActionResult Index()
        {
            return View();
        }
    }
}

FredsCars\Areas\Controllers\RolesController.cs

using Microsoft.AspNetCore.Mvc;

namespace FredsCars.Areas.Admin.Controllers
{
    [Area("Admin")]
    public class RolesController : Controller
    {
        public IActionResult Index()
        {
            return View();
        }
    }
}

In the code above we have marked both controllers with an Area attribute with the string param of “Area” which specifies an area containing a controller or action.

Both controllers have an Index method returning an Index view.


Now fill the Roles and Users Index views with the following placeholder code.

FredsCars\Areas\Views\Roles\Index.cshtml

@{
    ViewData["Title"] = "Roles";
}

<div class="container-fluid py-4">
    <h3 class="text-center bg-primary-subtle py-2"
        style="border: 1px solid black;">
        Roles
    </h3>
</div>

FredsCars\Areas\Views\Users\Index.cshtml

@{
    ViewData["Title"] = "Users";
}

<div class="container-fluid py-4">
    <h3 class="text-center bg-primary-subtle py-2"
        style="border: 1px solid black;">
        Users
    </h3>
</div>

We also need to create a _ViewStart in the Areas/Admin/Views folder just like we did for the Views folder in the root of the FredsCars project.

Create a file named _ViewStart.cshtml and fill it with the following code.

FredsCars\Areas\Admin\Views_ViewStart.cshtml

@{
    Layout = "_Layout";
}

Configure Routing

Modify Program.cs to add routing for areas.

FredsCars\Program.cs

... existing code ...
// Add custom routes
app.MapControllerRoute("cat-and-page",
    "{category}/Page{pageNumber:int}",
    new { Controller = "Home", action = "Index" });

app.MapControllerRoute("paging",
    "Page{pageNumber:int}",
    new { Controller = "Home", action = "Index" });

/*** Area route mapping ***/
app.MapControllerRoute(
    name: "areas",
    pattern: "{area:exists}/{controller=Home}/{action=Index}/{id?}");

/*** Add endpoints for controller actions and
       the default route ***/
app.MapDefaultControllerRoute();
... existing code ...

Make sure the new area routing comes before the default route.


Now restart the application and navigate to https://localhost:40443/Admin/Users.

You browser should look similar to the following.

Navigate to https://localhost:40443/Admin/Roles and your browser should look similar to the following.

Locking Down Controllers and Actions with Authorize

We don’t want just any user to have access to the Users and Roles controllers and actions. We only want administrators and staff to have access to these areas of the application. Not customers.

So we are going to lock down these controllers to only authorized personnel using the Authorize attribute from the Microsoft.AspNetCore.Authorization namespace.

Modify the Users and Roles controllers with the following code.

FredsCars\Areas\Admin\Controllers\UsersController.cs

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

namespace FredsCars.Areas.Admin.Controllers
{
    [Area("Admin")]
    [Authorize]
    public class UsersController : Controller
    {
        public IActionResult Index()
        {
            return View();
        }
    }
}

FredsCars\Areas\Admin\Controllers\RolesController.cs

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

namespace FredsCars.Areas.Admin.Controllers
{
    [Area("Admin")]
    [Authorize]
    public class RolesController : Controller
    {
        public IActionResult Index()
        {
            return View();
        }
    }
}

In both controllers above, we have brought in the Microsoft.AspNetCore.Authorization namespace and marked both controllers with the Authorize attribute.


Now, restart the application. In one browser navigate to https://localhost:40443/Admin/Users. In another browser navigate to https://localhost:40443/Admin/Roles.

The first browser will be redirected to the URL:
https://localhost:40443/Account/Login?ReturnUrl=%2FAdmin%2FUsers%2F.

The second browser will be redirected to the URL:
https://localhost:40443/Account/Login?ReturnUrl=%2FAdmin%2FRoles.

Because the user of the browser is not authenticated, it is redirected to a URL pointing to the Login view of an Account controller (neither of which is created yet. We will create these in a later section) with a query string containing a return URL.
The return URL for the Users Index view is:
ReturnUrl=%2FAdmin%2FUsers%2F.
The escape sequence ‘%2F’ translates to a forward slash ‘/’.
So the return URL is:
ReturnUrl=/Admin/Users/.
The same pattern is used for the Roles return URL.

Since the redirect cannot find a Login view in an Account controller, because neither exist yet, the request is redirected once more to the Status Code page we set up in the last module, Error Handling, with the UseStatusCodePagesWithReExecute middleware extension method in Program.cs.

app.UseStatusCodePagesWithReExecute("/Home/Status", "?code={0}");

Identity Seeding: 1st user -> admin

The [Authorize] attribute we applied to the Users and Roles controllers in the last section block all anonymous users (any user not authenticated).

In order to let a user have access to controllers and actions locked down in this way we need to build a login page in an account controller so a user can login and authenticate his or herself.

But first in this section, we are going to seed the Identity database with an Admin user account so that when we do build the login in the next section, there will be an already existing account to login with. This will also act as a superuser or backdoor into the system so that there will be some way to create more users and roles through a user interface.

Create a new class called IdentitySeedData in the Models folder of the FredsCars project.

FredsCars\Models\IdentitySeedData.cs

using FredsCars.Models.Identity;
using Microsoft.AspNetCore.Identity;

namespace FredsCars.Models
{
    public class IdentitySeedData
    {
        private const string _adminUserName = "admin";
        private const string _adminPassword = "Adm!n863";
        private const string _adminEmail = "admin@fredscars.com";
                
        public static async Task Initialize(UserManager<ApplicationUser> userManager)
        {
            // Ensure admin user exists
            var adminUser = await userManager.FindByNameAsync("admin");
            if (adminUser == null)
            {
                adminUser = new ApplicationUser
                {
                    UserName = _adminUserName,
                    Email = _adminEmail,
                    EmailConfirmed = true
                };

                var result = await userManager.CreateAsync(adminUser, _adminPassword);
            }
        }
    }
}

The code above contains a static asynchronous class method that will be called from Program.cs and passed a UserManager for an ApplicationUser type which inherits from IdentityUser.

We start off by searching the Identity AspNetUsers table for a user with the UserName “admin” through the Identity API.

var adminUser = await userManager.FindByNameAsync("admin");

The UserManager<T>, in this case UserManager<ApplicationUser>, received as a parameter in the Initialize method named userManager provides the APIs for managing users in a persistence store; which for us is SQL Server.

If no user with that UserName is found we create a new user again with through the UserManager API this time with the CreateAsync method.

if (adminUser == null)
{
    adminUser = new ApplicationUser
    {
        UserName = _adminUserName,
        Email = _adminEmail,
        EmailConfirmed = true
    };

    var result = await userManager.CreateAsync(adminUser, _adminPassword);
}

And finally, we need to call the IdentitySeedData.Initialize method from Program.cs.

Modify Program.cs with the following code.

FredsCars\Program.cs

... existing code ...

/*** Seed the business database ***/
if (builder.Environment.IsDevelopment())
{
    // Alternative C# 8 using declaration.
    using var scope = app.Services.CreateScope();
    var context = scope.ServiceProvider
            .GetRequiredService<FredsCarsDbContext>();
    SeedData.Initialize(context);
}
/*** Seed the identity database ***/
if (!builder.Environment.IsEnvironment("Test"))
{
    using var scope = app.Services.CreateScope();
    var userManager = scope.ServiceProvider
        .GetRequiredService<UserManager<ApplicationUser>>();
    //var roleManager = scope.ServiceProvider
    //    .GetRequiredService<RoleManager<IdentityRole>>();

    await IdentitySeedData.Initialize(userManager);
}

app.Run();
... existing code ...

In the code above we seed the identity database when the environment is anything but Test. So Development and Production will be seeded with the admin user.

if (!builder.Environment.IsEnvironment("Test"))
{
   ...
}

Similar to where we seed the business database, here we also start off by creating a new IServiceScope that can be used to resolve services.

using var scope = app.Services.CreateScope();

Then we use the GetRequiredService<T> method of the ISericeScope (the ServiceProvider) to retrieve an instance of UserManager<ApplicationUser> and assign it to a variable named userManager.

using var scope = app.Services.CreateScope();
var userManager = scope.ServiceProvider
    .GetRequiredService<UserManager<ApplicationUser>>();

Finally, we make the asynchronous call to IdentitySeedData.Initialize passing it the UserManager<T> instance.


Now restart the application.

If you view the data in the AspNetUsers table you will see the existing user with a UserName of “admin”.

In the above screenshot I’ve selected some of the more interesting columns.
Id is a GUID value. The password we gave the admin user when creating a new IdentityUser, “Adm!n863”, is stored as a Hash value for security reasons. We set EmailConfirmed to 1 (true) so we won’t have to verify the email the first time we log in. LockoutEnabled is 1 (true) by default.

Build a Login: Authenticate & Authorize

In this section we are going to create the Login feature.

Create the Login Model

Before we create the Account controller, let’s set up a Login view model in order to make it easier to pass user login information back and forth between the HttpPost Login method and the Login view.

Create a new folder named ViewModels in the Models/Identity folder in the FredsCars project. In the new ViewModels folder create a class named LoginViewModel and fill it with the code below.

FredsCars\Models\Identity\ViewModels\LoginViewModel.cs

using System.ComponentModel.DataAnnotations;

namespace FredsCars.Models.Identity.ViewModels
{
    public class LoginViewModel
    {
        [Required]
        [Display(Name = "User Name")]
        public required string UserName { get; set; }

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

        [Display(Name = "Remember Me")]
        public bool RememberMe { get; set; }
    }
}

The Login view model above has three properties.

  1. UserName is marked with the Display attribute giving it a display name of “User Name”.
  2. Password is marked with the the DataType attribute and the Password hint from the DataType enumeration. Most modern browsers will render text in password fields using a visual character like an asterisk (*) or a filled dot (•) to obscure the entered characters, rather than displaying the actual text.

Both UserName and Password properties have the Required validation attribute prohibiting a user from submitting the Login form without a value for both of these fields and are marked with the required C# keyword to get rid of the null warning.

3. The RememberMe property is a bool value used in the Login Post action to decide whether or not to persist the login with a cookie.

Create the Account controller

Create a new controller in the FredsCars controllers folder named AccountController and fill it with the code below.

FredsCars\Controllers\AccountController.cs

using FredsCars.Models.Identity;
using FredsCars.Models.Identity.ViewModels;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;

namespace FredsCars.Controllers
{
    public class AccountController : Controller
    {
        private readonly SignInManager<ApplicationUser> _signInManager;

        public AccountController(SignInManager<ApplicationUser> signInManager)
        {
            _signInManager = signInManager;
        }

        [HttpGet]
        public ViewResult Login(string? returnUrl = null) 
        {
            ViewData["ReturnUrl"] = returnUrl;
            return View();
        }

        [HttpPost]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> Login(LoginViewModel model, string? returnUrl = null)
        {
            ViewData["ReturnUrl"] = returnUrl;

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

            var result = await _signInManager.PasswordSignInAsync(
                model.UserName,
                model.Password,
                model.RememberMe,
                lockoutOnFailure: false
            );

            if (result.Succeeded)
            {
                if (string.IsNullOrEmpty(returnUrl))
                    return RedirectToAction("Index", "Home");
                else
                    return LocalRedirect(returnUrl);
            }

            ModelState.AddModelError(string.Empty, "Invalid login attempt.");
            return View(model);
        }

        [HttpPost]
        public async Task<IActionResult> Logout()
        {
            await _signInManager.SignOutAsync();
            return RedirectToAction("Index", "Home");
        }
    }
}

In the code above we start by assigning an instance of the Identity SignInManager service which provides the APIs for user sign in to a private class field named _signInManager through Dependency injection in the constructor. Note SignInManager is generic of type T, SignInManager<T>, in this case of type ApplicationUser, SignInManager<ApplicationUser> just like UserManager<ApplicationUser> in IdentitySeedData.

The HttpGet Login action method takes in as a parameter a return URL as a nullable string and assigns it to a property named ReturnUrl in the ViewBag through the controller’s ViewData dictionary object. The action then returns the Login View to render in the browser.

The HttpPost Login action method takes in as a parameter an instance of the LoginViewModel class and a return URL as a nullable string. The return URL is again assigned to a property named ReturnUrl in the ViewBag object.

The model is then checked to make sure all values are valid and if not the Login view is returned with the model including its errors.

If the model is valid an attempt to sign in using the user login information from the model is made via the SignInManager.PasswordSignInAsync method.

var result = await _signInManager.PasswordSignInAsync(
    model.UserName,
    model.Password,
    model.RememberMe,
    lockoutOnFailure: false
);

We indicate that the user account should not be locked if the sign in attempt fails by setting the lockoutOnFailure parameter to false. And, PasswordSignInAsync returns a SignInResult object which is assigned to a variable named result.

We then check the SignInResult‘s Succeeded property which indicates whether or not the sign on was successful; true if so and false if not. If the sign in was successful and the returnUrl received as a parameter was not null the browser gets redirected to that URL which could be /Admin/Users or /Admin/Roles. If the returnURL is null the browser gets redirected to Index/Home.

if (result.Succeeded)
{
    if (string.IsNullOrEmpty(returnUrl))
        return RedirectToAction("Index", "Home");
    else
        return LocalRedirect(returnUrl);
}

If the sign in is not successful the redirect never happens and an error is added to the ModelState.

ModelState.AddModelError(string.Empty, "Invalid login attempt.");

Here, the Key parameter to AddModelError is set to string.Empty so the error message, “Invalid login attempt.”, gets added to the ModelState object itself and not one of the properties of the model such as UserName or Password.

Finally, for an unsuccessful login attempt a ViewResult is returned with the Login view and LoginViewModel as the model.

return View(result);

The last action method in the Account controller is Logout which uses the SignInManager.SignOUtAsync method to sign a user out and redirects the browser to Index/Home.

Create the Login view

Now let’s create the Login View. Create a subfolder called Account in the FredsCars/Views folder. In the new folder create a view named Login.cshtml and fill it with the code below.

FredsCars\Views\Account\Login.cshtml

@using FredsCars.Models.Identity.ViewModels;
@model LoginViewModel
@{
    ViewData["Title"] = "Login";
}

<div class="container-fluid mt-3">
    <h1 class="text-center bg-primary-subtle py-2"
        style="border: 1px solid black;">
        Login
    </h1>
</div>

<form asp-action="Login" method="post">
    <div class="container">
        <div asp-validation-summary="ModelOnly" class="text-danger"></div>
        <input type="hidden" value="@ViewData["ReturnUrl"]" />

        <div class="mb-3 col-4">
            <label asp-for="UserName" class="form-label fw-bold"></label>
            <input asp-for="UserName" class="form-control" />
            <span asp-validation-for="UserName" class="text-danger"></span>
        </div>

        <div class="mb-3 col-4">
            <label asp-for="Password" class="form-label fw-bold"></label>
            <input asp-for="Password" class="form-control" />
            <span asp-validation-for="Password" class="text-danger"></span>
        </div>

        <div class="form-check mb-3 col-4">
            <label asp-for="RememberMe" class="form-label fw-bold"></label>
            <input asp-for="RememberMe" class="form-check-input" />
            <span asp-validation-for="RememberMe" class="text-danger"></span>
        </div>

        <button type="submit" class="btn btn-primary">
            Login
        </button>
    </div>
</form>

<partial name="_ValidationScriptsPartial" />

In the code above I have set up the login form similarly to the Vehicles edit form as far as Bootstrap styles go.

We have a using statement at the top of the file to bring in the FredsCars.Models.Identity.ViewModels namespace so we can use LoginViewModel as the model of the view

@using FredsCars.Models.Identity.ViewModels;
@model LoginViewModel

There is a validation summary tag helper at the top of the form and a hidden field to catch the return URL from the ViewData dictionary and pass it back up to the HttpPost Login action on submission.

<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<input type="hidden" value="@ViewData["ReturnUrl"]" />

Next we have div groups for the three important properties in the LoginView model with the information needed to login; UserName, Password, and RememberMe.

<div class="mb-3 col-4">
    <label asp-for="UserName" class="form-label fw-bold"></label>
    <input asp-for="UserName" class="form-control" />
    <span asp-validation-for="UserName" class="text-danger"></span>
</div>

<div class="mb-3 col-4">
    <label asp-for="Password" class="form-label fw-bold"></label>
    <input asp-for="Password" class="form-control" />
    <span asp-validation-for="Password" class="text-danger"></span>
</div>

<div class="form-check mb-3 col-4">
    <label asp-for="RememberMe" class="form-label fw-bold"></label>
    <input asp-for="RememberMe" class="form-check-input" />
    <span asp-validation-for="RememberMe" class="text-danger"></span>
</div>

At the bottom of the form is a submit button to submit the Login form.

<button type="submit" class="btn btn-primary">
    Login
</button>

Finally, after the form, we bring in the validation scripts with the partial tag helper.

<partial name="_ValidationScriptsPartial" />

Now restart the application. Navigate to https://localhost:40443/Account/Login and enter anything into the UserName and Password fields other than our Admin account values: admin, Adm!n863. Your browser should look similar to the screenshot below.

Because we entered invalid credentials for UserName and Password, the HttpPost Login action adds the “Invalid login attempt.” error message to the ModelState which is shown by the validation summary tag helper at the top of the form.

Now try logging in with UserName admin and Password Adm!n863. The browser will then navigate back to the Home/Index page showing the Vehicle Results table.

Now you should be logged in and if you try to access the Users page, https://localhost:40443/Admin/Users, or the Roles page, https://localhost:40443/Admin/Roles, you will once again see the place holder pages instead of being redirected back to the login page.

Create an Admin Page

We now have an Admin area with a Users controller and Roles controller and placeholder pages for the Index actions and views of both controllers.

But, admin and staff users won’t know about these URLs unless they have been using the system a while or have them bookmarked. It would be nice to provide an admin page where Admin and Staff can find all of the Admin tools.

In this section we will build an Admin tools page. Create two subfolders named Tools under Areas/Admin/Controllers and Areas/Admin/Views in the FredsCars project.

In the Areas/Admin/Controllers folder create a class named ToolsController. In the Areas/Admin/Views create a view named Index.cshtml.

Fill the ToolsController with the following code.

FredsCars\Areas\Admin\Tools\ToolsController.cs

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;

namespace FredsCars.Areas.Admin.Tools
{
    public class ToolsController : Controller
    {
        [Area("Admin")]
        [Authorize]
        public IActionResult Index()
        {
            return View();
        }
    }
}

The above code will simply return the admin Tools view from the Index action method. Note the Index method is marked with the [Authorize] and [Area("Admin"] attributes to lock down the Tools controller to only Authorized users and let ASP.Net Core know this controller is part of the Admin Area.

Next, fill the Index view in the Views/Tools folder with the following code.

FredsCars\Areas\Admin\Views\Tools\Index.cshtml

@{
    ViewData["Title"] = "Admin Tools";
}

<div class="container-fluid py-4">
    <h3 class="text-center bg-primary-subtle py-2"
        style="border: 1px solid black;">
        Tools
    </h3>
</div>

<div class="container">
    <a class="btn btn-primary"
       asp-controller="Users">Users</a>
</div>

<div class="container mt-3">
    <a class="btn btn-primary"
       asp-controller="Roles">Roles</a>
</div>

The view code above has our usual blue page header in a nested h3 element within a div element. Here the text of the page header is Tools.

Next, we have two anchor tag helpers dressed up as Bootstrap buttons with the text Users and Roles. The former points to the Users controller. The later points to the Roles controller. Neither anchor button specifies an action so both will default to the Index action returning the views.

Finally, in order to make tag helpers work in Areas, we need to create a _ViewImports file just like we did for the Views folder in the root of the FredsCars project with a Razor directive letting the Razor view engine know that Microsoft out-of-the-box Tag Helpers should be available.

Create a _ViewImports.cshtml file in the Areas/Views folder in the FredsCars project and fill it with the following code.

FredsCars\Areas\Admin\Views_ViewImports.cshtml

@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

Restart the application and navigate to https://localhost:40443/Admin/Tools.

If you are still logged in your browser should look similar to the following screenshot.

From this page a user can see all of the tools available to them.

Build a Profile Control panel

In this section we are going to build a profile panel so that when a user navigates to our application, they will see a Login button in the upper right corner of the Header. Once logged in they will see a Sign Out button with some profile information and, if they have the Admin or Customer role, an Admin button to get to the Administration Tools.

Normally I like to use partial views for components like this. But I am going to want the profile panel to exist in the header throughout the whole application. So I won’t know which controller or action is sending down any needed model or information. Situations like this are nicely handled with View Components, which we learned about when making the side navigation on the vehicle results page with the Categories view component.

Create the View Component

Create a View Model for the component

Let’s start by creating a view model to make it easier to pass down all of the information we will need in the view from the controller.

Create a class named ProfileControlVM in the Models/ViewModels folder in the FredsCars project.

FredsCars\Models\ViewModels\ProfileControlVM.cs

namespace FredsCars.Models.ViewModels
{
    public class ProfileControlVM
    {
        public bool IsAuthenticated { get; set; }
    }
}

We just need one property to get started with the control panel. In the above code we have one property in the view model class of type bool named IsAuthenticated. The view needs to know if the user is authenticated to know which button to render; Login, or Sign out.

Create the View Component class

Now let’s create the actual view component class that uses the view model. create a class named ProfileControlComponent in the Components folder of the FredsCars project.

FredsCars\Components\ProfileControlComponent.cs

using FredsCars.Models.ViewModels;
using Microsoft.AspNetCore.Mvc;

namespace FredsCars.Components
{
    public class ProfileControlComponent : ViewComponent
    {
        public async Task<IViewComponentResult> InvokeAsync()
        {
            var profileControlVM = new ProfileControlVM
            {
                IsAuthenticated = User.Identity?.IsAuthenticated ?? false
            };
            
            return View(profileControlVM);
        }
    }
}

As noted before we have already built a ViewComponent earlier on; the Categories component to lay out the side nav for categories on the Home/Index page. To refresh our memory about view components, the code for the class above, ProfileControlComponent, inherits from the base class ViewComponent.

Components are called by the InvokeAsync method. Here in InvokeAsync, we instantiate an instance of the profile control view model and set its IsAuthenticated property to the value of the User Identity’s IsAuthenticated property.

var profileControlVM = new ProfileControlVM
{
    IsAuthenticated = User.Identity?.IsAuthenticated ?? false
};

The last statement returns a ViewViewComponentResult result. (Recall the funny naming of the result class for view components. The result from a normal controller is ViewResult. So the result for a view component controller is View[ViewComponent]Result.)


The User object above is a property of the ViewComponent base class of type IPrinciple in the System.Security.Principal namespace.

Principal is a .NET framework concept representing the authenticated identity and roles of a user or other system entity. It allows applications to enforce access controls. It can be a standard Windows identity, or an identity set by a system like ASP.Net Identity Core for application-defined roles, and serves as the basis for determining what actions a user or process is allowed to perform on a system or network.

Create the View Component view

Now let’s create the view for the View Component. Create a subfolder named CategoriesComponent in the Views/Shared/Components folder in the FredsCars project. In the new subfolder create a view named Default.cshtml and fill it with the code below. (Recall that by convention the view name for a ViewComponent is Default.cshtml.)

FredsCars\Views\Shared\Components\ProfileControlComponent\Default.cshtml

@using FredsCars.Models.ViewModels
@model ProfileControlVM

<div class="float-end">
    @if (Model.IsAuthenticated)
    {
        <form asp-controller="Account"
            asp-action="Logout"
            asp-area="" method="post"
            class="d-inline">
            <button type="submit"
                class="btn btn-primary btn-sm">
                Logout
            </button>   
        </form>
    }
    else
    {
        <a class="btn btn-primary btn-sm"
           asp-controller="Account"
           asp-action="Login">
            Sign In
        </a>
    }
</div>
<div class="clearfix"></div> <!-- Clear the float -->

In the Razor code above, we start by bringing in the FredsCars.Models.ViewModels namespace with the C# using keyword so we can mark the model of the view as a ProfileControlVM class.

@using FredsCars.Models.ViewModels
@model ProfileControlVM

We encapsulate the html body of the control into a div element and use the float-end Bootstrap class to make the control float right in the header element.

<div class="float-end">
   ...
</div>

Inside the right floating div element, we use an if block to either render the Login button or the Sign Out button depending on if the user is authenticated or not based on what we set the IsAuthenticated property of the ProfileControlVM view model class to in the controller.

<div class="float-end">
    @if (Model.IsAuthenticated)
    {
       // display Logout button
    }
    else
    {
      // display Sign Out button
    }
</div>

If the user is authenticated, we display the Logout button. The logout button is a submit button embedded in a form tag helper. The form tag helper is configured to navigate to the Logout action of the Account controller.

<form asp-controller="Account"
    asp-action="Logout"
    asp-area="" method="post"
    class="d-inline">
    <button type="submit"
        class="btn btn-primary btn-sm">
        Logout
    </button>   
</form>

I had to include the asp-area attribute of the form tag helper set to an empty string because if we are signing out from one of the Admin area pages without it:
* https://localhost:40443/Admin/Users
* https://localhost:40443/Admin/Roles
* https://localhost:40443/Admin/Tools

ASP.Net Core will look for Account/Login at
https://localhost:40443/Admin/Account/Login
rather then its true location
https://localhost:40443/Account/Login

Call the ProfileControl component in layout

The last thing we need to do is call the view component from the header element in the layout file.

Make the following change to _Layout.cshtml in the Views/Shared folder in the FredsCars project.

FredsCars\Views\Shared_Layout.cshtml

... existing code ...
<body>
    <header>
        Fred's Cars, Trucks & Jeeps
        <vc:profile-control-component />
    </header>

    <div>
        @RenderBody()
    </div>
... existing code ...

In the markup above we are using the ViewComponent tag helper to call and render the ProfileControl view component in the header element.


Now restart the application. If you navigate your browser to https://localhost:40443 there is now a Sign In button in the upper right hand corner in the header.

Now try to navigate to https://localhost:40443/Admin/Users.

Since the Users controller is marked with the [Authorize] attribute the browser gets redirected to the login page with the login form at the URL
https://localhost:40443/Account/Login?ReturnUrl=%2FAdmin%2FUsers.

Notice the query string
?ReturnUrl=%2FAdmin%2FUsers.
which translates to
?ReturnUrl=/Admin/Users

Fill out the User Name and Password fields with admin and Adm!n863 and click the Login button.

Now we get authenticated and redirected back to our original URL request:
https://localhost:40443/Admin/Users. (Admin/Users is still a placeholder page. We will build it out in a later section.)

https://localhost:40443/Admin/Roles
and
https://localhost:40443/Admin/Tools
will have the same behavior.
Take some time and experiment with signing in and signing out from different locations.

Also note if you click the Logout button from any location you are redirected back to the landing page at Home/Index.

Use the Authorize Roles policy

The [Authorize] attribute we have used so far to lock down our admin controllers gives access to anyone verified to be a user in our Identity database. I want to lock it down further now by role. We are going to give access to admin controllers only to users with the Administrator or Staff roles.

Make the following changes to the Admin controllers.

FredsCars\Areas\Admin\Controllers\UsersController.cs

... existing code ...
[Area("Admin")]
[Authorize(Roles = "Administrator,Staff")]
public class UsersController : Controller
... existing code ...

FredsCars\Areas\Admin\Controllers\RolesController.cs

... existing code ...
[Area("Admin")]
[Authorize(Roles = "Administrator,Staff")]
public class RolesController : Controller
... existing code ...

FredsCars\Areas\Admin\Tools\ToolsController.cs

... existing code ...
[Area("Admin")]
[Authorize(Roles = "Administrator,Staff")]
public IActionResult Index()
... existing code ...

In the three code snippets above we added the roles to the Authorize attribute one of which a user needs to access the admin controllers; Administrator or Staff.

Now if you sign out, log back in again, and navigate to https://localhost:40443/Admin/Users the browser is redirected to /Account/AccessDenied with the URL you were trying to access in the querystring.

The browser is then redirected to the Status Codes page with a [404-NotFound] message because we have not created the Access Denied page yet.

Create the Access Denied page

Modify the Account controller with following code.

FredsCars\Controllers\AccountController.cs

... existing code ...
[HttpPost]
public async Task<IActionResult> Logout()
{
    await _signInManager.SignOutAsync();
    return RedirectToAction("Index", "Home");
}

public ViewResult AccessDenied()
{
    return View();
}
... existing code ...

In the code above we have added an AccessDenied action method to the end of the Account controller, the action method in the Account controller ASP.Net Core looks for when access is denied. The method simply returns a view named AccessDenied in the Views/Account folder, which we will create next.

Create a view in the Views/Account folder in the FredsCars project named AccessDenied.cshtml and fill it with the following code.

FredsCars\Views\Account\AccessDenied.cshtml

@{
    ViewData["Title"] = "Access Denied";
}

<div class="container-fluid py-4">
    <h3 class="text-center bg-primary-subtle py-2"
        style="border: 1px solid black;">
        Access Denied
    </h3>
</div>

<div class="container">
    Sorry, you do not have access to this page.
</div>

Now restart the application. If you are still logged in to the FredsCars site, logout with the Logout button and log back in. Navigate to https://localhost:40443/Admin/Users and your browser should look similar to the following.

In the screenshot above the blue banner displays Access Denied and there is a text message that reads, “Sorry, you do not have access to this page”. And the URL reflects the redirect to the Account/AccessDenied page. The querystring contains the URL you were trying to access.
?ReturnUrl=%2FAdmin%2FUsers

Identity Seeding: 1st role -> Administrator

In this section we are going to return to the Identity seeding to add the roles and give the admin user the role of Administrator so it can once again access the admin tools.

Make the following changes to the IdentitySeedData.cs class.

FredsCars\Models\IdentitySeedData.cs

using FredsCars.Models.Identity;
using Microsoft.AspNetCore.Identity;

namespace FredsCars.Models
{
    public class IdentitySeedData
    {
        private const string _adminRole = "Administrator";
        private const string _adminUserName = "admin";
        private const string _adminPassword = "Adm!n863";
        private const string _adminEmail = "admin@fredscars.com";
                
        public static async Task Initialize(UserManager<ApplicationUser> userManager,
            RoleManager<IdentityRole> roleManager)
        {
            // Ensure Administrator and Staff role exists
            var roleAdmin = await roleManager.FindByNameAsync("Administrator");
            var roleStaff = await roleManager.FindByNameAsync("Staff");

            if (roleAdmin == null)
            {
                roleAdmin = new IdentityRole
                {
                    Name = "Administrator"
                };
                var roleAdminresult = await roleManager.CreateAsync(roleAdmin);
            }

            if (roleStaff == null)
            {
                roleStaff = new IdentityRole
                {
                    Name = "Staff"
                };
                var roleAdminresult = await roleManager.CreateAsync(roleStaff);
            }

            // Ensure admin user exists
            var adminUser = await userManager.FindByNameAsync("admin");
            if (adminUser == null)
            {
                adminUser = new ApplicationUser
                {
                    UserName = _adminUserName,
                    Email = _adminEmail,
                    EmailConfirmed = true
                };

                var result = await userManager.CreateAsync(adminUser, _adminPassword);
                if (result.Succeeded)
                {
                    await userManager.AddToRoleAsync(adminUser, _adminRole);
                }
            }
            else
            {
                // Ensure user is in role
                if (!await userManager.IsInRoleAsync(adminUser, _adminRole))
                {
                    await userManager.AddToRoleAsync(adminUser, _adminRole);
                }
            }
        }
    }
}

In the code above we have added a section before creating the admin user to add the Administrator and Staff roles if they do not exist. And in the create admin user section we have added code to ensure the admin user is added to the Administrator role.

The Initialize method now takes in as a parameter an instance of RoleManager<IdentityRole>, in addition to UserManager<ApplicationManager>.

public static async Task Initialize(UserManager<ApplicationUser> userManager,
    RoleManager<IdentityRole> roleManager)

The RoleManager service FindByNameAsync method is used to check if the Administrator and Staff roles exist yet.

var roleAdmin = await roleManager.FindByNameAsync("Administrator");
var roleStaff = await roleManager.FindByNameAsync("Staff");

And if not the RoleService CreateAsync method is used to create them.

var roleAdminresult = await roleManager.CreateAsync(roleAdmin);
var roleAdminresult = await roleManager.CreateAsync(roleStaff);

The UserManager service AddToRoleAsync method is used to add the admin user to the Administrator role.

await userManager.AddToRoleAsync(adminUser, _adminRole);

Now make the following change in Program.cs.

FredsCars\Program.cs

/*** Seed the identity database ***/
if (!builder.Environment.IsEnvironment("Test"))
{
    using var scope = app.Services.CreateScope();
    var userManager = scope.ServiceProvider
        .GetRequiredService<UserManager<ApplicationUser>>();
    var roleManager = scope.ServiceProvider
        .GetRequiredService<RoleManager<IdentityRole>>();

    await IdentitySeedData.Initialize(userManager, roleManager);
}

In the code above we get an instance of RoleManager<IdentityRole> from the Service Provider using the GetRequiredService method and pass it to the IdentitySeedData.Initialize method.


Restart the application in a console window.

In SSOX or SSMS run the following SQL Command.

SELECT * FROM AspNetRoles

There are now two roles showing in the results; Staff and Administrator.

NOTE: Ids in Identity are represented as GUID values. The Id GUIDs will differ in your database for all SQL Server result examples.

Run the following SQL Command.

SELECT * FROM AspNetUserRoles

We get back one result.

In the above results we see the UserId of the admin user, linked to the RoleId of the Administrator role.


Now logout with the Logout button in the upper right corner, click the Sign In button in the upper right corner to get back to the login screen. Login once again as admin with password Adm!n863 and navigate your browser to https://localhost:40443/Admin/Users.

You once again are able to access the Users placeholder page because as the admin user you have the Administrator role.

Add Profile Information to the Control Panel

In this section we are going to add profile information to the control panel. We are going to create an area in the panel that contains a person symbol. When a user clicks on the person symbol, a dropdown menu will pop up with the user’s UserName and a list of the user’s roles will drop down.

Modify the ProfileControlVM class with the code shown below.

FredsCars\Models\ViewModels\ProfileControlVM.cs

namespace FredsCars.Models.ViewModels
{
    public class ProfileControlVM
    {
        public bool IsAuthenticated { get; set; }

        public string? UserName { get; set; }
        public List<string> Roles { get; set; } = new();
    }
}

In the view model code above, we added two properties; UserName and Roles so we can send down the newly needed information from the view component’s controller to the view.


Make the following changes to the ProfileControlComponent class.

FredsCars\Components\ProfileControlComponent.cs

using FredsCars.Models.Identity;
using FredsCars.Models.ViewModels;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;

namespace FredsCars.Components
{
    public class ProfileControlComponent : ViewComponent
    {
        private readonly UserManager<ApplicationUser> _userManager;

        public ProfileControlComponent(UserManager<ApplicationUser> userManager)
        {
            _userManager = userManager;
        }
        
        public async Task<IViewComponentResult> InvokeAsync()
        {
            // Get userName
            string? userName = User.Identity?.Name ?? string.Empty;
            // Get user
            ApplicationUser? user = await _userManager.FindByNameAsync(userName);

            // Get roles
            var roles = new List<string>();
            if (user != null)
            {
                roles = (List<string>)await _userManager.GetRolesAsync(user);
            }
            
            var profileControlVM = new ProfileControlVM
            {
                IsAuthenticated = User.Identity?.IsAuthenticated ?? false,
                UserName = userName,
                Roles = roles
            };
            
            return View(profileControlVM);
        }
    }
}

In the view component’s code above we needed an instance of the UserManager service in order to fetch the user’s roles. We used the standard DI pattern at the top of the file to accomplish this.

private readonly UserManager<ApplicationUser> _userManager;

        public ProfileControlComponent(UserManager<ApplicationUser> userManager)
        {
            _userManager = userManager;
        }

We then added three new tasks before instantiating the ProfileControlVM view model.

First, we get the UserName from the base ViewComponent class’ User.Identity.Name property and assign it to the nullable string called userName. We use userName in two places further down in the code; once in the next line as a parameter to UserManager.FindByNameAsync to fetch the ApplicationUser, and again in the Get roles section as a parameter to the UserManager.GetRolesAsync method.

// Get userName
string? userName = User.Identity?.Name ?? string.Empty;

Secondly, we use the just fetched username to get the ApplicationUser (our custom IdentityUser object) by passing it as a parameter to UserManager.FindByNameAsync.

// Get user
ApplicationUser? user = await _userManager.FindByNameAsync(userName);

And, the third task is to get the roles for the current user. Here we create a new variable named roles of type List of string or List<string>. Then we pass the ApplicationUser to the UserManager.GetRolesAsync method, and assign the results to the roles variable just created.

// Get roles
var roles = new List<string>();
if (user != null)
{
    roles = (List<string>)await _userManager.GetRolesAsync(user);
}

At this point we have we the values we can assign to the two new properties of the view model we prepared in the three steps described above.

var profileControlVM = new ProfileControlVM
{
    IsAuthenticated = User.Identity?.IsAuthenticated ?? false,
    UserName = userName,
    Roles = roles
};

Also note we brought in two new namespaces at the top of the file.
using FredsCars.Models.Identity;
and
using Microsoft.AspNetCore.Identity;


Make the following changes to the ProfileControl Component view.

FredsCars\Views\Shared\Components\ProfileControlComponent\Default.cshtml

@using FredsCars.Models.ViewModels
@model ProfileControlVM

<style>
    .roles-toggle::after {
        display: none !important;
    }
</style>

<div class="float-end">
    @if (Model.IsAuthenticated)
    {
        <form asp-controller="Account"
            asp-action="Logout"
            asp-area="" method="post"
            class="d-inline">
            <button type="submit"
                class="btn btn-primary btn-sm">
                Logout
            </button>   
        </form>
    }
    else
    {
        <a class="btn btn-primary btn-sm"
           asp-controller="Account"
           asp-action="Login">
            Sign In
        </a>
    }
</div>
@if (Model.IsAuthenticated)
{
    <div class="float-end me-2">
        <a class="dropdown-toggle roles-toggle"
            href="#"    
            data-bs-toggle="dropdown"
            role="button"
            id="dropdownRolesLink"
            aria-expanded="false">
            <i class="bi bi-person bg-info btn-sm rounded-circle p-1 border border-white border-1"></i>
        </a>


        <div class="dropdown-menu"
             aria-labelledby="dropdownRolesLink">
              <span class="dropdown-item-text fw-semibold bg-info-subtle"><b>UserName</b></span>
             <hr />
             <span class="dropdown-item-text fw-semibold">@Model.UserName</span>
            <hr class="border-primary border-2" />
            <span class="dropdown-item-text fw-semibold bg-info-subtle"><b>Roles</b></span>
            <hr />
            @foreach (string role in Model.Roles)
            {
                <span class="dropdown-item-text fw-semibold">@role</span>
            }
        </div>
    </div>
}
<div class="clearfix"></div> <!-- Clear the float -->

In the view code above, the first change is the addition of a style tag at the top of the file. I will come back to describe this further down in this section.

The second change is a C# if/block which checks if the user is authenticated.

@if (Model.IsAuthenticated)
{
   ...
}

If so we add a div that contains a Bootstrap person icon that acts as a toggle for a Bootstrap dropdown menu I used as a popup for roles.

<div class="float-end
    me-2">
...
</div>

The containing div shown above has a couple Bootstrap classes.

  • float-end: makes the new person icon contained in the div float right in Bootstrap 5. This has changed from Bootstrap 4 where it was float-right.
  • me-2: creates two pixels of margin space to the right of the person icon div so that the it is not too close to the Logout/Sign In buttons. This has changed from Bootstrap 4 where it was mr-2.

Within the div we use the Bootstrap person icon within an anchor tag and use the anchor element as a toggle to drop down a list of roles contained in the div following the anchor in the code.

<a class="dropdown-toggle roles-toggle"
    href="#"    
    data-bs-toggle="dropdown"
    role="button"
    id="dropdownRolesLink"
    aria-expanded="false">
    <i class="bi bi-person bg-info btn-sm rounded-circle p-1 border border-white border-1"></i>
</a>

We have used Bootstrap icons before in the Vehicle results on the Home/Index page to lay out Details, Edit, and Delete buttons next to each Vehicle result. But the typical convention to layout a BootStrap icon is to dress up an <i> tag with the bi and bi-[iconName] classes. Our layout here looks like this:

<i class="bi bi-person bg-info btn-sm rounded-circle p-1 border border-white border-1"></i>

Again we use several Bootstrap classes on the icon element.

  • bi bi-person: bi denotes a Bootstrap icon and in this case bi-person specifies the person icon.
  • bg-info: Renders the person icon with a teal background.
  • btn-sm: renders the icon smaller then the normal size.
  • rounded-circle: renders the icon with round edges rather than as a square.
  • p-1: gives the icon symbol a padding of 1 pixel so the person image isn’t touching the border making it looked crushed.
  • border border-white border-1: gives the round person icon a white border 1 pixel thick.

The person icon is contained in an anchor element so we can wire it up to act as a toggle for the roles dropdown div.

<a class="dropdown-toggle roles-toggle"
    href="#"    
    data-bs-toggle="dropdown"
    role="button"
    id="dropdownRolesLink"
    aria-expanded="false">
    <i class="bi bi-person bg-info btn-sm rounded-circle p-1 "></i>
</a>

In the above snippet, both the Bootstrap dropdown-toggle class and data-bs-toggle="dropdown" attribute/value are needed to tell Bootstrap that this is a dropdown trigger and to wire it up to the dropdown JavaScript behavior.

Note the id="dropdownRolesLink" attribute/value. The anchor element is assigned an id of dropdownRolesLink so that the dropdown menu will know what anchor or element it is toggled by.

The roles-toggle CSS class on the anchor element refers to the custom CSS class we added at the top of the file.

<style>
    .roles-toggle::after {
        display: none !important;
    }
</style>

Without this style Bootstrap adds a little carrot symbol to the right of the toggle icon which we do not want in this case.

By adding the style we give it a nice clean look.

The UserName and list of roles is contained in a div tag marked with the dropdown-menu Bootstrap class.

    <div class="dropdown-menu"
         aria-labelledby="dropdownRolesLink">
        <span class="dropdown-item-text fw-semibold bg-info-subtle"><b>UserName</b></span>
        <hr />
        <span class="dropdown-item-text fw-semibold">@Model.UserName</span>
        <hr class="border-primary border-2" />
        <span class="dropdown-item-text fw-semibold bg-info-subtle"><b>Roles</b></span>
        <hr />
        @foreach (string role in Model.Roles)
        {
            <span class="dropdown-item-text fw-semibold">@role</span>
        }
    </div>
</div>

The aria-labelledby="dropdownRolesLink" attribute/value tells the Bootstrap DropdownMenu control what anchor or element toggles it on and off. In this case, the id of the previous anchor element just described with an id of dropdownRolesLink.

We show two different headers in the dropdown in span tags with the literal text, “UserName”, and “Roles” each followed by an hr tag as a separator between the headers and their actual content.

<span class="dropdown-item-text fw-semibold bg-info-subtle"><b>UserName</b></span>
<hr />
<span class="dropdown-item-text fw-semibold bg-info-subtle"><b>Roles</b></span>
<hr />

Notice each header span has the Bootstrap bg-info-subtle class giving it a nice light blue background.

Following the UserName span element we show the actual UserName in another span.

<span class="dropdown-item-text fw-semibold">@Model.UserName</span>

Then following the Roles span we use a C# foreach block to iterate through the roles in the Model.Roles property, and render each one in its own a span.

@foreach (string role in Model.Roles)
{
    <span class="dropdown-item-text fw-semibold">@role</span>
}

At the end of the file there is a div element with a Bootstrap clearfix class that deserves some explanation.

<div class="clearfix"></div> <!-- Clear the float -->

When using floats, it’s often necessary to clear the float to prevent layout issues with subsequent content. The clearfix utility class (or a custom clear-fix solution) addresses this.


We need to make one more change for the Bootstrap dropdown menu component to work. Bootstrap components rely on two JavaScript.

  • jquery.js
  • bootstrap.bundle.js

We already imported jQuery and Bootstrap to wire up the details, create, edit, and delete button icons and functionality for the Home/Index Vehicle results page and form validation for the create and edit forms relied on jQuery.

Add links to the JavaScript files for jQuery and the Bootstrap bundle at the end of the layout file.

FredsCars\Views\Shared_Layout.cshtml

... existing code ...    
    <footer class="footer">
        Contact us: (555)555-5555<br />
        121 Fredway St.<br />
        Cartown USA
    </footer>
</body>
</html>

<script src="~/lib/jquery/jquery.js"></script>
<script src="~/lib/bootstrap/js/bootstrap.bundle.js"></script>

Now we just need to remove the link to jQuery in the _ValidationScriptsPartial.cshtml so the jQuery JavaScript file isn’t added twice to the Vehicle Create and Edit pages redundantly.

Make the following changes to the _ValidationScriptsPartial.cshtml file.

FredsCars\Views\Shared_ValidationScriptsPartial.cshtml

@* removed /lib/jquery/jquery.min.js *@
<script src="~/lib/jquery-validation/jquery.validate.min.js"></script>
<script src="~/lib/jquery-validation-unobtrusive/jquery.validate.unobtrusive.min.js"></script>

Now restart the application. If you are logged in, Sign Out and log back in as Admin.

Click the new person icon in the upper right profile control panel and a dropdown containing the UserName and list of roles is displayed. Admin has only one role; Administrator.

Unit Test Fix: Can_Update_Vehicle

At this point the Can_Update_Vehicle unit test fails because we have introduced the UserManager<ApplicationService> into the ProfileControlComponent class via DI.

public class ProfileControlComponent : ViewComponent
{
    private readonly UserManager<ApplicationUser> _userManager;

    public ProfileControlComponent(UserManager<ApplicationUser> userManager)
    {
        _userManager = userManager;
    }

Recall when we wrote the Can_Update_Vehicle unit test we used a CustomWebApplicationFactory class to register all of the needed DbContexts and repos. Now we need to add to that the registration for the UserManager<ApplicationUser> service.

Modify the CustomWebApplicationFactory class with the following changes shown below.

FredsCars.Tests\Infrastructure\CustomWebApplicationFactory.cs

... existing code ... 
// register repos that depend on shared dbContext
 services.AddScoped<IVehicleRepository, EFVehicleRepository>();
 services.AddScoped<IVehicleTypeRepository, EFVehicleTypeRepository>();

 // Mock UserManager so ProfileControlComponent doesn’t throw
 //    Unable to resolve service for type UserManager<ApplicationUser>  
 var store = new Mock<IUserStore<ApplicationUser>>();
 var userManager = new Mock<UserManager<ApplicationUser>>(
     store.Object, null!, null!, null!, null!, null!, null!, null!, null!);
 services.AddScoped(_ => userManager.Object);
... existing code ... 

If you run the Can_Update_Vehicle unit test at this point it should pass successfully.

Add an Admin Button

In this section we are going to add an admin button to the profile control panel in order to give users once logged in if they are an Administrator or Staff a way to get to the Admin Tools page.

Modify the view for the ProfileControlComponent class.

FredsCars\Views\Shared\Components\ProfileControlComponent\Default.cshtml

... existing code ...
<div class="float-end">
    @if (Model.IsAuthenticated)
    {
        <form asp-controller="Account"
            asp-action="Logout"
            asp-area="" method="post"
            class="d-inline">
            <button type="submit"
                class="btn btn-primary btn-sm">
                Logout
            </button>   
        </form>
    }
    else
    {
        <a class="btn btn-primary btn-sm"
           asp-controller="Account"
           asp-action="Login">
            Sign In
        </a>
    }
</div>
@if (Model.IsAuthenticated && 
Model.Roles.Any(r => r is "Administrator" or "Staff"))
{
    <div class="float-end me-2">
        <a asp-controller="Tools"
           asp-action="Index"
           asp-area="Admin"
           class="btn btn-outline-success button">
            <b>
                <i class="bi-wrench"></i>
                Admin
            </b>
        </a>
    </div>
}
@if (Model.IsAuthenticated)
{
    <div class="float-end me-2">
... existing code ...

In the markup code above we have added an Admin button in-between the person icon and the Logout/Sign In buttons.

The new anchor button is contained in an if/block which checks to make sure the user is authenticated AND the user has either the Administrator or Staff roles. Only if this condition passes does the Admin button get rendered to the user.

@if (Model.IsAuthenticated && 
Model.Roles.Any(r => r is "Administrator" or "Staff"))
{
   ...
}

Notice we use a lambda expression and the LINQ Any method to check if the user is in the Administrator OR Staff roles.

Model.Roles.Any(r => r is "Administrator" or "Staff")

The LINQ Any keyword determines whether any element in a sequence satisfies a condition. Here, at least one of the roles of the current user must be an Administrator OR have the Staff role.

Within the if/block is an anchor tag helper with the literal text, “Admin”, and a Bootstrap wrench icon as its content.

<div class="float-end me-2">
    <a asp-controller="Tools"
       asp-action="Index"
       asp-area="Admin"
       class="btn btn-outline-success button">
        <b>
            <i class="bi-wrench"></i>
            Admin
        </b>
    </a>
</div>

The tag helper is wired up to go to the Index action of the Tools controller in the Admin area.

Restart the application, sign in as admin and the new Admin button appears.

Notice on the anchor tag helper we used the Bootstrap btn-outline-success class along with btn and button. This gives the Admin button a nice solid green background color along with white text when the user hovers their mouse over it.

Now if you are signed in as admin and click the Admin button you are taken to the admin tools page.


At this point we should give the user a way to get back to the main home page.

Let’s make the header text in the main layout, “Fred’s Cars, Trucks & Jeeps”, a link back to the home page.

Modify the layout.cshtml file in the Views/Shared folder of the FredsCars project.

FredsCars\Views\Shared_Layout.cshtml

<!DOCTYPE html>

<html>
<head>
    <meta name="viewport" content="width=device-ixwidth" />
    <title>Fred's Cars - @ViewBag.Title</title>
    <link href="~/lib/bootstrap/css/bootstrap.min.css" rel="stylesheet" />
    <link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.13.1/font/bootstrap-icons.min.css" rel="stylesheet" />
    <link href="~/css/site.css" rel="stylesheet" />
</head>
<body>
    <header>
        <a asp-controller="Home"
           asp-action="Index"
           asp-area=""
            style="color:white; text-decoration:none">
            Fred's Cars, Trucks & Jeeps</a>
        <vc:profile-control-component />
    </header>

    <div>
        @RenderBody()
    </div>
... existing code ...

In the code above, we surrounded the text, Fred’s Cars, Trucks & Jeeps, in an anchor tag helper wired up to go to the Index action of the Home controller. Links by default are blue and underlined so we added an inline style to the anchor tag changing the text color back to white and removing the underline with the CSS color and text-decoration properties.

Now if you restart the application and log back in as admin, hover over the header text and the cursor turns into a hand letting the user know it is a link. Click the header text and you are taken back to the home page.

Notice now the header text has lost its centering. At this point the header text would probably look better more left aligned. Let’s take care of that real quick.

Make the following changes to the site.css file in the wwwroot/css folder of the FredsCars project.

FredsCars\wwwroot\css\site.css

... existing code ...
header {
    padding: 5px;
    padding-left: 15px;
    background-color: black;
    color: white;
    /*text-align: center;*/
    font-size: 16pt;
    font-weight: bold;
    font-family: arial;
}
... existing code ...

In the CSS code above, we commented out, text-align: center, to get rid of the centering and added, padding-left: 15px, to give a little padding between the header text and the left edge.

Now restart the application and navigate to https://localhost:40443. The top bar now has everything nicely positioned with the header text to the left and the profile control panel right aligned.

Build out the Roles Admin pages

Now we finally have everything in place to start building out the admin tools. In this section we are going to build out the Roles admin pages and features.

A lot of the steps here will look very similar to the steps we took to build the Home/Index page with the Vehicle Results. Our learning curve should be getting higher now so I am going to pick up the speed a little.

Roles Index/List feature

Let’s start by developing the Roles landing page much like we did for the Vehicles landing page at Home/Index.

Create the Roles Repository

Create the Roles Repo Interface

Create a new subfolder in the Models/Identity folder of the FredsCars project named Repositories. In the new folder create an Interface file named IRolesRepository.cs and fill it with the code below.

FredsCars\Models\Identity\Repositories\IRolesRepository.cs

using Microsoft.AspNetCore.Identity;

namespace FredsCars.Models.Identity.Repositories
{
    public interface IRolesRepository
    {
        IQueryable<IdentityRole> Roles { get; }
    }
}
Create the Roles Repo Implementation

Create a class file in the Models/Identity/Repositories folder named EFRolesRepository.cs and fill it with the code below.

FredsCars\Models\Identity\Repositories\EFRolesRepository.cs

using FredsCars.Data;
using Microsoft.AspNetCore.Identity;

namespace FredsCars.Models.Identity.Repositories
{
    public class EFRolesRepository : IRolesRepository
    {
        private FCMvcIdentityCoreDbContext _context;

        public EFRolesRepository(FCMvcIdentityCoreDbContext context)
        {
            _context = context;
        }

        public IQueryable<IdentityRole> Roles => _context.Roles;
    }
}

In the code above we get the Identity context, an instance of the FCMvcIdentityCoreDbContext service, using the DI pattern and assign it to a private class field named _context.

private FCMvcIdentityCoreDbContext _context;

public EFRolesRepository(FCMvcIdentityCoreDbContext context)
{
    _context = context;
}

Next, we implement the Roles property of the repo’s interface by getting a list of Roles (as a DbSet) from the IdentityDbContext base class’ Roles property and assigning it as an IQueryable much like we did for Vehicles. Remember DbSet maps well to IQueryable and allows flexibility if we choose to get into sorting, paging, and filtering.

public IQueryable<IdentityRole> Roles => _context.Roles;
Register the Roles Repository

Next, we need to register the Roles Repo service in the DI Service container. Make the following changes to the Program.cs file.

FredsCars\Program.cs

... existing code ...
builder.Services.AddScoped<IVehicleRepository, EFVehicleRepository>();
builder.Services.AddScoped<IVehicleTypeRepository, EFVehicleTypeRepository>();
builder.Services.AddScoped<IRolesRepository, EFRolesRepository>();

var app = builder.Build();
... existing code ...

Notice we also brought in a new namespace at the top of the file.

using FredsCars.Models.Identity.Repositories;

Create the Roles List View Model

When working with Vehicles, we used a view model called VehiclesListViewModel.cs. Let’s make a similar view model here for roles.

In the Models/Identity/ViewModels folder of the FredsCars project, create a class named RolesListViewModel and fill it with the code below.

FredsCars\Models\Identity\ViewModels\RolesListViewModel.cs

using Microsoft.AspNetCore.Identity;

namespace FredsCars.Models.Identity.ViewModels
{
    public class RolesListViewModel
    {
        public List<IdentityRole> Roles { get; set; } = new();
    }
}

Update _ViewImports file for Admin Area

Next, we need to add a using statement to the Admin Area’s _ViewImports file so the Roles/Index View can find the RolesListViewModel class and use it as its model. Make the following addition as shown in the code below.

FredsCars\Areas\Admin\Views_ViewImports.cshtml

@using FredsCars.Models.Identity.ViewModels
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

Update the Roles Controller

Modify the Roles controller with the code shown below.

FredsCars\Areas\Admin\Controllers\RolesController.cs

using FredsCars.Models.Identity.Repositories;
using FredsCars.Models.Identity.ViewModels;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;

namespace FredsCars.Areas.Admin.Controllers
{
    [Area("Admin")]
    [Authorize(Roles = "Administrator,Staff")]
    public class RolesController : Controller
    {
        private IRolesRepository _repo;

        public RolesController(IRolesRepository repo)
        {
            _repo = repo;
        }

        public async Task<ViewResult> Index()
        {
            var roles = _repo.Roles;

            return View(new RolesListViewModel
            {
                Roles = await roles
                    .AsNoTracking()
                    .OrderBy(r => r.Name)
                    .ToListAsync()
            });
        }
    }
}

In the code above, we inject the RolesRepository service into the constructor and assign it to a private class field named _repo.

private IRolesRepository _repo;

public RolesController(IRolesRepository repo)
{
    _repo = repo;
}

Next we mark the Index method as asyncronous (so we can use the ToListAsync method of the Roles IQueryable in the return statement) and specify it returns a Task of ViewResult.

public async Task<ViewResult> Index()
{
   ...
}

Inside the Index method we grab the IQueryable of IdentityRoles from the repository and assign it to a variable called roles.

var roles = _repo.Roles;

Finally, we call the Roles/Index view in a return statement and pass to it as the model an instance of the RolesListViewModel class. The RolesList view model is instantiated inline in the return statement and we set its Roles property to the asynchronous result (using the await keyword) of calling the ToListAsync method of the roles IQueryable.

return View(new RolesListViewModel
{
    Roles = await roles
        .AsNoTracking()
        .OrderBy(r => r.Name)
        .ToListAsync()
});

Notice we use the OrderBy LINQ method to order Roles by Name and the AsNoTracking method to turn off EF Core entity tracking for state changes such as updated, created, and deleted for better performance.

Update the Roles View

Modify the Index View for the Roles controller with the code shown below.

FredsCars\Areas\Admin\Views\Roles\Index.cshtml

@model RolesListViewModel
@{
    ViewData["Title"] = "Roles";
}

<div class="container-fluid py-4">
    <h3 class="text-center bg-primary-subtle py-2"
        style="border: 1px solid black;">
        Roles
    </h3>
</div>

<div class="container"
     style="margin-top: 20px; border: 1px solid black">

    <!-- Results -->
    <table class="results table table-striped">
        <thead>
            <tr>
                <th></th>
                <th>@Html.DisplayNameFor(m => m.Roles[0].Name)</th>
            </tr>
        </thead>
        <tbody>
            @foreach (var r in Model.Roles)
            {
                <tr>
                    <td></td>
                    <td>@r.Name</td>
                </tr>
            }
        </tbody>
    </table>

</div>

In the markup code above, we first declared the model of the view to be of type RolesListViewModel with the @model Razor directive.

@model RolesListViewModel

Next we added a container div (using the Bootstrap container class) to hold the results of the User List again much the same as we did for Vehicles on the Home/Index page.

<div class="container"
     style="margin-top: 20px; border: 1px solid black">

    <!-- Results -->
    ...
</div>

Inside of the container div tag we create a table with thead and tbody elements. The table uses the Bootstrap table and table-striped classes to alternate colors between rows.

<table class="results table table-striped">
        <thead>
            <tr>
               ...
            </tr>
        </thead>
        <tbody>
           ...
        </tbody>
    </table>

Inside of the thead tag we create one table row. Inside of the table row we have one empty th tag. The matching td tag in the tbody section will contain a delete icon button. The second th tag renders as its content the name of the Roles property in the model using the Html helper object’s DisplayNameFor method.

<tr>
                <th></th>
                <th>@Html.DisplayNameFor(m => m.Roles[0].Name)</th>
            </tr>

Within the tbody tag we have a foreach statement that iterates through all of the roles in the repository and lays out a tr element for each one. Within the tr we lay out one empty td (a placeholder for the delete icon button we will eventually need and any other icon buttons or links we will need).

@foreach (var r in Model.Roles)
{
	<tr>
		<td></td>
		<td>@r.Name</td>
	</tr>
}

Restart the application and if you are not logged in go ahead and log in as admin (admin, Adm!n863) and navigate to https://localhost:40443/Admin/Roles. You browser should look similar to the screenshot below and you should see a list of users; Administrator and Staff.

The Create Role feature

Next, we need to give administrators and staff a way to create new roles. In this section we are going to develop the Create Role feature.

Update the Roles Repository

The first thing we need to do is update the roles repository with a Create method.

Modify the Roles Repo Interface

Make the following changes to the IRolesRepository Interface.

FredsCars\Models\Identity\Repositories\IRolesRepository.cs

using Microsoft.AspNetCore.Identity;

namespace FredsCars.Models.Identity.Repositories
{
    public interface IRolesRepository
    {
        IQueryable<IdentityRole> Roles { get; }

        Task CreateAsync(IdentityRole role);
    }
}

Here we just added the CreateAsync method to the Interface which as you’ll recall acts as a kind of API a developer can interact with. The new method takes in an Identity role as a parameter and returns a Task since it is going to be asynchronous. Returning Task for an asynchronous method is like returning void for a Synchronous method.

Modify the Roles Repo Implementation

Modify the EFRolesRepository class with the code shown below.

FredsCars\Models\Identity\Repositories\EFRolesRepository.cs

... existing code ...
public class EFRolesRepository : IRolesRepository
{
	... existing code ...

	public IQueryable<IdentityRole> Roles => _context.Roles;

	public async Task CreateAsync(IdentityRole role)
	{
		_context.Roles.Add(role);
		await _context.SaveChangesAsync();
	}
}
... existing code ...

Here we implement the new CreateAsync method for concrete instances of the repository service. The implementation follows the interface contract and takes in as a parameter an IdentityRole object and returns Task. It uses the Identity Core dbContext to add a role and the EF Core SaveChangesAsync method to save the changes to the database asynchronously.

Update the Roles Controller

Modify the RolesController class with the code shown below.

FredsCars\Areas\Admin\Controllers\RolesController.cs

using FredsCars.Models.Identity.Repositories;
using FredsCars.Models.Identity.ViewModels;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;

namespace FredsCars.Areas.Admin.Controllers
{
    [Area("Admin")]
    [Authorize(Roles = "Administrator,Staff")]
    public class RolesController : Controller
    {
        private IRolesRepository _repo;

        public RolesController(IRolesRepository repo)
        {
            _repo = repo;
        }

        public async Task<ViewResult> Index()
        {
            var roles = _repo.Roles;

            return View(new RolesListViewModel
            {
                Roles = await roles
                    .AsNoTracking()
                    .OrderBy(r => r.Name)
                    .ToListAsync()
            });
        }

        public IActionResult Create()
        {
            return View();
        }

        [HttpPost]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> Create([Bind("Name")] IdentityRole role)
        {
            if (string.IsNullOrWhiteSpace(role.Name))
            {
                ModelState.AddModelError("Name", "Role name is required");
            }

            var dupRole = _repo.Roles.Where(r => r.Name == role.Name).FirstOrDefault();
            if (dupRole != null)
            {
               ModelState.AddModelError("Name", "Duplicate Role. Role already exists.");
            }
            
            if (ModelState.IsValid)
            {
                if (role.NormalizedName == null)
                {
                    role.NormalizedName = role.Name!.ToUpper();
                }
                
                await _repo.CreateAsync(role);
                return RedirectToAction("Index", "Roles");
            }

            return View(role);
        }
    }
}

In the controller code above we add the HttpGet and HttpPost versions of the Create action methods.

The HttpGet method simply returns a Create form to create a new role.

public IActionResult Create()
{
    return View();
}

Of course we know by now we don’t need to mark this method with the HttpGet attribute because it is the Get method by default.

The HttpPost version of Create takes in as a parameter an IdentityRole object. Of course we use the Bind attribute to protect against overposting and only allow the property “Name” to be bound to the IdentityRole object.

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Create([Bind("Name")] IdentityRole role)
{
   ...
}

In the body of the method we start by ensuring the incoming IdentityRole has a populated Name property and that Name is not null. If Name is null or white space we add an error to the Name property of the ModelState (which will be represented by a Model of type IdentityRole in the view).

if (string.IsNullOrWhiteSpace(role.Name))
{
    ModelState.AddModelError("Name", "Role name is required");
}

If the incoming IdentityRole object passes the null check, we do a second check to make sure it is not a duplicate role by searching the Roles repository for a role with the same name. If the role is a duplicate we add a dup error message to the Name property of the ModelState.

var dupRole = _repo.Roles.Where(r => r.Name == role.Name).FirstOrDefault();
if (dupRole != null)
{
    ModelState.AddModelError("Name", "Duplicate Role. Role already exists.");
}

Next, we use an if block to ensure the ModelState is valid. If so we use the new method in the Roles Repository, CreateAsync, to add the new role to the database and redirect to the Roles Index/List page.

if (ModelState.IsValid)
{
    if (role.NormalizedName == null)
    {
        role.NormalizedName = role.Name!.ToUpper();
    }
    
    await _repo.CreateAsync(role);
    return RedirectToAction("Index", "Roles");
}

But notice we have to do one thing in the if/block before we actually create and save the role. We have to check that the NormalizedName property of the IdentityRole object is not null and if it is we need to set the NormalizedName property to the upper case version of the Name property. This is important because Identity Core, EF Core, and SQL Server use the NormalizedName property to perform case insensitive searches.

if (ModelState.IsValid)
{
    if (role.NormalizedName == null)
    {
        role.NormalizedName = role.Name!.ToUpper();
    }
    
    await _repo.CreateAsync(role);
    return RedirectToAction("Index", "Roles");
}

Finally, if the ModelState is not valid, we return a ViewResult again with the Create view in the Areas/Admin/Views/Roles folder.

return View(role);

Update the Admin Area _ViewImports file

Next, let’s update the _viewImports file for the Admin area so we don’t need to put a using statement for the Microsoft.AspNetCore.Identity namespace in all of our Roles and Users views.

Modify the _ViewImports file for the Admin area as shown below.

FredsCars\Areas\Admin\Views_ViewImports.cshtml

@using FredsCars.Models.Identity.ViewModels
@using Microsoft.AspNetCore.Identity
@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers

Add the Roles Create view

Create a new Razor View file named Create.cshtml in the Areas/Admin/Views/Roles folder of the FredsCars project and fill it with the markup code shown below.

@model IdentityRole

@{
    ViewData["Title"] = "Create Role";
}

<div class="container-fluid mt-3">
    <h3 class="text-center bg-primary-subtle py-2"
        style="border: 1px solid black;">
        Create a Role
    </h3>
</div>

<form asp-area="Admin" asp-controller="Roles" asp-action="Create">
    <div class="container"
         style="margin-top: 20px; border: 0px solid black">
        <div asp-validation-summary="ModelOnly" class="text-danger"></div>
        <div class="row">
            <div class="col-4">
                <div class="mb-3">
                    <label asp-for="Name" class="form-label fw-bold"></label>
                    <input asp-for="Name" class="form-control" />
                    <span asp-validation-for="Name" class="text-danger"></span>
                </div>
            </div>
        </div>
        <div class="row">
            <div class="col-4 text-center">
                <div>
                    <input type="submit"
                           value="Create"
                           class="btn btn-primary" />
                    <a asp-area="Admin" asp-controller="Roles"
                       asp-action="Index"
                       class="btn btn-secondary">Cancel</a>
                </div>
            </div>
        </div>
    </div>
</form>

In the code above we set the model of the view to an IdentityRole, the title in the ViewData dictionary to “Create Role” and layout our usual blue header which here reads, “Create a Role”.

@model IdentityRole

@{
    ViewData["Title"] = "Create Role";
}

<div class="container-fluid mt-3">
    <h3 class="text-center bg-primary-subtle py-2"
        style="border: 1px solid black;">
        Create a Role
    </h3>
</div>

Next we have a form tag helper which will render a form to create a new role.

<form asp-area="Admin" asp-controller="Roles" asp-action="Create">
   ...
</form>

The form upon submission is set via the asp-area attribute to go to the Admin area in order to find the Roles controller and the Roles/Create view.

Within the form tag we have a Bootstrap container div to hold all of the form content; form-groups, labels, input controls, and validators.

<form asp-area="Admin" asp-controller="Roles" asp-action="Create">
    <div class="container"
         style="margin-top: 20px; border: 0px solid black">
   ...
   </div>
</form>

At the top of the content, we have a validation-summary tag helper set to model only so it will display model level errors only and not property errors.

<div asp-validation-summary="ModelOnly" class="text-danger"></div>

Next, we set up a Bootstrap row to hold the form group for the Name property of the IdentityRole model. Nested in the row div is a Bootstrap column div four (out of twelve possible) columns wide.

<div class="row">
    <div class="col-4">
        <div class="mb-3">
            <label asp-for="Name" class="form-label fw-bold"></label>
            <input asp-for="Name" class="form-control" />
            <span asp-validation-for="Name" class="text-danger"></span>
        </div>
    </div>
</div>

Finally, we have a second Bootstrap row to contain the submit and cancel buttons. The submit button will go to the URL created by ASP.Net Core routing configured by the form tag helper’s asp-area, asp-controller, and asp-action attributes which turns out to be /Admin/Roles/Create. The cancel button will go to the Roles controller Index view in the Admin area.

<div class="row">
    <div class="col-4 text-center">
        <div>
            <input type="submit"
                   value="Create"
                   class="btn btn-primary" />
            <a asp-area="Admin" asp-controller="Roles"
               asp-action="Index"
               class="btn btn-secondary">Cancel</a>
        </div>
    </div>
</div>

Add a Create New Link to the Roles Index page

We now have all of the functionality in place to create a new role from the Roles admin tool. We just need to give users a way to get there. Let’s add a Create New link to the Roles Index page.

Modify the Roles/Index view with the following changes.

FredsCars\Areas\Admin\Views\Roles\Index.cshtml

... existing code ...
<div class="container-fluid py-4">
    <h3 class="text-center bg-primary-subtle py-2"
        style="border: 1px solid black;">
        Roles
    </h3>
</div>

<p class="container text-start">
    <a asp-controller="Roles" asp-action="Create">Create New</a>
</p>
<div class="container"
     style="margin-top: 20px; border: 1px solid black">
    <!-- Results -->
... existing code ...

In the above snippet we have added an anchor tag helper as a link set to navigate to the Roles controller Create action. It is contained in a paragraph tag which has the Bootstrap text-start class to left align it with the results below it.


Restart the application and navigate to https://localhost:40443/Admin/Roles.

Click the Create New link.

Your browser will navigate to the new role form and should look similar to the screen shot below.

Try clicking the Create button without entering a Name for the new role. Leave the Name field blank.

You’ll see the “Role name is required” error message for the name property.

Now enter “TestRole” into the name field and click the create button.

Your browser should be redirected back to the Roles Index page and the new role named TestRole should show in the results.

Now if you were to go back to the Roles Create form and tried to enter TestRole for the name again and click the Create button you would see the duplicate role error message.

Now if you check for the new role in the AspNetRoles table of the FCMvcIdentity database you will see the new role with the Name TestRole and NormalizedName TESTROLE.

Recall we made sure to set the NormalizedName property in the controller so case insensitive searches would work.

if (ModelState.IsValid)
{
    if (role.NormalizedName == null)
    {
        role.NormalizedName = role.Name!.ToUpper();
    }
    
    await _repo.CreateAsync(role);
    return RedirectToAction("Index", "Roles");
}

The Delete Role feature

In this section we are going to give administrators and staff a way to delete roles.

NOTE: At first glance it might seem like the Create Role form is so simple with only one field for Name that we could forgo creating an edit role feature. If an admin or staff needed to rename a role we could just instruct them to delete the role they need to rename and create a new one. But if a role that is attached to a user is deleted and a new one is created, the user would lose the assignment to the old role and have to be re-assigned to the new one.

Update Roles Repository (2)

As usual we first need to update the repository. Here we will add the Create CRUD function to the Roles repo.

Modify the Roles Repo Interface (2)

Make the following change to the Roles repo interface.

FredsCars\Models\Identity\Repositories\IRolesRepository.cs

using Microsoft.AspNetCore.Identity;

namespace FredsCars.Models.Identity.Repositories
{
    public interface IRolesRepository
    {
        IQueryable<IdentityRole> Roles { get; }

        Task CreateAsync(IdentityRole role);
        Task DeleteAsync(string id);
    }
}

Here we have defined a new method in the interface contract named DeleteAsync which takes in a string parameter and returns an asynchronous Task. The Id is stored as a Guid in the AspNetRoles table in SQL Server but the IdentityRole class’ Id property is a string. So we need to pass the Id around as a string.

Modify the Roles Repo Implementation (2)

Modify the EFRolesRepository class with the code shown below.

FredsCars\Models\Identity\Repositories\EFRolesRepository.cs

using FredsCars.Data;
using Microsoft.AspNetCore.Identity;
using Microsoft.EntityFrameworkCore;

namespace FredsCars.Models.Identity.Repositories
{
    public class EFRolesRepository : IRolesRepository
    {
        ... existing code

        public async Task CreateAsync(IdentityRole role)
        {
            _context.Roles.Add(role);
            await _context.SaveChangesAsync();
        }

        public async Task DeleteAsync(string id)
        {
            // DELETE: EF CORE read-first approach
            // -- this approach hits the database twice.
            // -- once to read the db, and once to save to and update the db.
            //IdentityRole? role = await _context.Roles.FindAsync(id);

            //if (role != null)
            //{
            //    _context.Roles.Remove(role);
            //    await _context.SaveChangesAsync();
            //}

            // DELETE: EF CORE create-and-attach approach
            // --create IdentityRole and attach to EF Core Entity State/Changes tracker
            // -- -- very challenging to unit test with an in-memory db.
            //IdentityRole role = new IdentityRole() { Id = id };
            //_context.Entry(role).State = EntityState.Deleted;

            //await _context.SaveChangesAsync();

            // DELETE: EF CORE no-reads-at-all approach
            // -- Solves multiple hit problem and 
            // -- in-memory db problem for unit tests
            await _context.Roles.Where(r => r.Id == id)
                        .ExecuteDeleteAsync();
        }
    }
}

As you might recall we looked at three approaches to deleting an entity with EF Core in module 25, “Create the Delete Page“, under the, “Modify the Implementation” section.

The three approaches were:

  • Read-first approach: needs to hit database twice. Could be costly in high traffic situations.
  • Create-and-attach approach: solves multiple db hit problem but challenging to test when unit tests use an in-memory database.
  • No-reads-at-all approach: uses ExecuteDeleteAsync method to hit db only once and right away when it is called. It does not wait for SaveChangesAsync to be called to update the database.
    Solves multiple hit problem and in-memory db problem for unit tests.

In this implementation I’ve used the no-reads-at-all approach again to keep consistent with how we handle Vehicles. But the other two approaches are included and commented out. Feel free to use any approach that fits your style and circumstance.

NOTE: Notice I haven’t been unit testing in this module. In a real project I would continue to unit test all of my controllers and components. But in the interest of time and space I have forgone unit testing here. And we have had plenty of examples up to this point for one to go back and reference.

Update the Roles Controller (2)

Modify the RolesController class with the code shown below and add the Delete actions at the bottom of the class after the Create actions.

FredsCars\Areas\Admin\Controllers\RolesController.cs

... existing code ...

[Area("Admin")]
[Authorize(Roles = "Administrator,Staff")]
public class RolesController : Controller
{

	... existing code ...

	public async Task<ViewResult> Delete(string id)
	{
		IdentityRole? role = await _repo.Roles
			.FirstOrDefaultAsync(r => r.Id == id);

		return View(role);
	}

	[HttpPost]
	[ActionName("Delete")]
	[ValidateAntiForgeryToken]
	public async Task<IActionResult> DeleteConfirmed(string id)
	{
		await _repo.DeleteAsync(id);
		return RedirectToAction("Index", "Roles");
	}
}

... existing code ...

In the code above we’ve first added the HttpGet Delete method which takes in as a parameter from the last segment in the URL a string Id (which will be a GUID) and grabs a role using the repo Roles IQueryable property’s FirstOrDefaultAsync method.

public async Task<ViewResult> Delete(string id)
{
    IdentityRole? role = await _repo.Roles
        .FirstOrDefaultAsync(r => r.Id == id);

    return View(role);
}

We then call the Roles Delete view as a ViewResult using a return view statement and passing the just fetched role as the view’s model.

public async Task<ViewResult> Delete(string id)
{
    IdentityRole? role = await _repo.Roles
        .FirstOrDefaultAsync(r => r.Id == id);

    return View(role);
}

We then added the HttpPost version of the Delete method. The action method takes in as a parameter a string Id just like the HttpGet version. But here rather then receiving the id from the last URL segment, it will receive it in the form post from a hidden input control in the Delete form a user will submit from the view.

[HttpPost]
[ActionName("Delete")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> DeleteConfirmed(string id)
{
    await _repo.DeleteAsync(id);
    return RedirectToAction("Index", "Roles");
}

In the body of the method we simply call the Roles repo DeleteAsync method passing the GUID Id (albeit as a string) and return a RedirectToAction result with a return statement redirecting the User’s browser to the Index controller and Roles View at /Admin/Roles/Index.

[HttpPost]
[ActionName("Delete")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> DeleteConfirmed(string id)
{
    await _repo.DeleteAsync(id);
    return RedirectToAction("Index", "Roles");
}

Notice how neat and clean the body of the method is. There are only two lines of very readable code. We don’t need to save the changes to the database here after deleting the role. The repository takes care of that for us. All of the database work is offloaded to the repository. You can see the value of the time we spent from early on in this chapter to now in order to set up our patterns such as DI (Dependency Injection) and Repository services.

Also notice the HttpPost Delete method is actually named DeleteConfirmed to avoid a C# error of multiple methods defined with the same name and parameter types. The method is marked with the ActionName attribute with the string parameter of “Delete” so ASP.Net Core routing can find an HttpPost version of the Delete method when a user submits the Delete role form.

[HttpPost]
[ActionName("Delete")]
[ValidateAntiForgeryToken]
public async Task<IActionResult> DeleteConfirmed(string id)
{
    await _repo.DeleteAsync(id);
    return RedirectToAction("Index", "Roles");
}

Add the Roles Delete view

Create a new Razor View file named Delete.cshtml in the Areas/Admin/Views/Roles folder of the FredsCars project and fill it with the markup code shown below.

FredsCars\Areas\Admin\Views\Roles\Create.cshtml

@model IdentityRole

@{
    ViewData["Title"] = "Delete Role";
}

<div class="container-fluid mt-3">
    <h3 class="text-center bg-primary-subtle py-2"
        style="border: 1px solid black;">
        Delete Role
    </h3>
    <h3 class="mt-5">Are you sure you want to delete this role?</h3>

    <hr />
</div>

<div class="container">
    <div class="row">
        <div class="col-4">
            <div class="mb-3">
                <b>@Html.DisplayNameFor(model => model.Name)</b>
                <br />
                @Html.DisplayFor(model => model.Name)
            </div>
        </div>
    </div>
</div>

<form asp-action="Delete" class="mt-3">
    <div class="container">
        <div class="row">
            <div class="col-4 text-center">
                <input type="hidden" asp-for="Id" />
                <input type="submit" value="Delete" class="btn btn-danger" />
                <a asp-controller="Roles"
                   asp-action="Index"
                   class="btn btn-secondary">Cancel</a>
            </div>
        </div>
    </div>
</form>

In the code above we again set the model of the view to an IdentityRole, the title in the ViewData dictionary to “Delete Role” and layout our usual blue header which here reads, “Delete Role”. The div element containing our blue header bar also includes a message below the blue header in an h3 element asking if you are sure you want to delete this role since this is a Delete Confirm page.

Next, we have our usual Bootstrap div container, a nested Bootstrap div row, and nested within that a Bootstrap div taking up 4 columns of the row. Within the 4 grid column div we display the properties of the IdentityRole we want to delete using the Html helper object’s DisplayNameFor and DisplayFor methods. For an IdentityRole we only need to show the Name property.

Finally, we have a form tag helper whose asp-action attribute points to the Delete action of the Roles controller in the Admin area. Since asp-controller and asp-area are not specified, ASP.Net Core routing sets the submission URL to the same Area and Controller the view sits in which will look something like
/Admin/Roles/Delete/[GuidId].

Nested within the form we have our Bootstrap container/row/4-column div structure once again.

<form asp-action="Delete" class="mt-3">
    <div class="container">
        <div class="row">
            <div class="col-4 text-center">
                <!- form content -->
            </div
        </div>
    </div>
</form>

In the form body we have a hidden input control from an input tag helper for the IdentityRole view model’s Id property set with the tag helper’s asp-for attribute.

<input type="hidden" asp-for="Id" />

Next we have our Submit and Cancel buttons. The submit button is rendered by an input control tag. The cancel button is rendered by an anchor tag helper dressed up as a Bootstrap button.

Add a Delete link for each Role result

Now we just need to add delete buttons to the role results on the Roles Index page. Modify the Roles Index view with the code shown below.

FredsCars\Areas\Admin\Views\Roles\Index.cshtml

... existing code ...
<tbody>
    @foreach (var r in Model.Roles)
    {
        <tr>
            <td>
                <a asp-controller="Roles"
                   asp-action="Delete"
                   asp-route-id="@r.Id">
                    <i class="bi bi-trash text-danger"></i>
                </a>
            </td>
            <td>@r.Name</td>
        </tr>
    }
</tbody>
... existing code ...

We’ve seen Bootstrap icon buttons before. This one is set to the trash icon representing a delete action and points to the Delete action of the Roles controller. The link will know to hit the HttpGet version to render the View for the first time rather then the HttpPost version because an anchor link always sends a Get request. If we want to send a Post request we need to use a form.


Restart the application and navigate to https://localhost:40443/Admin/Roles.

If you are not logged in log in as user: admin, password: Adm!n863.

Your browser should look similar to the results below. Click the delete icon for TestRole.

The browser is redirected to the delete role URL. Note your GUID in the last segment of the URL will be different then mine.

https://localhost:40443/Admin/Roles/Delete/6cade0f7-a5e5-4a72-af07-8c6a3a5113e2

The important thing here is to notice that the GUID is passed in through the URL from the previous anchor tag helper dressed up as a Bootstrap trash icon we just looked at by the asp-route-id attribute because anchor tags send Get requests.

<a asp-controller="Roles"
   asp-action="Delete"
   asp-route-id="@r.Id">
    <i class="bi bi-trash text-danger"></i>
</a>

The browser at this point should look similar to the screenshot below. Click the Delete button to delete TestRole.

The browser will be redirected back to the Roles index page and TestRole will no longer be in the results as it is now deleted from the database.

The Edit Role feature

As mentioned in the last section we do need to make an Edit feature for roles in addition to the Create and Delete features. And as usual we will start with the repository.

Update Roles Repository (3)

Make the following change to the IRolesRepository interface.

FredsCars\Models\Identity\Repositories\IRolesRepository.cs

using Microsoft.AspNetCore.Identity;

namespace FredsCars.Models.Identity.Repositories
{
    public interface IRolesRepository
    {
        IQueryable<IdentityRole> Roles { get; }

        Task CreateAsync(IdentityRole role);
        Task UpdateAsync(IdentityRole role);
        Task DeleteAsync(string id);
    }
}

Now add the implementation to the EFRolesRepository class.

... existing code ...
public class EFRolesRepository : IRolesRepository
{
	... existing code ...

	public async Task CreateAsync(IdentityRole role)
	{
		_context.Roles.Add(role);
		await _context.SaveChangesAsync();
	}

	public async Task DeleteAsync(string id)
	{
		... existing code ...
	}

	public async Task UpdateAsync(IdentityRole role)
	{
		_context.Roles.Update(role);
		await _context.SaveChangesAsync();
	}
}
... existing code ...

In the above repo code the UpdateAsync method satisfies the interface API signature by taking in an IdentityRole object as a parameter and returning a Task. The body of the method updates changes to the to the Identity database using the Identity dbContext and saves the changes with the Identity dbContext’s SaveChangesAsync method.

Update the Roles Controller (3)

Modify the Roles controller once more with the code shown below.

FredsCars\Areas\Admin\Controllers\RolesController.cs.

... existing code ... 
[HttpPost]
 [ValidateAntiForgeryToken]
 public async Task<IActionResult> Create([Bind("Name")] IdentityRole role)
 {
    ... existing code ...
 }

 public async Task<ViewResult> Edit(string id)
 {
     IdentityRole? role = await _repo.Roles
         .FirstOrDefaultAsync(r => r.Id == id);

     return View(role);
 }

 [HttpPost]
 [ActionName("Edit")]
 [ValidateAntiForgeryToken]
 public async Task<IActionResult> EditPost(string id)
 {
     var roleName =    HttpContext.Request.Form["Name"].ToString();
     if (string.IsNullOrWhiteSpace(roleName))
     {
         ModelState.AddModelError("Name", "Role name is required");
     }     
  
     var role = await _repo.Roles
         .FirstOrDefaultAsync(r => r.Id == id);

     if (await TryUpdateModelAsync<IdentityRole>(role!,
             "",
             r => r.Name
     ))
     {
         await _repo.UpdateAsync(role!);
         return RedirectToAction("Index", "Roles");
     }

     return View();
 }

 public async Task<ViewResult> Delete(string id)
 {
     IdentityRole? role = await _repo.Roles
         .FirstOrDefaultAsync(r => r.Id == id);

     return View(role);
 }
... existing code ...

In the controller code above we added the HttpGet and HttpPost Edit action methods. The HttpGet method first checks if the incoming form post value for “Name” is null and if so adds an error message to the ModelState. It fetches the Form post value to check from the FormCollection object of the ControllerBase class’ HttpContext.Request object which lets us look at a variety of Request information in the current HttpContext.

var roleName = HttpContext.Request.Form["Name"].ToString();
if (string.IsNullOrWhiteSpace(roleName))
{
    ModelState.AddModelError("Name", "Role name is required");
}

Next, if “Name” in the FormCollection is not null, we fetch an identity role based on the incoming GUID Id in string form from the URL, uses the FirstOrDefaultAsync method of the Roles IQueryable property of the Roles repo to fetch the IdentityRole needed to edit, and returns the IdentityRole as the model of the view in the return View statement which returns the Edit view as the ViewResult.

The HttpPost version of the Edit method also takes in a Guid Id in string form but from a form post in the view. The actual method name is EditPost so again we mark the method with the ActionName attribute specifying to the ASP.Net Core routing system to use this method when looking for the post version of Edit in the Roles controller.

We again start by fetching the role to edit the same way as in the HttpGet version with a lambda condition passed to the FirstOrDefaultAsync method of the Roles repo service’s Roles IQueryable property.

Next we use the TryUpdateModel pattern to protect against overposting like we do for editing vehicles.

if (await TryUpdateModelAsync<IdentityRole>(role!,
        "",
        r => r.Name
))
{
    ... update role in db
    ... redirect to Roles/Index
}

As a refresher to overposting and the TryUpdateModel pattern, overposting is when a hacker posts a value to a field we do not want a user to have access to. So we specify what properties ASP.Net Core should bind from the incoming form post to the IdentityRole we are editing; in this case just Name.

r => r.Name

In the body of the if/block containing the TryUpdateModel pattern we update the role and save it to the database via the Roles repo service and redirect to the Index action of the Roles controller in the Admin area.

await _repo.UpdateAsync(role!);
return RedirectToAction("Index", "Roles");

If the TryUpdateModelAsync method fails, possibly because of an invalid model state, we return the Edit view again so the user can correct the problem.

I have trimmed the pattern down a bit from what we did in editing vehicles. In vehicles we have a try/catch block within the body of the TryUpdateModelAsync method. The try/block tries to update the database. If it fails the catch/block logs the error to the console and to the database using Serilog.

As with testing, in the interest of space and time I trimmed this bit out here. But, in a real project I would have included this functionality and unit tested it.

Add the Roles Edit view

Create a Razor view file named Edit.cshtml in the Areas/Admin/Views/Roles folder of the FredsCars project.

FredsCars\Areas\Admin\Views\Roles\Edit.cshtml

@model IdentityRole

@{
    ViewData["Title"] = "Edit";
}

<div class="container-fluid mt-3">
    <h3 class="text-center bg-primary-subtle py-2"
        style="border: 1px solid black;">
        Edit Role
    </h3>
</div>

<form asp-action="Edit">
    <div class="container">
        <div asp-validation-summary="ModelOnly" class="text-danger"></div>
        <input type="hidden" asp-for="Id" />
        <div class="row">
            <div class="col-4">
                <div class="mb-3">
                    <label asp-for="Name" class="form-label fw-bold"></label>
                    <input asp-for="Name" class="form-control" />
                    <span asp-validation-for="Name" class="text-danger"></span>
                </div>
            </div>
        </div>
        <div class="row">
            <div class="col-4 text-center">
                <div>
                    <input type="submit"
                           value="Edit"
                           class="btn btn-success" />
                    <a asp-controller="Roles"
                       asp-action="Index"
                       class="btn btn-secondary">Cancel</a>
                </div>
            </div>
        </div>
    </div>
</form>

In the razor code above we start by setting the model to IdentityRole, the Title key of the ViewDataDictionary object to “Edit” and the text of our blue header to “Edit Role”.

Next we have a form tag-helper with the asp-action attribute set to the Edit action. So when a user submits the Roles Edit form it will post to the HttpPost Edit action (EditPost) of the Roles Controller in the Admin area.

Inside the form we have an input tag helper set to type hidden to create a hidden field in order to post the Id of the IdentityRole to the HttpPost Edit action.

<input type="hidden" asp-for="Id" />

The asp-for attribute of the input tag-helper binds the Id property of the IdentityRole to the value of the input control.

After the hidden field we have a form-group with label, input, and validation tag-helpers all bound to the Name property of the IdentityRole model.

And finally, we have a second Bootstrap row after the form-group row containing the Submit and Cancel buttons.

Add an Edit link for each Role result

Modify the Index view for the Roles controller with the changes shown below.

FredsCars\Areas\Admin\Views\Roles\Index.cshtml

<tbody>
    @foreach (var r in Model.Roles)
    {
        <tr>
            <td>
                <a asp-controller="Roles"
                   asp-action="Edit"
                   asp-route-id="@r.Id">
                    <i class="bi bi-pencil text-success"></i></a>
                <a asp-controller="Roles"
                   asp-action="Delete"
                   asp-route-id="@r.Id">
                    <i class="bi bi-trash text-danger"></i>
                </a>
            </td>
            <td>@r.Name</td>
        </tr>
    }
</tbody>

Here we added the edit Bootstrap icon for each Role result inside of an anchor tag-helper that will take the user to the Edit Role form when clicked.


Restart the application and navigate to https://localhost:40443/Admin/Roles. Log in as an admin and click the “Create New” Link to recreate the TestRole role just like we did in the Add a Create New Link to the Roles Index page section.

Click the Edit icon next to the TestRole result.

On the Edit Role form, change the IdentityRole Name to TestRoleEdit and click the Edit button.

The browser is redirected back to the Role results page and TestRole has been changed to TestRoleEdit.

You can delete the TestRoleEdit role at this point.

Build out the Users Admin Pages

In this section we are going to build out the Users admin pages and features. Some of the steps will look the same as those we took to build out the Roles admin pages but an IdentityUser, and in our case the custom ApplicatinUser, has more fields then an IdentityRole. Also we will need to supply a dropdown list of Roles to assign a user for the Create and Edit pages.

Users Index/List feature

Again, we’ll start with the Users landing page as we did for Vehicles and Role.

Create the UserRoles View Model

As usual we are going to create a view model for the Index method in the Users controller which will make it easier to pass down all of the needed information to the view in one swoop in a strongly typed fashion.

Create a class named UserRolesViewModel in the Models/Identity/ViewModels folder of the FredsCars project and fill it with the code shown below.

FredsCars\Models\Identity\ViewModels\UserRolesViewModel.cs

namespace FredsCars.Models.Identity.ViewModels
{
    public class UserRolesViewModel
    {
        public ApplicationUser User { get; set; } = new();
        public IList<string>? Roles { get; set; }
    }
}

The view model above represents an ApplicationUser, our custom IdentityUser class, and a list of that user’s roles.

Update the Users Controller (1)

Modify the UsersController class with the code shown below.

FredsCars\Areas\Admin\Controllers\UsersController.cs

using FredsCars.Models.Identity;
using FredsCars.Models.Identity.ViewModels;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.EntityFrameworkCore;

namespace FredsCars.Areas.Admin.Controllers
{
    [Area("Admin")]
    [Authorize(Roles = "Administrator,Staff")]
    public class UsersController : Controller
    {
        private readonly UserManager<ApplicationUser> _userManager;

        public UsersController(UserManager<ApplicationUser> userManager)
        {
            _userManager = userManager;
        }

        public async Task<IActionResult> Index()
        {
            var users = await _userManager.Users
                .OrderBy(u => u.UserName)
                .ToListAsync();
            var userRolesViewModel = new List<UserRolesViewModel>();
            
            foreach (var user in users)
            {
                var roles = await _userManager.GetRolesAsync(user)
                    ?? new List<string>();
                roles = roles.OrderBy(r => r).ToList();
                userRolesViewModel.Add(new UserRolesViewModel
                {
                    User = user,
                    Roles = roles
                });
            }

            return View(userRolesViewModel);
        }
    }
}

In the controller code above the first modification we made was to inject the UserManager service into the controller and assign it to a private class field member named _userManager.

private readonly UserManager<ApplicationUser> _userManager;

public UsersController(UserManager<ApplicationUser> userManager)
{
    _userManager = userManager;
}

We then mark the Index method as asynchronous with the C# async keyword so we can use the await keyword for asynchronous calls in the body of the mehtod.

public async Task<IActionResult> Index()
{
   ...
}

Within the body of the Index method we start by getting an IQueryable of users from the Users property of the UserManager service, ordering the query by UserName, and executing the query with the ToListAsync method. We use the await C# keyword to make sure the call to the database is done asynchronously. We then instantiate an instance of a List of UserRoles view models (List<UserRolesViewModel>).

var users = await _userManager.Users
    .OrderBy(u => u.UserName)
    .ToListAsync();
var userRolesViewModel = new List<UserRolesViewModel>();

We then call a foreach statement iterating through the list of users. For each iteration we get a list of roles for the current user form the GetRolesAsync method of the UserManager service, order the list alphabetically by role name, and add an instance of UserRolesViewModel to the List of UserRoles view models (List<UserRolesViewModel).

foreach (var user in users)
{
    var roles = await _userManager.GetRolesAsync(user)
        ?? new List<string>();
    roles = roles.OrderBy(r => r).ToList();
    userRolesViewModel.Add(new UserRolesViewModel
    {
        User = user,
        Roles = roles
    });
}

Finally, we call the Index view and pass the List of UserRole view models as the model of the view.

return View(userRolesViewModel);

Update the Users Index View

Modify the Index view of the Users controller with the code shown below.

FredsCars\Areas\Admin\Views\Users\Index.cshtml

@model IEnumerable<UserRolesViewModel>
@{
    ViewData["Title"] = "Users";
}

<div class="container-fluid py-4">
    <h3 class="text-center bg-primary-subtle py-2"
        style="border: 1px solid black;">
        Users
    </h3>
</div>

<div class="container"
     style="margin-top: 20px; border: 1px solid black">
    <!-- Results -->
    <table class="results table table-striped">
        <thead>
            <tr>
                <th></th>
                <th>@Html.DisplayNameFor(m => m.First().User.UserName)</th>
                <th>@Html.DisplayNameFor(m => m.First().User.Email)</th>
                <th>@Html.DisplayNameFor(m => m.First().Roles)</th>
            </tr>
        </thead>
        <tbody>
            @foreach (var item in Model)
            {
                <tr>
                    <td></td>
                    <td>@item.User.UserName</td>
                    <td>@item.User.Email</td>
                    <td>
                        @if (item.Roles != null && item.Roles.Any())
                        {
                            foreach (var role in item.Roles)
                            {
                                <div>@role</div>
                            }
                        }
                        else
                        {
                            <span class="text-muted">No roles assigned</span>
                        }
                    </td>
                </tr>
            }
        </tbody>
    </table>

</div>

In the view code above we added a table to render our user results nested in a Boostrap container div.

We use the Html helper object’s DisplayNameFor method to render the User.UserName, User.Email, and Roles properties of the view’s model, IEnumerable<UserRolesViewModel>, as column headers in the th elements.

<thead>
    <tr>
        <th></th>
        <th>@Html.DisplayNameFor(m => m.First().User.UserName)</th>
        <th>@Html.DisplayNameFor(m => m.First().User.Email)</th>
        <th>@Html.DisplayNameFor(m => m.First().Roles)</th>
    </tr>

In the body of the table we iterate through the UserRolesViewModel objects of the view’s model using a foreach block and lay out a table row (tr element) for each one including hte UserName, Email, and list of roles for each user.


Restart the application and navigate to https://localhost:40443/Admin/Users. Log in as admin if you are not logged in.

If the admin user had multiple roles assigned the browser might look similar to the following.

But since the admin user only has a single role assigned, Administrator, it should look like the following.

The Create User feature

In this section we are going to give administrators and staff a way to create new Users and assign the users roles.

Add the Create User view model

Add a class named CreateUserViewModel to the Models\Identity\ViewModels folder in the FredsCars project.

FredsCars\Models\Identity\ViewModels\CreateUserViewModel.cs

using System.ComponentModel.DataAnnotations;

namespace FredsCars.Models.Identity.ViewModels
{
    public class CreateUserViewModel
    {
        [Required(ErrorMessage = "UserName required")]
        public required string UserName { get; set; }
        
        [Required(ErrorMessage = "Password required")]
        [DataType(DataType.Password)]
        public required string Password { get; set; }

        [Display(Name = "First Name")]
        [Required(ErrorMessage = "First Name required")]
        public required string FirstName { get; set; }

        [Display(Name = "Last Name")]
        [Required(ErrorMessage = "LastName required")]
        public required string LastName { get; set; }

        [Required(ErrorMessage = "Email required")]
        public required string Email { get; set; }

        public List<string> Roles { get; set; } = new();
    }
}

The view model shown above will be used as the model of the Create view of the Users controller and as the incoming parameter of the HttpPost Create action. It contains all of the Property values we need in order to Create an ApplicationUser (inherited from IdentityUser) and the list of Roles that should be assigned to the new user.

Each property is decorated with the Required validation data annotation.

The Password property is also decorated with the DataType data annotation with its DataType enumeration parameter set to Password so the browser will render the Password property of the model with bullet or asterisk filler characters instead of the actual characters the user types in for the password.

Update the Users Controller

Modify the UsersController class with the code shown below.

FredsCars\Areas\Admin\Controllers\UsersController.cs

using FredsCars.Models.Identity;
using FredsCars.Models.Identity.ViewModels;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Identity;
using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.Rendering;
using Microsoft.EntityFrameworkCore;

namespace FredsCars.Areas.Admin.Controllers
{
    [Area("Admin")]
    [Authorize(Roles = "Administrator,Staff")]
    public class UsersController : Controller
    {
        private readonly UserManager<ApplicationUser> _userManager;
        private readonly RoleManager<IdentityRole> _roleManager;

        public UsersController(UserManager<ApplicationUser> userManager,
            RoleManager<IdentityRole> roleManager)
        {
            _userManager = userManager;
            _roleManager = roleManager;
        }

        public async Task<IActionResult> Index()
        {
            ... existing code ...
        }

        public ViewResult Create()
        {
              
            ViewBag.Roles = new SelectList(_roleManager.Roles,
               "Name", "Name");
            
            return View();
        }

        [HttpPost]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> Create([Bind("UserName,Password,FirstName,LastName,Email,Roles")] CreateUserViewModel model)
        {
            if (ModelState.IsValid)
            {
                var user = new ApplicationUser
                {
                    UserName = model.UserName,
                    Email = model.Email,
                    FirstName = model.FirstName,
                    LastName = model.LastName,
                    EmailConfirmed = true
                };

                var result = await _userManager.CreateAsync(user, model.Password);

                if (result.Succeeded)
                {
                    foreach (string role in model.Roles)
                    {
                        await _userManager.AddToRolesAsync(user, model.Roles);
                    }

                    return RedirectToAction("Index");
                }

                foreach (var error in result.Errors)
                {
                    ModelState.AddModelError(string.Empty, error.Description);
                }
            }

            ViewBag.Roles = new SelectList(_roleManager.Roles,
               "Name", "Name");

            return View(model);
        }
    }
}

In the updates to the controller code above, we start by adding in the RoleManager service to the services we bring in with DI in addition to the UserManager service.

private readonly UserManager<ApplicationUser> _userManager;
        private readonly RoleManager<IdentityRole> _roleManager;

        public UsersController(UserManager<ApplicationUser> userManager,
            RoleManager<IdentityRole> roleManager)
        {
            _userManager = userManager;
            _roleManager = roleManager;
        }

Next we create the HttpGet version of the Create action method. We start by adding a Roles property to the ViewBag object and assign to it a new SelectList. We pass to the items parameter of the SelectList the Roles property of the RoleManager service which contains an IQueryable of IdentityRole in the database and we assign to both the dataTextField and dataValueField parameters the Name property of IdentityRole so each item in the view’s Select dropdown list will contain the Name of each role for text and value in each dropdown item.

ViewBag.Roles = new SelectList(_roleManager.Roles,
   "Name", "Name");

We then return the default Create view for the Users controller.


For the HttpPost version of the Create action method we again use the Bind attribute on the signature of the method to protect from overposting passing as a parameter to the attribute a list of properties that are allowed to bind to the incoming model from a form post.

[HttpPost]
[ValidateAntiForgeryToken]
public async Task<IActionResult> Create([Bind("UserName,Password,FirstName,LastName,Email,Roles")] CreateUserViewModel model)
{
   ...
}

If the model is valid, we create a new instance of an Application user using the values of the incoming CreateUserViewModel class to set the properties of the new user, try to create the new user in the database using the CreateAsync method of the UserManager service capturing the result, an IdentityResult object, in a variable named result. If the result Succeeded property equals true we loop through the roles in the incoming model and assign each one to the new user using the AddToRolesAsync method of the UserManager service. We then redirect to the Index action of the Users controller.

If the result Succeeded property is not true we loop through the Errors collection of the IdentityResult and add each IdentityError description to the ModelState.

If either the ModelState is not valid or the Succeeded property of the IdentityResult from creating the user is not true, we add a SelectList with IdentityRole names to the Roles property of the ViewBag object just like we did in the HttpGet version of the Create action so that the DropDown list of roles can be re-rendered in the Create view with any errors the user needs to address. We then return the Create view passing the CreateUserViewModel instance as the model of the view which now will contain any ModelState errors added by ASP.Net Core or by us in the foreach block where we loop though the IdentityResult errors.

 foreach (var error in result.Errors)
 {
     ModelState.AddModelError(string.Empty, error.Description);
 }

Add the Users Create view

Create a new razor view in the Areas/Admin/Views/Users folder of the FredsCars project named Create.cshtml and fill it with the code below.

FredsCars\Areas\Admin\Views\Users\Create.cshtml

@model CreateUserViewModel

@{
    ViewData["Title"] = "Create User";
}

<div class="container-fluid mt-3">
    <h3 class="text-center bg-primary-subtle py-2"
        style="border: 1px solid black;">
        Create a User
    </h3>
</div>

<form asp-area="Admin" asp-controller="Users" asp-action="Create">
    <div class="container"
         style="margin-top: 20px; border: 0px solid black">
        <div asp-validation-summary="ModelOnly" class="text-danger"></div>
        <div class="row">
            <div class="col-2"></div>
            <div class="col-4">
                <div class="mb-3">
                    <label asp-for="UserName" class="form-label fw-bold"></label>
                    <input asp-for="UserName" class="form-control"
                        autocomplete="new-username"
                        tabindex="1" />
                    <span asp-validation-for="UserName" class="text-danger"></span>
                </div>
                <div class="mb-3">
                    <label asp-for="FirstName" class="form-label fw-bold"></label>
                    <input asp-for="FirstName" class="form-control"
                        tabindex="3" />
                    <span asp-validation-for="FirstName" class="text-danger"></span>
                </div>
                <div class="mb-3">
                    <label asp-for="Email" class="form-label fw-bold"></label>
                    <input asp-for="Email" class="form-control"
                        tabindex="5" />
                    <span asp-validation-for="Email" class="text-danger"></span>
                </div>
            </div>
            <div class="col-4">
                <div class="mb-3">
                    <label asp-for="Password" class="form-label fw-bold"></label>
                    <input asp-for="Password" class="form-control"
                           autocomplete="new-password"
                           tabindex="2" />
                    <span asp-validation-for="Password" class="text-danger"></span>
                </div>
                <div class="mb-3">
                    <label asp-for="LastName" class="form-label fw-bold"></label>
                    <input asp-for="LastName" class="form-control"
                        tabindex="4" />
                    <span asp-validation-for="LastName" class="text-danger"></span>
                </div>
                <div class="mb-3">
                    <label asp-for="Roles" class="form-label fw-bold"></label>
                    <select asp-for="Roles"
                            asp-items="@ViewBag.Roles"
                            class="form-select"
                            size="3">
                    </select>
                    <span asp-validation-for="Roles" class="text-danger"></span>
                </div>
            </div>
            <div class="col-2"></div>
        </div>
        <div class="row">
            <div class="col-2"></div>
            <div class="col-4 text-center">
                <div>
                    <input type="submit"
                           value="Create"
                           class="btn btn-primary" />
                    <a asp-area="Admin" asp-controller="Users"
                       asp-action="Index"
                       class="btn btn-secondary">Cancel</a>
                </div>
            </div>
            <div class="col-2"></div>
        </div>
    </div>
</form>

@section Scripts {
    <partial name="_ValidationScriptsPartial" />
}

In the razor code above we use the Razor directive to mark the model of the view a CreateUserViewModel object.

@model CreateUserViewModel

After our usual boiler plate code we have a form tag helper set to submit to the Create action of the Users controller in the Admin area when a user clicks the submit button.

<form asp-area="Admin" asp-controller="Users" asp-action="Create">
   ...
</form>

Nested in the form is a Bootstrap container div. Inside the container div we have an asp-validation-summary tag helper and two Bootstrap div rows with the familiar col-2 | col -4 | col-4 | col-2 structure to give us two columns to hold form groups in columns 4 grid columns wide and two columns 2 grid columns wide on the ends to center the form.

<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<div class="row">
	 <div class="col-2"></div>
 	<div class="col-4">
 		... left column form groups
 	</div>
 	<div class="col-4">
	 	... right column form groups
 	</div>
        <div class="col-2"></div>
</div>

The left 4 grid column contains form groups for the UserName, FirstName, and Email properties of the view’s model. Each form group contains label, input and asp-validation tag helpers.

<div class="mb-3">
    <label asp-for="UserName" class="form-label fw-bold"></label>
    <input asp-for="UserName" class="form-control"
        autocomplete="new-username"
        tabindex="1" />
    <span asp-validation-for="UserName" class="text-danger"></span>
</div>

The right 4 grid column contains form groups for the Password, LastName, and Roles properties of the view’s model.

The Roles form group contains a select tag helper instead of an input tag helper to contain a list of roles that can be assigned to a user. The items are populated by the tag helper’s asp-items attribute we populated in the controller in the ViewBag object and bound to the Roles property of the model with the asp-for attribute.

 <select asp-for="Roles"
         asp-items="@ViewBag.Roles"
         class="form-select"
         size="3">
 </select>

The second row of the container contains a submit button rendered by an input control with the html type attribute set to “submit” and a cancel button rendered by an anchor tag helper which redirects the browser back to the Index action of the users controller when clicked without making any changes to the database.

<div class="col-4 text-center">
    <div>
        <input type="submit"
               value="Create"
               class="btn btn-primary" />
        <a asp-area="Admin" asp-controller="Users"
           asp-action="Index"
           class="btn btn-secondary">Cancel</a>
    </div>
</div>

We also inject our jQuery validation scripts into the layout’s Scripts section as usual for Edit and Create forms.

@section Scripts {
    <partial name="_ValidationScriptsPartial" />
}

Add the jQuery validation scripts to the Admin
Area

We just added the _ValidationScripts partial view at the end of the Create form in the view nested in the Scripts section. But since the Create view of the Users controller is in the Admin area, ASP.Net Core will not know to look in the Views/Shared folder on the root of the FredsCars project. So we need to create a Views/Shared folder under Areas/Admin and copy the jQuery validation partial view there.

Create the new folder now.

FredsCars/Areas/Admin/Views/Shared

and copy _ValidationScriptsPartial.cshtml file there. Do this within Visual Studio, not in Windows Explorer. Your project structure should look like this in the VS Solution Explorer.

Add the Create New link to the Users/Index view

Make the following change to the Index view of the Users controller.

FredsCars\Areas\Admin\Views\Users\Index.cshtml

... existing code ...
<div class="container-fluid py-4">
    <h3 class="text-center bg-primary-subtle py-2"
        style="border: 1px solid black;">
        Users
    </h3>
</div>

<p class="container text-start">
    <a asp-controller="Users" asp-action="Create">Create New</a>
</p>
<div class="container"
     style="margin-top: 20px; border: 1px solid black">
    <!-- Results -->
    <table class="results table table-striped">
... existing code ...

In the markup above we just added a link via an anchor tag-helper to the Create User page.


Restart the application and navigate to https://localhost:40443/Admin/Users. Click the Create New link.

Your browser navigates to the Create a User page at https://localhost:40443/Admin/Users/Create.

Fill out the user form with UserName: TestUserName and Password: TestUserName and the rest of the values as shown including both Staff & Administrator roles. (No user would ever need to be both an Administrator and Staff user. But I just want to show the application can handle multiple roles.)

The browser is redirected back to the User index page and the User TestUserName has been added with both the Administrator and Staff roles.

The Edit User Feature

In this section we will create the Edit User feature.

Add the Users Edit view Model

Add a class named EditUserViewModel to the Models\Identity\ViewModels folder in the FredsCars project.

FredsCars\Models\Identity\ViewModels\EditUserViewModel.cs

using System.ComponentModel.DataAnnotations;

namespace FredsCars.Models.Identity.ViewModels
{
    public class EditUserViewModel
    {
        public required string Id { get; set; }
        
        [Required(ErrorMessage = "UserName required")]
        public required string UserName { get; set; }

        [Display(Name = "First Name")]
        [Required(ErrorMessage = "First Name required")]
        public required string FirstName { get; set; }

        [Display(Name = "Last Name")]
        [Required(ErrorMessage = "LastName required")]
        public required string LastName { get; set; }

        [Required(ErrorMessage = "Email required")]
        public required string Email { get; set; }

        public List<string> Roles { get; set; } = new();
    }
}

The view model shown above will be used as the model of the Edit view of the Users controller and as the incoming parameter of the HttpPost Edit action. It contains all of the Property values we need in order to Edit an ApplicationUser and the list of Roles that should be assigned to the user in the update.

The EditUser view model does not contain a Password property because we do not want anyone but the user to create or modify a password. We will create a “Forgot Password” link on the Login page in a later section and use some built-in features of identity to enable a user to reset their password if needed.

We do include an Id property so that a hidden field can bind to it on the Edit form in the view.

Update the Users controller (2)

Modify the UsersController class with the code shown below.

FredsCars\Areas\Admin\Controllers\UsersController.cs


        [HttpPost]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> Create([Bind("UserName,Password,FirstName,LastName,Email,Roles")] CreateUserViewModel model)
        {
            ... existing code ...
        }

        public async Task<ViewResult> Edit(string id)
        {
            ApplicationUser? user = await _userManager.FindByIdAsync(id);

            List<string> roleList = new();
            if (user != null)
            {
                var roles = await _userManager.GetRolesAsync(user);
                roleList = roles.ToList();
            }
 
            var userVm = new EditUserViewModel
            {
                Id = user?.Id!,
                UserName = user?.UserName!,
                FirstName = user?.FirstName!,
                LastName = user?.LastName!,
                Email = user?.Email!,
                Roles = roleList
            };

            ViewBag.Roles = new SelectList(_roleManager.Roles,
               "Name", "Name");

            return View(userVm);
        }

        [HttpPost]
        [ValidateAntiForgeryToken]
        public async Task<IActionResult> Edit(EditUserViewModel model)
        {
            var user = await _userManager.FindByIdAsync(model.Id);

            List<string> roleList = new();
            if (user != null)
            {
                var roles = await _userManager.GetRolesAsync(user);
                roleList = roles.ToList();
            }

            // Overposting protection against UserName and Password
            if (await TryUpdateModelAsync<ApplicationUser>(user!,
                "",
                u => u.FirstName, u => u.LastName,
                u => u.Email))
            {
                try
                {
                    // Update user
                    var result = await _userManager.UpdateAsync(user!);
                    // Update roles
                    if (result.Succeeded)
                    {
                        // clear roles first
                        var existingRoles = await _userManager.GetRolesAsync(user!);
                        await _userManager.RemoveFromRolesAsync(user!, existingRoles);
                        // add selected roles from the form
                        await _userManager.AddToRolesAsync(user!, model.Roles);

                        return RedirectToAction("Index", "Users");
                    }
                    else
                    {
                        ModelState.AddModelError("", "Failed to update user. Please try again or contact your administrator");
                    }   
                }
                catch (Exception ex)
                {
                    // let any errors bubble up
                    throw;
                }
            };

            ViewBag.Roles = new SelectList(_roleManager.Roles,
               "Name", "Name");

            var userVm = new EditUserViewModel
            {
                Id = user?.Id!,
                UserName = user?.UserName!,
                FirstName = user?.FirstName!,
                LastName = user?.LastName!,
                Email = user?.Email!,
                Roles = roleList
            };

            return View(userVm);
        }
    }
}

In the controller code above we have added the HttpGet and HttpPost Edit action methods.

The HttpGet Edit action takes in a Guid id parameter as a string datatype, uses it to fetch the user to be edited via the UserManager.FindByIdAsync method, and assigns the user to a variable named user of type ApplicationUser.

Next we declare a variable named roleList of type list of string (List<string>) and initialize it with the C# new operator combined with the sugar syntax of not having to define the datatype. Then if the user is not null, we get a list of the user’s roles using the UserManager.GetRolesAsync method which returns an IList of strings representing role names. We then convert the IList to a List and assign the new List to the roleList variable we defined before the if/block containing the user null check.

List<string> roleList = new();
if (user != null)
{
    var roles = await _userManager.GetRolesAsync(user);
    roleList = roles.ToList();
}

We then create a new instance of the EditUser view model and populate its properties from the values of the user we fetched on the first line of the action via FindByIdAsync.

var userVm = new EditUserViewModel
{
    Id = user?.Id!,
    UserName = user?.UserName!,
    FirstName = user?.FirstName!,
    LastName = user?.LastName!,
    Email = user?.Email!,
    Roles = roleList
};

Next, we instantiate a new SelectList object, fill its items with a complete list of roles via the RoleManager service’s Roles property, and assign it to a new dynamic property of the ViewBag object called Roles. The DataValueField and DataTextField parameters of the SelectList are both set to the Name property of each role.

ViewBag.Roles = new SelectList(_roleManager.Roles,
   "Name", "Name");

Finally, we return the Edit view of the Users controller using a return View statement and pass to it the EditUserViewModel instance as the view model.

return View(userVm);

The HttpPost version of the Edit action method takes in as a parameter an EditUserViewModel instance.

 [HttpPost]
 [ValidateAntiForgeryToken]
 public async Task<IActionResult> Edit(EditUserViewModel model)
 {
   ...
}

We again fetch the user to edit via the UserManager service’s FindByIdAsync method this time passing the model.Id property from the EditUser view model rather then an id from the URL.

Next, we have our role retrieval routine the same as in the HttpGet version of the Edit action which we will use at the bottom of the method if we need to re-render the form.

List<string> roleList = new();
if (user != null)
{
    var roles = await _userManager.GetRolesAsync(user);
    roleList = roles.ToList();
}

We then have our usual defensive mechanism against overposting using the TryUpdateModel pattern. We try to update the fetched user with the incoming form post values (FirstName, LastName, Email) within an if/block containing the Base Controller object’s TryUpdateModelAsync method as the condition of the if/block. If the ASP.Net Core model binding system is able to bind the incoming form post values to the fetched user successfully, we try to update the user within the try portion of a try/catch block with the changes to the database via the UserManager’s UpdateAsync method. If the update to the database is successfull, we get a list of the user’s current existing roles with the UserManager.GetRolesAsync method, clear the roles associated with the user from the database via the UserManager.RemoveRolesAsync method, and write the new role selections to the database using the UserManager.AddToRolesAsync method. The browser will then be redirected back to the Index view of the Users controller with a return RedirectToAction statement. If the user update via UserManager.UpdateAsync fails we add a model level error to the ModelState with the message, “Failed to update user. Please try again or contact your administrator.”

In the catch portion of the try/catch block if there are any errors we re-throw the exception and let it bubble up to our global error handling system.

NOTE: Notice we use throw and not throw ex.

throw ex; //resets stack trace
throw; //preserves stack trace

If TryUpdateModelAsync returns false or an error or exception is thrown in the try/catch block we add a Roles property to the ViewBag object the same way we do in the HttpGet version of the Edit action and fill it with a list of roles, create an instance of EditUserViewModel and populate with values from the fetched and updated user, and re-rendor the view with the EditUser view model.

Add the Users Edit View

Create a new Razor file named Edit.cshtml in the Areas\Admin\Views\Users folder of the FredsCars project and fill it with the code below.

FredsCars\Areas\Admin\Views\Users\Edit.cshtml

@model EditUserViewModel

@{
    ViewData["Title"] = "Edit User";
}

<div class="container-fluid mt-3">
    <h3 class="text-center bg-primary-subtle py-2"
        style="border: 1px solid black;">
        Edit User
    </h3>
</div>

@if (Model.Id != null)
{
    <form asp-area="Admin" asp-controller="Users" asp-action="Edit">
        <div class="container"
        style="margin-top: 20px; border: 0px solid black">
            <div asp-validation-summary="ModelOnly" class="text-danger"></div>
            <input type="hidden" asp-for="Id" />
            <div class="row">
                <div class="col-2"></div>
                <div class="col-4">
                    <div class="mb-3">
                        <label asp-for="UserName" class="form-label fw-bold"></label>
                        <input asp-for="UserName"
                            class="form-control text-muted bg-light"
                            autocomplete="new-username"
                            tabindex="1"
                            readonly />
                        <span asp-validation-for="UserName" class="text-danger"></span>
                    </div>
                </div>
                <div class="col-2"></div>
            </div>
            <div class="row">
                <div class="col-2"></div>
                <div class="col-4">
                    <div class="mb-3">
                        <label asp-for="FirstName" class="form-label fw-bold"></label>
                        <input asp-for="FirstName" class="form-control"
                        tabindex="3" />
                        <span asp-validation-for="FirstName" class="text-danger"></span>
                    </div>
                    <div class="mb-3">
                        <label asp-for="Email" class="form-label fw-bold"></label>
                        <input asp-for="Email" class="form-control"
                        tabindex="5" />
                        <span asp-validation-for="Email" class="text-danger"></span>
                    </div>
                </div>
                <div class="col-4">
                    <div class="mb-3">
                        <label asp-for="LastName" class="form-label fw-bold"></label>
                        <input asp-for="LastName" class="form-control"
                        tabindex="4" />
                        <span asp-validation-for="LastName" class="text-danger"></span>
                    </div>
                    <div class="mb-3">
                        <label asp-for="Roles" class="form-label fw-bold"></label>
                        <select asp-for="Roles"
                        asp-items="@ViewBag.Roles"
                        class="form-select"
                        size="3">
                        </select>
                        <span asp-validation-for="Roles" class="text-danger"></span>
                    </div>
                </div>
                <div class="col-2"></div>
            </div>
            <div class="row">
                <div class="col-2"></div>
                <div class="col-4 text-center">
                    <div>
                        <input type="submit"
                        value="Edit"
                        class="btn btn-success" />
                        <a asp-area="Admin" asp-controller="Users"
                        asp-action="Index"
                        class="btn btn-secondary">Cancel</a>
                    </div>
                </div>
                <div class="col-2"></div>
            </div>
        </div>
    </form>
} else
{
    <div class="container mt-5 text-center text-bg-danger">
        <b>No user exists with that id.</b>
    </div>
    
}

@section Scripts {
    <partial name="_ValidationScriptsPartial" />
}

In the razor code above we declare the model of the view to be of type EditUserViewModel so that we can send all of the information about the user and their roles up to the HttpPost version of the Edit action in one fell swoop.

@model EditUserViewModel

After the usual boiler plate code we check if the Model.Id property is null. If it is not null we lay out the user edit form. If the Model.Id property is null we render a message that no user exists with that id. This can happen if no id is provided in the URL or no user is found with that id in the HttpGet version of the Users Edit action.

@if (Model.Id != null)
{
    ... layout edit form ...
} else
{
    <div class="container mt-5 text-center text-bg-danger">
        <b>No user exists with that id.</b>
    </div>
}

We use a form tag-helper to render the form and wire it up to go to the Edit action of the Users controller in the Admin area.

<form asp-area="Admin" asp-controller="Users" asp-action="Edit">
   ...
</form>

At the top of the form we have a validation summary tag helper to display model level errors and a hidden field bound to the Model’s Id property.

<div asp-validation-summary="ModelOnly" class="text-danger"></div>
<input type="hidden" asp-for="Id" />

Next, we have a Bootstrap row with a form group to display UserName. However, we have disabled the editing of UserName. We don’t want a user to be able to edit that because ASP.Net Core Identity links other features and foreign keys from other tables to that column/property.

<div class="row">
    <div class="col-2"></div>
    <div class="col-4">
        <div class="mb-3">
            <label asp-for="UserName" class="form-label fw-bold"></label>
            <input asp-for="UserName"
                class="form-control text-muted bg-light"
                autocomplete="new-username"
                tabindex="1"
                readonly />
            <span asp-validation-for="UserName" class="text-danger"></span>
        </div>
    </div>
    <div class="col-2"></div>
</div>

We have a second row with form groups for FirstName and Email, a third row with form groups for LastName and Roles, and a fourth row for Submit and Cancel buttons.

Add Edit button icon to Users Index page

Modify the Index view of the Users controller with the code shown below.

FredsCars\Areas\Admin\Views\Users\Index.cshtml

... existing code ...
<tbody>
    @foreach (var item in Model)
    {
        <tr>
            <td>
               <a asp-controller="Users"
                   asp-action="Edit"
                   asp-route-id="@item.User.Id">
                    <i class="bi bi-pencil text-success"></i>
                </a>
            </td>
            <td>@item.User.UserName</td>
            <td>@item.User.Email</td>
            <td>
                @if (item.Roles != null && item.Roles.Any())
                {
                    foreach (var role in item.Roles)
                    {
                        <div>@role</div>
                    }
                }
                else
                {
                    <span class="text-muted">No roles assigned</span>
                }
            </td>
        </tr>
    }
</tbody>
... existing code ...

Restart the application, log in as Admin, and navigate to https://localhost:40443/Admin/Users.

Click the new edit icon next to TestUserName.

The browser is redirected to the edit page for the TestUserName user. Edit the user with the following values and click the Edit button.

  • First Name: Test FName1
  • Last Name: Test LName1
  • Email: TestUserName1@FredsCars.com
  • Roles: Staff (Hold down the Ctrl key and mouse click Administrator to deselect it.)

The browser is redirected back to the Users Index page and you can see that the email for TestUserName had been changed to TestUserName1@FredsCars.com and the Administrator role has been removed. We will be able to see the rest of the edits once we create the Details page or you can look in SSOX or SSMS now with a query.

SELECT * FROM  AspNetUsers
WHERE UserName = 'TestUserName'

The User Details Feature

Create UserDetails view model

Once again let’s start by creating a view model so we can pass information about both a user (ApplicationUser) and its roles from the controller to the view all in one fell swoop.

Create a class named UserDetailsViewModel in the Models/Identity/ViewModels folder of the FredsCars project.

FredsCars\Models\Identity\ViewModels\UserDetailsViewModel.cs

namespace FredsCars.Models.Identity.ViewModels
{
    public class UserDetailsViewModel
    {
        public ApplicationUser User { get; set; } = new();

        public List<string> Roles { get; set; } = new();
    }
}

Update the Users controller (3)

Modify the UsersController with the code shown below.

FredsCars\Areas\Admin\Controllers\UsersController.cs

... existing code ...		
		
	[HttpPost]
	[ValidateAntiForgeryToken]
	public async Task<IActionResult> Edit(EditUserViewModel model)
	{
           ... existing code ...
	}

        public async Task<ViewResult> Details(string id)
        {
            ApplicationUser? user = await _userManager.FindByIdAsync(id);

            List<string> roleList = new();
            if (user != null)
            {
                var roles = await _userManager.GetRolesAsync(user);
                roleList = roles.ToList();
            }
            
            var model = new UserDetailsViewModel
            {
                User = user!,
                Roles = roleList
            };

            return View(model);
        }
    }
}

There is nothing new here. We simply fetch the user and its roles, populate a view model and pass it to the view.

Create the User Details view

Create a Razor file named Details.cshtml in the Areas/Admin/Views/Users folder of the FredsCars project and fill it with the code below.

FredsCars\Areas\Admin\Views\Users\Details.cshtml

@model UserDetailsViewModel

@{
    ViewData["Title"] = "User Details";
}

<div class="container-fluid py-4">
    <h3 class="text-center bg-primary-subtle py-2"
        style="border: 1px solid black;">
        User Details
    </h3>
</div>

@if (Model.User != null)
{
    <div class="container">
        <div class="row">
            <div class="col-2"></div>
            <div class="col-4">
                <div class="mb-3">
                    <b>@Html.DisplayNameFor(m => m.User.UserName)</b>
                    <br />
                    @Html.DisplayFor(m => m.User.UserName)
                </div>
            </div>
            <div class="col-2"></div>
        </div>
        <div class="row">
            <div class="col-2"></div>
            <div class="col-4">
                <div class="mb-3">
                    <b>@Html.DisplayNameFor(m => m.User.FirstName)</b>
                    <br />
                    @Html.DisplayFor(m => m.User.FirstName)
                </div>
            
            </div>
            <div class="col-4">
                <div class="mb-3">
                    <b>@Html.DisplayNameFor(m => m.User.LastName)</b>
                    <br />
                    @Html.DisplayFor(m => m.User.LastName)
                </div>
            </div>
            <div class="col-2"></div>
        </div>
        <div class="row">
            <div class="col-2"></div>
            <div class="col-4">
                <div class="mb-3">
                    <b>@Html.DisplayNameFor(m => m.User.Email)</b>
                    <br />
                    @Html.DisplayFor(m => m.User.Email)
                </div>

            </div>
            <div class="col-4">
                <div class="mb-3">
                    <b>@Html.DisplayNameFor(m => m.Roles)</b>
                    <br />
                    @foreach (var item in Model.Roles)
                    {
                        <span>@item</span><br />
                    }
                </div>
            </div>
            <div class="col-2"></div>
        </div>
        <div class="row">
            <div class="col-2"></div>
            <div class="col-4 text-center">
                <a asp-action="Index">
                    &lt;&lt; Back to users
                </a>
            </div>
            <div class="col-2"></div>
        </div>
    </div>
}
else
{
    <div class="container mt-5 text-center text-bg-danger">
        <b>No user exists with that id.</b>
    </div>
}

There is nothing new in the view above either. We are simply binding values from the model to the Html object’s DispalyNameFor and Display methods to display a label and value for each view model property.

Add Details link to Users on Users Index page

Modify the Users Index view with the code shown below.

FredsCars\Areas\Admin\Views\Users\Index.cshtml

... existing code ...
    
<tbody>
	@foreach (var item in Model)
	{
		<tr>
			<td>
				<a asp-controller="Users"
				   asp-action="Details"
				   asp-area="Admin"
				   asp-route-id="@item.User.Id">
					Details</a>
				<a asp-controller="Users"
				   asp-action="Edit"
				   asp-route-id="@item.User.Id">
					<i class="bi bi-pencil text-success"></i>
				</a>

			</td>
			<td>@item.User.UserName</td>
			<td>@item.User.Email</td>
			<td>
				@if (item.Roles != null && item.Roles.Any())
				{
					foreach (var role in item.Roles)
					{
						<div>@role</div>
					}
				}
				else
				{
					<span class="text-muted">No roles assigned</span>
				}
			</td>
		</tr>
	}
</tbody>

... existing code ...

In the view code above we have added a link to the Details page for each User result using an anchor tag helper.


Restart the application, navigate to https://localhost:40443/Admin/Users, and log in as Admin if needed.

Click the Details button for the TestUserName user.

The Details page for user TestUserName appears.

The Delete User Feature

< Prev
Next >

Leave a ReplyCancel reply

Chapter 1: Static HTML – Designing the landing page.

  • Static HTML – Designing the landing page.
  • Let’s get started!
  • Mock your site with HTML
  • Make CSS easy with Bootstrap
  • Mock your content
  • Introducing JavaScript
  • JavaScript Code Improvements
  • Results Data
  • Images and the HTML Image Element.
  • Revisiting Reusability for CSS and JavaScript
  • Reuse for HTML: PART 1
  • Reuse for HTML: PART 2
  • Details Page – Using a Bootstrap Component
  • Creating Links
  • Chapter One Conclusion

Chapter 2: ASP.Net Core – Let’s talk Dynamic

  • Introduction to ASP.Net Core
  • What is .Net?
  • What is ASP.Net
  • Introduction to Entity Framework Core

Chapter 3: ASP.Net MVC Core – Models, Views, and Controllers [ASP.Net Core v9]

  • Introduction to ASP.Net Core MVC
  • Create the project: ASP.Net Core MVC
  • Explore the ASP.Net Core Empty Web Project Template
  • Configure the Application for MVC
  • Create a Controller: Home Controller
  • Create a View: Index View for the Home Controller
  • Install Bootstrap using Libman
  • Create the Layout template
  • Create the Model
  • Install EF Core & Create the Database
  • Seed the Database: Loading test data
  • DI (Dependency Injection): Display a List of Vehicles
  • Repository Pattern: The Vehicles Repo
  • Unit Test 1: Home Controller Can Use Vehicle Repository
  • Unit Test 2: Vehicle Repository Can Return List
  • Add the ImagePath Migration and Thumbnail images to results
  • Pagination: Create a Custom Tag Helper
  • Sorting
  • Category Filter
  • Partial View: Break out the vehicle results
  • View Component: Create dynamic category buttons
  • Create the Details page
  • Create the Create Page
  • Create the Update Page
  • Create the Delete Page
  • Validation
  • Logging & Configuration
  • Storing Secrets
  • Error Handling
  • Security & Administration

Chapter 7: Using Server Side & Client Side technologies together. [ASP.Net Core v7 & Angular v15]

  • Intro to Full Stack Development
  • Fred’s Cars – Full Stack Development
  • Prepare the environment
  • Create the Visual Studio Solution
  • Add the ASP.Net Core Web API project
  • Add the Angular Project
  • Wire it up!
  • WeatherForecast: Understanding the basics
  • Vehicles API Controller: Mock Data
  • Vehicles Angular Component: Consuming Data
  • Routing and Navigation
  • Using a Component Library: Angular Material
  • Our first Angular Material Component: MatToolbar
  • Configuring for Saas: CSS with superpowers
  • Create the Header & Footer components
  • Displaying Results with MatTable
  • Loading: Using a Progress Spinner
  • MatTable: Client-Side Paging and Sorting
  • MatSidenav: Create a Search Sidebar
  • MatCheckbox: Category Search UI
  • Adding an image to the welcome page
  • Create the database with Entity Framework Core migrations
  • MatPaginator & PageEvent: Custom Server-Side Paging
  • Unit Testing: Custom Server-Side Paging
  • Repository Pattern: VehicleRepository
  • Unit Test: Paging in the Vehicles controller
  • Server-Side Sorting
  • Unit Tests: Sorting
  • Filter (Quick Search)
  • Unit Tests: Filter feature
  • Advanced Search: Categories
  • Unit Tests: Search by category
  • Progress Spinner: Final Fix

TOC

  • What were WebForms?
  • Enter MVC
    • Understanding MVC
    • Advantages of MVC
  • ASP.Net Core MVC – A total rewrite
  • ASP.Net Core 2 MVC – Here come Razor Pages
    • Understanding Razor Pages
  • ASP.Net Core 3 – Dropping the MVC reference
    • Understanding Blazor
  • Dropping the MVC reference
  • Hello .Net 5!
  • What’s Next? – Here comes .Net 6.

Recent Posts

  • Angular Commands Cheat Sheet
  • Installing Git for Windows
  • Installing Postman
  • Installing SQL Server 2022 Express
  • Installing Visual Studio 2022

Recent Comments

No comments to show.

Archives

  • November 2023
  • October 2023
  • June 2023
  • October 2021

Categories

  • Angular
  • ASP.Net
  • Environment Setup
  • See All
  • SQL Server
  • Visual Studio
  • Web API & Rest Services

WordPress Theme Editor

Copyright © 2025 Web Development School.

Powered by PressBook Blog WordPress theme