/Cuture.AspNetCore.ResponseAutoWrapper

用于asp.net core的响应和异常自动包装器,使Action提供一致的响应内容格式

Primary LanguageC#MIT LicenseMIT

Cuture.AspNetCore.ResponseAutoWrapper

1. Intro

用于asp.net core的响应和异常自动包装器,使Action提供一致的响应内容格式

  • 不需要修改 ControllerAction 返回类型即可自动包装;
  • 支持Swagger,能够正确展示包装后的类型结构;
  • 支持自定义响应结构、自定义异常解析,取消状态码覆写等;
  • 支持复杂类型的 CodeMessage,不局限于 intstring
  • 基于asp.net core自身的特性实现,兼容性较好,性能影响较低(目前只做了初步的测试,在简单场景下,性能降低大概在5%左右);
  • 灵活的筛选方式,可以更准确的筛选出不需要包装的Action;

NOTE!!!

  • 不支持包装 Middleware 直接写入的响应内容,以及各种直接 MapMiniApi;(但出现异常时还是会触发异常包装)

执行流程概览: 执行流程概览

2. 注意项

  • 目标框架net6.0+
  • 包装功能由两个包装器实现:
    • 基于ResultFilterActionResult包装器:针对方法的返回值包装;
    • 基于中间件的包装器:针对异常、非200响应包装;
  • 默认响应格式为
    {
        "code": 200,  //状态码 (int)
        "message": "string", //消息 (string)
        "data": {}  //Action的原始响应内容
    }
  • 四个针对场景的包装器(都已经有默认实现,可以自行实现后注入DI容器,替换默认的功能):
    • IActionResultWrapper<TResponse, TCode, TMessage>: 针对ActionResult的包装器;
    • IExceptionWrapper<TResponse, TCode, TMessage>: 针对中间件中捕获到异常的包装器;
    • IInvalidModelStateWrapper<TResponse, TCode, TMessage>: 参数验证失败的包装器;
    • INotOKStatusCodeWrapper<TResponse, TCode, TMessage>: 中间件中StatusCode200的响应包装器;
  • 默认的IActionResultWrapper实现只会处理ObjectResultEmptyResult

可能与其它第三方组件存在的冲突点

  • ResultFilter中会频繁未加锁读取ActionDescriptor.Properties,如果存在不正确的写入,可能引发一些问题;
  • 使用动态添加ProducesResponseTypeAttribute的方式实现的OpenAPI支持,可能存在不完善的地方;
  • asp.net core3.1不支持IAuthorizationMiddlewareResultHandler,不支持选项HandleAuthorizationResult
  • 授权认证失败的包装需要手动指定对应组件的失败处理方法,否则可能无法包装;
  • 参数验证失败的包装通过设置ApiBehaviorOptions.InvalidModelStateResponseFactory实现,可能有处理逻辑冲突;

3. 如何使用

3.1 安装Nuget

Install-Package Cuture.AspNetCore.ResponseAutoWrapper -IncludePrerelease

3.2 启用ResultFilter包装器

Startup.ConfigureServices中添加相关服务并进行配置

services.AddResponseAutoWrapper(options =>
{
    //options.ActionNoWrapPredicate     //Action的筛选委托,默认会过滤掉标记了NoResponseWrapAttribute的方法
    //options.DisableOpenAPISupport     //禁用OpenAPI支持,Swagger将不会显示包装后的格式,也会解除响应类型必须为object泛型的限制
    //options.HandleAuthorizationResult     //处理授权结果(可能无效,需要自行测试)
    //options.HandleInvalidModelState       //处理无效模型状态
    //options.RewriteStatusCode;     //包装时不覆写非200的HTTP状态码
});

3.3 启用中间件包装器

Startup.Configure中启用中间件并进行配置

app.UseResponseAutoWrapper(options =>
{
    //options.CatchExceptions 是否捕获异常
    //options.ThrowCaughtExceptions 捕获到异常处理结束后,是否再将异常抛出
    //options.DefaultOutputFormatterSelector 默认输出格式化器选择委托,选择在请求中无 Accept 时,用于格式化响应的 IOutputFormatter
});

至此所有相关配置完成,Action的响应内容将被自动包装;


4. 定制化

4.1 自定义消息内容

  • 方法一:Action方法直接返回TResponse及其子类时,不会对其进行包装,默认TResponseGenericApiResponse<int, string, object>,使用默认配置时,方法直接返回ApiResponse及其子类即可

    [HttpGet]
    public ApiResponse GetWithCustomMessage()
    {
        return EmptyApiResponse.Create("自定义消息");
    }

    返回结果为

    {
    "data": null,
    "code": 200,
    "message": "自定义消息"
    }
  • 方法二:通过Microsoft.AspNetCore.Http命名空间下HttpContext的拓展方法DescribeResponse<TCode, TMessage>进行描述

    [HttpGet]
    public WeatherForecast[] Get()
    {
        HttpContext.DescribeResponse(10086, "Hello world!");
        return null;
    }

    返回结果为

    {
    "data": null,
    "code": 10086,
    "message": "Hello world!"
    }

4.2 自定义统一响应类型TResponse

