(25/30)大家一起學Blazor:添加角色功能

(25/30)大家一起學Blazor:添加角色功能

首先添加承載角色數據的`ViewModel`,因為接下來的權限會以角色判斷

最後更新 2021/12/23 下午11:08
StrayaWorker
預計閱讀 9 分鐘
分類
Blazor
專題
一起學Blazor系列
標籤
.NET C# ASP.NET Core Blazor

首先添加承載角色資料的ViewModel,因為接下來的權限會以角色判斷,ASP.NET Core Identity 承載角色的ModelIdentityRole,裡面有太多不該讓用戶看到的資訊,通常會自己寫新的ViewModel 以過濾多餘資訊,這邊只呈現RoleIdNameRole 底下所有用戶的名稱。

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; }
}

添加IRolesRepository.csRolesRepository.cs,這是專門處理角色的Service,把基本的角色CRUD(Create, Read, Update, Delete) 功能實現,再去Program.cs註冊。

介面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);
}

實現RolesRepository.cs,這邊注入的RoleManagerUserManagerASP.NET Core Identity 預置處理角色跟用戶的Service,之前在Program.cs寫的builder.Services.AddIdentity<IdentityUser, IdentityRole>()…就註冊了該功能,底下有各種RoleUser 相關API可以呼叫。

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
}

上面有兩個方法後面再實現。

Program.cs添加註冊

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

現在有處理資料的功能了,接下來要添加頁面。

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>
}

然後去NavMenu.razor添加 NavLink 跳轉角色管理。

<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>

這時打開網站可以看到這樣的頁面,我們來加上新增角色的頁面並新增一個角色 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>

有了添加功能就要有編輯功能,編輯完成或取消都直接跳轉回角色列表。

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>

角色 CRUD 功能大概就是這些,筆者只是用最簡單的方式處理,不過專案通常不會這麼簡單,還有其他細微功能要調整,明天來說明如何管理角色底下的用戶,以及如何套用角色授權。

引用:

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

註:本文程式碼透過 .NET 6 + Visual Studio 2022 重構,可點擊原文連結與重構後程式碼比較學習,謝謝閱讀,支持原作者

繼續探索

延伸閱讀

更多文章
同分類 / 同標籤 2021/12/25

(29/30)大家一起學Blazor:Blazor單元測試

開發一個系統最無聊的過程大概就是解決 Bug 了,尤其是那種嘗試對 null 物件取值的錯誤(`Object reference not set to an instance of an object.`),這應該是大部分人剛踏入程式設計領域最常碰到的問題,為了從枯燥的解決 Bug 過程解脫,這篇就來介紹`單元測試`。

繼續閱讀
同分類 / 同標籤 2021/12/25

(28/30)大家一起學Blazor:Policy-based authorization

之前有說到`ASP.NET Core Identity` 使用的是基於`Claim` 的驗證,其實`ASP.NET Core Identity` 有不同類型的授權方式,最簡單的`登入授權`、`角色授權`、`Claim 授權`,但上述幾種都是以一種方式實現:原則授權(`Policy-based authorization`)。

繼續閱讀