├── .editorconfig ├── .gitattributes ├── .github └── workflows │ └── PublishNugetPackage.yml ├── .gitignore ├── CodeMaid.config ├── Cuture.AspNetCore.ResponseAutoWrapper.slnx ├── Directory.Build.props ├── LICENSE ├── README.md ├── execution_flow.png ├── global.json ├── sample ├── CustomStructureWebApplication │ ├── CommonResponse.cs │ ├── Controllers │ │ └── WeatherForecastController.cs │ ├── CustomStructureWebApplication.csproj │ ├── CustomWrapper.cs │ ├── Program.cs │ ├── Properties │ │ └── launchSettings.json │ ├── Startup.cs │ ├── WeatherForecast.cs │ ├── appsettings.Development.json │ └── appsettings.json └── SimpleWebApplication │ ├── Controllers │ └── WeatherForecastController.cs │ ├── CustomExceptionWrapper.cs │ ├── Program.cs │ ├── Properties │ └── launchSettings.json │ ├── SimpleWebApplication.csproj │ ├── Startup.cs │ ├── WeatherForecast.cs │ ├── appsettings.Development.json │ └── appsettings.json ├── src └── Cuture.AspNetCore.ResponseAutoWrapper │ ├── Constants.cs │ ├── Cuture.AspNetCore.ResponseAutoWrapper.csproj │ ├── Extensions │ ├── ApplicationBuilderResponseAutoWrapperExtensions.cs │ ├── DoNotWrapResponseMarkHttpContextExtensions.cs │ ├── IResponseAutoWrapperBuilderExtensions.cs │ ├── ResponseDescriptionHttpContextExtensions.cs │ ├── ServiceCollectionResponseAutoWrapperExtensions.cs │ └── TypeExtensions.cs │ ├── Interfaces │ ├── IResponseAutoWrapperBuilder.cs │ ├── IWrapTypeCreator.cs │ └── Wrapper │ │ ├── IActionResultWrapper.cs │ │ ├── IExceptionWrapper.cs │ │ ├── IInvalidModelStateWrapper.cs │ │ ├── INotOKStatusCodeWrapper.cs │ │ └── IWrapper.cs │ ├── Internal │ ├── ActionResultPolicy.cs │ ├── ActivityExtensions.cs │ ├── ApiBehaviorOptionsPostConfigureOptions.cs │ ├── DefaultResponseAutoWrapperBuilder.cs │ ├── DefaultWrapTypeCreator.cs │ ├── MvcOptionsPostConfigureOptions.cs │ └── ObjectAccessor.cs │ ├── LegacyCompatibleResponseWrapperOptions.cs │ ├── Microsoft.AspNetCore │ ├── Authorization │ │ └── AutoWrapperAuthorizationMiddlewareResultHandler.cs │ └── Mvc │ │ ├── ApiResponse.cs │ │ ├── ApplicationModels │ │ ├── ActionResultPolicyTagAppModelConvention.cs │ │ └── OpenAPISupportAppModelConvention.cs │ │ ├── EmptyApiResponse.cs │ │ ├── Filters │ │ └── ResponseAutoWrapResultFilter.cs │ │ ├── GenericApiResponse.cs │ │ └── NoResponseWrapAttribute.cs │ ├── ResponseAutoWrapMiddleware.cs │ ├── ResponseAutoWrapMiddlewareOptions.cs │ ├── ResponseAutoWrapperOptions.cs │ ├── ResponseAutoWrapperWorkDelegateCollection.cs │ ├── ResponseDescription.cs │ └── Wrappers │ ├── AbstractResponseWrapper.cs │ ├── DefaultResponseWrapper.cs │ └── LegacyCompatibleResponseWrapper.cs └── test ├── ResponseAutoWrapper.BenchmarkHost ├── Controllers │ └── WeatherForecastController.cs ├── Program.cs ├── Properties │ └── launchSettings.json ├── ResponseAutoWrapper.BenchmarkHost.csproj ├── Startups.cs └── appsettings.json ├── ResponseAutoWrapper.Test ├── CRTests.cs ├── GenericApiTest.cs ├── LCRTests.cs ├── MiddlewareExceptionTest.cs ├── README.MD ├── ResponseAutoWrapper.Test.csproj └── TestServerBase.cs └── ResponseAutoWrapper.TestHost ├── AuthorizeMixedAttribute.cs ├── Controllers ├── CRWeatherForecastController.cs ├── GenericWeatherForecastController.cs ├── LCRWeatherForecastController.cs ├── LoginController.cs ├── NGCRWeatherForecastController.cs ├── NGLCRWeatherForecastController.cs └── WeatherForecastController.cs ├── CustomResponse.cs ├── CustomResponseWrapper.cs ├── Hosts.cs ├── InheritedLegacyResponse.cs ├── InheritedResponse.cs ├── InheritedTask.cs ├── LegacyCustomResponse.cs ├── LegacyCustomResponseWrapper.cs ├── Program.cs ├── Properties └── launchSettings.json ├── ResponseAutoWrapper.TestHost.csproj ├── Startups ├── BaseStartup.cs ├── CRStartup.cs ├── DefaultStartup.cs ├── DisableOpenAPISupportStartup.cs ├── EmptyStartup.cs ├── LCRStartup.cs ├── MiddlewareException │ ├── CustomResponseByResponseCreatorMiddlewareExceptionStartup.cs │ └── MiddlewareExceptionStartup.cs ├── NoWrapperStartup.cs ├── NotGenericCRStartup.cs └── NotGenericLCRStartup.cs ├── WeatherForecast.cs ├── appsettings.Development.json └── appsettings.json /.gitattributes: -------------------------------------------------------------------------------- 1 | ############################################################################### 2 | # Set default behavior to automatically normalize line endings. 3 | ############################################################################### 4 | * text=auto 5 | 6 | ############################################################################### 7 | # Set default behavior for command prompt diff. 8 | # 9 | # This is need for earlier builds of msysgit that does not have it on by 10 | # default for csharp files. 11 | # Note: This is only used by command line 12 | ############################################################################### 13 | #*.cs diff=csharp 14 | 15 | ############################################################################### 16 | # Set the merge driver for project and solution files 17 | # 18 | # Merging from the command prompt will add diff markers to the files if there 19 | # are conflicts (Merging from VS is not affected by the settings below, in VS 20 | # the diff markers are never inserted). Diff markers may cause the following 21 | # file extensions to fail to load in VS. An alternative would be to treat 22 | # these files as binary and thus will always conflict and require user 23 | # intervention with every merge. To do so, just uncomment the entries below 24 | ############################################################################### 25 | #*.sln merge=binary 26 | #*.csproj merge=binary 27 | #*.vbproj merge=binary 28 | #*.vcxproj merge=binary 29 | #*.vcproj merge=binary 30 | #*.dbproj merge=binary 31 | #*.fsproj merge=binary 32 | #*.lsproj merge=binary 33 | #*.wixproj merge=binary 34 | #*.modelproj merge=binary 35 | #*.sqlproj merge=binary 36 | #*.wwaproj merge=binary 37 | 38 | ############################################################################### 39 | # behavior for image files 40 | # 41 | # image files are treated as binary by default. 42 | ############################################################################### 43 | #*.jpg binary 44 | #*.png binary 45 | #*.gif binary 46 | 47 | ############################################################################### 48 | # diff behavior for common document formats 49 | # 50 | # Convert binary document formats to text before diffing them. This feature 51 | # is only available from the command line. Turn it on by uncommenting the 52 | # entries below. 53 | ############################################################################### 54 | #*.doc diff=astextplain 55 | #*.DOC diff=astextplain 56 | #*.docx diff=astextplain 57 | #*.DOCX diff=astextplain 58 | #*.dot diff=astextplain 59 | #*.DOT diff=astextplain 60 | #*.pdf diff=astextplain 61 | #*.PDF diff=astextplain 62 | #*.rtf diff=astextplain 63 | #*.RTF diff=astextplain 64 | -------------------------------------------------------------------------------- /.github/workflows/PublishNugetPackage.yml: -------------------------------------------------------------------------------- 1 | name: Publish Nuget Package 2 | 3 | on: 4 | release: 5 | types: [released,prereleased] 6 | branches: [ main ] 7 | 8 | jobs: 9 | publish-with-build: 10 | 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - uses: actions/checkout@v4 15 | - name: Setup .NET Core SDK 16 | uses: actions/setup-dotnet@v4 17 | with: 18 | dotnet-version: | 19 | 8.0.x 20 | 9.x 21 | - name: restore dependencies 22 | run: dotnet restore 23 | - name: build 24 | run: dotnet build --no-restore -c Release ./src/Cuture.AspNetCore.ResponseAutoWrapper/Cuture.AspNetCore.ResponseAutoWrapper.csproj 25 | - name: pack 26 | run: dotnet pack -c Release -o ./output --include-symbols 27 | - name: push package 28 | shell: pwsh 29 | working-directory: ./output 30 | run: Get-ChildItem -File -Filter '*.nupkg' | ForEach-Object { dotnet nuget push $_ -k ${{secrets.NUGET_KEY}} -s https://api.nuget.org/v3/index.json --no-service-endpoint --skip-duplicate; dotnet nuget push $_ -k ${{secrets.NUGET_GITHUB_KEY}} -s https://nuget.pkg.github.com/StratosBlue/index.json --no-service-endpoint --skip-duplicate; } 31 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Ww][Ii][Nn]32/ 27 | [Aa][Rr][Mm]/ 28 | [Aa][Rr][Mm]64/ 29 | bld/ 30 | [Bb]in/ 31 | [Oo]bj/ 32 | [Oo]ut/ 33 | [Ll]og/ 34 | [Ll]ogs/ 35 | 36 | # Visual Studio 2015/2017 cache/options directory 37 | .vs/ 38 | # Uncomment if you have tasks that create the project's static files in wwwroot 39 | #wwwroot/ 40 | 41 | # Visual Studio 2017 auto generated files 42 | Generated\ Files/ 43 | 44 | # MSTest test Results 45 | [Tt]est[Rr]esult*/ 46 | [Bb]uild[Ll]og.* 47 | 48 | # NUnit 49 | *.VisualState.xml 50 | TestResult.xml 51 | nunit-*.xml 52 | 53 | # Build Results of an ATL Project 54 | [Dd]ebugPS/ 55 | [Rr]eleasePS/ 56 | dlldata.c 57 | 58 | # Benchmark Results 59 | BenchmarkDotNet.Artifacts/ 60 | 61 | # .NET Core 62 | project.lock.json 63 | project.fragment.lock.json 64 | artifacts/ 65 | 66 | # ASP.NET Scaffolding 67 | ScaffoldingReadMe.txt 68 | 69 | # StyleCop 70 | StyleCopReport.xml 71 | 72 | # Files built by Visual Studio 73 | *_i.c 74 | *_p.c 75 | *_h.h 76 | *.ilk 77 | *.meta 78 | *.obj 79 | *.iobj 80 | *.pch 81 | *.pdb 82 | *.ipdb 83 | *.pgc 84 | *.pgd 85 | *.rsp 86 | *.sbr 87 | *.tlb 88 | *.tli 89 | *.tlh 90 | *.tmp 91 | *.tmp_proj 92 | *_wpftmp.csproj 93 | *.log 94 | *.vspscc 95 | *.vssscc 96 | .builds 97 | *.pidb 98 | *.svclog 99 | *.scc 100 | 101 | # Chutzpah Test files 102 | _Chutzpah* 103 | 104 | # Visual C++ cache files 105 | ipch/ 106 | *.aps 107 | *.ncb 108 | *.opendb 109 | *.opensdf 110 | *.sdf 111 | *.cachefile 112 | *.VC.db 113 | *.VC.VC.opendb 114 | 115 | # Visual Studio profiler 116 | *.psess 117 | *.vsp 118 | *.vspx 119 | *.sap 120 | 121 | # Visual Studio Trace Files 122 | *.e2e 123 | 124 | # TFS 2012 Local Workspace 125 | $tf/ 126 | 127 | # Guidance Automation Toolkit 128 | *.gpState 129 | 130 | # ReSharper is a .NET coding add-in 131 | _ReSharper*/ 132 | *.[Rr]e[Ss]harper 133 | *.DotSettings.user 134 | 135 | # TeamCity is a build add-in 136 | _TeamCity* 137 | 138 | # DotCover is a Code Coverage Tool 139 | *.dotCover 140 | 141 | # AxoCover is a Code Coverage Tool 142 | .axoCover/* 143 | !.axoCover/settings.json 144 | 145 | # Coverlet is a free, cross platform Code Coverage Tool 146 | coverage*.json 147 | coverage*.xml 148 | coverage*.info 149 | 150 | # Visual Studio code coverage results 151 | *.coverage 152 | *.coveragexml 153 | 154 | # NCrunch 155 | _NCrunch_* 156 | .*crunch*.local.xml 157 | nCrunchTemp_* 158 | 159 | # MightyMoose 160 | *.mm.* 161 | AutoTest.Net/ 162 | 163 | # Web workbench (sass) 164 | .sass-cache/ 165 | 166 | # Installshield output folder 167 | [Ee]xpress/ 168 | 169 | # DocProject is a documentation generator add-in 170 | DocProject/buildhelp/ 171 | DocProject/Help/*.HxT 172 | DocProject/Help/*.HxC 173 | DocProject/Help/*.hhc 174 | DocProject/Help/*.hhk 175 | DocProject/Help/*.hhp 176 | DocProject/Help/Html2 177 | DocProject/Help/html 178 | 179 | # Click-Once directory 180 | publish/ 181 | 182 | # Publish Web Output 183 | *.[Pp]ublish.xml 184 | *.azurePubxml 185 | # Note: Comment the next line if you want to checkin your web deploy settings, 186 | # but database connection strings (with potential passwords) will be unencrypted 187 | *.pubxml 188 | *.publishproj 189 | 190 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 191 | # checkin your Azure Web App publish settings, but sensitive information contained 192 | # in these scripts will be unencrypted 193 | PublishScripts/ 194 | 195 | # NuGet Packages 196 | *.nupkg 197 | # NuGet Symbol Packages 198 | *.snupkg 199 | # The packages folder can be ignored because of Package Restore 200 | **/[Pp]ackages/* 201 | # except build/, which is used as an MSBuild target. 202 | !**/[Pp]ackages/build/ 203 | # Uncomment if necessary however generally it will be regenerated when needed 204 | #!**/[Pp]ackages/repositories.config 205 | # NuGet v3's project.json files produces more ignorable files 206 | *.nuget.props 207 | *.nuget.targets 208 | 209 | # Microsoft Azure Build Output 210 | csx/ 211 | *.build.csdef 212 | 213 | # Microsoft Azure Emulator 214 | ecf/ 215 | rcf/ 216 | 217 | # Windows Store app package directories and files 218 | AppPackages/ 219 | BundleArtifacts/ 220 | Package.StoreAssociation.xml 221 | _pkginfo.txt 222 | *.appx 223 | *.appxbundle 224 | *.appxupload 225 | 226 | # Visual Studio cache files 227 | # files ending in .cache can be ignored 228 | *.[Cc]ache 229 | # but keep track of directories ending in .cache 230 | !?*.[Cc]ache/ 231 | 232 | # Others 233 | ClientBin/ 234 | ~$* 235 | *~ 236 | *.dbmdl 237 | *.dbproj.schemaview 238 | *.jfm 239 | *.pfx 240 | *.publishsettings 241 | orleans.codegen.cs 242 | 243 | # Including strong name files can present a security risk 244 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 245 | #*.snk 246 | 247 | # Since there are multiple workflows, uncomment next line to ignore bower_components 248 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 249 | #bower_components/ 250 | 251 | # RIA/Silverlight projects 252 | Generated_Code/ 253 | 254 | # Backup & report files from converting an old project file 255 | # to a newer Visual Studio version. Backup files are not needed, 256 | # because we have git ;-) 257 | _UpgradeReport_Files/ 258 | Backup*/ 259 | UpgradeLog*.XML 260 | UpgradeLog*.htm 261 | ServiceFabricBackup/ 262 | *.rptproj.bak 263 | 264 | # SQL Server files 265 | *.mdf 266 | *.ldf 267 | *.ndf 268 | 269 | # Business Intelligence projects 270 | *.rdl.data 271 | *.bim.layout 272 | *.bim_*.settings 273 | *.rptproj.rsuser 274 | *- [Bb]ackup.rdl 275 | *- [Bb]ackup ([0-9]).rdl 276 | *- [Bb]ackup ([0-9][0-9]).rdl 277 | 278 | # Microsoft Fakes 279 | FakesAssemblies/ 280 | 281 | # GhostDoc plugin setting file 282 | *.GhostDoc.xml 283 | 284 | # Node.js Tools for Visual Studio 285 | .ntvs_analysis.dat 286 | node_modules/ 287 | 288 | # Visual Studio 6 build log 289 | *.plg 290 | 291 | # Visual Studio 6 workspace options file 292 | *.opt 293 | 294 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 295 | *.vbw 296 | 297 | # Visual Studio LightSwitch build output 298 | **/*.HTMLClient/GeneratedArtifacts 299 | **/*.DesktopClient/GeneratedArtifacts 300 | **/*.DesktopClient/ModelManifest.xml 301 | **/*.Server/GeneratedArtifacts 302 | **/*.Server/ModelManifest.xml 303 | _Pvt_Extensions 304 | 305 | # Paket dependency manager 306 | .paket/paket.exe 307 | paket-files/ 308 | 309 | # FAKE - F# Make 310 | .fake/ 311 | 312 | # CodeRush personal settings 313 | .cr/personal 314 | 315 | # Python Tools for Visual Studio (PTVS) 316 | __pycache__/ 317 | *.pyc 318 | 319 | # Cake - Uncomment if you are using it 320 | # tools/** 321 | # !tools/packages.config 322 | 323 | # Tabs Studio 324 | *.tss 325 | 326 | # Telerik's JustMock configuration file 327 | *.jmconfig 328 | 329 | # BizTalk build output 330 | *.btp.cs 331 | *.btm.cs 332 | *.odx.cs 333 | *.xsd.cs 334 | 335 | # OpenCover UI analysis results 336 | OpenCover/ 337 | 338 | # Azure Stream Analytics local run output 339 | ASALocalRun/ 340 | 341 | # MSBuild Binary and Structured Log 342 | *.binlog 343 | 344 | # NVidia Nsight GPU debugger configuration file 345 | *.nvuser 346 | 347 | # MFractors (Xamarin productivity tool) working folder 348 | .mfractor/ 349 | 350 | # Local History for Visual Studio 351 | .localhistory/ 352 | 353 | # BeatPulse healthcheck temp database 354 | healthchecksdb 355 | 356 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 357 | MigrationBackup/ 358 | 359 | # Ionide (cross platform F# VS Code tools) working folder 360 | .ionide/ 361 | 362 | # Fody - auto-generated XML schema 363 | FodyWeavers.xsd -------------------------------------------------------------------------------- /CodeMaid.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 |
6 | 7 | 8 | 9 | 10 | 11 | Fields||3||字段 12 | 13 | 14 | Properties||4||属性 15 | 16 | 17 | Methods||9||方法 18 | 19 | 20 | Delegates||1||委托 21 | 22 | 23 | Events||2||事件 24 | 25 | 26 | Interfaces||8||接口 27 | 28 | 29 | Constructors||6||构造函数 30 | 31 | 32 | Enums||10||枚举 33 | 34 | 35 | Destructors||7||析构函数 36 | 37 | 38 | Indexers||5||索引器 39 | 40 | 41 | Classes||12||类 42 | 43 | 44 | Structs||11||结构体 45 | 46 | 47 | True 48 | 49 | 50 | True 51 | 52 | 53 | False 54 | 55 | 56 | 0 57 | 58 | 59 | True 60 | 61 | 62 | Consolas 63 | 64 | 65 | False 66 | 67 | 68 | True 69 | 70 | 71 | 1 72 | 73 | 74 | False 75 | 76 | 77 | False 78 | 79 | 80 | False 81 | 82 | 83 | False 84 | 85 | 86 | True 87 | 88 | 90 | True 91 | 92 | 94 | True 95 | 96 | 97 | True 98 | 99 | 100 | False 101 | 102 | 103 | True 104 | 105 | 106 | False 107 | 108 | 109 | 110 | 111 | -------------------------------------------------------------------------------- /Cuture.AspNetCore.ResponseAutoWrapper.slnx: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /Directory.Build.props: -------------------------------------------------------------------------------- 1 |  2 | 3 | net8.0;net9.0 4 | 5 | enable 6 | latest 7 | false 8 | 9 | 10 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Stratos 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Cuture.AspNetCore.ResponseAutoWrapper 2 | ## 1. Intro 3 | 用于`asp.net core`的响应和异常自动包装器,使`Action`提供一致的响应内容格式 4 | 5 | - 不需要修改 `Controller` 的 `Action` 返回类型即可自动包装; 6 | - 支持`Swagger`,能够正确展示包装后的类型结构; 7 | - 支持自定义响应结构、自定义异常解析,取消状态码覆写等; 8 | - 支持复杂类型的 `Code` 和 `Message`,不局限于 `int` 和 `string`; 9 | - 基于`asp.net core`自身的特性实现,兼容性较好,性能影响较低(目前只做了初步的测试,在简单场景下,性能降低大概在`5%`左右); 10 | - 灵活的筛选方式,可以更准确的筛选出不需要包装的Action; 11 | 12 | ### NOTE!!! 13 | - 不支持包装 `Middleware` 直接写入的响应内容,以及各种直接 `Map` 的 `MiniApi`;(但出现异常时还是会触发异常包装) 14 | 15 | 16 | 执行流程概览: 17 | ![执行流程概览](./execution_flow.png) 18 | 19 | ## 2. 注意项 20 | - 目标框架`net8.0+` 21 | - 包装功能由两个包装器实现: 22 | - 基于`ResultFilter`的`ActionResult`包装器:针对方法的返回值包装; 23 | - 基于`中间件`的包装器:针对异常、非200响应包装; 24 | - 默认响应格式为 25 | ```json 26 | { 27 | "code": 200, //状态码 (int) 28 | "message": "string", //消息 (string) 29 | "data": {} //Action的原始响应内容 30 | } 31 | ``` 32 | - 四个针对场景的包装器(都已经有默认实现,可以自行实现后注入DI容器,替换默认的功能): 33 | - `IActionResultWrapper`: 针对`ActionResult`的包装器; 34 | - `IExceptionWrapper`: 针对`中间件中捕获到异常`的包装器; 35 | - `IInvalidModelStateWrapper`: `参数验证失败`的包装器; 36 | - `INotOKStatusCodeWrapper`: 中间件中`StatusCode`非`200`的响应包装器; 37 | - 默认的`IActionResultWrapper`实现只会处理`ObjectResult`、`EmptyResult`; 38 | 39 | ### 可能与其它第三方组件存在的冲突点 40 | - `ResultFilter`中会频繁`未加锁`读取`ActionDescriptor.Properties`,如果存在不正确的写入,可能引发一些问题; 41 | - 使用动态添加`ProducesResponseTypeAttribute`的方式实现的`OpenAPI`支持,可能存在不完善的地方; 42 | - `授权`和`认证`失败的包装需要手动指定对应组件的失败处理方法,否则可能无法包装; 43 | - 参数验证失败的包装通过设置`ApiBehaviorOptions.InvalidModelStateResponseFactory`实现,可能有处理逻辑冲突; 44 | 45 | ## 3. 如何使用 46 | 47 | ### 3.1 安装`Nuget`包 48 | 49 | ```PowerShell 50 | Install-Package Cuture.AspNetCore.ResponseAutoWrapper 51 | ``` 52 | 53 | ### 3.2 启用`ResultFilter`包装器 54 | 55 | 在`Startup.ConfigureServices`中添加相关服务并进行配置 56 | 57 | ```C# 58 | services.AddResponseAutoWrapper(options => 59 | { 60 | //options.ActionNoWrapPredicate //Action的筛选委托,默认会过滤掉标记了NoResponseWrapAttribute的方法 61 | //options.DisableOpenAPISupport //禁用OpenAPI支持,Swagger将不会显示包装后的格式,也会解除响应类型必须为object泛型的限制 62 | //options.HandleAuthorizationResult //处理授权结果(可能无效,需要自行测试) 63 | //options.HandleInvalidModelState //处理无效模型状态 64 | //options.RewriteStatusCode; //包装时不覆写非200的HTTP状态码 65 | }); 66 | ``` 67 | 68 | ### 3.3 启用中间件包装器 69 | 70 | 在`Startup.Configure`中启用中间件并进行配置 71 | 72 | ```C# 73 | app.UseResponseAutoWrapper(options => 74 | { 75 | //options.CatchExceptions 是否捕获异常 76 | //options.ThrowCaughtExceptions 捕获到异常处理结束后,是否再将异常抛出 77 | //options.DefaultOutputFormatterSelector 默认输出格式化器选择委托,选择在请求中无 Accept 时,用于格式化响应的 IOutputFormatter 78 | }); 79 | ``` 80 | 81 | #### 至此所有相关配置完成,`Action`的响应内容将被自动包装; 82 | 83 | ------- 84 | 85 | ## 4. 定制化 86 | 87 | ### 4.1 自定义消息内容 88 | - 方法一:Action方法直接返回`TResponse`及其子类时,不会对其进行包装,默认`TResponse`为`GenericApiResponse`,使用默认配置时,方法直接返回`ApiResponse`及其子类即可 89 | ```C# 90 | [HttpGet] 91 | public ApiResponse GetWithCustomMessage() 92 | { 93 | return EmptyApiResponse.Create("自定义消息"); 94 | } 95 | ``` 96 | 97 | 返回结果为 98 | ```json 99 | { 100 | "data": null, 101 | "code": 200, 102 | "message": "自定义消息" 103 | } 104 | ``` 105 | 106 | - 方法二:通过`Microsoft.AspNetCore.Http`命名空间下`HttpContext`的拓展方法`DescribeResponse`进行描述 107 | ```C# 108 | [HttpGet] 109 | public WeatherForecast[] Get() 110 | { 111 | HttpContext.DescribeResponse(10086, "Hello world!"); 112 | return null; 113 | } 114 | ``` 115 | 116 | 返回结果为 117 | ```json 118 | { 119 | "data": null, 120 | "code": 10086, 121 | "message": "Hello world!" 122 | } 123 | ``` 124 | 125 | ### 4.2 自定义统一响应类型`TResponse` 126 | 默认的`ApiResponse`不能满足需求时,可自行实现并替换`TResponse` 127 | 128 | #### 4.1.1 定义类型 129 | ```C# 130 | public class CommonResponse 131 | { 132 | public string Code { get; set; } 133 | 134 | public string Tips { get; set; } 135 | 136 | public TData Result { get; set; } 137 | } 138 | ``` 139 | - `Data` 对应的泛型参数必须为最后一个泛型参数; 140 | - 当禁用 `OpenAPI支持` 时,响应类型可以不是泛型; 141 | 142 | #### 4.1.2 实现Wrapper 143 | Wrapper可以自行分别实现每个接口,也可以继承 `AbstractResponseWrapper` 快速实现 144 | 145 | ```C# 146 | public class CustomWrapper : AbstractResponseWrapper, string, string> 147 | { 148 | public CustomWrapper(IWrapTypeCreator wrapTypeCreator, IOptions optionsAccessor) : base(wrapTypeCreator, optionsAccessor) 149 | { 150 | } 151 | 152 | public override CommonResponse? ExceptionWrap(HttpContext context, Exception exception) 153 | { 154 | return new CommonResponse() { Code = "E4000", Tips = "SERVER ERROR" }; 155 | } 156 | 157 | public override CommonResponse? InvalidModelStateWrap(ActionContext context) 158 | { 159 | return new CommonResponse() { Code = "E3000", Tips = "SERVER ERROR" }; 160 | } 161 | 162 | public override CommonResponse? NotOKStatusCodeWrap(HttpContext context) 163 | { 164 | return null; 165 | } 166 | 167 | protected override CommonResponse? ActionEmptyResultWrap(ResultExecutingContext context, EmptyResult emptyResult, ResponseDescription? description) 168 | { 169 | return new CommonResponse() { Code = description?.Code ?? "E2000", Tips = description?.Message ?? "NO CONTENT" }; 170 | } 171 | 172 | protected override CommonResponse? ActionObjectResultWrap(ResultExecutingContext context, ObjectResult objectResult, ResponseDescription? description) 173 | { 174 | return new CommonResponse() { Code = description?.Code ?? "E2000", Tips = description?.Message ?? "NO CONTENT", Result = objectResult.Value }; 175 | } 176 | } 177 | ``` 178 | 179 | - 当 `wrapper` 返回 `null` 时,则不进行包装; 180 | 181 | #### 4.1.3 配置使用自定义类型 182 | 183 | ```C# 184 | services.AddResponseAutoWrapper, string, string>() 185 | .ConfigureWrappers(options => options.AddWrappers()); 186 | ``` 187 | 188 | - Response `Data` 对应的泛型参数在此处必须为 `object`; 189 | - 此示例中 `TCode` 为 `string`,`TMessage` 为 `string`,则使用 `DescribeResponse` 进行描述时,参数类型必须对应为 `string`, `string`; 190 | 191 | 至此已完成配置,统一响应内容格式变更为: 192 | ```json 193 | { 194 | "code": "string", 195 | "tips": "string", 196 | "result": {} 197 | } 198 | ``` 199 | 200 | ------ 201 | 202 | ## Note!!! 203 | - 仅当`禁用OpenAPI支持时`,`TResponse`才能不是一个泛型参数为`object`的泛型; 204 | - 更多信息可参考 `sample/CustomStructureWebApplication`、`sample/SimpleWebApplication` 以及 `test/ResponseAutoWrapper.TestHost` 项目; 205 | - 默认情况下不会包装使用`[NoResponseWrapAttribute]`标记的方法; 206 | 207 | ### 4.3 其它自定义 208 | 209 | 使用自行实现的接口注入DI容器替换掉默认实现即可完成一些其它的自定义 210 | 211 | - `IActionResultWrapper`: ActionResult包装器; 212 | - `IExceptionWrapper`: 捕获异常时的响应包装器; 213 | - `IInvalidModelStateWrapper`: 模型验证失败时的响应包装器; 214 | - `INotOKStatusCodeWrapper`: 非200状态码时的响应包装器; 215 | - `IWrapTypeCreator`: 确认Action返回对象类型是否需要包装,以及创建OpenAPI展示的泛型类; 216 | 217 | ### 4.4 动态取消包装 218 | 调用 `HttpContext` 的拓展方法 `DoNotWrapResponse` ,以动态的取消对当前响应的包装;使用拓展方法 `IsSetDoNotWrapResponse` 可以检查当前上下文是否已标记为不包装响应值; 219 | ```C# 220 | HttpContext.DoNotWrapResponse(); 221 | ``` 222 | 223 | ## 5. 性能测试结果 224 | 225 | #### 多次迭代后,数据可能略微变动,但影响理论上仍然是固定比例的,不会随响应内容大小而变化 226 | 227 | - 系统:`Ubuntu20.04 on WSL2` host by Windows10-21H1 228 | - CPU:`I7-8700` 229 | - 平台:`asp.net core 5.0` 230 | - 测试软件:`wrk` 231 | - 测试软件参数:`-t 3 -c 100 -d 30s` 232 | - 测试Action为: 233 | ```C# 234 | [HttpGet] 235 | public IEnumerable Get(int count = 5) 236 | { 237 | return WeatherForecast.GenerateData(count); 238 | } 239 | ``` 240 | - 测试均为使用`localhost`,以尽量减少网络的影响; 241 | - 对比对象分别为: 242 | - Origin:原生,没有进行包装 243 | - Cuture.AspNetCore.ResponseAutoWrapper:此自动包装库 244 | - [AutoWrapper.Core](https://github.com/proudmonkey/AutoWrapper):另一个同类型的自动包装库 245 | - 结果为多次运行取峰值; 246 | - 测试环境不专业,会有一定的误差,数值仅供参考; 247 | 248 | ### 数据(单位 `Requests/sec`) 249 | |`count` | Origin |Cuture.AspNetCore.ResponseAutoWrapper| AutoWrapper.Core | 250 | | ---- | ---- | ---- | ---- | 251 | | 1 |123267.40 |111868.34 |91202.04 | 252 | | 10 |108264.80 |103125.32 |67001.92 | 253 | | 50 |76310.72 |73451.83 |32275.47 | 254 | -------------------------------------------------------------------------------- /execution_flow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/stratosblue/Cuture.AspNetCore.ResponseAutoWrapper/826393b4ba22b72e4206d2ef111a638642c366bb/execution_flow.png -------------------------------------------------------------------------------- /global.json: -------------------------------------------------------------------------------- 1 | { 2 | "msbuild-sdks": { 3 | "MSTest.Sdk": "3.8.3" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /sample/CustomStructureWebApplication/CommonResponse.cs: -------------------------------------------------------------------------------- 1 | using System.Text.Json.Serialization; 2 | 3 | namespace CustomStructureWebApplication; 4 | 5 | public class CommonResponse 6 | { 7 | #region Public 属性 8 | 9 | public string Code { get; set; } 10 | 11 | public TData Data { get; set; } 12 | 13 | public RichMessage Message { get; set; } 14 | 15 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingNull)] 16 | public string TraceId { get; set; } 17 | 18 | #endregion Public 属性 19 | } 20 | 21 | public class RichMessage 22 | { 23 | #region Public 属性 24 | 25 | public string Content { get; set; } 26 | 27 | #endregion Public 属性 28 | } 29 | -------------------------------------------------------------------------------- /sample/CustomStructureWebApplication/Controllers/WeatherForecastController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | using Microsoft.AspNetCore.Http; 6 | using Microsoft.AspNetCore.Mvc; 7 | using Microsoft.Extensions.Logging; 8 | 9 | namespace CustomStructureWebApplication.Controllers; 10 | 11 | [ApiController] 12 | [Route("[controller]")] 13 | public class WeatherForecastController(ILogger logger) : ControllerBase 14 | { 15 | #region Private 字段 16 | 17 | private static readonly string[] Summaries = new[] 18 | { 19 | "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" 20 | }; 21 | 22 | #endregion Private 字段 23 | #region Public 构造函数 24 | 25 | #endregion Public 构造函数 26 | 27 | #region Public 方法 28 | 29 | [HttpGet] 30 | public IEnumerable Get() 31 | { 32 | HttpContext.DescribeResponse("E1001", new RichMessage() { Content = "SUCCESS" }); 33 | var rng = new Random(); 34 | return Enumerable.Range(1, 5).Select(index => new WeatherForecast 35 | { 36 | Date = DateTime.Now.AddDays(index), 37 | TemperatureC = rng.Next(-20, 55), 38 | Summary = Summaries[rng.Next(Summaries.Length)] 39 | }) 40 | .ToArray(); 41 | } 42 | 43 | [HttpGet] 44 | [Route("cm")] 45 | public CommonResponse GetWithCustomMessage() 46 | { 47 | return new CommonResponse() { Code = "E2000", Message = new() { Content = "自定义消息" } }; 48 | } 49 | 50 | [HttpGet] 51 | [Route("ex")] 52 | public CommonResponse ThrowException() 53 | { 54 | throw new Exception("Some Exception Throwed."); 55 | } 56 | 57 | #endregion Public 方法 58 | } 59 | -------------------------------------------------------------------------------- /sample/CustomStructureWebApplication/CustomStructureWebApplication.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net8.0 5 | 6 | false 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /sample/CustomStructureWebApplication/CustomWrapper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | 4 | using Cuture.AspNetCore.ResponseAutoWrapper; 5 | 6 | using Microsoft.AspNetCore.Http; 7 | using Microsoft.AspNetCore.Mvc; 8 | using Microsoft.AspNetCore.Mvc.Filters; 9 | using Microsoft.Extensions.Options; 10 | 11 | namespace CustomStructureWebApplication; 12 | 13 | public class CustomWrapper(IWrapTypeCreator wrapTypeCreator, IOptions optionsAccessor) 14 | : AbstractResponseWrapper, string, RichMessage>(wrapTypeCreator, optionsAccessor) 15 | { 16 | 17 | #region Public 方法 18 | 19 | public override CommonResponse? ExceptionWrap(HttpContext context, Exception exception) 20 | { 21 | return new CommonResponse() { Code = "E4000", Message = new RichMessage() { Content = "SERVER ERROR" }, TraceId = Activity.Current.TraceId.ToString() }; 22 | } 23 | 24 | public override CommonResponse? InvalidModelStateWrap(ActionContext context) 25 | { 26 | return new CommonResponse() { Code = "E3000", Message = new RichMessage() { Content = "INPUT ERROR" }, TraceId = Activity.Current.TraceId.ToString() }; 27 | } 28 | 29 | public override CommonResponse? NotOKStatusCodeWrap(HttpContext context) 30 | { 31 | return null; 32 | } 33 | 34 | #endregion Public 方法 35 | 36 | #region Protected 方法 37 | 38 | protected override CommonResponse? ActionEmptyResultWrap(ResultExecutingContext context, EmptyResult emptyResult, ResponseDescription? description) 39 | { 40 | return new CommonResponse() { Code = description?.Code ?? "E2000", Message = description?.Message ?? new RichMessage() { Content = "NO CONTENT" } }; 41 | } 42 | 43 | protected override CommonResponse? ActionObjectResultWrap(ResultExecutingContext context, ObjectResult objectResult, ResponseDescription? description) 44 | { 45 | return new CommonResponse() { Code = description?.Code ?? "E1000", Data = objectResult.Value, Message = description?.Message ?? new RichMessage() { Content = "OK" } }; 46 | } 47 | 48 | #endregion Protected 方法 49 | } 50 | -------------------------------------------------------------------------------- /sample/CustomStructureWebApplication/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Hosting; 2 | using Microsoft.Extensions.Hosting; 3 | 4 | namespace CustomStructureWebApplication; 5 | 6 | public class Program 7 | { 8 | #region Public 方法 9 | 10 | public static IHostBuilder CreateHostBuilder(string[] args) => 11 | Host.CreateDefaultBuilder(args) 12 | .ConfigureWebHostDefaults(webBuilder => 13 | { 14 | webBuilder.UseStartup(); 15 | }); 16 | 17 | public static void Main(string[] args) 18 | { 19 | CreateHostBuilder(args).Build().Run(); 20 | } 21 | 22 | #endregion Public 方法 23 | } 24 | -------------------------------------------------------------------------------- /sample/CustomStructureWebApplication/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "CustomStructureWebApplication": { 4 | "commandName": "Project", 5 | "dotnetRunMessages": "true", 6 | "launchBrowser": true, 7 | "launchUrl": "swagger", 8 | "applicationUrl": "http://localhost:5000", 9 | "environmentVariables": { 10 | "ASPNETCORE_ENVIRONMENT": "Development" 11 | } 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /sample/CustomStructureWebApplication/Startup.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | 3 | using Microsoft.AspNetCore.Builder; 4 | using Microsoft.AspNetCore.Hosting; 5 | using Microsoft.Extensions.DependencyInjection; 6 | using Microsoft.Extensions.Hosting; 7 | using Microsoft.OpenApi.Models; 8 | 9 | namespace CustomStructureWebApplication; 10 | 11 | public class Startup 12 | { 13 | 14 | #region Public 方法 15 | 16 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 17 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env) 18 | { 19 | app.Use(async (context, next) => 20 | { 21 | using var activity = new Activity("request"); 22 | Activity.Current = activity; 23 | activity.Start(); 24 | try 25 | { 26 | await next(); 27 | } 28 | finally 29 | { 30 | activity.Stop(); 31 | } 32 | }); 33 | 34 | app.UseResponseAutoWrapper(); 35 | 36 | if (env.IsDevelopment()) 37 | { 38 | //app.UseDeveloperExceptionPage(); 39 | app.UseSwagger(); 40 | app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "CustomStructureWebApplication v1")); 41 | } 42 | 43 | app.UseRouting(); 44 | 45 | app.UseAuthorization(); 46 | 47 | app.UseEndpoints(endpoints => 48 | { 49 | endpoints.MapControllers(); 50 | }); 51 | } 52 | 53 | // This method gets called by the runtime. Use this method to add services to the container. 54 | public void ConfigureServices(IServiceCollection services) 55 | { 56 | services.AddControllers(); 57 | services.AddSwaggerGen(c => 58 | { 59 | c.SwaggerDoc("v1", new OpenApiInfo { Title = "CustomStructureWebApplication", Version = "v1" }); 60 | }); 61 | 62 | services.AddResponseAutoWrapper, string, RichMessage>() 63 | .ConfigureWrappers(options => options.AddWrappers()); 64 | } 65 | 66 | #endregion Public 方法 67 | } 68 | -------------------------------------------------------------------------------- /sample/CustomStructureWebApplication/WeatherForecast.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace CustomStructureWebApplication; 4 | 5 | public class WeatherForecast 6 | { 7 | #region Public 属性 8 | 9 | public DateTime Date { get; set; } 10 | 11 | public string Summary { get; set; } 12 | 13 | public int TemperatureC { get; set; } 14 | 15 | public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); 16 | 17 | #endregion Public 属性 18 | } 19 | -------------------------------------------------------------------------------- /sample/CustomStructureWebApplication/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /sample/CustomStructureWebApplication/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | }, 9 | "AllowedHosts": "*" 10 | } 11 | -------------------------------------------------------------------------------- /sample/SimpleWebApplication/Controllers/WeatherForecastController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | 5 | using Microsoft.AspNetCore.Http; 6 | using Microsoft.AspNetCore.Mvc; 7 | 8 | namespace SimpleWebApplication.Controllers; 9 | 10 | [ApiController] 11 | [Route("[controller]")] 12 | public class WeatherForecastController 13 | : ControllerBase 14 | { 15 | #region Private 字段 16 | 17 | private static readonly string[] Summaries = new[] 18 | { 19 | "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" 20 | }; 21 | 22 | #endregion Private 字段 23 | #region Public 构造函数 24 | 25 | #endregion Public 构造函数 26 | 27 | #region Public 方法 28 | 29 | [HttpGet] 30 | public IEnumerable Get() 31 | { 32 | HttpContext.DescribeResponse(10086, "OK"); 33 | var rng = new Random(); 34 | return Enumerable.Range(1, 5).Select(index => new WeatherForecast 35 | { 36 | Date = DateTime.Now.AddDays(index), 37 | TemperatureC = rng.Next(-20, 55), 38 | Summary = Summaries[rng.Next(Summaries.Length)] 39 | }) 40 | .ToArray(); 41 | } 42 | 43 | [HttpGet] 44 | [Route("cm")] 45 | public ApiResponse GetWithCustomMessage() 46 | { 47 | return EmptyApiResponse.Create(200, "自定义消息"); 48 | } 49 | 50 | [HttpGet] 51 | [Route("ex")] 52 | public ApiResponse ThrowException() 53 | { 54 | throw new Exception("Some Exception Throwed."); 55 | } 56 | 57 | #endregion Public 方法 58 | } 59 | -------------------------------------------------------------------------------- /sample/SimpleWebApplication/CustomExceptionWrapper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | using Cuture.AspNetCore.ResponseAutoWrapper; 4 | 5 | using Microsoft.AspNetCore.Http; 6 | using Microsoft.AspNetCore.Mvc; 7 | 8 | namespace SimpleWebApplication; 9 | 10 | // 自定义 Exception 包装器 11 | 12 | public class CustomExceptionWrapper : IExceptionWrapper, int, string> 13 | { 14 | #region Public 方法 15 | 16 | public GenericApiResponse? Wrap(HttpContext context, Exception exception) 17 | { 18 | return new GenericApiResponse(13579) 19 | { 20 | Message = exception.StackTrace, 21 | }; 22 | } 23 | 24 | #endregion Public 方法 25 | } 26 | -------------------------------------------------------------------------------- /sample/SimpleWebApplication/Program.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Hosting; 2 | using Microsoft.Extensions.Hosting; 3 | 4 | namespace SimpleWebApplication; 5 | 6 | public class Program 7 | { 8 | #region Public 方法 9 | 10 | public static IHostBuilder CreateHostBuilder(string[] args) => 11 | Host.CreateDefaultBuilder(args) 12 | .ConfigureWebHostDefaults(webBuilder => 13 | { 14 | webBuilder.UseStartup(); 15 | }); 16 | 17 | public static void Main(string[] args) 18 | { 19 | CreateHostBuilder(args).Build().Run(); 20 | } 21 | 22 | #endregion Public 方法 23 | } 24 | -------------------------------------------------------------------------------- /sample/SimpleWebApplication/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "SimpleWebApplication": { 4 | "commandName": "Project", 5 | "dotnetRunMessages": "true", 6 | "launchBrowser": true, 7 | "launchUrl": "swagger", 8 | "applicationUrl": "http://localhost:5000", 9 | "environmentVariables": { 10 | "ASPNETCORE_ENVIRONMENT": "Development" 11 | } 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /sample/SimpleWebApplication/SimpleWebApplication.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net8.0 5 | 6 | false 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /sample/SimpleWebApplication/Startup.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Builder; 2 | using Microsoft.AspNetCore.Hosting; 3 | using Microsoft.Extensions.DependencyInjection; 4 | using Microsoft.Extensions.Hosting; 5 | using Microsoft.OpenApi.Models; 6 | 7 | namespace SimpleWebApplication; 8 | 9 | public class Startup 10 | { 11 | 12 | #region Public 方法 13 | 14 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 15 | public void Configure(IApplicationBuilder app, IWebHostEnvironment env) 16 | { 17 | //捕获异常、非200状态码的请求,包装响应 18 | app.UseResponseAutoWrapper(options => 19 | { 20 | //配置 21 | //options.CatchExceptions 是否捕获异常 22 | //options.ThrowCaughtExceptions 捕获到异常处理结束后,是否再将异常抛出 23 | //options.DefaultOutputFormatterSelector 默认输出格式化器选择委托,选择在请求中无 Accept 时,用于格式化响应的 IOutputFormatter 24 | }); 25 | 26 | if (env.IsDevelopment()) 27 | { 28 | //app.UseDeveloperExceptionPage(); 29 | app.UseSwagger(); 30 | app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "SimpleWebApplication v1")); 31 | } 32 | 33 | app.UseRouting(); 34 | 35 | app.UseAuthorization(); 36 | 37 | app.UseEndpoints(endpoints => 38 | { 39 | endpoints.MapControllers(); 40 | }); 41 | } 42 | 43 | // This method gets called by the runtime. Use this method to add services to the container. 44 | public void ConfigureServices(IServiceCollection services) 45 | { 46 | services.AddControllers(); 47 | services.AddSwaggerGen(c => 48 | { 49 | c.SwaggerDoc("v1", new OpenApiInfo { Title = "SimpleWebApplication", Version = "v1" }); 50 | }); 51 | 52 | //自动包装ActionResult 53 | services.AddResponseAutoWrapper(options => 54 | { 55 | //配置 56 | //options.ActionNoWrapPredicate //Action的筛选委托,默认会过滤掉标记了NoResponseWrapAttribute的方法 57 | //options.DisableOpenAPISupport //禁用OpenAPI支持,Swagger将不会显示包装后的格式,也会解除响应类型必须为object泛型的限制 58 | //options.HandleAuthorizationResult //处理授权结果(可能无效,需要自行测试) 59 | //options.HandleInvalidModelState //处理无效模型状态 60 | //options.RewriteStatusCode = null; //包装时不覆写非200的HTTP状态码 61 | }); 62 | } 63 | 64 | #endregion Public 方法 65 | } 66 | -------------------------------------------------------------------------------- /sample/SimpleWebApplication/WeatherForecast.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace SimpleWebApplication; 4 | 5 | public class WeatherForecast 6 | { 7 | #region Public 属性 8 | 9 | public DateTime Date { get; set; } 10 | 11 | public string Summary { get; set; } 12 | 13 | public int TemperatureC { get; set; } 14 | 15 | public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); 16 | 17 | #endregion Public 属性 18 | } 19 | -------------------------------------------------------------------------------- /sample/SimpleWebApplication/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /sample/SimpleWebApplication/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | }, 9 | "AllowedHosts": "*" 10 | } 11 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseAutoWrapper/Constants.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Http; 2 | using Microsoft.AspNetCore.Mvc.ApplicationModels; 3 | 4 | namespace Cuture.AspNetCore.ResponseAutoWrapper; 5 | 6 | /// 7 | /// ResponseAutoWrapper 相关常量 8 | /// 9 | public static class Constants 10 | { 11 | #region Public 字段 12 | 13 | /// 14 | /// 中存放 ActionResult 处理策略的Key 15 | /// 16 | public const string ActionPropertiesResultPolicyKey = "ACTION_PROPERTIES_RESULT_POLICY_KEY"; 17 | 18 | /// 19 | /// 默认的 FilterOrder 20 | /// 21 | public const int DefaultFilterOrder = -5000; 22 | 23 | /// 24 | /// 中存放不包装响应内容的标记的Key 25 | /// 26 | public const string HttpContextDoNotWrapResponseMarkKey = "HTTPCONTEXT_DO_NOT_WRAP_RESPONSE_MARK_KEY"; 27 | 28 | #endregion Public 字段 29 | } 30 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseAutoWrapper/Cuture.AspNetCore.ResponseAutoWrapper.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | README.md 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | true 17 | true 18 | 19 | 1.2.5 20 | Response and exception automatic wrapper for `asp.net core` to provide a consistent response content format for `Action`. 用于`asp.net core`的响应和异常自动包装器,使`Action`提供一致的响应内容格式。 21 | 22 | Cuture.AspNetCore.ResponseAutoWrapper 23 | Stratos 24 | MIT 25 | https://github.com/StratosBlue/Cuture.AspNetCore.ResponseAutoWrapper 26 | 27 | git 28 | $(PackageProjectUrl) 29 | 30 | response-wrapper responsewrapper response-wrap responsewrap autowrap auto-wrapper autowrapper response-auto-wrapper responseautowrapper asp-net-core 31 | 32 | 33 | 34 | true 35 | true 36 | snupkg 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | true 45 | 46 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseAutoWrapper/Extensions/ApplicationBuilderResponseAutoWrapperExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | using Cuture.AspNetCore.ResponseAutoWrapper; 4 | 5 | using Microsoft.AspNetCore.Http; 6 | 7 | namespace Microsoft.AspNetCore.Builder; 8 | 9 | /// 10 | /// AutoWrapper的 ApplicationBuilder 拓展 11 | /// 12 | public static class ApplicationBuilderResponseAutoWrapperExtensions 13 | { 14 | #region Public 方法 15 | 16 | /// 17 | /// 使用AutoWrapper中间件 18 | /// 包装请求中的异常,并有条件的包装响应状态码非 的请求 19 | /// 20 | /// 21 | /// 中间件配置 22 | /// 23 | public static IApplicationBuilder UseResponseAutoWrapper(this IApplicationBuilder app, ResponseAutoWrapMiddlewareOptions? options = null) 24 | { 25 | return app.UseMiddleware(app.ApplicationServices, options ?? new ResponseAutoWrapMiddlewareOptions()); 26 | } 27 | 28 | /// 29 | /// 使用AutoWrapper中间件 30 | /// 包装请求中的异常,并有条件的包装响应状态码非 的请求 31 | /// 32 | /// 33 | /// 中间件配置设置委托 34 | /// 35 | public static IApplicationBuilder UseResponseAutoWrapper(this IApplicationBuilder app, Action? optionsSetupAction) 36 | { 37 | var options = new ResponseAutoWrapMiddlewareOptions(); 38 | optionsSetupAction?.Invoke(options); 39 | 40 | return app.UseMiddleware(app.ApplicationServices, options); 41 | } 42 | 43 | #endregion Public 方法 44 | } 45 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseAutoWrapper/Extensions/DoNotWrapResponseMarkHttpContextExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Runtime.CompilerServices; 2 | 3 | using Cuture.AspNetCore.ResponseAutoWrapper; 4 | 5 | namespace Microsoft.AspNetCore.Http; 6 | 7 | /// 8 | /// 标记不包装响应的HttpContext拓展 9 | /// 10 | public static class DoNotWrapResponseMarkHttpContextExtensions 11 | { 12 | //TODO add test for this 13 | 14 | #region Public 方法 15 | 16 | /// 17 | /// 标记不包装当前上下文的响应 18 | /// 19 | /// 20 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 21 | public static void DoNotWrapResponse(this HttpContext httpContext) 22 | { 23 | if (!httpContext.Items.ContainsKey(Constants.HttpContextDoNotWrapResponseMarkKey)) 24 | { 25 | httpContext.Items.Add(Constants.HttpContextDoNotWrapResponseMarkKey, string.Empty); 26 | } 27 | } 28 | 29 | /// 30 | /// 检查当前上下文是否标记了不包装响应 31 | /// 32 | /// 33 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 34 | public static bool IsSetDoNotWrapResponse(this HttpContext httpContext) => httpContext.Items.ContainsKey(Constants.HttpContextDoNotWrapResponseMarkKey); 35 | 36 | #endregion Public 方法 37 | } 38 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseAutoWrapper/Extensions/IResponseAutoWrapperBuilderExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | using Cuture.AspNetCore.ResponseAutoWrapper; 4 | 5 | using Microsoft.AspNetCore.Mvc; 6 | using Microsoft.Extensions.DependencyInjection.Extensions; 7 | 8 | namespace Microsoft.Extensions.DependencyInjection; 9 | 10 | /// 11 | /// 12 | /// 13 | public static class IResponseAutoWrapperBuilderExtensions 14 | { 15 | #region Public 方法 16 | 17 | /// 18 | /// 配置使用的包装器 19 | /// 20 | /// 21 | /// 22 | /// 23 | /// 24 | /// 25 | /// 26 | public static IResponseAutoWrapperBuilder ConfigureWrappers(this IResponseAutoWrapperBuilder builder, 27 | Action> wrapperConfigureAction) 28 | where TResponse : class 29 | { 30 | wrapperConfigureAction(new WrapperBuilder(builder.Services)); 31 | return builder; 32 | } 33 | 34 | /// 35 | /// 添加默认的旧响应包装器(TCode 为 ,TMessage 为 的响应类型) 36 | /// 37 | /// 38 | /// 39 | internal static IResponseAutoWrapperBuilder, int, string> AddDefaultLegacyWrappers(this IResponseAutoWrapperBuilder, int, string> builder) 40 | { 41 | new WrapperBuilder, int, string>(builder.Services).AddLegacyWrappers(); 42 | return builder; 43 | } 44 | 45 | #endregion Public 方法 46 | 47 | #region Public 类 48 | 49 | /// 50 | /// Wrapper构建器 51 | /// 52 | /// 53 | /// 54 | /// 55 | public sealed class WrapperBuilder 56 | where TResponse : class 57 | { 58 | #region Private 字段 59 | 60 | private readonly IServiceCollection _services; 61 | 62 | #endregion Private 字段 63 | 64 | #region Public 构造函数 65 | 66 | internal WrapperBuilder(IServiceCollection services) 67 | { 68 | ArgumentNullException.ThrowIfNull(services); 69 | 70 | _services = services; 71 | } 72 | 73 | #endregion Public 构造函数 74 | 75 | #region Public 方法 76 | 77 | /// 78 | /// 尝试将派生自 的类添加为多个 Wrapper 79 | /// 80 | /// 81 | public WrapperBuilder AddLegacyWrappers() 82 | where TLegacyCompatibleWrapper : LegacyCompatibleResponseWrapper 83 | { 84 | _services.TryAddWrapper(); 85 | return this; 86 | } 87 | 88 | /// 89 | /// 检查 实现的包装接口,将其添加为对应的包装器 90 | /// 91 | /// 92 | /// 注册DI容器的生命周期 93 | /// 94 | /// 95 | public WrapperBuilder AddWrapper(ServiceLifetime lifetime = ServiceLifetime.Singleton) 96 | where TWrapper : IWrapper 97 | { 98 | bool hasAdded = false; 99 | if (TryAddWrapper>(lifetime)) 100 | { 101 | hasAdded = true; 102 | } 103 | if (TryAddWrapper>(lifetime)) 104 | { 105 | hasAdded = true; 106 | } 107 | if (TryAddWrapper>(lifetime)) 108 | { 109 | hasAdded = true; 110 | } 111 | if (TryAddWrapper>(lifetime)) 112 | { 113 | hasAdded = true; 114 | } 115 | if (!hasAdded) 116 | { 117 | throw new ArgumentException($"{typeof(TWrapper)} not implemented any available wrapper interface for response type {typeof(TResponse)}."); 118 | } 119 | return this; 120 | } 121 | 122 | /// 123 | /// 将 添加为 包装器 124 | /// 125 | /// 126 | /// 127 | /// 注册DI容器的生命周期 128 | /// 129 | /// 130 | public WrapperBuilder AddWrapper(ServiceLifetime lifetime = ServiceLifetime.Singleton) 131 | where TWrapper : IWrapper, TWrapperInterface 132 | where TWrapperInterface : IWrapper 133 | { 134 | if (!TryAddWrapper(lifetime)) 135 | { 136 | throw new ArgumentException($"{typeof(TWrapper)} not implemented wrapper interface {typeof(TWrapperInterface)} for response type {typeof(TResponse)}."); 137 | } 138 | 139 | return this; 140 | } 141 | 142 | /// 143 | /// 尝试将派生自 的类添加为多个 Wrapper 144 | /// 145 | /// 146 | /// 注册DI容器的生命周期 147 | /// 148 | public WrapperBuilder AddWrappers(ServiceLifetime lifetime = ServiceLifetime.Singleton) 149 | where TWrapper : AbstractResponseWrapper 150 | { 151 | _services.TryAddWrapper(lifetime); 152 | return this; 153 | } 154 | 155 | /// 156 | /// 尝试将 添加为 包装器 157 | /// 158 | /// 159 | /// 160 | /// 注册DI容器的生命周期 161 | /// 162 | /// 163 | public bool TryAddWrapper(ServiceLifetime lifetime = ServiceLifetime.Singleton) 164 | where TWrapper : IWrapper 165 | where TWrapperInterface : IWrapper 166 | { 167 | var wrapperType = typeof(TWrapper); 168 | if (wrapperType.IsAssignableTo(typeof(IActionResultWrapper))) 169 | { 170 | _services.TryAddEnumerable(ServiceDescriptor.Describe(typeof(TWrapperInterface), wrapperType, lifetime)); 171 | return true; 172 | } 173 | 174 | return false; 175 | } 176 | 177 | #endregion Public 方法 178 | } 179 | 180 | #endregion Public 类 181 | } 182 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseAutoWrapper/Extensions/ResponseDescriptionHttpContextExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics.CodeAnalysis; 3 | using System.Runtime.CompilerServices; 4 | 5 | using Cuture.AspNetCore.ResponseAutoWrapper; 6 | 7 | using Microsoft.AspNetCore.Mvc.Filters; 8 | 9 | namespace Microsoft.AspNetCore.Http; 10 | 11 | /// 12 | /// 响应描述的HttpContext拓展 13 | /// 14 | public static class ResponseDescriptionHttpContextExtensions 15 | { 16 | #region Public 方法 17 | 18 | /// 19 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 20 | public static void DescribeResponse(this HttpContext httpContext, TCode code) => httpContext.Items[typeof(ResponseDescription<,>)] = new ResponseDescription(code); 21 | 22 | /// 23 | /// 描述响应 24 | /// 通过向 添加 以描述响应 25 | /// 26 | /// 27 | /// 响应码 28 | /// 响应消息 29 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 30 | public static void DescribeResponse(this HttpContext httpContext, TCode code, TMessage message) => httpContext.Items[typeof(ResponseDescription<,>)] = new ResponseDescription(code, message); 31 | 32 | /// 33 | /// 尝试获取描述响应 34 | /// 从 中读取 35 | /// 36 | /// 37 | /// 返回 中的 ,当不存在时,返回 null 38 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 39 | public static ResponseDescription? GetResponseDescription(this HttpContext httpContext) 40 | { 41 | if (httpContext.Items.TryGetValue(typeof(ResponseDescription<,>), out var storedObject)) 42 | { 43 | if (storedObject is ResponseDescription storedDescription) 44 | { 45 | return storedDescription; 46 | } 47 | throw DescriptionTypeNotMatchException(); 48 | } 49 | 50 | return null; 51 | } 52 | 53 | /// 54 | /// 55 | /// 56 | /// 57 | /// 58 | /// 59 | /// 60 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 61 | public static ResponseDescription? GetResponseDescription(this ResultExecutingContext executingContext) 62 | { 63 | return executingContext.HttpContext.GetResponseDescription(); 64 | } 65 | 66 | /// 67 | /// 尝试获取描述响应 68 | /// 从 中读取 69 | /// 70 | /// 71 | /// 72 | /// 返回 中的 ,当不存在时,返回 null 73 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 74 | public static bool TryGetResponseDescription(this HttpContext httpContext, [NotNullWhen(true)] out ResponseDescription? description) 75 | { 76 | if (httpContext.Items.TryGetValue(typeof(ResponseDescription<,>), out var storedObject)) 77 | { 78 | if (storedObject is ResponseDescription storedDescription) 79 | { 80 | description = storedDescription; 81 | return true; 82 | } 83 | throw DescriptionTypeNotMatchException(); 84 | } 85 | 86 | description = null; 87 | return false; 88 | } 89 | 90 | /// 91 | /// 92 | /// 93 | /// 94 | /// 95 | /// 96 | /// 97 | /// 98 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 99 | public static bool TryGetResponseDescription(this ResultExecutingContext executingContext, [NotNullWhen(true)] out ResponseDescription? description) 100 | { 101 | return executingContext.HttpContext.TryGetResponseDescription(out description); 102 | } 103 | 104 | #endregion Public 方法 105 | 106 | #region Private 方法 107 | 108 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 109 | private static InvalidOperationException DescriptionTypeNotMatchException() 110 | { 111 | return new InvalidOperationException($"The http context has description object. But it's not the instance of {typeof(ResponseDescription)}. This situation is likely to be the description type use error. Please check it."); 112 | } 113 | 114 | #endregion Private 方法 115 | } 116 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseAutoWrapper/Extensions/TypeExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using System.Threading.Tasks; 3 | 4 | namespace System; 5 | 6 | internal static class TypeExtensions 7 | { 8 | #region Public 方法 9 | 10 | /// 11 | /// 查找 所实现的泛型类定义 的泛型参数 12 | /// 13 | /// 14 | /// typeof(GenericClassName<>) 15 | /// 16 | public static Type[] FindImplementedGenericClassArguments(this Type type, Type genericClassType) 17 | { 18 | ArgumentNullException.ThrowIfNull(type); 19 | ArgumentNullException.ThrowIfNull(genericClassType); 20 | 21 | if (!genericClassType.IsGenericTypeDefinition 22 | || genericClassType.IsInterface) 23 | { 24 | throw new ArgumentException("must be a generic type definition class.", nameof(genericClassType)); 25 | } 26 | 27 | while (type != null && type != typeof(object)) 28 | { 29 | if (type.IsTheRawGenericType(genericClassType)) 30 | { 31 | return type.GetGenericArguments(); 32 | } 33 | type = type.BaseType!; 34 | } 35 | 36 | return []; 37 | } 38 | 39 | /// 40 | /// 查找所有 所实现的泛型接口定义 的泛型参数 41 | /// 42 | /// 43 | /// typeof(GenericInterfaceName<>) 44 | /// 45 | public static Type[][] FindImplementedGenericInterfaceArguments(this Type type, Type genericInterfaceType) 46 | { 47 | ArgumentNullException.ThrowIfNull(type); 48 | ArgumentNullException.ThrowIfNull(genericInterfaceType); 49 | 50 | if (!genericInterfaceType.IsGenericTypeDefinition 51 | || !genericInterfaceType.IsInterface) 52 | { 53 | throw new ArgumentException("must be a generic type definition interface.", nameof(genericInterfaceType)); 54 | } 55 | 56 | return type.GetInterfaces() 57 | .Where(m => m.IsTheRawGenericType(genericInterfaceType)) 58 | .Select(m => m.GetGenericArguments()) 59 | .ToArray(); 60 | } 61 | 62 | /// 63 | /// 判断类型 是否是指定泛型类型 的子类型 64 | /// 65 | /// 66 | /// typeof(GenericTypeName<>) 67 | /// 是否是泛型接口的子类型 68 | public static bool HasImplementedGeneric(this Type type, Type generic) 69 | { 70 | ArgumentNullException.ThrowIfNull(type); 71 | ArgumentNullException.ThrowIfNull(generic); 72 | 73 | if (!generic.IsGenericTypeDefinition) 74 | { 75 | throw new ArgumentException("must be a generic type definition.", nameof(generic)); 76 | } 77 | 78 | if (generic.IsInterface) 79 | { 80 | return type.GetInterfaces().Any(m => m.IsTheRawGenericType(generic)); 81 | } 82 | else 83 | { 84 | while (type != null && type != typeof(object)) 85 | { 86 | if (type.IsTheRawGenericType(generic)) 87 | { 88 | return true; 89 | } 90 | type = type.BaseType!; 91 | } 92 | } 93 | 94 | return false; 95 | } 96 | 97 | /// 98 | /// 判断类型 是否是指定泛型类型 的子类型 99 | /// 100 | /// 101 | /// typeof(GenericTypeName<>) 102 | /// 103 | /// 104 | /// 是否是泛型接口的子类型 105 | public static bool HasImplementedGenericSpecial(this Type originType, Type genericType, Type[] genericArguments, int dataArgumentIndex) 106 | { 107 | //TODO Add test for this 108 | //继承检查的特化处理逻辑 109 | 110 | while (originType != null && originType != typeof(object)) 111 | { 112 | if (originType.IsGenericType 113 | && originType.GetGenericTypeDefinition() is Type originGenericTypeDefinition 114 | && originGenericTypeDefinition == genericType) 115 | { 116 | var originGenericArguments = originType.GetGenericArguments(); 117 | if (originGenericArguments.Length == genericArguments.Length) 118 | { 119 | if (genericArguments.Length == 1) 120 | { 121 | return true; 122 | } 123 | bool isMatch = true; 124 | for (int i = 0; i < genericArguments.Length; i++) 125 | { 126 | if (i == dataArgumentIndex) 127 | { 128 | continue; 129 | } 130 | if (originGenericArguments[i] != genericArguments[i]) 131 | { 132 | isMatch = false; 133 | break; 134 | } 135 | } 136 | if (isMatch) 137 | { 138 | return true; 139 | } 140 | } 141 | } 142 | else if (originType == genericType) 143 | { 144 | return true; 145 | } 146 | originType = originType.BaseType!; 147 | } 148 | 149 | return false; 150 | } 151 | 152 | /// 153 | /// 判断 是否是泛型 本身,或其直接实现 154 | /// 155 | /// 156 | /// typeof(GenericTypeName<>) 157 | /// 158 | public static bool IsTheRawGenericType(this Type type, Type generic) => generic == (type.IsGenericType ? type.GetGenericTypeDefinition() : type); 159 | 160 | /// 161 | /// 解包出类型的 泛型参数 TResult 162 | /// 如果 或其子类,也非 实现,则返回自身 163 | /// 164 | /// 165 | /// 166 | public static Type UnwrapTaskResult(this Type type) 167 | { 168 | if (type.IsValueType) //值类型 169 | { 170 | //获取ValueTask的返回类型 171 | if (type == typeof(ValueTask)) 172 | { 173 | return typeof(void); 174 | } 175 | else if (type.IsGenericType 176 | && type.GetGenericTypeDefinition() == typeof(ValueTask<>)) 177 | { 178 | return type.GetGenericArguments()[0]; 179 | } 180 | } 181 | else //引用类型 182 | { 183 | //获取Task的返回类型 184 | if (type.FindImplementedGenericClassArguments(typeof(Task<>)) is Type[] taskResultTypes 185 | && taskResultTypes.Length == 1) 186 | { 187 | return taskResultTypes[0]; 188 | } 189 | else if (type.IsAssignableTo(typeof(Task))) 190 | { 191 | return typeof(void); 192 | } 193 | } 194 | 195 | return type; 196 | } 197 | 198 | #endregion Public 方法 199 | } 200 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseAutoWrapper/Interfaces/IResponseAutoWrapperBuilder.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.DependencyInjection; 2 | 3 | namespace Cuture.AspNetCore.ResponseAutoWrapper; 4 | 5 | /// 6 | /// ResponseAutoWrapper构建器 7 | /// 8 | public interface IResponseAutoWrapperBuilder 9 | where TResponse : class 10 | { 11 | #region Public 属性 12 | 13 | /// 14 | /// 15 | /// 16 | IServiceCollection Services { get; } 17 | 18 | #endregion Public 属性 19 | } 20 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseAutoWrapper/Interfaces/IWrapTypeCreator.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Cuture.AspNetCore.ResponseAutoWrapper; 4 | 5 | /// 6 | /// 包装类型创建器 7 | /// 8 | public interface IWrapTypeCreator 9 | { 10 | #region Public 属性 11 | 12 | /// 13 | /// 响应类型泛型定义 14 | /// 15 | Type? ResponseGenericType { get; } 16 | 17 | /// 18 | /// 响应类型 19 | /// 20 | Type ResponseType { get; } 21 | 22 | #endregion Public 属性 23 | 24 | #region Public 方法 25 | 26 | /// 27 | /// 创建包装后的类型 28 | /// 29 | /// 30 | /// 31 | Type MakeWrapType(Type type); 32 | 33 | /// 34 | /// 是否应该包装 35 | /// 36 | /// 37 | /// 38 | bool ShouldWrap(Type type); 39 | 40 | #endregion Public 方法 41 | } 42 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseAutoWrapper/Interfaces/Wrapper/IActionResultWrapper.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc.Filters; 2 | 3 | namespace Cuture.AspNetCore.ResponseAutoWrapper; 4 | 5 | /// 6 | /// 中用以包装 ActionResult 的包装器 7 | /// 8 | /// 统一响应类型 9 | /// Code类型 10 | /// Message类型 11 | public interface IActionResultWrapper : IWrapper where TResponse : class 12 | { 13 | #region Public 方法 14 | 15 | /// 16 | /// 通过 返回包装后的统一响应 17 | /// 18 | /// 19 | /// 包装后的统一响应类型对象,返回 null 时,不做处理 20 | TResponse? Wrap(ResultExecutingContext context); 21 | 22 | #endregion Public 方法 23 | } 24 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseAutoWrapper/Interfaces/Wrapper/IExceptionWrapper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | using Microsoft.AspNetCore.Http; 4 | 5 | namespace Cuture.AspNetCore.ResponseAutoWrapper; 6 | 7 | /// 8 | /// 异常包装器 9 | /// 10 | /// 统一响应类型 11 | /// Code类型 12 | /// Message类型 13 | public interface IExceptionWrapper : IWrapper 14 | { 15 | #region Public 方法 16 | 17 | /// 18 | /// 通过 返回包装后的统一响应 19 | /// 20 | /// 21 | /// 22 | /// 包装后的统一响应类型对象,返回 null 时,不做处理 23 | TResponse? Wrap(HttpContext context, Exception exception); 24 | 25 | #endregion Public 方法 26 | } 27 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseAutoWrapper/Interfaces/Wrapper/IInvalidModelStateWrapper.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | 3 | namespace Cuture.AspNetCore.ResponseAutoWrapper; 4 | 5 | /// 6 | /// 无效模型状态包装器 7 | /// 8 | /// 统一响应类型 9 | /// Code类型 10 | /// Message类型 11 | public interface IInvalidModelStateWrapper : IWrapper 12 | { 13 | #region Public 方法 14 | 15 | /// 16 | /// 通过 返回包装后的统一响应 17 | /// 18 | /// 19 | /// 包装后的统一响应类型对象,返回 null ,则使用原始处理逻辑进行处理 20 | TResponse? Wrap(ActionContext context); 21 | 22 | #endregion Public 方法 23 | } 24 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseAutoWrapper/Interfaces/Wrapper/INotOKStatusCodeWrapper.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Http; 2 | 3 | namespace Cuture.AspNetCore.ResponseAutoWrapper; 4 | 5 | /// 6 | /// 时的响应包装器 7 | /// 8 | /// 统一响应类型 9 | /// Code类型 10 | /// Message类型 11 | public interface INotOKStatusCodeWrapper : IWrapper 12 | { 13 | #region Public 方法 14 | 15 | /// 16 | /// 通过 返回包装后的统一响应 17 | /// 18 | /// 19 | /// 包装后的统一响应类型对象,返回 null 时,不做处理 20 | TResponse? Wrap(HttpContext context); 21 | 22 | #endregion Public 方法 23 | } 24 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseAutoWrapper/Interfaces/Wrapper/IWrapper.cs: -------------------------------------------------------------------------------- 1 | namespace Cuture.AspNetCore.ResponseAutoWrapper; 2 | 3 | /// 4 | /// 包装器 5 | /// 6 | public interface IWrapper 7 | { 8 | } 9 | 10 | /// 11 | /// 包装器 12 | /// 13 | /// 统一响应类型 14 | /// Code类型 15 | /// Message类型 16 | public interface IWrapper : IWrapper 17 | { 18 | } 19 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseAutoWrapper/Internal/ActionResultPolicy.cs: -------------------------------------------------------------------------------- 1 | namespace Microsoft.AspNetCore.Mvc.Filters; 2 | 3 | /// 4 | /// Action Result 处理策略 5 | /// 6 | internal enum ActionResultPolicy 7 | { 8 | /// 9 | /// 未知 10 | /// 11 | Unknown = 0, 12 | 13 | /// 14 | /// 处理 15 | /// 16 | Process, 17 | 18 | /// 19 | /// 跳过 20 | /// 21 | Skip, 22 | } 23 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseAutoWrapper/Internal/ActivityExtensions.cs: -------------------------------------------------------------------------------- 1 | #pragma warning disable IDE0130 // 命名空间与文件夹结构不匹配 2 | 3 | using System.Collections.Generic; 4 | 5 | namespace System.Diagnostics; 6 | 7 | internal static class ActivityExtensions 8 | { 9 | #region Public 方法 10 | 11 | /// 12 | /// https://github.com/dotnet/runtime/blob/release/9.0/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/Activity.cs#L552 13 | /// 14 | /// 15 | /// 16 | /// 17 | /// 18 | /// 19 | /// 20 | public static Activity AddException(this Activity activity, Exception exception, in TagList tags = default, DateTimeOffset timestamp = default) 21 | { 22 | ArgumentNullException.ThrowIfNull(exception); 23 | 24 | TagList exceptionTags = tags; 25 | 26 | const string ExceptionEventName = "exception"; 27 | const string ExceptionMessageTag = "exception.message"; 28 | const string ExceptionStackTraceTag = "exception.stacktrace"; 29 | const string ExceptionTypeTag = "exception.type"; 30 | 31 | bool hasMessage = false; 32 | bool hasStackTrace = false; 33 | bool hasType = false; 34 | 35 | for (int i = 0; i < exceptionTags.Count; i++) 36 | { 37 | if (exceptionTags[i].Key == ExceptionMessageTag) 38 | { 39 | hasMessage = true; 40 | } 41 | else if (exceptionTags[i].Key == ExceptionStackTraceTag) 42 | { 43 | hasStackTrace = true; 44 | } 45 | else if (exceptionTags[i].Key == ExceptionTypeTag) 46 | { 47 | hasType = true; 48 | } 49 | } 50 | 51 | if (!hasMessage) 52 | { 53 | exceptionTags.Add(new KeyValuePair(ExceptionMessageTag, exception.Message)); 54 | } 55 | 56 | if (!hasStackTrace) 57 | { 58 | exceptionTags.Add(new KeyValuePair(ExceptionStackTraceTag, exception.ToString())); 59 | } 60 | 61 | if (!hasType) 62 | { 63 | exceptionTags.Add(new KeyValuePair(ExceptionTypeTag, exception.GetType().ToString())); 64 | } 65 | 66 | return activity.AddEvent(new ActivityEvent(ExceptionEventName, timestamp, [.. exceptionTags])); 67 | } 68 | 69 | public static Activity? RecordException(this Activity? activity, Exception exception, in TagList tags = default, DateTimeOffset timestamp = default) 70 | { 71 | if (activity is not null) 72 | { 73 | activity.SetStatus(ActivityStatusCode.Error); 74 | activity.AddException(exception, tags, timestamp); 75 | } 76 | return activity; 77 | } 78 | 79 | #endregion Public 方法 80 | } 81 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseAutoWrapper/Internal/ApiBehaviorOptionsPostConfigureOptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | using Microsoft.AspNetCore.Mvc; 4 | using Microsoft.Extensions.Options; 5 | 6 | namespace Cuture.AspNetCore.ResponseAutoWrapper.Internal; 7 | 8 | internal class ApiBehaviorOptionsPostConfigureOptions(string name, Action action) 9 | : PostConfigureOptions(name, action) 10 | { 11 | } 12 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseAutoWrapper/Internal/DefaultResponseAutoWrapperBuilder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | using Microsoft.Extensions.DependencyInjection; 4 | 5 | namespace Cuture.AspNetCore.ResponseAutoWrapper; 6 | 7 | internal sealed class DefaultResponseAutoWrapperBuilder 8 | : IResponseAutoWrapperBuilder 9 | where TResponse : class 10 | { 11 | #region Public 属性 12 | 13 | public IServiceCollection Services { get; } 14 | 15 | #endregion Public 属性 16 | 17 | #region Public 构造函数 18 | 19 | public DefaultResponseAutoWrapperBuilder(IServiceCollection services) 20 | { 21 | ArgumentNullException.ThrowIfNull(services); 22 | 23 | Services = services; 24 | } 25 | 26 | #endregion Public 构造函数 27 | } 28 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseAutoWrapper/Internal/DefaultWrapTypeCreator.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using System.Reflection; 4 | 5 | namespace Cuture.AspNetCore.ResponseAutoWrapper.Internal; 6 | 7 | internal class DefaultWrapTypeCreator : IWrapTypeCreator 8 | { 9 | #region Private 字段 10 | 11 | /// 12 | /// 创建包装类型的委托 13 | /// 14 | private readonly Func _makeWrapTypeDelegate; 15 | 16 | /// 17 | /// 响应数据的泛型参数位置索引 18 | /// 19 | private readonly int _responseDataGenericTypeIndex = -1; 20 | 21 | /// 22 | /// 响应类型的泛型参数列表 23 | /// 24 | private readonly Type[] _responseTypeGenericArguments = []; 25 | 26 | /// 27 | /// 类型是否应该包装检查委托 28 | /// 29 | private readonly Func _shouldWrapCheckDelegate; 30 | 31 | /// 32 | /// 类型的包装策略缓存 33 | /// 34 | private readonly ConcurrentDictionary _typeWrapPolicyCache = new(); 35 | 36 | #endregion Private 字段 37 | 38 | #region Public 属性 39 | 40 | /// 41 | public Type? ResponseGenericType { get; } 42 | 43 | /// 44 | public Type ResponseType { get; } 45 | 46 | #endregion Public 属性 47 | 48 | #region Public 构造函数 49 | 50 | public DefaultWrapTypeCreator(Type responseType, Type? responseGenericType) 51 | { 52 | ArgumentNullException.ThrowIfNull(responseType); 53 | 54 | ResponseType = responseType; 55 | ResponseGenericType = responseGenericType ??= (responseType.IsGenericType ? responseType.GetGenericTypeDefinition() : null); 56 | 57 | if (responseGenericType is null) 58 | { 59 | _shouldWrapCheckDelegate = ShouldWrapCheckWithOutGenericType; 60 | _makeWrapTypeDelegate = _ => ResponseType; 61 | } 62 | else 63 | { 64 | var genericTypeParameters = responseGenericType.GetTypeInfo().GenericTypeParameters; 65 | _responseDataGenericTypeIndex = genericTypeParameters.Length - 1; 66 | _responseTypeGenericArguments = responseType.GetGenericArguments(); 67 | 68 | _shouldWrapCheckDelegate = ShouldWrapCheck; 69 | _makeWrapTypeDelegate = InternalMakeWrapType; 70 | } 71 | } 72 | 73 | #endregion Public 构造函数 74 | 75 | #region Public 方法 76 | 77 | /// 78 | public Type MakeWrapType(Type type) => _makeWrapTypeDelegate(type); 79 | 80 | /// 81 | public bool ShouldWrap(Type type) => _shouldWrapCheckDelegate(type); 82 | 83 | #endregion Public 方法 84 | 85 | #region Private 方法 86 | 87 | private Type InternalMakeWrapType(Type type) 88 | { 89 | var genericArguments = (Type[])_responseTypeGenericArguments.Clone(); 90 | genericArguments[_responseDataGenericTypeIndex] = type; 91 | return ResponseGenericType!.MakeGenericType(genericArguments); 92 | } 93 | 94 | /// 95 | /// 检查类型是否需要包装(包含泛型检查) 96 | /// 97 | /// 98 | /// 99 | private bool ShouldWrapCheck(Type type) 100 | { 101 | if (_typeWrapPolicyCache.TryGetValue(type.TypeHandle.Value, out var shouldWrap)) 102 | { 103 | return shouldWrap; 104 | } 105 | 106 | //返回值本身就是响应类型或其子类 107 | shouldWrap = !(type.IsAssignableTo(ResponseType) 108 | || type.HasImplementedGenericSpecial(ResponseGenericType!, _responseTypeGenericArguments, _responseDataGenericTypeIndex)); 109 | 110 | _typeWrapPolicyCache.TryAdd(type.TypeHandle.Value, shouldWrap); 111 | 112 | return shouldWrap; 113 | } 114 | 115 | /// 116 | /// 检查类型是否需要包装(不包含泛型检查) 117 | /// 118 | /// 119 | /// 120 | private bool ShouldWrapCheckWithOutGenericType(Type type) 121 | { 122 | //返回值本身就是响应类型或其子类 123 | return !type.IsAssignableTo(ResponseType); 124 | } 125 | 126 | #endregion Private 方法 127 | } 128 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseAutoWrapper/Internal/MvcOptionsPostConfigureOptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | using Microsoft.AspNetCore.Mvc; 4 | using Microsoft.Extensions.Options; 5 | 6 | namespace Cuture.AspNetCore.ResponseAutoWrapper.Internal; 7 | 8 | internal class MvcOptionsPostConfigureOptions(string name, Action action) 9 | : PostConfigureOptions(name, action) 10 | { 11 | } 12 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseAutoWrapper/Internal/ObjectAccessor.cs: -------------------------------------------------------------------------------- 1 | namespace Cuture.AspNetCore.ResponseAutoWrapper.Internal; 2 | 3 | internal class ObjectAccessor 4 | { 5 | #region Public 属性 6 | 7 | public T? Value { get; set; } 8 | 9 | #endregion Public 属性 10 | 11 | #region Public 构造函数 12 | 13 | public ObjectAccessor() 14 | { 15 | } 16 | 17 | public ObjectAccessor(T? value) 18 | { 19 | Value = value; 20 | } 21 | 22 | #endregion Public 构造函数 23 | } 24 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseAutoWrapper/LegacyCompatibleResponseWrapperOptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | using Microsoft.AspNetCore.Http; 4 | using Microsoft.AspNetCore.Mvc.Filters; 5 | 6 | namespace Cuture.AspNetCore.ResponseAutoWrapper; 7 | 8 | /// 9 | /// 兼容旧响应格式的包装器的选项 10 | /// 11 | public class LegacyCompatibleResponseWrapperOptions 12 | { 13 | #region Public 属性 14 | 15 | #region Codes 16 | 17 | /// 18 | /// 异常时的Code 19 | /// 20 | public int ExceptionCode { get; set; } = StatusCodes.Status500InternalServerError; 21 | 22 | /// 23 | /// 无效模型时的Code 24 | /// 25 | public int InvalidModelStateCode { get; set; } = StatusCodes.Status400BadRequest; 26 | 27 | /// 28 | /// 成功时的Code 29 | /// 30 | public int SuccessCode { get; set; } = StatusCodes.Status200OK; 31 | 32 | #endregion Codes 33 | 34 | #region Messages 35 | 36 | /// 37 | /// 在 中使用的默认Message 38 | /// 39 | public string ActionResultWrapMessage { get; set; } = "SUCCESS"; 40 | 41 | /// 42 | /// 在 中使用的默认Message 43 | /// 44 | public string ExceptionWrapMessage { get; set; } = "Server Error"; 45 | 46 | #endregion Messages 47 | 48 | #endregion Public 属性 49 | } 50 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseAutoWrapper/Microsoft.AspNetCore/Authorization/AutoWrapperAuthorizationMiddlewareResultHandler.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | using Microsoft.AspNetCore.Authorization.Policy; 4 | using Microsoft.AspNetCore.Http; 5 | 6 | namespace Microsoft.AspNetCore.Authorization; 7 | 8 | /// 9 | /// AutoWrapper AuthorizationMiddlewareResultHandler 10 | /// 将认证、授权失败的行为修改为设置状态码,并中断后续操作 11 | /// 12 | internal class AutoWrapperAuthorizationMiddlewareResultHandler : IAuthorizationMiddlewareResultHandler 13 | { 14 | #region Public 方法 15 | 16 | /// 17 | public Task HandleAsync(RequestDelegate next, HttpContext context, AuthorizationPolicy policy, PolicyAuthorizationResult authorizeResult) 18 | { 19 | if (authorizeResult.Challenged) 20 | { 21 | context.Response.StatusCode = StatusCodes.Status401Unauthorized; 22 | return Task.CompletedTask; 23 | } 24 | else if (authorizeResult.Forbidden) 25 | { 26 | context.Response.StatusCode = StatusCodes.Status403Forbidden; 27 | return Task.CompletedTask; 28 | } 29 | 30 | return next(context); 31 | } 32 | 33 | #endregion Public 方法 34 | } 35 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseAutoWrapper/Microsoft.AspNetCore/Mvc/ApiResponse.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | using Microsoft.AspNetCore.Http; 4 | 5 | namespace Microsoft.AspNetCore.Mvc; 6 | 7 | /// 8 | /// API响应 9 | /// 10 | [Serializable] 11 | public class ApiResponse(int code) : ApiResponse(code) 12 | { 13 | 14 | #region Public 方法 15 | 16 | /// 17 | /// 创建一个响应 18 | /// 19 | /// 20 | /// 21 | /// 22 | public static ApiResponse Create(int code) => new() { Code = code }; 23 | 24 | /// 25 | /// 创建一个响应 26 | /// 27 | /// 28 | /// 29 | /// 30 | public static ApiResponse Create(TData? data) => new() { Data = data }; 31 | 32 | /// 33 | /// 创建一个响应 34 | /// 35 | /// 36 | /// 37 | /// 38 | /// 39 | public static ApiResponse Create(int code, TData? data) => new() { Code = code, Data = data }; 40 | 41 | /// 42 | /// 创建一个响应 43 | /// 44 | /// 45 | /// 46 | /// 47 | /// 48 | public static ApiResponse Create(int code, string message) => new() { Code = code, Message = message }; 49 | 50 | /// 51 | /// 创建一个响应 52 | /// 53 | /// 54 | /// 55 | /// 56 | /// 57 | /// 58 | public static ApiResponse Create(int code, TData? data, string? message) => new() { Code = code, Data = data, Message = message }; 59 | 60 | #endregion Public 方法 61 | } 62 | 63 | /// 64 | /// 有数据的API响应 65 | /// 66 | /// 67 | [Serializable] 68 | public class ApiResponse(int code) : GenericApiResponse(code) 69 | { 70 | #region Public 构造函数 71 | 72 | /// 73 | public ApiResponse() : this(StatusCodes.Status200OK) 74 | { 75 | } 76 | 77 | #endregion Public 构造函数 78 | } 79 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseAutoWrapper/Microsoft.AspNetCore/Mvc/ApplicationModels/ActionResultPolicyTagAppModelConvention.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Reflection; 4 | 5 | using Cuture.AspNetCore.ResponseAutoWrapper; 6 | 7 | using Microsoft.AspNetCore.Mvc.Filters; 8 | 9 | namespace Microsoft.AspNetCore.Mvc.ApplicationModels; 10 | 11 | /// 12 | /// 用于标记ActionResult处理策略的ApplicationModelConvention 13 | /// 14 | internal class ActionResultPolicyTagAppModelConvention : IApplicationModelConvention 15 | { 16 | #region Private 字段 17 | 18 | private readonly Func _actionNoWrapPredicate; 19 | 20 | #endregion Private 字段 21 | 22 | #region Protected 属性 23 | 24 | protected IWrapTypeCreator WrapTypeCreator { get; } 25 | 26 | #endregion Protected 属性 27 | 28 | #region Public 构造函数 29 | 30 | public ActionResultPolicyTagAppModelConvention(IWrapTypeCreator wrapTypeCreator, Func actionNoWrapPredicate) 31 | { 32 | ArgumentNullException.ThrowIfNull(wrapTypeCreator); 33 | ArgumentNullException.ThrowIfNull(actionNoWrapPredicate); 34 | 35 | WrapTypeCreator = wrapTypeCreator; 36 | _actionNoWrapPredicate = actionNoWrapPredicate; 37 | } 38 | 39 | #endregion Public 构造函数 40 | 41 | #region Public 方法 42 | 43 | public void Apply(ApplicationModel application) 44 | { 45 | var actions = application.Controllers.SelectMany(m => m.Actions); 46 | foreach (var action in actions) 47 | { 48 | ProcessActionModel(action); 49 | } 50 | } 51 | 52 | #endregion Public 方法 53 | 54 | #region Protected 方法 55 | 56 | /// 57 | /// 设置Action的Result处理策略 58 | /// 59 | /// 60 | /// 61 | protected static void SetActionResultPolicy(ActionModel action, ActionResultPolicy policy) 62 | { 63 | action.Properties[Constants.ActionPropertiesResultPolicyKey] = policy; 64 | } 65 | 66 | /// 67 | /// 在Action需要包装时触发的函数 68 | /// 69 | /// Action 70 | /// 实际返回数据类型 71 | protected virtual void OnActionShouldWrap(ActionModel action, Type returnType) 72 | { 73 | } 74 | 75 | #endregion Protected 方法 76 | 77 | #region Private 方法 78 | 79 | private void ProcessActionModel(ActionModel action) 80 | { 81 | var returnType = action.ActionMethod.ReturnType.UnwrapTaskResult(); 82 | 83 | if (_actionNoWrapPredicate(action.ActionMethod) 84 | || _actionNoWrapPredicate(action.Controller.ControllerType) 85 | || !WrapTypeCreator.ShouldWrap(returnType)) 86 | { 87 | SetActionResultPolicy(action, ActionResultPolicy.Skip); 88 | return; 89 | } 90 | 91 | SetActionResultPolicy(action, returnType == typeof(object) ? ActionResultPolicy.Unknown : ActionResultPolicy.Process); 92 | 93 | OnActionShouldWrap(action, returnType); 94 | } 95 | 96 | #endregion Private 方法 97 | } 98 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseAutoWrapper/Microsoft.AspNetCore/Mvc/ApplicationModels/OpenAPISupportAppModelConvention.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reflection; 3 | 4 | using Cuture.AspNetCore.ResponseAutoWrapper; 5 | 6 | using Microsoft.AspNetCore.Http; 7 | 8 | namespace Microsoft.AspNetCore.Mvc.ApplicationModels; 9 | 10 | internal class OpenAPISupportAppModelConvention(IWrapTypeCreator wrapTypeCreator, Func actionNoWrapPredicate) 11 | : ActionResultPolicyTagAppModelConvention(wrapTypeCreator, actionNoWrapPredicate) 12 | { 13 | 14 | #region Protected 方法 15 | 16 | /// 17 | protected override void OnActionShouldWrap(ActionModel action, Type returnType) 18 | { 19 | base.OnActionShouldWrap(action, returnType); 20 | 21 | var redirectReturnType = returnType == typeof(void) 22 | ? WrapTypeCreator.ResponseType 23 | : WrapTypeCreator.MakeWrapType(returnType); 24 | 25 | action.Filters.Add(new ProducesResponseTypeAttribute(redirectReturnType, StatusCodes.Status200OK)); 26 | } 27 | 28 | #endregion Protected 方法 29 | } 30 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseAutoWrapper/Microsoft.AspNetCore/Mvc/EmptyApiResponse.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Http; 2 | 3 | namespace Microsoft.AspNetCore.Mvc; 4 | 5 | /// 6 | /// 空API响应 7 | /// 8 | public class EmptyApiResponse(int code) : ApiResponse(code) 9 | { 10 | 11 | #region Public 方法 12 | 13 | /// 14 | /// 创建一个响应 15 | /// 16 | /// 17 | /// 18 | public static EmptyApiResponse Create(int code) => new(code); 19 | 20 | /// 21 | /// 创建一个响应 22 | /// 23 | /// 24 | /// 25 | /// 26 | public static EmptyApiResponse Create(int code, string message) => new(code) { Message = message }; 27 | 28 | /// 29 | /// 创建一个响应 30 | /// 31 | /// 32 | /// 33 | public static EmptyApiResponse Create(string message) => new(StatusCodes.Status200OK) { Message = message }; 34 | 35 | #endregion Public 方法 36 | } 37 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseAutoWrapper/Microsoft.AspNetCore/Mvc/Filters/ResponseAutoWrapResultFilter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.Runtime.CompilerServices; 4 | using System.Threading.Tasks; 5 | 6 | using Cuture.AspNetCore.ResponseAutoWrapper; 7 | 8 | using Microsoft.AspNetCore.Http; 9 | 10 | namespace Microsoft.AspNetCore.Mvc.Filters; 11 | 12 | internal class ResponseAutoWrapResultFilter : IAsyncAlwaysRunResultFilter 13 | where TResponse : class 14 | { 15 | #region Private 字段 16 | 17 | private readonly IActionResultWrapper _actionResultWrapper; 18 | 19 | #endregion Private 字段 20 | 21 | #region Public 构造函数 22 | 23 | /// 24 | public ResponseAutoWrapResultFilter(IActionResultWrapper actionResultWrapper) 25 | { 26 | ArgumentNullException.ThrowIfNull(actionResultWrapper); 27 | 28 | _actionResultWrapper = actionResultWrapper; 29 | } 30 | 31 | #endregion Public 构造函数 32 | 33 | #region Public 方法 34 | 35 | /// 36 | public Task OnResultExecutionAsync(ResultExecutingContext context, ResultExecutionDelegate next) 37 | { 38 | if (GetActionResultPolicy(context) == ActionResultPolicy.Skip) 39 | { 40 | context.HttpContext.DoNotWrapResponse(); 41 | return next(); 42 | } 43 | 44 | if (_actionResultWrapper.Wrap(context) is TResponse response) 45 | { 46 | context.Result = new OkObjectResult(response); 47 | } 48 | 49 | return next(); 50 | } 51 | 52 | #endregion Public 方法 53 | 54 | #region Private 方法 55 | 56 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 57 | private static ActionResultPolicy GetActionResultPolicy(ResultExecutingContext context) 58 | { 59 | //HACK 不加锁读取 ActionDescriptor.Properties ,可能存在某些问题 60 | if (context.ActionDescriptor.Properties.TryGetValue(Constants.ActionPropertiesResultPolicyKey, out var cachedPolicy)) 61 | { 62 | return (ActionResultPolicy)cachedPolicy!; 63 | } 64 | 65 | //HACK 理论上不应该执行到这里 66 | Debug.WriteLine("Warning!!! Not found Constants.ActionPropertiesResultPolicyKey in ActionDescriptor.Properties"); 67 | 68 | return ActionResultPolicy.Unknown; 69 | } 70 | 71 | #endregion Private 方法 72 | } 73 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseAutoWrapper/Microsoft.AspNetCore/Mvc/GenericApiResponse.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.CompilerServices; 3 | 4 | using Cuture.AspNetCore.ResponseAutoWrapper; 5 | 6 | namespace Microsoft.AspNetCore.Mvc; 7 | 8 | /// 9 | /// 通用的API响应类型 10 | /// 11 | [Serializable] 12 | public abstract class GenericApiResponse 13 | { 14 | #region Public 方法 15 | 16 | /// 17 | /// 创建一个响应 18 | /// 19 | /// 20 | /// 21 | /// 22 | /// 23 | /// 24 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 25 | public static GenericApiResponse Create(TCode code) => new(code); 26 | 27 | /// 28 | /// 创建一个响应 29 | /// 30 | /// 31 | /// 32 | /// 33 | /// 34 | /// 35 | /// 36 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 37 | public static GenericApiResponse Create(TCode code, TMessage? message) => new(code) { Message = message }; 38 | 39 | /// 40 | /// 创建一个响应 41 | /// 42 | /// 43 | /// 44 | /// 45 | /// 46 | /// 47 | /// 48 | /// 49 | [MethodImpl(MethodImplOptions.AggressiveInlining)] 50 | public static GenericApiResponse Create(TCode code, TMessage? message, TData? data) => new(code) { Message = message, Data = data }; 51 | 52 | #endregion Public 方法 53 | } 54 | 55 | /// 56 | /// 通用的API响应类型 57 | /// 58 | /// 指定Code类型 59 | [Serializable] 60 | public abstract class GenericApiResponse(TCode code) : GenericApiResponse 61 | { 62 | #region Public 属性 63 | 64 | /// 65 | /// 状态码 66 | /// 67 | public TCode Code { get; set; } = code; 68 | 69 | #endregion Public 属性 70 | } 71 | 72 | /// 73 | /// 通用的API响应类型 74 | /// 75 | /// 指定Code类型 76 | /// 指定Message类型 77 | [Serializable] 78 | public abstract class GenericApiResponse(TCode code) : GenericApiResponse(code) 79 | { 80 | #region Public 属性 81 | 82 | /// 83 | /// 消息 84 | /// 85 | public TMessage? Message { get; set; } 86 | 87 | #endregion Public 属性 88 | } 89 | 90 | /// 91 | /// 有数据的通用的API响应类型 92 | /// 93 | /// 94 | /// 95 | /// 96 | [Serializable] 97 | public class GenericApiResponse(TCode code) : GenericApiResponse(code) 98 | { 99 | #region Public 属性 100 | 101 | /// 102 | /// 数据 103 | /// 104 | public TData? Data { get; set; } 105 | 106 | #endregion Public 属性 107 | } 108 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseAutoWrapper/Microsoft.AspNetCore/Mvc/NoResponseWrapAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Microsoft.AspNetCore.Mvc; 4 | 5 | /// 6 | /// 不进行响应包装 7 | /// 8 | [AttributeUsage(AttributeTargets.Class | AttributeTargets.Method, AllowMultiple = false, Inherited = true)] 9 | public class NoResponseWrapAttribute : Attribute 10 | { 11 | } 12 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseAutoWrapper/ResponseAutoWrapMiddlewareOptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using Microsoft.AspNetCore.Mvc.Formatters; 4 | 5 | namespace Cuture.AspNetCore.ResponseAutoWrapper; 6 | 7 | /// 8 | /// 自动响应包装中间件选项 9 | /// 10 | public class ResponseAutoWrapMiddlewareOptions 11 | { 12 | #region Public 属性 13 | 14 | /// 15 | /// 是否捕获异常 16 | /// default is 17 | /// 18 | public bool CatchExceptions { get; set; } = true; 19 | 20 | /// 21 | /// 默认输出格式化器选择委托 22 | /// 选择在请求中无 Accept 时,用于格式化响应的 23 | /// 默认时会选择 ,不存在则会抛出异常 24 | /// 25 | public Func, IOutputFormatter> DefaultOutputFormatterSelector { get; set; } 26 | = static formatters => formatters.FirstOrDefault(m => m.GetType() == typeof(SystemTextJsonOutputFormatter)) 27 | ?? throw new InvalidOperationException($"Can not found {nameof(SystemTextJsonOutputFormatter)} by default. Must select a formatter manually by \"{nameof(ResponseAutoWrapMiddlewareOptions)}.{nameof(DefaultOutputFormatterSelector)}\" at middleware setup."); 28 | 29 | /// 30 | /// 忽略 OPTIONS 请求 31 | /// 32 | public bool IgnoreOptionsRequest { get; set; } = true; 33 | 34 | /// 35 | /// 是否将捕获到的异常抛出给上层中间件 36 | /// default is 37 | /// 38 | public bool ThrowCaughtExceptions { get; set; } = false; 39 | 40 | #endregion Public 属性 41 | } 42 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseAutoWrapper/ResponseAutoWrapperOptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Reflection; 3 | using Microsoft.AspNetCore.Authorization; 4 | using Microsoft.AspNetCore.Http; 5 | using Microsoft.AspNetCore.Mvc; 6 | 7 | namespace Cuture.AspNetCore.ResponseAutoWrapper; 8 | 9 | /// 10 | /// 11 | /// 12 | public class ResponseAutoWrapperOptions 13 | { 14 | #region Private 字段 15 | 16 | /// 17 | private Func? _actionNoWrapPredicate; 18 | 19 | #endregion Private 字段 20 | 21 | #region Public 属性 22 | 23 | /// 24 | /// Action 是否需要包装的筛选委托 25 | /// 委托返回 true 时,表明此方法应该被跳过,不进行包装 26 | /// 27 | public Func ActionNoWrapPredicate { get => _actionNoWrapPredicate ?? DefaultActionNoWrapCheck; set => _actionNoWrapPredicate = value; } 28 | 29 | /// 30 | /// 禁用 OpenAPI 支持 31 | /// 未禁用时,将限制 响应类型 必须为泛型,且具有唯一的泛型参数 。即 - TResponse<> 32 | /// 33 | /// default value is 'false' 34 | public bool DisableOpenAPISupport { get; set; } = false; 35 | 36 | /// 37 | /// 处理授权失败响应 38 | /// 注册 39 | /// 将授权结果行为替换为响应状态码 40 | /// 41 | /// default value is 'false' 42 | public bool HandleAuthorizationResult { get; set; } = false; 43 | 44 | /// 45 | /// 处理无效模型绑定状态 46 | /// 设置 为使用 处理的委托 47 | /// 可以通过注入自定义的 替换默认行为 48 | /// 49 | /// default value is 'true' 50 | public bool HandleInvalidModelState { get; set; } = true; 51 | 52 | /// 53 | /// 重写状态码 54 | /// 如果此项的值不为空,则当响应状态码不为 时,使用该值进行重新设置状态码 55 | /// 默认为 56 | /// 57 | public int? RewriteStatusCode { get; set; } = StatusCodes.Status200OK; 58 | 59 | #endregion Public 属性 60 | 61 | #region Public 方法 62 | 63 | /// 64 | /// 默认的 Action 是否需要包装的筛选委托 65 | /// 66 | /// 67 | /// 68 | public static bool DefaultActionNoWrapCheck(MemberInfo memberInfo) 69 | => Attribute.GetCustomAttribute(memberInfo, typeof(NoResponseWrapAttribute)) is not null; 70 | 71 | #endregion Public 方法 72 | } 73 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseAutoWrapper/ResponseAutoWrapperWorkDelegateCollection.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | using Microsoft.AspNetCore.Http; 4 | 5 | namespace Cuture.AspNetCore.ResponseAutoWrapper; 6 | 7 | /// 8 | /// 响应自动包装工作委托集合 9 | /// 10 | public class ResponseAutoWrapperWorkDelegateCollection 11 | { 12 | #region Public 属性 13 | 14 | /// 15 | /// 异常包装委托 16 | /// 17 | public Func ExceptionWrapDelegate { get; } 18 | 19 | /// 20 | /// 非200状态码包装委托 21 | /// 22 | public Func NotOKStatusCodeWrapDelegate { get; } 23 | 24 | #endregion Public 属性 25 | 26 | #region Public 构造函数 27 | 28 | /// 29 | /// 30 | /// 31 | /// 异常包装委托 32 | /// 非200状态码包装委托 33 | public ResponseAutoWrapperWorkDelegateCollection(Func exceptionWrapDelegate, 34 | Func notOKStatusCodeWrapDelegate) 35 | { 36 | ArgumentNullException.ThrowIfNull(exceptionWrapDelegate); 37 | ArgumentNullException.ThrowIfNull(notOKStatusCodeWrapDelegate); 38 | 39 | ExceptionWrapDelegate = exceptionWrapDelegate; 40 | NotOKStatusCodeWrapDelegate = notOKStatusCodeWrapDelegate; 41 | } 42 | 43 | #endregion Public 构造函数 44 | } 45 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseAutoWrapper/ResponseDescription.cs: -------------------------------------------------------------------------------- 1 | namespace Cuture.AspNetCore.ResponseAutoWrapper; 2 | 3 | /// 4 | /// 响应描述 5 | /// 6 | public class ResponseDescription 7 | { 8 | #region Public 属性 9 | 10 | /// 11 | /// Code 12 | /// 13 | public TCode Code { get; } 14 | 15 | /// 16 | /// 消息 17 | /// 18 | public TMessage? Message { get; } 19 | 20 | #endregion Public 属性 21 | 22 | #region Public 构造函数 23 | 24 | /// 25 | public ResponseDescription(TCode code) 26 | { 27 | Code = code; 28 | } 29 | 30 | /// 31 | /// 32 | /// 33 | /// Code 34 | /// 消息 35 | public ResponseDescription(TCode code, TMessage? message) 36 | { 37 | Code = code; 38 | Message = message; 39 | } 40 | 41 | #endregion Public 构造函数 42 | 43 | #region Public 方法 44 | 45 | /// 46 | public override string ToString() => $"Code: {Code} , Message: {Message}"; 47 | 48 | #endregion Public 方法 49 | } 50 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseAutoWrapper/Wrappers/AbstractResponseWrapper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using Microsoft.AspNetCore.Http; 4 | using Microsoft.AspNetCore.Mvc; 5 | using Microsoft.AspNetCore.Mvc.Filters; 6 | using Microsoft.Extensions.Options; 7 | 8 | namespace Cuture.AspNetCore.ResponseAutoWrapper; 9 | 10 | /// 11 | /// 抽象响应包装器 12 | /// 13 | /// 统一响应类型 14 | /// Code类型 15 | /// Message类型 16 | public abstract class AbstractResponseWrapper 17 | : IActionResultWrapper 18 | , INotOKStatusCodeWrapper 19 | , IExceptionWrapper 20 | , IInvalidModelStateWrapper 21 | where TResponse : class 22 | { 23 | #region Private 字段 24 | 25 | /// 26 | protected readonly int? RewriteStatusCode; 27 | 28 | /// 29 | protected readonly IWrapTypeCreator WrapTypeCreator; 30 | 31 | #endregion Private 字段 32 | 33 | #region Public 构造函数 34 | 35 | /// 36 | public AbstractResponseWrapper(IWrapTypeCreator wrapTypeCreator, IOptions optionsAccessor) 37 | { 38 | ArgumentNullException.ThrowIfNull(wrapTypeCreator); 39 | 40 | WrapTypeCreator = wrapTypeCreator; 41 | RewriteStatusCode = optionsAccessor?.Value?.RewriteStatusCode; 42 | } 43 | 44 | #endregion Public 构造函数 45 | 46 | #region Wrap 47 | 48 | #region ActionResultWrap 49 | 50 | /// 51 | /// 52 | /// 53 | /// 派生于 54 | /// 仅当 返回值类型为 时执行 55 | /// 56 | /// 57 | /// 58 | /// 当前请求的响应描述 59 | /// 60 | protected abstract TResponse? ActionEmptyResultWrap(ResultExecutingContext context, EmptyResult emptyResult, ResponseDescription? description); 61 | 62 | /// 63 | /// 64 | /// 65 | /// 派生于 66 | /// 仅当 返回值类型为 时执行 67 | /// 68 | /// 69 | /// 70 | /// 当前请求的响应描述 71 | /// 72 | protected abstract TResponse? ActionObjectResultWrap(ResultExecutingContext context, ObjectResult objectResult, ResponseDescription? description); 73 | 74 | #endregion ActionResultWrap 75 | 76 | /// 77 | /// 78 | /// 79 | /// 等价于 80 | /// 81 | /// 82 | /// 83 | /// 84 | public abstract TResponse? ExceptionWrap(HttpContext context, Exception exception); 85 | 86 | /// 87 | /// 88 | /// 89 | /// 等价于 90 | /// 91 | /// 92 | /// 93 | public abstract TResponse? InvalidModelStateWrap(ActionContext context); 94 | 95 | /// 96 | /// 97 | /// 98 | /// 等价于 99 | /// 100 | /// 101 | /// 102 | public abstract TResponse? NotOKStatusCodeWrap(HttpContext context); 103 | 104 | #endregion Wrap 105 | 106 | #region Protected 方法 107 | 108 | /// 109 | /// 110 | /// 111 | /// 等价于 112 | /// 113 | /// 114 | /// 115 | protected virtual TResponse? ActionResultWrap(ResultExecutingContext context) 116 | { 117 | return context.Result switch 118 | { 119 | ObjectResult objectResult => InternalActionObjectResultWrap(context, objectResult), 120 | EmptyResult emptyResult => ActionEmptyResultWrap(context, emptyResult, context.GetResponseDescription()), 121 | _ => null, 122 | }; 123 | } 124 | 125 | #endregion Protected 方法 126 | 127 | #region Private 方法 128 | 129 | private TResponse? InternalActionObjectResultWrap(ResultExecutingContext context, ObjectResult objectResult) 130 | { 131 | if (objectResult.Value is not TResponse 132 | && (objectResult.Value is null 133 | || WrapTypeCreator.ShouldWrap(objectResult.Value.GetType()))) 134 | { 135 | return ActionObjectResultWrap(context, objectResult, context.GetResponseDescription()); 136 | } 137 | return null; 138 | } 139 | 140 | #endregion Private 方法 141 | 142 | #region InterfaceImpl 143 | 144 | TResponse? IInvalidModelStateWrapper.Wrap(ActionContext context) 145 | { 146 | if (ShouldSkipWrap(context.HttpContext)) 147 | { 148 | return null; 149 | } 150 | return AfterWrap(context.HttpContext, InvalidModelStateWrap(context)); 151 | } 152 | 153 | TResponse? IActionResultWrapper.Wrap(ResultExecutingContext context) 154 | { 155 | if (ShouldSkipWrap(context.HttpContext)) 156 | { 157 | return null; 158 | } 159 | return AfterWrap(context.HttpContext, ActionResultWrap(context)); 160 | } 161 | 162 | TResponse? INotOKStatusCodeWrapper.Wrap(HttpContext context) 163 | { 164 | if (ShouldSkipWrap(context)) 165 | { 166 | return null; 167 | } 168 | return AfterWrap(context, NotOKStatusCodeWrap(context)); 169 | } 170 | 171 | TResponse? IExceptionWrapper.Wrap(HttpContext context, Exception exception) 172 | { 173 | if (ShouldSkipWrap(context)) 174 | { 175 | return null; 176 | } 177 | 178 | Activity.Current.RecordException(exception: exception, tags: default, timestamp: DateTimeOffset.UtcNow); 179 | 180 | return AfterWrap(context, ExceptionWrap(context, exception)); 181 | } 182 | 183 | #endregion InterfaceImpl 184 | 185 | #region Protected 方法 186 | 187 | /// 188 | /// 在包装后执行的代码 189 | /// 190 | /// Http上下文 191 | /// 包装后的响应值 192 | protected virtual TResponse? AfterWrap(HttpContext httpContext, TResponse? wrappedResponse) 193 | { 194 | if (wrappedResponse is not null 195 | && RewriteStatusCode.HasValue) 196 | { 197 | httpContext.Response.StatusCode = RewriteStatusCode.Value; 198 | } 199 | return wrappedResponse; 200 | } 201 | 202 | /// 203 | /// 检查是否应该跳过包装 204 | /// 205 | /// Http上下文 206 | /// 是否应该跳过包装 207 | protected virtual bool ShouldSkipWrap(HttpContext httpContext) => httpContext.IsSetDoNotWrapResponse(); 208 | 209 | #endregion Protected 方法 210 | } 211 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseAutoWrapper/Wrappers/DefaultResponseWrapper.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | using Microsoft.Extensions.Options; 3 | 4 | namespace Cuture.AspNetCore.ResponseAutoWrapper; 5 | 6 | /// 7 | /// 默认的响应包装器 8 | /// TCode 为 9 | /// TMessage 为 10 | /// 11 | /// 12 | internal sealed class DefaultResponseWrapper(IWrapTypeCreator wrapTypeCreator, 13 | IOptions optionsAccessor, 14 | IOptions wrapperOptionsAccessor) 15 | : LegacyCompatibleResponseWrapper>(wrapTypeCreator, optionsAccessor, wrapperOptionsAccessor) 16 | { 17 | 18 | #region CreateResponse 19 | 20 | /// 21 | protected override GenericApiResponse? CreateResponse(int code) => new(code); 22 | 23 | /// 24 | protected override GenericApiResponse? CreateResponse(int code, string? message) => new(code) { Message = message }; 25 | 26 | /// 27 | protected override GenericApiResponse? CreateResponse(int code, string? message, object? data) => new(code) { Data = data, Message = message }; 28 | 29 | #endregion CreateResponse 30 | } 31 | -------------------------------------------------------------------------------- /src/Cuture.AspNetCore.ResponseAutoWrapper/Wrappers/LegacyCompatibleResponseWrapper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Net; 4 | 5 | using Microsoft.AspNetCore.Http; 6 | using Microsoft.AspNetCore.Mvc; 7 | using Microsoft.AspNetCore.Mvc.Filters; 8 | using Microsoft.Extensions.Options; 9 | 10 | namespace Cuture.AspNetCore.ResponseAutoWrapper; 11 | 12 | using TCode = Int32; 13 | using TMessage = String; 14 | 15 | /// 16 | /// 兼容旧响应包装逻辑的包装器(TCode 为 ,TMessage 为 的响应类型) 17 | /// 18 | public abstract class LegacyCompatibleResponseWrapper : AbstractResponseWrapper 19 | where TResponse : class 20 | { 21 | #region Private 字段 22 | 23 | private readonly LegacyCompatibleResponseWrapperOptions _options; 24 | 25 | #endregion Private 字段 26 | 27 | #region Public 构造函数 28 | 29 | /// 30 | public LegacyCompatibleResponseWrapper(IWrapTypeCreator wrapTypeCreator, 31 | IOptions optionsAccessor, 32 | IOptions wrapperOptionsAccessor) 33 | : base(wrapTypeCreator, optionsAccessor) 34 | { 35 | ArgumentNullException.ThrowIfNull(wrapperOptionsAccessor?.Value); 36 | 37 | _options = wrapperOptionsAccessor.Value; 38 | } 39 | 40 | #endregion Public 构造函数 41 | 42 | #region Wrap 43 | 44 | #region ActionResultWrap 45 | 46 | /// 47 | protected override TResponse? ActionEmptyResultWrap(ResultExecutingContext context, EmptyResult emptyResult, ResponseDescription? description) 48 | { 49 | return CreateResponse(description?.Code ?? _options.SuccessCode, description?.Message ?? _options.ActionResultWrapMessage); 50 | } 51 | 52 | /// 53 | protected override TResponse? ActionObjectResultWrap(ResultExecutingContext context, ObjectResult objectResult, ResponseDescription? description) 54 | { 55 | return CreateResponse(description?.Code ?? _options.SuccessCode, description?.Message ?? _options.ActionResultWrapMessage, objectResult.Value); 56 | } 57 | 58 | #endregion ActionResultWrap 59 | 60 | /// 61 | public override TResponse? ExceptionWrap(HttpContext context, Exception exception) 62 | { 63 | return CreateResponse(_options.ExceptionCode, _options.ExceptionWrapMessage); 64 | } 65 | 66 | /// 67 | public override TResponse? InvalidModelStateWrap(ActionContext context) 68 | { 69 | var errorMessages = context.ModelState.Where(m => m.Value?.Errors.Count > 0) 70 | .Select(m => $"{m.Key} - {m.Value?.Errors.FirstOrDefault()?.ErrorMessage}"); 71 | 72 | var message = string.Join(Environment.NewLine, errorMessages); 73 | 74 | return CreateResponse(_options.InvalidModelStateCode, message); 75 | } 76 | 77 | /// 78 | public override TResponse? NotOKStatusCodeWrap(HttpContext context) 79 | { 80 | var statusCode = context.Response.StatusCode; 81 | if (statusCode is >= 300 and < 400) 82 | { 83 | return null; 84 | } 85 | 86 | string? message; 87 | 88 | if (context.TryGetResponseDescription(out var description)) 89 | { 90 | statusCode = description.Code; 91 | message = description.Message; 92 | } 93 | else 94 | { 95 | message = Enum.IsDefined(typeof(HttpStatusCode), statusCode) ? ((HttpStatusCode)statusCode).ToString() : null; 96 | } 97 | 98 | return CreateResponse(statusCode, message); 99 | } 100 | 101 | #endregion Wrap 102 | 103 | #region CreateResponse 104 | 105 | /// 106 | /// 创建响应 107 | /// 108 | /// 109 | /// 110 | protected abstract TResponse? CreateResponse(TCode code); 111 | 112 | /// 113 | /// 创建响应 114 | /// 115 | /// 116 | /// 117 | /// 118 | protected abstract TResponse? CreateResponse(TCode code, TMessage? message); 119 | 120 | /// 121 | /// 创建响应 122 | /// 123 | /// 124 | /// 125 | /// 126 | /// 127 | protected abstract TResponse? CreateResponse(TCode code, TMessage? message, object? data); 128 | 129 | #endregion CreateResponse 130 | } 131 | -------------------------------------------------------------------------------- /test/ResponseAutoWrapper.BenchmarkHost/Controllers/WeatherForecastController.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | using Microsoft.AspNetCore.Mvc; 4 | 5 | using ResponseAutoWrapper.TestHost; 6 | 7 | namespace ResponseAutoWrapper.BenchmarkHost.Controllers; 8 | 9 | [ApiController] 10 | [Route("[controller]")] 11 | public class WeatherForecastController : ControllerBase 12 | { 13 | #region Public 方法 14 | 15 | [HttpGet] 16 | public IEnumerable Get(int count = 5) 17 | { 18 | return WeatherForecast.GenerateData(count); 19 | } 20 | 21 | #endregion Public 方法 22 | } 23 | -------------------------------------------------------------------------------- /test/ResponseAutoWrapper.BenchmarkHost/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Reflection; 4 | 5 | using Microsoft.AspNetCore.Hosting; 6 | using Microsoft.Extensions.Hosting; 7 | 8 | namespace ResponseAutoWrapper.BenchmarkHost; 9 | 10 | public class Program 11 | { 12 | #region Public 方法 13 | 14 | public static void Main(string[] args) 15 | { 16 | var startupName = args.Length > 0 17 | ? args[0].Trim() 18 | : "DefaultStartup"; 19 | 20 | var startupType = Assembly.GetExecutingAssembly() 21 | .GetTypes() 22 | .Where(m => m.Name == startupName) 23 | .FirstOrDefault()!; 24 | 25 | Console.WriteLine($"Running with - {startupType.FullName}"); 26 | 27 | var runMethod = typeof(Program).GetMethod("RunWithStartup", BindingFlags.Static | BindingFlags.NonPublic)!; 28 | 29 | runMethod.MakeGenericMethod(startupType) 30 | .Invoke(null, new object[] { args }); 31 | } 32 | 33 | #endregion Public 方法 34 | 35 | #region Private 方法 36 | 37 | private static void RunWithStartup(string[] args) where TStartup : BaseStartup 38 | { 39 | Host.CreateDefaultBuilder(args) 40 | .ConfigureWebHostDefaults(webBuilder => 41 | { 42 | webBuilder.UseStartup(); 43 | }) 44 | .Build() 45 | .Run(); 46 | } 47 | 48 | #endregion Private 方法 49 | } 50 | -------------------------------------------------------------------------------- /test/ResponseAutoWrapper.BenchmarkHost/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "http://json.schemastore.org/launchsettings.json", 3 | "profiles": { 4 | "BenchmarkHost - NoWrapper": { 5 | "commandName": "Project", 6 | "commandLineArgs": "NoWrapperStartup", 7 | "environmentVariables": {}, 8 | "applicationUrl": "http://0.0.0.0:5000" 9 | }, 10 | "BenchmarkHost - AutoWrapperCore": { 11 | "commandName": "Project", 12 | "commandLineArgs": "AutoWrapperCoreStartup", 13 | "environmentVariables": {}, 14 | "applicationUrl": "http://0.0.0.0:5000" 15 | }, 16 | "BenchmarkHost - ResponseAutoWrapper": { 17 | "commandName": "Project", 18 | "commandLineArgs": "ResponseAutoWrapperStartup", 19 | "environmentVariables": {}, 20 | "applicationUrl": "http://0.0.0.0:5000" 21 | }, 22 | "WSL": { 23 | "commandName": "WSL2", 24 | "environmentVariables": { 25 | "ASPNETCORE_URLS": "http://0.0.0.0:5000" 26 | }, 27 | "distributionName": "" 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /test/ResponseAutoWrapper.BenchmarkHost/ResponseAutoWrapper.BenchmarkHost.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net8.0 5 | net8.0 6 | 7 | false 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /test/ResponseAutoWrapper.BenchmarkHost/Startups.cs: -------------------------------------------------------------------------------- 1 | using AutoWrapper; 2 | 3 | using Microsoft.AspNetCore.Builder; 4 | using Microsoft.Extensions.DependencyInjection; 5 | 6 | namespace ResponseAutoWrapper.BenchmarkHost; 7 | 8 | public class AutoWrapperCoreStartup : BaseStartup 9 | { 10 | #region Public 方法 11 | 12 | public override void Configure(IApplicationBuilder app) 13 | { 14 | app.UseAutoWrapper(); 15 | base.Configure(app); 16 | } 17 | 18 | #endregion Public 方法 19 | } 20 | 21 | public abstract class BaseStartup 22 | { 23 | #region Public 方法 24 | 25 | public virtual void Configure(IApplicationBuilder app) 26 | { 27 | app.UseRouting(); 28 | 29 | app.UseEndpoints(endpoints => 30 | { 31 | endpoints.MapControllers(); 32 | }); 33 | } 34 | 35 | public virtual void ConfigureServices(IServiceCollection services) 36 | { 37 | services.AddControllers(); 38 | } 39 | 40 | #endregion Public 方法 41 | } 42 | 43 | public class NoWrapperStartup : BaseStartup 44 | { 45 | } 46 | 47 | public class ResponseAutoWrapperStartup : BaseStartup 48 | { 49 | #region Public 方法 50 | 51 | public override void Configure(IApplicationBuilder app) 52 | { 53 | app.UseResponseAutoWrapper(); 54 | 55 | base.Configure(app); 56 | } 57 | 58 | public override void ConfigureServices(IServiceCollection services) 59 | { 60 | base.ConfigureServices(services); 61 | 62 | services.AddResponseAutoWrapper(); 63 | } 64 | 65 | #endregion Public 方法 66 | } 67 | -------------------------------------------------------------------------------- /test/ResponseAutoWrapper.BenchmarkHost/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Warning", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | }, 9 | "AllowedHosts": "*" 10 | } 11 | -------------------------------------------------------------------------------- /test/ResponseAutoWrapper.Test/GenericApiTest.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.Net.Http.Json; 4 | using System.Threading.Tasks; 5 | 6 | using Cuture.AspNetCore.ResponseAutoWrapper; 7 | using Cuture.Http; 8 | 9 | using Microsoft.AspNetCore.Http; 10 | using Microsoft.AspNetCore.Mvc; 11 | using Microsoft.Extensions.DependencyInjection; 12 | using Microsoft.Extensions.Hosting; 13 | using Microsoft.VisualStudio.TestTools.UnitTesting; 14 | 15 | using ResponseAutoWrapper.TestHost; 16 | 17 | namespace ResponseAutoWrapper.Test; 18 | 19 | [TestClass] 20 | public class GenericApiTest : TestServerBase 21 | { 22 | #region Public 方法 23 | 24 | [TestMethod] 25 | [DataRow("/api/WeatherForecast/get?type=1")] 26 | [DataRow("/api/WeatherForecast/get?type=2")] 27 | [DataRow("/api/WeatherForecast/get-inheritedtask?type=1")] 28 | [DataRow("/api/WeatherForecast/get-inheritedtask?type=2")] 29 | [DataRow("/api/WeatherForecast/get-inheritedtask2?type=1")] 30 | [DataRow("/api/WeatherForecast/get-inheritedtask2?type=2")] 31 | [DataRow("/api/WeatherForecast/get-task?type=1")] 32 | [DataRow("/api/WeatherForecast/get-task?type=2")] 33 | [DataRow("/api/WeatherForecast/get-valuetask?type=1")] 34 | [DataRow("/api/WeatherForecast/get-valuetask?type=2")] 35 | public async Task Should_CustomDescription(string requestPath) 36 | { 37 | var response = await Client.GetFromJsonAsync>(requestPath); 38 | 39 | CheckResponseCode(response, 10086); 40 | 41 | Assert.AreEqual("Hello world!", response.Message); 42 | 43 | Debug.WriteLine(response.Message); 44 | } 45 | 46 | [TestMethod] 47 | [DataRow("/api/WeatherForecast/get")] 48 | [DataRow("/api/WeatherForecast/get-inheritedtask")] 49 | [DataRow("/api/WeatherForecast/get-inheritedtask2")] 50 | [DataRow("/api/WeatherForecast/get-direct-api-response")] 51 | [DataRow("/api/WeatherForecast/get-direct-inherited-api-response")] 52 | [DataRow("/api/WeatherForecast/get-dynamic?resultType=0")] 53 | [DataRow("/api/WeatherForecast/get-dynamic?resultType=1")] 54 | [DataRow("/api/WeatherForecast/get-dynamic?resultType=2")] 55 | [DataRow("/api/WeatherForecast/get-dynamic-task?resultType=0")] 56 | [DataRow("/api/WeatherForecast/get-dynamic-task?resultType=1")] 57 | [DataRow("/api/WeatherForecast/get-dynamic-task?resultType=2")] 58 | [DataRow("/api/WeatherForecast/get-dynamic-valuetask?resultType=0")] 59 | [DataRow("/api/WeatherForecast/get-dynamic-valuetask?resultType=1")] 60 | [DataRow("/api/WeatherForecast/get-dynamic-valuetask?resultType=2")] 61 | [DataRow("/api/WeatherForecast/get-task")] 62 | [DataRow("/api/WeatherForecast/get-task-direct-api-response")] 63 | [DataRow("/api/WeatherForecast/get-valuetask")] 64 | [DataRow("/api/WeatherForecast/get-valuetask-direct-api-response")] 65 | [DataRow("/api/WeatherForecast/get-with-param")] 66 | [DataRow("/api/WeatherForecast/get-with-param-required?count=5")] 67 | [DataRow("/api/WeatherForecast/get-with-param-required-limit?count=5")] 68 | public async Task Should_Wrapped200(string requestPath) 69 | { 70 | var response = await Client.GetFromJsonAsync>(requestPath); 71 | 72 | CheckResponseCode(response); 73 | CheckWeatherForecast(response.Data); 74 | } 75 | 76 | [TestMethod] 77 | [DataRow("/api/WeatherForecast/get-authorize")] 78 | public async Task Should_Wrapped200ThroughAuthorize(string requestPath) 79 | { 80 | var response = await Client.GetFromJsonAsync>(requestPath); 81 | 82 | CheckResponseCode(response, 401); 83 | 84 | Assert.IsNull(response.Data); 85 | 86 | Debug.WriteLine($"No authentication Message: {response.Message}"); 87 | 88 | #region Cookie 89 | 90 | var cookie = await LoginAsync(false, true); 91 | 92 | response = await Client.CreateRequest(requestPath) 93 | .UseCookie(cookie) 94 | .GetAsObjectAsync>(); 95 | 96 | CheckResponseCode(response, 403); 97 | 98 | Debug.WriteLine($"Cookie can not access Message: {response.Message}"); 99 | 100 | cookie = await LoginAsync(true, true); 101 | 102 | response = await Client.CreateRequest(requestPath) 103 | .UseCookie(cookie) 104 | .GetAsObjectAsync>(); 105 | 106 | CheckResponseCode(response); 107 | CheckWeatherForecast(response.Data); 108 | 109 | #endregion Cookie 110 | 111 | #region Jwt 112 | 113 | var token = await LoginAsync(false, false); 114 | 115 | response = await Client.CreateRequest(requestPath) 116 | .UseBearerToken(token) 117 | .GetAsObjectAsync>(); 118 | 119 | CheckResponseCode(response, 403); 120 | 121 | Debug.WriteLine($"Jwt can not access Message: {response.Message}"); 122 | 123 | token = await LoginAsync(true, false); 124 | 125 | response = await Client.CreateRequest(requestPath) 126 | .AddHeader("Authorization", $"Bearer {token}") 127 | .GetAsObjectAsync>(); 128 | 129 | CheckResponseCode(response); 130 | CheckWeatherForecast(response.Data); 131 | 132 | #endregion Jwt 133 | } 134 | 135 | [TestMethod] 136 | [DataRow("/api/WeatherForecast/get-with-param-required")] 137 | [DataRow("/api/WeatherForecast/get-with-param-required-limit?count=0")] 138 | [DataRow("/api/WeatherForecast/get-with-param-required-limit?count=11")] 139 | public async Task Should_Wrapped400(string requestPath) 140 | { 141 | var response = await Client.GetFromJsonAsync>(requestPath); 142 | 143 | CheckResponseCode(response, 400); 144 | 145 | Debug.WriteLine(response.Message); 146 | } 147 | 148 | [TestMethod] 149 | [DataRow("/api/WeatherForecast/get-authentication")] 150 | [DataRow("/api/WeatherForecast/get-authorize")] 151 | public async Task Should_Wrapped401(string requestPath) 152 | { 153 | var response = await Client.GetFromJsonAsync>(requestPath); 154 | 155 | CheckResponseCode(response, 401); 156 | 157 | Assert.IsNull(response.Data); 158 | 159 | Debug.WriteLine(response.Message); 160 | } 161 | 162 | [TestMethod] 163 | [DataRow("/api/WeatherForecast/get-exception-throw")] 164 | public async Task Should_Wrapped500(string requestPath) 165 | { 166 | var response = await Client.GetFromJsonAsync>(requestPath); 167 | 168 | CheckResponseCode(response, 500); 169 | 170 | Debug.WriteLine(response.Message); 171 | } 172 | 173 | [TestMethod] 174 | [DataRow("/api/WeatherForecast/get-nowrap")] 175 | public async Task ShouldReturnNotWrapped(string requestPath) 176 | { 177 | var response = await Client.GetFromJsonAsync(requestPath); 178 | 179 | CheckWeatherForecast(response); 180 | } 181 | 182 | #endregion Public 方法 183 | 184 | #region Protected 方法 185 | 186 | protected static void CheckResponseCode(ApiResponse? apiResponse, int code = StatusCodes.Status200OK) 187 | { 188 | Assert.IsNotNull(apiResponse); 189 | Assert.AreEqual(code, apiResponse.Code); 190 | } 191 | 192 | protected override async Task CreateServerHostBuilderAsync() 193 | { 194 | var builder = await CreateServerHost(); 195 | builder.ConfigureServices(services => services.AddResponseAutoWrapper(GetOptionsSetupAction())); 196 | return builder; 197 | } 198 | 199 | protected virtual Action? GetOptionsSetupAction() 200 | { 201 | return options => { }; 202 | } 203 | 204 | #endregion Protected 方法 205 | } 206 | 207 | [TestClass] 208 | public class GenericApiTest_DisableOpenAPISupport : GenericApiTest 209 | { 210 | #region Protected 方法 211 | 212 | protected override Action? GetOptionsSetupAction() 213 | { 214 | return options => options.DisableOpenAPISupport = true; 215 | } 216 | 217 | #endregion Protected 方法 218 | } 219 | -------------------------------------------------------------------------------- /test/ResponseAutoWrapper.Test/LCRTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.Diagnostics.CodeAnalysis; 4 | using System.Net.Http.Json; 5 | using System.Threading.Tasks; 6 | 7 | using Cuture.AspNetCore.ResponseAutoWrapper; 8 | using Cuture.Http; 9 | 10 | using Microsoft.AspNetCore.Http; 11 | using Microsoft.Extensions.DependencyInjection; 12 | using Microsoft.Extensions.Hosting; 13 | using Microsoft.VisualStudio.TestTools.UnitTesting; 14 | 15 | using ResponseAutoWrapper.TestHost; 16 | 17 | namespace ResponseAutoWrapper.Test; 18 | 19 | [TestClass] 20 | public class LCRTest : TestServerBase 21 | { 22 | #region Public 属性 23 | 24 | public virtual string UriPrefix { get; set; } = "/api/LCRWeatherForecast"; 25 | 26 | #endregion Public 属性 27 | 28 | #region Public 方法 29 | 30 | [TestMethod] 31 | [DataRow("/get?type=1")] 32 | [DataRow("/get?type=2")] 33 | [DataRow("/get-inheritedtask?type=1")] 34 | [DataRow("/get-inheritedtask?type=2")] 35 | [DataRow("/get-inheritedtask2?type=1")] 36 | [DataRow("/get-inheritedtask2?type=2")] 37 | [DataRow("/get-task?type=1")] 38 | [DataRow("/get-task?type=2")] 39 | [DataRow("/get-valuetask?type=1")] 40 | [DataRow("/get-valuetask?type=2")] 41 | public async Task Should_CustomDescription(string requestPath) 42 | { 43 | var response = await Client.GetFromJsonAsync>(CombineUri(requestPath)); 44 | 45 | CheckResponseCode(response, 10086); 46 | 47 | Assert.AreEqual("Hello world!", response.Info); 48 | 49 | Debug.WriteLine(response.Info); 50 | } 51 | 52 | [TestMethod] 53 | [DataRow("/get")] 54 | [DataRow("/get-inheritedtask")] 55 | [DataRow("/get-inheritedtask2")] 56 | [DataRow("/get-direct-api-response")] 57 | [DataRow("/get-direct-inherited-api-response")] 58 | [DataRow("/get-dynamic?resultType=0")] 59 | [DataRow("/get-dynamic?resultType=1")] 60 | [DataRow("/get-dynamic?resultType=2")] 61 | [DataRow("/get-dynamic-task?resultType=0")] 62 | [DataRow("/get-dynamic-task?resultType=1")] 63 | [DataRow("/get-dynamic-task?resultType=2")] 64 | [DataRow("/get-dynamic-valuetask?resultType=0")] 65 | [DataRow("/get-dynamic-valuetask?resultType=1")] 66 | [DataRow("/get-dynamic-valuetask?resultType=2")] 67 | [DataRow("/get-task")] 68 | [DataRow("/get-task-direct-api-response")] 69 | [DataRow("/get-valuetask")] 70 | [DataRow("/get-valuetask-direct-api-response")] 71 | [DataRow("/get-with-param")] 72 | [DataRow("/get-with-param-required?count=5")] 73 | [DataRow("/get-with-param-required-limit?count=5")] 74 | public async Task Should_Wrapped200(string requestPath) 75 | { 76 | var response = await Client.GetFromJsonAsync>(CombineUri(requestPath)); 77 | 78 | CheckResponseCode(response); 79 | CheckWeatherForecast(response.Datas); 80 | } 81 | 82 | [TestMethod] 83 | [DataRow("/get-authorize")] 84 | public async Task Should_Wrapped200ThroughAuthorize(string requestPath) 85 | { 86 | var response = await Client.GetFromJsonAsync>(CombineUri(requestPath)); 87 | 88 | CheckResponseCode(response, 401); 89 | 90 | Assert.IsNull(response.Datas); 91 | 92 | Debug.WriteLine($"No authentication Message: {response.Info}"); 93 | 94 | #region Cookie 95 | 96 | var cookie = await LoginAsync(false, true); 97 | 98 | response = await Client.CreateRequest(CombineUri(requestPath)) 99 | .UseCookie(cookie) 100 | .GetAsObjectAsync>(); 101 | 102 | CheckResponseCode(response, 403); 103 | 104 | Debug.WriteLine($"Cookie can not access Message: {response.Info}"); 105 | 106 | cookie = await LoginAsync(true, true); 107 | 108 | response = await Client.CreateRequest(CombineUri(requestPath)) 109 | .UseCookie(cookie) 110 | .GetAsObjectAsync>(); 111 | 112 | CheckResponseCode(response); 113 | CheckWeatherForecast(response.Datas); 114 | 115 | #endregion Cookie 116 | 117 | #region Jwt 118 | 119 | var token = await LoginAsync(false, false, "datas"); 120 | 121 | response = await Client.CreateRequest(CombineUri(requestPath)) 122 | .UseBearerToken(token) 123 | .GetAsObjectAsync>(); 124 | 125 | CheckResponseCode(response, 403); 126 | 127 | Debug.WriteLine($"Jwt can not access Message: {response.Info}"); 128 | 129 | token = await LoginAsync(true, false, "datas"); 130 | 131 | response = await Client.CreateRequest(CombineUri(requestPath)) 132 | .AddHeader("Authorization", $"Bearer {token}") 133 | .GetAsObjectAsync>(); 134 | 135 | CheckResponseCode(response); 136 | CheckWeatherForecast(response.Datas); 137 | 138 | #endregion Jwt 139 | } 140 | 141 | [TestMethod] 142 | [DataRow("/get-with-param-required")] 143 | [DataRow("/get-with-param-required-limit?count=0")] 144 | [DataRow("/get-with-param-required-limit?count=11")] 145 | public async Task Should_Wrapped400(string requestPath) 146 | { 147 | var response = await Client.GetFromJsonAsync>(CombineUri(requestPath)); 148 | 149 | CheckResponseCode(response, 400); 150 | 151 | Debug.WriteLine(response.Info); 152 | } 153 | 154 | [TestMethod] 155 | [DataRow("/get-authentication")] 156 | [DataRow("/get-authorize")] 157 | public async Task Should_Wrapped401(string requestPath) 158 | { 159 | var response = await Client.GetFromJsonAsync>(CombineUri(requestPath)); 160 | 161 | CheckResponseCode(response, 401); 162 | 163 | Assert.IsNull(response.Datas); 164 | 165 | Debug.WriteLine(response.Info); 166 | } 167 | 168 | [TestMethod] 169 | [DataRow("/get-exception-throw")] 170 | public async Task Should_Wrapped500(string requestPath) 171 | { 172 | var response = await Client.GetFromJsonAsync>(CombineUri(requestPath)); 173 | 174 | CheckResponseCode(response, 500); 175 | 176 | Debug.WriteLine(response.Info); 177 | } 178 | 179 | [TestMethod] 180 | [DataRow("/get-nowrap")] 181 | public async Task ShouldReturnNotWrapped(string requestPath) 182 | { 183 | var response = await Client.GetFromJsonAsync(CombineUri(requestPath)); 184 | 185 | CheckWeatherForecast(response); 186 | } 187 | 188 | #endregion Public 方法 189 | 190 | #region Protected 方法 191 | 192 | protected static void CheckResponseCode([NotNull] LegacyCustomResponse? apiResponse, int code = StatusCodes.Status200OK) 193 | { 194 | Assert.IsNotNull(apiResponse); 195 | Assert.AreEqual(code, apiResponse.StatusCode); 196 | } 197 | 198 | [DebuggerStepThrough] 199 | protected string CombineUri(string path) => $"{UriPrefix}{path}"; 200 | 201 | protected override async Task CreateServerHostBuilderAsync() 202 | { 203 | var builder = await CreateServerHost(); 204 | builder.ConfigureServices(services => 205 | { 206 | services.AddResponseAutoWrapper, int, string>(GetOptionsSetupAction()) 207 | .ConfigureWrappers(builder => builder.AddLegacyWrappers()); 208 | }); 209 | return builder; 210 | } 211 | 212 | protected virtual Action? GetOptionsSetupAction() 213 | { 214 | return options => { }; 215 | } 216 | 217 | #endregion Protected 方法 218 | } 219 | 220 | [TestClass] 221 | public class LCRTest_DisableOpenAPISupport : LCRTest 222 | { 223 | #region Protected 方法 224 | 225 | protected override Action? GetOptionsSetupAction() 226 | { 227 | return options => options.DisableOpenAPISupport = true; 228 | } 229 | 230 | #endregion Protected 方法 231 | } 232 | 233 | [TestClass] 234 | public class LCRTest_NotGeneric : LCRTest 235 | { 236 | #region Public 属性 237 | 238 | public override string UriPrefix { get; set; } = "/api/NGLCRWeatherForecast"; 239 | 240 | #endregion Public 属性 241 | 242 | #region Protected 方法 243 | 244 | protected override Task CreateServerHostBuilderAsync() => CreateServerHostWithStartup(); 245 | 246 | #endregion Protected 方法 247 | } 248 | -------------------------------------------------------------------------------- /test/ResponseAutoWrapper.Test/MiddlewareExceptionTest.cs: -------------------------------------------------------------------------------- 1 | using System.Net.Http.Json; 2 | using System.Threading.Tasks; 3 | 4 | using Microsoft.AspNetCore.Mvc; 5 | using Microsoft.Extensions.Hosting; 6 | using Microsoft.VisualStudio.TestTools.UnitTesting; 7 | 8 | using ResponseAutoWrapper.TestHost; 9 | 10 | namespace ResponseAutoWrapper.Test; 11 | 12 | [TestClass] 13 | public class MiddlewareExceptionCustomResponseByResponseCreatorTest : TestServerBase 14 | { 15 | #region Public 方法 16 | 17 | [TestMethod] 18 | [DataRow("/api/LCRWeatherForecast/get")] 19 | public async Task Should_Wrapped500(string requestPath) 20 | { 21 | var response = await Client.GetFromJsonAsync>(requestPath); 22 | 23 | Assert.AreEqual(500, response.StatusCode); 24 | } 25 | 26 | #endregion Public 方法 27 | 28 | #region Protected 方法 29 | 30 | protected override Task CreateServerHostBuilderAsync() => CreateServerHostWithStartup(); 31 | 32 | #endregion Protected 方法 33 | } 34 | 35 | [TestClass] 36 | public class MiddlewareExceptionTest : TestServerBase 37 | { 38 | #region Public 方法 39 | 40 | [TestMethod] 41 | [DataRow("/api/WeatherForecast/get")] 42 | public async Task Should_Wrapped500(string requestPath) 43 | { 44 | var response = await Client.GetFromJsonAsync>(requestPath); 45 | 46 | Assert.AreEqual(500, response.Code); 47 | } 48 | 49 | #endregion Public 方法 50 | 51 | #region Protected 方法 52 | 53 | protected override Task CreateServerHostBuilderAsync() => CreateServerHostWithStartup(); 54 | 55 | #endregion Protected 方法 56 | } 57 | -------------------------------------------------------------------------------- /test/ResponseAutoWrapper.Test/README.MD: -------------------------------------------------------------------------------- 1 | - 关键词 2 | 3 | CR - CustomResponse 4 | LCR - LegacyCustomResponse 5 | NGCR - Not Generic CustomResponse 6 | NGLCR - Not Generic LegacyCustomResponse 7 | 8 | - TestHost 9 | 启动参数 [0] 可以指定使用的Startup类,达到切换配置的效果 10 | -------------------------------------------------------------------------------- /test/ResponseAutoWrapper.Test/ResponseAutoWrapper.Test.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | false 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /test/ResponseAutoWrapper.Test/TestServerBase.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Net.Http; 5 | using System.Threading.Tasks; 6 | 7 | using Cuture.Http; 8 | 9 | using Microsoft.AspNetCore.TestHost; 10 | using Microsoft.Extensions.Hosting; 11 | using Microsoft.VisualStudio.TestTools.UnitTesting; 12 | 13 | using ResponseAutoWrapper.TestHost; 14 | 15 | namespace ResponseAutoWrapper.Test; 16 | 17 | public abstract class TestServerBase 18 | { 19 | #region Private 字段 20 | 21 | private IHost _host = null!; 22 | 23 | #endregion Private 字段 24 | 25 | #region Protected 属性 26 | 27 | protected HttpClient Client { get; private set; } = null!; 28 | 29 | #endregion Protected 属性 30 | 31 | #region Public 方法 32 | 33 | [TestCleanup] 34 | public async Task CleanupAsync() 35 | { 36 | await _host.StopAsync(); 37 | _host.Dispose(); 38 | _host = null!; 39 | } 40 | 41 | [TestInitialize] 42 | public async Task InitAsync() 43 | { 44 | var hostBuilder = await CreateServerHostBuilderAsync(); 45 | _host = hostBuilder.Build(); 46 | await _host.StartAsync(); 47 | Client = _host.GetTestClient(); 48 | } 49 | 50 | #endregion Public 方法 51 | 52 | #region Protected 方法 53 | 54 | protected static void CheckWeatherForecast(WeatherForecast? weatherForecast) 55 | { 56 | Assert.IsNotNull(weatherForecast); 57 | Assert.IsFalse(string.IsNullOrWhiteSpace(weatherForecast.Summary)); 58 | Assert.IsTrue(weatherForecast.Date >= DateTime.Today); 59 | } 60 | 61 | protected static void CheckWeatherForecast(IEnumerable? weatherForecasts, int count = 5) 62 | { 63 | Assert.IsNotNull(weatherForecasts); 64 | Assert.AreEqual(count, weatherForecasts.Count()); 65 | 66 | foreach (var item in weatherForecasts) 67 | { 68 | CheckWeatherForecast(item); 69 | } 70 | } 71 | 72 | protected Task CreateServerHost() 73 | => Task.FromResult(Hosts.CreateHostBuilder(true)); 74 | 75 | protected abstract Task CreateServerHostBuilderAsync(); 76 | 77 | protected Task CreateServerHostWithStartup() where TStartup : BaseStartup 78 | => Task.FromResult(Hosts.CreateHostBuilder(true)); 79 | 80 | protected async Task LoginAsync(bool canAccess, bool isCookie, string jwtPropertyName = "data") 81 | { 82 | if (isCookie) 83 | { 84 | //cookie 85 | var responseMessage = await Client.CreateRequest($"/api/Login/cookie?canAccess={canAccess}").TryGetAsStringAsync(); 86 | 87 | Assert.IsNotNull(responseMessage.ResponseMessage); 88 | return responseMessage.ResponseMessage.GetCookie(); 89 | } 90 | else 91 | { 92 | //jwt 93 | var responseMessage = await Client.CreateRequest($"/api/Login/jwt?canAccess={canAccess}").TryGetAsJsonDocumentAsync(); 94 | 95 | Assert.IsNotNull(responseMessage.Data?.RootElement); 96 | var token = responseMessage.Data.RootElement.GetProperty(jwtPropertyName).GetString(); 97 | 98 | Assert.IsNotNull(token); 99 | return token; 100 | } 101 | } 102 | 103 | #endregion Protected 方法 104 | } 105 | -------------------------------------------------------------------------------- /test/ResponseAutoWrapper.TestHost/AuthorizeMixedAttribute.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Authentication.Cookies; 2 | using Microsoft.AspNetCore.Authentication.JwtBearer; 3 | using Microsoft.AspNetCore.Authorization; 4 | 5 | namespace ResponseAutoWrapper.TestHost; 6 | 7 | public class AuthorizeMixedAttribute : AuthorizeAttribute 8 | { 9 | #region Public 构造函数 10 | 11 | public AuthorizeMixedAttribute() 12 | { 13 | AuthenticationSchemes = $"{CookieAuthenticationDefaults.AuthenticationScheme},{JwtBearerDefaults.AuthenticationScheme}"; 14 | } 15 | 16 | public AuthorizeMixedAttribute(string policy) : base(policy) 17 | { 18 | AuthenticationSchemes = $"{CookieAuthenticationDefaults.AuthenticationScheme},{JwtBearerDefaults.AuthenticationScheme}"; 19 | } 20 | 21 | #endregion Public 构造函数 22 | } 23 | -------------------------------------------------------------------------------- /test/ResponseAutoWrapper.TestHost/Controllers/CRWeatherForecastController.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | using Microsoft.AspNetCore.Http; 4 | using Microsoft.AspNetCore.Mvc; 5 | 6 | namespace ResponseAutoWrapper.TestHost.Controllers; 7 | 8 | // Copy from LCRWeatherForecastController 9 | 10 | [ApiController] 11 | [Route("api/[controller]")] 12 | public class CRWeatherForecastController : GenericWeatherForecastController 13 | { 14 | #region Public 方法 15 | 16 | [HttpGet] 17 | [Route("get-direct-api-response")] 18 | public CustomResponse GetDirectApiResponse() 19 | { 20 | return new CustomResponse() { Data = WeatherForecast.GenerateData() }; 21 | } 22 | 23 | [HttpGet] 24 | [Route("get-direct-inherited-api-response")] 25 | public InheritedCustomResponse GetDirectInheritedApiResponse() 26 | { 27 | return new InheritedCustomResponse() 28 | { 29 | Data = WeatherForecast.GenerateData() 30 | }; 31 | } 32 | 33 | [HttpGet] 34 | [Route("get-dynamic")] 35 | public override dynamic GetDynamic(int resultType, int type = 0) 36 | { 37 | if (type == 1) 38 | { 39 | HttpContext.DescribeResponse(CustomCode, CustomMessage); 40 | } 41 | 42 | return type switch 43 | { 44 | 0 => WeatherForecast.GenerateData(), 45 | 1 => new CustomResponse() { Data = WeatherForecast.GenerateData() }, 46 | 2 => new InheritedCustomResponse() { Data = WeatherForecast.GenerateData() }, 47 | _ => WeatherForecast.GenerateData(), 48 | }; 49 | } 50 | 51 | [HttpGet] 52 | [Route("get-task-direct-api-response")] 53 | public async Task> GetTaskDirectApiResponseAsync() 54 | { 55 | await Task.Delay(1); 56 | return new CustomResponse() { Data = WeatherForecast.GenerateData() }; 57 | } 58 | 59 | [HttpGet] 60 | [Route("get-valuetask-direct-api-response")] 61 | public async ValueTask> GetValueTaskDirectApiResponseAsync() 62 | { 63 | await Task.Delay(1); 64 | return new CustomResponse() { Data = WeatherForecast.GenerateData() }; 65 | } 66 | 67 | #endregion Public 方法 68 | 69 | #region Protected 方法 70 | 71 | protected override TResult DescribeResponse(TResult result) 72 | { 73 | HttpContext.DescribeResponse(new ResponseCode(ResponseState.Success, 10086), new ResponseMessage() { Text = CustomMessage }); 74 | 75 | return result; 76 | } 77 | 78 | #endregion Protected 方法 79 | } 80 | -------------------------------------------------------------------------------- /test/ResponseAutoWrapper.TestHost/Controllers/GenericWeatherForecastController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel.DataAnnotations; 3 | using System.Threading.Tasks; 4 | 5 | using Microsoft.AspNetCore.Http; 6 | using Microsoft.AspNetCore.Mvc; 7 | 8 | namespace ResponseAutoWrapper.TestHost.Controllers; 9 | 10 | /// 11 | /// 通用的 WeatherForecastController 12 | /// 13 | [ApiController] 14 | [Route("api/[controller]")] 15 | public abstract class GenericWeatherForecastController : ControllerBase 16 | { 17 | #region Public 字段 18 | 19 | public const int CustomCode = 10086; 20 | 21 | public const string CustomMessage = "Hello world!"; 22 | 23 | #endregion Public 字段 24 | 25 | #region Public 方法 26 | 27 | [HttpGet] 28 | [Route("get")] 29 | public WeatherForecast[] Get(int type = 0) 30 | { 31 | return type switch 32 | { 33 | 1 or 2 => DescribeResponse(type == 1 ? WeatherForecast.GenerateData() : null), 34 | _ => WeatherForecast.GenerateData(), 35 | }; 36 | } 37 | 38 | [HttpGet] 39 | [Route("get-inheritedtask")] 40 | public InheritedTask GetByInheritedTask(int type = 0) 41 | { 42 | var task = new InheritedTask(() => type switch 43 | { 44 | 1 or 2 => DescribeResponse(type == 1 ? WeatherForecast.GenerateData() : null), 45 | _ => WeatherForecast.GenerateData(), 46 | }); 47 | 48 | task.Start(); 49 | return task; 50 | } 51 | 52 | [HttpGet] 53 | [Route("get-inheritedtask2")] 54 | public TwiceInheritedTask GetByInheritedTask2(int type = 0) 55 | { 56 | var task = new TwiceInheritedTask(() => type switch 57 | { 58 | 1 or 2 => DescribeResponse(type == 1 ? WeatherForecast.GenerateData() : null), 59 | _ => WeatherForecast.GenerateData(), 60 | }); 61 | task.Start(); 62 | return task; 63 | } 64 | 65 | [HttpGet] 66 | [Route("get-dynamic")] 67 | public abstract dynamic GetDynamic(int resultType, int type = 0); 68 | 69 | [HttpGet] 70 | [Route("get-dynamic-task")] 71 | public async Task GetDynamicTaskAsync(int resultType, int type = 0) 72 | { 73 | await Task.Delay(1); 74 | 75 | return GetDynamic(resultType, type); 76 | } 77 | 78 | [HttpGet] 79 | [Route("get-dynamic-valuetask")] 80 | public async ValueTask GetDynamicValueTaskAsync(int resultType, int type = 0) 81 | { 82 | await Task.Delay(1); 83 | 84 | return GetDynamic(resultType, type); 85 | } 86 | 87 | [HttpGet] 88 | [AuthorizeMixed] 89 | [Route("get-authentication")] 90 | public WeatherForecast[] GetNeedAuthentication() 91 | { 92 | return WeatherForecast.GenerateData(); 93 | } 94 | 95 | [HttpGet] 96 | [AuthorizeMixed("CanAccess")] 97 | [Route("get-authorize")] 98 | public WeatherForecast[] GetNeedAuthorize() 99 | { 100 | return WeatherForecast.GenerateData(); 101 | } 102 | 103 | [HttpGet] 104 | [Route("get-nowrap")] 105 | [NoResponseWrap] 106 | public WeatherForecast[] GetNoWrap() 107 | { 108 | return WeatherForecast.GenerateData(); 109 | } 110 | 111 | [HttpGet] 112 | [Route("get-task")] 113 | public async Task GetTaskAsync(int type = 0) 114 | { 115 | await Task.Delay(1); 116 | 117 | return type switch 118 | { 119 | 1 or 2 => DescribeResponse(type == 1 ? WeatherForecast.GenerateData() : null), 120 | _ => WeatherForecast.GenerateData(), 121 | }; 122 | } 123 | 124 | [HttpGet] 125 | [Route("get-valuetask")] 126 | public async ValueTask GetValueTaskAsync(int type = 0) 127 | { 128 | await Task.Delay(1); 129 | 130 | return type switch 131 | { 132 | 1 or 2 => DescribeResponse(type == 1 ? WeatherForecast.GenerateData() : null), 133 | _ => WeatherForecast.GenerateData(), 134 | }; 135 | } 136 | 137 | [HttpGet] 138 | [Route("get-exception-throw")] 139 | public WeatherForecast[] GetWithExceptionThrow() 140 | { 141 | throw new Exception("There has some exception"); 142 | } 143 | 144 | [HttpGet] 145 | [Route("get-with-param")] 146 | public WeatherForecast[] GetWithParam(int? count) 147 | { 148 | return WeatherForecast.GenerateData(count ?? 5); 149 | } 150 | 151 | [HttpGet] 152 | [Route("get-with-param-required")] 153 | public WeatherForecast[] GetWithParamRequired([Required] int? count) 154 | { 155 | return WeatherForecast.GenerateData(count!.Value); 156 | } 157 | 158 | [HttpGet] 159 | [Route("get-with-param-required-limit")] 160 | public WeatherForecast[] GetWithParamRequiredAndLimit([Required][Range(1, 10)] int? count) 161 | { 162 | return WeatherForecast.GenerateData(count!.Value); 163 | } 164 | 165 | #endregion Public 方法 166 | 167 | #region Protected 方法 168 | 169 | protected virtual TResult DescribeResponse(TResult result) 170 | { 171 | HttpContext.DescribeResponse(CustomCode, CustomMessage); 172 | return result; 173 | } 174 | 175 | #endregion Protected 方法 176 | } 177 | -------------------------------------------------------------------------------- /test/ResponseAutoWrapper.TestHost/Controllers/LCRWeatherForecastController.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | using Microsoft.AspNetCore.Http; 4 | using Microsoft.AspNetCore.Mvc; 5 | 6 | namespace ResponseAutoWrapper.TestHost.Controllers; 7 | 8 | [ApiController] 9 | [Route("api/[controller]")] 10 | public class LCRWeatherForecastController : GenericWeatherForecastController 11 | { 12 | #region Public 方法 13 | 14 | [HttpGet] 15 | [Route("get-direct-api-response")] 16 | public LegacyCustomResponse GetDirectApiResponse() 17 | { 18 | return new LegacyCustomResponse() { Datas = WeatherForecast.GenerateData() }; 19 | } 20 | 21 | [HttpGet] 22 | [Route("get-direct-inherited-api-response")] 23 | public InheritedLegacyCustomResponse GetDirectInheritedApiResponse() 24 | { 25 | return new InheritedLegacyCustomResponse() 26 | { 27 | Datas = WeatherForecast.GenerateData() 28 | }; 29 | } 30 | 31 | [HttpGet] 32 | [Route("get-dynamic")] 33 | public override dynamic GetDynamic(int resultType, int type = 0) 34 | { 35 | if (type == 1) 36 | { 37 | HttpContext.DescribeResponse(CustomCode, CustomMessage); 38 | } 39 | 40 | return type switch 41 | { 42 | 0 => WeatherForecast.GenerateData(), 43 | 1 => new LegacyCustomResponse() { Datas = WeatherForecast.GenerateData() }, 44 | 2 => new InheritedLegacyCustomResponse() { Datas = WeatherForecast.GenerateData() }, 45 | _ => WeatherForecast.GenerateData(), 46 | }; 47 | } 48 | 49 | [HttpGet] 50 | [Route("get-task-direct-api-response")] 51 | public async Task> GetTaskDirectApiResponseAsync() 52 | { 53 | await Task.Delay(1); 54 | return new LegacyCustomResponse() { Datas = WeatherForecast.GenerateData() }; 55 | } 56 | 57 | [HttpGet] 58 | [Route("get-valuetask-direct-api-response")] 59 | public async ValueTask> GetValueTaskDirectApiResponseAsync() 60 | { 61 | await Task.Delay(1); 62 | return new LegacyCustomResponse() { Datas = WeatherForecast.GenerateData() }; 63 | } 64 | 65 | #endregion Public 方法 66 | } 67 | -------------------------------------------------------------------------------- /test/ResponseAutoWrapper.TestHost/Controllers/LoginController.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IdentityModel.Tokens.Jwt; 3 | using System.Security.Claims; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using Microsoft.AspNetCore.Authentication; 7 | using Microsoft.AspNetCore.Authentication.Cookies; 8 | using Microsoft.AspNetCore.Http; 9 | using Microsoft.AspNetCore.Mvc; 10 | using Microsoft.IdentityModel.Tokens; 11 | 12 | namespace ResponseAutoWrapper.TestHost.Controllers; 13 | 14 | [ApiController] 15 | [Route("api/[controller]")] 16 | public class LoginController : ControllerBase 17 | { 18 | #region Public 方法 19 | 20 | [HttpGet] 21 | [Route("cookie")] 22 | public async Task CookieAsync([FromQuery] bool canAccess) 23 | { 24 | var claimsIdentity = new ClaimsIdentity(new[] 25 | { 26 | new Claim("CanAccess",canAccess?"1":"0"), 27 | }, CookieAuthenticationDefaults.AuthenticationScheme); 28 | var claimsPrincipal = new ClaimsPrincipal(claimsIdentity); 29 | await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, claimsPrincipal); 30 | } 31 | 32 | [HttpGet] 33 | [Route("jwt")] 34 | public string Jwt([FromQuery] bool canAccess) 35 | { 36 | var claims = new[] 37 | { 38 | new Claim("CanAccess",canAccess?"1":"0"), 39 | }; 40 | var key = new SymmetricSecurityKey(Encoding.ASCII.GetBytes("123456789123456789_123456789123456789")); 41 | var credentials = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); 42 | var jwtToken = new JwtSecurityToken("Issuer", "Audience", claims, expires: DateTime.Now.AddMinutes(600), signingCredentials: credentials); 43 | 44 | var token = new JwtSecurityTokenHandler().WriteToken(jwtToken); 45 | 46 | return token; 47 | } 48 | 49 | #endregion Public 方法 50 | } 51 | -------------------------------------------------------------------------------- /test/ResponseAutoWrapper.TestHost/Controllers/NGCRWeatherForecastController.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | using Microsoft.AspNetCore.Http; 4 | using Microsoft.AspNetCore.Mvc; 5 | 6 | namespace ResponseAutoWrapper.TestHost.Controllers; 7 | 8 | //Copy from NGLCRWeatherForecastController 9 | 10 | [ApiController] 11 | [Route("api/[controller]")] 12 | public class NGCRWeatherForecastController : GenericWeatherForecastController 13 | { 14 | #region Public 方法 15 | 16 | [HttpGet] 17 | [Route("get-direct-api-response")] 18 | public CustomResponse GetDirectApiResponse() 19 | { 20 | return new CustomResponse() { Data = WeatherForecast.GenerateData() }; 21 | } 22 | 23 | [HttpGet] 24 | [Route("get-direct-inherited-api-response")] 25 | public InheritedCustomResponseNotGeneric GetDirectInheritedApiResponse() 26 | { 27 | return new InheritedCustomResponseNotGeneric() 28 | { 29 | Data = WeatherForecast.GenerateData() 30 | }; 31 | } 32 | 33 | [HttpGet] 34 | [Route("get-dynamic")] 35 | public override dynamic GetDynamic(int resultType, int type = 0) 36 | { 37 | if (type == 1) 38 | { 39 | HttpContext.DescribeResponse(CustomCode, CustomMessage); 40 | } 41 | 42 | return type switch 43 | { 44 | 0 => WeatherForecast.GenerateData(), 45 | 1 => new CustomResponse() { Data = WeatherForecast.GenerateData() }, 46 | 2 => new InheritedCustomResponseNotGeneric() { Data = WeatherForecast.GenerateData() }, 47 | _ => WeatherForecast.GenerateData(), 48 | }; 49 | } 50 | 51 | [HttpGet] 52 | [Route("get-task-direct-api-response")] 53 | public async Task GetTaskDirectApiResponseAsync() 54 | { 55 | await Task.Delay(1); 56 | return new CustomResponse() { Data = WeatherForecast.GenerateData() }; 57 | } 58 | 59 | [HttpGet] 60 | [Route("get-valuetask-direct-api-response")] 61 | public async ValueTask GetValueTaskDirectApiResponseAsync() 62 | { 63 | await Task.Delay(1); 64 | return new CustomResponse() { Data = WeatherForecast.GenerateData() }; 65 | } 66 | 67 | #endregion Public 方法 68 | 69 | #region Protected 方法 70 | 71 | protected override TResult DescribeResponse(TResult result) 72 | { 73 | HttpContext.DescribeResponse(new ResponseCode(ResponseState.Success, 10086), new ResponseMessage() { Text = CustomMessage }); 74 | 75 | return result; 76 | } 77 | 78 | #endregion Protected 方法 79 | } 80 | -------------------------------------------------------------------------------- /test/ResponseAutoWrapper.TestHost/Controllers/NGLCRWeatherForecastController.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | using Microsoft.AspNetCore.Http; 4 | using Microsoft.AspNetCore.Mvc; 5 | 6 | namespace ResponseAutoWrapper.TestHost.Controllers; 7 | 8 | [ApiController] 9 | [Route("api/[controller]")] 10 | public class NGLCRWeatherForecastController : GenericWeatherForecastController 11 | { 12 | #region Public 方法 13 | 14 | [HttpGet] 15 | [Route("get-direct-api-response")] 16 | public LegacyCustomResponse GetDirectApiResponse() 17 | { 18 | return new LegacyCustomResponse() { Datas = WeatherForecast.GenerateData() }; 19 | } 20 | 21 | [HttpGet] 22 | [Route("get-direct-inherited-api-response")] 23 | public InheritedLegacyCustomResponseNotGeneric GetDirectInheritedApiResponse() 24 | { 25 | return new InheritedLegacyCustomResponseNotGeneric() 26 | { 27 | Datas = WeatherForecast.GenerateData() 28 | }; 29 | } 30 | 31 | [HttpGet] 32 | [Route("get-dynamic")] 33 | public override dynamic GetDynamic(int resultType, int type = 0) 34 | { 35 | if (type == 1) 36 | { 37 | HttpContext.DescribeResponse(CustomCode, CustomMessage); 38 | } 39 | 40 | return type switch 41 | { 42 | 0 => WeatherForecast.GenerateData(), 43 | 1 => new LegacyCustomResponse() { Datas = WeatherForecast.GenerateData() }, 44 | 2 => new InheritedLegacyCustomResponseNotGeneric() { Datas = WeatherForecast.GenerateData() }, 45 | _ => WeatherForecast.GenerateData(), 46 | }; 47 | } 48 | 49 | [HttpGet] 50 | [Route("get-task-direct-api-response")] 51 | public async Task GetTaskDirectApiResponseAsync() 52 | { 53 | await Task.Delay(1); 54 | return new LegacyCustomResponse() { Datas = WeatherForecast.GenerateData() }; 55 | } 56 | 57 | [HttpGet] 58 | [Route("get-valuetask-direct-api-response")] 59 | public async ValueTask GetValueTaskDirectApiResponseAsync() 60 | { 61 | await Task.Delay(1); 62 | return new LegacyCustomResponse() { Datas = WeatherForecast.GenerateData() }; 63 | } 64 | 65 | #endregion Public 方法 66 | } 67 | -------------------------------------------------------------------------------- /test/ResponseAutoWrapper.TestHost/Controllers/WeatherForecastController.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | 3 | using Microsoft.AspNetCore.Http; 4 | using Microsoft.AspNetCore.Mvc; 5 | 6 | namespace ResponseAutoWrapper.TestHost.Controllers; 7 | 8 | [ApiController] 9 | [Route("api/[controller]")] 10 | public class WeatherForecastController : GenericWeatherForecastController 11 | { 12 | #region Public 方法 13 | 14 | [HttpGet] 15 | [Route("get-direct-api-response")] 16 | public ApiResponse GetDirectApiResponse() 17 | { 18 | return ApiResponse.Create(WeatherForecast.GenerateData()); 19 | } 20 | 21 | [HttpGet] 22 | [Route("get-direct-inherited-api-response")] 23 | public InheritedLegacyResponse GetDirectInheritedApiResponse() 24 | { 25 | return new InheritedLegacyResponse() 26 | { 27 | Data = WeatherForecast.GenerateData() 28 | }; 29 | } 30 | 31 | [HttpGet] 32 | [Route("get-dynamic")] 33 | public override dynamic GetDynamic(int resultType, int type = 0) 34 | { 35 | if (type == 1) 36 | { 37 | HttpContext.DescribeResponse(CustomCode, CustomMessage); 38 | } 39 | 40 | return resultType switch 41 | { 42 | 0 => WeatherForecast.GenerateData(), 43 | 1 => ApiResponse.Create(WeatherForecast.GenerateData()), 44 | 2 => new InheritedLegacyResponse() { Data = WeatherForecast.GenerateData() }, 45 | _ => WeatherForecast.GenerateData(), 46 | }; 47 | } 48 | 49 | [HttpGet] 50 | [Route("get-task-direct-api-response")] 51 | public async Task> GetTaskDirectApiResponseAsync() 52 | { 53 | await Task.Delay(1); 54 | return ApiResponse.Create(WeatherForecast.GenerateData()); 55 | } 56 | 57 | [HttpGet] 58 | [Route("get-valuetask-direct-api-response")] 59 | public async ValueTask> GetValueTaskDirectApiResponseAsync() 60 | { 61 | await Task.Delay(1); 62 | return ApiResponse.Create(WeatherForecast.GenerateData()); 63 | } 64 | 65 | #endregion Public 方法 66 | } 67 | -------------------------------------------------------------------------------- /test/ResponseAutoWrapper.TestHost/CustomResponse.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.Linq; 5 | using System.Text; 6 | using System.Text.Json; 7 | using System.Text.Json.Serialization; 8 | using System.Xml.Linq; 9 | 10 | using Cuture.AspNetCore.ResponseAutoWrapper; 11 | 12 | namespace ResponseAutoWrapper.TestHost; 13 | 14 | public class CustomResponse 15 | { 16 | #region Public 属性 17 | 18 | public ResponseCode Code { get; set; } = ResponseCode.Success; 19 | 20 | public object Data { get; set; } 21 | 22 | public ResponseMessage Message { get; set; } 23 | 24 | #endregion Public 属性 25 | } 26 | 27 | public class CustomResponse 28 | { 29 | #region Public 属性 30 | 31 | public ResponseCode Code { get; set; } = ResponseCode.Success; 32 | 33 | public TData Data { get; set; } 34 | 35 | public ResponseMessage Message { get; set; } 36 | 37 | #endregion Public 属性 38 | } 39 | 40 | #region Code 41 | 42 | public enum ResponseState 43 | { 44 | Success, 45 | 46 | Fail, 47 | 48 | Error, 49 | } 50 | 51 | [JsonConverter(typeof(ResponseCodeConverter))] 52 | public readonly struct ResponseCode(ResponseState state, int businessCode) 53 | { 54 | #region Public 字段 55 | 56 | public static readonly ResponseCode Success = new(ResponseState.Success, 0); 57 | 58 | #endregion Public 字段 59 | 60 | #region Public 属性 61 | 62 | public int BusinessCode { get; } = businessCode; 63 | 64 | public ResponseState State { get; } = state; 65 | 66 | #endregion Public 属性 67 | 68 | #region Public 构造函数 69 | 70 | static ResponseCode() 71 | { 72 | } 73 | 74 | #endregion Public 构造函数 75 | 76 | private class ResponseCodeConverter : JsonConverter 77 | { 78 | #region Private 字段 79 | 80 | private static readonly Dictionary s_codePrefixMap = new() 81 | { 82 | { ResponseState.Success, (byte)'S' }, 83 | { ResponseState.Fail, (byte)'F' }, 84 | { ResponseState.Error, (byte)'E' }, 85 | }; 86 | 87 | private static readonly Dictionary s_codePrefixReverseMap = new() 88 | { 89 | { 'S',ResponseState.Success }, 90 | { 'F',ResponseState.Fail }, 91 | { 'E',ResponseState.Error }, 92 | }; 93 | 94 | #endregion Private 字段 95 | 96 | #region Public 方法 97 | 98 | public override ResponseCode Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) 99 | { 100 | if (reader.GetString() is string stringValue) 101 | { 102 | var span = stringValue.AsSpan(); 103 | return new ResponseCode(s_codePrefixReverseMap[span[0]], int.Parse(span.Slice(1))); 104 | } 105 | return new ResponseCode(); 106 | } 107 | 108 | public override void Write(Utf8JsonWriter writer, ResponseCode value, JsonSerializerOptions options) 109 | { 110 | Span spanValue = stackalloc byte[10]; 111 | spanValue[0] = s_codePrefixMap[value.State]; 112 | var index = Encoding.UTF8.GetBytes(value.BusinessCode.ToString(), spanValue.Slice(1)); 113 | writer.WriteStringValue(spanValue.Slice(0, index + 1)); 114 | } 115 | 116 | #endregion Public 方法 117 | } 118 | } 119 | 120 | #endregion Code 121 | 122 | #region Message 123 | 124 | public class ResponseMessage 125 | { 126 | #region Public 字段 127 | 128 | public static readonly ResponseMessage Success = new() { Text = "SUCCESS" }; 129 | 130 | #endregion Public 字段 131 | 132 | #region Public 属性 133 | 134 | public string Text { get; set; } 135 | 136 | [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)] 137 | public ActivityTraceId? TraceId { get; set; } 138 | 139 | #endregion Public 属性 140 | } 141 | 142 | #endregion Message 143 | -------------------------------------------------------------------------------- /test/ResponseAutoWrapper.TestHost/CustomResponseWrapper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.Net; 4 | 5 | using Cuture.AspNetCore.ResponseAutoWrapper; 6 | 7 | using Microsoft.AspNetCore.Http; 8 | using Microsoft.AspNetCore.Mvc; 9 | using Microsoft.AspNetCore.Mvc.Filters; 10 | using Microsoft.Extensions.Options; 11 | 12 | namespace ResponseAutoWrapper.TestHost; 13 | 14 | public class NotGenericCustomResponseWrapper(IWrapTypeCreator wrapTypeCreator, IOptions optionsAccessor) 15 | : AbstractResponseWrapper(wrapTypeCreator, optionsAccessor) 16 | { 17 | 18 | #region Public 方法 19 | 20 | public override CustomResponse? ExceptionWrap(HttpContext context, Exception exception) => new() { Code = new(ResponseState.Error, 30000), Message = new() { Text = exception.Message, TraceId = Activity.Current.TraceId } }; 21 | 22 | public override CustomResponse? InvalidModelStateWrap(ActionContext context) => new() { Code = new(ResponseState.Error, 20000) }; 23 | 24 | public override CustomResponse? NotOKStatusCodeWrap(HttpContext context) 25 | { 26 | var statusCode = context.Response.StatusCode; 27 | if (statusCode is >= 300 and < 400) 28 | { 29 | return null; 30 | } 31 | 32 | ResponseCode code; 33 | ResponseMessage message; 34 | 35 | if (context.TryGetResponseDescription(out var description)) 36 | { 37 | code = description.Code; 38 | message = description.Message; 39 | } 40 | else 41 | { 42 | code = new(ResponseState.Error, 25000); 43 | message = new() { Text = Enum.IsDefined(typeof(HttpStatusCode), statusCode) ? ((HttpStatusCode)statusCode).ToString() : null }; 44 | } 45 | 46 | return new() { Code = code, Message = message }; 47 | } 48 | 49 | #endregion Public 方法 50 | 51 | #region Protected 方法 52 | 53 | protected override CustomResponse? ActionEmptyResultWrap(ResultExecutingContext context, EmptyResult emptyResult, ResponseDescription? description) 54 | { 55 | return new() { Code = description?.Code ?? ResponseCode.Success, Message = description?.Message }; 56 | } 57 | 58 | protected override CustomResponse? ActionObjectResultWrap(ResultExecutingContext context, ObjectResult objectResult, ResponseDescription? description) 59 | { 60 | return new() { Code = description?.Code ?? ResponseCode.Success, Message = description?.Message, Data = objectResult.Value }; 61 | } 62 | 63 | #endregion Protected 方法 64 | } 65 | 66 | public class CustomResponseWrapper(IWrapTypeCreator wrapTypeCreator, IOptions optionsAccessor) 67 | : AbstractResponseWrapper, ResponseCode, ResponseMessage>(wrapTypeCreator, optionsAccessor) 68 | { 69 | 70 | #region Public 方法 71 | 72 | public override CustomResponse? ExceptionWrap(HttpContext context, Exception exception) => new() { Code = new(ResponseState.Error, 30000), Message = new() { Text = exception.Message, TraceId = Activity.Current.TraceId } }; 73 | 74 | public override CustomResponse? InvalidModelStateWrap(ActionContext context) => new() { Code = new(ResponseState.Error, 20000) }; 75 | 76 | public override CustomResponse? NotOKStatusCodeWrap(HttpContext context) 77 | { 78 | var statusCode = context.Response.StatusCode; 79 | if (statusCode is >= 300 and < 400) 80 | { 81 | return null; 82 | } 83 | 84 | ResponseCode code; 85 | ResponseMessage message; 86 | 87 | if (context.TryGetResponseDescription(out var description)) 88 | { 89 | code = description.Code; 90 | message = description.Message; 91 | } 92 | else 93 | { 94 | code = new(ResponseState.Error, 25000); 95 | message = new() { Text = Enum.IsDefined(typeof(HttpStatusCode), statusCode) ? ((HttpStatusCode)statusCode).ToString() : null }; 96 | } 97 | 98 | return new() { Code = code, Message = message }; 99 | } 100 | 101 | #endregion Public 方法 102 | 103 | #region Protected 方法 104 | 105 | protected override CustomResponse? ActionEmptyResultWrap(ResultExecutingContext context, EmptyResult emptyResult, ResponseDescription? description) 106 | { 107 | return new() { Code = description?.Code ?? ResponseCode.Success, Message = description?.Message }; 108 | } 109 | 110 | protected override CustomResponse? ActionObjectResultWrap(ResultExecutingContext context, ObjectResult objectResult, ResponseDescription? description) 111 | { 112 | return new() { Code = description?.Code ?? ResponseCode.Success, Message = description?.Message, Data = objectResult.Value }; 113 | } 114 | 115 | #endregion Protected 方法 116 | } 117 | -------------------------------------------------------------------------------- /test/ResponseAutoWrapper.TestHost/Hosts.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Hosting; 2 | using Microsoft.AspNetCore.TestHost; 3 | using Microsoft.Extensions.Hosting; 4 | 5 | namespace ResponseAutoWrapper.TestHost; 6 | 7 | public static class Hosts 8 | { 9 | #region Public 方法 10 | 11 | public static IHostBuilder CreateHostBuilder(bool useTestServer, params string[] args) 12 | { 13 | return Host.CreateDefaultBuilder(args) 14 | .ConfigureWebHostDefaults(webBuilder => 15 | { 16 | if (useTestServer) 17 | { 18 | webBuilder.UseTestServer(); 19 | } 20 | 21 | webBuilder.UseStartup(); 22 | }); 23 | } 24 | 25 | public static IHostBuilder CreateHostBuilder(bool useTestServer, params string[] args) where TStartup : BaseStartup 26 | { 27 | return Host.CreateDefaultBuilder(args) 28 | .ConfigureWebHostDefaults(webBuilder => 29 | { 30 | if (useTestServer) 31 | { 32 | webBuilder.UseTestServer(); 33 | } 34 | 35 | webBuilder.UseStartup(); 36 | }); 37 | } 38 | 39 | #endregion Public 方法 40 | } 41 | -------------------------------------------------------------------------------- /test/ResponseAutoWrapper.TestHost/InheritedLegacyResponse.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | 3 | namespace ResponseAutoWrapper.TestHost; 4 | 5 | public class InheritedLegacyCustomResponse : LegacyCustomResponse 6 | { 7 | } 8 | 9 | public class InheritedLegacyResponse : ApiResponse 10 | { 11 | #region Public 构造函数 12 | 13 | public InheritedLegacyResponse() : base(200) 14 | { 15 | } 16 | 17 | #endregion Public 构造函数 18 | } 19 | 20 | public class InheritedLegacyCustomResponseNotGeneric : LegacyCustomResponse 21 | { 22 | } 23 | -------------------------------------------------------------------------------- /test/ResponseAutoWrapper.TestHost/InheritedResponse.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | 3 | namespace ResponseAutoWrapper.TestHost; 4 | 5 | public class InheritedCustomResponse : CustomResponse 6 | { 7 | } 8 | 9 | public class InheritedResponse : GenericApiResponse 10 | { 11 | #region Public 构造函数 12 | 13 | public InheritedResponse() : base(ResponseCode.Success) 14 | { 15 | } 16 | 17 | #endregion Public 构造函数 18 | } 19 | 20 | public class InheritedCustomResponseNotGeneric : CustomResponse 21 | { 22 | } 23 | -------------------------------------------------------------------------------- /test/ResponseAutoWrapper.TestHost/InheritedTask.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading; 3 | using System.Threading.Tasks; 4 | 5 | namespace ResponseAutoWrapper.TestHost; 6 | 7 | public class InheritedTask : Task 8 | { 9 | #region Public 构造函数 10 | 11 | public InheritedTask(Func function) : base(function) 12 | { 13 | } 14 | 15 | public InheritedTask(Func function, object? state) : base(function, state) 16 | { 17 | } 18 | 19 | public InheritedTask(Func function, CancellationToken cancellationToken) : base(function, cancellationToken) 20 | { 21 | } 22 | 23 | public InheritedTask(Func function, TaskCreationOptions creationOptions) : base(function, creationOptions) 24 | { 25 | } 26 | 27 | public InheritedTask(Func function, object? state, CancellationToken cancellationToken) : base(function, state, cancellationToken) 28 | { 29 | } 30 | 31 | public InheritedTask(Func function, object? state, TaskCreationOptions creationOptions) : base(function, state, creationOptions) 32 | { 33 | } 34 | 35 | public InheritedTask(Func function, CancellationToken cancellationToken, TaskCreationOptions creationOptions) : base(function, cancellationToken, creationOptions) 36 | { 37 | } 38 | 39 | public InheritedTask(Func function, object? state, CancellationToken cancellationToken, TaskCreationOptions creationOptions) : base(function, state, cancellationToken, creationOptions) 40 | { 41 | } 42 | 43 | #endregion Public 构造函数 44 | } 45 | 46 | public class TwiceInheritedTask : InheritedTask 47 | { 48 | #region Public 构造函数 49 | 50 | public TwiceInheritedTask(Func function) : base(function) 51 | { 52 | } 53 | 54 | public TwiceInheritedTask(Func function, object? state) : base(function, state) 55 | { 56 | } 57 | 58 | public TwiceInheritedTask(Func function, CancellationToken cancellationToken) : base(function, cancellationToken) 59 | { 60 | } 61 | 62 | public TwiceInheritedTask(Func function, TaskCreationOptions creationOptions) : base(function, creationOptions) 63 | { 64 | } 65 | 66 | public TwiceInheritedTask(Func function, object? state, CancellationToken cancellationToken) : base(function, state, cancellationToken) 67 | { 68 | } 69 | 70 | public TwiceInheritedTask(Func function, object? state, TaskCreationOptions creationOptions) : base(function, state, creationOptions) 71 | { 72 | } 73 | 74 | public TwiceInheritedTask(Func function, CancellationToken cancellationToken, TaskCreationOptions creationOptions) : base(function, cancellationToken, creationOptions) 75 | { 76 | } 77 | 78 | public TwiceInheritedTask(Func function, object? state, CancellationToken cancellationToken, TaskCreationOptions creationOptions) : base(function, state, cancellationToken, creationOptions) 79 | { 80 | } 81 | 82 | #endregion Public 构造函数 83 | } 84 | -------------------------------------------------------------------------------- /test/ResponseAutoWrapper.TestHost/LegacyCustomResponse.cs: -------------------------------------------------------------------------------- 1 | namespace ResponseAutoWrapper.TestHost; 2 | 3 | public class LegacyCustomResponse 4 | { 5 | #region Public 属性 6 | 7 | public object Datas { get; set; } 8 | 9 | public string Info { get; set; } 10 | 11 | public int StatusCode { get; set; } = 200; 12 | 13 | #endregion Public 属性 14 | } 15 | 16 | public class LegacyCustomResponse 17 | { 18 | #region Public 属性 19 | 20 | public TData Datas { get; set; } 21 | 22 | public string Info { get; set; } 23 | 24 | public int StatusCode { get; set; } = 200; 25 | 26 | #endregion Public 属性 27 | } 28 | -------------------------------------------------------------------------------- /test/ResponseAutoWrapper.TestHost/LegacyCustomResponseWrapper.cs: -------------------------------------------------------------------------------- 1 | using Cuture.AspNetCore.ResponseAutoWrapper; 2 | 3 | using Microsoft.Extensions.Options; 4 | 5 | namespace ResponseAutoWrapper.TestHost; 6 | 7 | public class NotGenericLegacyCustomResponseWrapper(IWrapTypeCreator wrapTypeCreator, IOptions optionsAccessor, IOptions wrapperOptionsAccessor) 8 | : LegacyCompatibleResponseWrapper(wrapTypeCreator, optionsAccessor, wrapperOptionsAccessor) 9 | { 10 | 11 | #region Protected 方法 12 | 13 | protected override LegacyCustomResponse? CreateResponse(int code) => new() { StatusCode = code }; 14 | 15 | protected override LegacyCustomResponse? CreateResponse(int code, string? message) => new() { StatusCode = code, Info = message }; 16 | 17 | protected override LegacyCustomResponse? CreateResponse(int code, string? message, object? data) => new() { StatusCode = code, Info = message, Datas = data }; 18 | 19 | #endregion Protected 方法 20 | } 21 | 22 | public class LegacyCustomResponseWrapper(IWrapTypeCreator wrapTypeCreator, IOptions optionsAccessor, IOptions wrapperOptionsAccessor) 23 | : LegacyCompatibleResponseWrapper>(wrapTypeCreator, optionsAccessor, wrapperOptionsAccessor) 24 | { 25 | 26 | #region Protected 方法 27 | 28 | protected override LegacyCustomResponse? CreateResponse(int code) => new() { StatusCode = code }; 29 | 30 | protected override LegacyCustomResponse? CreateResponse(int code, string? message) => new() { StatusCode = code, Info = message }; 31 | 32 | protected override LegacyCustomResponse? CreateResponse(int code, string? message, object? data) => new() { StatusCode = code, Info = message, Datas = data }; 33 | 34 | #endregion Protected 方法 35 | } 36 | -------------------------------------------------------------------------------- /test/ResponseAutoWrapper.TestHost/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using System.Reflection; 4 | 5 | using Microsoft.AspNetCore.Hosting; 6 | using Microsoft.Extensions.Hosting; 7 | 8 | namespace ResponseAutoWrapper.TestHost; 9 | 10 | public class Program 11 | { 12 | #region Public 方法 13 | 14 | //保留CreateHostBuilder,避免某些情况下编译失败 15 | public static IHostBuilder CreateHostBuilder(params string[] args) => Hosts.CreateHostBuilder(false, args); 16 | 17 | public static void Main(string[] args) 18 | { 19 | var startupName = args.Length > 0 20 | ? args[0].Trim() 21 | : "DefaultStartup"; 22 | 23 | var startupType = Assembly.GetExecutingAssembly() 24 | .GetTypes() 25 | .Where(m => m.Name == startupName) 26 | .FirstOrDefault()!; 27 | 28 | Console.WriteLine($"Running with - {startupType.FullName}"); 29 | 30 | var runMethod = typeof(Program).GetMethod("RunWithStartup", BindingFlags.Static | BindingFlags.NonPublic)!; 31 | 32 | runMethod.MakeGenericMethod(startupType) 33 | .Invoke(null, new object[] { args }); 34 | } 35 | 36 | #endregion Public 方法 37 | 38 | #region Private 方法 39 | 40 | private static void RunWithStartup(string[] args) where TStartup : BaseStartup 41 | { 42 | Hosts.CreateHostBuilder(false, args).Build().Run(); 43 | } 44 | 45 | #endregion Private 方法 46 | } 47 | -------------------------------------------------------------------------------- /test/ResponseAutoWrapper.TestHost/Properties/launchSettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "profiles": { 3 | "TestHost.DefaultStartup": { 4 | "commandName": "Project", 5 | "commandLineArgs": "DefaultStartup", 6 | "environmentVariables": { 7 | "ASPNETCORE_ENVIRONMENT": "Development" 8 | }, 9 | "applicationUrl": "http://localhost:5000" 10 | }, 11 | "TestHost.NoWrapperStartup": { 12 | "commandName": "Project", 13 | "commandLineArgs": "NoWrapperStartup", 14 | "environmentVariables": { 15 | "ASPNETCORE_ENVIRONMENT": "Development" 16 | }, 17 | "applicationUrl": "http://localhost:5000" 18 | }, 19 | "TestHost.MiddlewareExceptionStartup": { 20 | "commandName": "Project", 21 | "commandLineArgs": "MiddlewareExceptionStartup", 22 | "environmentVariables": { 23 | "ASPNETCORE_ENVIRONMENT": "Development" 24 | }, 25 | "applicationUrl": "http://localhost:5000" 26 | }, 27 | "TestHost.LCRStartup": { 28 | "commandName": "Project", 29 | "commandLineArgs": "LCRStartup", 30 | "environmentVariables": { 31 | "ASPNETCORE_ENVIRONMENT": "Development" 32 | }, 33 | "applicationUrl": "http://localhost:5000" 34 | }, 35 | "TestHost.CRStartup": { 36 | "commandName": "Project", 37 | "commandLineArgs": "CRStartup", 38 | "environmentVariables": { 39 | "ASPNETCORE_ENVIRONMENT": "Development" 40 | }, 41 | "applicationUrl": "http://localhost:5000" 42 | }, 43 | "TestHost.DisableOpenAPISupportStartup": { 44 | "commandName": "Project", 45 | "commandLineArgs": "DisableOpenAPISupportStartup", 46 | "environmentVariables": { 47 | "ASPNETCORE_ENVIRONMENT": "Development" 48 | }, 49 | "applicationUrl": "http://localhost:5000" 50 | }, 51 | "TestHost.NotGenericLCRStartup": { 52 | "commandName": "Project", 53 | "commandLineArgs": "NotGenericLCRStartup", 54 | "environmentVariables": { 55 | "ASPNETCORE_ENVIRONMENT": "Development" 56 | }, 57 | "applicationUrl": "http://localhost:5000" 58 | }, 59 | "TestHost.NotGenericCRStartup": { 60 | "commandName": "Project", 61 | "commandLineArgs": "NotGenericCRStartup", 62 | "environmentVariables": { 63 | "ASPNETCORE_ENVIRONMENT": "Development" 64 | }, 65 | "applicationUrl": "http://localhost:5000" 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /test/ResponseAutoWrapper.TestHost/ResponseAutoWrapper.TestHost.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | false 5 | $(NoWarn);CS8600;CS8604;CS8601;CS8618;CS0162 6 | 7 | false 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /test/ResponseAutoWrapper.TestHost/Startups/BaseStartup.cs: -------------------------------------------------------------------------------- 1 | using System.Text; 2 | using System.Threading.Tasks; 3 | 4 | using Microsoft.AspNetCore.Authentication.Cookies; 5 | using Microsoft.AspNetCore.Authentication.JwtBearer; 6 | using Microsoft.AspNetCore.Builder; 7 | using Microsoft.AspNetCore.Http; 8 | using Microsoft.Extensions.Configuration; 9 | using Microsoft.Extensions.DependencyInjection; 10 | using Microsoft.IdentityModel.Tokens; 11 | using Microsoft.OpenApi.Models; 12 | 13 | namespace ResponseAutoWrapper.TestHost; 14 | 15 | public abstract class BaseStartup(IConfiguration configuration) 16 | { 17 | #region Public 属性 18 | 19 | public IConfiguration Configuration { get; } = configuration; 20 | 21 | #endregion Public 属性 22 | 23 | #region Public 方法 24 | 25 | public virtual void Configure(IApplicationBuilder app) 26 | { 27 | app.UseSwagger(); 28 | 29 | app.UseSwaggerUI(c => c.SwaggerEndpoint("/swagger/v1/swagger.json", "ResponseAutoWrapper.TestHost v1")); 30 | 31 | app.UseRouting(); 32 | 33 | app.UseAuthentication(); 34 | 35 | app.UseAuthorization(); 36 | 37 | app.UseEndpoints(endpoints => 38 | { 39 | endpoints.MapControllers(); 40 | }); 41 | } 42 | 43 | public virtual void ConfigureServices(IServiceCollection services) 44 | { 45 | services.AddControllers(); 46 | 47 | services.AddSwaggerGen(c => 48 | { 49 | c.SwaggerDoc("v1", new OpenApiInfo { Title = "ResponseAutoWrapper.TestHost", Version = "v1" }); 50 | }); 51 | 52 | services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme) 53 | .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, options => 54 | { 55 | //覆盖默认的认证相关操作,改为修改状态码 56 | options.Events.OnRedirectToAccessDenied = ctx => { ctx.HttpContext.Response.StatusCode = StatusCodes.Status403Forbidden; return Task.CompletedTask; }; 57 | options.Events.OnRedirectToLogin = ctx => { ctx.HttpContext.Response.StatusCode = StatusCodes.Status401Unauthorized; return Task.CompletedTask; }; 58 | options.Events.OnRedirectToLogout = _ => Task.CompletedTask; 59 | options.Events.OnRedirectToReturnUrl = _ => Task.CompletedTask; 60 | }) 61 | .AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options => 62 | { 63 | //覆盖默认的认证相关操作,改为修改状态码 64 | options.Events = new JwtBearerEvents 65 | { 66 | OnAuthenticationFailed = ctx => { ctx.HttpContext.Response.StatusCode = StatusCodes.Status401Unauthorized; return Task.CompletedTask; }, 67 | OnChallenge = ctx => { ctx.HttpContext.Response.StatusCode = StatusCodes.Status401Unauthorized; return Task.CompletedTask; }, 68 | OnForbidden = ctx => { ctx.HttpContext.Response.StatusCode = StatusCodes.Status403Forbidden; return Task.CompletedTask; } 69 | }; 70 | 71 | options.RequireHttpsMetadata = false; 72 | options.SaveToken = true; 73 | options.TokenValidationParameters = new TokenValidationParameters 74 | { 75 | ValidateIssuerSigningKey = true, 76 | IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes("123456789123456789_123456789123456789")), 77 | ValidIssuer = "Issuer", 78 | ValidAudience = "Audience", 79 | ValidateIssuer = false, 80 | ValidateAudience = false 81 | }; 82 | }); 83 | 84 | services.AddAuthorization(options => 85 | { 86 | options.AddPolicy("CanAccess", p => p.RequireClaim("CanAccess", "1")); 87 | }); 88 | } 89 | 90 | #endregion Public 方法 91 | } 92 | -------------------------------------------------------------------------------- /test/ResponseAutoWrapper.TestHost/Startups/CRStartup.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Builder; 2 | using Microsoft.Extensions.Configuration; 3 | using Microsoft.Extensions.DependencyInjection; 4 | 5 | namespace ResponseAutoWrapper.TestHost; 6 | 7 | public class CRStartup(IConfiguration configuration) : BaseStartup(configuration) 8 | { 9 | 10 | #region Public 方法 11 | 12 | public override void Configure(IApplicationBuilder app) 13 | { 14 | app.UseResponseAutoWrapper(); 15 | 16 | base.Configure(app); 17 | } 18 | 19 | public override void ConfigureServices(IServiceCollection services) 20 | { 21 | base.ConfigureServices(services); 22 | 23 | services.AddResponseAutoWrapper, ResponseCode, ResponseMessage>() 24 | .ConfigureWrappers(builder => builder.AddWrappers()); 25 | } 26 | 27 | #endregion Public 方法 28 | } 29 | -------------------------------------------------------------------------------- /test/ResponseAutoWrapper.TestHost/Startups/DefaultStartup.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Builder; 2 | using Microsoft.Extensions.Configuration; 3 | using Microsoft.Extensions.DependencyInjection; 4 | 5 | namespace ResponseAutoWrapper.TestHost; 6 | 7 | public class DefaultStartup(IConfiguration configuration) : BaseStartup(configuration) 8 | { 9 | 10 | #region Public 方法 11 | 12 | public override void Configure(IApplicationBuilder app) 13 | { 14 | app.UseResponseAutoWrapper(); 15 | 16 | base.Configure(app); 17 | } 18 | 19 | public override void ConfigureServices(IServiceCollection services) 20 | { 21 | base.ConfigureServices(services); 22 | 23 | services.AddResponseAutoWrapper(); 24 | } 25 | 26 | #endregion Public 方法 27 | } 28 | -------------------------------------------------------------------------------- /test/ResponseAutoWrapper.TestHost/Startups/DisableOpenAPISupportStartup.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Builder; 2 | using Microsoft.Extensions.Configuration; 3 | using Microsoft.Extensions.DependencyInjection; 4 | 5 | namespace ResponseAutoWrapper.TestHost; 6 | 7 | public class DisableOpenAPISupportStartup(IConfiguration configuration) : BaseStartup(configuration) 8 | { 9 | 10 | #region Public 方法 11 | 12 | public override void Configure(IApplicationBuilder app) 13 | { 14 | app.UseResponseAutoWrapper(); 15 | 16 | base.Configure(app); 17 | } 18 | 19 | public override void ConfigureServices(IServiceCollection services) 20 | { 21 | base.ConfigureServices(services); 22 | 23 | services.AddResponseAutoWrapper(options => options.DisableOpenAPISupport = true); 24 | } 25 | 26 | #endregion Public 方法 27 | } 28 | -------------------------------------------------------------------------------- /test/ResponseAutoWrapper.TestHost/Startups/EmptyStartup.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Builder; 2 | using Microsoft.Extensions.Configuration; 3 | 4 | namespace ResponseAutoWrapper.TestHost; 5 | 6 | public class EmptyStartup(IConfiguration configuration) : BaseStartup(configuration) 7 | { 8 | 9 | #region Public 方法 10 | 11 | public override void Configure(IApplicationBuilder app) 12 | { 13 | app.UseResponseAutoWrapper(); 14 | 15 | base.Configure(app); 16 | } 17 | 18 | #endregion Public 方法 19 | } 20 | -------------------------------------------------------------------------------- /test/ResponseAutoWrapper.TestHost/Startups/LCRStartup.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Builder; 2 | using Microsoft.Extensions.Configuration; 3 | using Microsoft.Extensions.DependencyInjection; 4 | 5 | namespace ResponseAutoWrapper.TestHost; 6 | 7 | public class LCRStartup(IConfiguration configuration) : BaseStartup(configuration) 8 | { 9 | 10 | #region Public 方法 11 | 12 | public override void Configure(IApplicationBuilder app) 13 | { 14 | app.UseResponseAutoWrapper(); 15 | 16 | base.Configure(app); 17 | } 18 | 19 | public override void ConfigureServices(IServiceCollection services) 20 | { 21 | base.ConfigureServices(services); 22 | 23 | services.AddResponseAutoWrapper, int, string>() 24 | .ConfigureWrappers(builder => builder.AddLegacyWrappers()); 25 | } 26 | 27 | #endregion Public 方法 28 | } 29 | -------------------------------------------------------------------------------- /test/ResponseAutoWrapper.TestHost/Startups/MiddlewareException/CustomResponseByResponseCreatorMiddlewareExceptionStartup.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | using Microsoft.AspNetCore.Builder; 4 | using Microsoft.Extensions.Configuration; 5 | using Microsoft.Extensions.DependencyInjection; 6 | 7 | namespace ResponseAutoWrapper.TestHost; 8 | 9 | public class CustomResponseByResponseCreatorMiddlewareExceptionStartup(IConfiguration configuration) : BaseStartup(configuration) 10 | { 11 | 12 | #region Public 方法 13 | 14 | public override void Configure(IApplicationBuilder app) 15 | { 16 | app.UseResponseAutoWrapper(); 17 | 18 | app.Use(async (context, next) => 19 | { 20 | throw new Exception("Middleware throw Exception"); 21 | await next(); 22 | }); 23 | 24 | base.Configure(app); 25 | } 26 | 27 | public override void ConfigureServices(IServiceCollection services) 28 | { 29 | base.ConfigureServices(services); 30 | 31 | services.AddResponseAutoWrapper, int, string>() 32 | .ConfigureWrappers(builder => builder.AddLegacyWrappers()); 33 | } 34 | 35 | #endregion Public 方法 36 | } 37 | -------------------------------------------------------------------------------- /test/ResponseAutoWrapper.TestHost/Startups/MiddlewareException/MiddlewareExceptionStartup.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | using Microsoft.AspNetCore.Builder; 4 | using Microsoft.Extensions.Configuration; 5 | using Microsoft.Extensions.DependencyInjection; 6 | 7 | namespace ResponseAutoWrapper.TestHost; 8 | 9 | public class MiddlewareExceptionStartup(IConfiguration configuration) : BaseStartup(configuration) 10 | { 11 | 12 | #region Public 方法 13 | 14 | public override void Configure(IApplicationBuilder app) 15 | { 16 | app.UseResponseAutoWrapper(); 17 | 18 | app.Use(async (context, next) => 19 | { 20 | throw new Exception("Middleware throw Exception"); 21 | await next(); 22 | }); 23 | 24 | base.Configure(app); 25 | } 26 | 27 | public override void ConfigureServices(IServiceCollection services) 28 | { 29 | base.ConfigureServices(services); 30 | 31 | services.AddResponseAutoWrapper(); 32 | } 33 | 34 | #endregion Public 方法 35 | } 36 | -------------------------------------------------------------------------------- /test/ResponseAutoWrapper.TestHost/Startups/NoWrapperStartup.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Configuration; 2 | 3 | namespace ResponseAutoWrapper.TestHost; 4 | 5 | public class NoWrapperStartup(IConfiguration configuration) : BaseStartup(configuration) 6 | { 7 | } 8 | -------------------------------------------------------------------------------- /test/ResponseAutoWrapper.TestHost/Startups/NotGenericCRStartup.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Builder; 2 | using Microsoft.Extensions.Configuration; 3 | using Microsoft.Extensions.DependencyInjection; 4 | 5 | namespace ResponseAutoWrapper.TestHost; 6 | 7 | public class NotGenericCRStartup(IConfiguration configuration) : BaseStartup(configuration) 8 | { 9 | 10 | #region Public 方法 11 | 12 | public override void Configure(IApplicationBuilder app) 13 | { 14 | app.UseResponseAutoWrapper(); 15 | 16 | base.Configure(app); 17 | } 18 | 19 | public override void ConfigureServices(IServiceCollection services) 20 | { 21 | base.ConfigureServices(services); 22 | 23 | services.AddResponseAutoWrapper(options => options.DisableOpenAPISupport = true) 24 | .ConfigureWrappers(builder => builder.AddWrappers()); 25 | } 26 | 27 | #endregion Public 方法 28 | } 29 | -------------------------------------------------------------------------------- /test/ResponseAutoWrapper.TestHost/Startups/NotGenericLCRStartup.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Builder; 2 | using Microsoft.Extensions.Configuration; 3 | using Microsoft.Extensions.DependencyInjection; 4 | 5 | namespace ResponseAutoWrapper.TestHost; 6 | 7 | public class NotGenericLCRStartup(IConfiguration configuration) : BaseStartup(configuration) 8 | { 9 | 10 | #region Public 方法 11 | 12 | public override void Configure(IApplicationBuilder app) 13 | { 14 | app.UseResponseAutoWrapper(); 15 | 16 | base.Configure(app); 17 | } 18 | 19 | public override void ConfigureServices(IServiceCollection services) 20 | { 21 | base.ConfigureServices(services); 22 | 23 | services.AddResponseAutoWrapper(options => options.DisableOpenAPISupport = true) 24 | .ConfigureWrappers(builder => builder.AddLegacyWrappers()); 25 | } 26 | 27 | #endregion Public 方法 28 | } 29 | -------------------------------------------------------------------------------- /test/ResponseAutoWrapper.TestHost/WeatherForecast.cs: -------------------------------------------------------------------------------- 1 | // 2 | using System; 3 | using System.Linq; 4 | 5 | namespace ResponseAutoWrapper.TestHost; 6 | 7 | public class WeatherForecast 8 | { 9 | private static readonly string[] s_summaries = new[] 10 | { 11 | "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" 12 | }; 13 | 14 | public DateTime Date { get; set; } 15 | 16 | public int TemperatureC { get; set; } 17 | 18 | public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); 19 | 20 | public string? Summary { get; set; } 21 | 22 | public static WeatherForecast[] GenerateData(int count = 5) 23 | { 24 | var rng = new Random(); 25 | return Enumerable.Range(1, count).Select(index => new WeatherForecast 26 | { 27 | Date = DateTime.Now.AddDays(index), 28 | TemperatureC = rng.Next(-20, 55), 29 | Summary = s_summaries[rng.Next(s_summaries.Length)] 30 | }) 31 | .ToArray(); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /test/ResponseAutoWrapper.TestHost/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Information", 5 | "Microsoft": "Information", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /test/ResponseAutoWrapper.TestHost/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "Logging": { 3 | "LogLevel": { 4 | "Default": "Warning", 5 | "Microsoft": "Warning", 6 | "Microsoft.Hosting.Lifetime": "Information" 7 | } 8 | }, 9 | "AllowedHosts": "*" 10 | } 11 | --------------------------------------------------------------------------------