基于.NetCore開發博客項目 StarBlog - (24) 統一接口數據返回格式
前言
開發接口,是給客戶端(Web前端、App)用的,前面說的RESTFul,是接口的規范,有了統一的接口風格,客戶端開發人員在訪問后端功能的時候能更快找到需要的接口,能寫出可維護性更高的代碼。
而接口的數據返回格式也是接口規范的重要一環,不然一個接口返回JSON,一個返回純字符串,客戶端對接到數據時一臉懵逼,沒法處理啊。
合格的接口返回值應該包括狀態碼、提示信息和數據。
就像這樣:
{
"statusCode": 200,
"successful": true,
"message": null,
"data": {}
}
默認AspNetCore
的WebAPI
模板是沒有特定的返回格式,因為這些業務性質的東西需要開發者自己來定義和完成。
在前面的文章中,可以看到本項目的接口返回值都是 ApiResponse
及其派生類型,這就是在StarBlog里定制的統一返回格式。事實上我的其他項目也在用這套接口返回值,這已經算是一個 Utilities 性質的組件了。
PS:今天寫這篇文章時,我順手把這個返回值發布了一個nuget包,以后在其他項目里使用就不用復制粘貼了~
分析一下
在 AspNetCore 里寫 WebApi ,我們的 Controller 需要繼承 ControllerBase
這個類
接口 Action 可以設置返回值為 IActionResult
或 ActionResult<T>
類型,然后返回數據的時候,可以使用 ControllerBase
封裝好的 Ok()
, NotFound()
等方法,這些方法在返回數據的同時會自動設置響應的HTTP狀態碼。
PS:關于
IActionResult
或ActionResult<T>
這倆的區別請參考官方文檔。本文只提關鍵的一點:
ActionResult<T>
返回類型可以讓接口在swagger文檔中直觀看出返回的數據類型。
所以我們不僅要封裝統一的返回值,還要實現類似 Ok()
, NotFound()
, BadRequest()
的快捷方法。
顯然當接口返回類型全都是 ApiResponse<T>
時,這樣返回的狀態碼都是200,不符合需求。
而且有些接口之前已經寫好了,返回類型是 List<T>
這類的,我們也要把這些接口的返回值包裝起來,統一返回格式。
要解決這些問題,我們得了解一下 AspNetCore 的管道模型。
AspNetCore 管道模型
最外層,是中間件,一個請求進來,經過一個個中間件,到最后一個中間件,生成響應,再依次經過一個個中間件走出來,得到最終響應。
常用的 AspNetCore 項目中間件有這些,如下圖所示:
最后的 Endpoint 就是最終生成響應的中間件。
在本項目中,Program.cs
配置里的最后一個中間件,就是添加了一個處理 MVC 的 Endpoint
app.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
這個 Endpoint 的結構又是這樣的:
可以看到有很多 Filter 包圍在用戶代碼的前后。
所以得出結論,要修改請求的響應,我們可以選擇:
- 寫一個中間件處理
- 使用過濾器(Filter)
那么,來開始寫代碼吧~
定義ApiResponse
首先是這個出現頻率很高的 ApiResponse
,終于要揭曉了~
在 StarBlog.Web/ViewModels/Response
命名空間下,我創建了三個文件,分別是:
- ApiResponse.cs
- ApiResponsePaged.cs: 分頁響應
- IApiResponse.cs: 幾個相關的接口
ApiResponse.cs 中,其實是兩個類,一個 ApiResponse<T>
,另一個 ApiResponse
,帶泛型和不帶泛型。
PS:C#的泛型有點復雜,當時搞這東西搞得暈暈的,又復習了一些逆變和協變,不過最終沒有用上。
接口代碼
上代碼,先是幾個接口的代碼
public interface IApiResponse {
public int StatusCode { get; set; }
public bool Successful { get; set; }
public string? Message { get; set; }
}
public interface IApiResponse<T> : IApiResponse {
public T? Data { get; set; }
}
public interface IApiErrorResponse {
public Dictionary<string,object> ErrorData { get; set; }
}
保證了所有相關對象都來自 IApiResponse
接口。
ApiResponse<T>
接著看 ApiResponse<T>
的代碼。
public class ApiResponse<T> : IApiResponse<T> {
public ApiResponse() {
}
public ApiResponse(T? data) {
Data = data;
}
public int StatusCode { get; set; } = 200;
public bool Successful { get; set; } = true;
public string? Message { get; set; }
public T? Data { get; set; }
/// <summary>
/// 實現將 <see cref="ApiResponse"/> 隱式轉換為 <see cref="ApiResponse{T}"/>
/// </summary>
/// <param name="apiResponse"><see cref="ApiResponse"/></param>
public static implicit operator ApiResponse<T>(ApiResponse apiResponse) {
return new ApiResponse<T> {
StatusCode = apiResponse.StatusCode,
Successful = apiResponse.Successful,
Message = apiResponse.Message
};
}
}
這里使用運算符重載,實現了 ApiResponse
到 ApiResponse<T>
的隱式轉換。
等下就能看出有啥用了~
ApiResponse
繼續看 ApiResponse
代碼,比較長,封裝了幾個常用的方法在里面,會有一些重復代碼。
這個類實現了倆接口:IApiResponse
, IApiErrorResponse
public class ApiResponse : IApiResponse, IApiErrorResponse {
public int StatusCode { get; set; } = 200;
public bool Successful { get; set; } = true;
public string? Message { get; set; }
public object? Data { get; set; }
/// <summary>
/// 可序列化的錯誤
/// <para>用于保存模型驗證失敗的錯誤信息</para>
/// </summary>
public Dictionary<string,object>? ErrorData { get; set; }
public ApiResponse() {
}
public ApiResponse(object data) {
Data = data;
}
public static ApiResponse NoContent(string message = "NoContent") {
return new ApiResponse {
StatusCode = StatusCodes.Status204NoContent,
Successful = true, Message = message
};
}
public static ApiResponse Ok(string message = "Ok") {
return new ApiResponse {
StatusCode = StatusCodes.Status200OK,
Successful = true, Message = message
};
}
public static ApiResponse Ok(object data, string message = "Ok") {
return new ApiResponse {
StatusCode = StatusCodes.Status200OK,
Successful = true, Message = message,
Data = data
};
}
public static ApiResponse Unauthorized(string message = "Unauthorized") {
return new ApiResponse {
StatusCode = StatusCodes.Status401Unauthorized,
Successful = false, Message = message
};
}
public static ApiResponse NotFound(string message = "NotFound") {
return new ApiResponse {
StatusCode = StatusCodes.Status404NotFound,
Successful = false, Message = message
};
}
public static ApiResponse BadRequest(string message = "BadRequest") {
return new ApiResponse {
StatusCode = StatusCodes.Status400BadRequest,
Successful = false, Message = message
};
}
public static ApiResponse BadRequest(ModelStateDictionary modelState, string message = "ModelState is not valid.") {
return new ApiResponse {
StatusCode = StatusCodes.Status400BadRequest,
Successful = false, Message = message,
ErrorData = new SerializableError(modelState)
};
}
public static ApiResponse Error(string message = "Error", Exception? exception = null) {
object? data = null;
if (exception != null) {
data = new {
exception.Message,
exception.Data
};
}
return new ApiResponse {
StatusCode = StatusCodes.Status500InternalServerError,
Successful = false,
Message = message,
Data = data
};
}
}
ApiResponsePaged<T>
這個分頁是最簡單的,只是多了個 Pagination
屬性而已
public class ApiResponsePaged<T> : ApiResponse<List<T>> where T : class {
public ApiResponsePaged() {
}
public ApiResponsePaged(IPagedList<T> pagedList) {
Data = pagedList.ToList();
Pagination = pagedList.ToPaginationMetadata();
}
public PaginationMetadata? Pagination { get; set; }
}
類型隱式轉換
來看這個接口
public ApiResponse<Post> Get(string id) {
var post = _postService.GetById(id);
return post == null ? ApiResponse.NotFound() : new ApiResponse<Post>(post);
}
根據上面的代碼,可以發現 ApiResponse.NotFound()
返回的是一個 ApiResponse
對象
但這接口的返回值明明是 ApiResponse<Post>
類型呀,這不是類型不一致嗎?
不過在 ApiResponse<T>
中,我們定義了一個運算符重載,實現了 ApiResponse
類型到 ApiResponse<T>
的隱式轉換,所以就完美解決這個問題,大大減少了代碼量。
不然原本是要寫成這樣的
return post == null ?
new ApiResponse<Post> {
StatusCode = StatusCodes.Status404NotFound,
Successful = false, Message = "未找到"
} :
new ApiResponse<Post>(post);
現在只需簡簡單單的 ApiResponse.NotFound()
,就跟 AspNetCore 自帶的一樣妙~
包裝返回值
除了這些以 ApiResponse
或 ApiResponse<T>
作為返回類型的接口,還有很多其他返回類型的接口,比如
public List<ConfigItem> GetAll() {
return _service.GetAll();
}
還有
public async Task<string> Poem() {
return await _crawlService.GetPoem();
}
這些接口在 AspNetCore 生成響應的時候,會把這些返回值歸類為 ObjectResult
,如果不做處理,就會直接序列化成不符合我們返回值規范的格式。
這個不行,必須對這部分接口的返回格式也統一起來。
因為種種原因,最終我選擇使用過濾器來實現這個功能。
關于過濾器的詳細用法,可以參考官方文檔,本文就不展開了,直接上代碼。
創建文件 StarBlog.Web/Filters/ResponseWrapperFilter.cs
public class ResponseWrapperFilter : IAsyncResultFilter {
public async Task OnResultExecutionAsync(ResultExecutingContext context, ResultExecutionDelegate next) {
if (context.Result is ObjectResult objectResult) {
if (objectResult.Value is IApiResponse apiResponse) {
objectResult.StatusCode = apiResponse.StatusCode;
context.HttpContext.Response.StatusCode = apiResponse.StatusCode;
}
else {
var statusCode = objectResult.StatusCode ?? context.HttpContext.Response.StatusCode;
var wrapperResp = new ApiResponse<object> {
StatusCode = statusCode,
Successful = statusCode is >= 200 and < 400,
Data = objectResult.Value,
};
objectResult.Value = wrapperResp;
objectResult.DeclaredType = wrapperResp.GetType();
}
}
await next();
}
}
在代碼中進行判斷,當響應的類型是 ObjectResult
時,把這個響應結果拿出來,再判斷是不是 IApiResponse
類型。
前面我們介紹過,所有 ApiResponse
都實現了 IApiResponse
這個接口,所以可以判斷是不是 IApiResponse
類型來確定這個返回結果是否包裝過。
沒包裝的話就給包裝一下,就這么簡單。
之后在 Program.cs
里注冊一下這個過濾器。
var mvcBuilder = builder.Services.AddControllersWithViews(
options => { options.Filters.Add<ResponseWrapperFilter>(); }
);
搞定
這樣就完事兒啦~
最后所有接口(可序列化的),返回格式就都變成了這樣
{
"statusCode": 200,
"successful": true,
"message": null,
"data": {}
}
強迫癥表示舒服了~
PS:對了,返回文件的那類接口除外。
在其他項目中使用
這個 ApiRepsonse
,我已經發布了nuget包
需要在其他項目使用的話,可以直接安裝 CodeLab.Share
這個包
引入 CodeLab.Share.ViewModels.Response
命名空間就完事了~
不用每次都復制粘貼這幾個類,還得改命名空間。
PS:這個包里不包括過濾器!
參考資料
系列文章
- 基于.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 - (23) 文章列表接口分頁、過濾、搜索、排序
- 基于.NetCore開發博客項目 StarBlog - (24) 統一接口數據返回格式