StarBlog Development Notes (22): Developing Blog Post Related APIs
Introduction This article covers the development of blog post related APIs. As the first article introducing API development, it will be written in more detail to get the ball rolling; later APIs will only be covered briefly, with the focus on the surrounding infrastructure of WebApi development. APIs involved: post CRUD, pinning posts, recommending posts, etc. Before we start, a brief introduction to the basic concepts of the AspNetCore framework. Both the MVC pattern (frontend and backend not separated) and the WebApi pattern (frontend and backend separated) have Controllers. The difference is that the former's Controller inherits from Contr
目录23 个章节
Introduction
This article covers the development of blog post related APIs. As the first article introducing API development, it will be written in more detail to get the ball rolling; later APIs will only be covered briefly, with the focus on the surrounding infrastructure of WebApi development.
APIs involved: post CRUD, pinning posts, recommending posts, etc.
Before we start, a brief introduction to the basic concepts of the AspNetCore framework. Both the MVC pattern (frontend and backend not separated) and the WebApi pattern (frontend and backend separated) have Controllers.
The difference is that the former's Controller inherits from the Controller class, while the latter inherits from the ControllerBase class.
Whether for the blog frontend or the APIs, most of the logic is shared, so I encapsulated this logic in service to reduce redundant code.
Post CRUD
In previous articles, the post list and post detail features have already been implemented, which means the R (Retrieve) "read" part of CRUD is done.
The related code is in the StarBlog.Web/Services/PostService.cs file.
PS: According to the RESTFul specification, different CRUD operations correspond to different HTTP methods
In AspNetCore, you can mark the HTTP method and URL used by an endpoint by adding attributes such as
[HttpPost]and[HttpDelete("{id}")]on the Action.
Now we need to implement the create, update, and delete features.
Create and Update (Create/Update)
Because these two features are pretty similar, they're implemented together. Many ORMs also combine Insert and Update, i.e. InsertOrUpdate
DTO
In computer programming, a data transfer object (DTO) is an object that carries data between two processes. Inter-process communication is typically used for expensive operations of remote interfaces (such as web services). The bulk of the cost is the round-trip communication time between the client and the server. To reduce the number of such calls, DTOs are used to aggregate data that would otherwise need to be transferred over multiple communications.
The difference between a DTO and a business object or data access object is that a DTO does not have any behavior except for storage, retrieval, serialization, and deserialization of its data (mutators and accessors, parsers, serializers). That is, a DTO is a simple object that contains no business logic, but may include serialization and deserialization for transferring data.
by Wikipedia
Adding a post only requires a few properties of the Post model, so it's not suitable to use the entire Post model as a parameter. Therefore, we first need to define a DTO as the parameter for adding a post.
File path 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
With a DTO as the parameter, when saving a post we would need to manually assign the properties from the DTO object to the Post object one by one, like this:
var post = new Post {
Id = Guid.NewGuid(),
Title = dto.Title,
Summary = dto.Summary,
Content = dto.Content,
CategoryId = dto.CategoryId
};
One or two is fine, but with more APIs, a large amount of repetitive code becomes quite annoying, and it's also easy to make mistakes.
Fortunately, we can use the AutoMapper component to implement automatic object mapping.
Install the AutoMapper.Extensions.Microsoft.DependencyInjection package via nuget
Register the service:
builder.Services.AddAutoMapper(typeof(Program));
Then create the corresponding Profile (configuration). If there's no special configuration, you actually don't need to add this config file and can just use the default mapping behavior.
As an example, this article will briefly introduce it. Create the StarBlog.Web/Properties/AutoMapper/PostProfile.cs file
public class PostProfile : Profile {
public PostProfile() {
CreateMap<PostUpdateDto, Post>();
CreateMap<PostCreationDto, Post>();
}
}
In the constructor, call CreateMap to configure the mapping from left to right.
The code above configures the mapping from the PostUpdateDto / PostCreationDto objects to the Post object.
If some fields should not be mapped, you can write it like this:
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);
}
}
The rest of the code stays the same; just modify the _unmapped field.
Then inject the IMapper object in the Controller
private readonly IMapper _mapper;
Usage is very simple
var post = _mapper.Map<Post>(dto);
Pass in a dto of type PostCreationDto and you get a Post object.
Controller
First, the Controller code
[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;
}
}
Here's a one-by-one introduction to the four attributes on the Controller
Authorizemeans all APIs under this controller require login to accessApiControllerindicates this is a WebApi ControllerRoutespecifies this Controller's route template, meaning all the APIs below start withApi/BlogPostControllerApiExplorerSettingsis for API grouping, which looks clearer in the swagger docs
Next, add and update are two APIs, discussed separately.
Add
Easy, here's the code
[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));
}
This is the Add method
Currently CategoryService is only needed in this add API, so there's no need to inject it at the Controller level; inject it in the Add method using the [FromServices] attribute.
Later there's getting the category hierarchy. Because StarBlog is designed to support multi-level categories, to reduce computation when displaying the post category hierarchy on the frontend, I store the post's category hierarchy (in the form of category IDs separated by commas, e.g.: 1,3,5,7,9) directly in the database — trading space for time.
Finally, call the InsertOrUpdateAsync method in PostService to parse and process the post content and save the post to the database.
PS: This project's API return values have already been uniformly wrapped. You can see
ApiResponseused extensively as the return type; this will be covered in a later article.
Update
Oh, there's also the update post (Update) API. Updates use the PUT method
[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));
}
Still very simple; the comments inside are quite clear
AutoMapper can perform mapping on top of an existing object
mapper.Map(source)gets a brand new objectmapper.Map(source, dest)modifies based on the dest object
Done.
Service
As a multi-layered architecture project, the core logic still goes in the Service
And here create and update are combined into one — elegant~
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;
}
Additionally, this part of the code was already covered in previous articles related to markdown rendering and automatically downloading external images, so this article won't repeat it. For details, see article 17 of this series.
Delete
Not much to say, here's the code directly
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
Retrieve is split into two kinds: list and single.
Single
Let's start with the single one, which is relatively easy.
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);
}
Here the endpoint is decorated with [AllowAnonymous], meaning it can be accessed without logging in.
List
The simplest approach is to return the full list of posts directly.
[HttpGet]
public List<Post> GetAll() {
return _postService.GetAll();
}
The complete functionality is more complex, requiring filtering, sorting, pagination, and so on. These features were already covered in part 6. See: Building a Blog Project with .NetCore: StarBlog - (6) Page Development: Blog Post List
Related Operations on Posts
Plain CRUD is not enough to meet the functional requirements
So on top of RESTful APIs, we combine some RPC-style endpoints to implement the features we need.
Set Featured Post
There is a dedicated model for managing featured posts, named FeaturedPost
To set a featured post, just insert a record into it. Conversely, canceling means deleting the corresponding record.
Here's the code
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));
}
After configuration, the URL becomes: Api/BlogPost/{id}/SetFeatured
Cancel Featured Post
The reverse of the featured operation above
The service looks like this
public int DeleteFeaturedPost(Post post) {
var item = _fPostRepo.Where(a => a.PostId == post.Id).First();
return item == null ? 0 : _fPostRepo.Delete(item);
}
The controller goes like this
[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.");
}
Set as Top Post
StarBlog is designed to allow only one top post
Setting a new top post will knock off the existing one
Service code
/// <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);
}
First delete the existing top post, then insert the new one. The return value uses tuple syntax.
Controller code
[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."};
}
That's it, simple as that.
Upload Images
Scenario: when editing a post in the admin backend, some images will be inserted.
Because this endpoint needs to upload files, it receives parameters via FormData. The frontend needs to pay attention when making the request.
Here is the controller code:
[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)
});
}
The PostService.UploadImage() method that follows won't be covered in this article (due to length constraints). I'll leave a placeholder and introduce it later together with the image management APIs~
Related Blog Operations
Just now we were mostly doing CRUD on posts. Don't forget there's also a BlogController~😏
Its features include getting featured posts, getting the top post, blog overview, packaged upload, and so on.
I'll briefly introduce them here as well.
Service code for getting featured and top posts:
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;
}
The controller is too simple, so I won't write it out.
Overview Information
This isn't wrapped in a service—I don't think it will be used elsewhere, so no over-encapsulation.
Read directly from the ORM: counts of posts, categories, photos, featured items, etc.
PS: If we were to build a large-screen dashboard, this would still not be enough. We'll add more later
(flag planted)
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()
};
}
Packaged Upload
This feature works as follows: zip up locally written markdown files together with images and other resources and upload them. StarBlog parses the markdown, processes the image attachments, and stores them in the database, making it convenient to write posts locally and publish them to the blog.
The specific implementation was already covered in a previous article, so I won't repeat it here. For details, see part 18 of this series. Building a Blog Project with .NetCore: StarBlog - (18) Implementing Local Typora Post Packaged Upload
Summary
There's a lot that can be written about developing AspNetCore WebApi. I've been learning continuously during development, and many fun new features and slick tricks were added to the StarBlog project later. But to keep this series coherent to read, even if a feature was already implemented when the article was written, it might not be included. I'll write a separate article later to cover these (definitely not padding), to improve the reading experience.
Also, as a beginner-oriented tutorial, I'll try to write in fairly thorough detail (lots of rambling), which makes the articles long, but still can't cover every detail of AspNetCore. Readers who are learning as they go are advised to read along with the official AspNetCore docs or textbooks~
Series Articles
- Building a Blog Project with .NetCore: StarBlog - (1) Why Do You Need to Write Your Own Blog?
- Building a Blog Project with .NetCore: StarBlog - (2) Environment Setup and Creating the Project
- Building a Blog Project with .NetCore: StarBlog - (3) Model Design
- Building a Blog Project with .NetCore: StarBlog - (4) Batch Import of Markdown Blog Posts
- Building a Blog Project with .NetCore: StarBlog - (5) Starting to Build the Web Project
- Building a Blog Project with .NetCore: StarBlog - (6) Page Development: Blog Post List
- Building a Blog Project with .NetCore: StarBlog - (7) Page Development: Post Detail Page
- Building a Blog Project with .NetCore: StarBlog - (8) Displaying the Category Hierarchy
- Building a Blog Project with .NetCore: StarBlog - (9) Batch Image Import
- Building a Blog Project with .NetCore: StarBlog - (10) Image Waterfall Layout
- Building a Blog Project with .NetCore: StarBlog - (11) Implementing Visit Statistics
- Building a Blog Project with .NetCore: StarBlog - (12) Razor Page Dynamic Compilation
- Building a Blog Project with .NetCore: StarBlog - (13) Adding the Friend Links Feature
- Building a Blog Project with .NetCore: StarBlog - (14) Implementing Theme Switching
- Building a Blog Project with .NetCore: StarBlog - (15) Generating Random-Size Images
- Building a Blog Project with .NetCore: StarBlog - (16) Some New Features (Monitoring/Statistics/Config/Initialization)
- Building a Blog Project with .NetCore: StarBlog - (17) Automatically Downloading External Images in Posts
- Building a Blog Project with .NetCore: StarBlog - (18) Implementing Local Typora Post Packaged Upload
- Building a Blog Project with .NetCore: StarBlog - (19) Exploring Markdown Rendering Solutions
- Building a Blog Project with .NetCore: StarBlog - (20) Image Display Optimization
- Building a Blog Project with .NetCore: StarBlog - (21) Starting to Develop RESTful APIs
- Building a Blog Project with .NetCore: StarBlog - (22) Developing Blog Post Related APIs
_EOF_
StarBlog Development Notes (22): Developing Blog Post Related APIs
- 本文作者:
- 程序设计实验室
- 版权声明:
- 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
相关文章
StarBlog博客开发笔记(20):图片显示优化
前言 我的服务器带宽比较高,博客部署在上面访问的时候几乎没感觉有加载延迟,就没做图片这块的优化,不过最近有小伙伴说博客的图片加载比较慢,那就来把图片优化完善一下吧~ 目前有两个地方需要完善 图片瀑布流 图片缩略图 图片瀑布流 关于瀑布流之前的文章有介绍: 基于.NetCore开发博客项目 StarBlog - (10) 图片瀑布流 不过当时直接套用的 Bootstrap5 例子代码,有偶尔显示错位
2022-11-27 · StarBlog博客开发笔记 · 约 1 分钟
StarBlog博客开发笔记(3):模型设计
系列文章 基于.NetCore开发博客项目 StarBlog - (1) 为什么需要自己写一个博客? 基于.NetCore开发博客项目 StarBlog - (2) 环境准备和创建项目 基于.NetCore开发博客项目 StarBlog - (3) 模型设计 ... 基于.NetCore开发博客项目 StarBlog - 接口返回值包装 基于.NetCore开发博客项目 StarBlog - 上传
2022-04-22 · StarBlog博客开发笔记 · 约 1 分钟
StarBlog博客开发笔记(25):图片接口与文件上传
前言 上传文件的接口设计有两种风格,一种是整个项目只设置一个接口用来上传,然后其他需要用到文件的地方,都只存一个引用ID;另一种是每个需要文件的地方单独管理各自的文件。这俩各有优劣吧,本项目中选择的是后者的风格,文章图片和照片模块又要能CRUD又要批量导入,还是各自管理文件比较好。 图片接口 说会正题,先介绍一下图片相关接口。 图片列表 首先CRUD是肯定有的,图片列表的分页查看也是有的,不过因为
2023-02-05 · StarBlog博客开发笔记 · 约 1 分钟
StarBlog博客开发笔记(14):实现主题切换功能
系列文章 基于.NetCore开发博客项目 StarBlog - (1) 为什么需要自己写一个博客? 基于.NetCore开发博客项目 StarBlog - (2) 环境准备和创建项目 基于.NetCore开发博客项目 StarBlog - (3) 模型设计 基于.NetCore开发博客项目 StarBlog - (4) markdown博客批量导入 基于.NetCore开发博客项目 StarBl
2022-07-03 · StarBlog博客开发笔记 · 约 1 分钟
讨论
共 0 条加载评论中…