前言

开发接口,是给客户端(Web前端、App)用的,前面说的RESTFul,是接口的规范,有了统一的接口风格,客户端开发人员在访问后端功能的时候能更快找到需要的接口,能写出可维护性更高的代码。

而接口的数据返回格式也是接口规范的重要一环,不然一个接口返回JSON,一个返回纯字符串,客户端对接到数据时一脸懵逼,没法处理啊。

合格的接口返回值应该包括状态码、提示信息和数据。

就像这样:

{
"statusCode": 200,
"successful": true,
"message": null,
"data": {}
}

默认AspNetCoreWebAPI模板是没有特定的返回格式,因为这些业务性质的东西需要开发者自己来定义和完成。

在前面的文章中,可以看到本项目的接口返回值都是 ApiResponse 及其派生类型,这就是在StarBlog里定制的统一返回格式。事实上我的其他项目也在用这套接口返回值,这已经算是一个 Utilities 性质的组件了。

PS:今天写这篇文章时,我顺手把这个返回值发布了一个nuget包,以后在其他项目里使用就不用复制粘贴了~

分析一下

在 AspNetCore 里写 WebApi ,我们的 Controller 需要继承 ControllerBase 这个类

接口 Action 可以设置返回值为 IActionResultActionResult<T> 类型,然后返回数据的时候,可以使用 ControllerBase 封装好的 Ok(), NotFound() 等方法,这些方法在返回数据的同时会自动设置响应的HTTP状态码。

PS:关于 IActionResultActionResult<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
};
}
}

这里使用运算符重载,实现了 ApiResponseApiResponse<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 自带的一样妙~

包装返回值

除了这些以 ApiResponseApiResponse<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 - (24) 统一接口数据返回格式的更多相关文章

  1. 基于.NetCore开发博客项目 StarBlog - (16) 一些新功能 (监控/统计/配置/初始化)

    系列文章 基于.NetCore开发博客项目 StarBlog - (1) 为什么需要自己写一个博客? 基于.NetCore开发博客项目 StarBlog - (2) 环境准备和创建项目 基于.NetC ...

  2. 基于.NetCore开发博客项目 StarBlog - (2) 环境准备和创建项目

    系列文章 基于.NetCore开发博客项目 StarBlog - (1) 为什么需要自己写一个博客? 基于.NetCore开发博客项目 StarBlog - (2) 环境准备和创建项目 ... 基于. ...

  3. 基于.NetCore开发博客项目 StarBlog - (3) 模型设计

    系列文章 基于.NetCore开发博客项目 StarBlog - (1) 为什么需要自己写一个博客? 基于.NetCore开发博客项目 StarBlog - (2) 环境准备和创建项目 基于.NetC ...

  4. 基于.NetCore开发博客项目 StarBlog - (4) markdown博客批量导入

    系列文章 基于.NetCore开发博客项目 StarBlog - (1) 为什么需要自己写一个博客? 基于.NetCore开发博客项目 StarBlog - (2) 环境准备和创建项目 基于.NetC ...

  5. 基于.NetCore开发博客项目 StarBlog - (5) 开始搭建Web项目

    系列文章 基于.NetCore开发博客项目 StarBlog - (1) 为什么需要自己写一个博客? 基于.NetCore开发博客项目 StarBlog - (2) 环境准备和创建项目 基于.NetC ...

  6. 基于.NetCore开发博客项目 StarBlog - (6) 页面开发之博客文章列表

    系列文章 基于.NetCore开发博客项目 StarBlog - (1) 为什么需要自己写一个博客? 基于.NetCore开发博客项目 StarBlog - (2) 环境准备和创建项目 基于.NetC ...

  7. 基于.NetCore开发博客项目 StarBlog - (7) 页面开发之文章详情页面

    系列文章 基于.NetCore开发博客项目 StarBlog - (1) 为什么需要自己写一个博客? 基于.NetCore开发博客项目 StarBlog - (2) 环境准备和创建项目 基于.NetC ...

  8. 基于.NetCore开发博客项目 StarBlog - (8) 分类层级结构展示

    系列文章 基于.NetCore开发博客项目 StarBlog - (1) 为什么需要自己写一个博客? 基于.NetCore开发博客项目 StarBlog - (2) 环境准备和创建项目 基于.NetC ...

  9. 基于.NetCore开发博客项目 StarBlog - (9) 图片批量导入

    系列文章 基于.NetCore开发博客项目 StarBlog - (1) 为什么需要自己写一个博客? 基于.NetCore开发博客项目 StarBlog - (2) 环境准备和创建项目 基于.NetC ...

  10. 基于.NetCore开发博客项目 StarBlog - (10) 图片瀑布流

    系列文章 基于.NetCore开发博客项目 StarBlog - (1) 为什么需要自己写一个博客? 基于.NetCore开发博客项目 StarBlog - (2) 环境准备和创建项目 基于.NetC ...

