MobileShop Website Part 34 | User Roles Settings Setup in ASP.NET Core MVC


 Welcome to Part 34 of our Mobile Shop development series! Today, we are diving into the User Roles Settings Setup.

In Step 1, we implement the Details Action Method inside your administrative user management controller. This method acts as a comprehensive customer profile inspector. When an administrator clicks to view a user, this action fetches their identity profile, dynamically pulls their assigned security permissions, tracks down their last 10 retail orders from your database, and prepares this composite view data for the frontend layout canvas.

Here is the step-by-step structural breakdown of how this profile retrieval method functions.

Step 1: User Profile & Order History Inspector

The Multi-Table Data Aggregation Pipeline

C# / Areas/Admin/Controllers/UsersController.cs (Details Method)
public async Task<IActionResult> Details(string id)
{
    var user = await _userManager.FindByIdAsync(id);
    if (user == null)
        return NotFound();

    var roles = await _userManager.GetRolesAsync(user);
    var orders = await _context.Orders
        .Where(o => o.UserId == id)
        .OrderByDescending(o => o.OrderDate)
        .Take(10)
        .ToListAsync();

    ViewBag.Roles = roles;
    ViewBag.Orders = orders;

    return View(user);
}

Step-by-Step Code Explanation

Identity Validation & Null-Guard Check

