基于.NetCore开发博客项目 StarBlog - (24) 统一接口数据返回格式

基于,netcore,开发,博客,项目,starblog,统一,接口,数据,返回,格式 · 浏览次数 : 1280

小编点评

**关于过滤器** 过滤器是将请求转换为响应时的一种中间过程。它可以应用于各种请求,例如获取数据、处理请求或设置响应。 **过滤器的主要用途** * **对请求进行处理:**过滤器可以应用于各种请求,例如获取数据、处理请求或设置响应。 * **将请求转换为响应:**过滤器可以将请求转换为响应,例如将数据请求转换为响应,或将请求转换为响应,例如设置响应。 * **设置响应:**过滤器可以设置响应,例如设置状态码或响应体。 **一些常用的过滤器** * **ResponseWrapperFilter**:该过滤器用于响应包装,它可以将响应结果包装成一个新的响应体。 * **IAsyncFilter**:该接口用于异步过滤器,它可以用于在非阻塞请求中进行异步操作。 * **ResultExecutingContext**:该接口用于结果执行上下文,它可以用于在非阻塞请求中进行异步操作。 **创建过滤器** 创建过滤器的方法需要传递请求参数。例如,可以使用 `ResponseWrapperFilter` 创建响应包装过滤器,可以使用 `IAsyncFilter` 创建异步过滤器,可以使用 `ResultExecutingContext` 创建非阻塞过滤器。 **注册过滤器** 可以注册过滤器的方法在 `Configure` 方法中。例如,可以使用 `builder.Services.AddControllersWithViews` 注册控制器,可以使用 `builder.Services.AddFilters` 注册过滤器。 **使用过滤器** 可以使用过滤器在 `Controller` 中使用。例如,可以使用 `ResponseWrapperFilter` 在 `Get` 方法中使用响应包装过滤器,可以使用 `IAsyncFilter` 在 `GetAsync` 方法中使用异步过滤器。

正文

前言

开发接口,是给客户端(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 管道模型

最外层,是中间件,一个请求进来,经过一个个中间件,到最后一个中间件,生成响应,再依次经过一个个中间件走出来,得到最终响应。

image

常用的 AspNetCore 项目中间件有这些,如下图所示:

image

最后的 Endpoint 就是最终生成响应的中间件。

在本项目中,Program.cs 配置里的最后一个中间件,就是添加了一个处理 MVC 的 Endpoint

app.MapControllerRoute(
    name: "default",
    pattern: "{controller=Home}/{action=Index}/{id?}");

这个 Endpoint 的结构又是这样的:

image

可以看到有很多 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) 统一接口数据返回格式相似的内容:

基于.NetCore开发博客项目 StarBlog - (24) 统一接口数据返回格式

## 前言 开发接口,是给客户端(Web前端、App)用的,前面说的RESTFul,是接口的规范,有了统一的接口风格,客户端开发人员在访问后端功能的时候能更快找到需要的接口,能写出可维护性更高的代码。 而接口的数据返回格式也是接口规范的重要一环,不然一个接口返回JSON,一个返回纯字符串,客户端对接

基于.NetCore开发博客项目 StarBlog - (19) Markdown渲染方案探索

## 前言 笔者认为,一个博客网站,最核心的是阅读体验。 在开发StarBlog的过程中,最耗时的恰恰也是文章的展示部分功能。 最开始还没研究出来如何很好的使用后端渲染,所以只能先用Editor.md组件做前端渲染,过渡一下。前端渲染我是不满意的,因为性能较差,页面加载出来还会闪一下,有割裂感,影响

基于.NetCore开发博客项目 StarBlog - (20) 图片显示优化

## 前言 我的服务器带宽比较高,博客部署在上面访问的时候几乎没感觉有加载延迟,就没做图片这块的优化,不过最近有小伙伴说博客的图片加载比较慢,那就来把图片优化完善一下吧~ 目前有两个地方需要完善 - 图片瀑布流 - 图片缩略图 ## 图片瀑布流 关于瀑布流之前的文章有介绍: [基于.NetCore开

基于.NetCore开发博客项目 StarBlog - (21) 开始开发RESTFul接口

## 前言 最近电脑坏了,开源项目的进度也受到一些影响 这篇酝酿很久了,作为本系列第二部分(API接口开发)的第一篇,得想一个好的开头,想着想着就鸽了好久,索性不扯那么多了,直接开写吧~ ## 关于RESTFul 网上很多相关的文章都要把RESTFul历史来龙去脉给复制一遍,所以我这就不重复了,现在

基于.NetCore开发博客项目 StarBlog - (22) 开发博客文章相关接口

## 前言 本文介绍博客文章相关接口的开发,作为接口开发介绍的第一篇,会写得比较详细,以抛砖引玉,后面的其他接口就粗略带过了,着重于WebApi开发的周边设施。 涉及到的接口:文章CRUD、置顶文章、推荐文章等。 开始前先介绍下AspNetCore框架的基础概念,MVC模式(前后端不分离)、WebA

基于.NetCore开发博客项目 StarBlog - (23) 文章列表接口分页、过滤、搜索、排序

## 前言 上一篇留的坑,火速补上。 在之前的第6篇中,已经有初步介绍,本文做一些补充,已经搞定这部分的同学可以快速跳过,[基于.NetCore开发博客项目 StarBlog - (6) 页面开发之博客文章列表](https://www.cnblogs.com/deali/p/16286780.ht

基于.NetCore开发博客项目 StarBlog - (25) 图片接口与文件上传

## 前言 上传文件的接口设计有两种风格,一种是整个项目只设置一个接口用来上传,然后其他需要用到文件的地方,都只存一个引用ID;另一种是每个需要文件的地方单独管理各自的文件。这俩各有优劣吧,本项目中选择的是后者的风格,文章图片和照片模块又要能CRUD又要批量导入,还是各自管理文件比较好。 ## 图片

基于.NetCore开发博客项目 StarBlog - (26) 集成Swagger接口文档

## 前言 这是StarBlog系列在2023年的第一篇更新😃~ 在之前的文章里,我们已经完成了部分接口的开发,接下来需要使用 curl、Postman 这类工具对这些接口进行测试,但接口一多,每次测试都要一个个填入地址和对应参数会比较麻烦… 我们需要一种直观的方式来汇总项目里的所有接口,并且如果

基于.NetCore开发博客项目 StarBlog - (27) 使用JWT保护接口

## 前言 这是StarBlog系列在2023年的第二篇更新😂 这几个月都在忙,更新变得很不勤快,但是拖着不更新我的心里更慌,很久没写,要开头就变得很难😑 说回正题,之前的文章里,我们已经把博客关键的接口都开发完成了,但还少了一个最关键的「认证授权」,少了这东西,网站就跟筛子一样,谁都可以来添加

基于.NetCore开发博客项目 StarBlog - (28) 开发友情链接相关接口

## 前言 之前介绍的友情链接功能,只实现了友情链接的展示和管理接口。 还缺失友情链接申请、审核管理、通知,现在把这块功能补全。 Model 什么的之前那篇文章都有,本文直接补全逻辑代码~ 详见: [基于.NetCore开发博客项目 StarBlog - (13) 加入友情链接功能](https:/