基于.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();
}
完整功能比較復雜,需要過濾篩選、排序、分頁等功能,這些功能在之前第6篇已經介紹過了。詳見:基于.NetCore開發博客項目 StarBlog - (6) 頁面開發之博客文章列表
文章的相關操作
單純的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) 開發博客文章相關接口

浙公網安備 33010602011771號