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) 统一接口数据返回格式