学习ASP.NET Core(06)-Restful与WebAPI

  • A+

上一篇我们使用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

学习ASP.NET Core(06)-Restful与WebAPI

3、安全性和幂等性

安全性是指方法执行后不会改变资源的表述;幂等性是指方法无论执行多少次都会得到相同的结果

学习ASP.NET Core(06)-Restful与WebAPI

二、状态码相关

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

我们可以在非开发环境进行如下配置,以确保生产环境异常时能查看到相关异常说明,通常这里会写入日志记录异常,我们会在后面的章节添加日志功能,这里先修改如下:

学习ASP.NET Core(06)-Restful与WebAPI

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

学习ASP.NET Core(06)-Restful与WebAPI

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 前后端分离

声明
90DIR-CMD