--- tags : .NET --- <style> .red{ background-color:#fff1f0; border-color:#ffa39e; color:#cf1322; padding:4px; font-size:1.75rem; } .blue{ background-color:#e6f7ff; border-color:#91d5ff; color:#096dd9; padding:4px; width:150px; text-align:center; } .green{ background-color:#f6ffed; border-color:#b7eb8f; color:#389e0d; padding:4px; width:150px; text-align:center; } .yellow{ background-color:#fffbe6; border-color:#ffe58f; color:#d48806; padding:4px; width:150px; text-align:center; } .orange{ background-color:#fffbe6; border-color:#ffd591; color:#d46b08; padding:4px; width:150px; text-align:center; } </style> # 7. Identity ### 7-1 新增Identity套件 ***在開始本章節操作之前,需先確認使用的套件版本皆為 .Net 6的最新版*** 1. AspDotNetDemo點擊滑鼠右鍵 -> 管理 NutGet 套件 -> 更新,將所有套件更新到6的最新版(須注意不要更新到 7 以上,那是 .Net 7使用的版本,在這邊更新會導致專案報錯) ![](https://i.imgur.com/JWcRChf.png) AspDotNetDemo.DataAccess點擊滑鼠右鍵 -> 管理 NutGet 套件 -> 更新,將所有套件更新到6的最新版(須注意不要更新到 7 以上,那是 .Net 7使用的版本,在這邊更新會導致專案報錯) ![](https://i.imgur.com/g19jzDn.png) ****** 2. AspDotNetDemo.DataAccess上點擊滑鼠右鍵 -> 管理NuGet套件 -> 選擇瀏覽分頁 -> 搜尋欄輸入 Microsoft.AspNetCore.Identity.EntityFrameworkCore -> 選擇版本後點選安裝(版本須跟上面一樣,選擇安裝最新版) ![](https://i.imgur.com/uhLqX7o.png) 點選I Accept ![](https://i.imgur.com/QtS8tzd.png) 3. 開啟AspDotNetDemo.DataAccess/Data/ApplicationDbContext.cs,修改程式碼 ```C#= using AspDotNetDemo.Models; using Microsoft.AspNetCore.Identity.EntityFrameworkCore; using Microsoft.EntityFrameworkCore; namespace AspDotNetDemo.DataAccess { public class ApplicationDbContext : IdentityDbContext //本次修改程式碼 { public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options) { } public DbSet<Category> Categories { get; set; } public DbSet<CoverType> CoverTypes { get; set; } public DbSet<Product> Products { get; set; } } } ``` 修改完後如果發現第7行有錯誤提示,一樣將滑鼠移至紅底線部分,出現燈泡後點選```using Microsoft.AspNetCore.Identity.EntityFrameworkCore;```,錯誤提示就會消失。 4. 接著點擊AspDotNetDemo滑鼠右鍵 -> 加入 -> 新增Scaffold 項目 點擊識別 -> 加入 ![](https://i.imgur.com/TcjdiMD.png) 勾選覆寫所有檔案 -> 資料內容類別選擇 ApplicationDbContext (AspDotNetDemo.DataAccess) -> 新增 ![](https://i.imgur.com/buET7D1.png) 完成後會出現以下畫面 ![](https://i.imgur.com/N7zoFLU.png) Areas 中也會多一個Identity資料夾 ![](https://i.imgur.com/oMtu8NE.png) 可以在AspDotNetDemo點擊滑鼠右鍵 -> 重建,如果建置成功,就代表到目前都沒有問題! 接著開啟AspDotNetDemo/Program.cs,修改部分程式碼 ```C#= using AspDotNetDemo.DataAccess; using AspDotNetDemo.DataAccess.Repository; using AspDotNetDemo.DataAccess.Repository.IRepository; using Microsoft.EntityFrameworkCore; using Microsoft.AspNetCore.Identity; var builder = WebApplication.CreateBuilder(args); // Add services to the container. builder.Services.AddControllersWithViews(); builder.Services.AddDbContext<ApplicationDbContext>(options => options.UseSqlServer( builder.Configuration.GetConnectionString("DefaultConnection") )); //本次修改部分 builder.Services.AddDefaultIdentity<IdentityUser>() .AddEntityFrameworkStores<ApplicationDbContext>(); builder.Services.AddScoped<IUnitOfWork, UnitOfWork>(); builder.Services.AddRazorPages().AddRazorRuntimeCompilation(); var app = builder.Build(); // Configure the HTTP request pipeline. if (!app.Environment.IsDevelopment()) { app.UseExceptionHandler("/Home/Error"); // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. app.UseHsts(); } app.UseHttpsRedirection(); app.UseStaticFiles(); app.UseRouting(); app.UseAuthentication(); ; app.UseAuthorization(); app.MapControllerRoute( name: "default", pattern: "{area=Customer}/{controller=Home}/{action=Index}/{id?}"); app.Run(); ``` 完成後打開AspDotNetDemo/Views/Shared/_LoginPartial.cshtml,將```text-dark```的部分刪除,完成後如下 ```html= @using Microsoft.AspNetCore.Identity @inject SignInManager<IdentityUser> SignInManager @inject UserManager<IdentityUser> UserManager <ul class="navbar-nav"> @if (SignInManager.IsSignedIn(User)) { <li class="nav-item"> <a id="manage" class="nav-link" asp-area="Identity" asp-page="/Account/Manage/Index" title="Manage">Hello @UserManager.GetUserName(User)!</a> </li> <li class="nav-item"> <form id="logoutForm" class="form-inline" asp-area="Identity" asp-page="/Account/Logout" asp-route-returnUrl="@Url.Action("Index", "Home", new { area = "" })"> <button id="logout" type="submit" class="nav-link btn btn-link">Logout</button> </form> </li> } else { <li class="nav-item"> <a class="nav-link" id="register" asp-area="Identity" asp-page="/Account/Register">Register</a> </li> <li class="nav-item"> <a class="nav-link" id="login" asp-area="Identity" asp-page="/Account/Login">Login</a> </li> } </ul> ``` 接下來要讓首頁的右上方出現註冊及登入的按鈕 開啟AspDotNetDemo/Views/Shared/_Layout.cshtml ```html= <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>@ViewData["Title"] - AspDotNetDemo</title> @*<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" />*@ <link rel="stylesheet" href="~/css/bootswatchTheme.css" /> <link rel="stylesheet" href="~/css/site.css" asp-append-version="true" /> <link rel="stylesheet" href="~/AspDotNetDemo.styles.css" asp-append-version="true" /> <link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/toastr.js/latest/toastr.min.css"/> <link rel="stylesheet" href="//cdn.datatables.net/1.13.1/css/jquery.dataTables.min.css" /> </head> <body> <header> <nav class="navbar navbar-expand-lg navbar-dark bg-primary"> <div class="container-fluid"> <a class="navbar-brand" href="#">Double Drinks</a> <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarColor02" aria-controls="navbarColor02" aria-expanded="false" aria-label="Toggle navigation"> <span class="navbar-toggler-icon"></span> </button> <div class="collapse navbar-collapse" id="navbarColor02"> <ul class="navbar-nav me-auto"> <li class="nav-item"> <a class="nav-link" asp-area="Customer" asp-controller="Home" asp-action="Index">首頁</a> </li> <li class="nav-item"> <a class="nav-link" asp-area="Admin" asp-controller="Category" asp-action="Index">類別</a> </li> <li class="nav-item"> <a class="nav-link" asp-area="Admin" asp-controller="Product" asp-action="Index">產品</a> </li> @*<li class="nav-item dropdown"> <a class="nav-link dropdown-toggle" data-bs-toggle="dropdown" href="#" role="button" aria-haspopup="true" aria-expanded="false">Dropdown</a> <div class="dropdown-menu"> <a class="dropdown-item" href="#">Action</a> <a class="dropdown-item" href="#">Another action</a> <a class="dropdown-item" href="#">Something else here</a> <div class="dropdown-divider"></div> <a class="dropdown-item" href="#">Separated link</a> </div> </li>*@ </ul> @*本次修改部分*@ <partial name="_LoginPartial" /> @*<form class="d-flex"> <input class="form-control me-sm-2" type="text" placeholder="Search"> <button class="btn btn-secondary my-2 my-sm-0" type="submit">Search</button> </form>*@ </div> </div> </nav> </header> <div class="container content"> <main role="main" class="py-3"> @RenderBody() </main> </div> <footer class="footer text-white bg-primary p-1"> <div class="container text-center"> &copy; 2022 - AspDotNetDemo - <a asp-area="" asp-controller="Home" asp-action="Privacy" class="text-warning">Privacy</a> </div> </footer> <script src="~/lib/jquery/dist/jquery.min.js"></script> @*<script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script>*@ <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.1/dist/js/bootstrap.bundle.min.js" integrity="sha384-/bQdsTh/da6pkI1MST/rWKFNjaCP5gBSY4sEBT38Q/9RBh9AH40zEOg7Hlq2THRZ" crossorigin="anonymous"></script> <script src="~/js/site.js" asp-append-version="true"></script> <script src="//cdn.jsdelivr.net/npm/sweetalert2@11"></script> <script src="//cdn.datatables.net/1.13.1/js/jquery.dataTables.min.js"></script> <script src="//cdnjs.cloudflare.com/ajax/libs/toastr.js/latest/toastr.min.js"></script> @await RenderSectionAsync("Scripts", required: false) </body> </html> ``` 完成後執行應用程式會發現他卡住了,並會出現以下的錯誤訊息 ![](https://i.imgur.com/Pysf3gC.png) 會有這個問題是因為 .Net Core在建立Identity時,會建立一個新的ApplicationDbContext,導致在執行時會產生錯誤,只需要將其註解或刪除就可以解決問題。 開啟AspDotNetDemo/Areas/Identity/Data/ApplicationDbContext.cs,並將其註解 ![](https://i.imgur.com/cKNcaJg.png) 完成後就可以再次執行應用程式囉,這次就不會有錯誤訊息了,並且首頁的右上方有出現註冊及登入的按鈕,就代表到目前為止都沒問題。 ![](https://i.imgur.com/6ok88lx.png) 接著點擊註冊按鈕,會發現他找不到該網頁 ![](https://i.imgur.com/Rc7kjBv.png) 開啟AspDotNetDemo/Program.cs,這裡需要新增一行程式碼 ```C#= using AspDotNetDemo.DataAccess; using AspDotNetDemo.DataAccess.Repository; using AspDotNetDemo.DataAccess.Repository.IRepository; using Microsoft.EntityFrameworkCore; using Microsoft.AspNetCore.Identity; var builder = WebApplication.CreateBuilder(args); // Add services to the container. builder.Services.AddControllersWithViews(); builder.Services.AddDbContext<ApplicationDbContext>(options => options.UseSqlServer( builder.Configuration.GetConnectionString("DefaultConnection") )); builder.Services.AddDefaultIdentity<IdentityUser>() .AddEntityFrameworkStores<ApplicationDbContext>(); builder.Services.AddScoped<IUnitOfWork, UnitOfWork>(); builder.Services.AddRazorPages().AddRazorRuntimeCompilation(); var app = builder.Build(); // Configure the HTTP request pipeline. if (!app.Environment.IsDevelopment()) { app.UseExceptionHandler("/Home/Error"); // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. app.UseHsts(); } app.UseHttpsRedirection(); app.UseStaticFiles(); app.UseRouting(); app.UseAuthentication(); ; app.UseAuthorization(); //本次新增部分 app.MapRazorPages(); app.MapControllerRoute( name: "default", pattern: "{area=Customer}/{controller=Home}/{action=Index}/{id?}"); app.Run(); ``` 完成後就可以打開註冊頁面囉。 ![](https://i.imgur.com/3lOWs1p.png) ### 7-2 建立Identity Table 因為我們使用了Idenity這個套件,裡面具有註冊用戶跟所有用戶的詳細資訊。裡面還具有身分所需的其他規則跟表格,一切都是預設完成的,因此會節省很多開發時間。 1. 上方工具列點選工具 -> NuGet 套件管理員 -> 套件管理主控台,主控台出現後記得更改預設專案,改為AspDotNetDemo.DataAccess,完成後輸入指令 ``` add-migration AddIdentityToDb ``` 接著更新資料庫,完成後就會產生資料表了。 ``` update-database ``` ![](https://i.imgur.com/yVtl2u4.png) 2. 自定義User資料 ![](https://i.imgur.com/qRn8zq8.png) 在Program.cs中可以找到,Identity有預設的使用者資料表,但是他並沒有包含太多的屬性,因此我們想擴展我們的使用者資料並為我們的網站創建一個具有更多屬性的使用者Model,像是使用者名稱、地址、城市等等。 AspDotNetDemo.Models點擊滑鼠右鍵 -> 加入 -> 類別,將其命名為ApplicationUser後新增 ![](https://i.imgur.com/OcJBEhp.png) 將剛建立的ApplicationUser修改為下方程式碼 ```C#= using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace AspDotNetDemo.Models { public class ApplicationUser : IdentityUser { } } ``` 修改完後會發現有錯誤提示,將滑鼠移至紅底線部分,出現燈泡後點選安裝套件 'Microsoft.Extensions.Identity.Stores' -> 選擇使用本機版本,完成後就會看到錯誤提示消失了 ![](https://i.imgur.com/GprW3NN.png) 接著再加上欄位型態與名稱 ```C#= using Microsoft.AspNetCore.Identity; using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; using System.Text; using System.Threading.Tasks; namespace AspDotNetDemo.Models { public class ApplicationUser : IdentityUser { [Required] public string Name { get; set; } public string? StreetAddress { get; set; } public string? City { get; set; } public string? State { get; set; } public string? PostalCode { get; set; } } } ``` * 在string後面加上?是因為要讓欄位變成非必填 * 第14行出現紅色底線時,點擊該行出現的燈泡,並引入套件即可 3. 接著開啟AspDotNetDemo.DataAccess/Data/ApplicationDbContext.cs,新增一行程式碼 ```C# using AspDotNetDemo.Models; using Microsoft.AspNetCore.Identity.EntityFrameworkCore; using Microsoft.EntityFrameworkCore; namespace AspDotNetDemo.DataAccess { public class ApplicationDbContext : IdentityDbContext { public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options) { } public DbSet<Category> Categories { get; set; } public DbSet<CoverType> CoverTypes { get; set; } public DbSet<Product> Products { get; set; } public DbSet<ApplicationUser> ApplicationUsers { get; set; } //本次新增部分 } } ``` 完成後就要下指令新增Migration,打開套件管理主控台,輸入下方指令(確認預設專案須為AspDotNetDemo.DataAccess) ``` add-migration extendIdentityUser ``` 接著更新資料庫 ``` update-database ``` 完成後去資料庫打開AspNetUser資料表,就可以看到剛剛新增的欄位出現在資料表上了 ![](https://i.imgur.com/MxdSaXV.png) 4. 測試註冊功能 接著我們可以到我們的註冊頁面測試我們的註冊功能。 ![](https://i.imgur.com/fUaddKn.png) 註冊完成後,就可以看到使用者被加進我們的使用者資料表中了,可以觀察到這邊有一個Discriminator欄位,.net core 非常聰明,在這邊幫我們新增了一個判斷的欄位,他可以判斷我們新增的是甚麼身分的用戶。 ![](https://i.imgur.com/G55SRrR.png) ### 7-3 建立Company CRUD 因為我們的網站中,會有使用者、公司、跟管理者等不同角色,因此,我們在這邊想要創建一個公司的資料表,裡面包含了Id、Name、StreetAddress等不同的欄位,當然也會使用到Repository、UnitOfWork的部分,這個小節將會帶大家創建公司的資料。 #### 7-3-1 建立 Company Model AspDotNetDemo.Models點擊滑鼠右鍵 -> 加入 -> 類別,將其命名為Company後新增 ![](https://i.imgur.com/OcJBEhp.png) 將剛建立的ApplicationUser修改為下方程式碼 ```C#= using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; using System.Text; using System.Threading.Tasks; namespace AspDotNetDemo.Models { public class Company { public int Id { get; set; } [Required] public string Name { get; set; } public string? StreetAddress { get; set; } public string? City { get; set; } public string? State { get; set; } public string? PostalCode { get; set; } public string? PhoneNumber { get; set; } } } ``` * Id是我們的主鍵,Name 為必填欄位 #### 7-3-2 修改 ApplicationDbContext 開啟AspDotNetDemo.DataAccess/Data/ApplicationDbContext.cs,新增一行程式碼 ```C# using AspDotNetDemo.Models; using Microsoft.AspNetCore.Identity.EntityFrameworkCore; using Microsoft.EntityFrameworkCore; namespace AspDotNetDemo.DataAccess { public class ApplicationDbContext : IdentityDbContext { public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options) : base(options) { } public DbSet<Category> Categories { get; set; } public DbSet<CoverType> CoverTypes { get; set; } public DbSet<Product> Products { get; set; } public DbSet<ApplicationUser> ApplicationUsers { get; set; } public DbSet<Company> Companies { get; set; } //本次新增部分 } } ``` 完成後就要下指令新增Migration,打開套件管理主控台,輸入下方指令(確認預設專案須為AspDotNetDemo.DataAccess) ``` add-migration addCompanyToDb ``` 接著更新資料庫 ``` update-database ``` #### 7-3-3 建立 Company 的 Repository 以及 IRepository 在AspDotNetDemo.DataAccess/Repository/IRepository新增檔案(資料夾點擊右鍵 -> 加入 -> 類別),將其命名為ICompanyRepository.cs 然後將ICompanyRepository.cs的程式碼修改 ```C#= using AspDotNetDemo.Models; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace AspDotNetDemo.DataAccess.Repository.IRepository { public interface ICompanyRepository : IRepository<Company> { void Update(Company obj); } } ``` 在AspDotNetDemo.DataAccess/Repository下新增檔案(資料夾點擊右鍵 -> 加入 -> 類別),將其命名為CompanyRepository.cs 然後將CompanyRepository.cs的程式碼修改 ```C#= using AspDotNetDemo.DataAccess.Repository.IRepository; using AspDotNetDemo.Models; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace AspDotNetDemo.DataAccess.Repository { public class CompanyRepository : Repository<Company>, ICompanyRepository { private ApplicationDbContext _db; public CompanyRepository(ApplicationDbContext db) : base(db) { _db = db; } public void Update(Company obj) { _db.Companies.Update(obj); } } } ``` #### 7-3-4 建立 Company 的 UnitOfWork 開啟AspDotNetDemo.DataAccess/Repository/IRepository下的IUnitOfWork.cs,新增程式碼 ```C#= using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace AspDotNetDemo.DataAccess.Repository.IRepository { public interface IUnitOfWork { ICategoryRepository Category { get; } ICoverTypeRepository CoverType { get; } IProductRepository Product { get; } ICompanyRepository Company { get; } //本次新增程式碼 void Save(); } } ``` 開啟AspDotNetDemo.DataAccess/Repository下的UnitOfWork.cs,新增程式碼 ```C#= using AspDotNetDemo.DataAccess.Repository.IRepository; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace AspDotNetDemo.DataAccess.Repository { public class UnitOfWork : IUnitOfWork { private ApplicationDbContext _db; public UnitOfWork(ApplicationDbContext db) { _db = db; Category = new CategoryRepository(_db); CoverType = new CoverTypeRepository(_db); Product = new ProductRepository(_db); Company = new CompanyRepository(_db); //本次新增程式碼 } public ICategoryRepository Category { get; private set; } public ICoverTypeRepository CoverType { get; private set; } public IProductRepository Product { get; private set; } public ICompanyRepository Company { get; private set; } //本次新增程式碼 public void Save() { _db.SaveChanges(); } } } ``` #### 7-3-5 建立 Company 的 Controller 在AspDotNetDemo/Areas/Admin/Controllers下新增檔案(資料夾點擊右鍵 -> 加入 -> 類別),將其命名為CompanyController.cs,然後將ProductController.cs的內容全部複製過來,並修改程式碼。 ```C#= using AspDotNetDemo.DataAccess; using AspDotNetDemo.DataAccess.Repository.IRepository; using AspDotNetDemo.Models; using AspDotNetDemo.Models.ViewModels; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.Rendering; namespace AspDotNetDemo.Areas.Admin.Controllers { [Area("Admin")] public class CompanyController : Controller { private readonly IUnitOfWork _unitOfWork; public CompanyController(IUnitOfWork unitOfWork) { _unitOfWork = unitOfWork; } public IActionResult Index() { return View(); } //GET public IActionResult Upsert(int? id) { Company company = new(); if (id == null || id == 0) { return View(company); } else { //update company company = _unitOfWork.Company.GetFirstOrDefault(u => u.Id == id); return View(company); } } //POST [HttpPost] [ValidateAntiForgeryToken] public IActionResult Upsert(Company obj, IFormFile? file) { if(ModelState.IsValid) { if (obj.Id == 0) { _unitOfWork.Company.Add(obj); TempData["success"] = "Company createed successfully!"; } else { _unitOfWork.Company.Update(obj); TempData["success"] = "Company updated successfully!"; } _unitOfWork.Save(); return RedirectToAction("Index"); } return View(obj); } #region API CALLS [HttpGet] public IActionResult GetAll() { var companyList = _unitOfWork.Company.GetAll(); return Json(new { data = companyList }); } //POST [HttpDelete] public IActionResult Delete(int? id) { var obj = _unitOfWork.Company.GetFirstOrDefault(u => u.Id == id); if (obj == null) { return Json(new { success = false, message = "Error while deleting" }); } _unitOfWork.Company.Remove(obj); _unitOfWork.Save(); return Json(new { success = true, message = "Delete Successfully!!" }); } #endregion } } ``` #### 7-3-6 建立 Company 的 View 複製AspDotNetDemo/Areas/Admin/Views/Product資料夾,並在AspDotNetDemo/Areas/Admin/Views中貼上重新命名為Company 修改AspDotNetDemo/Areas/Admin/Views/Company/Index.cshtml的程式碼。 ```html= <div class="container p-3"> <div class="row pt-4"> <div class="col-6"> <h2 class="text-primary">Company List</h2> </div> <div class="col-6 text-end"> <a asp-controller="Company" asp-action="Upsert" class="btn btn-primary"> 新增公司 </a> </div> </div> <table id="tblData" class="table table-bordered table-striped" style="width:100%"> <thead> <tr class="align-middle"> <th>Name</th> <th>Address</th> <th>City</th> <th>State</th> <th>Phone Number</th> <th></th> </tr> </thead> </table> </div> @section Scripts{ <script src="~/js/company.js"></script> } ``` #### 7-3-7 修改 Company 的 Upsert.cshtml 修改AspDotNetDemo/Areas/Admin/Views/Company/Upsert.cshtml的程式碼。 ```htmlembedded= @model Company <form method="post" asp-action="Upsert" enctype="multipart/form-data"> <input asp-for="Id" hidden /> <div class="row"> <div class="col-10"> <div class="border p-3 mt-4 row"> <div class="col-12 pb-2"> <h2 class="text-primary">@(Model.Id!=0?"Update": "Create") Company</h2> <hr /> </div> <div class="mb-3 col-6"> <label asp-for="Name"></label> <input asp-for="Name" class="form-control" /> <span asp-validation-for="Name" class="text-danger"></span> </div> <div class="mb-3 col-6"> <label asp-for="PhoneNumber"></label> <input asp-for="PhoneNumber" class="form-control" /> <span asp-validation-for="PhoneNumber" class="text-danger"></span> </div> <div class="mb-3 col-6"> <label asp-for="StreetAddress"></label> <input asp-for="StreetAddress" class="form-control" /> <span asp-validation-for="StreetAddress" class="text-danger"></span> </div> <div class="mb-3 col-6"> <label asp-for="City"></label> <input asp-for="City" class="form-control" /> <span asp-validation-for="City" class="text-danger"></span> </div> <div class="mb-3 col-6"> <label asp-for="State"></label> <input asp-for="State" class="form-control" /> <span asp-validation-for="State" class="text-danger"></span> </div> <div class="mb-3 col-6"> <label asp-for="PostalCode"></label> <input asp-for="PostalCode" class="form-control" /> <span asp-validation-for="PostalCode" class="text-danger"></span> </div> <div class="col-12"> @if (Model.Id != 0) { <button type="submit" class="btn btn-primary" style="width:150px">更新</button> } else { <button type="submit" class="btn btn-primary" style="width:150px">新增</button> } <a asp-controller="Company" asp-action="Index" class="btn btn-secondary" style="width:150px">返回</a> </div> </div> </div> </div> </form> @section Scripts{ @{ <partial name="_ValidationScriptsPartial" /> } } ``` #### 7-3-8 建立 Company 的 JavaScript 複製AspDotNetDemo/wwwroot/js/product.js檔案並在AspDotNetDemo/wwwroot/js貼上重新命名為company.js 修改AspDotNetDemo/wwwroot/js/company.js的程式碼。 ```javascript= var dataTable; $(document).ready(function () { loadDataTable(); }); function loadDataTable() { dataTable = $('#tblData').DataTable({ "ajax": { "url": "/Admin/Company/GetAll", }, "columns": [//注意這裡的欄位數量要跟html的欄位數量相同,否則會報錯 { "data": "name", "width": "15%" }, { "data": "streetAddress", "width": "15%" }, { "data": "city", "width": "15%" }, { "data": "state", "width": "15%" }, { "data": "phoneNumber", "width": "15%" }, { "data": "id", "render": function (data) { return ` <div class="w-75 btn-group" role="group"> <a href="/Admin/Company/Upsert?id=${data}" class="btn btn-primary mx-2"><i class="bi bi-pencil-square"></i>Edit</a> <a onClick=Delete('/Admin/Company/Delete/${data}') class="btn btn-danger mx-2"><i class="bi bi-trash"></i>Delete</a> </div> ` }, "width": "15%" }, ] }); } function Delete(url) { Swal.fire({ title: 'Are you sure?', text: "You won't be able to revert this!", icon: 'warning', showCancelButton: true, confirmButtonColor: '#3085d6', cancelButtonColor: '#d33', confirmButtonText: 'Yes, delete it!' }).then((result) => { if (result.isConfirmed) { $.ajax({ url: url, type: "DELETE", success: function (data) { if (data.success) { toastr.success(data.message); dataTable.ajax.reload(); } else { toastr.error(data.message); } } }) } }) } ``` #### 7-3-9 修改_Layout.cshtml 修改AspDotNetDemo/Views/Shared/_Layout.cshtml的程式碼。 ```htmlembedded= <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>@ViewData["Title"] - AspDotNetDemo</title> @*<link rel="stylesheet" href="~/lib/bootstrap/dist/css/bootstrap.min.css" />*@ <link rel="stylesheet" href="~/css/bootswatchTheme.css" /> <link rel="stylesheet" href="~/css/site.css" asp-append-version="true" /> <link rel="stylesheet" href="~/AspDotNetDemo.styles.css" asp-append-version="true" /> <link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/toastr.js/latest/toastr.min.css"/> <link rel="stylesheet" href="//cdn.datatables.net/1.13.1/css/jquery.dataTables.min.css" /> </head> <body> <header> <nav class="navbar navbar-expand-lg navbar-dark bg-primary"> <div class="container-fluid"> <a class="navbar-brand" href="#">Double Drinks</a> <button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarColor02" aria-controls="navbarColor02" aria-expanded="false" aria-label="Toggle navigation"> <span class="navbar-toggler-icon"></span> </button> <div class="collapse navbar-collapse" id="navbarColor02"> <ul class="navbar-nav me-auto"> // 本次修改程式碼 <li class="nav-item"> <a class="nav-link" asp-area="Customer" asp-controller="Home" asp-action="Index">首頁</a> </li> <li class="nav-item dropdown"> <a class="nav-link dropdown-toggle" data-bs-toggle="dropdown" href="#" role="button" aria-haspopup="true" aria-expanded="false">內容管理</a> <div class="dropdown-menu"> <a class="dropdown-item" asp-area="Admin" asp-controller="Category" asp-action="Index">類別</a> <a class="dropdown-item" asp-area="Admin" asp-controller="Product" asp-action="Index">產品</a> <div class="dropdown-divider"></div> <a class="dropdown-item" asp-area="Admin" asp-controller="Company" asp-action="Index">公司</a> </div> </li> </ul> <partial name="_LoginPartial" /> @*<form class="d-flex"> <input class="form-control me-sm-2" type="text" placeholder="Search"> <button class="btn btn-secondary my-2 my-sm-0" type="submit">Search</button> </form>*@ </div> </div> </nav> </header> <div class="container content"> <main role="main" class="py-3"> @RenderBody() </main> </div> <footer class="footer text-white bg-primary p-1"> <div class="container text-center"> &copy; 2022 - AspDotNetDemo - <a asp-area="" asp-controller="Home" asp-action="Privacy" class="text-warning">Privacy</a> </div> </footer> <script src="~/lib/jquery/dist/jquery.min.js"></script> @*<script src="~/lib/bootstrap/dist/js/bootstrap.bundle.min.js"></script>*@ <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.1/dist/js/bootstrap.bundle.min.js" integrity="sha384-/bQdsTh/da6pkI1MST/rWKFNjaCP5gBSY4sEBT38Q/9RBh9AH40zEOg7Hlq2THRZ" crossorigin="anonymous"></script> <script src="~/js/site.js" asp-append-version="true"></script> <script src="//cdn.jsdelivr.net/npm/sweetalert2@11"></script> <script src="//cdn.datatables.net/1.13.1/js/jquery.dataTables.min.js"></script> <script src="//cdnjs.cloudflare.com/ajax/libs/toastr.js/latest/toastr.min.js"></script> @await RenderSectionAsync("Scripts", required: false) </body> </html> ``` ![](https://i.imgur.com/VP7zA3O.png) 以上步驟都完成後就可以執行應用程式,測試Company的新增、編輯、刪除等功能是否正常,如果都可以正常執行那就代表到目前這邊都完成囉。 ### 7-4 建立 Role 在上一個章節我們已經完成了基本的公司運作,接下來讓我們回到用戶方面,因為任何的網站都有多種類型的角色。我們會有公司的客戶、公司、管理者等等。 #### 7-4-1 修改 Program.cs ![](https://i.imgur.com/bTd5LsO.png) 可以到我們的資料庫中查看,預設情況沒有產生角色的規則,所以在這個章節我們必須創建我們的規則。 因此,接下來讓我們修改Program的程式碼,讓Identity多經過一層角色的判斷,因為我們這邊使用了自定義的角色,因此這邊需要產生Token,原本預設的程式碼可以到Identity/Pages/Account/Manage/Register.cshtml.cs中的OnPostAsync的Function查看Token產生的過程。 開啟AspDotNetDemo/Program.cs,並修改部分程式碼 ```C#= using AspDotNetDemo.DataAccess; using AspDotNetDemo.DataAccess.Repository; using AspDotNetDemo.DataAccess.Repository.IRepository; using Microsoft.EntityFrameworkCore; using Microsoft.AspNetCore.Identity; var builder = WebApplication.CreateBuilder(args); // Add services to the container. builder.Services.AddControllersWithViews(); builder.Services.AddDbContext<ApplicationDbContext>(options => options.UseSqlServer( builder.Configuration.GetConnectionString("DefaultConnection") )); // 本次修改部分 builder.Services.AddIdentity<IdentityUser, IdentityRole>().AddDefaultTokenProviders() .AddEntityFrameworkStores<ApplicationDbContext>(); builder.Services.AddScoped<IUnitOfWork, UnitOfWork>(); builder.Services.AddRazorPages().AddRazorRuntimeCompilation(); var app = builder.Build(); // Configure the HTTP request pipeline. if (!app.Environment.IsDevelopment()) { app.UseExceptionHandler("/Home/Error"); // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. app.UseHsts(); } app.UseHttpsRedirection(); app.UseStaticFiles(); app.UseRouting(); app.UseAuthentication();; app.UseAuthorization(); app.MapRazorPages(); app.MapControllerRoute( name: "default", pattern: "{area=Customer}/{controller=Home}/{action=Index}/{id?}"); app.Run(); ``` #### 7-4-2 修改 Register.cshtml.cs 及建立 SD.cs 開啟AspDotNetDemo.Utility/SD.cs,並新增程式碼 ```C#= using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace AspDotNetDemo.Utility { public static class SD // 修改成 public static { public const string Role_User_Cust = "Customer"; public const string Role_Employee = "Employee"; public const string Role_User_Comp = "Company"; public const string Role_Admin = "Admin"; } } ``` 開啟AspDotNetDemo/Areas/Identity/Pages/Account/Register.cshtml/Register.cshtml.cs,並修改程式碼 ```C#= // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. #nullable disable using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.Linq; using System.Text; using System.Text.Encodings.Web; using System.Threading; using System.Threading.Tasks; using AspDotNetDemo.Utility; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity.UI.Services; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; using Microsoft.AspNetCore.WebUtilities; using Microsoft.Extensions.Logging; namespace AspDotNetDemo.Areas.Identity.Pages.Account { public class RegisterModel : PageModel { private readonly SignInManager<IdentityUser> _signInManager; private readonly UserManager<IdentityUser> _userManager; private readonly IUserStore<IdentityUser> _userStore; private readonly IUserEmailStore<IdentityUser> _emailStore; private readonly ILogger<RegisterModel> _logger; private readonly IEmailSender _emailSender; // 本次新增部分 private readonly RoleManager<IdentityRole> _roleManager; public RegisterModel( UserManager<IdentityUser> userManager, IUserStore<IdentityUser> userStore, SignInManager<IdentityUser> signInManager, ILogger<RegisterModel> logger, IEmailSender emailSender, RoleManager<IdentityRole> roleManager) // 本次新增部分 { _roleManager = roleManager; // 本次新增部分 _userManager = userManager; _userStore = userStore; _emailStore = GetEmailStore(); _signInManager = signInManager; _logger = logger; _emailSender = emailSender; } /// <summary> /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used /// directly from your code. This API may change or be removed in future releases. /// </summary> [BindProperty] public InputModel Input { get; set; } /// <summary> /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used /// directly from your code. This API may change or be removed in future releases. /// </summary> public string ReturnUrl { get; set; } /// <summary> /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used /// directly from your code. This API may change or be removed in future releases. /// </summary> public IList<AuthenticationScheme> ExternalLogins { get; set; } /// <summary> /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used /// directly from your code. This API may change or be removed in future releases. /// </summary> public class InputModel { /// <summary> /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used /// directly from your code. This API may change or be removed in future releases. /// </summary> [Required] [EmailAddress] [Display(Name = "Email")] public string Email { get; set; } /// <summary> /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used /// directly from your code. This API may change or be removed in future releases. /// </summary> [Required] [StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)] [DataType(DataType.Password)] [Display(Name = "Password")] public string Password { get; set; } /// <summary> /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used /// directly from your code. This API may change or be removed in future releases. /// </summary> [DataType(DataType.Password)] [Display(Name = "Confirm password")] [Compare("Password", ErrorMessage = "The password and confirmation password do not match.")] public string ConfirmPassword { get; set; } } public async Task OnGetAsync(string returnUrl = null) { // 本次新增部分 if (!_roleManager.RoleExistsAsync(SD.Role_Admin).GetAwaiter().GetResult()) { _roleManager.CreateAsync(new IdentityRole(SD.Role_Admin)).GetAwaiter().GetResult(); _roleManager.CreateAsync(new IdentityRole(SD.Role_Employee)).GetAwaiter().GetResult(); _roleManager.CreateAsync(new IdentityRole(SD.Role_User_Cust)).GetAwaiter().GetResult(); _roleManager.CreateAsync(new IdentityRole(SD.Role_User_Comp)).GetAwaiter().GetResult(); } ReturnUrl = returnUrl; ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList(); } public async Task<IActionResult> OnPostAsync(string returnUrl = null) { returnUrl ??= Url.Content("~/"); ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList(); if (ModelState.IsValid) { var user = CreateUser(); await _userStore.SetUserNameAsync(user, Input.Email, CancellationToken.None); await _emailStore.SetEmailAsync(user, Input.Email, CancellationToken.None); var result = await _userManager.CreateAsync(user, Input.Password); if (result.Succeeded) { _logger.LogInformation("User created a new account with password."); var userId = await _userManager.GetUserIdAsync(user); var code = await _userManager.GenerateEmailConfirmationTokenAsync(user); code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code)); var callbackUrl = Url.Page( "/Account/ConfirmEmail", pageHandler: null, values: new { area = "Identity", userId = userId, code = code, returnUrl = returnUrl }, protocol: Request.Scheme); await _emailSender.SendEmailAsync(Input.Email, "Confirm your email", $"Please confirm your account by <a href='{HtmlEncoder.Default.Encode(callbackUrl)}'>clicking here</a>."); if (_userManager.Options.SignIn.RequireConfirmedAccount) { return RedirectToPage("RegisterConfirmation", new { email = Input.Email, returnUrl = returnUrl }); } else { await _signInManager.SignInAsync(user, isPersistent: false); return LocalRedirect(returnUrl); } } foreach (var error in result.Errors) { ModelState.AddModelError(string.Empty, error.Description); } } // If we got this far, something failed, redisplay form return Page(); } private IdentityUser CreateUser() { try { return Activator.CreateInstance<IdentityUser>(); } catch { throw new InvalidOperationException($"Can't create an instance of '{nameof(IdentityUser)}'. " + $"Ensure that '{nameof(IdentityUser)}' is not an abstract class and has a parameterless constructor, or alternatively " + $"override the register page in /Areas/Identity/Pages/Account/Register.cshtml"); } } private IUserEmailStore<IdentityUser> GetEmailStore() { if (!_userManager.SupportsUserEmail) { throw new NotSupportedException("The default UI requires a user store with email support."); } return (IUserEmailStore<IdentityUser>)_userStore; } } } ``` 為了創建規則,我們在這裡有一個叫做Role Manager的東西,可以注意到這邊我們在依賴注入的幫助下獲得的Manager。因此我們可以在這邊創建一個私有的只允許讀取的Role Manager,通過IdentityRole來取得角色。 #### 7-4-3 修復 Bug 完成後執行應用程式,點擊註冊按鈕會發現Error ![](https://i.imgur.com/0k9n98r.png) 點擊AspDotNetDemo.Utility滑鼠右鍵 -> 加入 -> 類別,將其命名為EmailSender後新增 ![](https://i.imgur.com/kMSS6S8.png) 修改為下方程式碼 ```C#= using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace AspDotNetDemo.Utility { public class EmailSender : IEmailSender { } } ``` 會發現```IEmailSender```的部分有錯誤提示,將滑鼠移至紅線部分,出現燈泡後點選安裝套件 Microsoft.AspNetCore.Identity.UI -> 選擇使用本機版本 ![](https://i.imgur.com/BJrGGaf.png) 會發現紅底線部分沒有消失,因為還沒有新增實作介面,一樣將滑鼠移至紅底線部分,出現燈泡後點選實作介面,並修改程式碼 ![](https://i.imgur.com/N8RHjSA.png) ```C#= using Microsoft.AspNetCore.Identity.UI.Services; using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; namespace AspDotNetDemo.Utility { public class EmailSender : IEmailSender { public Task SendEmailAsync(string email, string subject, string htmlMessage) { return Task.CompletedTask; } } } ``` 開啟 Program.cs,新增部分程式碼,出現紅底線的部分一樣點擊燈泡後引入套件即可。 ```C#= using AspDotNetDemo.DataAccess; using AspDotNetDemo.DataAccess.Repository; using AspDotNetDemo.DataAccess.Repository.IRepository; using Microsoft.EntityFrameworkCore; using Microsoft.AspNetCore.Identity; // 本次新增部分 using Microsoft.AspNetCore.Identity.UI.Services; using AspDotNetDemo.Utility; var builder = WebApplication.CreateBuilder(args); // Add services to the container. builder.Services.AddControllersWithViews(); builder.Services.AddDbContext<ApplicationDbContext>(options => options.UseSqlServer( builder.Configuration.GetConnectionString("DefaultConnection") )); builder.Services.AddIdentity<IdentityUser, IdentityRole>().AddDefaultTokenProviders() .AddEntityFrameworkStores<ApplicationDbContext>(); builder.Services.AddScoped<IUnitOfWork, UnitOfWork>(); // 本次新增部分 builder.Services.AddSingleton<IEmailSender, EmailSender>(); builder.Services.AddRazorPages().AddRazorRuntimeCompilation(); var app = builder.Build(); // Configure the HTTP request pipeline. if (!app.Environment.IsDevelopment()) { app.UseExceptionHandler("/Home/Error"); // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts. app.UseHsts(); } app.UseHttpsRedirection(); app.UseStaticFiles(); app.UseRouting(); app.UseAuthentication();; app.UseAuthorization(); app.MapRazorPages(); app.MapControllerRoute( name: "default", pattern: "{area=Customer}/{controller=Home}/{action=Index}/{id?}"); app.Run(); ``` 完成以上步驟後,執行應用程式,點擊註冊按鈕,如果能正常顯示畫面跟資料庫的Role的表有新增我們剛剛增加的角色,那就代表成功了。 ![](https://i.imgur.com/5B97Hy2.png) #### 7-4-4 編輯註冊頁面 開啟AspDotNetDemo/Areas/Identity/Pages/Account/Register.cshtml/Register.cshtml.cs,並修改程式碼 ```C#= namespace AspDotNetDemo.Areas.Identity.Pages.Account { public class RegisterModel : PageModel { . . . public class InputModel { . . . /// <summary> /// This API supports the ASP.NET Core Identity default UI infrastructure and is not intended to be used /// directly from your code. This API may change or be removed in future releases. /// </summary> [DataType(DataType.Password)] [Display(Name = "Confirm password")] [Compare("Password", ErrorMessage = "The password and confirmation password do not match.")] public string ConfirmPassword { get; set; } // 本次新增部分 [Required] public string Name { get; set; } public string? StreetAddress { get; set; } public string? City { get; set; } public string? State { get; set; } public string? PostalCode { get; set; } public string? PhoneNumber { get; set; } } . . . public async Task<IActionResult> OnPostAsync(string returnUrl = null) { returnUrl ??= Url.Content("~/"); ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList(); if (ModelState.IsValid) { var user = CreateUser(); await _userStore.SetUserNameAsync(user, Input.Email, CancellationToken.None); await _emailStore.SetEmailAsync(user, Input.Email, CancellationToken.None); //本次新增部分 user.StreetAddress = Input.StreetAddress; user.City = Input.City; user.State = Input.State; user.PostalCode = Input.PostalCode; user.Name = Input.Name; user.PhoneNumber = Input.PhoneNumber; var result = await _userManager.CreateAsync(user, Input.Password); if (result.Succeeded) { _logger.LogInformation("User created a new account with password."); var userId = await _userManager.GetUserIdAsync(user); var code = await _userManager.GenerateEmailConfirmationTokenAsync(user); code = WebEncoders.Base64UrlEncode(Encoding.UTF8.GetBytes(code)); var callbackUrl = Url.Page( "/Account/ConfirmEmail", pageHandler: null, values: new { area = "Identity", userId = userId, code = code, returnUrl = returnUrl }, protocol: Request.Scheme); await _emailSender.SendEmailAsync(Input.Email, "Confirm your email", $"Please confirm your account by <a href='{HtmlEncoder.Default.Encode(callbackUrl)}'>clicking here</a>."); if (_userManager.Options.SignIn.RequireConfirmedAccount) { return RedirectToPage("RegisterConfirmation", new { email = Input.Email, returnUrl = returnUrl }); } else { await _signInManager.SignInAsync(user, isPersistent: false); return LocalRedirect(returnUrl); } } foreach (var error in result.Errors) { ModelState.AddModelError(string.Empty, error.Description); } } // If we got this far, something failed, redisplay form return Page(); } // 本次修改部分 private ApplicationUser CreateUser() { try { return Activator.CreateInstance<ApplicationUser>(); } catch { throw new InvalidOperationException($"Can't create an instance of '{nameof(IdentityUser)}'. " + $"Ensure that '{nameof(IdentityUser)}' is not an abstract class and has a parameterless constructor, or alternatively " + $"override the register page in /Areas/Identity/Pages/Account/Register.cshtml"); } } private IUserEmailStore<IdentityUser> GetEmailStore() { if (!_userManager.SupportsUserEmail) { throw new NotSupportedException("The default UI requires a user store with email support."); } return (IUserEmailStore<IdentityUser>)_userStore; } } } ``` 開啟AspDotNetDemo/Areas/Identity/Pages/Account/Register.cshtml,並修改程式碼 ```html= @page @model RegisterModel @{ ViewData["Title"] = "Register"; } <h1 class="pt-4">@ViewData["Title"]</h1> <div class="row pt-4"> <div class="col-md-7 row"> <form id="registerForm" class="row" asp-route-returnUrl="@Model.ReturnUrl" method="post"> <h2>Create a new account.</h2> <hr /> <div asp-validation-summary="ModelOnly" class="text-danger"></div> <div class="form-floating py-2 col-12"> <input asp-for="Input.Email" class="form-control" autocomplete="username" aria-required="true" /> <label asp-for="Input.Email"></label> <span asp-validation-for="Input.Email" class="text-danger"></span> </div> <div class="form-floating py-2 col-6"> <input asp-for="Input.Name" class="form-control" aria-required="true" /> <label asp-for="Input.Name"></label> <span asp-validation-for="Input.Name" class="text-danger"></span> </div> <div class="form-floating py-2 col-6"> <input asp-for="Input.PhoneNumber" class="form-control" aria-required="true" /> <label asp-for="Input.PhoneNumber"></label> <span asp-validation-for="Input.PhoneNumber" class="text-danger"></span> </div> <div class="form-floating py-2 col-6"> <input asp-for="Input.StreetAddress" class="form-control" aria-required="true" /> <label asp-for="Input.StreetAddress"></label> <span asp-validation-for="Input.StreetAddress" class="text-danger"></span> </div> <div class="form-floating py-2 col-6"> <input asp-for="Input.City" class="form-control" aria-required="true" /> <label asp-for="Input.City"></label> <span asp-validation-for="Input.City" class="text-danger"></span> </div> <div class="form-floating py-2 col-6"> <input asp-for="Input.State" class="form-control" aria-required="true" /> <label asp-for="Input.State"></label> <span asp-validation-for="Input.State" class="text-danger"></span> </div> <div class="form-floating py-2 col-6"> <input asp-for="Input.PostalCode" class="form-control" aria-required="true" /> <label asp-for="Input.PostalCode"></label> <span asp-validation-for="Input.PostalCode" class="text-danger"></span> </div> <div class="form-floating py-2 col-6"> <input asp-for="Input.Password" class="form-control" autocomplete="new-password" aria-required="true" /> <label asp-for="Input.Password"></label> <span asp-validation-for="Input.Password" class="text-danger"></span> </div> <div class="form-floating py-2 col-6"> <input asp-for="Input.ConfirmPassword" class="form-control" autocomplete="new-password" aria-required="true" /> <label asp-for="Input.ConfirmPassword"></label> <span asp-validation-for="Input.ConfirmPassword" class="text-danger"></span> </div> <button id="registerSubmit" type="submit" class="w-100 btn btn-primary">Register</button> </form> </div> <div class="col-md-5"> <section> <h3>Use another service to register.</h3> <hr /> @{ if ((Model.ExternalLogins?.Count ?? 0) == 0) { <div> <p> There are no external authentication services configured. See this <a href="https://go.microsoft.com/fwlink/?LinkID=532715">article about setting up this ASP.NET application to support logging in via external services</a>. </p> </div> } else { <form id="external-account" asp-page="./ExternalLogin" asp-route-returnUrl="@Model.ReturnUrl" method="post" class="form-horizontal"> <div> <p> @foreach (var provider in Model.ExternalLogins!) { <button type="submit" class="btn btn-primary" name="provider" value="@provider.Name" title="Log in using your @provider.DisplayName account">@provider.DisplayName</button> } </p> </div> </form> } } </section> </div> </div> @section Scripts { <partial name="_ValidationScriptsPartial" /> } ``` 完成後註冊頁面會如下圖。 ![](https://i.imgur.com/e94ke64.png) #### 7-4-5 建立 Role 的下拉式選單 開啟AspDotNetDemo/Areas/Identity/Pages/Account/Register.cshtml/Register.cshtml.cs,並修改程式碼 ```C#= namespace AspDotNetDemo.Areas.Identity.Pages.Account { public class RegisterModel : PageModel { . . . public class InputModel { . . . [Required] public string Name { get; set; } public string? StreetAddress { get; set; } public string? City { get; set; } public string? State { get; set; } public string? PostalCode { get; set; } public string? PhoneNumber { get; set; } // 本次新增部分 public string? Role { get; set; } [ValidateNever] public IEnumerable<SelectListItem> RoleList { get; set; } } public async Task OnGetAsync(string returnUrl = null) { if (!_roleManager.RoleExistsAsync(SD.Role_Admin).GetAwaiter().GetResult()) { _roleManager.CreateAsync(new IdentityRole(SD.Role_Admin)).GetAwaiter().GetResult(); _roleManager.CreateAsync(new IdentityRole(SD.Role_Employee)).GetAwaiter().GetResult(); _roleManager.CreateAsync(new IdentityRole(SD.Role_User_Indi)).GetAwaiter().GetResult(); _roleManager.CreateAsync(new IdentityRole(SD.Role_User_Comp)).GetAwaiter().GetResult(); } ReturnUrl = returnUrl; ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList(); // 本次新增部分 Input = new InputModel() { RoleList = _roleManager.Roles.Select(x => x.Name).Select(i => new SelectListItem { Text = i, Value = i }) }; } . . . } } ``` 開啟AspDotNetDemo/Areas/Identity/Pages/Account/Register.cshtml,找到下方ConfirmPassword的標籤,並在下方新增下拉選單 ```C#= <div class="form-floating py-2 col-6"> <input asp-for="Input.ConfirmPassword" class="form-control" autocomplete="new-password" aria-required="true" /> <label asp-for="Input.ConfirmPassword"></label> <span asp-validation-for="Input.ConfirmPassword" class="text-danger"></span> </div> <!-- 本次新增部分--> <div class="form-floating py-2 col-6"> <select asp-for="Input.Role" asp-items="@Model.Input.RoleList" class="form-select"> <option disabled selected>-Select Role-</option> </select> </div> <button id="registerSubmit" type="submit" class="w-100 btn btn-primary">Register</button> </form> ``` 完成後就會看到出現了 Role 的下拉式選單 ![](https://i.imgur.com/P9mkg2C.png) 接著要把使用者選取的 Role 寫進資料庫 開啟AspDotNetDemo/Areas/Identity/Pages/Account/Register.cshtml/Register.cshtml.cs,找到下方程式碼並修改,若有出現紅底線報錯,將滑鼠移至紅底線程式碼部分,點擊燈泡 using 套件即可解決 ```C#= public async Task<IActionResult> OnPostAsync(string returnUrl = null) { returnUrl ??= Url.Content("~/"); ExternalLogins = (await _signInManager.GetExternalAuthenticationSchemesAsync()).ToList(); if (ModelState.IsValid) { var user = CreateUser(); await _userStore.SetUserNameAsync(user, Input.Email, CancellationToken.None); await _emailStore.SetEmailAsync(user, Input.Email, CancellationToken.None); user.StreeAddress = Input.StreeAddress; user.City = Input.City; user.State = Input.State; user.PostalCode = Input.PostalCode; user.Name = Input.Name; user.PhoneNumber = Input.PhoneNumber; var result = await _userManager.CreateAsync(user, Input.Password); if (result.Succeeded) { _logger.LogInformation("User created a new account with password."); // 本次新增部分 if(Input.Role == null) { await _userManager.AddToRoleAsync(user, SD.Role_User_Cust); } else { await _userManager.AddToRoleAsync(user, Input.Role); } var userId = await _userManager.GetUserIdAsync(user); . . . } ``` * 這部分主要是在處理如果使用者沒有選取角色,我們將會預設顧客的身分給使用者 接下來測試註冊功能,欄位填完後選取 Role 送出 ![](https://i.imgur.com/0rQwvvm.png) 會發現資料庫寫入了ApplicationUser ![](https://i.imgur.com/AtbNp33.png) 開啟AspNetUserRoles資料表,這張表是使用者跟 Role 的中介表,會發現剛剛建立的使用者ID綁定了Admin ![](https://i.imgur.com/ZKyHY5I.png) ![](https://i.imgur.com/BTliyvz.png) #### 7-4-6 建立 Company 的下拉式選單 現在我們需要在使用者的資料表中新增一個CompanyId的欄位, 開啟AspDotNetDemo.Models/ApplicationUser.cs,並新增部分程式碼 ```C#= using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Mvc.ModelBinding.Validation; using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; using System.Linq; using System.Text; using System.Threading.Tasks; namespace AspDotNetDemo.Models { public class ApplicationUser : IdentityUser { [Required] public string Name { get; set; } public string? StreetAddress { get; set; } public string? City { get; set; } public string? State { get; set; } public string? PostalCode { get; set; } // 本次新增部分 public int? CompanyId { get; set; } [ForeignKey("CompanyId")] [ValidateNever] public Company Company { get; set; } } } ``` 完成後開啟套件管理主控台,確認預設專案為AspDotNetDemo.DataAccess,並輸入以下指令 ``` add-migration addCompanyIdToUser update-database ``` 完成後使用者的資料表就會新增CompanyId的欄位了。 接下來要完成當使用者身分為公司時,會出現公司的下拉式選單的功能。 首先開啟AspDotNetDemo/Areas/Identity/Pages/Account/Register.cshtml/Register.cshtml.cs,並修改程式碼,若有出現紅底線報錯,將滑鼠移至紅底線程式碼部分,點擊燈泡 using 套件即可解決 ```C#= namespace AspDotNetDemo.Areas.Identity.Pages.Account { public class RegisterModel : PageModel { private readonly SignInManager<IdentityUser> _signInManager; private readonly UserManager<IdentityUser> _userManager; private readonly IUserStore<IdentityUser> _userStore; private readonly IUserEmailStore<IdentityUser> _emailStore; private readonly ILogger<RegisterModel> _logger; private readonly IEmailSender _emailSender; private readonly RoleManager<IdentityRole> _roleManager; // 本次新增部分 private readonly IUnitOfWork _unitOfWork; public RegisterModel( UserManager<IdentityUser> userManager, IUserStore<IdentityUser> userStore, SignInManager<IdentityUser> signInManager, ILogger<RegisterModel> logger, IEmailSender emailSender, RoleManager<IdentityRole> roleManager, IUnitOfWork unitOfWork) // 本次新增部分 { _unitOfWork = unitOfWork; // 本次新增部分 _roleManager = roleManager; _userManager = userManager; _userStore = userStore; _emailStore = GetEmailStore(); _signInManager = signInManager; _logger = logger; _emailSender = emailSender; } . . [中間部分省略] . [Required] public string Name { get; set; } public string? StreetAddress { get; set; } public string? City { get; set; } public string? State { get; set; } public string? PostalCode { get; set; } public string? PhoneNumber { get; set; } public string? Role { get; set; } //本次新增部分 public int? CompanyId { get; set; } [ValidateNever] public IEnumerable<SelectListItem> RoleList { get; set; } //本次新增部分 [ValidateNever] public IEnumerable<SelectListItem> CompanyList { get; set; } } public async Task OnGetAsync(string returnUrl = null) { . . [中間部分省略] . Input = new InputModel() { RoleList = _roleManager.Roles.Select(x => x.Name).Select(i => new SelectListItem { Text = i, Value = i }), // 本次新增部分 CompanyList = _unitOfWork.Company.GetAll().Select(i => new SelectListItem { Text = i.Name, Value = i.Id.ToString() }) }; } ``` 接下來要在註冊頁面新增一個公司的下拉式選單,開啟AspDotNetDemo/Areas/Identity/Pages/Account/Register.cshtml,找到下方程式碼並新增程式碼 ```C#= . . [中間部分省略] . <div class="form-floating py-2 col-6"> <select asp-for="Input.Role" asp-items="@Model.Input.RoleList" class="form-select"> <option disabled selected>-Select Role-</option> </select> </div> <!-- 本次新增部分--> <div class="form-floating py-2 col-6"> <select asp-for="Input.CompanyId" asp-items="@Model.Input.CompanyList" class="form-select"> <option disabled selected>-Select Company-</option> </select> </div> <button id="registerSubmit" type="submit" class="w-100 btn btn-primary">Register</button> </form> </div> ``` 完成後在下拉式選單就會出現先前建立的公司選項囉。 ![](https://i.imgur.com/K8dB30V.png) ![](https://i.imgur.com/FoVpzUs.png) 但公司的下拉式選單應該在使用者選取 Role 為 Company 時才會出現,所以頁面的部分要再做修改。 開啟AspDotNetDemo/Areas/Identity/Pages/Account/Register.cshtml,並新增程式碼 ```html= <!-- 本次修改部分--> <div class="form-floating py-2 col-6"> <select asp-for="Input.CompanyId" style="display:none;" asp-items="@Model.Input.CompanyList" class="form-select"> <option disabled selected>-Select Company-</option> </select> </div> ``` 下方的部分也要修改 ```html= @section Scripts { <partial name="_ValidationScriptsPartial" /> @*本次新增部分*@ <script> $(document).ready(function() { $('#Input_Role').change(function() { var selection = $('#Input_Role Option:Selected').text(); if (selection != 'Company') { $('#Input_CompanyId').hide(); } else { $('#Input_CompanyId').show(); } }) }) </script> } ``` 完成後執行應用程式,就會發現如果使用者在註冊時選取的 Role 不為 Comapny,則下拉式選單就不會出現。 接下來要將使用者選取的 CompanyId 在註冊時寫入使用者表 開啟AspDotNetDemo/Areas/Identity/Pages/Account/Register.cshtml/Register.cshtml.cs,並修改部分程式碼 ```C#= var user = CreateUser(); await _userStore.SetUserNameAsync(user, Input.Email, CancellationToken.None); await _emailStore.SetEmailAsync(user, Input.Email, CancellationToken.None); user.StreetAddress = Input.StreetAddress; user.City = Input.City; user.State = Input.State; user.PostalCode = Input.PostalCode; user.Name = Input.Name; user.PhoneNumber = Input.PhoneNumber; // 本次新增部分 if (Input.Role == SD.Role_User_Comp) { user.CompanyId = Input.CompanyId; } ``` 完成就會發現註冊完成後,使用者表的 CompanyId 欄位出現對應的 Id 了,之後可以嘗試註冊看看公司的角色,看看所有功能是否都能符合預期。 ![](https://i.imgur.com/4XPqVNw.png)