默认的ApiResponse不能满足需求时,可自行实现并替换TResponse

4.1.1 定义类型

public class CommonResponse<TData>
{
    public string Code { get; set; }

    public string Tips { get; set; }

    public TData Result { get; set; }
}
  • Data 对应的泛型参数必须为最后一个泛型参数;
  • 当禁用 OpenAPI支持 时,响应类型可以不是泛型;

4.1.2 实现Wrapper

Wrapper可以自行分别实现每个接口,也可以继承 AbstractResponseWrapper<TResponse, TCode, TMessage> 快速实现

public class CustomWrapper : AbstractResponseWrapper<CommonResponse<object>, string, string>
{
    public CustomWrapper(IWrapTypeCreator<string, string> wrapTypeCreator, IOptions<ResponseAutoWrapperOptions> optionsAccessor) : base(wrapTypeCreator, optionsAccessor)
    {
    }

    public override CommonResponse<object>? ExceptionWrap(HttpContext context, Exception exception)
    {
        return new CommonResponse<object>() { Code = "E4000", Tips = "SERVER ERROR" };
    }

    public override CommonResponse<object>? InvalidModelStateWrap(ActionContext context)
    {
        return new CommonResponse<object>() { Code = "E3000", Tips = "SERVER ERROR" };
    }

    public override CommonResponse<object>? NotOKStatusCodeWrap(HttpContext context)
    {
        return null;
    }

    protected override CommonResponse<object>? ActionEmptyResultWrap(ResultExecutingContext context, EmptyResult emptyResult, ResponseDescription<string, string>? description)
    {
        return new CommonResponse<object>() { Code = description?.Code ?? "E2000", Tips = description?.Message ?? "NO CONTENT" };
    }

    protected override CommonResponse<object>? ActionObjectResultWrap(ResultExecutingContext context, ObjectResult objectResult, ResponseDescription<string, string>? description)
    {
        return new CommonResponse<object>() { Code = description?.Code ?? "E2000", Tips = description?.Message ?? "NO CONTENT", Result = objectResult.Value };
    }
}
  • wrapper 返回 null 时,则不进行包装;

4.1.3 配置使用自定义类型

services.AddResponseAutoWrapper<CommonResponse<object>, string, string>()
        .ConfigureWrappers(options => options.AddWrappers<CustomWrapper>());
  • Response Data 对应的泛型参数在此处必须为 object
  • 此示例中 TCodestringTMessagestring,则使用 DescribeResponse 进行描述时,参数类型必须对应为 string, string

至此已完成配置,统一响应内容格式变更为:

{
    "code": "string",
    "tips": "string",
    "result": {}
}

Note!!!

  • 仅当禁用OpenAPI支持时TResponse才能不是一个泛型参数为object的泛型;
  • 更多信息可参考 sample/CustomStructureWebApplicationsample/SimpleWebApplication 以及 test/ResponseAutoWrapper.TestHost 项目;
  • 默认情况下不会包装使用[NoResponseWrapAttribute]标记的方法;

4.3 其它自定义

使用自行实现的接口注入DI容器替换掉默认实现即可完成一些其它的自定义

  • IActionResultWrapper<TResponse, TCode, TMessage>: ActionResult包装器;
  • IExceptionWrapper<TResponse, TCode, TMessage>: 捕获异常时的响应包装器;
  • IInvalidModelStateWrapper<TResponse, TCode, TMessage>: 模型验证失败时的响应包装器;
  • INotOKStatusCodeWrapper<TResponse, TCode, TMessage>: 非200状态码时的响应包装器;
  • IWrapTypeCreator<TCode, TMessage>: 确认Action返回对象类型是否需要包装,以及创建OpenAPI展示的泛型类;

4.4 动态取消包装

调用 HttpContext 的拓展方法 DoNotWrapResponse ,以动态的取消对当前响应的包装;使用拓展方法 IsSetDoNotWrapResponse 可以检查当前上下文是否已标记为不包装响应值;

HttpContext.DoNotWrapResponse();

5. 性能测试结果

多次迭代后,数据可能略微变动,但影响理论上仍然是固定比例的,不会随响应内容大小而变化

  • 系统:Ubuntu20.04 on WSL2 host by Windows10-21H1
  • CPU:I7-8700
  • 平台:asp.net core 5.0
  • 测试软件:wrk
  • 测试软件参数:-t 3 -c 100 -d 30s
  • 测试Action为:
    [HttpGet]
    public IEnumerable<WeatherForecast> Get(int count = 5)
    {
        return WeatherForecast.GenerateData(count);
    }
  • 测试均为使用localhost,以尽量减少网络的影响;
  • 对比对象分别为:
    • Origin:原生,没有进行包装
    • Cuture.AspNetCore.ResponseAutoWrapper:此自动包装库
    • AutoWrapper.Core:另一个同类型的自动包装库
  • 结果为多次运行取峰值;
  • 测试环境不专业,会有一定的误差,数值仅供参考;

数据(单位 Requests/sec

count Origin Cuture.AspNetCore.ResponseAutoWrapper AutoWrapper.Core
1 123267.40 111868.34 91202.04
10 108264.80 103125.32 67001.92
50 76310.72 73451.83 32275.47