public async Task<IActionResult> Details(string id)
{
    var user = await _userManager.FindByIdAsync(id);
    if (user == null)
        return NotFound();
  • FindByIdAsync(id): Uses your injected Microsoft Identity UserManager API to search the underlying core database strings for a matching string primary key identifier.

  • Safety Guard: If the id parameter is malformed, intentionally manipulated by a user, or doesn't exist, the system short-circuits instantly and drops a clean 404 NotFound() HTTP response code, ensuring no null reference errors exception crashes occur down the page.

Permissions Mapping Query

var roles = await _userManager.GetRolesAsync(user);
  • GetRolesAsync(user): Instructs Identity to read across the many-to-many junction mapping table (AspNetUserRoles). It collects all text permission tags associated with this user profile (such as Admin, Customer, or Employee) into a flat string list collection.

Performance-Optimized Purchase Log Slicing

var orders = await _context.Orders
    .Where(o => o.UserId == id)
    .OrderByDescending(o => o.OrderDate)
    .Take(10)
    .ToListAsync();
  • This queries your primary e-commerce database entity context table mapping (_context.Orders).

  • Where(o => o.UserId == id): Screens the global purchase index table, isolating only the order records belonging explicitly to this customer.

  • OrderByDescending(o => o.OrderDate): Chronologically sorts the database records, putting the newest receipts right at the top of the list.

  • Take(10): This is an essential database performance best practice. If a loyal customer has placed hundreds of orders over multiple years, loading their entire transactional history onto a single summary card will slow down your server's database response. Limiting the pull to the most recent 10 transactions ensures the dashboard panel renders instantly.

Dynamic Packaging and Payload Delivery

ViewBag.Roles = roles;
ViewBag.Orders = orders;

return View(user);
  • Since a controller view can only accept a single object model data pass contract via return View(user), we utilize the flexible ViewBag data storage collection.

  • This allows us to load the core user model into the main page slot, while smoothly sliding the separate roles list and orders data array along the side of the request pipeline into your Razor view components.

In Step 2 of Part 34, we build out the front-end interface: the Details.cshtml view for User Roles and Settings.

This view functions as a 3-column administrative control cockpit. It displays complete customer data profile strings, handles shipping destination fields, loops through recent transactional orders using a C# switch pattern, and implements an interactive role management panel featuring inline POST forms to add or strip user access levels.

Step 2: Advanced User Profile UI Anatomy

The 3-Column Cockpit Layout Hierarchy

CSHTML / Areas/Admin/Views/Users/Details.cshtml
@model ApplicationUser
@{
    ViewData["Title"] = $"User: {Model.FullName}";
}

<div class="d-flex justify-content-between align-items-center mb-4">
    <h3><i class="bi bi-person"></i> @Model.FullName</h3>
    <a asp-action="Index" class="btn btn-secondary"><i class="bi bi-arrow-left"></i> Back to Users</a>
</div>

<div class="row g-4">
    <div class="col-lg-4">
        <div class="card shadow-sm">
            <div class="card-header"><h5 class="mb-0">User Information</h5></div>
            <div class="card-body">
                <p class="mb-2"><strong>User ID:</strong> <code class="small">@Model.Id</code></p>
                <p class="mb-2"><strong>Full Name:</strong> @Model.FullName</p>
                <p class="mb-2"><strong>Email:</strong> @Model.Email</p>
                <p class="mb-2"><strong>Phone:</strong> @(string.IsNullOrEmpty(Model.PhoneNumber) ? "Not provided" : Model.PhoneNumber)</p>
                <p class="mb-2"><strong>Date of Birth:</strong> @(Model.DateOfBirth?.ToString("MMM dd, yyyy") ?? "Not provided")</p>
                <p class="mb-2"><strong>Joined:</strong> @Model.CreatedAt.ToString("MMM dd, yyyy HH:mm")</p>
                <p class="mb-0">
                    <strong>Status:</strong>
                    @if (Model.IsActive)
                    {
                        <span class="badge bg-success">Active</span>
                    }
                    else
                    {
                        <span class="badge bg-secondary">Inactive</span>
                    }
                </p>
            </div>
        </div>

        <div class="card shadow-sm mt-4">
            <div class="card-header"><h5 class="mb-0">Roles</h5></div>
            <div class="card-body">
                @foreach (var role in ViewBag.Roles)
                {
                    <div class="d-flex justify-content-between align-items-center mb-2">
                        <span class="badge bg-primary">@role</span>
                        <form asp-action="UpdateRole" method="post" class="d-inline">
                            <input type="hidden" name="id" value="@Model.Id" />
                            <input type="hidden" name="role" value="@role" />
                            <input type="hidden" name="add" value="false" />
                            <button type="submit" class="btn btn-sm btn-outline-danger">Remove</button>
                        </form>
                    </div>
                }

                <hr />
                <form asp-action="UpdateRole" method="post" class="d-flex gap-2">
                    <input type="hidden" name="id" value="@Model.Id" />
                    <input type="hidden" name="add" value="true" />
                    <select name="role" class="form-select form-select-sm">
                        <option value="Admin">Admin</option>
                        <option value="Customer">Customer</option>
                        <option value="Manager">Manager</option>
                    </select>
                    <button type="submit" class="btn btn-sm btn-success">Add</button>
                </form>
            </div>
        </div>
    </div>

    <div class="col-lg-4">
        <div class="card shadow-sm">
            <div class="card-header"><h5 class="mb-0">Address</h5></div>
            <div class="card-body">
                <p class="mb-1"><strong>Address:</strong> @(Model.Address ?? "Not provided")</p>
                <p class="mb-1"><strong>City:</strong> @(Model.City ?? "-")</p>
                <p class="mb-1"><strong>Postal Code:</strong> @(Model.PostalCode ?? "-")</p>
                <p class="mb-0"><strong>Country:</strong> @(Model.Country ?? "-")</p>
            </div>
        </div>
    </div>

    <div class="col-lg-4">
        <div class="card shadow-sm">
            <div class="card-header"><h5 class="mb-0">Recent Orders</h5></div>
            <div class="card-body">
                @if (ViewBag.Orders != null && ((List<Order>)ViewBag.Orders).Count > 0)
                {
                    <div class="list-group list-group-flush">
                        @foreach (var order in (List<Order>)ViewBag.Orders)
                        {
                            <div class="list-group-item d-flex justify-content-between align-items-center">
                                <div>
                                    <h6 class="mb-0">@order.OrderNumber</h6>
                                    <small class="text-muted">@order.OrderDate.ToString("MMM dd, yyyy")</small>
                                </div>
                                <span class="badge @(order.Status switch {
                                    OrderStatus.Pending => "bg-warning",
                                    OrderStatus.Processing => "bg-info",
                                    OrderStatus.Shipped => "bg-primary",
                                    OrderStatus.Delivered => "bg-success",
                                    OrderStatus.Cancelled => "bg-danger",
                                    _ => "bg-secondary"
                                })">@order.Status</span>
                            </div>
                        }
                    </div>
                }
                else
                {
                    <p class="text-muted mb-0">No orders yet.</p>
                }
            </div>
        </div>

        <div class="card shadow-sm mt-4">
            <div class="card-header"><h5 class="mb-0">Actions</h5></div>
            <div class="card-body">
                <form asp-action="ToggleStatus" method="post">
                    <input type="hidden" name="id" value="@Model.Id" />
                    <button type="submit" class="btn @(Model.IsActive ? "btn-warning" : "btn-success") w-100">
                        <i class="bi @(Model.IsActive ? "bi-pause-circle" : "bi-play-circle")"></i>
                        @(Model.IsActive ? "Deactivate User" : "Activate User")
                    </button>
                </form>
            </div>
        </div>
    </div>
</div>

Step-by-Step UI Layout Explanation

Integrated Role-Management Form Architecture

@foreach (var role in ViewBag.Roles) {
    <form asp-action="UpdateRole" method="post" class="d-inline">
        <input type="hidden" name="id" value="@Model.Id" />
        <input type="hidden" name="role" value="@role" />
        <input type="hidden" name="add" value="false" />
        <button type="submit" class="btn btn-sm btn-outline-danger">Remove</button>
    </form>
}
  • Granular Role Revocation: The view loops through all roles currently held by the user. Next to each badge, it renders a specialized, self-contained POST configuration form.

  • By explicitly passing name="add" value="false", clicking "Remove" targets our upcoming backend action method to safely strip that exact access token away without affecting other assignments.

