上一篇咱們使用Swagger添加了接口文檔,使用Jwt完成了受權,本章咱們簡答介紹一下RESTful風格的WebAPI開發過程當中涉及到的一些知識點,並完善一下還沒有完成的功能html
.NET下的WebAPI是一種無限接近RESTful風格的框架,RESTful風格它有着本身的一套理論,它的大概意思就是說使用標準的Http方法,將Web系統的服務抽象爲資源。稍微具體一點的介紹能夠查看阮一峯的這篇文章RESTful API最佳實踐。咱們這裏就分幾部分介紹一下構建RESTful API須要瞭解的基礎知識vue
注:本章介紹部分的內容大可能是基於solenovex的使用 ASP.NET Core 3.x 構建 RESTful Web API視頻內容的整理,若想進一步瞭解相關知識,請查看原視頻git
HTTP方法是對Web服務器的說明,說明如何處理請求的資源。HTTP1.0 定義了三種請求方法: GET, POST 和 HEAD方法;HTTP1.1 新增了六種請求方法:OPTIONS、PUT、PATCH、DELETE、TRACE 和 CONNECT 方法。github
GET:一般用來獲取資源;GET請求會返回請求路徑對應的資源,但它分兩種狀況:web
①獲取單個資源,經過使用URL的形式帶上惟一標識,示例:api/Articles/{ArticleId};數據庫
②獲取集合資源中符合條件的資源,會經過QueryString的形式在URL後面添加?查詢條件做爲篩選條件,示例:api/Articles?title=WebAPIjson
POST:一般用來建立資源;POST的參數會放在請求body中,POST請求應該返回新建立的資源以及能夠獲取該資源的惟一標識URL,示例:api/Articles/{新增的ArticleId}c#
DELETE:一般用來移除/刪除對應路徑的資源;經過使用URL的形式帶上惟一標識,或者和GET同樣使用QueryString,處理完成後一般不會返回資源,只返回狀態碼204,示例:api/Articles/{ArticleId};後端
PUT:一般用來徹底替換對應路徑的資源信息;POST的參數會放在請求body中,且爲一個完整對象,示例:api/Articles/{ArticleId};與此同時,它分兩類狀況:api
①對應的資源不存在,則新增對應的資源,後續處理和POST同樣;
②對應的資源存在,則替換對應的資源,處理完成不須要返回信息,只返回狀態碼204
PATCH:一般用來更新對應路徑資源的局部信息;PATCH的參數會放在請求頭中,處理完成後一般不會返回資源,只返回狀態碼204,示例:api/Articles/{ArticleId};
綜上:給出一張圖例,來自solenovex,使用 ASP.NET Core 3.x 構建 RESTful Web API
安全性是指方法執行後不會改變資源的表述;冪等性是指方法不管執行多少次都會獲得相同的結果
HTTP狀態碼是表示Web服務器響應狀態的3位數字代碼。一般會以第一位數字爲區分
1xx:屬於信息性的狀態碼,WebAPI不使用
2xx:表示請求執行成功,經常使用的有200—請求成功,201—建立成功,204—請求成功無返回信息,如刪除
3xx:用於跳轉,如告訴搜索引擎,網址已改變。大多數WebAPI不須要使用這類狀態碼
4xx:表示客戶端錯誤
5xx:表示服務器錯誤
基於HTTP請求狀態碼,咱們須要瞭解一下錯誤和故障的區別
錯誤:API正常工做,可是API用戶請求傳遞的數據不合理,因此請求被拒絕。對應4xx錯誤;
故障:API工做異常,API用戶請求是合理的,可是API沒法響應。對應5xx錯誤
咱們能夠在非開發環境進行以下配置,以確保生產環境異常時能查看到相關異常說明,一般這裏會寫入日誌記錄異常,咱們會在後面的章節添加日誌功能,這裏先修改以下:
PS:若是沒有設置請求格式,就返回默認格式;而若是請求的格式不存在,則應當返回406狀態碼;
ASP.NET Core目前的設置是僅返回Json格式信息,不支持XML;若是請求的是XML或沒有設置,它一樣會返回Json;若是但願關閉此項設置,即不存在返回406狀態碼,能夠在Controller服務註冊時添加以下設置;
而若是但願支持輸出和輸入都支持XML格式,能夠配置以下:
客戶端數據能夠經過多種方式傳遞給API,Binding Source Attribute則是負責處理綁定的對象,它會爲告知Model的綁定引擎,從哪裏能夠找到綁定源,Binding Source Attribute一共有六種綁定數據來源,以下:
ASP.NET Core WebAPI中咱們一般會使用[ApiController]特性來修飾咱們的Controller對象,該特性爲了更好的適應API方法,對上述分類規則進行了修改,修改以下:
一些特殊狀況,須要手動指明對象的來源,如在HttpGet方法中,查詢參數是一個複雜的類類型,則ApiController對象會默認綁定源爲請求body, 這時候就須要手動指明綁定源爲FromQuery;
一般咱們會使用一些驗證規則對客戶端的輸入內容進行限制,像用戶名不能包含特殊字符,用戶名長度等
WebAPI中內置了一組名爲Data Annotations的驗證規則,像以前咱們添加的[Required],[StringLength...]都屬於這個類型。或者咱們能夠自定義一個類,實現IValidatableObject接口,對多個字段進行限制;固然咱們也能夠針對類或者是屬性自定義一些驗證規則,須要繼承ValidationAttribute類重寫IsValid方法
檢查時會使用ModelState對象,它是一個字典,包含model的狀態和model的綁定驗證信息;同時它還包含針對每一個提交的屬性值的錯誤信息的集合,每當有請求進來的時候,定義好的驗證規則就會被檢查。若是驗證不經過,ModelState.IsValid()就會返回false;
如發生驗證錯誤,應當返回Unprocessable Entity 422錯誤,並在響應的body中包含驗證錯誤信息;ASP.NET Core已經定義好了這部份內容,當Controller使用[ApiController]屬性進行註解時,若是遇到錯誤,那麼將會自返回400錯誤狀態碼
controller功能的實現是大多基於對BLL層的引用,雖然咱們在第3小結中已經實現了數據層和邏輯層的基礎功能,但在Controller實現時仍是發現了不少不合理的地方,因此調整了不少內容,下面咱們依次來看一下
一、首先對Model的層進行了調整,調整了出生日期和性別的默認值
using System; using System.ComponentModel.DataAnnotations; namespace BlogSystem.Model { /// <summary> /// 用戶 /// </summary> public class User : BaseEntity { /// <summary> /// 帳戶 /// </summary> [Required, StringLength(40)] public string Account { get; set; } /// <summary> /// 密碼 /// </summary> [Required, StringLength(200)] public string Password { get; set; } /// <summary> /// 頭像 /// </summary> public string ProfilePhoto { get; set; } /// <summary> /// 出生日期 /// </summary> public DateTime BirthOfDate { get; set; } = DateTime.Today; /// <summary> /// 性別 /// </summary> public Gender Gender { get; set; } = Gender.保密; /// <summary> /// 用戶等級 /// </summary> public Level Level { get; set; } = Level.普通用戶; /// <summary> /// 粉絲數 /// </summary> public int FansNum { get; set; } /// <summary> /// 關注數 /// </summary> public int FocusNum { get; set; } } }
對ViewModel進行了調整,以下:
using System.ComponentModel.DataAnnotations; namespace BlogSystem.Model.ViewModels { /// <summary> /// 用戶註冊 /// </summary> public class RegisterViewModel { /// <summary> /// 帳號 /// </summary> [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; } /// <summary> /// 密碼 /// </summary> [Required, StringLength(20, MinimumLength = 6)] public string Password { get; set; } /// <summary> /// 確認密碼 /// </summary> [Required, Compare(nameof(Password))] public string RequirePassword { get; set; } } }
using System.ComponentModel.DataAnnotations; namespace BlogSystem.Model.ViewModels { /// <summary> /// 用戶登陸 /// </summary> public class LoginViewModel { /// <summary> /// 用戶名稱 /// </summary> [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; } /// <summary> /// 用戶密碼 /// </summary> [Required, StringLength(20, MinimumLength = 6), DataType(DataType.Password)] public string Password { get; set; } } }
using System.ComponentModel.DataAnnotations; namespace BlogSystem.Model.ViewModels { /// <summary> /// 修改用戶密碼 /// </summary> public class ChangePwdViewModel { /// <summary> /// 舊密碼 /// </summary> [Required] public string OldPassword { get; set; } /// <summary> /// 新密碼 /// </summary> [Required] public string NewPassword { get; set; } /// <summary> /// 確認新密碼 /// </summary> [Required, Compare(nameof(NewPassword))] public string RequirePassword { get; set; } } }
using System; using System.ComponentModel.DataAnnotations; namespace BlogSystem.Model.ViewModels { /// <summary> /// 修改用戶資料 /// </summary> public class ChangeUserInfoViewModel { /// <summary> /// 帳號 /// </summary> public string Account { get; set; } /// <summary> /// 出生日期 /// </summary> [DataType(DataType.Date)] public DateTime BirthOfDate { get; set; } /// <summary> /// 性別 /// </summary> public Gender Gender { get; set; } } }
namespace BlogSystem.Model.ViewModels { /// <summary> /// 用戶詳細信息 /// </summary> public class UserDetailsViewModel { /// <summary> /// 帳號 /// </summary> public string Account { get; set; } /// <summary> /// 頭像 /// </summary> public string ProfilePhoto { get; set; } /// <summary> /// 年齡 /// </summary> public int Age { get; set; } /// <summary> /// 性別 /// </summary> public string Gender { get; set; } /// <summary> /// 用戶等級 /// </summary> public string Level { get; set; } /// <summary> /// 粉絲數 /// </summary> public int FansNum { get; set; } /// <summary> /// 關注數 /// </summary> public int FocusNum { get; set; } } }
二、IBLL和BLL層調整以下:
using System; using BlogSystem.Model; using BlogSystem.Model.ViewModels; using System.Threading.Tasks; namespace BlogSystem.IBLL { /// <summary> /// 用戶服務接口 /// </summary> public interface IUserService : IBaseService<User> { /// <summary> /// 註冊 /// </summary> /// <param name="model"></param> /// <returns></returns> Task<bool> Register(RegisterViewModel model); /// <summary> /// 登陸成功返回userId /// </summary> /// <param name="model"></param> /// <returns></returns> Task<Guid> Login(LoginViewModel model); /// <summary> /// 修改用戶密碼 /// </summary> /// <param name="model"></param> /// <param name="userId"></param> /// <returns></returns> Task<bool> ChangePassword(ChangePwdViewModel model, Guid userId); /// <summary> /// 修改用戶頭像 /// </summary> /// <param name="profilePhoto"></param> /// <param name="userId"></param> /// <returns></returns> Task<bool> ChangeUserPhoto(string profilePhoto, Guid userId); /// <summary> /// 修改用戶信息 /// </summary> /// <param name="model"></param> /// <param name="userId"></param> /// <returns></returns> Task<bool> ChangeUserInfo(ChangeUserInfoViewModel model, Guid userId); /// <summary> /// 使用account獲取用戶信息 /// </summary> /// <param name="account"></param> /// <returns></returns> Task<UserDetailsViewModel> GetUserInfoByAccount(string account); } }
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<User>, IUserService { private readonly IUserRepository _userRepository; public UserService(IUserRepository userRepository) { _userRepository = userRepository; BaseRepository = userRepository; } /// <summary> /// 用戶註冊 /// </summary> /// <param name="model"></param> /// <returns></returns> public async Task<bool> 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; } /// <summary> /// 用戶登陸 /// </summary> /// <param name="model"></param> /// <returns></returns> public async Task<Guid> 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; } /// <summary> /// 修改用戶密碼 /// </summary> /// <param name="model"></param> /// <param name="userId"></param> /// <returns></returns> public async Task<bool> 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; } /// <summary> /// 修改用戶照片 /// </summary> /// <param name="profilePhoto"></param> /// <param name="userId"></param> /// <returns></returns> public async Task<bool> 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; } /// <summary> /// 修改用戶信息 /// </summary> /// <param name="model"></param> /// <param name="userId"></param> /// <returns></returns> public async Task<bool> 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; } /// <summary> /// 經過帳號名稱獲取用戶信息 /// </summary> /// <param name="account"></param> /// <returns></returns> public async Task<UserDetailsViewModel> 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(); } } }
三、Controller層功能的實現大多數須要基於UserId,咱們怎麼獲取UserId呢?還記得Jwt嗎?客戶端發送請求時會在Header中帶上Jwt字符串,咱們能夠解析該字符串獲得用戶名。在自定義的JwtHelper中咱們實現了兩個方法,一個是加密Jwt,一個是解密Jwt,咱們對解密方法進行調整,以下:
/// <summary> /// Jwt解密 /// </summary> /// <param name="jwtStr"></param> /// <returns></returns> 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對象便可,以下:
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; } /// <summary> /// 用戶註冊 /// </summary> /// <param name="model"></param> /// <returns></returns> [HttpPost(nameof(Register))] public async Task<IActionResult> Register(RegisterViewModel model) { if (!await _userService.Register(model)) { return Ok("用戶已存在"); } //建立成功返回到登陸方法,並返回註冊成功的account return CreatedAtRoute(nameof(Login), model.Account); } /// <summary> /// 用戶登陸 /// </summary> /// <param name="model"></param> /// <returns></returns> [HttpPost("Login", Name = nameof(Login))] public async Task<IActionResult> 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); } /// <summary> /// 獲取用戶信息 /// </summary> /// <param name="account"></param> /// <returns></returns> [HttpGet("{account}")] public async Task<IActionResult> UserInfo(string account) { var list = await _userService.GetUserInfoByAccount(account); if (string.IsNullOrEmpty(list.Account)) { return NotFound(); } return Ok(list); } /// <summary> /// 修改用戶密碼 /// </summary> /// <param name="model"></param> /// <returns></returns> [Authorize] [HttpPatch("password")] public async Task<IActionResult> ChangePassword(ChangePwdViewModel model) { if (!await _userService.ChangePassword(model, _userId)) { return NotFound("用戶密碼錯誤!"); } return NoContent(); } /// <summary> /// 修改用戶照片 /// </summary> /// <param name="profilePhoto"></param> /// <returns></returns> [Authorize] [HttpPatch("photo")] public async Task<IActionResult> ChangeUserPhoto([FromBody]string profilePhoto) { if (!await _userService.ChangeUserPhoto(profilePhoto, _userId)) { return NotFound(); } return NoContent(); } /// <summary> /// 修改用戶信息 /// </summary> /// <param name="model"></param> /// <returns></returns> [Authorize] [HttpPatch("info")] public async Task<IActionResult> ChangeUserInfo(ChangeUserInfoViewModel model) { if (!await _userService.ChangeUserInfo(model, _userId)) { return Ok("用戶名已存在"); } return NoContent(); } } }
一、調整ViewModel層以下:
using System; using System.ComponentModel.DataAnnotations; namespace BlogSystem.Model.ViewModels { /// <summary> /// 編輯分類 /// </summary> public class EditCategoryViewModel { /// <summary> /// 分類Id /// </summary> public Guid CategoryId { get; set; } /// <summary> /// 分類名稱 /// </summary> [Required, StringLength(30, MinimumLength = 2)] public string CategoryName { get; set; } } }
using System; using System.ComponentModel.DataAnnotations; namespace BlogSystem.Model.ViewModels { /// <summary> /// 分類列表 /// </summary> public class CategoryListViewModel { /// <summary> /// 分類Id /// </summary> public Guid CategoryId { get; set; } /// <summary> /// 分類名稱 /// </summary> [Required, StringLength(30, MinimumLength = 2)] public string CategoryName { get; set; } } }
using System; using System.ComponentModel.DataAnnotations; namespace BlogSystem.Model.ViewModels { /// <summary> /// 建立文章分類 /// </summary> public class CreateCategoryViewModel { /// <summary> /// 分類Id /// </summary> public Guid CategoryId { get; set; } /// <summary> /// 分類名稱 /// </summary> [Required, StringLength(30, MinimumLength = 2)] public string CategoryName { get; set; } } }
二、調整IBLL和BLL層以下:
using BlogSystem.Model; using System; using System.Collections.Generic; using System.Threading.Tasks; using BlogSystem.Model.ViewModels; namespace BlogSystem.IBLL { /// <summary> /// 分類服務接口 /// </summary> public interface ICategoryService : IBaseService<Category> { /// <summary> /// 建立分類 /// </summary> /// <param name="categoryName"></param> /// <param name="userId"></param> /// <returns></returns> Task<Guid> CreateCategory(string categoryName, Guid userId); /// <summary> /// 編輯分類 /// </summary> /// <param name="model"></param> /// <param name="userId"></param> /// <returns></returns> Task<bool> EditCategory(EditCategoryViewModel model, Guid userId); /// <summary> /// 經過用戶Id獲取全部分類 /// </summary> /// <param name="userId"></param> /// <returns></returns> Task<List<CategoryListViewModel>> GetCategoryByUserIdAsync(Guid userId); } }
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<Category>, ICategoryService { private readonly ICategoryRepository _categoryRepository; public CategoryService(ICategoryRepository categoryRepository) { _categoryRepository = categoryRepository; BaseRepository = categoryRepository; } /// <summary> /// 建立分類 /// </summary> /// <param name="categoryName"></param> /// <param name="userId"></param> /// <returns></returns> public async Task<Guid> 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; } /// <summary> /// 編輯分類 /// </summary> /// <param name="model"></param> /// <param name="userId"></param> /// <returns></returns> public async Task<bool> 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; } /// <summary> /// 經過用戶Id獲取全部分類 /// </summary> /// <param name="userId"></param> /// <returns></returns> public Task<List<CategoryListViewModel>> GetCategoryByUserIdAsync(Guid userId) { return _categoryRepository.GetAll().Where(m => m.UserId == userId).Select(m => new CategoryListViewModel { CategoryId = m.Id, CategoryName = m.CategoryName }).ToListAsync(); } } }
三、調整Controller功能以下:
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; } /// <summary> /// 查詢用戶的文章分類 /// </summary> /// <param name="userId"></param> /// <returns></returns> [HttpGet("{userId}", Name = nameof(GetCategoryByUserId))] public async Task<IActionResult> GetCategoryByUserId(Guid userId) { if (userId == Guid.Empty) { return NotFound(); } var list = await _categoryService.GetCategoryByUserIdAsync(userId); return Ok(list); } /// <summary> /// 新增文章分類 /// </summary> /// <param name="categoryName"></param> /// <returns></returns> [Authorize] [HttpPost] public async Task<IActionResult> 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); } /// <summary> /// 刪除分類 /// </summary> /// <param name="categoryId"></param> /// <returns></returns> [Authorize] [HttpDelete("{categoryId}")] public async Task<IActionResult> 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(); } /// <summary> /// 編輯分類 /// </summary> /// <param name="model"></param> /// <returns></returns> [Authorize] [HttpPatch] public async Task<IActionResult> EditCategory(EditCategoryViewModel model) { if (!await _categoryService.EditCategory(model, _userId)) { return NotFound(); } return NoContent(); } } }
一、這裏我在操做時遇到了文章內容亂碼的問題,多是由於數據庫的text格式和輸入格式有衝突,因此這裏我暫時將其改爲了nvarchar(max)的類型
using System; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; namespace BlogSystem.Model { /// <summary> /// 文章 /// </summary> public class Article : BaseEntity { /// <summary> /// 文章標題 /// </summary> [Required] public string Title { get; set; } /// <summary> /// 文章內容 /// </summary> [Required] public string Content { get; set; } /// <summary> /// 發表人的Id,用戶表的外鍵 /// </summary> [ForeignKey(nameof(User))] public Guid UserId { get; set; } public User User { get; set; } /// <summary> /// 看好人數 /// </summary> public int GoodCount { get; set; } /// <summary> /// 不看好人數 /// </summary> public int BadCount { get; set; } /// <summary> /// 文章查看所需等級 /// </summary> public Level Level { get; set; } = Level.普通用戶; } }
ViewModel調整以下:
using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; namespace BlogSystem.Model.ViewModels { /// <summary> /// 建立文章 /// </summary> public class CreateArticleViewModel { /// <summary> /// 文章標題 /// </summary> [Required] public string Title { get; set; } /// <summary> /// 文章內容 /// </summary> [Required] public string Content { get; set; } /// <summary> /// 文章分類 /// </summary> [Required] public List<Guid> CategoryIds { get; set; } } }
using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; namespace BlogSystem.Model.ViewModels { /// <summary> /// 編輯文章 /// </summary> public class EditArticleViewModel { /// <summary> /// 文章Id /// </summary> public Guid Id { get; set; } /// <summary> /// 文章標題 /// </summary> [Required] public string Title { get; set; } /// <summary> /// 文章內容 /// </summary> [Required] public string Content { get; set; } /// <summary> /// 文章分類 /// </summary> public List<Guid> CategoryIds { get; set; } } }
using System; namespace BlogSystem.Model.ViewModels { /// <summary> /// 文章列表 /// </summary> public class ArticleListViewModel { /// <summary> /// 文章Id /// </summary> public Guid ArticleId { get; set; } /// <summary> /// 文章標題 /// </summary> public string Title { get; set; } /// <summary> /// 文章內容 /// </summary> public string Content { get; set; } /// <summary> /// 建立時間 /// </summary> public DateTime CreateTime { get; set; } /// <summary> /// 帳號 /// </summary> public string Account { get; set; } /// <summary> /// 頭像 /// </summary> public string ProfilePhoto { get; set; } } }
using System; using System.Collections.Generic; namespace BlogSystem.Model.ViewModels { /// <summary> /// 文章詳情 /// </summary> public class ArticleDetailsViewModel { /// <summary> /// 文章Id /// </summary> public Guid Id { get; set; } /// <summary> /// 文章標題 /// </summary> public string Title { get; set; } /// <summary> /// 文章內容 /// </summary> public string Content { get; set; } /// <summary> /// 建立時間 /// </summary> public DateTime CreateTime { get; set; } /// <summary> /// 做者 /// </summary> public string Account { get; set; } /// <summary> /// 頭像 /// </summary> public string ProfilePhoto { get; set; } /// <summary> /// 分類Id /// </summary> public List<Guid> CategoryIds { get; set; } /// <summary> /// 分類名稱 /// </summary> public List<string> CategoryNames { get; set; } /// <summary> /// 看好人數 /// </summary> public int GoodCount { get; set; } /// <summary> /// 不看好人數 /// </summary> public int BadCount { get; set; } } }
二、調整IBLL和BLL內容,以下
using BlogSystem.Model; using System; using System.Collections.Generic; using System.Threading.Tasks; using BlogSystem.Model.ViewModels; namespace BlogSystem.IBLL { /// <summary> /// 評論服務接口 /// </summary> public interface ICommentService : IBaseService<ArticleComment> { /// <summary> /// 添加評論 /// </summary> /// <param name="model"></param> /// <param name="articleId"></param> /// <param name="userId"></param> /// <returns></returns> Task CreateComment(CreateCommentViewModel model, Guid articleId, Guid userId); /// <summary> /// 添加普通評論的回覆 /// </summary> /// <param name="model"></param> /// <param name="articleId"></param> /// <param name="commentId"></param> /// <param name="userId"></param> /// <returns></returns> Task CreateReplyComment(CreateApplyCommentViewModel model, Guid articleId, Guid commentId, Guid userId); /// <summary> /// 添加回複評論的回覆 /// </summary> /// <param name="model"></param> /// <param name="articleId"></param> /// <param name="commentId"></param> /// <param name="userId"></param> /// <returns></returns> Task CreateToReplyComment(CreateApplyCommentViewModel model, Guid articleId, Guid commentId, Guid userId); /// <summary> /// 經過文章Id獲取全部評論 /// </summary> /// <param name="articleId"></param> /// <returns></returns> Task<List<CommentListViewModel>> GetCommentsByArticleIdAsync(Guid articleId); /// <summary> /// 確認回覆型評論是否存在 /// </summary> /// <param name="commentId"></param> /// <returns></returns> Task<bool> ReplyExistAsync(Guid commentId); } }
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<ArticleComment>, ICommentService { private readonly IArticleCommentRepository _commentRepository; private readonly ICommentReplyRepository _commentReplyRepository; public CommentService(IArticleCommentRepository commentRepository, ICommentReplyRepository commentReplyRepository) { _commentRepository = commentRepository; BaseRepository = commentRepository; _commentReplyRepository = commentReplyRepository; } /// <summary> /// 添加評論 /// </summary> /// <param name="model"></param> /// <param name="articleId"></param> /// <param name="userId"></param> /// <returns></returns> public async Task CreateComment(CreateCommentViewModel model, Guid articleId, Guid userId) { await _commentRepository.CreateAsync(new ArticleComment() { ArticleId = articleId, Content = model.Content, UserId = userId }); } /// <summary> /// 添加普通評論的回覆 /// </summary> /// <param name="model"></param> /// <param name="articleId"></param> /// <param name="commentId"></param> /// <param name="userId"></param> /// <returns></returns> 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 }); } /// <summary> /// 添加回復型評論的回覆 /// </summary> /// <param name="model"></param> /// <param name="articleId"></param> /// <param name="commentId"></param> /// <param name="userId"></param> /// <returns></returns> 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 }); } /// <summary> /// 根據文章Id獲取評論信息 /// </summary> /// <param name="articleId"></param> /// <returns></returns> public async Task<List<CommentListViewModel>> 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; } /// <summary> /// 確認回覆型評論是否存在 /// </summary> /// <param name="commentId"></param> /// <returns></returns> public async Task<bool> ReplyExistAsync(Guid commentId) { return await _commentReplyRepository.GetAll().AnyAsync(m => m.Id == commentId); } } }
三、調整Controller以下:
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; } /// <summary> /// 添加評論 /// </summary> /// <param name="articleId"></param> /// <param name="model"></param> /// <returns></returns> [Authorize] [HttpPost] public async Task<IActionResult> 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); } /// <summary> /// 添加回復型評論 /// </summary> /// <param name="articleId"></param> /// <param name="commentId"></param> /// <param name="model"></param> /// <returns></returns> [Authorize] [HttpPost("reply")] public async Task<IActionResult> 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(); } /// <summary> /// 獲取評論 /// </summary> /// <param name="articleId"></param> /// <returns></returns> [HttpGet(Name = nameof(GetComments))] public async Task<IActionResult> GetComments(Guid articleId) { if (!await _articleService.ExistsAsync(articleId)) { return NotFound(); } var list = await _commentService.GetCommentsByArticleIdAsync(articleId); return Ok(list); } } }
一、這裏發現評論回覆表CommentReply設計存在問題,由於回覆也有多是針對回覆型評論的,因此調整以後須要使用EF的遷移命令更行數據庫,以下:
using System; using System.ComponentModel.DataAnnotations; using System.ComponentModel.DataAnnotations.Schema; namespace BlogSystem.Model { /// <summary> /// 評論回覆表 /// </summary> public class CommentReply : BaseEntity { /// <summary> /// 回覆指向的評論Id /// </summary> public Guid CommentId { get; set; } /// <summary> /// 回覆指向的用戶Id /// </summary> [ForeignKey(nameof(ToUser))] public Guid ToUserId { get; set; } public User ToUser { get; set; } /// <summary> /// 文章ID /// </summary> [ForeignKey(nameof(Article))] public Guid ArticleId { get; set; } public Article Article { get; set; } /// <summary> /// 用戶Id /// </summary> [ForeignKey(nameof(User))] public Guid UserId { get; set; } public User User { get; set; } /// <summary> /// 回覆的內容 /// </summary> [Required, StringLength(800)] public string Content { get; set; } } }
調整ViewModel以下,有人發現評論和回覆的ViewModel相同,爲何不使用一個?是爲了應對後續兩張表欄位不一樣時,須要調整的狀況
using System; using System.ComponentModel.DataAnnotations; namespace BlogSystem.Model.ViewModels { /// <summary> /// 文章評論 /// </summary> public class CreateCommentViewModel { /// <summary> /// 評論內容 /// </summary> [Required, StringLength(800)] public string Content { get; set; } } }
using System.ComponentModel.DataAnnotations; namespace BlogSystem.Model.ViewModels { /// <summary> /// 添加回復型評論 /// </summary> public class CreateApplyCommentViewModel { /// <summary> /// 回覆的內容 /// </summary> [Required, StringLength(800)] public string Content { get; set; } } }
using System; namespace BlogSystem.Model.ViewModels { /// <summary> /// 文章評論列表 /// </summary> public class CommentListViewModel { /// <summary> /// 文章Id /// </summary> public Guid ArticleId { get; set; } /// <summary> /// 用戶Id /// </summary> public Guid UserId { get; set; } /// <summary> /// 帳號 /// </summary> public string Account { get; set; } /// <summary> /// 頭像 /// </summary> public string ProfilePhoto { get; set; } /// <summary> /// 評論Id /// </summary> public Guid CommentId { get; set; } /// <summary> /// 評論內容 /// </summary> public string CommentContent { get; set; } /// <summary> /// 建立時間 /// </summary> public DateTime CreateTime { get; set; } } }
二、調整IBLL和BLL以下:
using BlogSystem.Model; using System; using System.Collections.Generic; using System.Threading.Tasks; using BlogSystem.Model.ViewModels; namespace BlogSystem.IBLL { /// <summary> /// 評論服務接口 /// </summary> public interface ICommentService : IBaseService<ArticleComment> { /// <summary> /// 添加評論 /// </summary> /// <param name="model"></param> /// <param name="articleId"></param> /// <param name="userId"></param> /// <returns></returns> Task CreateComment(CreateCommentViewModel model, Guid articleId, Guid userId); /// <summary> /// 添加普通評論的回覆 /// </summary> /// <param name="model"></param> /// <param name="articleId"></param> /// <param name="commentId"></param> /// <param name="userId"></param> /// <returns></returns> Task CreateReplyComment(CreateApplyCommentViewModel model, Guid articleId, Guid commentId, Guid userId); /// <summary> /// 添加回複評論的回覆 /// </summary> /// <param name="model"></param> /// <param name="articleId"></param> /// <param name="commentId"></param> /// <param name="userId"></param> /// <returns></returns> Task CreateToReplyComment(CreateApplyCommentViewModel model, Guid articleId, Guid commentId, Guid userId); /// <summary> /// 經過文章Id獲取全部評論 /// </summary> /// <param name="articleId"></param> /// <returns></returns> Task<List<CommentListViewModel>> GetCommentsByArticleIdAsync(Guid articleId); /// <summary> /// 確認回覆型評論是否存在 /// </summary> /// <param name="commentId"></param> /// <returns></returns> Task<bool> ReplyExistAsync(Guid commentId); } }
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<ArticleComment>, ICommentService { private readonly IArticleCommentRepository _commentRepository; private readonly ICommentReplyRepository _commentReplyRepository; public CommentService(IArticleCommentRepository commentRepository, ICommentReplyRepository commentReplyRepository) { _commentRepository = commentRepository; BaseRepository = commentRepository; _commentReplyRepository = commentReplyRepository; } /// <summary> /// 添加評論 /// </summary> /// <param name="model"></param> /// <param name="articleId"></param> /// <param name="userId"></param> /// <returns></returns> public async Task CreateComment(CreateCommentViewModel model, Guid articleId, Guid userId) { await _commentRepository.CreateAsync(new ArticleComment() { ArticleId = articleId, Content = model.Content, UserId = userId }); } /// <summary> /// 添加普通評論的回覆 /// </summary> /// <param name="model"></param> /// <param name="articleId"></param> /// <param name="commentId"></param> /// <param name="userId"></param> /// <returns></returns> 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 }); } /// <summary> /// 添加回復型評論的回覆 /// </summary> /// <param name="model"></param> /// <param name="articleId"></param> /// <param name="commentId"></param> /// <param name="userId"></param> /// <returns></returns> 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 }); } /// <summary> /// 根據文章Id獲取評論信息 /// </summary> /// <param name="articleId"></param> /// <returns></returns> public async Task<List<CommentListViewModel>> 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; } /// <summary> /// 確認回覆型評論是否存在 /// </summary> /// <param name="commentId"></param> /// <returns></returns> public async Task<bool> ReplyExistAsync(Guid commentId) { return await _commentReplyRepository.GetAll().AnyAsync(m => m.Id == commentId); } } }
三、調整Controller以下:
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; } /// <summary> /// 添加評論 /// </summary> /// <param name="articleId"></param> /// <param name="model"></param> /// <returns></returns> [Authorize] [HttpPost] public async Task<IActionResult> 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); } /// <summary> /// 添加回復型評論 /// </summary> /// <param name="articleId"></param> /// <param name="commentId"></param> /// <param name="model"></param> /// <returns></returns> [Authorize] [HttpPost("reply")] public async Task<IActionResult> 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(); } /// <summary> /// 獲取評論 /// </summary> /// <param name="articleId"></param> /// <returns></returns> [HttpGet(Name = nameof(GetComments))] public async Task<IActionResult> GetComments(Guid articleId) { if (!await _articleService.ExistsAsync(articleId)) { return NotFound(); } var list = await _commentService.GetCommentsByArticleIdAsync(articleId); return Ok(list); } } }
該項目源碼已上傳至GitHub,有須要的朋友能夠下載使用:https://github.com/Jscroop/BlogSystem
本人知識點有限,若文中有錯誤的地方請及時指正,方便你們更好的學習和交流。
本文部份內容參考了網絡上的視頻內容和文章,僅爲學習和交流,視頻地址以下:
solenovex,ASP.NET Core 3.x 入門視頻
solenovex,使用 ASP.NET Core 3.x 構建 RESTful Web API
老張的哲學,系列教程一目錄:.netcore+vue 先後端分離