Back to: ASP.NET Core Identity Tutorials
Users Management in ASP.NET Core Identity and MVC
In this article, I will discuss Users Management in ASP.NET Core Identity using an MVC Application. Please read our previous article discussing Roles Management in ASP.NET Core Identity. User management in ASP.NET Core Identity involves creating, updating, retrieving, and deleting user accounts within an application that uses the Identity framework for authentication and authorization.
The goal of the Users Management is to provide administrators with full control over the user lifecycle, ensuring secure handling of sensitive information and maintaining proper access levels across the application.
What is a User?
A User is any individual who interacts with a system, application, or platform by logging in with a unique identity. In ASP.NET Core Identity, that’s typically an ApplicationUser record (extending IdentityUser) with credentials, profile data, and security attributes (e.g., email confirmed, lockout, 2FA, claims). In plain terms:
- Who they are (ID, email/username, profile)
- How they sign in (password, external login, MFA)
- What they’re allowed to do (roles/claims/permissions)
Example:
- In an e-commerce application, john@example.com is a registered customer user.
- In a school management system, admin@school.com is an admin user.
Why Do We Have Different Types of Users?
Not all users should have the same access or capabilities. Different types of users allow us to enforce role-based access control (RBAC) and security boundaries.
- Security: Prevents unauthorized access to sensitive features (e.g., only admins can manage roles).
- Personalization: Different users get different experiences (e.g., a customer sees their orders; a seller sees their products).
- Efficiency: Reduces clutter by showing only relevant features to each user type.
Common User Types in ASP.NET Core Identity:
- Admin: Full control over the application (manage users, roles, and system settings).
- Manager: Can approve/reject content or transactions, but cannot manage other admins.
- Customer/User: Can only manage their own profile, data, and actions.
- Guest: Limited or read-only access without logging in.
Why Do We Need User Management?
User Management is the process of creating, modifying, viewing, and deleting user accounts while controlling their roles, permissions, and status.
We need it for:
- Access Control: Define who can do what in the application.
- Security: Activate/deactivate accounts, reset passwords, and enforce authentication policies.
- Data Integrity: Ensure user data (email, phone, DOB) is accurate and up to date.
- Account Lifecycle Management: Handle onboarding, profile updates, role changes, and account deletion.
- Operational Efficiency: Quickly manage large user bases without directly interacting with the database.
Example:
- If an employee leaves the company, the admin can deactivate their account immediately.
- If a customer requests an email update, admins can change it in the system while maintaining data integrity.
User Management using ASP.NET Core MVC and Identity:
Now, we will implement the following pages:
Index Page:
This Page:
- Shows a paginated, filterable grid of users with key fields (name, email, phone) and quick actions. It follows the same list/pager pattern used elsewhere for consistency and scale.
- Uses a standard pager (First/Prev/Next/Last) driven by page metadata, so navigation is lightweight and predictable.
- Acts as the operational “home” for user administration before drilling down to create, edit, details, delete, or role assignment.
Create Page:
This Page:
- Collects profile and account inputs (name, email as username, phone, DOB, flags) plus password/confirm with validation and helper text for a smooth first-time setup.
- Surfaces friendly, model-level errors (e.g., duplicate/invalid) and per-field messages to guide admins.
- On success, reports a clear success message; otherwise, shows identity/DB errors without losing form state.
Edit Page:
This Page:
- Updates existing profile/account fields with an optimistic concurrency token so conflicting edits are caught and explained.
- Mirrors create-form ergonomics: iconified inputs, validation summary, and clear “save/cancel” actions.
- Emits precise feedback: success on save, specific concurrency guidance, or DB/unknown error messages.
Details Page:
This Page:
- Presents a read-optimized snapshot of the account (identity/profile, timestamps, activity flags) along with the list of assigned roles for review.
- Loads safely and, if lookup fails, guides the admin back with a friendly message instead of an error screen.
Delete Page:
This Page:
- Provides a safety-first confirmation step before irreversible removal, summarizing the target account and blocking risky cases (e.g., last Admin).
- Returns clear outcomes: “deleted” on success or a helpful explanation (not found / DB error / protected case).
Manage Roles Page:
This Page:
- Shows the role catalogue as selectable items (name + description) for the current user, reflecting a complete role state in one place.
- Optimizes role updates by computing add/remove diffs server-side, then confirming success or surfacing validation errors cleanly.
- Fits the overall admin flow as one of the primary destinations from details/list, per the documented page set.
Creating View Models:
First, create a folder named Users within the ViewModels folder, where we will create all our View Models related to Admin User management.
UserListFilterViewModel
Create a class file named UserListFilterViewModel.cs within the ViewModels\Users folder, then copy and paste the following code. It holds search text, status flags, and paging knobs that define which users and how many to fetch per page.
using System.ComponentModel.DataAnnotations; namespace ASPNETCoreIdentityDemo.ViewModels.Users { public class UserListFilterViewModel { [Display(Name = "Search")] [StringLength(100, ErrorMessage = "Search text cannot exceed 100 characters.")] public string? Search { get; set; } // Search by Email/UserName/First/Last/Phone [Display(Name = "Active Status")] public bool? IsActive { get; set; } // All/Active/Inactive [Display(Name = "Email Confirmation")] public bool? EmailConfirmed { get; set; } // All/Confirmed/Unconfirmed [Display(Name = "Page Number")] [Range(1, int.MaxValue, ErrorMessage = "Page number must be 1 or greater.")] public int PageNumber { get; set; } = 1; // Default page number [Display(Name = "Page Size")] [Range(1, 100, ErrorMessage = "Page size must be between 1 and 100.")] public int PageSize { get; set; } = 5; // Default page size } }
UserListItemViewModel
Create a class file named UserListItemViewModel.cs within the ViewModels\Users folder, then copy and paste the following code. It is a lightweight row shape for lists: identity, contact, flags (active / email confirmed), and created date.
namespace ASPNETCoreIdentityDemo.ViewModels.Users { public class UserListItemViewModel { public Guid Id { get; init; } public string Email { get; init; } = string.Empty; public string UserName { get; init; } = string.Empty; public string? FirstName { get; init; } public string? LastName { get; init; } public string? PhoneNumber { get; init; } public bool IsActive { get; init; } public bool EmailConfirmed { get; init; } public DateTime? CreatedOn { get; init; } } }
UserCreateViewModel
Create a class file named UserCreateViewModel.cs within the ViewModels\Users folder, then copy and paste the following code. It captures all inputs necessary to create a user safely (profile fields, password/confirm, policy validations, and optional email confirmation).
using Microsoft.AspNetCore.Mvc; using System.ComponentModel.DataAnnotations; namespace ASPNETCoreIdentityDemo.ViewModels.Users { public class UserCreateViewModel : IValidatableObject { [Required(ErrorMessage = "First name is required.")] [StringLength(50, ErrorMessage = "First name cannot exceed 50 characters.")] [Display(Name = "First Name")] public string FirstName { get; set; } = null!; [StringLength(50, ErrorMessage = "Last name cannot exceed 50 characters.")] [Display(Name = "Last Name")] public string? LastName { get; set; } [Required(ErrorMessage = "Email is required.")] [EmailAddress(ErrorMessage = "Enter a valid email address.")] [Remote(action: "IsEmailAvailable", controller: "RemoteValidation", ErrorMessage = "This email is already registered.")] [Display(Name = "Email")] public string Email { get; set; } = null!; [Phone(ErrorMessage = "Enter a valid phone number.")] [Display(Name = "Phone Number")] public string? PhoneNumber { get; set; } [DataType(DataType.Date)] [Display(Name = "Date of Birth")] public DateTime? DateOfBirth { get; set; } [Display(Name = "Active?")] public bool IsActive { get; set; } = true; [Display(Name = "Mark Email Confirmed?")] public bool MarkEmailConfirmed { get; set; } = true; [Required(ErrorMessage = "Password is required.")] [StringLength(100, MinimumLength = 8, ErrorMessage = "Password must be at least {2} characters long.")] [DataType(DataType.Password)] [Display(Name = "Password")] public string Password { get; set; } = null!; [Required(ErrorMessage = "Please confirm the password.")] [DataType(DataType.Password)] [Compare(nameof(Password), ErrorMessage = "Passwords do not match.")] [Display(Name = "Confirm Password")] public string ConfirmPassword { get; set; } = null!; // Model-level validations that are easier to express in code than attributes public IEnumerable<ValidationResult> Validate(ValidationContext validationContext) { if (DateOfBirth.HasValue && DateOfBirth.Value.Date > DateTime.Today) { yield return new ValidationResult( "Date of birth cannot be in the future.", new[] { nameof(DateOfBirth) }); } } } }
UserEditViewModel
Create a class file named UserEditViewModel.cs within the ViewModels\Users folder, then copy and paste the following code. It carries editable profile fields and a concurrency token so updates can be validated and race-safe.
using Microsoft.AspNetCore.Mvc; using System.ComponentModel.DataAnnotations; namespace ASPNETCoreIdentityDemo.ViewModels.Users { public class UserEditViewModel : IValidatableObject { [Required(ErrorMessage = "Invalid user.")] public Guid Id { get; set; } [Required(ErrorMessage = "First name is required.")] [StringLength(50, ErrorMessage = "First name cannot exceed 50 characters.")] [Display(Name = "First Name")] public string FirstName { get; set; } = null!; [StringLength(50, ErrorMessage = "Last name cannot exceed 50 characters.")] [Display(Name = "Last Name")] public string? LastName { get; set; } [Required(ErrorMessage = "Email is required.")] [EmailAddress(ErrorMessage = "Enter a valid email address.")] [Display(Name = "Email")] public string Email { get; set; } = null!; [Phone(ErrorMessage = "Enter a valid phone number.")] [Display(Name = "Phone Number")] public string? PhoneNumber { get; set; } [DataType(DataType.Date)] [Display(Name = "Date of Birth")] public DateTime? DateOfBirth { get; set; } [Display(Name = "Active?")] public bool IsActive { get; set; } [Display(Name = "Email Confirmed?")] public bool EmailConfirmed { get; set; } // Keep this posted via hidden input; adding [HiddenInput] is optional since your view already posts it. [Required(ErrorMessage = "Concurrency token is missing. Please reload and try again.")] [HiddenInput(DisplayValue = false)] public string? ConcurrencyStamp { get; set; } public IEnumerable<ValidationResult> Validate(ValidationContext validationContext) { if (DateOfBirth.HasValue && DateOfBirth.Value.Date > DateTime.Today) { yield return new ValidationResult( "Date of birth cannot be in the future.", new[] { nameof(DateOfBirth) }); } } } }
UserDetailsViewModel
Create a class file named UserDetailsViewModel.cs within the ViewModels\Users folder, then copy and paste the following code. It presents a read-optimized, complete snapshot of a user (identity, profile, activity timestamps, flags, and role names).
namespace ASPNETCoreIdentityDemo.ViewModels.Users { public class UserDetailsViewModel { public Guid Id { get; init; } public string Email { get; init; } = string.Empty; public string UserName { get; init; } = string.Empty; public string FirstName { get; init; } = string.Empty; public string? LastName { get; init; } public string? PhoneNumber { get; init; } public DateTime? DateOfBirth { get; init; } public DateTime? LastLogin { get; init; } public bool IsActive { get; init; } public bool EmailConfirmed { get; init; } public DateTime? CreatedOn { get; init; } public DateTime? ModifiedOn { get; init; } public List<string> Roles { get; init; } = new(); } }
RoleCheckboxItem
Create a class file named RoleCheckboxItem.cs within the ViewModels\Users folder, then copy and paste the following code. It describes one role in a checkbox list (id, name, description, selected state) for role assignment scenarios.
using System.ComponentModel.DataAnnotations; namespace ASPNETCoreIdentityDemo.ViewModels.Users { public class RoleCheckboxItem { [Required(ErrorMessage = "Role Id is required.")] public Guid RoleId { get; set; } [Required(ErrorMessage = "Role name is required.")] public string RoleName { get; set; } = string.Empty; public string? Description { get; set; } public bool IsSelected { get; set; } } }
UserRolesEditViewModel
Create a class file named UserRolesEditViewModel.cs within the ViewModels\Users folder, then copy and paste the following code. It bundles a user identifier/name with a collection of RoleCheckboxItem entries to express the target role set for that user.
using System.ComponentModel.DataAnnotations; namespace ASPNETCoreIdentityDemo.ViewModels.Users { public class UserRolesEditViewModel { [Required(ErrorMessage = "Invalid user.")] public Guid UserId { get; set; } [Display(Name = "User Name")] public string UserName { get; set; } = string.Empty; // Roles collection can be empty (no roles selected), so no Required here. public List<RoleCheckboxItem> Roles { get; set; } = new(); } }
User Service Interface
Create an interface named IUserService.cs within the Services folder, then copy and paste the following code. It defines the contract for user lifecycle operations: paging/filtering, create/update/delete, details retrieval, and role assignment updates.
using ASPNETCoreIdentityDemo.ViewModels; using ASPNETCoreIdentityDemo.ViewModels.Users; using Microsoft.AspNetCore.Identity; namespace ASPNETCoreIdentityDemo.Services { public interface IUserService { Task<PagedResult<UserListItemViewModel>> GetUsersAsync(UserListFilterViewModel filter); Task<(IdentityResult Result, Guid? UserId)> CreateAsync(UserCreateViewModel model); Task<UserEditViewModel?> GetForEditAsync(Guid id); Task<IdentityResult> UpdateAsync(UserEditViewModel model); Task<UserDetailsViewModel?> GetDetailsAsync(Guid id); Task<IdentityResult> DeleteAsync(Guid id); Task<UserRolesEditViewModel?> GetRolesForEditAsync(Guid userId); Task<IdentityResult> UpdateRolesAsync(Guid userId, IEnumerable<Guid> selectedRoleIds); } }
User Service Implementation
Create an interface named UserService.cs within the Services folder, then copy and paste the following code. It implements the contract with Identity + EF Core, including create/update/delete transactions, search, paging, and concurrency checks.
using ASPNETCoreIdentityDemo.Data; using ASPNETCoreIdentityDemo.Models; using ASPNETCoreIdentityDemo.ViewModels; using ASPNETCoreIdentityDemo.ViewModels.Users; using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; namespace ASPNETCoreIdentityDemo.Services { public class UserService : IUserService { private const int MaxPageSize = 100; private readonly UserManager<ApplicationUser> _userManager; private readonly RoleManager<ApplicationRole> _roleManager; private readonly ApplicationDbContext _db; public UserService( UserManager<ApplicationUser> userManager, RoleManager<ApplicationRole> roleManager, ApplicationDbContext db) { _userManager = userManager; _roleManager = roleManager; _db = db; } // Returns a paged list of users with filter/search. // Uses normalized columns (index-friendly) where possible for best performance. public async Task<PagedResult<UserListItemViewModel>> GetUsersAsync(UserListFilterViewModel filter) { // Normalize and clamp paging inputs to safe values var pageNumber = filter.PageNumber < 1 ? 1 : filter.PageNumber; var pageSize = filter.PageSize < 1 ? 10 : (filter.PageSize > MaxPageSize ? MaxPageSize : filter.PageSize); // Base query (read-only fast path) var query = _userManager.Users.AsNoTracking(); // Search heuristic: // - If it looks like an email, use NormalizedEmail (indexed). // - If it's numeric, filter by PhoneNumber prefix (common usage). // - Else, use NormalizedUserName (indexed) + First/Last name prefix. if (!string.IsNullOrWhiteSpace(filter.Search)) { var s = filter.Search.Trim(); var sUpper = s.ToUpperInvariant(); if (s.Contains('@')) { query = query.Where(u => u.NormalizedEmail != null && u.NormalizedEmail.StartsWith(sUpper)); } else if (s.All(char.IsDigit)) { query = query.Where(u => (u.PhoneNumber ?? "").StartsWith(s)); } else { query = query.Where(u => (u.NormalizedUserName != null && u.NormalizedUserName.StartsWith(sUpper)) || (u.FirstName ?? "").StartsWith(s) || (u.LastName ?? "").StartsWith(s)); } } if (filter.IsActive.HasValue) query = query.Where(u => u.IsActive == filter.IsActive.Value); if (filter.EmailConfirmed.HasValue) query = query.Where(u => u.EmailConfirmed == filter.EmailConfirmed.Value); // Total count for pager (single scalar query) var total = await query.CountAsync(); var items = await query // Current sort: friendly alphabetical. // For raw performance, you can swap to .OrderByDescending(u => u.CreatedOn).ThenBy(u => u.Id) .OrderBy(u => u.FirstName).ThenBy(u => u.LastName).ThenBy(u => u.Email).ThenBy(u => u.Id) .Skip((pageNumber - 1) * pageSize) .Take(pageSize) // Project only what you need .Select(u => new UserListItemViewModel { Id = u.Id, Email = u.Email!, UserName = u.UserName!, FirstName = u.FirstName, LastName = u.LastName, PhoneNumber = u.PhoneNumber, IsActive = u.IsActive, EmailConfirmed = u.EmailConfirmed, CreatedOn = u.CreatedOn }) .ToListAsync(); return new PagedResult<UserListItemViewModel> { Items = items, TotalCount = total, PageIndex = pageNumber, PageSize = pageSize }; } // Creates a new user with password. // We rely on Identity's built-in uniqueness/validation (avoid extra pre-check round trip). public async Task<(IdentityResult Result, Guid? UserId)> CreateAsync(UserCreateViewModel model) { // ExecutionStrategy adds resiliency (automatic retries for transient SQL errors) var strategy = _db.Database.CreateExecutionStrategy(); return await strategy.ExecuteAsync<(IdentityResult, Guid?)>(async () => { // Start an explicit transaction await using var tx = await _db.Database.BeginTransactionAsync(); try { // Prepare a new ApplicationUser (keep UserName = Email for simplicity/consistency) var user = new ApplicationUser { Id = Guid.NewGuid(), FirstName = model.FirstName.Trim(), LastName = model.LastName?.Trim(), Email = model.Email.Trim(), UserName = model.Email.Trim(), PhoneNumber = model.PhoneNumber, DateOfBirth = model.DateOfBirth, IsActive = model.IsActive, EmailConfirmed = model.MarkEmailConfirmed, CreatedOn = DateTime.UtcNow, ModifiedOn = DateTime.UtcNow }; // Let Identity enforce password policy + unique constraints (inside the transaction) var create = await _userManager.CreateAsync(user, model.Password); if (!create.Succeeded) { await tx.RollbackAsync(); return (create, null); } await tx.CommitAsync(); return (IdentityResult.Success, user.Id); } catch { await tx.RollbackAsync(); throw; // let middleware/logging handle it; caller gets a 500 } }); } // Loads user data for the Edit form (read-only). public async Task<UserEditViewModel?> GetForEditAsync(Guid id) { // AsNoTracking -> we don't need change tracking for display var u = await _userManager.Users.AsNoTracking().FirstOrDefaultAsync(x => x.Id == id); if (u == null) return null; return new UserEditViewModel { Id = u.Id, FirstName = u.FirstName, LastName = u.LastName, Email = u.Email!, PhoneNumber = u.PhoneNumber, DateOfBirth = u.DateOfBirth, IsActive = u.IsActive, EmailConfirmed = u.EmailConfirmed, ConcurrencyStamp = u.ConcurrencyStamp // used for optimistic concurrency in Update }; } // Updates a user with optimistic concurrency check via ConcurrencyStamp. public async Task<IdentityResult> UpdateAsync(UserEditViewModel model) { // ExecutionStrategy adds resiliency (automatic retries for transient SQL errors) var strategy = _db.Database.CreateExecutionStrategy(); return await strategy.ExecuteAsync<IdentityResult>(async () => { await using var tx = await _db.Database.BeginTransactionAsync(); try { var user = await _userManager.FindByIdAsync(model.Id.ToString()); if (user == null) { await tx.RollbackAsync(); return IdentityResult.Failed(new IdentityError { Code = "NotFound", Description = "User not found." }); } // Optimistic concurrency guard: // If stamp changed, someone else updated the record if (!string.Equals(user.ConcurrencyStamp, model.ConcurrencyStamp, StringComparison.Ordinal)) { await tx.RollbackAsync(); return IdentityResult.Failed(new IdentityError { Code = "ConcurrencyFailure", Description = "This user was modified by another admin. Please reload and try again." }); } // If email changed, update both Email & UserName (Identity will SaveChanges inside the transaction) if (!string.Equals(user.Email, model.Email, StringComparison.OrdinalIgnoreCase)) { var emailResult = await _userManager.SetEmailAsync(user, model.Email.Trim()); if (!emailResult.Succeeded) { await tx.RollbackAsync(); return emailResult; } var usernameResult = await _userManager.SetUserNameAsync(user, model.Email.Trim()); if (!usernameResult.Succeeded) { await tx.RollbackAsync(); return usernameResult; } } // Update profile fields user.FirstName = model.FirstName.Trim(); user.LastName = model.LastName?.Trim(); user.PhoneNumber = model.PhoneNumber; user.DateOfBirth = model.DateOfBirth; user.IsActive = model.IsActive; user.EmailConfirmed = model.EmailConfirmed; user.ModifiedOn = DateTime.UtcNow; var update = await _userManager.UpdateAsync(user); if (!update.Succeeded) { await tx.RollbackAsync(); return update; } await tx.CommitAsync(); return IdentityResult.Success; } catch { await tx.RollbackAsync(); throw; } }); } // Returns detailed view model including assigned roles. public async Task<UserDetailsViewModel?> GetDetailsAsync(Guid id) { // Read-only entity for display var user = await _userManager.Users.AsNoTracking().FirstOrDefaultAsync(x => x.Id == id); if (user == null) return null; // Identity API requires the user entity for role lookup var roles = await _userManager.GetRolesAsync(user); return new UserDetailsViewModel { Id = user.Id, Email = user.Email!, UserName = user.UserName!, FirstName = user.FirstName, LastName = user.LastName, PhoneNumber = user.PhoneNumber, DateOfBirth = user.DateOfBirth, LastLogin = user.LastLogin, IsActive = user.IsActive, EmailConfirmed = user.EmailConfirmed, CreatedOn = user.CreatedOn, ModifiedOn = user.ModifiedOn, Roles = roles.OrderBy(r => r).ToList() }; } // Deletes a user with a guard to prevent removing the last Admin. public async Task<IdentityResult> DeleteAsync(Guid id) { var strategy = _db.Database.CreateExecutionStrategy(); return await strategy.ExecuteAsync<IdentityResult>(async () => { await using var tx = await _db.Database.BeginTransactionAsync(); try { var user = await _userManager.FindByIdAsync(id.ToString()); if (user == null) { await tx.RollbackAsync(); return IdentityResult.Failed(new IdentityError { Code = "NotFound", Description = "User not found." }); } // Safety: block deleting the last "Admin" var adminRole = await _roleManager.FindByNameAsync("Admin"); if (adminRole != null) { var isAdmin = await _userManager.IsInRoleAsync(user, "Admin"); if (isAdmin) { var anotherAdminExists = await _db.Set<IdentityUserRole<Guid>>() .AnyAsync(ur => ur.RoleId == adminRole.Id && ur.UserId != user.Id); if (!anotherAdminExists) { await tx.RollbackAsync(); return IdentityResult.Failed(new IdentityError { Code = "LastAdmin", Description = "You cannot delete the last user in the 'Admin' role." }); } } } var delete = await _userManager.DeleteAsync(user); if (!delete.Succeeded) { await tx.RollbackAsync(); return delete; } await tx.CommitAsync(); return IdentityResult.Success; } catch { await tx.RollbackAsync(); throw; } }); } // Builds the roles editor (checkbox list) with pre-checked assignments. public async Task<UserRolesEditViewModel?> GetRolesForEditAsync(Guid userId) { var user = await _userManager.FindByIdAsync(userId.ToString()); if (user == null) return null; // List all roles (read-only) var allRoles = await _roleManager.Roles .AsNoTracking() .OrderBy(r => r.Name) .ToListAsync(); // Current assignments for the user var assigned = await _userManager.GetRolesAsync(user); // Case-insensitive check to avoid surprises with different normalizations var userRolesEditViewModel = new UserRolesEditViewModel { UserId = user.Id, UserName = user.UserName!, Roles = allRoles.Select(role => new RoleCheckboxItem { RoleId = role.Id, RoleName = role.Name!, Description = role.Description, IsSelected = assigned.Contains(role.Name!, StringComparer.OrdinalIgnoreCase) }).ToList() }; return userRolesEditViewModel; } // Updates a user's roles using batched operations public async Task<IdentityResult> UpdateRolesAsync(Guid userId, IEnumerable<Guid> selectedRoleIds) { var strategy = _db.Database.CreateExecutionStrategy(); return await strategy.ExecuteAsync<IdentityResult>(async () => { await using var tx = await _db.Database.BeginTransactionAsync(); try { var user = await _userManager.FindByIdAsync(userId.ToString()); if (user == null) { await tx.RollbackAsync(); return IdentityResult.Failed(new IdentityError { Code = "NotFound", Description = "User not found." }); } // Normalize and de-duplicate incoming IDs var ids = (selectedRoleIds ?? Enumerable.Empty<Guid>()).Distinct().ToArray(); // Map ONLY requested IDs -> names (read-only) var selectedRoleNames = (ids.Length == 0) ? Array.Empty<string>() : await _roleManager.Roles .AsNoTracking() .Where(r => ids.Contains(r.Id)) .Select(r => r.Name!) .ToArrayAsync(); // Validate existence if (selectedRoleNames.Length != ids.Length) { await tx.RollbackAsync(); return IdentityResult.Failed(new IdentityError { Code = "RoleNotFound", Description = "One or more selected roles do not exist." }); } // Current roles var currentRoles = await _userManager.GetRolesAsync(user); // Compute diffs (case-insensitive) var current = new HashSet<string>(currentRoles, StringComparer.OrdinalIgnoreCase); var target = new HashSet<string>(selectedRoleNames, StringComparer.OrdinalIgnoreCase); var toAdd = target.Except(current, StringComparer.OrdinalIgnoreCase).ToArray(); var toRemove = current.Except(target, StringComparer.OrdinalIgnoreCase).ToArray(); if (toAdd.Length == 0 && toRemove.Length == 0) { await tx.CommitAsync(); // nothing to do return IdentityResult.Success; } // Batch add/remove to minimize round-trips; both inside the same transaction if (toAdd.Length > 0) { var add = await _userManager.AddToRolesAsync(user, toAdd); if (!add.Succeeded) { await tx.RollbackAsync(); return add; } } if (toRemove.Length > 0) { var rem = await _userManager.RemoveFromRolesAsync(user, toRemove); if (!rem.Succeeded) { await tx.RollbackAsync(); return rem; } } await tx.CommitAsync(); return IdentityResult.Success; } catch { await tx.RollbackAsync(); throw; } }); } } }
What are Transient SQL Errors?
Transient SQL errors are short-lived, temporary failures when talking to your database that usually resolve themselves if you retry the operation after a short delay. They’re not bugs in your code or permanent issues with the database. The following are some of the Common causes:
- Network Blips: A brief drop in connection between your app server and the SQL Server.
- Deadlocks: Two transactions block each other, SQL Server picks one to roll back.
- Resource Throttling: In Azure SQL or other cloud DBs, the service may throttle queries if you hit resource limits.
- Failovers: The database is failing over to a secondary node in a high-availability setup.
- Lock Timeouts: A transaction waited too long for a lock and got canceled.
Typical Error Codes (SQL Server)
Some well-known transient error numbers:
- 4060 – Cannot open the database requested by the login
- 10928 / 10929 – Resource limits reached (Azure SQL)
- 40501 – Throttled by Azure SQL Database
- 40197 – The service encountered an error and is not available (retry)
- 40613 – Database unavailable
- 49918 / 49919 / 49920 – Resource limits or service busy
- 1205 – Deadlock victim
Why EF Core Cares?
Entity Framework Core for SQL Server has a built-in ExecutionStrategy (sometimes called SqlServerRetryingExecutionStrategy). If enabled, it will catch transient errors and retry the operation a few times with exponential backoff before failing.
Here, if a transient error (like a brief connection loss) happens anywhere in that block, EF Core will retry the entire block.
Register the User Service in DI:
Please add the following statement to register the Role service into the dependency Injection container within the Program class.
builder.Services.AddScoped<IUserService, UserService>();
Creating Users Controller:
Create an empty MVC Controller named UsersController within the Controllers folder and then copy and paste the following code. It manages user workflows end-to-end, mapping service results to friendly success/error messages and returning the right models to the views.
using ASPNETCoreIdentityDemo.Services; using ASPNETCoreIdentityDemo.ViewModels; using ASPNETCoreIdentityDemo.ViewModels.Users; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc; using Microsoft.EntityFrameworkCore; using System.Security.Claims; namespace ASPNETCoreIdentityDemo.Controllers { [Authorize] public class UsersController : Controller { private readonly IUserService _userService; private readonly ILogger<UsersController> _logger; public UsersController(IUserService userService, ILogger<UsersController> logger) { _userService = userService; _logger = logger; } // GET: /Users [HttpGet] public async Task<IActionResult> Index([FromQuery] UserListFilterViewModel filter) { // List page is read-only; exceptions here are unlikely, but log & show friendly error. try { var result = await _userService.GetUsersAsync(filter); ViewBag.Filter = filter; return View(result); } catch (Exception ex) { _logger.LogError(ex, "Error loading users list."); SetError("We couldn’t load the users right now. Please try again."); return View(new PagedResult<UserListItemViewModel>()); // empty model to avoid null view } } // GET: /Users/Create [HttpGet] public IActionResult Create() { return View(new UserCreateViewModel()); } // POST: /Users/Create [HttpPost] [ValidateAntiForgeryToken] public async Task<IActionResult> Create(UserCreateViewModel model) { try { if (!ModelState.IsValid) return View(model); var (result, newId) = await _userService.CreateAsync(model); if (result.Succeeded) { SetSuccess($"User '{model.Email}' was created successfully."); return RedirectToAction(nameof(Index)); } AddIdentityErrors(result); return View(model); } catch (DbUpdateException dbx) { // Most common: unique index conflicts or other DB issues _logger.LogError(dbx, $"DB error while creating user {model.Email}"); SetError("We couldn’t create the user due to a database error. Please try again."); return View(model); } catch (Exception ex) { _logger.LogError(ex, $"Unexpected error creating user {model.Email}"); SetError("An unexpected error occurred while creating the user."); return View(model); } } // GET: /Users/Edit/{id} [HttpGet] public async Task<IActionResult> Edit(Guid id) { try { var userEditViewModel = await _userService.GetForEditAsync(id); if (userEditViewModel == null) { SetError("The user you’re trying to edit was not found."); return RedirectToAction(nameof(Index)); } return View(userEditViewModel); } catch (Exception ex) { _logger.LogError(ex, $"Error loading edit form for user {id}"); SetError("We couldn’t load the edit form. Please try again."); return RedirectToAction(nameof(Index)); } } // POST: /Users/Edit [HttpPost] [ValidateAntiForgeryToken] public async Task<IActionResult> Edit(UserEditViewModel model) { try { if (!ModelState.IsValid) return View(model); var result = await _userService.UpdateAsync(model); if (result.Succeeded) { SetSuccess("User was updated successfully."); return RedirectToAction(nameof(Index)); } // Detect optimistic concurrency from service error code and show friendlier message if (result.Errors.Any(e => string.Equals(e.Code, "ConcurrencyFailure", StringComparison.OrdinalIgnoreCase))) { SetError("This user was modified by another admin. Please reload the page and try again."); } AddIdentityErrors(result); return View(model); } catch (DbUpdateConcurrencyException cex) { _logger.LogWarning(cex, $"Concurrency error updating user {model.Id}"); SetError("Your changes could not be saved because another update occurred. Please reload and try again."); return View(model); } catch (DbUpdateException dbx) { _logger.LogError(dbx, $"DB error while updating user {model.Id}"); SetError("We couldn’t update the user due to a database error. Please try again."); return View(model); } catch (Exception ex) { _logger.LogError(ex, $"Unexpected error updating user {model.Id}"); SetError("An unexpected error occurred while updating the user."); return View(model); } } // GET: /Users/Details/{id} [HttpGet] public async Task<IActionResult> Details(Guid id) { try { var userDetailsViewModel = await _userService.GetDetailsAsync(id); if (userDetailsViewModel == null) { SetError("The requested user was not found."); return RedirectToAction(nameof(Index)); } return View(userDetailsViewModel); } catch (Exception ex) { _logger.LogError(ex, $"Error loading details for user {id}"); SetError("We couldn’t load the user details. Please try again."); return RedirectToAction(nameof(Index)); } } // GET: /Users/Delete/{id} [HttpGet] public async Task<IActionResult> Delete(Guid id) { try { var userDetailsViewModel = await _userService.GetDetailsAsync(id); if (userDetailsViewModel == null) { SetError("The user you’re trying to delete was not found."); return RedirectToAction(nameof(Index)); } return View(userDetailsViewModel); // confirm page } catch (Exception ex) { _logger.LogError(ex, $"Error loading delete confirmation for user {id}"); SetError("We couldn’t load the delete confirmation. Please try again."); return RedirectToAction(nameof(Index)); } } // POST: /Users/Delete/{id} [HttpPost] [ValidateAntiForgeryToken] public async Task<IActionResult> DeleteConfirmed(Guid id) { if (id == Guid.Empty) return NotFound(); try { var currentUserId = Guid.TryParse(User.FindFirstValue(ClaimTypes.NameIdentifier), out var g) ? g : Guid.Empty; var result = await _userService.DeleteAsync(id); if (result.Succeeded) { SetSuccess("User was deleted successfully."); return RedirectToAction(nameof(Index)); } // Last Admin cannot be deleted if (result.Errors.Any(e => string.Equals(e.Code, "LastAdmin", StringComparison.OrdinalIgnoreCase))) { SetError("You cannot delete the last user in the ‘Admin’ role."); } else if (result.Errors.Any(e => string.Equals(e.Code, "NotFound", StringComparison.OrdinalIgnoreCase))) { SetError("The user no longer exists."); } else { SetError(string.Join(" ", result.Errors.Select(e => e.Description))); } return RedirectToAction(nameof(Index)); } catch (DbUpdateException dbx) { _logger.LogError(dbx, $"DB error while deleting user {id}"); SetError("We couldn’t delete the user due to a database error. Please try again."); return RedirectToAction(nameof(Index)); } catch (Exception ex) { _logger.LogError(ex, $"Unexpected error deleting user {id}"); SetError("An unexpected error occurred while deleting the user."); return RedirectToAction(nameof(Index)); } } // GET: /Users/ManageRoles/{id} [HttpGet] public async Task<IActionResult> ManageRoles(Guid id) { try { var userRolesEditViewModel = await _userService.GetRolesForEditAsync(id); if (userRolesEditViewModel == null) { SetError("The user was not found."); return RedirectToAction(nameof(Index)); } return View(userRolesEditViewModel); } catch (Exception ex) { _logger.LogError(ex, $"Error loading roles editor for user {id}"); SetError("We couldn’t load the roles editor. Please try again."); return RedirectToAction(nameof(Index)); } } // POST: /Users/ManageRoles [HttpPost, ValidateAntiForgeryToken] public async Task<IActionResult> ManageRoles(UserRolesEditViewModel model) { if (model == null || model.UserId == Guid.Empty) return NotFound(); try { var selected = model.Roles.Where(r => r.IsSelected).Select(r => r.RoleId).ToList(); var result = await _userService.UpdateRolesAsync(model.UserId, selected); if (result.Succeeded) { SetSuccess("User roles were updated successfully."); return RedirectToAction(nameof(Details), new { id = model.UserId }); } // Surface specific role errors cleanly if (result.Errors.Any(e => string.Equals(e.Code, "RoleNotFound", StringComparison.OrdinalIgnoreCase))) { SetError("One or more selected roles no longer exist. Please refresh and try again."); } AddIdentityErrors(result); // Reload editor if failed (to re-populate checkbox list accurately) var userRolesEditViewModel = await _userService.GetRolesForEditAsync(model.UserId); return View(userRolesEditViewModel ?? model); } catch (DbUpdateException dbx) { _logger.LogError(dbx, $"DB error while updating roles for user {model.UserId}"); SetError("We couldn’t update roles due to a database error. Please try again."); var vm = await _userService.GetRolesForEditAsync(model.UserId); return View(vm ?? model); } catch (Exception ex) { _logger.LogError(ex, $"Unexpected error updating roles for user {model.UserId}"); SetError("An unexpected error occurred while updating roles."); var vm = await _userService.GetRolesForEditAsync(model.UserId); return View(vm ?? model); } } #region Helpers // Push a success message to TempData. private void SetSuccess(string message) { TempData["Success"] = message; } // Push an error message to TempData. private void SetError(string message) { TempData["Error"] = message; } // Adds IdentityResult errors into ModelState for field and model-level display. private void AddIdentityErrors(IdentityResult result) { if (result == null || result.Succeeded) return; foreach (var e in result.Errors) { ModelState.AddModelError(string.Empty, e.Description); } } #endregion } }
Creating Views:
Index View:
Create a view named Index.cshtml within the Views/Users folder, then copy and paste the following code. The Users list UI. Renders the filter bar (search, status, email confirmation, page size), the table with colored badges for Active/Inactive and Email status, and action buttons (Details, Edit, Manage Roles, Delete). Uses the shared pager partial for navigation
@using ASPNETCoreIdentityDemo.ViewModels @using ASPNETCoreIdentityDemo.ViewModels.Users @model PagedResult<UserListItemViewModel> @{ ViewData["Title"] = "Users"; var filter = (UserListFilterViewModel)ViewBag.Filter; var start = Model.TotalCount == 0 ? 0 : ((Model.PageIndex - 1) * Model.PageSize) + 1; var end = Math.Min(Model.PageIndex * Model.PageSize, Model.TotalCount); } <div class="container mt-1"> <div class="d-flex flex-wrap align-items-center justify-content-between gap-2 mb-4 pb-2 border-bottom"> <div> <h1 class="h3 mb-1 fw-bold text-primary">Users Administration</h1> <p class="text-muted mb-0">Search, filter, and manage application users.</p> </div> <div> <a asp-action="Create" class="btn btn-primary"> <i class="bi bi-plus-lg me-1"></i> Add New User </a> </div> </div> @if (TempData["Success"] is string sMsg) { <div class="alert alert-success alert-dismissible fade show" role="alert"> <i class="bi bi-check-circle me-2"></i>@sMsg <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button> </div> } @if (TempData["Error"] is string eMsg) { <div class="alert alert-danger alert-dismissible fade show" role="alert"> <i class="bi bi-exclamation-triangle me-2"></i>@eMsg <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button> </div> } <!-- Filter Bar --> <form method="get" class="row g-2 align-items-end mb-3"> <div class="col-md-4"> <label class="form-label">Search</label> <input name="Search" value="@filter.Search" class="form-control" placeholder="Search name, email, phone, username" /> </div> <div class="col-md-2"> <label class="form-label">Status</label> <select name="IsActive" class="form-select"> <option value="">All Status</option> <option value="true" selected="@(filter.IsActive == true)">Active</option> <option value="false" selected="@(filter.IsActive == false)">Inactive</option> </select> </div> <div class="col-md-2"> <label class="form-label">Email</label> <select name="EmailConfirmed" class="form-select"> <option value="">All Emails</option> <option value="true" selected="@(filter.EmailConfirmed == true)">Confirmed</option> <option value="false" selected="@(filter.EmailConfirmed == false)">Unconfirmed</option> </select> </div> <div class="col-md-2"> <label class="form-label">Page Size</label> <select name="PageSize" class="form-select"> @foreach (var size in new[] { 5, 10, 20, 50 }) { <option value="@size" selected="@(filter.PageSize == size)">@size / page</option> } </select> </div> <div class="col-md-2 d-grid d-sm-flex gap-2"> <button class="btn btn-primary" type="submit"> <i class="bi bi-funnel me-1"></i> Apply </button> <!-- Clear = go to Index without query string --> <a asp-action="Index" class="btn btn-info"> <i class="bi bi-x-circle me-1"></i> Clear </a> </div> </form> <div class="card shadow border-0 rounded"> <div class="card-body p-0"> <div class="table-responsive"> <table class="table align-middle mb-0"> <!-- Black/Dark header --> <thead class="table-dark"> <tr> <th>Name</th> <th>Email</th> <th>Phone</th> <th>Status</th> <th>Email Status</th> <th>Created</th> <th class="text-end">Actions</th> </tr> </thead> <tbody> @if (!Model.Items.Any()) { <tr> <td colspan="7" class="text-center text-muted py-4">No users found.</td> </tr> } else { foreach (var u in Model.Items) { <tr> <td>@($"{u.FirstName} {u.LastName}".Trim())</td> <td>@u.Email</td> <td>@u.PhoneNumber</td> <!-- Active / Inactive with colors --> <td> @if (u.IsActive) { <span class="badge rounded-pill bg-success">Active</span> } else { <span class="badge rounded-pill bg-secondary">Inactive</span> } </td> <!-- Email Confirmed / Unconfirmed with colors --> <td> @if (u.EmailConfirmed) { <span class="badge rounded-pill bg-success">Confirmed</span> } else { <span class="badge rounded-pill bg-warning text-dark">Unconfirmed</span> } </td> <td>@u.CreatedOn?.ToString("yyyy-MM-dd")</td> <td class="text-end"> <a asp-action="Details" asp-route-id="@u.Id" class="btn btn-sm btn-outline-info">Details</a> <a asp-action="Edit" asp-route-id="@u.Id" class="btn btn-sm btn-outline-primary">Edit</a> <a asp-action="ManageRoles" asp-route-id="@u.Id" class="btn btn-sm btn-outline-dark">Manage Roles</a> <a asp-action="Delete" asp-route-id="@u.Id" class="btn btn-sm btn-outline-danger">Delete</a> </td> </tr> } } </tbody> </table> </div> </div> </div> <partial name="~/Views/Shared/_Pager.cshtml" model="new PagedResult<object> { Items = Array.Empty<object>(), TotalCount = Model.TotalCount, PageIndex = Model.PageIndex, PageSize = Model.PageSize }" /> </div>
Create View:
Create a view named Create.cshtml within the Views/Users folder, then copy and paste the following code. Provides a validated form for adding a new user, including password and optional “mark email confirmed” behavior.
@model ASPNETCoreIdentityDemo.ViewModels.Users.UserCreateViewModel @{ ViewData["Title"] = "Create User"; } <div class="container mt-1"> <!-- Page header --> <div class="d-flex flex-wrap align-items-center justify-content-between gap-2 mb-3 pb-2 border-bottom"> <div> <h1 class="h4 fw-bold mb-1 text-primary">Create User</h1> <p class="text-muted small mb-0">Add a new user account, set credentials, and define initial status flags.</p> </div> <div class="d-flex gap-2"> <a asp-action="Index" class="btn btn-info"> <i class="bi bi-arrow-left me-1"></i> Back to Users </a> </div> </div> <div class="row g-4"> <!-- Form Section --> <div class="col-lg-7 col-xl-6"> <div class="card shadow border-0 rounded"> <div class="card-header bg-dark text-white"> <i class="bi bi-person-plus me-2"></i> New User Information </div> <div class="card-body"> <form asp-action="Create" method="post" novalidate> <!-- Validation --> @if (!ViewData.ModelState.IsValid && ViewData.ModelState.Values.Any(v => v.Errors.Count > 0)) { <div class="alert alert-danger alert-dismissible fade show" role="alert"> <div class="fw-semibold mb-1">We couldn’t create the user.</div> <div class="small text-muted mb-2">Please review the messages below and try again.</div> <div asp-validation-summary="All"></div> <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button> </div> } <div class="row g-3"> <!-- First Name --> <div class="col-md-6"> <label asp-for="FirstName" class="form-label"></label> <div class="input-group"> <span class="input-group-text"><i class="bi bi-person"></i></span> <input asp-for="FirstName" class="form-control" autocomplete="given-name" /> </div> <span asp-validation-for="FirstName" class="text-danger"></span> </div> <!-- Last Name --> <div class="col-md-6"> <label asp-for="LastName" class="form-label"></label> <div class="input-group"> <span class="input-group-text"><i class="bi bi-person"></i></span> <input asp-for="LastName" class="form-control" autocomplete="family-name" /> </div> <span asp-validation-for="LastName" class="text-danger"></span> </div> <!-- Email full width --> <div class="col-12"> <label asp-for="Email" class="form-label">Email (will be used as username)</label> <div class="input-group"> <span class="input-group-text"><i class="bi bi-envelope"></i></span> <input asp-for="Email" class="form-control" type="email" autocomplete="email" /> </div> <div class="form-text">We use the email address as the username during sign-in.</div> <span asp-validation-for="Email" class="text-danger"></span> </div> <!-- DOB + Phone in same row --> <div class="col-md-6"> <label asp-for="DateOfBirth" class="form-label"></label> <div class="input-group"> <span class="input-group-text"><i class="bi bi-calendar-date"></i></span> <input asp-for="DateOfBirth" class="form-control" type="date" /> </div> <span asp-validation-for="DateOfBirth" class="text-danger"></span> </div> <div class="col-md-6"> <label asp-for="PhoneNumber" class="form-label"></label> <div class="input-group"> <span class="input-group-text"><i class="bi bi-telephone"></i></span> <input asp-for="PhoneNumber" class="form-control" type="tel" autocomplete="tel" /> </div> <span asp-validation-for="PhoneNumber" class="text-danger"></span> </div> <!-- Password + Confirm Password same row --> <div class="col-md-6"> <label asp-for="Password" class="form-label"></label> <div class="input-group"> <span class="input-group-text"><i class="bi bi-shield-lock"></i></span> <input asp-for="Password" class="form-control" type="password" autocomplete="new-password" id="pwd" /> <button class="btn btn-outline-secondary" type="button" id="togglePwd"> <i class="bi bi-eye"></i> </button> </div> <span asp-validation-for="Password" class="text-danger"></span> </div> <div class="col-md-6"> <label asp-for="ConfirmPassword" class="form-label"></label> <div class="input-group"> <span class="input-group-text"><i class="bi bi-shield-lock"></i></span> <input asp-for="ConfirmPassword" class="form-control" type="password" autocomplete="new-password" id="cpwd" /> <button class="btn btn-outline-secondary" type="button" id="toggleCPwd"> <i class="bi bi-eye"></i> </button> </div> <span asp-validation-for="ConfirmPassword" class="text-danger"></span> </div> <!-- IsActive + EmailConfirmed same row --> <div class="col-md-6 d-flex align-items-center gap-4"> <div class="form-check form-switch"> <input asp-for="IsActive" class="form-check-input" /> <label asp-for="IsActive" class="form-check-label"></label> </div> <div class="form-check form-switch"> <input asp-for="MarkEmailConfirmed" class="form-check-input" /> <label asp-for="MarkEmailConfirmed" class="form-check-label">Email confirmed</label> </div> </div> </div> <!-- Actions --> <div class="d-flex justify-content-end gap-2 mt-4"> <button class="btn btn-primary"> <i class="bi bi-person-check me-1"></i> Create User </button> <a asp-action="Index" class="btn btn-info"> <i class="bi bi-x-circle me-1"></i> Cancel </a> </div> </form> </div> </div> </div> <!-- Guidelines Section --> <div class="col-lg-5 col-xl-6"> <div class="card border-0 shadow-sm"> <div class="card-header bg-dark text-white"> <i class="bi bi-lightbulb me-2"></i> Guidelines </div> <div class="card-body"> <ul class="list-unstyled small mb-0"> <li class="mb-2"><i class="bi bi-check-circle-fill text-success me-1"></i> First and last names should be properly capitalized.</li> <li class="mb-2"><i class="bi bi-check-circle-fill text-success me-1"></i> Email must be unique and valid (also used as username).</li> <li class="mb-2"><i class="bi bi-check-circle-fill text-success me-1"></i> Phone number should include country code if international.</li> <li class="mb-2"><i class="bi bi-check-circle-fill text-success me-1"></i> Inactive users will not be able to log in.</li> <li class="mb-2"><i class="bi bi-check-circle-fill text-success me-1"></i> Confirm email if verified out-of-band to enable notifications and password reset.</li> <li class="mb-2"><i class="bi bi-check-circle-fill text-success me-1"></i> Passwords should follow your policy (length, upper/lower, digit, symbol).</li> <li class="mb-2"><i class="bi bi-check-circle-fill text-success me-1"></i> Need to reset the user’s password later? Use the <strong>Reset Password</strong> flow or send a reset email.</li> <li><i class="bi bi-check-circle-fill text-success me-1"></i> Use <strong>Manage Roles</strong> after creating the user to assign permissions.</li> </ul> </div> </div> </div> </div> </div> @section Scripts { <partial name="_ValidationScriptsPartial" /> <script> $(function () { function wireToggle(btnSelector, inputSelector) { var $btn = $(btnSelector); var $input = $(inputSelector); if (!$btn.length || !$input.length) return; $btn.on('click', function (e) { e.preventDefault(); var isPassword = $input.attr('type') === 'password'; $input.attr('type', isPassword ? 'text' : 'password'); var $icon = $btn.find('i'); if ($icon.length) { $icon.toggleClass('bi-eye bi-eye-slash'); } }); } wireToggle('#togglePwd', '#pwd'); wireToggle('#toggleCPwd', '#cpwd'); }); </script> }
Edit View:
Create a view named Edit.cshtml within the Views/Users folder, then copy and paste the following code. The edit form for an existing user (no passwords). Mirrors the Create layout: name pair, full-width email, DOB+phone on one row, and the IsActive/EmailConfirmed switches on the same line, plus the hidden ConcurrencyStamp. Shows guidelines on the right.
@model ASPNETCoreIdentityDemo.ViewModels.Users.UserEditViewModel @{ ViewData["Title"] = "Edit User"; } <div class="container mt-1"> <!-- Page header --> <div class="d-flex flex-wrap align-items-center justify-content-between gap-2 mb-3 pb-2 border-bottom"> <div> <h1 class="h4 fw-bold mb-1 text-primary">Edit User</h1> <p class="text-muted small mb-0">Update the user’s profile and account settings.</p> </div> <div class="d-flex gap-2"> <a asp-action="Index" class="btn btn-info"> <i class="bi bi-arrow-left me-1"></i> Back to Users </a> </div> </div> <div class="row g-4"> <!-- Form Section --> <div class="col-lg-7 col-xl-6"> <div class="card shadow border-0 rounded"> <div class="card-header bg-dark text-white"> <i class="bi bi-person-gear me-2"></i> User Information </div> <div class="card-body"> <form asp-action="Edit" method="post" novalidate> <input type="hidden" asp-for="Id" /> <input type="hidden" asp-for="ConcurrencyStamp" /> <!-- Validation --> @if (!ViewData.ModelState.IsValid && ViewData.ModelState.Values.Any(v => v.Errors.Count > 0)) { <div class="alert alert-danger alert-dismissible fade show" role="alert"> <div class="fw-semibold mb-1">We couldn’t save your changes.</div> <div class="small text-muted mb-2">Please review the messages below and try again.</div> <div asp-validation-summary="All"></div> <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button> </div> } <div class="row g-3"> <!-- First & Last Name (same row) --> <div class="col-md-6"> <label asp-for="FirstName" class="form-label"></label> <div class="input-group"> <span class="input-group-text"><i class="bi bi-person"></i></span> <input asp-for="FirstName" class="form-control" autocomplete="given-name" /> </div> <span asp-validation-for="FirstName" class="text-danger"></span> </div> <div class="col-md-6"> <label asp-for="LastName" class="form-label"></label> <div class="input-group"> <span class="input-group-text"><i class="bi bi-person"></i></span> <input asp-for="LastName" class="form-control" autocomplete="family-name" /> </div> <span asp-validation-for="LastName" class="text-danger"></span> </div> <!-- Email (full width single row) --> <div class="col-12"> <label asp-for="Email" class="form-label"></label> <div class="input-group"> <span class="input-group-text"><i class="bi bi-envelope"></i></span> <input asp-for="Email" class="form-control" type="email" autocomplete="email" /> </div> <span asp-validation-for="Email" class="text-danger"></span> </div> <!-- Date of Birth & Phone (same row) --> <div class="col-md-6"> <label asp-for="DateOfBirth" class="form-label"></label> <div class="input-group"> <span class="input-group-text"><i class="bi bi-calendar-date"></i></span> <input asp-for="DateOfBirth" class="form-control" type="date" /> </div> <span asp-validation-for="DateOfBirth" class="text-danger"></span> </div> <div class="col-md-6"> <label asp-for="PhoneNumber" class="form-label"></label> <div class="input-group"> <span class="input-group-text"><i class="bi bi-telephone"></i></span> <input asp-for="PhoneNumber" class="form-control" type="tel" autocomplete="tel" /> </div> <span asp-validation-for="PhoneNumber" class="text-danger"></span> </div> <!-- IsActive & EmailConfirmed (same row) --> <div class="col-md-6 d-flex align-items-center gap-4"> <div class="form-check form-switch"> <input asp-for="IsActive" class="form-check-input" /> <label asp-for="IsActive" class="form-check-label"></label> </div> <div class="form-check form-switch"> <input asp-for="EmailConfirmed" class="form-check-input" /> <label asp-for="EmailConfirmed" class="form-check-label"></label> </div> </div> </div> <!-- Actions --> <div class="d-flex justify-content-end gap-2 mt-4"> <button class="btn btn-primary"> <i class="bi bi-save me-1"></i> Save Changes </button> <a asp-action="Index" class="btn btn-info"> <i class="bi bi-x-circle me-1"></i> Cancel </a> </div> </form> </div> </div> </div> <!-- Guidelines Section --> <div class="col-lg-5 col-xl-6"> <div class="card border-0 shadow-sm"> <div class="card-header bg-dark text-white"> <i class="bi bi-lightbulb me-2"></i> Guidelines </div> <div class="card-body"> <ul class="list-unstyled small mb-0"> <li class="mb-2"><i class="bi bi-check-circle-fill text-success me-1"></i> First and last names should be properly capitalized.</li> <li class="mb-2"><i class="bi bi-check-circle-fill text-success me-1"></i> Email must be unique and valid.</li> <li class="mb-2"><i class="bi bi-check-circle-fill text-success me-1"></i> Phone number should include country code if international.</li> <li class="mb-2"><i class="bi bi-check-circle-fill text-success me-1"></i> Inactive users will not be able to log in.</li> <li class="mb-2"><i class="bi bi-check-circle-fill text-success me-1"></i> Confirmed email is required for password reset and notifications.</li> <li class="mb-2"><i class="bi bi-check-circle-fill text-success me-1"></i> Use Manage Roles to adjust user permissions.</li> <li class="mb-2"><i class="bi bi-check-circle-fill text-success me-1"></i> Use Reset Password flow or send a password reset email to manage password.</li> </ul> </div> </div> </div> </div> </div> @section Scripts { <partial name="_ValidationScriptsPartial" /> }
Details View:
Create a view named Details.cshtml within the Views/Users folder, then copy and paste the following code. Shows a clean read-only profile summary with key metadata and the user’s role list.
@model ASPNETCoreIdentityDemo.ViewModels.Users.UserDetailsViewModel @{ ViewData["Title"] = "User Details"; string fullName = $"{Model.FirstName} {Model.LastName}".Trim(); string initials = (!string.IsNullOrWhiteSpace(Model.FirstName) ? Model.FirstName[0].ToString() : "") + (!string.IsNullOrWhiteSpace(Model.LastName) ? Model.LastName[0].ToString() : ""); string activeBadge = Model.IsActive ? "bg-success" : "bg-secondary"; string emailBadge = Model.EmailConfirmed ? "bg-success" : "bg-warning text-dark"; } <div class="container mt-1"> <!-- Page header --> <div class="d-flex flex-wrap align-items-center justify-content-between gap-2 mb-3 pb-2 border-bottom"> <div> <h2 class="h4 fw-bold text-primary mb-1"> User Profile: @(!string.IsNullOrWhiteSpace(fullName) ? fullName : Model.UserName) </h2> <p class="text-muted small mb-0">Review account details, health, and role assignments.</p> </div> <div class="d-flex gap-2"> <a asp-controller="Users" asp-action="ManageRoles" asp-route-id="@Model.Id" class="btn btn-primary"> <i class="bi bi-people me-1"></i> Manage Roles </a> <a asp-controller="Users" asp-action="Edit" asp-route-id="@Model.Id" class="btn btn-info"> <i class="bi bi-pencil-square me-1"></i> Edit User </a> <a asp-controller="Users" asp-action="Index" class="btn btn-dark"> <i class="bi bi-arrow-left me-1"></i> Back to Users </a> </div> </div> <!-- Alerts --> @if (TempData["Success"] is string sMsg) { <div class="alert alert-success alert-dismissible fade show" role="alert"> <i class="bi bi-check-circle me-2"></i>@sMsg <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button> </div> } @if (TempData["Error"] is string eMsg) { <div class="alert alert-danger alert-dismissible fade show" role="alert"> <i class="bi bi-exclamation-triangle me-2"></i>@eMsg <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button> </div> } <div class="row g-3"> <!-- Profile & Info --> <div class="col-lg-7"> <div class="card border-0 shadow"> <div class="card-header bg-dark text-white"> <div class="d-flex align-items-center justify-content-between"> <div class="d-flex align-items-center gap-3"> <div class="rounded-circle bg-secondary d-inline-flex align-items-center justify-content-center text-white fw-bold" style="width:52px;height:52px;"> @initials.ToUpper() </div> <div> <div class="h5 mb-0">@(!string.IsNullOrWhiteSpace(fullName) ? fullName : Model.UserName)</div> <small class="text-white-50">@Model.UserName</small> </div> </div> <div class="d-flex gap-2"> <span class="badge rounded-pill @activeBadge"> @(Model.IsActive ? "Active" : "Inactive") </span> <span class="badge rounded-pill @emailBadge"> @(Model.EmailConfirmed ? "Email Confirmed" : "Email Unconfirmed") </span> </div> </div> </div> <div class="card-body"> <!-- Contact section --> <div class="mb-2"> <div class="small text-uppercase text-muted mb-2">Contact</div> <ul class="list-group list-group-flush"> <li class="list-group-item px-0 d-flex justify-content-between align-items-center"> <span class="text-muted"> <i class="bi bi-envelope me-2"></i>Email </span> <span class="d-flex align-items-center gap-2"> <a href="mailto:@Model.Email" class="link-dark text-decoration-none">@Model.Email</a> <button type="button" class="btn btn-sm btn-outline-secondary" data-bs-toggle="tooltip" data-bs-title="Copy email" onclick="navigator.clipboard.writeText('@Model.Email')"> <i class="bi bi-clipboard"></i> </button> </span> </li> <li class="list-group-item px-0 d-flex justify-content-between align-items-center"> <span class="text-muted"> <i class="bi bi-telephone me-2"></i>Phone </span> <span>@(string.IsNullOrWhiteSpace(Model.PhoneNumber) ? "-" : Model.PhoneNumber)</span> </li> </ul> </div> <!-- Account section --> <div class="mt-4"> <div class="small text-uppercase text-muted mb-2">Account</div> <ul class="list-group list-group-flush"> <li class="list-group-item px-0 d-flex justify-content-between align-items-center"> <span class="text-muted"><i class="bi bi-cake2 me-2"></i>Date of Birth</span> <span>@(Model.DateOfBirth?.ToString("yyyy-MM-dd") ?? "-")</span> </li> <li class="list-group-item px-0 d-flex justify-content-between align-items-center"> <span class="text-muted"><i class="bi bi-box-arrow-in-right me-2"></i>Last Login</span> <span>@(Model.LastLogin?.ToString("yyyy-MM-dd HH:mm") ?? "Never")</span> </li> <li class="list-group-item px-0 d-flex justify-content-between align-items-center"> <span class="text-muted"><i class="bi bi-calendar2-plus me-2"></i>Created</span> <span>@(Model.CreatedOn?.ToString("yyyy-MM-dd HH:mm") ?? "-")</span> </li> <li class="list-group-item px-0 d-flex justify-content-between align-items-center"> <span class="text-muted"><i class="bi bi-calendar2-check me-2"></i>Modified</span> <span>@(Model.ModifiedOn?.ToString("yyyy-MM-dd HH:mm") ?? "-")</span> </li> </ul> </div> </div> </div> </div> <!-- Roles --> <div class="col-lg-5"> <div class="card border-0 shadow h-100"> <div class="card-header bg-dark text-white d-flex justify-content-between align-items-center"> <span class="fw-semibold">Roles & Permissions</span> <a asp-action="ManageRoles" asp-route-id="@Model.Id" class="btn btn-sm btn-primary"> <i class="bi bi-people me-1"></i> Manage </a> </div> <div class="card-body"> @if (Model.Roles == null || Model.Roles.Count == 0) { <div class="text-center text-muted py-3"> <i class="bi bi-shield-lock mb-2" style="font-size:1.4rem;"></i> <div>No roles assigned</div> <div class="mt-2"> <a asp-action="ManageRoles" asp-route-id="@Model.Id" class="btn btn-sm btn-outline-primary">Assign Roles</a> </div> </div> } else { <div class="d-flex flex-wrap gap-2"> @foreach (var role in Model.Roles.OrderBy(x => x)) { <span class="badge rounded-pill bg-primary"> <i class="bi bi-shield-check me-1"></i>@role </span> } </div> } </div> </div> </div> </div> </div> @section Scripts { <script> document.querySelectorAll('[data-bs-toggle="tooltip"]').forEach(el => new bootstrap.Tooltip(el)); </script> }
Delete View:
Create a view named Delete.cshtml within the Views/Users folder, then copy and paste the following code. A confirmation page for deleting a user. Summarizes key identity info and warns about irreversible deletion and the “last Admin” rule before posting to DeleteConfirmed.
@model ASPNETCoreIdentityDemo.ViewModels.Users.UserDetailsViewModel @{ ViewData["Title"] = "Delete User"; var fullName = $"{Model.FirstName} {Model.LastName}".Trim(); } <div class="container mt-0"> <div class="row g-4 justify-content-center"> <div class="col-lg-8 col-xl-7"> <div class="card shadow border-0 rounded"> <div class="card-header bg-danger text-white"> <i class="bi bi-exclamation-octagon me-2"></i> Confirm Deletion </div> @if (TempData["Error"] is string err) { <div class="alert alert-danger alert-dismissible fade show" role="alert"> <i class="bi bi-exclamation-triangle me-2"></i>@err <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button> </div> } @if (TempData["Success"] is string ok) { <div class="alert alert-success alert-dismissible fade show" role="alert"> <i class="bi bi-check-circle me-2"></i>@ok <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button> </div> } <div class="card-body"> <!-- Strong warning --> <div class="alert alert-danger" role="alert"> <div class="fw-semibold mb-1">Are you sure you want to delete this user?</div> <div class="small mb-0"> This action cannot be undone. If this user is the last <strong>Admin</strong>, deletion will be blocked. </div> </div> <!-- User summary --> <div class="mb-3"> <div class="small text-uppercase text-muted mb-2">User Summary</div> <ul class="list-group list-group-flush"> <li class="list-group-item d-flex justify-content-between align-items-center px-0"> <span class="text-muted"><i class="bi bi-person me-2"></i>Name</span> <span class="fw-semibold">@(!string.IsNullOrWhiteSpace(fullName) ? fullName : "-")</span> </li> <li class="list-group-item d-flex justify-content-between align-items-center px-0"> <span class="text-muted"><i class="bi bi-at me-2"></i>Username</span> <span class="fw-semibold">@Model.UserName</span> </li> <li class="list-group-item d-flex justify-content-between align-items-center px-0"> <span class="text-muted"><i class="bi bi-envelope me-2"></i>Email</span> <span class="fw-semibold">@Model.Email</span> </li> <li class="list-group-item d-flex justify-content-between align-items-center px-0"> <span class="text-muted"><i class="bi bi-activity me-2"></i>Status</span> <span> <span class="badge rounded-pill @(Model.IsActive ? "bg-success" : "bg-secondary")"> @(Model.IsActive ? "Active" : "Inactive") </span> <span class="badge rounded-pill @(Model.EmailConfirmed ? "bg-success" : "bg-warning text-dark") ms-1"> @(Model.EmailConfirmed ? "Email Confirmed" : "Email Unconfirmed") </span> </span> </li> <li class="list-group-item d-flex justify-content-between align-items-center px-0"> <span class="text-muted"><i class="bi bi-people me-2"></i>Roles</span> <span class="fw-semibold">@((Model.Roles?.Count ?? 0)) assigned</span> </li> </ul> @if (Model.Roles != null && Model.Roles.Count > 0) { <div class="mt-2 d-flex flex-wrap gap-2"> @foreach (var r in Model.Roles.OrderBy(x => x)) { <span class="badge rounded-pill bg-primary"> <i class="bi bi-shield-check me-1"></i>@r </span> } </div> } </div> <!-- Confirm form --> <form asp-action="DeleteConfirmed" method="post" class="mt-3"> <input type="hidden" name="id" value="@Model.Id" /> <div class="form-check mb-3"> <input class="form-check-input" type="checkbox" value="true" id="confirmChk"> <label class="form-check-label" for="confirmChk"> I understand this action is permanent and cannot be undone. </label> </div> <div class="d-flex justify-content-end gap-2"> <button type="submit" class="btn btn-danger" id="deleteBtn" disabled> <i class="bi bi-trash3 me-1"></i> Yes, Delete </button> <a asp-action="Index" class="btn btn-info"> <i class="bi bi-x-circle me-1"></i> Cancel </a> <a asp-action="Index" class="btn btn-primary"> <i class="bi bi-arrow-left me-1"></i> Back to Users </a> </div> </form> </div> <div class="card-footer bg-light small text-muted"> Deleted users cannot be recovered. Consider deactivating instead if you might need this account later. </div> </div> </div> </div> </div> @section Scripts { <script> // Enable the Delete button only when the confirmation is checked const chk = document.getElementById('confirmChk'); const btn = document.getElementById('deleteBtn'); if (chk && btn) { chk.addEventListener('change', () => btn.disabled = !chk.checked); } </script> }
ManageRoles View:
Create a view named ManageRoles.cshtml within the Views/Users folder, then copy and paste the following code. The role assignment UI for a single user. Shows a two-column list of roles, each with its name/description and a checkbox, plus header badges for total and selected counts, and convenient “Select all / Clear all” actions.
@model ASPNETCoreIdentityDemo.ViewModels.Users.UserRolesEditViewModel @{ ViewData["Title"] = "Manage User Roles"; } <div class="container mt-2"> <div class="mx-auto" style="max-width: 1100px;"> <!-- Page header --> <div class="d-flex flex-wrap align-items-center justify-content-between gap-2 mb-3 pb-2 border-bottom"> <div> <h1 class="h4 fw-bold text-primary mb-1"> Manage Roles for <span class="text-dark">@Model.UserName</span> </h1> <p class="text-muted small mb-0">Assign or remove roles. Changes apply immediately after saving.</p> </div> <div> <a asp-action="Details" asp-route-id="@Model.UserId" class="btn btn-dark"> <i class="bi bi-arrow-left me-1"></i> Back to Details </a> </div> </div> <div class="card shadow border-0 rounded"> <!-- Header with actions --> <div class="card-header bg-dark text-white d-flex align-items-center justify-content-between flex-wrap gap-2"> <div class="d-flex align-items-center gap-4"> <span class="fw-semibold fs-5"><i class="bi bi-shield-lock me-2"></i>Role Assignments</span> <div class="d-flex gap-3 align-items-center"> <span class="badge bg-info text-dark fs-6 px-3 py-2 shadow-sm rounded-pill"> <i class="bi bi-people-fill me-1"></i>Total: @Model.Roles.Count </span> <span class="badge bg-success fs-6 px-3 py-2 shadow-sm rounded-pill" id="selectedCount"> <i class="bi bi-check-circle-fill me-1"></i>Selected: @(Model.Roles.Count(r => r.IsSelected)) </span> </div> </div> <div class="d-flex gap-2"> <button type="button" class="btn btn-success btn-sm px-3 shadow-sm" id="selectAllBtn"> <i class="bi bi-check2-square me-1"></i>Select All </button> <button type="button" class="btn btn-warning btn-sm px-3 shadow-sm text-dark" id="clearAllBtn"> <i class="bi bi-x-circle me-1"></i>Clear All </button> </div> </div> <div class="card-body"> <form asp-action="ManageRoles" method="post" id="rolesForm"> <input type="hidden" asp-for="UserId" /> @if (!ViewData.ModelState.IsValid && ViewData.ModelState.Values.Any(v => v.Errors.Count > 0)) { <div class="alert alert-danger alert-dismissible fade show" role="alert"> <div class="fw-semibold mb-1">We couldn’t save your changes.</div> <div class="small text-muted mb-2">Please review the messages below and try again.</div> <div asp-validation-summary="All"></div> <button type="button" class="btn-close" data-bs-dismiss="alert" aria-label="Close"></button> </div> } <!-- Roles list in two columns --> <div class="row" id="rolesList"> @for (int i = 0; i < Model.Roles.Count; i++) { var roleId = $"role_{i}"; var name = Model.Roles[i].RoleName ?? ""; var desc = Model.Roles[i].Description ?? ""; <div class="col-md-6 col-lg-6 mb-3"> <label class="list-group-item rounded shadow-sm border"> <div class="d-flex align-items-start"> <input asp-for="Roles[i].IsSelected" class="form-check-input me-2 mt-1" id="@roleId" /> <div class="flex-grow-1"> <div class="fw-semibold">@name</div> <div class="text-muted small"> @(string.IsNullOrWhiteSpace(desc) ? "No description available" : desc) </div> </div> </div> <input asp-for="Roles[i].RoleId" type="hidden" /> <input asp-for="Roles[i].RoleName" type="hidden" /> </label> </div> } </div> <!-- Actions --> <div class="d-flex justify-content-end gap-2 mt-1"> <button type="submit" class="btn btn-primary px-4"> <i class="bi bi-save me-1"></i> Save Roles </button> <a asp-action="Details" asp-route-id="@Model.UserId" class="btn btn-info px-4"> Cancel </a> </div> </form> </div> <div class="card-footer bg-light small text-muted"> Tip: “Select all” and “Clear all” apply to every role shown on this page. </div> </div> </div> </div> @section Scripts { <partial name="_ValidationScriptsPartial" /> <script> (function ($) { const $list = $('#rolesList'); const $selectAllBtn = $('#selectAllBtn'); const $clearAllBtn = $('#clearAllBtn'); const $selectedCount = $('#selectedCount'); function updateSelectedCount() { const count = $list.find('input[type="checkbox"]:checked').length; $selectedCount.text('Selected: ' + count); } $selectAllBtn.on('click', function () { $list.find('input[type="checkbox"]').prop('checked', true); updateSelectedCount(); }); $clearAllBtn.on('click', function () { $list.find('input[type="checkbox"]').prop('checked', false); updateSelectedCount(); }); $list.on('change', 'input[type="checkbox"]', function () { updateSelectedCount(); }); updateSelectedCount(); })(jQuery); </script> }
Modifying Layout View:
Please modify the Layout View as follows. We have added a link for User management.
@{ ViewData["Title"] = ViewData["Title"] ?? "Dot Net Tutorials"; } <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8" /> <title>@ViewData["Title"] - Dot Net Tutorials</title> <meta name="viewport" content="width=device-width, initial-scale=1" /> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" /> <link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.3/font/bootstrap-icons.min.css" rel="stylesheet"> </head> <body class="d-flex flex-column min-vh-100"> <!-- Dark Navbar --> <nav class="navbar navbar-expand-lg navbar-dark bg-dark"> <div class="container"> <a class="navbar-brand fw-bold" href="@Url.Action("Index", "Home")">Dot Net Tutorials</a> <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarContent" aria-controls="navbarContent" aria-expanded="false" aria-label="Toggle navigation"> <span class="navbar-toggler-icon"></span> </button> <div class="collapse navbar-collapse" id="navbarContent"> <ul class="navbar-nav me-auto mb-2 mb-lg-0"> <li class="nav-item"> <a class="nav-link" href="@Url.Action("Index", "Home")">Home</a> </li> <li class="nav-item"> <a class="nav-link" href="@Url.Action("About", "Home")">About</a> </li> <li class="nav-item"> <a class="nav-link" asp-controller="Home" asp-action="SecureMethod">Secure</a> </li> <li class="nav-item"> <a class="nav-link" asp-controller="Home" asp-action="NonSecureMethod">Non Secure</a> </li> <li class="nav-item"> <a class="nav-link" asp-controller="Roles" asp-action="Index">Roles</a> </li> <li class="nav-item"> <a class="nav-link" asp-controller="Users" asp-action="Index">Users</a> </li> </ul> <ul class="navbar-nav mb-2 mb-lg-0"> @if (User.Identity?.IsAuthenticated == true) { <li class="nav-item dropdown"> <a class="nav-link dropdown-toggle" href="#" id="userDropdown" role="button" data-bs-toggle="dropdown" aria-expanded="false"> <i class="bi bi-person-circle me-1"></i> Hello, @User.Identity.Name </a> <ul class="dropdown-menu dropdown-menu-end" aria-labelledby="userDropdown"> <li> <a class="dropdown-item" href="@Url.Action("Profile", "Account")"> <i class="bi bi-person-lines-fill me-2"></i> Profile </a> </li> <li><hr class="dropdown-divider" /></li> <li> <form method="post" asp-controller="Account" asp-action="Logout" class="px-3 py-1"> <button type="submit" class="btn btn-link text-decoration-none w-100 text-start"> <i class="bi bi-box-arrow-right me-2"></i> Logout </button> </form> </li> </ul> </li> } else { <li class="nav-item"><a class="nav-link" href="@Url.Action("Login", "Account")">Login</a></li> <li class="nav-item"><a class="nav-link" href="@Url.Action("Register", "Account")">Register</a></li> } </ul> </div> </div> </nav> <!-- Main Content --> <main class="container flex-grow-1 py-3"> @RenderBody() </main> <!-- Footer --> <footer class="bg-dark text-light py-4 mt-auto"> <div class="container d-flex flex-column flex-md-row justify-content-between align-items-center"> <div> © @DateTime.Now.Year Dot Net Tutorials. All rights reserved. </div> <div> <a href="@Url.Action("Contact", "Home")" class="text-light me-3 text-decoration-none">Contact</a> <a href="@Url.Action("Privacy", "Home")" class="text-light me-3 text-decoration-none">Privacy Policy</a> <a href="@Url.Action("Terms", "Home")" class="text-light text-decoration-none">Terms of Service</a> </div> </div> </footer> <!-- Scripts --> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script> <script src="~/js/site.js" asp-append-version="true"></script> <script src="~/lib/jquery/dist/jquery.min.js"></script> @await RenderSectionAsync("Scripts", required: false) </body> </html>
That’s it. Now, run the application and test the functionalities; it should work as expected. In the next article, I will discuss Role-Based Authentication in ASP.NET Core Identity. In this article, I explain User Management in ASP.NET Core Identity. I hope you enjoy this article, Users Management in ASP.NET Core Identity.
I think it would be better to check if the regular user is logged in and prevent him from accessing the account registration page.
if (signInManager.IsSignedIn(User) && !(User.IsInRole(“Admin”) || User.IsInRole(“SuperAdmin”)))
{
return RedirectToAction(“Index”, “Home”);
}