基于.NetCore开发博客项目 StarBlog - (22) 开发博客文章相关接口
前言
本文介绍博客文章相关接口的开发,作为接口开发介绍的第一篇,会写得比较详细,以抛砖引玉,后面的其他接口就粗略带过了,着重于WebApi开发的周边设施。
涉及到的接口:文章CRUD、置顶文章、推荐文章等。
开始前先介绍下AspNetCore框架的基础概念,MVC模式(前后端不分离)、WebApi模式(前后端分离),都是有Controller的。
区别在前者的Controller集成自 Controller 类,后者继承自 ControllerBase 类。
无论博客前台,还是接口,大部分逻辑都是通用的,因此我把这些逻辑封装在 service 中,以减少冗余代码。
文章CRUD
在之前的文章里,已经实现了文章列表、文章详情的功能,等于是CRUD里的 R (Retrieve) “查”功能已经实现。
相关代码在 StarBlog.Web/Services/PostService.cs 文件中。
PS:根据RESTFul规范,CRUD不同的操作对应不同的HTTP方法
在AspNetCore中,可以通过在 Action 上加上
[HttpPost]、[HttpDelete("{id}")]这样的特性来标记接口使用的HTTP方法和URL。
现在需要实现“增删改”的功能。
增和改 (Create/Update)
因为这俩功能差不多,所以放在一起实现,很多ORM也是把 Insert 和 Update 合在一起,即 InsertOrUpdate
DTO
在计算机编程中,数据传输对象 (data transfer object,DTO)是在2个进程中携带数据的对象。因为进程间通信通常用于远程接口(如web服务)的昂贵操作。成本的主体是客户和服务器之间的来回通信时间。为降低这种调用次数,使用DTO聚合本来需要多次通信传输的数据。
DAO与业务对象或数据访问对象的区别是:DTO的数据的变异子与访问子(mutator和accessor)、语法分析(parser)、序列化(serializer)时不会有任何存储、获取、序列化和反序列化的异常。即DTO是简单对象,不含任何业务逻辑,但可包含序列化和反序列化以用于传输数据。
by Wikipedia
添加文章只需要 Post 模型的其中几个属性就行,不适合把整个 Post 模型作为参数,所以,首先要定义一个DTO作为添加文章的参数。
文件路径 StarBlog.Web/ViewModels/Blog/PostCreationDto.cs
public class PostCreationDto {
/// <summary>
/// 标题
/// </summary>
public string? Title { get; set; }
/// <summary>
/// 梗概
/// </summary>
public string? Summary { get; set; }
/// <summary>
/// 内容(markdown格式)
/// </summary>
public string? Content { get; set; }
/// <summary>
/// 分类ID
/// </summary>
public int CategoryId { get; set; }
}
AutoMapper
有了DTO作为参数,在保存文章的时候,我们需要手动把DTO对象里面的属性,一个个赋值到 Post 对象上,像这样:
var post = new Post {
Id = Guid.NewGuid(),
Title = dto.Title,
Summary = dto.Summary,
Content = dto.Content,
CategoryId = dto.CategoryId
};
一个俩个还好,接口多了的话,大量重复的代码会很烦人,而且也容易出错。
还好我们可以用AutoMapper组件来实现对象自动映射。
通过nuget安装 AutoMapper.Extensions.Microsoft.DependencyInjection 这个包
注册服务:
builder.Services.AddAutoMapper(typeof(Program));
然后再创建对应的Profile(配置),如果没有特殊配置其实也可以不添加这个配置文件,执行默认的映射行为即可。
作为例子,本文简单介绍一下,创建 StarBlog.Web/Properties/AutoMapper/PostProfile.cs 文件
public class PostProfile : Profile {
public PostProfile() {
CreateMap<PostUpdateDto, Post>();
CreateMap<PostCreationDto, Post>();
}
}
在构造方法里执行 CreateMap 配置从左到右的映射关系。
上面的代码配置了从 PostUpdateDto / PostCreationDto 这两个对象到 Post 对象的映射关系。
如果有些字段不要映射的,可以这样写:
public class PostProfile : Profile {
private readonly List<string> _unmapped = new List<string> {
"Categories",
};
public PostProfile() {
CreateMap<PostUpdateDto, Post>();
CreateMap<PostCreationDto, Post>();
ShouldMapProperty = property => !_unmapped.Contains(property.Name);
}
}
其他代码不变,修改 _unmapped 这个字段就行。
接着在 Controller 里注入 IMapper 对象
private readonly IMapper _mapper;
使用方法很简单
var post = _mapper.Map<Post>(dto);
传入一个 PostCreationDto 类型的 dto,可以得到 Post 对象。
Controller
先上Controller的代码
[Authorize]
[ApiController]
[Route("Api/[controller]")]
[ApiExplorerSettings(GroupName = "blog")]
public class BlogPostController : ControllerBase {
private readonly IMapper _mapper;
private readonly PostService _postService;
private readonly BlogService _blogService;
public BlogPostController(PostService postService, BlogService blogService, IMapper mapper) {
_postService = postService;
_blogService = blogService;
_mapper = mapper;
}
}
加在Controller上面的四个特性,挨个介绍
Authorize表示这个controller下面的所有接口需要登录才能访问ApiController表示这是个WebApi ControllerRoute指定了这个Controller的路由模板,即下面的接口全是以Api/BlogPostController开头ApiExplorerSettings接口分组,在swagger文档里看会更清晰
接下来,添加和修改是俩接口,分开说。
添加
很容易,直接上代码了
[HttpPost]
public async Task<ApiResponse<Post>> Add(PostCreationDto dto, [FromServices] CategoryService categoryService) {
// 使用 AutoMapper,前面介绍过的
var post = _mapper.Map<Post>(dto);
// 获取文章分类,如果不存在就返回报错信息
var category = categoryService.GetById(dto.CategoryId);
if (category == null) return ApiResponse.BadRequest($"分类 {dto.CategoryId} 不存在!");
// 生成文章的ID、创建、更新时间
post.Id = GuidUtils.GuidTo16String();
post.CreationTime = DateTime.Now;
post.LastUpdateTime = DateTime.Now;
// 设置文章状态为已发布
post.IsPublish = true;
// 获取分类的层级结构
post.Categories = categoryService.GetCategoryBreadcrumb(category);
return new ApiResponse<Post>(await _postService.InsertOrUpdateAsync(post));
}
就是这个 Add 方法
目前 CategoryService 只需要在这个添加的接口里用到,所以不用整个Controller注入,在 Add 方法里使用 [FromServices] 特性注入。
后面有个获取分类的层级结构,因为StarBlog的设计是支持多级分类,为了在前台展示文章分类层级的时候减少运算量,所以我把文章的分类层级结构(形式是分类ID用逗号分隔开,如:1,3,5,7,9)直接存入数据库,空间换时间。
最后,执行 PostService 里的 InsertOrUpdateAsync 方法,解析处理文章内容,并将文章存入数据库。
PS:本项目的接口返回值已经做统一包装处理,可以看到大量使用
ApiResponse作为返回值,这个后续文章会介绍。
修改
噢,还有 修改文章(Update) 的接口,修改使用 PUT 方法
[HttpPut("{id}")]
public async Task<ApiResponse<Post>> Update(string id, PostUpdateDto dto) {
// 先获取文章对象
var post = _postService.GetById(id);
if (post == null) return ApiResponse.NotFound($"博客 {id} 不存在");
// 在已有对象的基础上进行映射
post = _mapper.Map(dto, post);
// 更新修改时间
post.LastUpdateTime = DateTime.Now;
return new ApiResponse<Post>(await _postService.InsertOrUpdateAsync(post));
}
依然很简单,里面注释写得很清楚了
AutoMapper可以对已有对象的基础上进行映射
mapper.Map(source)得到一个全新的对象mapper.Map(source, dest)在 dest 对象的基础上修改
搞定。
Service
作为一个多层架构项目,核心逻辑依然放在 Service 里
并且这里是添加和修改二合一,优雅~
public async Task<Post> InsertOrUpdateAsync(Post post) {
var postId = post.Id;
// 是新文章的话,先保存到数据库
if (await _postRepo.Where(a => a.Id == postId).CountAsync() == 0) {
post = await _postRepo.InsertAsync(post);
}
// 检查文章中的外部图片,下载并进行替换
// todo 将外部图片下载放到异步任务中执行,以免保存文章的时候太慢
post.Content = await MdExternalUrlDownloadAsync(post);
// 修改文章时,将markdown中的图片地址替换成相对路径再保存
post.Content = MdImageLinkConvert(post, false);
// 处理完内容再更新一次
await _postRepo.UpdateAsync(post);
return post;
}
另外,这部分代码在之前的markdown渲染和自动下载外部图片的相关文章里已经介绍过了,本文不再重复。详情可以看本系列的第17篇文章。
删 (Delete)
没什么好说的,直接上代码
StarBlog.Web/Services/PostService.cs
public int Delete(string id) {
return _postRepo.Delete(a => a.Id == id);
}
StarBlog.Web/Apis/Blog/BlogPostController.cs
[HttpDelete("{id}")]
public ApiResponse Delete(string id) {
var post = _postService.GetById(id);
if (post == null) return ApiResponse.NotFound($"博客 {id} 不存在");
var rows = _postService.Delete(id);
return ApiResponse.Ok($"删除了 {rows} 篇博客");
}
查 (Retrieve)
查,分成两种,一种是列表,一种是单个。
单个
先说单个的,比较容易。
StarBlog.Web/Services/PostService.cs
public Post? GetById(string id) {
// 获取文章的时候对markdown中的图片地址解析,加上完整地址返回给前端
var post = _postRepo.Where(a => a.Id == id).Include(a => a.Category).First();
if (post != null) post.Content = MdImageLinkConvert(post, true);
return post;
}
StarBlog.Web/Apis/Blog/BlogPostController.cs
[AllowAnonymous]
[HttpGet("{id}")]
public ApiResponse<Post> Get(string id) {
var post = _postService.GetById(id);
return post == null ? ApiResponse.NotFound() : new ApiResponse<Post>(post);
}
这里接口加了个 [AllowAnonymous],表示这接口不用登录也能访问。
列表
列表有点麻烦,需要过滤筛选、排序、分页等功能,我打算把这些功能放到后面的文章讲,不然本文的篇幅就爆炸了…
那最简单的就是直接返回全部文章列表。
[HttpGet]
public List<Post> GetAll() {
return _postService.GetAll();
}
够简单吧?
文章的相关操作
单纯的CRUD是无法满足功能需求的
所以要在RESTFul接口的接触上,配合一些RPC风格接口,实现我们需要的功能。
设置推荐文章
有一个模型专门管理推荐文章,名为 FeaturedPost
要设置推荐文章,直接往里面添加数据就行了。反之,取消就是删除对应的记录。
上代码
StarBlog.Web/Services/PostService.cs
public FeaturedPost AddFeaturedPost(Post post) {
var item = _fPostRepo.Where(a => a.PostId == post.Id).First();
if (item != null) return item;
item = new FeaturedPost {PostId = post.Id};
_fPostRepo.Insert(item);
return item;
}
StarBlog.Web/Apis/Blog/BlogPostController.cs
[HttpPost("{id}/[action]")]
public ApiResponse<FeaturedPost> SetFeatured(string id) {
var post = _postService.GetById(id);
return post == null
? ApiResponse.NotFound()
: new ApiResponse<FeaturedPost>(_blogService.AddFeaturedPost(post));
}
配置完URL就是:Api/BlogPost/{id}/SetFeatured 了
取消推荐文章
上面那个推荐的逆向操作
service这样写
public int DeleteFeaturedPost(Post post) {
var item = _fPostRepo.Where(a => a.PostId == post.Id).First();
return item == null ? 0 : _fPostRepo.Delete(item);
}
controller酱子
[HttpPost("{id}/[action]")]
public ApiResponse CancelFeatured(string id) {
var post = _postService.GetById(id);
if (post == null) return ApiResponse.NotFound($"博客 {id} 不存在");
var rows = _blogService.DeleteFeaturedPost(post);
return ApiResponse.Ok($"delete {rows} rows.");
}
设置置顶
StarBlog设计为只允许一篇置顶文章
设置新的置顶文章,会把原有的顶掉
service代码
/// <returns>返回 <see cref="TopPost"/> 对象和删除原有置顶博客的行数</returns>
public (TopPost, int) SetTopPost(Post post) {
var rows = _topPostRepo.Select.ToDelete().ExecuteAffrows();
var item = new TopPost {PostId = post.Id};
_topPostRepo.Insert(item);
return (item, rows);
}
先删除已有置顶文章,再添加新的进去。返回值用了元组语法。
controller代码
[HttpPost("{id}/[action]")]
public ApiResponse<TopPost> SetTop(string id) {
var post = _postService.GetById(id);
if (post == null) return ApiResponse.NotFound($"博客 {id} 不存在");
var (data, rows) = _blogService.SetTopPost(post);
return new ApiResponse<TopPost> {Data = data, Message = $"ok. deleted {rows} old topPosts."};
}
就这样,简简单单。
上传图片
场景:在后台编辑文章,会插入一些图片。
这个接口因为要上传文件,所以使用FormData接收参数,前端发起请求需要注意。
这是controller代码:
[HttpPost("{id}/[action]")]
public ApiResponse UploadImage(string id, IFormFile file) {
var post = _postService.GetById(id);
if (post == null) return ApiResponse.NotFound($"博客 {id} 不存在");
var imgUrl = _postService.UploadImage(post, file);
return ApiResponse.Ok(new {
imgUrl,
imgName = Path.GetFileNameWithoutExtension(imgUrl)
});
}
后面的 PostService.UploadImage() 方法,本文(囿于篇幅关系)先不介绍了,留个坑,放在后面图片管理接口里一起介绍哈~
博客的相关操作
刚才基本是在对文章做CRUD,别忘了还有个 BlogController 呢~
功能就是获取推荐、获取置顶、博客文章总览、打包上传之类的。
这里也大概介绍一下。
获取推荐、置顶的service代码:
public List<Post> GetFeaturedPosts() {
return _fPostRepo.Select.Include(a => a.Post.Category)
.ToList(a => a.Post);
}
public Post? GetTopOnePost() {
return _topPostRepo.Select.Include(a => a.Post.Category).First()?.Post;
}
controller太简单,就不写了。
总览信息
这里没封装到service里,感觉其他地方不会用到,拒绝过度封装。
直接从ORM读取,文章、分类、图片、推荐等的数量。
PS:要做展示大屏的话,这些应该还是不够的,后续再增加(flag立下了)
public BlogOverview Overview() {
return new BlogOverview {
PostsCount = _postRepo.Select.Count(),
CategoriesCount = _categoryRepo.Select.Count(),
PhotosCount = _photoRepo.Select.Count(),
FeaturedPostsCount = _fPostRepo.Select.Count(),
FeaturedCategoriesCount = _fCategoryRepo.Select.Count(),
FeaturedPhotosCount = _fPhotoRepo.Select.Count()
};
}
打包上传
这个功能是:把本地写完的markdown文件连同图片等资源一起打包zip上传,StarBlog解析markdown并将图片附件处理后存入数据库,实现很方便的本地写文章,博客发表功能。
具体实现已经在之前的文章里介绍过了,这里就不重复啦,详情可以查看本系列的第18篇文章。基于.NetCore开发博客项目 StarBlog - (18) 实现本地Typora文章打包上传
小结
AspNetCore WebApi的开发有很多东西可以写的,在开发过程中我也在不断学习,有很多好玩的新功能、骚操作是在后面才加入StarBlog项目的,但为了保证本系列文章阅读的连贯性,即使某功能在文章撰写时已经实现,也可能不会加入介绍。这些我会在后面单独写一篇文章来介绍(绝不是在水哦),以提升读者的阅读体验。
还有,作为新手向教程,我会尽量写得比较详细(废话比较多),导致篇幅较长,但但仍无法面面俱到介绍AspNetCore的全部细节,建议边看边学的读者搭配AspNetCore官方文档或教材阅读~
系列文章
- 基于.NetCore开发博客项目 StarBlog - (1) 为什么需要自己写一个博客?
- 基于.NetCore开发博客项目 StarBlog - (2) 环境准备和创建项目
- 基于.NetCore开发博客项目 StarBlog - (3) 模型设计
- 基于.NetCore开发博客项目 StarBlog - (4) markdown博客批量导入
- 基于.NetCore开发博客项目 StarBlog - (5) 开始搭建Web项目
- 基于.NetCore开发博客项目 StarBlog - (6) 页面开发之博客文章列表
- 基于.NetCore开发博客项目 StarBlog - (7) 页面开发之文章详情页面
- 基于.NetCore开发博客项目 StarBlog - (8) 分类层级结构展示
- 基于.NetCore开发博客项目 StarBlog - (9) 图片批量导入
- 基于.NetCore开发博客项目 StarBlog - (10) 图片瀑布流
- 基于.NetCore开发博客项目 StarBlog - (11) 实现访问统计
- 基于.NetCore开发博客项目 StarBlog - (12) Razor页面动态编译
- 基于.NetCore开发博客项目 StarBlog - (13) 加入友情链接功能
- 基于.NetCore开发博客项目 StarBlog - (14) 实现主题切换功能
- 基于.NetCore开发博客项目 StarBlog - (15) 生成随机尺寸图片
- 基于.NetCore开发博客项目 StarBlog - (16) 一些新功能 (监控/统计/配置/初始化)
- 基于.NetCore开发博客项目 StarBlog - (17) 自动下载文章里的外部图片
- 基于.NetCore开发博客项目 StarBlog - (18) 实现本地Typora文章打包上传
- 基于.NetCore开发博客项目 StarBlog - (19) Markdown渲染方案探索
- 基于.NetCore开发博客项目 StarBlog - (20) 图片显示优化
- 基于.NetCore开发博客项目 StarBlog - (21) 开始开发RESTFul接口
- 基于.NetCore开发博客项目 StarBlog - (22) 开发博客文章相关接口
基于.NetCore开发博客项目 StarBlog - (22) 开发博客文章相关接口的更多相关文章
- 基于.NetCore开发博客项目 StarBlog - (4) markdown博客批量导入
系列文章 基于.NetCore开发博客项目 StarBlog - (1) 为什么需要自己写一个博客? 基于.NetCore开发博客项目 StarBlog - (2) 环境准备和创建项目 基于.NetC ...
- 基于.NetCore开发博客项目 StarBlog - (18) 实现本地Typora文章打包上传
前言 九月太忙,只更新了三篇文章,本来这个功能是从九月初就开始做的,结果一直拖到现在国庆假期才有时间完善并且写文章~ 之前我更新了几篇关于 Python 的文章,有朋友留言问是不是不更新 .Net 了 ...
- 基于.NetCore开发博客项目 StarBlog - (16) 一些新功能 (监控/统计/配置/初始化)
系列文章 基于.NetCore开发博客项目 StarBlog - (1) 为什么需要自己写一个博客? 基于.NetCore开发博客项目 StarBlog - (2) 环境准备和创建项目 基于.NetC ...
- 基于.NetCore开发博客项目 StarBlog - (17) 自动下载文章里的外部图片
系列文章 基于.NetCore开发博客项目 StarBlog - (1) 为什么需要自己写一个博客? 基于.NetCore开发博客项目 StarBlog - (2) 环境准备和创建项目 基于.NetC ...
- 基于.NetCore开发博客项目 StarBlog - (2) 环境准备和创建项目
系列文章 基于.NetCore开发博客项目 StarBlog - (1) 为什么需要自己写一个博客? 基于.NetCore开发博客项目 StarBlog - (2) 环境准备和创建项目 ... 基于. ...
- 基于.NetCore开发博客项目 StarBlog - (3) 模型设计
系列文章 基于.NetCore开发博客项目 StarBlog - (1) 为什么需要自己写一个博客? 基于.NetCore开发博客项目 StarBlog - (2) 环境准备和创建项目 基于.NetC ...
- 基于.NetCore开发博客项目 StarBlog - (5) 开始搭建Web项目
系列文章 基于.NetCore开发博客项目 StarBlog - (1) 为什么需要自己写一个博客? 基于.NetCore开发博客项目 StarBlog - (2) 环境准备和创建项目 基于.NetC ...
- 基于.NetCore开发博客项目 StarBlog - (6) 页面开发之博客文章列表
系列文章 基于.NetCore开发博客项目 StarBlog - (1) 为什么需要自己写一个博客? 基于.NetCore开发博客项目 StarBlog - (2) 环境准备和创建项目 基于.NetC ...
- 基于.NetCore开发博客项目 StarBlog - (7) 页面开发之文章详情页面
系列文章 基于.NetCore开发博客项目 StarBlog - (1) 为什么需要自己写一个博客? 基于.NetCore开发博客项目 StarBlog - (2) 环境准备和创建项目 基于.NetC ...
- 基于.NetCore开发博客项目 StarBlog - (8) 分类层级结构展示
系列文章 基于.NetCore开发博客项目 StarBlog - (1) 为什么需要自己写一个博客? 基于.NetCore开发博客项目 StarBlog - (2) 环境准备和创建项目 基于.NetC ...
随机推荐
- Opengl ES之VBO和VAO
前言 本文主要介绍了什么是VBO/VAO,为什么需要使用VBO/VAO以及如何使用VBO和VAO. VBO 什么是VBO VBO(vertex Buffer Object):顶点缓冲对象.是在显卡存储 ...
- Multi-View Intent Disentangle Graph Networks for Bundle Recommendation解读
简要论述 bundle recommendation 的目的是向user推荐一个整体的bundle package about items.以前的模型捕获了user对item and item关联的偏 ...
- EF在二手市场中的使用
二手市场这个小项目是我第一次用EF,边学边写边记录吧 首先明确几个知识点 存储过程 存储过程简单来说,就是为以后的使用而保存的一条或多条SQL语句的集合.可将其视为批件,虽然它们的作用不仅限于批处理. ...
- 15. MongoDB系列之选择片键
1. 片键类型 1.1 升序片键 升序片键通常类似于date或ObjectId--随着时间稳步增长的字段. 这种模式通常会使MongoDB更难保持块的平衡,因为所有的块都是由一个分片创建的. 1.2 ...
- 开源数字基础设施 项目 -- Speckle
[Speckle](https://speckle.systems/)是用于 3D 设计的任何东西的开源数字基础设施.处理软件孤岛.实时协作.数据管理.版本控制和自动化之间的互操作性.致力于构建一个开 ...
- 20220925 - CSP-S 模拟赛 #2
20220925 - CSP-S 模拟赛 #2 时间记录 \(8:00-8:20\) 浏览题面 \(8:20-8:45\) T1 想到了分块计算,但是在手推样例的过程中,发现样例的数据并不能真正构成一 ...
- Qt Quick 用cmake怎么玩子项目
以下内容为本人的著作,如需要转载,请声明原文链接微信公众号「englyf」https://mp.weixin.qq.com/s/o-_aGqreuQda-ZmKktvxwA 以往在公司开发众多的项目中 ...
- 【题解】CF1720C
题意简述 给你一个 01 矩阵,每一次你可以在这个矩阵中找到一个 \(L\) 型,将它全部变成 0.\(L\) 型的定义是在一个 \(2\times2\) 矩阵中,除开一个角之外的图形,其中必须包含至 ...
- iptables和firewalld基础
1.四表五链概念: filter表 过滤数据包 Nat表 用于网络地址转换(IP.端口) Mangle表 修改数据包的服务类型.TTL.并且可以配置路由实现QOS Raw表 决定数据包是否被状态跟踪机 ...
- Mysql+Mycat+NFS+Rsync+LVS+DNS+IPtables综合实验
1.环境准备 服务器 IP地址 作用 系统版本 Mysql-master eth0:10.0.0.58 主数据库 Rocky8.6 Mysql-slave1 eth0:10.0.0.68 备数据库 R ...