{"id":18300793,"url":"https://github.com/stratosblue/cuture.aspnetcore.responseautowrapper","last_synced_at":"2025-04-05T14:30:45.910Z","repository":{"id":65372765,"uuid":"393894343","full_name":"stratosblue/Cuture.AspNetCore.ResponseAutoWrapper","owner":"stratosblue","description":"用于asp.net core的响应和异常自动包装器，使Action提供一致的响应内容格式","archived":false,"fork":false,"pushed_at":"2023-11-15T01:27:33.000Z","size":453,"stargazers_count":25,"open_issues_count":0,"forks_count":2,"subscribers_count":2,"default_branch":"main","last_synced_at":"2024-04-27T02:01:55.715Z","etag":null,"topics":["asp-net-core","auto-wrapper","autowrapper","response-auto-wrapper","response-wrapper"],"latest_commit_sha":null,"homepage":"","language":"C#","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/stratosblue.png","metadata":{"files":{"readme":"README.MD","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2021-08-08T07:45:45.000Z","updated_at":"2024-08-11T15:23:41.000Z","dependencies_parsed_at":"2023-11-15T02:43:40.422Z","dependency_job_id":"f0df03ee-b74a-45bc-9769-a1e94d31ffcc","html_url":"https://github.com/stratosblue/Cuture.AspNetCore.ResponseAutoWrapper","commit_stats":null,"previous_names":[],"tags_count":14,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stratosblue%2FCuture.AspNetCore.ResponseAutoWrapper","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stratosblue%2FCuture.AspNetCore.ResponseAutoWrapper/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stratosblue%2FCuture.AspNetCore.ResponseAutoWrapper/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/stratosblue%2FCuture.AspNetCore.ResponseAutoWrapper/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/stratosblue","download_url":"https://codeload.github.com/stratosblue/Cuture.AspNetCore.ResponseAutoWrapper/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247352244,"owners_count":20925235,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["asp-net-core","auto-wrapper","autowrapper","response-auto-wrapper","response-wrapper"],"created_at":"2024-11-05T15:13:23.399Z","updated_at":"2025-04-05T14:30:45.420Z","avatar_url":"https://github.com/stratosblue.png","language":"C#","funding_links":[],"categories":[],"sub_categories":[],"readme":"﻿# Cuture.AspNetCore.ResponseAutoWrapper\n## 1. Intro\n用于`asp.net core`的响应和异常自动包装器，使`Action`提供一致的响应内容格式\n\n- 不需要修改 `Controller` 的 `Action` 返回类型即可自动包装；\n- 支持`Swagger`，能够正确展示包装后的类型结构；\n- 支持自定义响应结构、自定义异常解析，取消状态码覆写等；\n- 支持复杂类型的 `Code` 和 `Message`，不局限于 `int` 和 `string`；\n- 基于`asp.net core`自身的特性实现，兼容性较好，性能影响较低（目前只做了初步的测试，在简单场景下，性能降低大概在`5%`左右）；\n- 灵活的筛选方式，可以更准确的筛选出不需要包装的Action；\n\n### NOTE!!!\n- 不支持包装 `Middleware` 直接写入的响应内容，以及各种直接 `Map` 的 `MiniApi`；（但出现异常时还是会触发异常包装）\n\n\n执行流程概览：\n![执行流程概览](./execution_flow.png)\n\n## 2. 注意项\n- 目标框架`net6.0+`\n- 包装功能由两个包装器实现：\n    - 基于`ResultFilter`的`ActionResult`包装器：针对方法的返回值包装；\n    - 基于`中间件`的包装器：针对异常、非200响应包装；\n- 默认响应格式为\n    ```json\n    {\n        \"code\": 200,  //状态码 (int)\n        \"message\": \"string\", //消息 (string)\n        \"data\": {}  //Action的原始响应内容\n    }\n    ```\n- 四个针对场景的包装器（都已经有默认实现，可以自行实现后注入DI容器，替换默认的功能）：\n    - `IActionResultWrapper\u003cTResponse, TCode, TMessage\u003e`: 针对`ActionResult`的包装器；\n    - `IExceptionWrapper\u003cTResponse, TCode, TMessage\u003e`: 针对`中间件中捕获到异常`的包装器；\n    - `IInvalidModelStateWrapper\u003cTResponse, TCode, TMessage\u003e`: `参数验证失败`的包装器；\n    - `INotOKStatusCodeWrapper\u003cTResponse, TCode, TMessage\u003e`: 中间件中`StatusCode`非`200`的响应包装器；\n- 默认的`IActionResultWrapper`实现只会处理`ObjectResult`、`EmptyResult`；\n\n### 可能与其它第三方组件存在的冲突点\n- `ResultFilter`中会频繁`未加锁`读取`ActionDescriptor.Properties`，如果存在不正确的写入，可能引发一些问题；\n- 使用动态添加`ProducesResponseTypeAttribute`的方式实现的`OpenAPI`支持，可能存在不完善的地方；\n- `asp.net core3.1`不支持`IAuthorizationMiddlewareResultHandler`，不支持选项`HandleAuthorizationResult`；\n- `授权`和`认证`失败的包装需要手动指定对应组件的失败处理方法，否则可能无法包装；\n- 参数验证失败的包装通过设置`ApiBehaviorOptions.InvalidModelStateResponseFactory`实现，可能有处理逻辑冲突；\n\n## 3. 如何使用\n\n### 3.1 安装`Nuget`包\n\n```PowerShell\nInstall-Package Cuture.AspNetCore.ResponseAutoWrapper -IncludePrerelease\n```\n\n### 3.2 启用`ResultFilter`包装器\n\n在`Startup.ConfigureServices`中添加相关服务并进行配置\n\n```C#\nservices.AddResponseAutoWrapper(options =\u003e\n{\n    //options.ActionNoWrapPredicate     //Action的筛选委托，默认会过滤掉标记了NoResponseWrapAttribute的方法\n    //options.DisableOpenAPISupport     //禁用OpenAPI支持，Swagger将不会显示包装后的格式，也会解除响应类型必须为object泛型的限制\n    //options.HandleAuthorizationResult     //处理授权结果（可能无效，需要自行测试）\n    //options.HandleInvalidModelState       //处理无效模型状态\n    //options.RewriteStatusCode;     //包装时不覆写非200的HTTP状态码\n});\n```\n\n### 3.3 启用中间件包装器\n\n在`Startup.Configure`中启用中间件并进行配置\n\n```C#\napp.UseResponseAutoWrapper(options =\u003e\n{\n    //options.CatchExceptions 是否捕获异常\n    //options.ThrowCaughtExceptions 捕获到异常处理结束后，是否再将异常抛出\n    //options.DefaultOutputFormatterSelector 默认输出格式化器选择委托，选择在请求中无 Accept 时，用于格式化响应的 IOutputFormatter\n});\n```\n\n#### 至此所有相关配置完成，`Action`的响应内容将被自动包装；\n\n-------\n\n## 4. 定制化\n\n### 4.1 自定义消息内容\n - 方法一：Action方法直接返回`TResponse`及其子类时，不会对其进行包装，默认`TResponse`为`GenericApiResponse\u003cint, string, object\u003e`，使用默认配置时，方法直接返回`ApiResponse`及其子类即可\n    ```C#\n    [HttpGet]\n    public ApiResponse GetWithCustomMessage()\n    {\n        return EmptyApiResponse.Create(\"自定义消息\");\n    }\n    ```\n\n    返回结果为\n    ```json\n    {\n    \"data\": null,\n    \"code\": 200,\n    \"message\": \"自定义消息\"\n    }\n    ```\n\n - 方法二：通过`Microsoft.AspNetCore.Http`命名空间下`HttpContext`的拓展方法`DescribeResponse\u003cTCode, TMessage\u003e`进行描述\n    ```C#\n    [HttpGet]\n    public WeatherForecast[] Get()\n    {\n        HttpContext.DescribeResponse(10086, \"Hello world!\");\n        return null;\n    }\n    ```\n\n    返回结果为\n    ```json\n    {\n    \"data\": null,\n    \"code\": 10086,\n    \"message\": \"Hello world!\"\n    }\n    ```\n\n### 4.2 自定义统一响应类型`TResponse`\n默认的`ApiResponse`不能满足需求时，可自行实现并替换`TResponse`\n\n#### 4.1.1 定义类型\n```C#\npublic class CommonResponse\u003cTData\u003e\n{\n    public string Code { get; set; }\n\n    public string Tips { get; set; }\n\n    public TData Result { get; set; }\n}\n```\n - `Data` 对应的泛型参数必须为最后一个泛型参数；\n - 当禁用 `OpenAPI支持` 时，响应类型可以不是泛型；\n\n#### 4.1.2 实现Wrapper\nWrapper可以自行分别实现每个接口，也可以继承 `AbstractResponseWrapper\u003cTResponse, TCode, TMessage\u003e` 快速实现\n\n```C#\npublic class CustomWrapper : AbstractResponseWrapper\u003cCommonResponse\u003cobject\u003e, string, string\u003e\n{\n    public CustomWrapper(IWrapTypeCreator\u003cstring, string\u003e wrapTypeCreator, IOptions\u003cResponseAutoWrapperOptions\u003e optionsAccessor) : base(wrapTypeCreator, optionsAccessor)\n    {\n    }\n\n    public override CommonResponse\u003cobject\u003e? ExceptionWrap(HttpContext context, Exception exception)\n    {\n        return new CommonResponse\u003cobject\u003e() { Code = \"E4000\", Tips = \"SERVER ERROR\" };\n    }\n\n    public override CommonResponse\u003cobject\u003e? InvalidModelStateWrap(ActionContext context)\n    {\n        return new CommonResponse\u003cobject\u003e() { Code = \"E3000\", Tips = \"SERVER ERROR\" };\n    }\n\n    public override CommonResponse\u003cobject\u003e? NotOKStatusCodeWrap(HttpContext context)\n    {\n        return null;\n    }\n\n    protected override CommonResponse\u003cobject\u003e? ActionEmptyResultWrap(ResultExecutingContext context, EmptyResult emptyResult, ResponseDescription\u003cstring, string\u003e? description)\n    {\n        return new CommonResponse\u003cobject\u003e() { Code = description?.Code ?? \"E2000\", Tips = description?.Message ?? \"NO CONTENT\" };\n    }\n\n    protected override CommonResponse\u003cobject\u003e? ActionObjectResultWrap(ResultExecutingContext context, ObjectResult objectResult, ResponseDescription\u003cstring, string\u003e? description)\n    {\n        return new CommonResponse\u003cobject\u003e() { Code = description?.Code ?? \"E2000\", Tips = description?.Message ?? \"NO CONTENT\", Result = objectResult.Value };\n    }\n}\n```\n\n - 当 `wrapper` 返回 `null` 时，则不进行包装；\n\n#### 4.1.3 配置使用自定义类型\n\n```C#\nservices.AddResponseAutoWrapper\u003cCommonResponse\u003cobject\u003e, string, string\u003e()\n        .ConfigureWrappers(options =\u003e options.AddWrappers\u003cCustomWrapper\u003e());\n```\n\n - Response `Data` 对应的泛型参数在此处必须为 `object`；\n - 此示例中 `TCode` 为 `string`，`TMessage` 为 `string`，则使用 `DescribeResponse` 进行描述时，参数类型必须对应为 `string`, `string`；\n\n至此已完成配置，统一响应内容格式变更为：\n```json\n{\n    \"code\": \"string\",\n    \"tips\": \"string\",\n    \"result\": {}\n}\n```\n\n------\n\n## Note!!!\n - 仅当`禁用OpenAPI支持时`，`TResponse`才能不是一个泛型参数为`object`的泛型；\n - 更多信息可参考 `sample/CustomStructureWebApplication`、`sample/SimpleWebApplication` 以及 `test/ResponseAutoWrapper.TestHost` 项目；\n - 默认情况下不会包装使用`[NoResponseWrapAttribute]`标记的方法；\n\n### 4.3 其它自定义\n\n使用自行实现的接口注入DI容器替换掉默认实现即可完成一些其它的自定义\n\n- `IActionResultWrapper\u003cTResponse, TCode, TMessage\u003e`: ActionResult包装器；\n- `IExceptionWrapper\u003cTResponse, TCode, TMessage\u003e`: 捕获异常时的响应包装器；\n- `IInvalidModelStateWrapper\u003cTResponse, TCode, TMessage\u003e`: 模型验证失败时的响应包装器；\n- `INotOKStatusCodeWrapper\u003cTResponse, TCode, TMessage\u003e`: 非200状态码时的响应包装器；\n- `IWrapTypeCreator\u003cTCode, TMessage\u003e`: 确认Action返回对象类型是否需要包装，以及创建OpenAPI展示的泛型类；\n\n### 4.4 动态取消包装\n调用 `HttpContext` 的拓展方法 `DoNotWrapResponse` ，以动态的取消对当前响应的包装；使用拓展方法 `IsSetDoNotWrapResponse` 可以检查当前上下文是否已标记为不包装响应值；\n```C#\nHttpContext.DoNotWrapResponse();\n```\n\n## 5. 性能测试结果\n\n#### 多次迭代后，数据可能略微变动，但影响理论上仍然是固定比例的，不会随响应内容大小而变化\n\n- 系统：`Ubuntu20.04 on WSL2` host by Windows10-21H1\n- CPU：`I7-8700`\n- 平台：`asp.net core 5.0`\n- 测试软件：`wrk`\n- 测试软件参数：`-t 3 -c 100 -d 30s`\n- 测试Action为：\n    ```C#\n    [HttpGet]\n    public IEnumerable\u003cWeatherForecast\u003e Get(int count = 5)\n    {\n        return WeatherForecast.GenerateData(count);\n    }\n    ```\n- 测试均为使用`localhost`，以尽量减少网络的影响；\n- 对比对象分别为：\n    - Origin：原生，没有进行包装\n    - Cuture.AspNetCore.ResponseAutoWrapper：此自动包装库\n    - [AutoWrapper.Core](https://github.com/proudmonkey/AutoWrapper)：另一个同类型的自动包装库\n- 结果为多次运行取峰值；\n- 测试环境不专业，会有一定的误差，数值仅供参考；\n\n### 数据（单位 `Requests/sec`）\n|`count`      |      Origin              |Cuture.AspNetCore.ResponseAutoWrapper|   AutoWrapper.Core      |\n|   ----      |   ----                   |                ----                 |         ----            |\n| 1           |123267.40                 |111868.34                            |91202.04                 |\n| 10          |108264.80                 |103125.32                            |67001.92                 |\n| 50          |76310.72                  |73451.83                             |32275.47                 |\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fstratosblue%2Fcuture.aspnetcore.responseautowrapper","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fstratosblue%2Fcuture.aspnetcore.responseautowrapper","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fstratosblue%2Fcuture.aspnetcore.responseautowrapper/lists"}