ASP.NET Core 2.2 - User Index Page

This article will cover adding a user index page to ASP.NET Core 2.2. I will assume you have created a new ASP.NET Core 2.2 Razor Pages project with Individual User Accounts, updated the database with the CreateIdentitySchema migration, scaffolded the Identity UI, implemented an EmailSender, require a confirmed email, implemented claims authorization policy, user's last login date and user claims. Or you can download the free ASP.NET Core 2.2 - Bootstrap Native Project from Manage > Assets. I was able to restore, build and run the project with VS 2017, VS 2019 and VS Code.

Manage Assets.

Claims are convenient for the current user but require extra methods from an administrative point of view. An administrator needs to employ UserManager.GetClaimsAsync(user) to access a user's claims. Because the index will not list all of a user's properties, I use an AJAX call and a modal to display user details. The user model for the JsonResult AJAX call implements JsonProperty PropertyName. Notice the user table will display more properties on larger screens.

Let's start by creating a new page for the user index. Create a new folder in Areas > Admin > Pages named Users and a new Index page in the Users folder.

Edit Index.cshtml:
@page
@model IndexModel
@{
    ViewData["Title"] = "User Admin";
}
<h1>@ViewData["Title"]</h1>
<div class="row">
    <div class="col-12">
        <table class="table table-striped table-sm border-bottom">
            <thead>
                <tr>
                    <th>
                        @Html.DisplayNameFor(model => model.DisplayUserList[0].UserName)
                    </th>
                    <th class="d-none d-md-table-cell">
                        @Html.DisplayNameFor(model => model.DisplayUserList[0].DisplayedName)
                    </th>
                    <th class="d-none d-lg-table-cell">
                        @Html.DisplayNameFor(model => model.DisplayUserList[0].Email)
                    </th>
                    <th>
                        @Html.DisplayNameFor(model => model.DisplayUserList[0].EmailConfirmed)
                    </th>
                    <th class="d-none d-lg-table-cell">
                        @Html.DisplayNameFor(model => model.DisplayUserList[0].CreateDate)
                    </th>
                    <th class="d-none d-lg-table-cell">
                        @Html.DisplayNameFor(model => model.DisplayUserList[0].LastLoginDate)
                    </th>
                    <th>
                        Actions
                    </th>
                </tr>
            </thead>
            <tbody>
                @if (Model.DisplayUserList.Count > 0)
                {
                    @foreach (var item in Model.DisplayUserList)
                    {
                        <tr>
                            <td>
                                @Html.DisplayFor(modelItem => item.UserName)
                            </td>
                            <td class="d-none d-md-table-cell">
                                @Html.DisplayFor(modelItem => item.DisplayedName)
                            </td>
                            <td class="d-none d-lg-table-cell">
                                @Html.DisplayFor(modelItem => item.Email)
                            </td>
                            <td class="custom-control custom-checkbox text-center">
                                <input type="checkbox" class="custom-control-input disabled" asp-for="@item.EmailConfirmed">
                                <label class="custom-control-label disabled">
                                    <span class="sr-only">Never</span>
                                </label>
                            </td>
                            <td class="d-none d-lg-table-cell">
                                @Html.DisplayFor(modelItem => item.CreateDate)
                            </td>
                            <td class="d-none d-lg-table-cell">
                                @Html.DisplayFor(modelItem => item.LastLoginDate)
                            </td>
                            <td>
                                <button type="button" class="btn btn-link p-0 pb-1 getdetails" data-id="@item.Id" data-toggle="modal" data-target="#userDetailsModal">Details</button>
                            </td>
                        </tr>
                    }
                }
                else
                {
                    <tr>
                        <td colspan="7" class="text-center">
                            No Users Found
                        </td>
                    </tr>
                }

            </tbody>
        </table>
    </div>
</div>
<div class="modal" id="userDetailsModal">
    <div class="modal-dialog">
        <div class="modal-content">

            <!-- Modal Header -->
            <div class="modal-header">
                <h4 class="modal-title">User Details</h4>
                <button type="button" class="close" data-dismiss="modal">×</button>
            </div>

            <!-- Modal body -->
            <div class="modal-body">
            </div>

            <!-- Modal footer -->
            <div class="modal-footer">
                <button type="button" class="btn btn-primary" data-dismiss="modal">Close</button>
            </div>

        </div>
    </div>
</div>