Additive Role Assignment Form

<form asp-action="UpdateRole" method="post" class="d-flex gap-2">
    <input type="hidden" name="id" value="@Model.Id" />
    <input type="hidden" name="add" value="true" />
    <select name="role" class="form-select form-select-sm">...</select>
    <button type="submit" class="btn btn-sm btn-success">Add</button>
</form>
  • Sitting directly below a thematic horizontal rule separator (<hr />), this form passes a contrasting parameter (name="add" value="true"). When the administrator selects a tier from the dropdown list and hits "Add", it submits an intent to append that security tier to the profile.

Explicit Model Casting & Inline Switch Pattern

@foreach (var order in (List<Order>)ViewBag.Orders) {
    <span class="badge @(order.Status switch {
        OrderStatus.Pending => "bg-warning",
        OrderStatus.Processing => "bg-info",
        OrderStatus.Shipped => "bg-primary",
        OrderStatus.Delivered => "bg-success",
        OrderStatus.Cancelled => "bg-danger",
        _ => "bg-secondary"
    })">@order.Status</span>
}
  • Explicit C# Casting: Because objects inside ViewBag are evaluated as dynamic types at runtime, we cast the payload explicitly using (List<Order>)ViewBag.Orders so the Razor template can safely map properties inside our loops.

  • The C# Switch Expression: We use a clean C# pattern inside our HTML class attribute wrapper. It evaluates the strongly typed order.Status enumeration value and returns a matching Bootstrap layout color helper class (e.g., yellow for Pending, green for Delivered, and red for Cancelled).

In Step 3 of Part 34, we implement the data-modification backend engine for our profile panel: the UpdateRole Action Method.

This method acts as the security processor for the two inline forms we built in the last step. It accepts the target user’s identifier, the role name string, and a boolean flag (add) determining the operation type. It then securely alters the user's security clearance mapping in the database before redirecting back to the profile cockpit.

Step 3: Role Modification Processing Workflow

The Role Assignment & Revocation Pipeline

C# / Areas/Admin/Controllers/UsersController.cs (UpdateRole Method)
[HttpPost]
public async Task<IActionResult> UpdateRole(string id, string role, bool add)
{
    var user = await _userManager.FindByIdAsync(id);
    if (user == null)
        return NotFound();

    if (add)
    {
        if (!await _userManager.IsInRoleAsync(user, role))
        {
            await _userManager.AddToRoleAsync(user, role);
            TempData["Success"] = $"Role '{role}' added to user.";
        }
    }
    else
    {
        if (await _userManager.IsInRoleAsync(user, role))
        {
            await _userManager.RemoveFromRoleAsync(user, role);
            TempData["Success"] = $"Role '{role}' removed from user.";
        }
    }

    return RedirectToAction(nameof(Details), new { id });
}

Step-by-Step Code Explanation

Secure Form Routing Parameters

[HttpPost]
public async Task<IActionResult> UpdateRole(string id, string role, bool add)
  • [HttpPost]: Restricts this action to HTTP POST requests. Because modifying security permissions alters system behavior, blocking GET requests prevents malicious trick URLs from changing user roles.

  • The add Parameter: This boolean variable maps directly to the hidden inputs from our forms (value="true" or value="false"), allowing a single action method to gracefully handle both granting and revoking roles.

User Profile Isolation

var user = await _userManager.FindByIdAsync(id);
if (user == null)
    return NotFound();
  • Searches your Identity framework table for the matching profile. If the ID is invalid or not found, it short-circuits the request and drops a clean 404 NotFound() warning page.

The Additive Permission Path (add == true)

if (add)
{
    if (!await _userManager.IsInRoleAsync(user, role))
    {
        await _userManager.AddToRoleAsync(user, role);
        TempData["Success"] = $"Role '{role}' added to user.";
    }
}
  • Idempotency Guard (IsInRoleAsync): Before adding a role, the system checks if the user already holds it. This prevents database duplicate-key crashes if an administrator opens multiple tabs and double-clicks the "Add" button.

  • AddToRoleAsync(user, role): Instructs Identity to safely insert a fresh relational mapping row linking this user's primary key to the target security role index.

The Subtractive Revocation Path (add == false)

else
{
    if (await _userManager.IsInRoleAsync(user, role))
    {
        await _userManager.RemoveFromRoleAsync(user, role);
        TempData["Success"] = $"Role '{role}' removed from user.";
    }
}
  • If add evaluates to false, execution drops down into the else block to handle role removal.

  • It verifies the user currently holds that role, and then triggers RemoveFromRoleAsync(user, role) to delete that specific mapping row from the database table.

 Dashboard Navigation Loop Synchronization

return RedirectToAction(nameof(Details), new { id });
  • new { id } Route Values: To refresh the screen correctly after processing the change, the controller redirects back to our Details inspector view. Because the Details method requires a route ID parameter to load a profile, we pass an anonymous route values object container (new { id }) containing our current user's identification string.


Comments