随机推荐

  1. 【前端必会】tapable、hook,webpack的灵魂

    背景 什么是tapable.hook,平时做vue开发时的webpack 配置一直都没弄懂,你也有这种情况吗? 还是看源码,闲来无聊又看一下webpack的源码,看看能否找到一些宝藏 tapable和 ...

  2. ERP 系统的核心是什么?有什么作用?

    ERP系统的核心就是系统的内部业务逻辑,这也是ERP复杂.专业性的体现!ERP系统需要适配企业的管理思想和业务流程,在技术上面也也要做到快速部署和个性化定制(客户化定制),而这些企业的规模不同.行业不 ...

  3. 修改-Python函数-2

    一.导入 $$f ( x , y ) = 2 x + 3 y$$ 上面括号里面的就是数学公式里的自变量,自变量就相当于函数里的参数. 二.为什么要有参数 如果一个大楼里有两种尺寸不一的窗户,显然在没有 ...

  4. 这次彻底读透 Redis

    1. Redis 管道 我们通常使用 Redis 的方式是,发送命令,命令排队,Redis 执行,然后返回结果,这个过程称为Round trip time(简称RTT, 往返时间).但是如果有多条命令 ...

  5. Spark基本知识

    Spark基本知识 Spark 是一种基于内存的快速.通用.可扩展的大数据分析计算引擎. spark与hadoop的区别 Hadoop Hadoop 是由 java 语言编写的,在分布式服务器集群上存 ...

  6. Day14 note1

    package com.oop.demo06;public class Person { public void run(){ System.out.println("run"); ...

  7. ES的java端API操作

    首先简单介绍下写这篇博文的背景,最近负责的一个聚合型的新项目要大量使用ES的检索功能,之前对es的了解还只是纯理论最多加个基于postman的索引创建操作,所以这次我得了解在java端如何编码实现:网 ...

  8. perl中ENV的使用

    在打印环境变量的时候可以用到.实际上是%ENV,perl中的哈希变量,里面保存的是环境变量.键是环境变量名,值是环境变量值.例如,有一个环境变量是PATH,其值为C:\windows,那么,打印这个环 ...

  9. 【ELK解决方案】ELK集群+RabbitMQ部署方案以及快速开发RabbitMQ生产者与消费者基础服务

    前言: 大概一年多前写过一个部署ELK系列的博客文章,前不久刚好在部署一个ELK的解决方案,我顺便就把一些基础的部分拎出来,再整合成一期文章.大概内容包括:搭建ELK集群,以及写一个简单的MQ服务. ...

  10. 大数据下一代变革之必研究数据湖技术Hudi原理实战双管齐下-上

    @ 目录 概述 定义 发展历史 特性 使用场景 编译安装 编译环境 编译Hudi 关键概念 TimeLine(时间轴) File Layouts(文件布局) 索引 表类型 查询类型 概述 定义 Apa ...