@section Scripts {
    <script>

        var modalBody = document.querySelector('.modal-body');

        function getUserById(id) {
            var xhr = new XMLHttpRequest();
            xhr.onreadystatechange = function () {
                if (this.readyState == 4 && this.status == 200) {
                    var responseJSON = JSON.parse(this.responseText);
                    // Set Data Modal
                    modalBody.innerHTML = '';
                    if (responseJSON.Status == 'Success') {
                        var dl = document.createElement('dl');
                        for (prop in responseJSON) {
                            var dt = document.createElement('dt');
                            dt.textContent = prop;
                            dl.appendChild(dt);
                            var dd = document.createElement('dd');
                            if (responseJSON[prop] == null) {
                                dd.textContent = 'null';
                            }
                            else if (responseJSON[prop].length == 0) {
                                dd.textContent = 'empty';
                            }
                            else {
                                dd.textContent = responseJSON[prop];
                            }

                            dl.appendChild(dd);
                        }
                        var successDiv = document.createElement('div');
                        successDiv.classList.add('alert', 'alert-success');
                        successDiv.appendChild(dl);
                        modalBody.appendChild(successDiv);
                    }
                    else {
                        var dl = document.createElement('dl');
                        for (prop in responseJSON) {
                            var dt = document.createElement('dt');
                            dt.textContent = prop;
                            dl.appendChild(dt);
                            var dd = document.createElement('dd');
                            dd.textContent = responseJSON[prop];
                            dl.appendChild(dd);
                        }
                        var alertDiv = document.createElement('div');
                        alertDiv.classList.add('alert', 'alert-danger');
                        alertDiv.appendChild(dl);
                        modalBody.appendChild(alertDiv);
                    }
                }
                else if (this.readyState == 4) {
                    // Set Bad Status Modal
                    modalBody.innerHTML = '';
                    var alertDiv = document.createElement('div');
                    alertDiv.classList.add('alert', 'alert-danger');
                    alertDiv.textContent = 'Error ' + this.status;
                    modalBody.appendChild(alertDiv);
                }
            };
            xhr.open('GET', '/admin/users/index?handler=UserById&userId=' + id);
            xhr.setRequestHeader('Content-Type', 'application/json;charset=UTF-8');
            xhr.onerror = function (error) {
                // Set Error Modal
                modalBody.innerHTML = '';
                var alertDiv = document.createElement('div');
                alertDiv.classList.add('alert', 'alert-danger');
                alertDiv.textContent = 'Error ' + error.target.status;
                modalBody.appendChild(alertDiv);
            };
            xhr.send();
        }

        document.addEventListener('DOMContentLoaded', function () {

            if (document.querySelector('.getdetails')) {
                var detailButtons = document.querySelectorAll('.getdetails');
                var dbl = detailButtons.length;
                for (b = 0; b < dbl; b++) {
                    detailButtons[b].addEventListener('click', function (event) {
                        // Set New Modal
                        modalBody.innerHTML = '';
                        // Begin Spinner - Bootstrap 4.2.1 or above
                        var spinnerDiv = document.createElement('div');
                        spinnerDiv.classList.add('spinner-border', 'text-alert', 'd-flex', 'mt-2', 'ml-auto', 'mr-auto');
                        spinnerDiv.setAttribute('role', 'status');
                        var span = document.createElement('span');
                        span.classList.add('sr-only');
                        span.innerText = 'Loading...';
                        spinnerDiv.appendChild(span);
                        modalBody.appendChild(spinnerDiv);
                        // End Spinner
                        getUserById(event.target.dataset.id);
                    });
                }
            }

        });

    </script>
}
Edit Index.cshtml.cs:
[Authorize(Policy = "AdministratorOnly")]
public class IndexModel : PageModel
{
    private readonly ApplicationUserManager _userManager;

    public IndexModel(
        ApplicationUserManager userManager)
    {
        _userManager = userManager;
    }

    public int Count { get; set; }

    public IList<DisplayUserModel> DisplayUserList { get; set; } = new List<DisplayUserModel>();

    public class DisplayUserModel
    {
        [Key]
        public string Id { get; set; }

        [Display(Name = "Login Name")]
        public string UserName { get; set; }

        [Display(Name = "Displayed")]
        public string DisplayedName { get; set; }

        public string Email { get; set; }

        [Display(Name = "Confirmed")]
        public bool EmailConfirmed { get; set; }

        [Display(Name = "Created")]
        public string CreateDate { get; set; }

        [Display(Name = "Last Login")]
        public string LastLoginDate { get; set; }
    }

    public UserJsonModel UserJson { get; set; }

