(25/30) Let's Learn Blazor Together: Add Role Functionality

(25/30) Let's Learn Blazor Together: Add Role Functionality

First, add a `ViewModel` to carry role data, because the upcoming permissions will be judged based on roles.

Last updated 12/23/2021 11:08 PM
StrayaWorker
9 min read
Category
Blazor
Topic
Let's Learn Blazor Series
Tags
.NET C# ASP.NET Core Blazor

First, add a ViewModel to carry role data, as permissions will be determined by roles in the future. ASP.NET Core Identity uses IdentityRole as the role model, which contains too much information that should not be exposed to users. Therefore, we typically create a new ViewModel to filter out unnecessary information. Here, we only expose the Role's Id, Name, and the names of all users under the role.

using System.ComponentModel.DataAnnotations;

namespace BlazorServer.ViewModels;

public class CustomRoleViewModel
{
	public string? Id { get; set; }

	[Required(ErrorMessage = "角色名称为必填")]
	public string? Name { get; set; }

	public List<string>? Users { get; set; }
}

Add IRolesRepository.cs and RolesRepository.cs, which are services specifically designed to handle roles. Implement the basic CRUD (Create, Read, Update, Delete) operations for roles, and then register them in Program.cs.

Interface IRolesRepository.cs

using BlazorServer.Models;
using BlazorServer.ViewModels;

namespace BlazorServer.Repository;

public interface IRolesRepository
{
	Task<CustomRoleViewModel> GetRoleAsync(string roleId);
	Task<List<CustomRoleViewModel>> GetRolesAsync();
	Task<ResultViewModel> CreateRoleAsync(CustomRoleViewModel model);
	Task<ResultViewModel> EditRoleAsync(CustomRoleViewModel model);
	Task<ResultViewModel> DeleteRoleAsync(string roleId);
	Task<List<CustomUserRoleViewModel>> EditUsersInRoleAsync(string roleId);
	Task<ResultViewModel> EditUsersInRoleAsync(List<CustomUserRoleViewModel> model, string roleId);
}

Implementation RolesRepository.cs. The injected RoleManager and UserManager are built-in ASP.NET Core Identity services for handling roles and users. The earlier builder.Services.AddIdentity<IdentityUser, IdentityRole>()… in Program.cs registered these functionalities, providing various Role and User related APIs.

using BlazorServer.Models;
using BlazorServer.ViewModels;
using Microsoft.AspNetCore.Identity;

namespace BlazorServer.Repository.Implement;

public class RolesRepository : IRolesRepository
{
	private readonly RoleManager<IdentityRole> _roleManager;
	private readonly UserManager<IdentityUser> _userManager;

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

	#region Roles

	public async Task<CustomRoleViewModel> GetRoleAsync(string roleId)
	{
		var role = await _roleManager.FindByIdAsync(roleId);
		var users = await _userManager.GetUsersInRoleAsync(role.Name);
		var result = new CustomRoleViewModel
		{
			Id = role.Id,
			Name = role.Name,
			Users = users.Select(u => u.UserName).ToList()
		};
		return result;
	}

	public async Task<List<CustomRoleViewModel>> GetRolesAsync()
	{
		var roles = _roleManager.Roles;
		var customRoles = new List<CustomRoleViewModel>();
		foreach (var role in roles)
		{
			customRoles.Add(new CustomRoleViewModel { Id = role.Id, Name = role.Name });
		}

		return await Task.Run(() => customRoles);
	}

	public async Task<ResultViewModel> CreateRoleAsync(CustomRoleViewModel model)
	{
		var identityRole = new IdentityRole
		{
			Name = model.Name
		};
		var result = await _roleManager.CreateAsync(identityRole);
		if (result.Succeeded)
		{
			return new ResultViewModel
			{
				Message = "角色创建成功!",
				IsSuccess = true
			};
		}

		return new ResultViewModel
		{
			Message = "角色创建失败!",
			IsSuccess = false
		};
	}

	public async Task<ResultViewModel> EditRoleAsync(CustomRoleViewModel model)
	{
		var role = await _roleManager.FindByIdAsync(model.Id);

		if (role == null)
		{
			return new ResultViewModel
			{
				Message = $"找不到 Id 为 {model.Id} 的角色",
				IsSuccess = false
			};
		}

		role.Name = model.Name;
		var result = await _roleManager.UpdateAsync(role);
		if (result.Succeeded)
		{
			return new ResultViewModel
			{
				Message = "角色更新成功!",
				IsSuccess = true
			};
		}

		return new ResultViewModel
		{
			Message = "角色更新失败!",
			IsSuccess = false
		};
	}

