1前言
開發接口,是給客戶端(Web前端、App)用的,前面說的RESTFul,是接口的規范,有了統一的接口風格,客戶端開發人員在訪問后端功能的時候能更快找到需要的接口,能寫出可維護性更高的代碼。
而接口的數據返回格式也是接口規范的重要一環,不然一個接口返回JSON,一個返回純字符串,客戶端對接到數據時一臉懵逼,沒法處理啊。
合格的接口返回值應該包括狀態碼、提示信息和數據。
就像這樣:
{"statusCode":?200,"successful":?true,"message":?null,"data":?{}
}
默認AspNetCore
的WebAPI
模板是沒有特定的返回格式,因為這些業務性質的東西需要開發者自己來定義和完成。
在前面的文章中,可以看到本項目的接口返回值都是 ApiResponse
及其派生類型,這就是在StarBlog里定制的統一返回格式。事實上我的其他項目也在用這套接口返回值,這已經算是一個 Utilities 性質的組件了。
PS:今天寫這篇文章時,我順手把這個返回值發布了一個nuget包,以后在其他項目里使用就不用復制粘貼了~
2分析一下
在 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)
那么,來開始寫代碼吧~
3定義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;?}
}
4類型隱式轉換
來看這個接口
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 自帶的一樣妙~
5包裝返回值
除了這些以 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>();?}
);
6搞定
這樣就完事兒啦~
最后所有接口(可序列化的),返回格式就都變成了這樣
{"statusCode":?200,"successful":?true,"message":?null,"data":?{}
}
強迫癥表示舒服了~
PS:對了,返回文件的那類接口除外。
7在其他項目中使用
這個 ApiRepsonse
,我已經發布了nuget包
需要在其他項目使用的話,可以直接安裝 CodeLab.Share
這個包
引入 CodeLab.Share.ViewModels.Response
命名空間就完事了~
不用每次都復制粘貼這幾個類,還得改命名空間。
PS:這個包里不包括過濾器!
8參考資料
https://learn.microsoft.com/en-us/aspnet/core/fundamentals/middleware/?view=aspnetcore-7.0
9系列文章
基于.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) 統一接口數據返回格式