asp.net core 6框架揭秘(從零開始學ASP.NETCoreIdentity框架)
2023-04-18 12:02:39 2
我們經常使用的各類網站和App均會涉及註冊、登錄和修改密碼等功能,登錄系統後,有些功能會提示沒有權限,甚至有些位置我們無法訪問,這些都是系統權限和認證的體現。
我們從本章及後面的章節中,將學習在ASP.NET Core應用程式中使用ASP.NET Core Identity實現安全認證相關功能所需要掌握的知識。
本章主要向讀者介紹如下內容。
什麼是ASP.NET Core Identity。如何在系統中啟用Identity服務。UserManager與SignInManager的API介紹及使用。登錄用戶的Cookie管理。21.1 ASP.NET Core Identity介紹ASP.NET Core Identity是一個會員身份系統,早期它的名字是Membership,當然那是一段「古老」的歷史,現在我們來了解全新的Identity。它允許我們創建、讀取、更新和刪除帳戶。支持帳號驗證、身份驗證、授權、恢復密碼和SMS雙因子身份驗證。它還支持微軟、Facebook和Google等第三方登錄提供商。它提供了一個豐富的API,並且這些API還可以進行大量的擴展。我們將在本書的後面實現這些功能。
添加ASP.NET Core Identity服務這裡採用的是EF Core,因為要讓我們的系統支持Identity服務,所以需要安裝它的程序包。打開NuGet管理器,安裝Microsoft.AspNetCore.Identity.EntityFrameworkCore即可。
以下是添加和配置ASP.NET Core Identity服務的步驟。
使AppDbContext繼承類IdentityDbContext,然後引入命名空間,代碼如下。
public class AppDbContext:IdentityDbContext{ //其餘代碼}
應用程式AppDbContextDbContext類必須繼承IdentityDbContext類而不是DbContext類。因為IdentityDbContext提供了管理SQL Server中的Identity表所需的所有DbSet屬性,所以將看到ASP.NET Core Identity框架中要生成的所有資料庫表。如果瀏覽IdentityDbContext類的定義(按F12鍵可以看到),則將看到它繼承自DbContext類。因此,如果類繼承自IdentityDbContext類,那麼不必顯式繼承DbContext類。配置ASP.NET Core Identity服務。在Startup類的ConfigureServices方法中,添加以下代碼行。
services.AddIdentity .AddEntityFrameworkStores;
AddIdentity方法是指為系統提供默認的用戶和角色類型的身份驗證系統。IdentityUser類由ASP.NET Core提供,包含UserName、PasswordHash和Email等屬性。這是ASP.NET Core Identity框架默認使用的類,用於管理應用程式的註冊用戶。如果讀者希望存儲有關註冊用戶的其他信息,比如性別、城市等,則需要創建一個派生自IdentityUser的自定義類。在此自定義類中添加所需的其他屬性,然後插入此類而不是內置的IdentityUser類。我們將在後面的章節中學習如何執行此操作。同樣,IdentityRole也是ASP.NET Core Identity提供的內置類,包含角色信息。使用EntityFrameWork Core從基礎SQL Server資料庫存儲和查詢註冊用戶的角色信息。使用AddEntityFrameworkStores方法,然後指定DbContext類作為泛型參數。接下來,將Authentication中間件添加到請求管道,代碼如下。
public void Configure(IApplicationBuilder app,IWebHostEnvironment env) { //如果環境是Development serve Developer Exception Page if(env.IsDevelopment) { app.UseDeveloperExceptionPage; } //否則顯示用戶友好的錯誤頁面 else if(env.IsStaging || env.IsProduction || env.IsEnvironment("UAT")) { app.UseExceptionHandler("/Error"); app.UseStatusCodePagesWithReExecute("/Error/{0}"); } //使用純靜態文件支持的中間件,而不使用帶有終端的中間件 app.UseStaticFiles; //添加驗證中間件 app.UseAuthentication; app.UseRouting; app.UseEndpoints(endpoints => { endpoints.MapControllerRoute( name:"default", pattern:"{controller=Home}/{action=Index}/{id?}"); }); }
在Startup類的Configure方法中,調用UseAuthentication方法將Authentication中間件添加到應用程式的請求處理管道中。我們希望能夠在請求到達MVC中間件之前對用戶進行身份驗證。因此,在請求處理管道的UseRouting中間件之前添加認證中間件。這很重要,因為我們之前講過中間件的添加順序不能亂。
現在開始添加身份遷移。在Visual Studio中的程序包控制臺窗口執行以下命令以添加新遷移。
Add-Migration AddingIdentity
此遷移包含用於創建ASP.NET Core Identity系統所需的表的代碼。
如果運行,則會出現以下錯誤。
The entity type'IdentityUserLogin'requires a primary key to be defined.
之前因為要封裝Seed方法,所以重寫OnModelCreating方法。出現這個錯誤是因為我們在DbContext類中重寫了OnModelCreating方法,但未調用基本IdentityDbContext類OnModelCreating方法。
Identity表的鍵映射在IdentityDbContext類的OnModelCreating方法中。因此,要解決這個錯誤,需要做的是,調用基類OnModelCreating使用該方法的基礎關鍵字,代碼如下。
public class AppDbContext:IdentityDbContext { public AppDbContext(DbContextOptions options):base(options) { } public DbSet Students{get;set;} protected override void OnModelCreating(ModelBuilder modelBuilder) { base.OnModelCreating(modelBuilder); modelBuilder.Seed; } }
執行Update-Database命令以應用遷移記錄並創建所需的身份表,如圖21.1所示。
圖21.1
21.2 使用ASP.NET Core Identity註冊新用戶現在已經創建好了表的信息,接下來我們增加一個註冊功能,讓用戶能夠註冊到系統中。
新用戶註冊視圖應如圖21.2所示。為了能夠註冊為新用戶,需要郵箱地址和密碼兩個欄位。
圖21.2
21.2.1 RegisterViewModel視圖模型我們將使用RegisterViewModel類作為Register視圖的模型,它負責將視圖中的信息傳遞給控制器。為了驗證信息是否正確,我們使用了幾個ASP.NET Core驗證屬性。在之前的章節中詳細說明過這些屬性和模型驗證。
using System.ComponentModel.DataAnnotations;namespace MockSchoolManagement.ViewModels{ public class RegisterViewModel { [Required] [EmailAddress] [Display(Name = "郵箱地址")] public string Email{get;set;} [Required] [DataType(DataType.Password)] [Display(Name = "密碼")] public string Password{get;set;} [DataType(DataType.Password)] [Display(Name = "確認密碼")] [Compare("Password", ErrorMessage = "密碼與確認密碼不一致,請重新輸入.")] public string ConfirmPassword{get;set;} }}
在這裡我們添加了DataType特性,它的主要作用是指定比資料庫內部類型更具體的數據類型。DataType枚舉提供了多種數據類型,比如日期、時間、電話號碼、貨幣和郵箱地址等。但是請注意,DataType特性不提供任何驗證,它主要服務於我們的視圖文件,比如,DataType.EmailAddress可以在視圖中創建mailto:連結,DataType.Date則會在支持HTML5的瀏覽器中提供日期選擇器。
21.2.2 帳戶控制器帳戶控制器(accountController)是指所有與帳戶相關的CRUD(增加、讀取、更新和刪除)操作都將在此控制器中。目前我們只有Register操作方法,可以通過向/account/register發出GET請求來實現此操作方法。
using Microsoft.AspNetCore.Mvc;namespace MockSchoolManagement.Controllers{ public class AccountController:Controller { [HttpGet] public IActionResult Register { return View; } }}
21.2.3 註冊視圖中的代碼將此視圖放在Views/Account文件夾中,此視圖的模型是我們在前面創建的Register ViewModel。
@model RegisterViewModel@{ViewBag.Title = "用戶註冊";}用戶註冊 註冊
21.2.4 添加註冊按鈕在布局視圖中添加註冊按鈕,我們需要在_Layout.cshtml文件中找到ID為collapsibleNavbar的導航菜單欄,在下方添加註冊按鈕,導航到對應的視圖,代碼如下。
學生列表 添加學生 註冊
運行項目後,單擊註冊按鈕即可看到圖21.2所示的效果圖,接下來我們實現處理HttpPOST請求到/account/register的Register操作方法。然後通過表單Taghelpers將數據發布到ASP.NET Core Identity中創建帳戶。
21.3 UserManager和SignInManager服務在本節我們學習使用ASP.NET Core Identity提供的UserManager服務創建新用戶,然後使用其提供的SignInManager服務來登錄用戶。
UserManager 類包含管理基礎數據存儲中的用戶所需的方法。比如,此類具有CreateAsync、DeleteAsync和UpdateAsync等方法來創建、刪除和更新用戶,如圖21.3所示。
圖21.3
SignInManager 類包含用戶登錄所需的方法。比如,SignInManager類具有SignInAsync、SignOutAsync等方法來登錄和註銷用戶,如圖21.4所示。
UserManager和SignInManager服務都需要使用構造函數注入AccountController,並且這兩個服務都接收泛型參數。這些服務接收泛型參數的User類。目前,我們使用內置的IdentityUser類作為泛型參數的參數。這兩個服務的通用參數User是一個擴展類。這意味著,我們可以自定義與用戶有關的信息和其他數據,來創建我們的自定義用戶。圖21.4
我們可以聲明自己的自定義類作為泛型參數,而不是內置的IdentityUser類。以下是AccountController的完整代碼。
using Microsoft.AspNetCore.Identity;using Microsoft.AspNetCore.Mvc;using MockSchoolManagement.ViewModels;using System.Threading.Tasks;namespace MockSchoolManagement.Controllers{ public class AccountController:Controller { private UserManager _userManager; private SignInManager _signInManager; public AccountController(UserManager userManager, SignInManager signInManager) { this._userManager = userManager; this._signInManager = signInManager; } [HttpGet] public IActionResult Register { return View; } [HttpPost] public async Task Register(RegisterViewModel model) { if(ModelState.IsValid) { //將數據從RegisterViewModel複製到IdentityUser var user = new IdentityUser { UserName = model.Email, Email = model.Email }; //將用戶數據存儲在AspNetUsers資料庫表中 var result = await _userManager.CreateAsync(user,model.Password); //如果成功創建用戶,則使用登錄服務登錄用戶信息 //並重定向到HomeController的索引操作 if(result.Succeeded) { await _signInManager.SignInAsync(user,isPersistent:false); return RedirectToAction("index","home"); } //如果有任何錯誤,則將它們添加到ModelState對象中 //將由驗證摘要標記助手顯示到視圖中 foreach(var error in result.Errors) { ModelState.AddModelError(string.Empty,error.Description); } } return View(model); } }}
此時,如果讀者運行項目並提供有效的郵箱地址和密碼,則它會在SQL Server資料庫的AspNetUsers表中創建帳戶。讀者可以從Visual Studio的SQL Server對象資源管理器中查看此數據,如圖21.5所示。
圖21.5
21.3.1 ASP.NET Core Identity中對密碼複雜度的處理在剛剛註冊的時候,我們發現有兩個問題。
密碼驗證機制太複雜了。它是英文的,對於我們來說支持不是很友好。這是因為ASP.NET Core IdentityOptions類在ASP.NET Core中用於配置密碼複雜性規則。默認情況下,ASP.NET Core身份不允許創建簡單的密碼來保護我們的應用程式免受自動暴力攻擊。
當我們嘗試使用像abc這樣的簡單密碼註冊新帳戶時,會顯示創建失敗,讀者將看到如圖21.6所示的驗證錯誤。
圖21.6
我們在圖21.6中看到中文提示,後面的章節會告訴讀者如何配置。
21.3.2 ASP.NET Core Identity密碼默認設置在ASP.NET Core Identity中,密碼默認設置在PasswordOptions類中。讀者可以在ASP.NET Core GitHub倉庫中找到此類的原始碼。只需在倉庫中搜索PasswordOptions類。
代碼如下。
public class PasswordOptions{ public int RequiredLength{get;set;} = 6; public int RequiredUniqueChars{get;set;} = 1; public bool RequireNonAlphanumeric{get;set;} = true; public bool RequireLowercase{get;set;} = true; public bool RequireUppercase{get;set;} = true; public bool RequireDigit{get;set;} = true;}
相關參數的說明如表21.1(略)所示。
21.3.3 覆蓋ASP.NET Core身份中的密碼默認設置我們可以通過在Startup類的ConfigureServices方法中使用IServiceCollection接口的Configure方法來實現這一點。
services.Configure(options =>{ options.Password.RequiredLength = 6; options.Password.RequiredUniqueChars = 3; options.Password.RequireNonAlphanumeric = false; options.Password.RequireLowercase = false; options.Password.RequireUppercase = false;});
也可以在添加身份服務時執行此操作,代碼如下。
services.AddIdentity(options =>{ options.Password.RequiredLength = 6; options.Password.RequiredUniqueChars = 3; options.Password.RequireNonAlphanumeric = false;}).AddEntityFrameworkStores;
當然,在這裡推薦使用IdentityOptions的形式進行配置,因為它可以作為一個獨立服務,而不是嵌套在AddIdentity方法中。
IdentityOptions對象中除了Password的配置信息,還有用戶、登錄、策略等配置信息,我們可以根據不同的場景進行靈活的配置。
UserOptions。SignInOptions。LockoutOptions。TokenOptions。StoreOptions。ClaimsIdentityOptions。21.3.4 修改中文提示的錯誤信息Identity提供了AddErrorDescriber方法,可方便我們進行錯誤內容的配置和處理。
ASP.NET Core默認提供的都是英文提示,我們可以將它們修改為中文。現在我們創建一個CustomIdentityErrorDescriber的類文件,路徑為根目錄下創建的CustomerMiddlewares文件夾,然後繼承IdentityErrorDescriber服務,添加以下代碼。
public class CustomIdentityErrorDescriber:IdentityErrorDescriber { public override IdentityError DefaultError { return new IdentityError{Code = nameof(DefaultError),Description = $"發生了未知的故障。" }; } public override IdentityError ConcurrencyFailure { return new IdentityError{Code = nameof(ConcurrencyFailure),Description = "樂觀並發失敗,對象已被修改。" }; } public override IdentityError PasswordMismatch { return new IdentityError{Code = nameof(PasswordMismatch),Description = "密碼錯誤" }; } public override IdentityError InvalidToken { return new IdentityError{Code = nameof(InvalidToken),Description = "無效的令牌." }; } public override IdentityError LoginAlreadyAssociated { return new IdentityError{Code = nameof(LoginAlreadyAssociated),Description = "具有此登錄的用戶已經存在." }; } public override IdentityError InvalidUserName(string userName) { return new IdentityError{Code = nameof(InvalidUserName),Description = $"用戶名'{userName}'無效,只能包含字母或數字." }; } public override IdentityError InvalidEmail(string email) { return new IdentityError{Code = nameof(InvalidEmail),Description = $"郵箱'{email}'無效." }; } public override IdentityError DuplicateUserName(string userName) { return new IdentityError{Code = nameof(DuplicateUserName),Description = $"用戶名'{userName}'已被使用." }; } public override IdentityError DuplicateEmail(string email) { return new IdentityError{Code = nameof(DuplicateEmail),Description = $"郵箱'{email}'已被使用." }; } public override IdentityError InvalidRoleName(string role) { return new IdentityError{Code = nameof(InvalidRoleName),Description = $"角色名'{role}'無效." }; } public override IdentityError DuplicateRoleName(string role) { return new IdentityError{Code = nameof(DuplicateRoleName),Description = $"角色名'{role}'已被使用." }; } public override IdentityError UserAlreadyHasPassword { return new IdentityError{Code = nameof(UserAlreadyHasPassword),Description = "該用戶已設置了密碼." }; } public override IdentityError UserLockoutNotEnabled { return new IdentityError{Code = nameof(UserLockoutNotEnabled),Description = "此用戶未啟用鎖定." }; } public override IdentityError UserAlreadyInRole(string role) { return new IdentityError{Code = nameof(UserAlreadyInRole),Description = $"用戶已關聯角色'{role}'." }; } public override IdentityError UserNotInRole(string role) { return new IdentityError{Code = nameof(UserNotInRole),Description = $"用戶未關聯角色'{role}'." }; } public override IdentityError PasswordTooShort(int length) { return new IdentityError{Code = nameof(PasswordTooShort),Description = $"密碼必須至少是{length}字符." }; } public override IdentityError PasswordRequiresNonAlphanumeric { return new IdentityError { Code = nameof(PasswordRequiresNonAlphanumeric), Description = "密碼必須至少有一個非字母數字字符." }; } public override IdentityError PasswordRequiresDigit { return new IdentityError{Code = nameof(PasswordRequiresDigit),Description = $"密碼必須至少有一個數字('0'-'9')." }; } public override IdentityError PasswordRequiresUniqueChars(int uniqueChars) { return new IdentityError{Code = nameof(PasswordRequiresUniqueChars),Description = $"密碼必須使用至少不同的{uniqueChars}字符。" }; } public override IdentityError PasswordRequiresLower { return new IdentityError{Code = nameof(PasswordRequiresLower),Description = "密碼必須至少有一個小寫字母('a'-'z')." }; } public override IdentityError PasswordRequiresUpper { return new IdentityError{Code = nameof(PasswordRequiresUpper),Description = "密碼必須至少有一個大寫字母('A'-'Z')." }; } }
回到Startup類的ConfigureServices方法中,在AddIdentity服務中使用AddErrorDescriber方法覆蓋默認的錯誤提示內容,代碼如下。
services.AddIdentity.AddErrorDescriber.AddEntityFrameworkStores;
配置完成之後,提示變為中文,註冊時密碼長度達到6位即可。
21.4 登錄狀態及註銷功能的實現在本節中我們學習如何判斷用戶是否登錄,以及註冊、登錄和註銷等功能是否可實現。
首先來看一看如何在ASP.NET Core中實現註銷功能。如果用戶未登錄,則顯示登錄和註冊按鈕,如圖21.7所示。
圖21.7
如果用戶已登錄,請隱藏登錄和註冊按鈕並顯示註銷按鈕,如圖21.8所示。
圖21.8
我們需要在_Layout.cshtml文件中找到ID為collapsibleNavbar的導航菜單欄,修改代碼如下。
在下方代碼中注入了SignInManager,以便我們檢查用戶是否已登錄,來決定顯示和隱藏的內容。
@using Microsoft.AspNetCore.Identity @inject SignInManager _signInManager 學生列表 添加學生 @*如果用戶已登錄,則顯示註銷連結*@ @if(_signInManager.IsSignedIn(User)) { 註銷 @User.Identity.Name }else{ 註冊 登錄 }
然後在AccountController中添加以下Logout方法。
[HttpPost] public async Task Logout { await _signInManager.SignOutAsync; return RedirectToAction("index","home"); }
請注意,我們使用POST請求將用戶註銷,而不使用GET請求,因為該方法可能會被濫用。惡意者可能會誘騙用戶單擊某張圖片,將圖片的src屬性設置為應用程式註銷URL,這樣會造成用戶在不知不覺中退出了帳戶。
21.5 ASP.NET Core Identity中的登錄功能實現在本節中,我們將討論使用ASP.NET Core Identity的API在ASP.NET Core應用程式中實現登錄功能。要在ASP.NET Core應用程式中實現登錄功能,我們需要實現以下功能。
登錄視圖模型。登錄視圖。AccountController中的兩個Login操作方法。21.5.1 LoginViewModel登錄視圖模型要在系統中登錄用戶,則需要其郵箱、用戶名、密碼以及使其選擇是否需要持久性Cookie或會話Cookie。
public class LoginViewModel { [Required] [EmailAddress] public string Email{get;set;} [Required] [DataType(DataType.Password)] public string Password{get;set;} [Display(Name = "記住我")] public bool RememberMe{get;set;} }
21.5.2 登錄視圖的代碼登錄視圖的代碼如下。
@model LoginViewModel@{ViewBag.Title = "用戶登錄";}用戶登錄 @Html.DisplayNameFor(m => m.RememberMe) 登錄
21.5.3 AccountController中的Login操作方法using Microsoft.AspNetCore.Identity;using Microsoft.AspNetCore.Mvc;using MockSchoolManagement.ViewModels;using System.Threading.Tasks;namespace MockSchoolManagement.Controllers{ public class AccountController:Controller { private UserManager _userManager; private SignInManager _signInManager; public AccountController(UserManager userManager, SignInManager signInManager) { this._userManager = userManager; this._signInManager = signInManager; } [HttpGet] public IActionResult Login { return View; } [HttpPost] public async Task Login(LoginViewModel model) { if(ModelState.IsValid) { var result = await _signInManager.PasswordSignInAsync( model.Email,model.Password,model.RememberMe,false); if(result.Succeeded) { return RedirectToAction("index","home"); } ModelState.AddModelError(string.Empty,"登錄失敗,請重試"); } return View(model); } }}
21.5.4 會話Cookie與持久性Cookie維基百科解釋:Cookie並不是它的原意「甜餅」的意思,而是一個保存在客戶機中的簡單的文本文件,這個文件與特定的Web文檔關聯在一起,保存了該客戶機訪問這個Web文檔時的信息,當客戶機再次訪問這個Web文檔時這些信息可供該文檔使用。由於「Cookie」具有可以保存在客戶機上的神奇特性,因此它可以幫助我們實現記錄用戶個人信息的功能,而這一切都不必使用複雜的CGI等程序。
簡單來說,我們把Cookie理解為一個大小不超過4kB,便於我們在客戶端保存一些用戶個人信息的功能。
在ASP.NET Core Identity中,用戶成功登錄後,將發出Cookie,並將此Cookie隨每個請求一起發送到伺服器,伺服器會解析此Cookie信息來了解用戶是否已經通過身份驗證和登錄。此Cookie可以是會話Cookie或持久Cookie。
會話Cookie是指用戶登錄成功後,Cookie會被創建並存儲在瀏覽器會話實例中。會話Cookie不包含過期時間,它會在瀏覽器窗口關閉時被永久刪除。
持久Cookie是指用戶登錄成功後,Cookie會被創建並存儲在瀏覽器中,因為是持久Cookie,所以在關閉瀏覽器窗口後,它不會被刪除。但是,它通常有一個到期時間,會在到期後被刪除。
在LoginViewModel.cs視圖模型中,我們已經添加了一個bool類型的RememberMe屬性。用戶可在登錄時選擇記住我,選中即使用持久性Cookie,而未選中則為會話Cookie。
現在運行項目,我們可以在登錄的時候選擇記住我,登錄成功後如圖21.9所示。
圖21.9
打開開發者工具(按F12鍵),觀察圖21.9框中的內容,可以發現過期時間是很長的。現在關閉瀏覽器,並將其再次打開,用戶也依然是登錄狀態。這便是持久性Cookie的作用,只有在到期時間到了之後才會刪除。
至於會話Cookie驗證,我們在登錄的時候取消選擇記住我,然後看到如圖21.10所示的內容。
圖21.10
這裡已經是一個會話了,它不包含過期時間,在關閉瀏覽器後,再次將其打開,系統會自動註銷用戶。
以上就是持久性Cookie與會話Cookie的區別了。
在本章中我們學習了Identity的基本功能,創建一個系統用戶並完成了登錄註冊及狀態檢查。在後面的章節中,內容會逐步深入,可配合原始碼學習。
21.6 小結本章介紹了ASP.NET Core Identity框架的定位及作用,並利用它提供的API完成了用戶的登錄與註銷等基本功能。在後面的章節中我們會使用更多的API將系統趨於完善。
本文摘自《深入淺出 ASP.NET Core》
這本書原本的計劃是描述EF Core中的知識點,帶領讀者完整地做一個管理系統。但是個人覺得這樣寫與市場上的其他圖書沒有什麼區別,它就是一本概述知識點的圖書,無非多了一個較為完整的功能系統而已。對於我而言這是有落差的。有一天和朋友吃飯,他建議把ABP中那些有效的、目前市場上流行的設計理念整合進圖書,不用講解得太明白,只是告訴讀者如何用以及這麼用的好處即可。
本書的結構本書分為以下5個部分。
第一部分(第1章~第9章)介紹ASP.NET Core的基礎知識,比如中間件、環境變量和配置信息等,簡單講解完整的ASP.NET Core的項目結構。第二部分(第10章~第20章)介紹並運用MVC模型及路由中間件,結合ASP.NET Core提供的TagHelper等新特性,完成對學生信息的增刪改查、圖片上傳;介紹簡單的倉儲模式與依賴注入的關係,為搭建管理系統做好基礎準備。第三部分(第21章~第29章) 通過搭建一個基礎管理系統,分析及處理實際業務場景中的常見問題,比如身份驗證和授權、客戶端及服務端驗證、配置信息、 EF Core數據訪問、數據分頁和統一異常處理等。第四部分(第30章~第38章) 介紹架構的作用以及意義,根據架構的思想應用設計模式,結合C#泛型特性優化倉儲模式,建立多層體系架構,通過並發、 LINQ及活用Entity Framework Core中的常用功能完成一個類似領域驅動設計的項目。第五部分(第39章~第42章)介紹簡單的Web API入門、部署ASP.NET Core項目以及從ASP .NET Core 2.2到ASP.NET Core 3.1的版本升級過程。,