	public async Task<ResultViewModel> DeleteRoleAsync(string roleId)
	{
		var role = await _roleManager.FindByIdAsync(roleId);

		if (role == null)
		{
			return new ResultViewModel
			{
				Message = $"找不到 Id 为 {roleId} 的角色",
				IsSuccess = false
			};
		}

		var result = await _roleManager.DeleteAsync(role);
		if (result.Succeeded)
		{
			return new ResultViewModel
			{
				Message = "角色删除成功!",
				IsSuccess = true
			};
		}

		return new ResultViewModel
		{
			Message = "角色删除失败!",
			IsSuccess = false
		};
	}

	#endregion
}

Two methods above will be implemented later.

Add registration in Program.cs:

builder.Services.AddScoped<IRolesRepository, RolesRepository>();

Now we have data processing functionality. Next, add pages.

RolesManagement.razor.cs

using BlazorServer.Repository;
using BlazorServer.Shared;
using BlazorServer.ViewModels;
using Microsoft.AspNetCore.Components;
using Microsoft.JSInterop;
using System.Text.Json;

namespace BlazorServer.Pages.RolesManagement;

public partial class RolesManagement
{
	[Inject] protected IRolesRepository? RolesRepository { get; set; }
	[Inject] protected IJSRuntime? Js { get; set; }
	[Inject] private NavigationManager? NavigationManager { get; set; }
	private JsInteropClasses? _jsClass;
	public List<CustomRoleViewModel> Roles { get; set; } = new();

	protected override async Task OnInitializedAsync()
	{
		await LoadData();
		_jsClass = new JsInteropClasses(Js!);
	}

	private async Task LoadData()
	{
		Roles = await RolesRepository!.GetRolesAsync();
	}

	private void EditRole(string id)
	{
		NavigationManager!.NavigateTo($"RolesManagement/EditRole/{id}");
	}

	private async Task DeleteRole(string id)
	{
		var sweetConfirm = new SweetConfirmViewModel()
		{
			RequestTitle = $"是否确定删除角色{id}?",
			RequestText = "这个动作不可恢复",
			ResponseTitle = "删除成功",
			ResponseText = "角色被删除了",
		};
		var jsonString = JsonSerializer.Serialize(sweetConfirm);
		var result = await _jsClass!.Confirm(jsonString);
		if (result)
		{
			var deleted = await RolesRepository!.DeleteRoleAsync(id);
			if (deleted.IsSuccess)
			{
				await LoadData();
			}
			else
			{
				await _jsClass.Alert(deleted.Message!);
			}
		}
	}
}

RolesManagement.razor

@page "/RolesManagement/RolesList" @attribute [Authorize]

<h1>所有角色</h1>

@if (Roles.Any())
{
    <NavLink class="btn btn-primary mb-3" href="RolesManagement/CreateRole" Match="NavLinkMatch.All">
        新增角色
    </NavLink>

    foreach (var role in Roles)
    {
        <div class="card mb-3 w-25">
            <div class="card-header">Role Id : @role.Id</div>
            <div class="card-body">
                <h5 class="card-title">@role.Name</h5>
            </div>
            <div class="card-footer">
                <button type="button" class="btn btn-primary" @onclick="() => EditRole(role.Id!)">编辑角色</button>
                <button type="button" class="btn btn-danger" @onclick="() => DeleteRole(role.Id!)">删除角色</button>
            </div>
        </div>
    }
}
else
{
    <div class="card w-25">
        <div class="card-header">还没有角色</div>
        <div class="card-body">
            <h5 class="card-title">点击底下的按钮添加角色</h5>
            <NavLink class="btn btn-primary mb-3" href="RolesManagement/CreateRole" Match="NavLinkMatch.All">
                新增角色
            </NavLink>
        </div>
    </div>
}

Then add a NavLink to the role management page in NavMenu.razor.

<div class="nav-item px-3">
    <NavLink class="nav-link" href="RolesManagement/RolesList" Match="NavLinkMatch.All">
        <span class="bi bi-kanban-fill h4 p-2 mb-0" aria-hidden="true"></span> Roles
    </NavLink>
</div>

Now when you open the website, you'll see this page. Let's add a page for creating a role and create a role named Admin.

CreateRole.razor.cs

using BlazorServer.Repository;
using BlazorServer.ViewModels;
using Microsoft.AspNetCore.Components;

namespace BlazorServer.Pages.RolesManagement;

public partial class CreateRole
{
	[Inject] protected IRolesRepository? RolesRepository { get; set; }
	[Inject] protected NavigationManager? NavigationManager { get; set; }
	public CustomRoleViewModel Role { get; set; } = new();

