Welcome to Part 33 of our Mobile Shop development series! Today, we are kicking off a critical security and administrative module: the User Management System.
In Step 1, we establish our base operations center by creating the UsersController inside our Admin area. This controller serves as the control hub where store managers can view registered users, assign roles (like Admin, Employee, or Customer), and manage account statuses.
Here is the step-by-step structural breakdown of how this foundational identity controller is constructed and secured.
Step 1: Users Controller Core Architecture
The Security & Identity Control Pipeline
Step-by-Step Code Explanation
Structural Routing & Strict Role Security
[Area("Admin")]
[Authorize(Roles = "Admin")]
public class UsersController : Controller
[Area("Admin")]: Informs the ASP.NET Core routing engine that this controller belongs inside our isolated administrative folder structure, sorting our workspace cleanly.[Authorize(Roles = "Admin")]: This is your primary security guard rail. By locking the entire controller down toRoles = "Admin", you ensure that standard storefront customers or malicious users can never guess or brute-force the URL path to modify user settings. If a non-admin attempts to access this, the framework instantly blocks them and redirects to an Access Denied landing page.
Core Dependency Injection Services
private readonly UserManager<ApplicationUser> _userManager;
private readonly RoleManager<IdentityRole> _roleManager;
private readonly ApplicationDbContext _context;
To fully manage user accounts, we inject three separate, powerful database management APIs:
UserManager<ApplicationUser>: The standard Microsoft Identity manager service. It handles safe password resets, user creation, email confirmations, and direct profile field updates.RoleManager<IdentityRole>: Responsible for the global roles table. It tracks what security roles exist in your mobile shop platform (e.g., creating, updating, or querying the Admin or Customer groups).ApplicationDbContext: Our core Entity Framework entity gateway, allowing us to perform quick, custom joins across standard relational database tables when compiling user summary lists.
Explicit Constructor Initialization
public UsersController(
UserManager<ApplicationUser> userManager,
RoleManager<IdentityRole> roleManager,
ApplicationDbContext context)
{
_userManager = userManager;
_roleManager = roleManager;
_context = context;
}
Leverages standard built-in Dependency Injection (DI). When a web request routes into this controller, the ASP.NET Core runtime automatically fetches the pre-configured Identity instances from your service container and pipes them safely into these local read-only backing fields.
In Step 2 of Part 33, we write the primary data coordination engine for our administration panel: the Index Action Method.
This method handles pulling the master registry of users out of Microsoft Identity. It processes server-side keyword filters, tracks down what access roles belong to each individual user, checks customer transaction histories to calculate lifetime purchase counts, and implements clean in-memory pagination to keep page rendering ultra-fast.
Here is the step-by-step breakdown of how this multi-stage retrieval pipeline runs.
Step 2: Advanced User Ingestion & Filtering Workflow
The Multi-Stage Account Aggregation Pipeline
Step-by-Step Code Explanation
Deferred Execution Query Setup
{
var query = _userManager.Users.AsQueryable();
Parameters: Accepts optional
searchkeywords, an optional structural securityrolestring, and an explicit fallback tracker parameterpage = 1for handling navigation requests.AsQueryable(): This is an essential performance best-practice. Instead of immediately executing a heavy dump of all users into system memory, this sets up an unexecuted IQueryable expression tree block. This allows us to append conditional queries dynamically before hitting the SQL server.
Multi-Field Structural Search Mapping
query = query.Where(u => u.Email.Contains(search) ||
u.FirstName.Contains(search) ||
u.LastName.Contains(search));
var users = await query
.OrderByDescending(u => u.CreatedAt)
.ToListAsync();
Evaluates if a search string was typed. If true, it adds SQL
LIKEstatement clauses across multiple data fields (Email,FirstName, andLastName) to isolate matches cleanly.ToListAsync(): Executes the query and pulls the filtered, chronologically sorted (
OrderByDescending) user account records into a lightweight backend list.
Role Tracking & Cross-Table Relational Lookups
{
var roles = await _userManager.GetRolesAsync(user);
var orderCount = await _context.Orders.CountAsync(o => o.UserId == user.Id);
Because Microsoft Identity stores roles in isolated map tables (
AspNetUserRoles), we iterate over our users and call GetRolesAsync(user) for each one to find their active permissions tier.Cross-Context Aggregation: It queries our standard e-commerce context concurrently via
CountAsync(o => o.UserId == user.Id)to calculate exactly how many physical checkout invoices this profile has placed.
Post-Filter Drop Guard
continue;
If an administrator has filtered the dashboard to show only a specific role (like Employee), this check checks the user's role list. If they do not match, the code triggers a
continue, skipping that record entirely so it never reaches the output model.
View Model Mapping & Population
{
UserId = user.Id,
FullName = user.FullName,
Email = user.Email!,
PhoneNumber = user.PhoneNumber ?? "",
Roles = roles.ToList(),
IsActive = user.IsActive,
CreatedAt = user.CreatedAt,
OrderCount = orderCount
});
Safely translates raw database entity shapes into our specialized, UI-ready UserManagementViewModel package container, protecting underlying framework parameters from client-side vulnerability disclosures.
In-Memory Sub-Pagination Logic
var totalItems = userViewModels.Count;
var pagedUsers = userViewModels
.Skip((page - 1) * pageSize)
.Take(pageSize)
.ToList();
To keep screen layouts clean and uniform, we limit view summaries to 20 records per page.
Using our page number index, we calculate the dynamic skip window offset—for instance, page 2 executes a skip of $(2 - 1) \times 20 = 20$ records—and safely captures the remaining block via
.Take(pageSize).
ViewBag Tracking Delivery Context
ViewBag.CurrentRole = role;
ViewBag.Search = search;
ViewBag.CurrentPage = page;
ViewBag.TotalPages = (int)Math.Ceiling(totalItems / (double)pageSize);
return View(pagedUsers);
Collects every global available role name from
_roleManagerto populate a drop-down filter component on our front end.Packs navigation tracking state parameters securely inside
ViewBagtokens to construct clean pagination button groups, returning our cleanly sliced data array straight to the user.
In Step 3 of Part 33, we are creating the front-end user interface: the Index.cshtml view for User Management.
This view provides store administrators with a comprehensive control panel. It includes an interactive top search filter bar, a responsive data grid summarizing user credentials, dynamic state styling badges, and an integrated pagination component to easily browse through your platform's customer and staff directory.
Step 3: User Management UI Anatomy
The User Directory Panel Layout
Step-by-Step UI Layout Explanation
Preserving Search States in the Filter Bar
<input type="text" name="search" class="form-control" placeholder="..." value="@ViewBag.Search" />
...
<option value="@role" selected="@(ViewBag.CurrentRole == role ? "selected" : null)">@role</option>
Form
method="get": Submits the filters via the URL query string. This enables bookmarking and ensures the parameters are cleanly picked up by the pagination system.State Preservation: By reading back
@ViewBag.Searchand using an inline ternary operator on the role options, the text fields and drop-downs remain populated with the admin's active filter criteria after the page reloads.
Responsive Data Grid Loop & Badges
@foreach (var role in user.Roles)
{
<span class="badge bg-primary me-1">@role</span>
}
Accounts can hold multiple permission tiers simultaneously (e.g., a user can be both an Employee and an Admin). This nested loop elegantly handles multiple items by outputting every active security group within a separate Bootstrap badge component.
Account Status Logic: Evaluates the boolean
@user.IsActivevariable inline. If true, it paints a greenActivebadge; if false, it shows a grayInactivelabel, allowing administrators to scan the directory for disabled accounts at a glance.
Inline Action Forms & Context-Aware Controls
<a asp-action="Details" asp-route-id="@user.UserId" class="btn btn-sm btn-outline-primary">...</a>
<form asp-action="ToggleStatus" method="post" class="d-inline">
<input type="hidden" name="id" value="@user.UserId" />
<button type="submit" class="btn btn-sm @(user.IsActive ? "btn-outline-warning" : "btn-outline-success")">
<i class="bi @(user.IsActive ? "bi-pause-circle" : "bi-play-circle")"></i>
</button>
</form>
Details Action: Provides a clean view link targeting
/Users/Details/{id}to inspect deeper account specifics.State-Altering Security Guard (
method="post"): Because changing an account's active state alters data, we wrap the freeze button inside a valid POST form instead of a standard hyperlink. This protects the endpoint against accidental background trigger modifications or malicious link-crawlers.Contextual UI Morphing: The action button automatically shifts style parameters depending on the user's current status:
If a profile is Active, the button changes to a yellow caution outline with a pause symbol (
bi-pause-circle), signaling a "suspend" option.If a profile is Suspended, it morphs into a green play outline (
bi-play-circle), inviting the admin to re-activate the account.
Filter-Aware Pagination Navigator
The Route-Sustaining Trick: This is a vital piece of navigation logic. When an administrator clicks to browse to page 2, the system must remember their active search queries. By binding
asp-route-roleandasp-route-searchstraight into the generated anchor tags, the layout appends all active query arguments to every page index step. This prevents the grid from resetting your filtered list back to the global un-filtered directory list.
In Step 4 of Part 33, we implement the core data-modification handler for our directory panel: the ToggleStatus Action Method.
This method handles state-changing requests sent by the inline forms we designed in the previous step. It locates the specific user record via their unique identity token, flips their active account status flag, commits those changes back to the SQL database using Microsoft Identity, and sends a temporary flash message to notify the admin of the change.
Step 4: Account Suspension & Activation Workflow
The State Toggle Lifecycle Pipeline
Step-by-Step Code Explanation
Secure HTTP Method Restriction
public async Task<IActionResult> ToggleStatus(string id)
[HttpPost]: This is an essential security attribute. Since this action alters data states (freezing or unfreezing accounts), it must never acceptGETrequests. Restricting it toPOSTprevents cross-site scripting vulnerabilities, accidental search-engine bot triggers, or malicious link-clicks from altering user status.
Records Validation Check
if (user == null)
return NotFound();
The method takes the incoming
idstring string and executesFindByIdAsync(id).If the user ID is invalid, modified by a client-side injection, or missing from the database entirely, it stops execution immediately and returns a standard
404 NotFound()HTTP response code.
Bitwise Boolean State Inversion
user.IsActive = !user.IsActive;
await _userManager.UpdateAsync(user);
!user.IsActive(Logical Negation Operator): Instead of writing separate methods for activation and deactivation, this single line acts as a clean, elegant toggle switch. IfIsActiveis currentlytrue, it flips tofalse; if it isfalse, it flips totrue._userManager.UpdateAsync(user): Instructs Microsoft Identity to build and run an optimized SQLUPDATEstatement, pushing the fresh status modification directly down into your persistent user rows.
Dynamic Feedback Broadcast & View Redirection
var status = user.IsActive ? "activated" : "deactivated";
TempData["Success"] = $"User {status} successfully.";
return RedirectToAction(nameof(Index));
Using a quick ternary statement, it evaluates the final state to construct a precise, clean feedback string notification message (e.g., "User deactivated successfully.").
TempData["Success"]: Stores this message inside the short-term cookie state cache. TempData survives exactly one redirect trip across pages, making it perfect for driving top-screen toast notification bars.RedirectToAction(nameof(Index)): Refreshes the browser workspace cleanly by redirecting administrators back to the main user directory table grid, showing the updated account status badge immediately.

Comments
Post a Comment