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

Jscroop發表於2020-05-19

上一篇我們使用Swagger新增了介面文件,使用Jwt完成了授權,本章我們簡答介紹一下RESTful風格的WebAPI開發過程中涉及到的一些知識點,並完善一下尚未完成的功能


.NET下的WebAPI是一種無限接近RESTful風格的框架,RESTful風格它有著自己的一套理論,它的大概意思就是說使用標準的Http方法,將Web系統的服務抽象為資源。稍微具體一點的介紹可以檢視阮一峰的這篇文章RESTful API最佳實踐。我們這裡就分幾部分介紹一下構建RESTful API需要了解的基礎知識

注:本章介紹部分的內容大多是基於solenovex的使用 ASP.NET Core 3.x 構建 RESTful Web API視訊內容的整理,若想進一步瞭解相關知識,請檢視原視訊

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

3、安全性和冪等性

安全性是指方法執行後不會改變資源的表述;冪等性是指方法無論執行多少次都會得到相同的結果

二、狀態碼相關

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、故障處理

我們可以在非開發環境進行如下配置,以確保生產環境異常時能檢視到相關異常說明,通常這裡會寫入日誌記錄異常,我們會在後面的章節新增日誌功能,這裡先修改如下:

三、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格式,可以配置如下:

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的層進行了調整,調整了出生日期和性別的預設值

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

2、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();
        }
    }
}

3、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();
        }
    }
}

2、分類Controller

1、調整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; }
    }
}

2、調整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();
        }
    }
}

3、調整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();
        }
    }
}

3、文章Controller

1、這裡我在操作時遇到了文章內容亂碼的問題,可能是因為資料庫的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; }

    }
}

2、調整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);
        }
    }
}

3、調整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);
        }
    }
}

4、評論Controller

1、這裡發現評論回覆表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; }

    }
}

2、調整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);
        }
    }
}

3、調整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 前後端分離

宣告

相關文章