	private async Task CreateRoleInfo()
	{
		await RolesRepository!.CreateRoleAsync(Role);
		NavigationManager!.NavigateTo("/RolesManagement/RolesList");
	}
}

CreateRole.razor

@page "/RolesManagement/CreateRole" @attribute [Authorize]

<EditForm class="mt-3" Model="Role" OnValidSubmit="CreateRoleInfo">
    <DataAnnotationsValidator />
    <ValidationSummary />
    <div class="form-group row">
        <label for="RoleName" class="col-sm-1 col-form-label">角色名称</label>
        <div class="col-sm-3">
            <InputText @bind-Value="Role.Name" id="RoleName" class="form-control" placeholder="角色名称"></InputText>
        </div>
    </div>

    <div class="form-group row">
        <div class="col-sm-10">
            <button type="submit" class="btn btn-primary">添加角色</button>
        </div>
    </div>
</EditForm>

Now that we have the create functionality, we need an edit page. After editing or canceling, it will navigate back to the role list.

EditRole.razor.cs

using BlazorServer.Repository;
using BlazorServer.ViewModels;
using Microsoft.AspNetCore.Components;

namespace BlazorServer.Pages.RolesManagement;

public partial class EditRole
{
	[Inject] protected IRolesRepository? RolesRepository { get; set; }
	[Inject] protected NavigationManager? NavigationManager { get; set; }
	public CustomRoleViewModel Role { get; set; } = new();
	[Parameter] public string? Id { get; set; }

	protected override async Task OnInitializedAsync()
	{
		var result = await RolesRepository!.GetRoleAsync(Id!);
		Role = new CustomRoleViewModel
		{
			Id = result.Id,
			Name = result.Name,
			Users = result.Users
		};
	}

	private async Task EditRoleInfo()
	{
		await RolesRepository!.EditRoleAsync(Role);
		NavigationManager!.NavigateTo("/RolesManagement/RolesList");
	}

	public void Cancel()
	{
		NavigationManager!.NavigateTo("/RolesManagement/RolesList");
	}
}

EditRole.razor

@page "/RolesManagement/EditRole/{Id}" @attribute [Authorize]

<EditForm class="mt-3" Model="Role" OnValidSubmit="EditRoleInfo">
    <DataAnnotationsValidator />
    <ValidationSummary />
    <div class="form-group row">
        <label for="RoleName" class="col-sm-1 col-form-label">角色名称</label>
        <div class="col-sm-3">
            <InputText @bind-Value="Role.Name" id="RoleName" class="form-control" placeholder="角色名称"></InputText>
        </div>
    </div>

    <div class="card mb-3 w-50">
        <div class="card-header">
            <h3>角色底下的用户</h3>
        </div>
        <div class="card-body">
            @if (Role.Users != null && Role.Users.Any())
            {
                foreach (var user in Role.Users)
                {
                    <h5 class="card-title">@user</h5>
                }
            }
            else
            {
                <h5 class="card-title">目前该角色没有指派给任何用户</h5>
            }
        </div>
        <div class="card-footer">
            <button type="submit" class="btn btn-primary">更新角色</button>
            <button type="button" class="btn btn-danger" @onclick="Cancel">取消</button>
        </div>
    </div>
</EditForm>

The role CRUD functionality is roughly covered here. The author used the simplest approach, but real projects are usually more complex and require additional fine-tuning. We'll explain how to manage users under roles and how to apply role-based authorization tomorrow.

References:

  1. Creating roles in asp net core
  2. Get list of roles in asp net core
  3. Edit role in asp net core

Note: This code has been refactored using .NET 6 + Visual Studio 2022. You can click the original link to compare with the refactored code. Thank you for reading and supporting the original author.

Keep Exploring

Related Reading

More Articles
Same category / Same tag 12/25/2021

(29/30)Learn Blazor Together: Blazor Unit Testing

The most boring part of developing a system is probably fixing bugs, especially errors like trying to access a null object (`Object reference not set to an instance of an object.`), which is the most common problem most people encounter when they first step into programming. To break free from the tedious bug-fixing process, this article introduces `unit testing`.

Continue Reading
Same category / Same tag 12/25/2021

(28/30) Learning Blazor Together: Policy-based Authorization

It was mentioned earlier that `ASP.NET Core Identity` uses `Claim`-based authentication. In fact, `ASP.NET Core Identity` has different types of authorization methods, the simplest being `Login Authorization`, `Role Authorization`, and `Claim Authorization`. However, all of the above are implemented in one way: Policy-based Authorization.

Continue Reading