    public class UserJsonModel
    {
        [JsonProperty(PropertyName = "Status")]
        public string Status { get; set; }
        [JsonProperty(PropertyName = "Id")]
        public string Id { get; set; }
        [JsonProperty(PropertyName = "Login Name")]
        public string UserName { get; set; }
        [JsonProperty(PropertyName = "Displayed Name")]
        public string DisplayedName { get; set; }
        [JsonProperty(PropertyName = "Email")]
        public string Email { get; set; }
        [JsonProperty(PropertyName = "Confirmed")]
        public bool EmailConfirmed { get; set; }
        [JsonProperty(PropertyName = "New Email")]
        public string UnconfirmedEmail { get; set; }
        [JsonProperty(PropertyName = "Phone")]
        public string PhoneNumber { get; set; }
        [JsonProperty(PropertyName = "Phone Confirmed")]
        public bool PhoneNumberConfirmed { get; set; }
        [JsonProperty(PropertyName = "2 Factor")]
        public bool TwoFactorEnabled { get; set; }
        [JsonProperty(PropertyName = "Access Failed")]
        public int AccessFailedCount { get; set; }
        [JsonProperty(PropertyName = "Lockout Till")]
        public string LockoutEnd { get; set; }
        [JsonProperty(PropertyName = "Claims Count")]
        public int ClaimsCount { get; set; }
        [JsonProperty(PropertyName = "Create Date")]
        public string CreateDate { get; set; }
        [JsonProperty(PropertyName = "Last Login Date")]
        public string LastLoginDate { get; set; }
    }

    public async Task OnGetAsync()
    {
        List<string> userIds = await _userManager.Users
            .OrderBy(u => u.UserName)
            .Select(u => u.Id)
            .ToListAsync();

        Count = userIds.Count();

        DisplayUserList = new List<DisplayUserModel>();

        foreach (var userId in userIds)
        {
            var aUser = await _userManager.FindByIdAsync(userId);
            var claims = await _userManager.GetClaimsAsync(aUser);
            var aUserDisplayedName = claims
                .Where(c => c.Type == ApplicationClaimType.DisplayedName)
                .Select(c => c.Value).SingleOrDefault();

            var dum = new DisplayUserModel
            {
                Id = aUser.Id,
                UserName = aUser.UserName,
                DisplayedName = aUserDisplayedName,
                Email = aUser.Email,
                EmailConfirmed = aUser.EmailConfirmed,
                CreateDate = string.Format("{0:MM/dd/yyyy}", aUser.CreateDate),
                LastLoginDate = aUser.LastLoginDate.Year < 2 ? "" : string.Format("{0:MM/dd/yyyy HH:mm}", aUser.LastLoginDate)
            };
            DisplayUserList.Add(dum);
        }
    }

    public async Task<JsonResult> OnGetUserByIdAsync(string userId)
    {
        if (userId == null)
        {
            return new JsonResult(new { Status = "Failed", Errors = "UserId = null" });
        }

        var user = await _userManager.FindByIdAsync(userId);

        if (user == null)
        {
            return new JsonResult(new { Status = "Failed", Errors = "User Not Found" });
        }

        IList<Claim> claims = await _userManager.GetClaimsAsync(user);

        var displayedName = claims.Where(c => c.Type == ApplicationClaimType.DisplayedName)
            .Select(c => c.Value).SingleOrDefault();

        UserJson = new UserJsonModel
        {
            Status = "Success",
            Id = user.Id,
            UserName = user.UserName,
            DisplayedName = displayedName,
            Email = user.Email,
            EmailConfirmed = user.EmailConfirmed,
            UnconfirmedEmail = string.IsNullOrEmpty(user.UnconfirmedEmail) ? "" : user.UnconfirmedEmail,
            PhoneNumber = string.IsNullOrEmpty(user.PhoneNumber) ? "" : user.PhoneNumber,
            PhoneNumberConfirmed = user.PhoneNumberConfirmed,
            TwoFactorEnabled = user.TwoFactorEnabled,
            AccessFailedCount = user.AccessFailedCount,
            LockoutEnd = user.LockoutEnd == null ? "" : string.Format("{0:MM/dd/yyyy HH:mm} GMT", user.LockoutEnd),
            ClaimsCount = claims.Count(),
            CreateDate = string.Format("{0:MM/dd/yyyy HH:mm}", user.CreateDate),
            LastLoginDate = user.LastLoginDate.Year < 2 ? "" : string.Format("{0:MM/dd/yyyy HH:mm}", user.LastLoginDate)
        };
        return new JsonResult(UserJson);
    }

}

Restrict folder access to authenticated users with AuthorizeAreaFolder.

Edit Startup.cs > ConfigureServices > services.AddMvc > AddRazorPagesOptions, add AuthorizeAreaFolder:
options.Conventions.AuthorizeAreaFolder("Admin", "/Users");

Add a link from the Admin > Index page to Admin > Users > Index page.

Edit Admin > Index.cshtml, add link:
<div class="row pt-2 pb-2 border-top border-bottom">
    <div class="col-12">
        <a class="btn btn-primary" asp-page="Users/Index">Users</a>
    </div>
</div>

Build, run and test.

Ken Haggerty
Created 04/27/19
Updated 04/28/19 08:05 GMT

Log In or Reset Quota to read more.

Successfully completed. Thank you for contributing.
Processing...
Something went wrong. Please try again.
Contribute to enjoy content without advertisments.
You can contribute without registering.

Comments(0)

Loading...
Loading...

Not accepting new comments.

Submit your comment. Comments are moderated.

User Image.
DisplayedName - Member Since ?