1. 程式人生 > >學習ASP.NET Core(06)-Restful與WebAPI

學習ASP.NET Core(06)-Restful與WebAPI

上一篇我們使用Swagger添加了介面文件,使用Jwt完成了授權,本章我們簡答介紹一下RESTful風格的WebAPI開發過程中涉及到的一些知識點,並完善一下尚未完成的功能 --- .NET下的WebAPI是一種無限接近RESTful風格的框架,RESTful風格它有著自己的一套理論,它的大概意思就是說使用標準的Http方法,將Web系統的服務抽象為資源。稍微具體一點的介紹可以檢視阮一峰的這篇文章[RESTful API最佳實踐](http://www.ruanyifeng.com/blog/2018/10/restful-api-best-practices.html "點選檢視")。我們這裡就分幾部分介紹一下構建RESTful API需要了解的基礎知識 注:本章介紹部分的內容大多是基於solenovex的[使用 ASP.NET Core 3.x 構建 RESTful Web API](https://www.bilibili.com/video/BV1XJ411q7yy "點選檢視")視訊內容的整理,若想進一步瞭解相關知識,請檢視原視訊 ## 一、HTTP方法 ### 1、什麼是HTTP方法 HTTP方法是對Web伺服器的說明,說明如何處理請求的資源。HTTP1.0 定義了三種請求方法: GET, POST 和 HEAD方法;HTTP1.1 新增了六種請求方法:OPTIONS、PUT、PATCH、DELETE、TRACE 和 CONNECT 方法。 ### 2、常用的HTTP方法 1. GET:通常用來獲取資源;GET請求會返回請求路徑對應的資源,但它分兩種情況: ①獲取單個資源,通過使用URL的形式帶上唯一標識,示例:api/Articles/{ArticleId}; ②獲取集合資源中符合條件的資源,會通過QueryString的形式在URL後面新增**?查詢條件**作為篩選條件,示例:api/Articles?title=WebAPI 2. POST:通常用來建立資源;POST的引數會放在請求body中,POST請求應該返回新建立的資源以及可以獲取該資源的唯一標識URL,示例:api/Articles/{新增的ArticleId} 3. DELETE:通常用來移除/刪除對應路徑的資源;通過使用URL的形式帶上唯一標識,或者和GET一樣使用QueryString,處理完成後通常不會返回資源,只返回狀態碼204,示例:api/Articles/{ArticleId}; 4. PUT:通常用來**完全替換**對應路徑的資源資訊;POST的引數會放在請求body中,且為一個完整物件,示例:api/Articles/{ArticleId};與此同時,它分兩類情況: ①對應的資源不存在,則新增對應的資源,後續處理和POST一樣; ②對應的資源存在,則替換對應的資源,處理完成不需要返回資訊,只返回狀態碼204 5. PATCH:通常用來更新對應路徑資源的**區域性資訊**;PATCH的引數會放在請求頭中,處理完成後通常不會返回資源,只返回狀態碼204,示例:api/Articles/{ArticleId}; 綜上:給出一張圖例,來自**solenovex,[使用 ASP.NET Core 3.x 構建 RESTful Web API](https://www.bilibili.com/video/BV1XJ411q7yy "點選檢視")** ![](https://img2020.cnblogs.com/blog/2019059/202005/2019059-20200519212807764-179716436.png) ### 3、安全性和冪等性 安全性是指方法執行後不會改變資源的表述;冪等性是指方法無論執行多少次都會得到相同的結果 ![](https://img2020.cnblogs.com/blog/2019059/202005/2019059-20200519212818254-1062774711.png) ## 二、狀態碼相關 ### 1、狀態碼 HTTP狀態碼是表示Web伺服器響應狀態的3位數字程式碼。通常會以第一位數字為區分 1xx:屬於資訊性的狀態碼,WebAPI不使用 2xx:表示請求執行成功,常用的有200—請求成功,201—建立成功,204—請求成功無返回資訊,如刪除 3xx:用於跳轉,如告訴搜尋引擎,網址已改變。大多數WebAPI不需要使用這類狀態碼 4xx:表示**客戶端錯誤** - 400:Bad Request,表示API使用者傳送到伺服器的請求存在錯誤; - 401:Unauthorized,表示沒有提供授權資訊,或者授權資訊不正確; - 403:Forbidden,表示身份認證成功,但是無許可權訪問請求的資源 - 404:Not Found,表示請求的資源不存在 - 405:Method not allowed,表示使用了不被支援的HTTP方法 - 406:Not Acceptable,表示API使用者請求的格式WebAPI不支援,且WebAPI不提供預設的表述格式 - 409:Conflict,表示衝突,一般用來表述併發問題,如修改資源期間,資源被已經被更新了 - 415:Unsupported Media Type,**與406相反**,表示伺服器接受的資源WebAPI不支援 - 422:Unprocessable Entity,表示伺服器已經解析了內容,但是無法處理,如實體驗證錯誤 5xx:表示**伺服器錯誤** - 500:INternal Server Error:表示伺服器發生了錯誤,客戶端無法處理 ### 2、錯誤與故障 基於HTTP請求狀態碼,我們需要了解一下錯誤和故障的區別 錯誤:API正常工作,但是API使用者請求傳遞的資料不合理,所以請求被拒絕。對應4xx錯誤; 故障:API工作異常,API使用者請求是合理的,但是API無法響應。對應5xx錯誤 ### 3、故障處理 我們可以在非開發環境進行如下配置,以確保生產環境異常時能檢視到相關異常說明,通常這裡會寫入日誌記錄異常,我們會在後面的章節新增日誌功能,這裡先修改如下: ![](https://img2020.cnblogs.com/blog/2019059/202005/2019059-20200519212839985-184583839.png) ## 三、WebAPI相關 ### 1、內容協商 1. 什麼是內容協商?即當有多種表述格式(Json/Xml等)可用時,選取最佳的一個進行表述。簡單來說就是請求什麼格式,服務端就返回什麼格式的資料; 2. 如何設定內容協商?首先我們要從服務端的角度區分**輸出和輸入**,輸出表示客戶端發出請求服務端響應資料;輸入表示客戶端提交資料服務端對其進行處理;舉例來說,Get就是輸出,Post就是輸入 - 先看輸出:在Http請求的Header中有一個**Accept Header**屬性,如該屬性設定的是application/json,那麼API返回的就應該是Json格式的;在ASP.NET Core中負責響應輸出格式的就是**Output Formatters**物件 - 再看輸入:HTTP請求的輸入格式對應的是**Content-Type Header**屬性,ASP.NET Core中負責響應輸入格式的就是**Input Formatters**物件 PS:如果沒有設定請求格式,就返回預設格式;而如果請求的格式不存在,則應當返回406狀態碼; ### 2、內容協商設定 ASP.NET Core目前的設定是僅返回Json格式資訊,不支援XML;如果請求的是XML或沒有設定,它同樣會返回Json;如果希望關閉此項設定,即不存在返回406狀態碼,可以在Controller服務註冊時新增如下設定; 而如果希望支援輸出和輸入都支援XML格式,可以配置如下: ![](https://img2020.cnblogs.com/blog/2019059/202005/2019059-20200519212852867-1557889937.png) ### 3、物件繫結 客戶端資料可以通過多種方式傳遞給API,Binding Source Attribute則是負責處理繫結的物件,它會為告知Model的繫結引擎,從哪裡可以找到繫結源,Binding Source Attribute一共有六種繫結資料來源,如下: - **FromBody**:從請求的Body中獲取繫結資料 - **FromForm**:從請求的Body中的form獲取繫結資料 - **FromHeader**:從請求的Header中獲取繫結資料 - **FromQuery**:從QueryString引數中獲取繫結資料 - **FromRoute**:從當前請求的路由中獲取繫結資料 - **FromService**:從作為Action引數而注入的服務中獲取繫結資料 ### 4、ApiController特性 ASP.NET Core WebAPI中我們通常會使用**[ApiController]特性**來修飾我們的Controller物件,該特性為了更好的適應API方法,對上述分類規則進行了修改,修改如下: - FormBody:通常用來推斷複雜型別的引數 - FromForm:通常用來推斷IFormFilr和IFormFileColllection型別的Action引數,即檔案上傳相對應的引數 - FromRoute:通常用來推斷Action的引數名和路由模板中的引數名一致的情況 - FromQuery:用來推斷其他的Action引數 一些特殊情況,需要手動指明物件的來源,如在HttpGet方法中,查詢引數是一個複雜的類型別,則ApiController物件會預設繫結源為請求body, 這時候就需要手動指明繫結源為FromQuery; ### 5、輸入驗證 通常我們會使用一些驗證規則對客戶端的輸入內容進行限制,像使用者名稱不能包含特殊字元,使用者名稱長度等 #### 1、驗證規則 WebAPI中內建了一組名為Data Annotations的驗證規則,像之前我們新增的[Required],[StringLength...]都屬於這個型別。或者我們可以自定義一個類,實現**IValidatableObject介面**,對多個欄位進行限制;當然我們也可以針對類或者是屬性自定義一些驗證規則,需要**繼承ValidationAttribute類重寫IsValid方法** #### 2、驗證檢查 檢查時會使用ModelState物件,它是一個字典,包含model的狀態和model的繫結驗證資訊;同時它還包含針對每個提交的屬性值的錯誤資訊的集合,每當有請求進來的時候,定義好的驗證規則就會被檢查。如果驗證不通過,ModelState.IsValid()就會返回false; #### 3、報告驗證錯誤 如發生驗證錯誤,應當返回Unprocessable Entity 422錯誤,並在響應的body中包含驗證錯誤資訊;ASP.NET Core已經定義好了這部分內容,當Controller使用[ApiController]屬性進行註解時,如果遇到錯誤,那麼將會自返回400錯誤狀態碼 ## 四、完成Controller基礎功能 controller功能的實現是大多基於對BLL層的引用,雖然我們在第3小結中已經實現了資料層和邏輯層的基礎功能,但在Controller實現時還是發現了很多不合理的地方,所以調整了很多內容,下面我們依次來看一下 ### 1、UserController 1、首先對Model的層進行了調整,調整了出生日期和性別的預設值 ```c# using System; using System.ComponentModel.DataAnnotations; namespace BlogSystem.Model { /// /// 使用者 /// public class User : BaseEntity { /// /// 賬戶 /// [Required, StringLength(40)] public string Account { get; set; } /// /// 密碼 /// [Required, StringLength(200)] public string Password { get; set; } /// /// 頭像 ///
public string ProfilePhoto { get; set; } /// /// 出生日期 /// public DateTime BirthOfDate { get; set; } = DateTime.Today; /// /// 性別 /// public Gender Gender { get; set; } = Gender.保密; /// /// 使用者等級 /// public Level Level { get; set; } = Level.普通使用者; /// /// 粉絲數 ///
public int FansNum { get; set; } /// /// 關注數 /// public int FocusNum { get; set; } } } ``` 對ViewModel進行了調整,如下: ```c# using System.ComponentModel.DataAnnotations; namespace BlogSystem.Model.ViewModels { /// /// 使用者註冊 /// public class RegisterViewModel { /// /// 賬號 ///
[Required, StringLength(40, MinimumLength = 4)] [RegularExpression(@"/^([\u4e00-\u9fa5]{2,4})|([A-Za-z0-9_]{4,16})|([a-zA-Z0-9_\u4e00-\u9fa5]{3,16})$/")] public string Account { get; set; } /// /// 密碼 /// [Required, StringLength(20, MinimumLength = 6)] public string Password { get; set; } /// /// 確認密碼 /// [Required, Compare(nameof(Password))] public string RequirePassword { get; set; } } } ``` ```c# using System.ComponentModel.DataAnnotations; namespace BlogSystem.Model.ViewModels { /// /// 使用者登入 /// public class LoginViewModel { /// /// 使用者名稱稱 /// [Required, StringLength(40, MinimumLength = 4)] [RegularExpression(@"/^([\u4e00-\u9fa5]{2,4})|([A-Za-z0-9_]{4,16})|([a-zA-Z0-9_\u4e00-\u9fa5]{3,16})$/")] public string Account { get; set; } /// /// 使用者密碼 /// [Required, StringLength(20, MinimumLength = 6), DataType(DataType.Password)] public string Password { get; set; } } } ``` ```C# using System.ComponentModel.DataAnnotations; namespace BlogSystem.Model.ViewModels { /// /// 修改使用者密碼 /// public class ChangePwdViewModel { /// /// 舊密碼 /// [Required] public string OldPassword { get; set; } /// /// 新密碼 /// [Required] public string NewPassword { get; set; } /// /// 確認新密碼 /// [Required, Compare(nameof(NewPassword))] public string RequirePassword { get; set; } } } ``` ```c# using System; using System.ComponentModel.DataAnnotations; namespace BlogSystem.Model.ViewModels { /// /// 修改使用者資料 /// public class ChangeUserInfoViewModel { /// /// 賬號 /// public string Account { get; set; } /// /// 出生日期 /// [DataType(DataType.Date)] public DateTime BirthOfDate { get; set; } /// /// 性別 /// public Gender Gender { get; set; } } } ``` ```c# namespace BlogSystem.Model.ViewModels { /// /// 使用者詳細資訊 /// public class UserDetailsViewModel { /// /// 賬號 /// public string Account { get; set; } /// /// 頭像 /// public string ProfilePhoto { get; set; } /// /// 年齡 /// public int Age { get; set; } /// /// 性別 /// public string Gender { get; set; } /// /// 使用者等級 /// public string Level { get; set; } /// /// 粉絲數 /// public int FansNum { get; set; } /// /// 關注數 /// public int FocusNum { get; set; } } } ``` 2、IBLL和BLL層調整如下: ```c# using System; using BlogSystem.Model; using BlogSystem.Model.ViewModels; using System.Threading.Tasks; namespace BlogSystem.IBLL { /// /// 使用者服務介面 /// public interface IUserService : IBaseService { /// /// 註冊 /// /// /// Task Register(RegisterViewModel model); /// /// 登入成功返回userId /// /// /// Task Login(LoginViewModel model); /// /// 修改使用者密碼 /// /// /// /// Task ChangePassword(ChangePwdViewModel model, Guid userId); /// /// 修改使用者頭像 /// /// /// /// Task ChangeUserPhoto(string profilePhoto, Guid userId); /// /// 修改使用者資訊 /// /// /// /// Task ChangeUserInfo(ChangeUserInfoViewModel model, Guid userId); /// /// 使用account獲取使用者資訊 /// /// /// Task GetUserInfoByAccount(string account); } } ``` ```c# using BlogSystem.Common.Helpers; using BlogSystem.IBLL; using BlogSystem.IDAL; using BlogSystem.Model; using BlogSystem.Model.ViewModels; using Microsoft.EntityFrameworkCore; using System; using System.Linq; using System.Threading.Tasks; namespace BlogSystem.BLL { public class UserService : BaseService, IUserService { private readonly IUserRepository _userRepository; public UserService(IUserRepository userRepository) { _userRepository = userRepository; BaseRepository = userRepository; } /// /// 使用者註冊 /// /// /// public async Task Register(RegisterViewModel model) { //判斷賬戶是否存在 if (await _userRepository.GetAll().AnyAsync(m => m.Account == model.Account)) { return false; } var pwd = Md5Helper.Md5Encrypt(model.Password); await _userRepository.CreateAsync(new User { Account = model.Account, Password = pwd }); return true; } /// /// 使用者登入 /// /// /// public async Task Login(LoginViewModel model) { var pwd = Md5Helper.Md5Encrypt(model.Password); var user = await _userRepository.GetAll().FirstOrDefaultAsync(m => m.Account == model.Account && m.Password == pwd); return user == null ? new Guid() : user.Id; } /// /// 修改使用者密碼 /// /// /// /// public async Task ChangePassword(ChangePwdViewModel model, Guid userId) { var oldPwd = Md5Helper.Md5Encrypt(model.OldPassword); var user = await _userRepository.GetAll().FirstOrDefaultAsync(m => m.Id == userId && m.Password == oldPwd); if (user == null) { return false; } var newPwd = Md5Helper.Md5Encrypt(model.NewPassword); user.Password = newPwd; await _userRepository.EditAsync(user); return true; } /// /// 修改使用者照片 /// /// /// /// public async Task ChangeUserPhoto(string profilePhoto, Guid userId) { var user = await _userRepository.GetAll().FirstOrDefaultAsync(m => m.Id == userId); if (user == null) return false; user.ProfilePhoto = profilePhoto; await _userRepository.EditAsync(user); return true; } /// /// 修改使用者資訊 /// /// /// /// public async Task ChangeUserInfo(ChangeUserInfoViewModel model, Guid userId) { //確保使用者名稱唯一 if (await _userRepository.GetAll().AnyAsync(m => m.Account == model.Account)) { return false; } var user = await _userRepository.GetOneByIdAsync(userId); user.Account = model.Account; user.Gender = model.Gender; user.BirthOfDate = model.BirthOfDate; await _userRepository.EditAsync(user); return true; } /// /// 通過賬號名稱獲取使用者資訊 /// /// /// public async Task GetUserInfoByAccount(string account) { if (await _userRepository.GetAll().AnyAsync(m => m.Account == account)) { return await _userRepository.GetAll().Where(m => m.Account == account).Select(m => new UserDetailsViewModel() { Account = m.Account, ProfilePhoto = m.ProfilePhoto, Age = DateTime.Now.Year - m.BirthOfDate.Year, Gender = m.Gender.ToString(), Level = m.Level.ToString(), FansNum = m.FansNum, FocusNum = m.FocusNum }).FirstAsync(); } return new UserDetailsViewModel(); } } } ``` 3、Controller層功能的實現大多數需要基於UserId,我們怎麼獲取UserId呢?還記得Jwt嗎?客戶端傳送請求時會在Header中帶上Jwt字串,我們可以解析該字串得到使用者名稱。在自定義的JwtHelper中我們實現了兩個方法,一個是加密Jwt,一個是解密Jwt,我們對解密方法進行調整,如下: ```c# /// /// Jwt解密 /// /// /// public static TokenModelJwt JwtDecrypt(string jwtStr) { if (string.IsNullOrEmpty(jwtStr) || string.IsNullOrWhiteSpace(jwtStr)) { return new TokenModelJwt(); } jwtStr = jwtStr.Substring(7);//擷取前面的Bearer和空格 var jwtHandler = new JwtSecurityTokenHandler(); JwtSecurityToken jwtToken = jwtHandler.ReadJwtToken(jwtStr); jwtToken.Payload.TryGetValue(ClaimTypes.Role, out object level); var model = new TokenModelJwt { UserId = Guid.Parse(jwtToken.Id), Level = level == null ? "" : level.ToString() }; return model; } ``` 在對應的Contoneller中我們可以使用HttpContext物件獲取Http請求的資訊,但是HttpContext的使用是需要註冊的,在StartUp的ConfigureServices中進行註冊,`services.AddHttpContextAccessor();`之後在對應的控制器建構函式中進行注入IHttpContextAccessor物件即可,如下: ```c# using BlogSystem.Core.Helpers; using BlogSystem.IBLL; using BlogSystem.Model.ViewModels; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using System; using System.Threading.Tasks; namespace BlogSystem.Core.Controllers { [ApiController] [Route("api/user")] public class UserController : ControllerBase { private readonly IUserService _userService; private readonly Guid _userId; public UserController(IUserService userService, IHttpContextAccessor httpContext) { _userService = userService ?? throw new ArgumentNullException(nameof(userService)); var accessor = httpContext ?? throw new ArgumentNullException(nameof(httpContext)); _userId = JwtHelper.JwtDecrypt(accessor.HttpContext.Request.Headers["Authorization"]).UserId; } /// /// 使用者註冊 /// /// /// [HttpPost(nameof(Register))] public async Task Register(RegisterViewModel model) { if (!await _userService.Register(model)) { return Ok("使用者已存在"); } //建立成功返回到登入方法,並返回註冊成功的account return CreatedAtRoute(nameof(Login), model.Account); } /// /// 使用者登入 /// /// /// [HttpPost("Login", Name = nameof(Login))] public async Task Login(LoginViewModel model) { //判斷賬號密碼是否正確 var userId = await _userService.Login(model); if (userId == Guid.Empty) return Ok("賬號或密碼錯誤!"); //登入成功進行jwt加密 var user = await _userService.GetOneByIdAsync(userId); TokenModelJwt tokenModel = new TokenModelJwt { UserId = user.Id, Level = user.Level.ToString() }; var jwtStr = JwtHelper.JwtEncrypt(tokenModel); return Ok(jwtStr); } /// /// 獲取使用者資訊 /// /// /// [HttpGet("{account}")] public async Task UserInfo(string account) { var list = await _userService.GetUserInfoByAccount(account); if (string.IsNullOrEmpty(list.Account)) { return NotFound(); } return Ok(list); } /// /// 修改使用者密碼 /// /// /// [Authorize] [HttpPatch("password")] public async Task ChangePassword(ChangePwdViewModel model) { if (!await _userService.ChangePassword(model, _userId)) { return NotFound("使用者密碼錯誤!"); } return NoContent(); } /// /// 修改使用者照片 /// /// /// [Authorize] [HttpPatch("photo")] public async Task ChangeUserPhoto([FromBody]string profilePhoto) { if (!await _userService.ChangeUserPhoto(profilePhoto, _userId)) { return NotFound(); } return NoContent(); } /// /// 修改使用者資訊 /// /// /// [Authorize] [HttpPatch("info")] public async Task ChangeUserInfo(ChangeUserInfoViewModel model) { if (!await _userService.ChangeUserInfo(model, _userId)) { return Ok("使用者名稱已存在"); } return NoContent(); } } } ``` ### 2、分類Controller 1、調整ViewModel層如下: ```c# using System; using System.ComponentModel.DataAnnotations; namespace BlogSystem.Model.ViewModels { /// /// 編輯分類 /// public class EditCategoryViewModel { /// /// 分類Id /// public Guid CategoryId { get; set; } /// /// 分類名稱 /// [Required, StringLength(30, MinimumLength = 2)] public string CategoryName { get; set; } } } ``` ```c# using System; using System.ComponentModel.DataAnnotations; namespace BlogSystem.Model.ViewModels { /// /// 分類列表 /// public class CategoryListViewModel { /// /// 分類Id /// public Guid CategoryId { get; set; } /// /// 分類名稱 /// [Required, StringLength(30, MinimumLength = 2)] public string CategoryName { get; set; } } } ``` ```C# using System; using System.ComponentModel.DataAnnotations; namespace BlogSystem.Model.ViewModels { /// /// 建立文章分類 /// public class CreateCategoryViewModel { /// /// 分類Id /// public Guid CategoryId { get; set; } /// /// 分類名稱 /// [Required, StringLength(30, MinimumLength = 2)] public string CategoryName { get; set; } } } ``` 2、調整IBLL和BLL層如下: ```c# using BlogSystem.Model; using System; using System.Collections.Generic; using System.Threading.Tasks; using BlogSystem.Model.ViewModels; namespace BlogSystem.IBLL { /// /// 分類服務介面 /// public interface ICategoryService : IBaseService { /// /// 建立分類 /// /// /// /// Task CreateCategory(string categoryName, Guid userId); /// /// 編輯分類 /// /// /// /// Task EditCategory(EditCategoryViewModel model, Guid userId); /// /// 通過使用者Id獲取所有分類 /// /// /// Task> GetCategoryByUserIdAsync(Guid userId); } } ``` ```c# using BlogSystem.IBLL; using BlogSystem.IDAL; using BlogSystem.Model; using BlogSystem.Model.ViewModels; using Microsoft.EntityFrameworkCore; using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; namespace BlogSystem.BLL { public class CategoryService : BaseService, ICategoryService { private readonly ICategoryRepository _categoryRepository; public CategoryService(ICategoryRepository categoryRepository) { _categoryRepository = categoryRepository; BaseRepository = categoryRepository; } /// /// 建立分類 /// /// /// /// public async Task CreateCategory(string categoryName, Guid userId) { //當前使用者存在該分類名稱則返回 if (string.IsNullOrEmpty(categoryName) || await _categoryRepository.GetAll() .AnyAsync(m => m.UserId == userId && m.CategoryName == categoryName)) { return Guid.Empty; } //建立成功返回分類Id var categoryId = Guid.NewGuid(); await _categoryRepository.CreateAsync(new Category { Id = categoryId, UserId = userId, CategoryName = categoryName }); return categoryId; } /// /// 編輯分類 /// /// /// /// public async Task EditCategory(EditCategoryViewModel model, Guid userId) { //使用者不存在該分類則返回 if (!await _categoryRepository.GetAll().AnyAsync(m => m.UserId == userId && m.Id == model.CategoryId)) { return false; } await _categoryRepository.EditAsync(new Category { UserId = userId, Id = model.CategoryId, CategoryName = model.CategoryName }); return true; } /// /// 通過使用者Id獲取所有分類 /// /// /// public Task> GetCategoryByUserIdAsync(Guid userId) { return _categoryRepository.GetAll().Where(m => m.UserId == userId).Select(m => new CategoryListViewModel { CategoryId = m.Id, CategoryName = m.CategoryName }).ToListAsync(); } } } ``` 3、調整Controller功能如下: ```c# using BlogSystem.Core.Helpers; using BlogSystem.IBLL; using BlogSystem.Model.ViewModels; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using System; using System.Threading.Tasks; namespace BlogSystem.Core.Controllers { [ApiController] [Route("api/category")] public class CategoryController : ControllerBase { private readonly ICategoryService _categoryService; private readonly IArticleService _aeArticleService; private readonly Guid _userId; public CategoryController(ICategoryService categoryService, IArticleService articleService, IHttpContextAccessor httpContext) { _categoryService = categoryService ?? throw new ArgumentNullException(nameof(categoryService)); _aeArticleService = articleService ?? throw new ArgumentNullException(nameof(articleService)); var accessor = httpContext ?? throw new ArgumentNullException(nameof(httpContext)); _userId = JwtHelper.JwtDecrypt(accessor.HttpContext.Request.Headers["Authorization"]).UserId; } /// /// 查詢使用者的文章分類 /// /// /// [HttpGet("{userId}", Name = nameof(GetCategoryByUserId))] public async Task GetCategoryByUserId(Guid userId) { if (userId == Guid.Empty) { return NotFound(); } var list = await _categoryService.GetCategoryByUserIdAsync(userId); return Ok(list); } /// /// 新增文章分類 /// /// /// [Authorize] [HttpPost] public async Task CreateCategory([FromBody]string categoryName) { var categoryId = await _categoryService.CreateCategory(categoryName, _userId); if (categoryId == Guid.Empty) { return BadRequest("重複分類!"); } //建立成功返回查詢頁面連結 var category = new CreateCategoryViewModel { CategoryId = categoryId, CategoryName = categoryName }; return CreatedAtRoute(nameof(GetCategoryByUserId), new { userId = _userId }, category); } /// /// 刪除分類 /// /// /// [Authorize] [HttpDelete("{categoryId}")] public async Task RemoveCategory(Guid categoryId) { //確認是否存在,操作人與歸屬人是否一致 var category = await _categoryService.GetOneByIdAsync(categoryId); if (category == null || category.UserId != _userId) { return NotFound(); } //有文章使用了該分類,無法刪除 var data = await _aeArticleService.GetArticlesByCategoryIdAsync(_userId, categoryId); if (data.Count > 0) { return BadRequest("存在使用該分類的文章!"); } await _categoryService.RemoveAsync(categoryId); return NoContent(); } /// /// 編輯分類 /// /// /// [Authorize] [HttpPatch] public async Task EditCategory(EditCategoryViewModel model) { if (!await _categoryService.EditCategory(model, _userId)) { return NotFound(); } return NoContent(); } } } ``` ### 3、文章Controller 1、這裡我在操作時遇到了文章內容亂碼的問題,可能是因為資料庫的text格式和輸入格式有衝突,所以這裡我暫時將其改成了nvarchar(max)的型別 ```c# using System; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; namespace BlogSystem.Model { /// /// 文章 /// public class Article : BaseEntity { /// /// 文章標題 /// [Required] public string Title { get; set; } /// /// 文章內容 /// [Required] public string Content { get; set; } /// /// 發表人的Id,使用者表的外來鍵 /// [ForeignKey(nameof(User))] public Guid UserId { get; set; } public User User { get; set; } /// /// 看好人數 /// public int GoodCount { get; set; } /// /// 不看好人數 /// public int BadCount { get; set; } /// /// 文章檢視所需等級 /// public Level Level { get; set; } = Level.普通使用者; } } ``` ViewModel調整如下: ```c# using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; namespace BlogSystem.Model.ViewModels { /// /// 建立文章 /// public class CreateArticleViewModel { /// /// 文章標題 /// [Required] public string Title { get; set; } /// /// 文章內容 /// [Required] public string Content { get; set; } /// /// 文章分類 /// [Required] public List CategoryIds { get; set; } } } ``` ```c# using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; namespace BlogSystem.Model.ViewModels { /// /// 編輯文章 /// public class EditArticleViewModel { /// /// 文章Id /// public Guid Id { get; set; } /// /// 文章標題 /// [Required] public string Title { get; set; } /// /// 文章內容 /// [Required] public string Content { get; set; } /// /// 文章分類 /// public List CategoryIds { get; set; } } } ``` ```C# using System; namespace BlogSystem.Model.ViewModels { /// /// 文章列表 /// public class ArticleListViewModel { /// /// 文章Id /// public Guid ArticleId { get; set; } /// /// 文章標題 /// public string Title { get; set; } /// /// 文章內容 /// public string Content { get; set; } /// /// 建立時間 /// public DateTime CreateTime { get; set; } /// /// 賬號 /// public string Account { get; set; } /// /// 頭像 /// public string ProfilePhoto { get; set; } } } ``` ```c# using System; using System.Collections.Generic; namespace BlogSystem.Model.ViewModels { /// /// 文章詳情 /// public class ArticleDetailsViewModel { /// /// 文章Id /// public Guid Id { get; set; } /// /// 文章標題 /// public string Title { get; set; } /// /// 文章內容 /// public string Content { get; set; } /// /// 建立時間 /// public DateTime CreateTime { get; set; } /// /// 作者 /// public string Account { get; set; } /// /// 頭像 /// public string ProfilePhoto { get; set; } /// /// 分類Id /// public List CategoryIds { get; set; } /// /// 分類名稱 /// public List CategoryNames { get; set; } /// /// 看好人數 /// public int GoodCount { get; set; } /// /// 不看好人數 /// public int BadCount { get; set; } } } ``` 2、調整IBLL和BLL內容,如下 ```c# using BlogSystem.Model; using System; using System.Collections.Generic; using System.Threading.Tasks; using BlogSystem.Model.ViewModels; namespace BlogSystem.IBLL { /// /// 評論服務介面 /// public interface ICommentService : IBaseService { /// /// 新增評論 /// /// /// /// /// Task CreateComment(CreateCommentViewModel model, Guid articleId, Guid userId); /// /// 新增普通評論的回覆 /// /// /// /// /// /// Task CreateReplyComment(CreateApplyCommentViewModel model, Guid articleId, Guid commentId, Guid userId); /// /// 添加回複評論的回覆 /// /// /// /// /// /// Task CreateToReplyComment(CreateApplyCommentViewModel model, Guid articleId, Guid commentId, Guid userId); /// /// 通過文章Id獲取所有評論 /// /// /// Task> GetCommentsByArticleIdAsync(Guid articleId); /// /// 確認回覆型評論是否存在 /// /// /// Task ReplyExistAsync(Guid commentId); } } ``` ```c# using BlogSystem.IBLL; using BlogSystem.IDAL; using BlogSystem.Model; using BlogSystem.Model.ViewModels; using Microsoft.EntityFrameworkCore; using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; namespace BlogSystem.BLL { public class CommentService : BaseService, ICommentService { private readonly IArticleCommentRepository _commentRepository; private readonly ICommentReplyRepository _commentReplyRepository; public CommentService(IArticleCommentRepository commentRepository, ICommentReplyRepository commentReplyRepository) { _commentRepository = commentRepository; BaseRepository = commentRepository; _commentReplyRepository = commentReplyRepository; } /// /// 新增評論 /// /// /// /// /// public async Task CreateComment(CreateCommentViewModel model, Guid articleId, Guid userId) { await _commentRepository.CreateAsync(new ArticleComment() { ArticleId = articleId, Content = model.Content, UserId = userId }); } /// /// 新增普通評論的回覆 /// /// /// /// /// /// public async Task CreateReplyComment(CreateApplyCommentViewModel model, Guid articleId, Guid commentId, Guid userId) { var comment = await _commentRepository.GetOneByIdAsync(commentId); var toUserId = comment.UserId; await _commentReplyRepository.CreateAsync(new CommentReply() { CommentId = commentId, ToUserId = toUserId, ArticleId = articleId, UserId = userId, Content = model.Content }); } /// /// 添加回復型評論的回覆 /// /// /// /// /// /// public async Task CreateToReplyComment(CreateApplyCommentViewModel model, Guid articleId, Guid commentId, Guid userId) { var comment = await _commentReplyRepository.GetOneByIdAsync(commentId); var toUserId = comment.UserId; await _commentReplyRepository.CreateAsync(new CommentReply() { CommentId = commentId, ToUserId = toUserId, ArticleId = articleId, UserId = userId, Content = model.Content }); } /// /// 根據文章Id獲取評論資訊 /// /// /// public async Task> GetCommentsByArticleIdAsync(Guid articleId) { //正常評論 var comment = await _commentRepository.GetAll().Where(m => m.ArticleId == articleId) .Include(m => m.User).Select(m => new CommentListViewModel { ArticleId = m.ArticleId, UserId = m.UserId, Account = m.User.Account, ProfilePhoto = m.User.ProfilePhoto, CommentId = m.Id, CommentContent = m.Content, CreateTime = m.CreateTime }).ToListAsync(); //回覆型的評論 var replyComment = await _commentReplyRepository.GetAll().Where(m => m.ArticleId == articleId) .Include(m => m.User).Select(m => new CommentListViewModel { ArticleId = m.ArticleId, UserId = m.UserId, Account = m.User.Account, ProfilePhoto = m.User.ProfilePhoto, CommentId = m.Id, CommentContent = $"@{m.ToUser.Account}" + Environment.NewLine + m.Content, CreateTime = m.CreateTime }).ToListAsync(); var list = comment.Union(replyComment).OrderByDescending(m => m.CreateTime).ToList(); return list; } /// /// 確認回覆型評論是否存在 /// /// /// public async Task ReplyExistAsync(Guid commentId) { return await _commentReplyRepository.GetAll().AnyAsync(m => m.Id == commentId); } } } ``` 3、調整Controller如下: ```c# using BlogSystem.Core.Helpers; using BlogSystem.IBLL; using BlogSystem.Model.ViewModels; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using System; using System.Threading.Tasks; namespace BlogSystem.Core.Controllers { [ApiController] [Route("api/Article/{articleId}/Comment")] public class CommentController : ControllerBase { private readonly ICommentService _commentService; private readonly IArticleService _articleService; private readonly Guid _userId; public CommentController(ICommentService commentService, IArticleService articleService, IHttpContextAccessor httpContext) { _commentService = commentService ?? throw new ArgumentNullException(nameof(commentService)); _articleService = articleService ?? throw new ArgumentNullException(nameof(articleService)); var accessor = httpContext ?? throw new ArgumentNullException(nameof(httpContext)); _userId = JwtHelper.JwtDecrypt(accessor.HttpContext.Request.Headers["Authorization"]).UserId; } /// /// 新增評論 /// /// /// /// [Authorize] [HttpPost] public async Task CreateComment(Guid articleId, CreateCommentViewModel model) { if (!await _articleService.ExistsAsync(articleId)) { return NotFound(); } await _commentService.CreateComment(model, articleId, _userId); return CreatedAtRoute(nameof(GetComments), new { articleId }, model); } /// /// 添加回復型評論 /// /// /// /// /// [Authorize] [HttpPost("reply")] public async Task CreateReplyComment(Guid articleId, Guid commentId, CreateApplyCommentViewModel model) { if (!await _articleService.ExistsAsync(articleId)) { return NotFound(); } //回覆的是正常評論 if (await _commentService.ExistsAsync(commentId)) { await _commentService.CreateReplyComment(model, articleId, commentId, _userId); return CreatedAtRoute(nameof(GetComments), new { articleId }, model); } //需要考慮回覆的是正常評論還是回覆型評論 if (await _commentService.ReplyExistAsync(commentId)) { await _commentService.CreateToReplyComment(model, articleId, commentId, _userId); return CreatedAtRoute(nameof(GetComments), new { articleId }, model); } return NotFound(); } /// /// 獲取評論 /// /// /// [HttpGet(Name = nameof(GetComments))] public async Task GetComments(Guid articleId) { if (!await _articleService.ExistsAsync(articleId)) { return NotFound(); } var list = await _commentService.GetCommentsByArticleIdAsync(articleId); return Ok(list); } } } ``` ### 4、評論Controller 1、這裡發現評論回覆表CommentReply設計存在問題,因為回覆也有可能是針對回覆型評論的,所以調整之後需要使用EF的遷移命令更行資料庫,如下: ```c# using System; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; namespace BlogSystem.Model { /// /// 評論回覆表 /// public class CommentReply : BaseEntity { /// /// 回覆指向的評論Id /// public Guid CommentId { get; set; } /// /// 回覆指向的使用者Id /// [ForeignKey(nameof(ToUser))] public Guid ToUserId { get; set; } public User ToUser { get; set; } /// /// 文章ID /// [ForeignKey(nameof(Article))] public Guid ArticleId { get; set; } public Article Article { get; set; } /// /// 使用者Id /// [ForeignKey(nameof(User))] public Guid UserId { get; set; } public User User { get; set; } /// /// 回覆的內容 /// [Required, StringLength(800)] public string Content { get; set; } } } ``` 調整ViewModel如下,有人發現評論和回覆的ViewModel相同,為什麼不使用一個?是為了應對後續兩張表欄位不同時,需要調整的情況 ```c# using System; using System.ComponentModel.DataAnnotations; namespace BlogSystem.Model.ViewModels { /// /// 文章評論 /// public class CreateCommentViewModel { /// /// 評論內容 /// [Required, StringLength(800)] public string Content { get; set; } } } ``` ```c# using System.ComponentModel.DataAnnotations; namespace BlogSystem.Model.ViewModels { /// /// 添加回復型評論 /// public class CreateApplyCommentViewModel { /// /// 回覆的內容 /// [Required, StringLength(800)] public string Content { get; set; } } } ``` ```c# using System; namespace BlogSystem.Model.ViewModels { /// /// 文章評論列表 /// public class CommentListViewModel { /// /// 文章Id /// public Guid ArticleId { get; set; } /// /// 使用者Id /// public Guid UserId { get; set; } /// /// 賬號 /// public string Account { get; set; } /// /// 頭像 /// public string ProfilePhoto { get; set; } /// /// 評論Id /// public Guid CommentId { get; set; } /// /// 評論內容 /// public string CommentContent { get; set; } /// /// 建立時間 /// public DateTime CreateTime { get; set; } } } ``` 2、調整IBLL和BLL如下: ```C# using BlogSystem.Model; using System; using System.Collections.Generic; using System.Threading.Tasks; using BlogSystem.Model.ViewModels; namespace BlogSystem.IBLL { /// /// 評論服務介面 /// public interface ICommentService : IBaseService { /// /// 新增評論 /// /// /// /// /// Task CreateComment(CreateCommentViewModel model, Guid articleId, Guid userId); /// /// 新增普通評論的回覆 /// /// /// /// /// /// Task CreateReplyComment(CreateApplyCommentViewModel model, Guid articleId, Guid commentId, Guid userId); /// /// 添加回複評論的回覆 /// /// /// /// /// /// Task CreateToReplyComment(CreateApplyCommentViewModel model, Guid articleId, Guid commentId, Guid userId); /// /// 通過文章Id獲取所有評論 /// /// /// Task> GetCommentsByArticleIdAsync(Guid articleId); /// /// 確認回覆型評論是否存在 /// /// /// Task ReplyExistAsync(Guid commentId); } } ``` ```c# using BlogSystem.IBLL; using BlogSystem.IDAL; using BlogSystem.Model; using BlogSystem.Model.ViewModels; using Microsoft.EntityFrameworkCore; using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; namespace BlogSystem.BLL { public class CommentService : BaseService, ICommentService { private readonly IArticleCommentRepository _commentRepository; private readonly ICommentReplyRepository _commentReplyRepository; public CommentService(IArticleCommentRepository commentRepository, ICommentReplyRepository commentReplyRepository) { _commentRepository = commentRepository; BaseRepository = commentRepository; _commentReplyRepository = commentReplyRepository; } /// /// 新增評論 /// /// /// /// /// public async Task CreateComment(CreateCommentViewModel model, Guid articleId, Guid userId) { await _commentRepository.CreateAsync(new ArticleComment() { ArticleId = articleId, Content = model.Content, UserId = userId }); } /// /// 新增普通評論的回覆 /// /// /// /// /// /// public async Task CreateReplyComment(CreateApplyCommentViewModel model, Guid articleId, Guid commentId, Guid userId) { var comment = await _commentRepository.GetOneByIdAsync(commentId); var toUserId = comment.UserId; await _commentReplyRepository.CreateAsync(new CommentReply() { CommentId = commentId, ToUserId = toUserId, ArticleId = articleId, UserId = userId, Content = model.Content }); } /// /// 添加回復型評論的回覆 /// /// /// /// /// /// public async Task CreateToReplyComment(CreateApplyCommentViewModel model, Guid articleId, Guid commentId, Guid userId) { var comment = await _commentReplyRepository.GetOneByIdAsync(commentId); var toUserId = comment.UserId; await _commentReplyRepository.CreateAsync(new CommentReply() { CommentId = commentId, ToUserId = toUserId, ArticleId = articleId, UserId = userId, Content = model.Content }); } /// /// 根據文章Id獲取評論資訊 /// /// /// public async Task> GetCommentsByArticleIdAsync(Guid articleId) { //正常評論 var comment = await _commentRepository.GetAll().Where(m => m.ArticleId == articleId) .Include(m => m.User).Select(m => new CommentListViewModel { ArticleId = m.ArticleId, UserId = m.UserId, Account = m.User.Account, ProfilePhoto = m.User.ProfilePhoto, CommentId = m.Id, CommentContent = m.Content, CreateTime = m.CreateTime }).ToListAsync(); //回覆型的評論 var replyComment = await _commentReplyRepository.GetAll().Where(m => m.ArticleId == articleId) .Include(m => m.User).Select(m => new CommentListViewModel { ArticleId = m.ArticleId, UserId = m.UserId, Account = m.User.Account, ProfilePhoto = m.User.ProfilePhoto, CommentId = m.Id, CommentContent = $"@{m.ToUser.Account}" + Environment.NewLine + m.Content, CreateTime = m.CreateTime }).ToListAsync(); var list = comment.Union(replyComment).OrderByDescending(m => m.CreateTime).ToList(); return list; } /// /// 確認回覆型評論是否存在 /// /// /// public async Task ReplyExistAsync(Guid commentId) { return await _commentReplyRepository.GetAll().AnyAsync(m => m.Id == commentId); } } } ``` 3、調整Controller如下: ```c# using BlogSystem.Core.Helpers; using BlogSystem.IBLL; using BlogSystem.Model.ViewModels; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Mvc; using System; using System.Threading.Tasks; namespace BlogSystem.Core.Controllers { [ApiController] [Route("api/Article/{articleId}/Comment")] public class CommentController : ControllerBase { private readonly ICommentService _commentService; private readonly IArticleService _articleService; private readonly Guid _userId; public CommentController(ICommentService commentService, IArticleService articleService, IHttpContextAccessor httpContext) { _commentService = commentService ?? throw new ArgumentNullException(nameof(commentService)); _articleService = articleService ?? throw new ArgumentNullException(nameof(articleService)); var accessor = httpContext ?? throw new ArgumentNullException(nameof(httpContext)); _userId = JwtHelper.JwtDecrypt(accessor.HttpContext.Request.Headers["Authorization"]).UserId; } /// /// 新增評論 /// /// /// /// [Authorize] [HttpPost] public async Task CreateComment(Guid articleId, CreateCommentViewModel model) { if (!await _articleService.ExistsAsync(articleId)) { return NotFound(); } await _commentService.CreateComment(model, articleId, _userId); return CreatedAtRoute(nameof(GetComments), new { articleId }, model); } /// /// 添加回復型評論 /// /// /// /// /// [Authorize] [HttpPost("reply")] public async Task CreateReplyComment(Guid articleId, Guid commentId, CreateApplyCommentViewModel model) { if (!await _articleService.ExistsAsync(articleId)) { return NotFound(); } //回覆的是正常評論 if (await _commentService.ExistsAsync(commentId)) { await _commentService.CreateReplyComment(model, articleId, commentId, _userId); return CreatedAtRoute(nameof(GetComments), new { articleId }, model); } //需要考慮回覆的是正常評論還是回覆型評論 if (await _commentService.ReplyExistAsync(commentId)) { await _commentService.CreateToReplyComment(model, articleId, commentId, _userId); return CreatedAtRoute(nameof(GetComments), new { articleId }, model); }