Response and exception automatic wrapper for `asp.net core` to provide a consistent response content format for `Action`. 用于`asp.net core`的响应和异常自动包装器,使`Action`提供一致的响应内容格式。
$ dotnet add package Cuture.AspNetCore.ResponseAutoWrapper用于asp.net core的响应和异常自动包装器,使Action提供一致的响应内容格式
Controller 的 Action 返回类型即可自动包装;Swagger,能够正确展示包装后的类型结构;Code 和 Message,不局限于 int 和 string;asp.net core自身的特性实现,兼容性较好,性能影响较低(目前只做了初步的测试,在简单场景下,性能降低大概在5%左右);Middleware 直接写入的响应内容,以及各种直接 Map 的 MiniApi;(但出现异常时还是会触发异常包装)执行流程概览:

net8.0+ResultFilter的ActionResult包装器:针对方法的返回值包装;中间件的包装器:针对异常、非200响应包装;{
"code": 200, //状态码 (int)
"message": "string", //消息 (string)
"data": {} //Action的原始响应内容
}
IActionResultWrapper<TResponse, TCode, TMessage>: 针对ActionResult的包装器;IExceptionWrapper<TResponse, TCode, TMessage>: 针对中间件中捕获到异常的包装器;IInvalidModelStateWrapper<TResponse, TCode, TMessage>: 参数验证失败的包装器;INotOKStatusCodeWrapper<TResponse, TCode, TMessage>: 中间件中StatusCode非200的响应包装器;IActionResultWrapper实现只会处理ObjectResult、EmptyResult;ResultFilter中会频繁未加锁读取ActionDescriptor.Properties,如果存在不正确的写入,可能引发一些问题;ProducesResponseTypeAttribute的方式实现的OpenAPI支持,可能存在不完善的地方;授权和认证失败的包装需要手动指定对应组件的失败处理方法,否则可能无法包装;ApiBehaviorOptions.InvalidModelStateResponseFactory实现,可能有处理逻辑冲突;Nuget包Install-Package Cuture.AspNetCore.ResponseAutoWrapper
ResultFilter包装器在Startup.ConfigureServices中添加相关服务并进行配置
services.AddResponseAutoWrapper(options =>
{
//options.ActionNoWrapPredicate //Action的筛选委托,默认会过滤掉标记了NoResponseWrapAttribute的方法
//options.DisableOpenAPISupport //禁用OpenAPI支持,Swagger将不会显示包装后的格式,也会解除响应类型必须为object泛型的限制
//options.HandleAuthorizationResult //处理授权结果(可能无效,需要自行测试)
//options.HandleInvalidModelState //处理无效模型状态
//options.RewriteStatusCode; //包装时不覆写非200的HTTP状态码
});
在Startup.Configure中启用中间件并进行配置
app.UseResponseAutoWrapper(options =>
{
//options.CatchExceptions 是否捕获异常
//options.ThrowCaughtExceptions 捕获到异常处理结束后,是否再将异常抛出
//options.DefaultOutputFormatterSelector 默认输出格式化器选择委托,选择在请求中无 Accept 时,用于格式化响应的 IOutputFormatter
});
Action的响应内容将被自动包装;方法一:Action方法直接返回TResponse及其子类时,不会对其进行包装,默认TResponse为GenericApiResponse<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!"
}
TResponse默认的ApiResponse不能满足需求时,可自行实现并替换TResponse
public class CommonResponse<TData>
{
public string Code { get; set; }
public string Tips { get; set; }
public TData Result { get; set; }
}
Data 对应的泛型参数必须为最后一个泛型参数;OpenAPI支持 时,响应类型可以不是泛型;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 时,则不进行包装;services.AddResponseAutoWrapper<CommonResponse<object>, string, string>()
.ConfigureWrappers(options => options.AddWrappers<CustomWrapper>());
Data 对应的泛型参数在此处必须为 object;TCode 为 string,TMessage 为 string,则使用 DescribeResponse 进行描述时,参数类型必须对应为 string, string;至此已完成配置,统一响应内容格式变更为:
{
"code": "string",
"tips": "string",
"result": {}
}
禁用OpenAPI支持时,TResponse才能不是一个泛型参数为object的泛型;sample/CustomStructureWebApplication、sample/SimpleWebApplication 以及 test/ResponseAutoWrapper.TestHost 项目;[NoResponseWrapAttribute]标记的方法;使用自行实现的接口注入DI容器替换掉默认实现即可完成一些其它的自定义
IActionResultWrapper<TResponse, TCode, TMessage>: ActionResult包装器;IExceptionWrapper<TResponse, TCode, TMessage>: 捕获异常时的响应包装器;IInvalidModelStateWrapper<TResponse, TCode, TMessage>: 模型验证失败时的响应包装器;INotOKStatusCodeWrapper<TResponse, TCode, TMessage>: 非200状态码时的响应包装器;IWrapTypeCreator<TCode, TMessage>: 确认Action返回对象类型是否需要包装,以及创建OpenAPI展示的泛型类;调用 HttpContext 的拓展方法 DoNotWrapResponse ,以动态的取消对当前响应的包装;使用拓展方法 IsSetDoNotWrapResponse 可以检查当前上下文是否已标记为不包装响应值;
HttpContext.DoNotWrapResponse();
Ubuntu20.04 on WSL2 host by Windows10-21H1I7-8700asp.net core 5.0wrk-t 3 -c 100 -d 30s[HttpGet]
public IEnumerable<WeatherForecast> Get(int count = 5)
{
return WeatherForecast.GenerateData(count);
}
localhost,以尽量减少网络的影响;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 |