├── .github ├── FUNDING.yml └── workflows │ └── dotnetcore.yml ├── .gitignore ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── README.zh-cn.md ├── RELEASE.MD └── src ├── AutoWrapper.Test ├── AutoWrapper.Test.csproj ├── AutoWrapperMiddlewareTests.cs ├── Helper │ └── JsonHelper.cs └── Models │ ├── MapResponseCustomErrorObject.cs │ ├── MapResponseObject.cs │ └── MyCustomApiResponse.cs ├── AutoWrapper.sln └── AutoWrapper ├── AutoWrapper.cs ├── AutoWrapper.csproj ├── AutoWrapperExcludePath.cs ├── AutoWrapperMembers.cs ├── AutoWrapperMiddleware.cs ├── AutoWrapperOptions.cs ├── AutoWrapperPropertyAttribute.cs ├── Base ├── OptionBase.cs └── WrapperBase.cs ├── Extensions ├── JsonExtension.cs ├── ModelStateExtension.cs ├── StringExtension.cs └── TypeConverterExtension.cs ├── Filters ├── AutoWrapIgnore.cs └── RequestDataLogIgnore.cs ├── Helpers ├── CustomContractResolver.cs ├── JsonHelper.cs ├── JsonSettings.cs ├── ResponseMessage.cs └── TypeIdentifier.cs ├── Models ├── ApiProblemDetailsExceptionResponse.cs ├── ApiProblemDetailsResponse.cs ├── ApiProblemDetailsValidationErrorResponse.cs └── ApiResultResponse.cs ├── Wrappers ├── ApiError.cs ├── ApiException.cs ├── ApiProblemDetails.cs ├── ApiProblemDetailsException.cs ├── ApiProblemDetailsMember.cs ├── ApiResponse.cs └── ValidationError.cs ├── icon.png └── logo.png /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [proudmonkey] 4 | patreon: # Replace with a single Patreon username 5 | tidelift: # nuget/AutoWrapper.Core 6 | custom: ["https://www.paypal.me/vmsdurano", "https://www.buymeacoffee.com/ImP9gONBW"] 7 | -------------------------------------------------------------------------------- /.github/workflows/dotnetcore.yml: -------------------------------------------------------------------------------- 1 | name: .NET Core 2 | 3 | on: 4 | push: 5 | branches: [ master ] 6 | pull_request: 7 | branches: [ master ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v2 16 | - name: Setup .NET Core 17 | uses: actions/setup-dotnet@v1 18 | with: 19 | dotnet-version: 3.1.101 20 | - name: Build with dotnet 21 | run: dotnet build src --configuration Release 22 | - name: Test with dotnet 23 | run : dotnet test src 24 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 |  2 | # Created by https://www.gitignore.io/api/aspnetcore,visualstudio 3 | 4 | ### ASPNETCore ### 5 | ## Ignore Visual Studio temporary files, build results, and 6 | ## files generated by popular Visual Studio add-ons. 7 | 8 | # User-specific files 9 | *.suo 10 | *.user 11 | *.userosscache 12 | *.sln.docstates 13 | 14 | # User-specific files (MonoDevelop/Xamarin Studio) 15 | *.userprefs 16 | 17 | # Build results 18 | [Dd]ebug/ 19 | [Dd]ebugPublic/ 20 | [Rr]elease/ 21 | [Rr]eleases/ 22 | x64/ 23 | x86/ 24 | bld/ 25 | [Bb]in/ 26 | [Oo]bj/ 27 | [Ll]og/ 28 | 29 | # Visual Studio 2015 cache/options directory 30 | .vs/ 31 | # Uncomment if you have tasks that create the project's static files in wwwroot 32 | #wwwroot/ 33 | 34 | # MSTest test Results 35 | [Tt]est[Rr]esult*/ 36 | [Bb]uild[Ll]og.* 37 | 38 | # NUNIT 39 | *.VisualState.xml 40 | TestResult.xml 41 | 42 | # Build Results of an ATL Project 43 | [Dd]ebugPS/ 44 | [Rr]eleasePS/ 45 | dlldata.c 46 | 47 | # DNX 48 | project.lock.json 49 | project.fragment.lock.json 50 | artifacts/ 51 | 52 | *_i.c 53 | *_p.c 54 | *_i.h 55 | *.ilk 56 | *.meta 57 | *.obj 58 | *.pch 59 | *.pdb 60 | *.pgc 61 | *.pgd 62 | *.rsp 63 | *.sbr 64 | *.tlb 65 | *.tli 66 | *.tlh 67 | *.tmp 68 | *.tmp_proj 69 | *.log 70 | *.vspscc 71 | *.vssscc 72 | .builds 73 | *.pidb 74 | *.svclog 75 | *.scc 76 | 77 | # Chutzpah Test files 78 | _Chutzpah* 79 | 80 | # Visual C++ cache files 81 | ipch/ 82 | *.aps 83 | *.ncb 84 | *.opendb 85 | *.opensdf 86 | *.sdf 87 | *.cachefile 88 | *.VC.db 89 | *.VC.VC.opendb 90 | 91 | # Visual Studio profiler 92 | *.psess 93 | *.vsp 94 | *.vspx 95 | *.sap 96 | 97 | # TFS 2012 Local Workspace 98 | $tf/ 99 | 100 | # Guidance Automation Toolkit 101 | *.gpState 102 | 103 | # ReSharper is a .NET coding add-in 104 | _ReSharper*/ 105 | *.[Rr]e[Ss]harper 106 | *.DotSettings.user 107 | 108 | # JustCode is a .NET coding add-in 109 | .JustCode 110 | 111 | # TeamCity is a build add-in 112 | _TeamCity* 113 | 114 | # DotCover is a Code Coverage Tool 115 | *.dotCover 116 | 117 | # Visual Studio code coverage results 118 | *.coverage 119 | *.coveragexml 120 | 121 | # NCrunch 122 | _NCrunch_* 123 | .*crunch*.local.xml 124 | nCrunchTemp_* 125 | 126 | # MightyMoose 127 | *.mm.* 128 | AutoTest.Net/ 129 | 130 | # Web workbench (sass) 131 | .sass-cache/ 132 | 133 | # Installshield output folder 134 | [Ee]xpress/ 135 | 136 | # DocProject is a documentation generator add-in 137 | DocProject/buildhelp/ 138 | DocProject/Help/*.HxT 139 | DocProject/Help/*.HxC 140 | DocProject/Help/*.hhc 141 | DocProject/Help/*.hhk 142 | DocProject/Help/*.hhp 143 | DocProject/Help/Html2 144 | DocProject/Help/html 145 | 146 | # Click-Once directory 147 | publish/ 148 | 149 | # Publish Web Output 150 | *.[Pp]ublish.xml 151 | *.azurePubxml 152 | # TODO: Comment the next line if you want to checkin your web deploy settings 153 | # but database connection strings (with potential passwords) will be unencrypted 154 | *.pubxml 155 | *.publishproj 156 | 157 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 158 | # checkin your Azure Web App publish settings, but sensitive information contained 159 | # in these scripts will be unencrypted 160 | PublishScripts/ 161 | 162 | # NuGet Packages 163 | *.nupkg 164 | # The packages folder can be ignored because of Package Restore 165 | **/packages/* 166 | # except build/, which is used as an MSBuild target. 167 | !**/packages/build/ 168 | # Uncomment if necessary however generally it will be regenerated when needed 169 | #!**/packages/repositories.config 170 | # NuGet v3's project.json files produces more ignoreable files 171 | *.nuget.props 172 | *.nuget.targets 173 | 174 | # Microsoft Azure Build Output 175 | csx/ 176 | *.build.csdef 177 | 178 | # Microsoft Azure Emulator 179 | ecf/ 180 | rcf/ 181 | 182 | # Windows Store app package directories and files 183 | AppPackages/ 184 | BundleArtifacts/ 185 | Package.StoreAssociation.xml 186 | _pkginfo.txt 187 | 188 | # Visual Studio cache files 189 | # files ending in .cache can be ignored 190 | *.[Cc]ache 191 | # but keep track of directories ending in .cache 192 | !*.[Cc]ache/ 193 | 194 | # Others 195 | ClientBin/ 196 | ~$* 197 | *~ 198 | *.dbmdl 199 | *.dbproj.schemaview 200 | *.jfm 201 | *.pfx 202 | *.publishsettings 203 | node_modules/ 204 | orleans.codegen.cs 205 | 206 | # Since there are multiple workflows, uncomment next line to ignore bower_components 207 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 208 | #bower_components/ 209 | 210 | # RIA/Silverlight projects 211 | Generated_Code/ 212 | 213 | # Backup & report files from converting an old project file 214 | # to a newer Visual Studio version. Backup files are not needed, 215 | # because we have git ;-) 216 | _UpgradeReport_Files/ 217 | Backup*/ 218 | UpgradeLog*.XML 219 | UpgradeLog*.htm 220 | 221 | # SQL Server files 222 | *.mdf 223 | *.ldf 224 | 225 | # Business Intelligence projects 226 | *.rdl.data 227 | *.bim.layout 228 | *.bim_*.settings 229 | 230 | # Microsoft Fakes 231 | FakesAssemblies/ 232 | 233 | # GhostDoc plugin setting file 234 | *.GhostDoc.xml 235 | 236 | # Node.js Tools for Visual Studio 237 | .ntvs_analysis.dat 238 | 239 | # Visual Studio 6 build log 240 | *.plg 241 | 242 | # Visual Studio 6 workspace options file 243 | *.opt 244 | 245 | # Visual Studio LightSwitch build output 246 | **/*.HTMLClient/GeneratedArtifacts 247 | **/*.DesktopClient/GeneratedArtifacts 248 | **/*.DesktopClient/ModelManifest.xml 249 | **/*.Server/GeneratedArtifacts 250 | **/*.Server/ModelManifest.xml 251 | _Pvt_Extensions 252 | 253 | # Paket dependency manager 254 | .paket/paket.exe 255 | paket-files/ 256 | 257 | # FAKE - F# Make 258 | .fake/ 259 | 260 | # JetBrains Rider 261 | .idea/ 262 | *.sln.iml 263 | 264 | # CodeRush 265 | .cr/ 266 | 267 | # Python Tools for Visual Studio (PTVS) 268 | __pycache__/ 269 | *.pyc 270 | 271 | # Cake - Uncomment if you are using it 272 | # tools/ 273 | 274 | ### VisualStudio ### 275 | ## Ignore Visual Studio temporary files, build results, and 276 | ## files generated by popular Visual Studio add-ons. 277 | ## 278 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 279 | 280 | # User-specific files 281 | 282 | # User-specific files (MonoDevelop/Xamarin Studio) 283 | 284 | # Build results 285 | 286 | # Visual Studio 2015 cache/options directory 287 | # Uncomment if you have tasks that create the project's static files in wwwroot 288 | #wwwroot/ 289 | 290 | # MSTest test Results 291 | 292 | # NUNIT 293 | 294 | # Build Results of an ATL Project 295 | 296 | # .NET Core 297 | **/Properties/launchSettings.json 298 | 299 | 300 | # Chutzpah Test files 301 | 302 | # Visual C++ cache files 303 | 304 | # Visual Studio profiler 305 | 306 | # TFS 2012 Local Workspace 307 | 308 | # Guidance Automation Toolkit 309 | 310 | # ReSharper is a .NET coding add-in 311 | 312 | # JustCode is a .NET coding add-in 313 | 314 | # TeamCity is a build add-in 315 | 316 | # DotCover is a Code Coverage Tool 317 | 318 | # Visual Studio code coverage results 319 | 320 | # NCrunch 321 | 322 | # MightyMoose 323 | 324 | # Web workbench (sass) 325 | 326 | # Installshield output folder 327 | 328 | # DocProject is a documentation generator add-in 329 | 330 | # Click-Once directory 331 | 332 | # Publish Web Output 333 | # TODO: Uncomment the next line to ignore your web deploy settings. 334 | # By default, sensitive information, such as encrypted password 335 | # should be stored in the .pubxml.user file. 336 | #*.pubxml 337 | *.pubxml.user 338 | 339 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 340 | # checkin your Azure Web App publish settings, but sensitive information contained 341 | # in these scripts will be unencrypted 342 | 343 | # NuGet Packages 344 | # The packages folder can be ignored because of Package Restore 345 | # except build/, which is used as an MSBuild target. 346 | # Uncomment if necessary however generally it will be regenerated when needed 347 | #!**/packages/repositories.config 348 | # NuGet v3's project.json files produces more ignorable files 349 | 350 | # Microsoft Azure Build Output 351 | 352 | # Microsoft Azure Emulator 353 | 354 | # Windows Store app package directories and files 355 | 356 | # Visual Studio cache files 357 | # files ending in .cache can be ignored 358 | # but keep track of directories ending in .cache 359 | 360 | # Others 361 | 362 | # Since there are multiple workflows, uncomment next line to ignore bower_components 363 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 364 | #bower_components/ 365 | 366 | # RIA/Silverlight projects 367 | 368 | # Backup & report files from converting an old project file 369 | # to a newer Visual Studio version. Backup files are not needed, 370 | # because we have git ;-) 371 | 372 | # SQL Server files 373 | *.ndf 374 | 375 | # Business Intelligence projects 376 | 377 | # Microsoft Fakes 378 | 379 | # GhostDoc plugin setting file 380 | 381 | # Node.js Tools for Visual Studio 382 | 383 | # Typescript v1 declaration files 384 | typings/ 385 | 386 | # Visual Studio 6 build log 387 | 388 | # Visual Studio 6 workspace options file 389 | 390 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 391 | *.vbw 392 | 393 | # Visual Studio LightSwitch build output 394 | 395 | # Paket dependency manager 396 | 397 | # FAKE - F# Make 398 | 399 | # JetBrains Rider 400 | 401 | # CodeRush 402 | 403 | # Python Tools for Visual Studio (PTVS) 404 | 405 | # Cake - Uncomment if you are using it 406 | # tools/** 407 | # !tools/packages.config 408 | 409 | # Telerik's JustMock configuration file 410 | *.jmconfig 411 | 412 | # BizTalk build output 413 | *.btp.cs 414 | *.btm.cs 415 | *.odx.cs 416 | *.xsd.cs 417 | 418 | ### VisualStudio Patch ### 419 | # By default, sensitive information, such as encrypted password 420 | # should be stored in the .pubxml.user file. 421 | 422 | /README.md.bak 423 | /RELEASE.MD.bak 424 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to AutoWrapper 2 | 3 | Your feedback and contribution is very valuable to improve this project better! If you'd like to contribute, please follow the steps below: 4 | 5 | 1. Submit a Github Issue and let's discuss what changes / features you want to make. 6 | 2. Fork the Repo. 7 | 3. Create your own feature branch from the master. 8 | 4. Add your changes and and make sure to unit test it. 9 | 5. Push changes in your feature branch. 10 | 6. Use the following tag when committing you changes: 11 | 12 | * `[FET]` for New features 13 | * `[RFT]` for Code Enhancements / Refactor 14 | * `[FIX]` for Bug Fixes 15 | * `[MOD]` for Documentation, Configuration and Package updates 16 | 17 | For example: 18 | `[FET] Update code in AutoWrapperBase class to include validation for blah...` 19 | 7. Submit your PR. 20 | 21 | # License 22 | By contributing, you agree that your contributions will be licensed under its [MIT](https://github.com/proudmonkey/AutoWrapper/blob/master/LICENSE) License. 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Vincent Maverick Durano 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 | 2 | 3 | # AutoWrapper [![Nuget](https://img.shields.io/nuget/v/AutoWrapper.Core?color=blue)](https://www.nuget.org/packages/AutoWrapper.Core) [![Nuget downloads](https://img.shields.io/nuget/dt/AutoWrapper.Core?color=green)](https://www.nuget.org/packages/AutoWrapper.Core) ![.NET Core](https://github.com/proudmonkey/AutoWrapper/workflows/.NET%20Core/badge.svg) 4 | 5 | Language: English | [中文](README.zh-cn.md) 6 | 7 | `AutoWrapper` is a simple, yet customizable global `HTTP` exception handler and response wrapper for ASP.NET Core APIs. It uses an ASP.NET Core `middleware` to intercept incoming `HTTP` requests and automatically wraps the responses for you by providing a consistent response format for both successful and error results. The goal is to let you focus on your business code specific requirements and let the wrapper automatically handle the `HTTP` response. This can speedup the development time when building your APIs while enforcing own standards for your `HTTP` responses. 8 | 9 | #### Main features: 10 | 11 | * Exception handling. 12 | * `ModelState` validation error handling (support both `Data Annotation` and `FluentValidation`). 13 | * A configurable `API` exception. 14 | * A consistent response format for `Result` and `Errors`. 15 | * A detailed `Result` response. 16 | * A detailed `Error` response. 17 | * A configurable `HTTP` `StatusCodes` and messages. 18 | * Add support for `Swagger`. 19 | * Add Logging support for `Request`, `Response` and `Exceptions`. 20 | * A configurable middleware `options` to configure the wrapper. See **Options** section below for details. 21 | * Enable property name mappings for the default `ApiResponse` properties. 22 | * Add support for implementing your own user-defined `Response` and `Error` schema / object. 23 | * Add support for Problem Details exception format. 24 | * Add support for ignoring action methods that don't need to be wrapped using `[AutoWrapIgnore]` filter attribute. 25 | * V3.x enable backwards compatibility support for `netcoreapp2.1` and `netcoreapp2.2` .NET Core frameworks. 26 | * Add `ExcludePaths` option to enable support for `SignalR` and `dapr` routes。 27 | 28 | # Installation 29 | 1. Download and Install the latest `AutoWrapper.Core` from NuGet or via CLI: 30 | 31 | ``` 32 | PM> Install-Package AutoWrapper.Core -Version 4.5.1 33 | ``` 34 | 35 | 2. Declare the following namespace within `Startup.cs` 36 | 37 | ```csharp 38 | using AutoWrapper; 39 | ``` 40 | 3. Register the `middleware` below within the `Configure()` method of `Startup.cs` "before" the `UseRouting()` `middleware`: 41 | 42 | ```csharp 43 | app.UseApiResponseAndExceptionWrapper(); 44 | ``` 45 | That's simple! Here’s how the response is going to look like for the default ASP.NET Core API template “`WeatherForecastController`” API: 46 | 47 | ```json 48 | { 49 | "message": "GET Request successful.", 50 | "result": [ 51 | { 52 | "date": "2019-09-16T23:37:51.5544349-05:00", 53 | "temperatureC": 21, 54 | "temperatureF": 69, 55 | "summary": "Mild" 56 | }, 57 | { 58 | "date": "2019-09-17T23:37:51.554466-05:00", 59 | "temperatureC": 28, 60 | "temperatureF": 82, 61 | "summary": "Cool" 62 | }, 63 | { 64 | "date": "2019-09-18T23:37:51.554467-05:00", 65 | "temperatureC": 21, 66 | "temperatureF": 69, 67 | "summary": "Sweltering" 68 | }, 69 | { 70 | "date": "2019-09-19T23:37:51.5544676-05:00", 71 | "temperatureC": 53, 72 | "temperatureF": 127, 73 | "summary": "Chilly" 74 | }, 75 | { 76 | "date": "2019-09-20T23:37:51.5544681-05:00", 77 | "temperatureC": 22, 78 | "temperatureF": 71, 79 | "summary": "Bracing" 80 | } 81 | ] 82 | } 83 | ``` 84 | 85 | # Defining Your Own Custom Message 86 | To display a custom message in your response, use the `ApiResponse` object from `AutoWrapper.Wrappers` namespace. For example, if you want to display a message when a successful `POST` has been made, then you can do something like this: 87 | 88 | ```csharp 89 | [HttpPost] 90 | public async Task Post([FromBody]CreatePersonRequest createRequest) 91 | { 92 | try 93 | { 94 | var personId = await _personManager.CreateAsync(createRequest); 95 | return new ApiResponse("New record has been created in the database.", personId, Status201Created); 96 | } 97 | catch (Exception ex) 98 | { 99 | //TO DO: Log ex 100 | throw; 101 | } 102 | } 103 | ``` 104 | Running the code will give you the following result when successful: 105 | 106 | ```json 107 | { 108 | "message": "New record has been created in the database.", 109 | "result": 100 110 | } 111 | ``` 112 | The `ApiResponse` object has the following overload constructors that you can use: 113 | 114 | ```csharp 115 | ApiResponse(string message, object result = null, int statusCode = 200, string apiVersion = "1.0.0.0") 116 | ApiResponse(object result, int statusCode = 200) 117 | ApiResponse(int statusCode, object apiError) 118 | ApiResponse() 119 | ``` 120 | 121 | # Defining Your Own Api Exception 122 | `AutoWrapper` provides two flavors that you can use to define your own custom exception: 123 | 124 | * `ApiException` - default 125 | * `ApiProblemDetailsException` - available only in version 4 and up. 126 | 127 | Here are a few examples for throwing your own exception message. 128 | 129 | #### Capturing ModelState Validation Errors 130 | 131 | ```csharp 132 | if (!ModelState.IsValid) 133 | { 134 | throw new ApiException(ModelState.AllErrors()); 135 | } 136 | ``` 137 | 138 | The format of the exception result would look something like this when validation fails: 139 | 140 | ```json 141 | { 142 | "isError": true, 143 | "responseException": { 144 | "exceptionMessage": "Request responded with one or more validation errors occurred.", 145 | "validationErrors": [ 146 | { 147 | "name": "LastName", 148 | "reason": "'Last Name' must not be empty." 149 | }, 150 | { 151 | "name": "FirstName", 152 | "reason": "'First Name' must not be empty." 153 | }, 154 | { 155 | "name": "DateOfBirth", 156 | "reason": "'Date Of Birth' must not be empty." 157 | } 158 | ] 159 | } 160 | } 161 | ``` 162 | 163 | To use Problem Details as an error format, just set `UseApiProblemDetailsException` to `true`: 164 | 165 | ```csharp 166 | app.UseApiResponseAndExceptionWrapper(new AutoWrapperOptions { UseApiProblemDetailsException = true }); 167 | ``` 168 | 169 | Then you can use the `ApiProblemDetailsException` object like in the following: 170 | 171 | ```csharp 172 | if (!ModelState.IsValid) 173 | { 174 | throw new ApiProblemDetailsException(ModelState); 175 | } 176 | 177 | ``` 178 | 179 | The format of the exception result would now look something like this when validation fails: 180 | 181 | ```json 182 | { 183 | "isError": true, 184 | "type": "https://httpstatuses.com/422", 185 | "title": "Unprocessable Entity", 186 | "status": 422, 187 | "detail": "Your request parameters didn't validate.", 188 | "instance": null, 189 | "extensions": {}, 190 | "validationErrors": [ 191 | { 192 | "name": "LastName", 193 | "reason": "'Last Name' must not be empty." 194 | }, 195 | { 196 | "name": "FirstName", 197 | "reason": "'First Name' must not be empty." 198 | }, 199 | { 200 | "name": "DateOfBirth", 201 | "reason": "'Date Of Birth' must not be empty." 202 | } 203 | ] 204 | } 205 | ``` 206 | 207 | You can see how the `validationErrors` property is automatically populated with the violated `name` from your model. 208 | 209 | #### Throwing Your Own Exception Message 210 | 211 | An example using `ApiException`: 212 | 213 | ```csharp 214 | throw new ApiException($"Record with id: {id} does not exist.", Status404NotFound); 215 | ``` 216 | 217 | The result would look something like this: 218 | 219 | ```json 220 | { 221 | "isError": true, 222 | "responseException": { 223 | "exceptionMessage": "Record with id: 1001 does not exist.", 224 | } 225 | } 226 | ``` 227 | 228 | An example using `ApiProblemDetailsException`: 229 | 230 | ```csharp 231 | throw new ApiProblemDetailsException($"Record with id: {id} does not exist.", Status404NotFound); 232 | ``` 233 | The result would look something like this: 234 | 235 | ```json 236 | { 237 | "isError": true, 238 | "type": "https://httpstatuses.com/404", 239 | "title": "Record with id: 1001 does not exist.", 240 | "status": 404, 241 | "detail": null, 242 | "instance": null, 243 | "extensions": {} 244 | } 245 | ``` 246 | 247 | The `ApiException` object contains the following overload constructors that you can use to define an exception: 248 | 249 | ```csharp 250 | ApiException(string message, int statusCode = 400, string errorCode = "", string refLink = "") 251 | ApiException(IEnumerable errors, int statusCode = 400) 252 | ApiException(System.Exception ex, int statusCode = 500) 253 | ApiException(object custom, int statusCode = 400) 254 | ``` 255 | 256 | The `ApiProblemDetailsException` object contains the following overload constructors that you can use to define an exception: 257 | 258 | ```csharp 259 | ApiProblemDetailsException(int statusCode) 260 | ApiProblemDetailsException(string title, int statusCode) 261 | ApiProblemDetailsException(ProblemDetails details) 262 | ApiProblemDetailsException(ModelStateDictionary modelState, int statusCode = Status422UnprocessableEntity) 263 | ``` 264 | 265 | For more information, checkout the links below at the **Samples** section. 266 | 267 | # Implement Model Validations 268 | `Model` validations allows you to enforce pre-defined validation rules at a `class`/`property` level. You'd normally use this validation technique to keep a clear separation of concerns, so your validation code becomes much simpler to write, maintain, and test. 269 | 270 | As you have already known, starting ASP.NET Core 2.1, it introduced the `ApiController` attribute which performs automatic model state validation for `400 Bad Request` error. When the `Controller` is decorated with `ApiController` attribute, the framework will automatically register a `ModelStateInvalidFilter` which runs on the `OnActionExecuting` event. This checks for the `Model State` validity and returns the response accordingly. This is a great feature, but since we want to return a custom response object instead of the `400 Bad Request` error, we will disable this feature in our case. 271 | 272 | To disable the automatic model state validation, just add the following code at `ConfigureServices()` method in `Startup.cs` file: 273 | 274 | ```csharp 275 | public void ConfigureServices(IServiceCollection services) { 276 | services.Configure(options => 277 | { 278 | options.SuppressModelStateInvalidFilter = true; 279 | }); 280 | 281 | } 282 | ``` 283 | 284 | # Enable Property Mappings 285 | 286 | > Note: Property Mappings is not available for Problem Details attributes. 287 | 288 | Use the `AutoWrapperPropertyMap` attribute to map the AutoWrapper default property to something else. For example, let's say you want to change the name of the `result` property to something else like `data`, then you can simply define your own schema for mapping it like in the following: 289 | 290 | ```csharp 291 | public class MapResponseObject 292 | { 293 | [AutoWrapperPropertyMap(Prop.Result)] 294 | public object Data { get; set; } 295 | } 296 | ``` 297 | You can then pass the `MapResponseObject` class to the `AutoWrapper` middleware like this: 298 | 299 | ```csharp 300 | app.UseApiResponseAndExceptionWrapper(); 301 | ``` 302 | 303 | On successful requests, your response should now look something like this after mapping: 304 | 305 | ```json 306 | { 307 | "message": "Request successful.", 308 | "isError": false, 309 | "data": { 310 | "id": 7002, 311 | "firstName": "Vianne", 312 | "lastName": "Durano", 313 | "dateOfBirth": "2018-11-01T00:00:00" 314 | } 315 | } 316 | ``` 317 | Notice that the default `result` attribute is now replaced with the `data` attribute. 318 | 319 | Keep in mind that you are free to choose whatever property that you want to map. Here is the list of default properties that you can map: 320 | 321 | ```csharp 322 | [AutoWrapperPropertyMap(Prop.Version)] 323 | [AutoWrapperPropertyMap(Prop.StatusCode)] 324 | [AutoWrapperPropertyMap(Prop.Message)] 325 | [AutoWrapperPropertyMap(Prop.IsError)] 326 | [AutoWrapperPropertyMap(Prop.Result)] 327 | [AutoWrapperPropertyMap(Prop.ResponseException)] 328 | [AutoWrapperPropertyMap(Prop.ResponseException_ExceptionMessage)] 329 | [AutoWrapperPropertyMap(Prop.ResponseException_Details)] 330 | [AutoWrapperPropertyMap(Prop.ResponseException_ReferenceErrorCode)] 331 | [AutoWrapperPropertyMap(Prop.ResponseException_ReferenceDocumentLink)] 332 | [AutoWrapperPropertyMap(Prop.ResponseException_ValidationErrors)] 333 | [AutoWrapperPropertyMap(Prop.ResponseException_ValidationErrors_Field)] 334 | [AutoWrapperPropertyMap(Prop.ResponseException_ValidationErrors_Message)] 335 | ``` 336 | 337 | # Using Your Own Error Schema 338 | You can define your own `Error` object and pass it to the `ApiException()` method. For example, if you have the following `Error` model with mapping configured: 339 | 340 | ```csharp 341 | public class MapResponseObject 342 | { 343 | [AutoWrapperPropertyMap(Prop.ResponseException)] 344 | public object Error { get; set; } 345 | } 346 | 347 | public class Error 348 | { 349 | public string Message { get; set; } 350 | 351 | public string Code { get; set; } 352 | public InnerError InnerError { get; set; } 353 | 354 | public Error(string message, string code, InnerError inner) 355 | { 356 | this.Message = message; 357 | this.Code = code; 358 | this.InnerError = inner; 359 | } 360 | 361 | } 362 | 363 | public class InnerError 364 | { 365 | public string RequestId { get; set; } 366 | public string Date { get; set; } 367 | 368 | public InnerError(string reqId, string reqDate) 369 | { 370 | this.RequestId = reqId; 371 | this.Date = reqDate; 372 | } 373 | } 374 | ``` 375 | You can then throw an error like this: 376 | 377 | ```csharp 378 | throw new ApiException( 379 | new Error("An error blah.", "InvalidRange", 380 | new InnerError("12345678", DateTime.Now.ToShortDateString()) 381 | )); 382 | ``` 383 | 384 | The format of the output will now look like this: 385 | 386 | ```json 387 | { 388 | "isError": true, 389 | "error": { 390 | "message": "An error blah.", 391 | "code": "InvalidRange", 392 | "innerError": { 393 | "requestId": "12345678", 394 | "date": "10/16/2019" 395 | } 396 | } 397 | } 398 | ``` 399 | # Using Your Own API Response Schema 400 | If mapping wont work for you and you need to add additional attributes to the default `API` response schema, then you can use your own custom schema/model to achieve that by setting the `UseCustomSchema` to true in `AutoWrapperOptions` as shown in the following code below: 401 | 402 | ```csharp 403 | app.UseApiResponseAndExceptionWrapper(new AutoWrapperOptions { UseCustomSchema = true }); 404 | ``` 405 | 406 | Now let's say for example you wanted to have an attribute `SentDate` and `Pagination` object as part of your main `API` response, you might want to define your `API` response schema to something like this: 407 | 408 | ```csharp 409 | public class MyCustomApiResponse 410 | { 411 | public int Code { get; set; } 412 | public string Message { get; set; } 413 | public object Payload { get; set; } 414 | public DateTime SentDate { get; set; } 415 | public Pagination Pagination { get; set; } 416 | 417 | public MyCustomApiResponse(DateTime sentDate, 418 | object payload = null, 419 | string message = "", 420 | int statusCode = 200, 421 | Pagination pagination = null) 422 | { 423 | this.Code = statusCode; 424 | this.Message = message == string.Empty ? "Success" : message; 425 | this.Payload = payload; 426 | this.SentDate = sentDate; 427 | this.Pagination = pagination; 428 | } 429 | 430 | public MyCustomApiResponse(DateTime sentDate, 431 | object payload = null, 432 | Pagination pagination = null) 433 | { 434 | this.Code = 200; 435 | this.Message = "Success"; 436 | this.Payload = payload; 437 | this.SentDate = sentDate; 438 | this.Pagination = pagination; 439 | } 440 | 441 | public MyCustomApiResponse(object payload) 442 | { 443 | this.Code = 200; 444 | this.Payload = payload; 445 | } 446 | 447 | } 448 | 449 | public class Pagination 450 | { 451 | public int TotalItemsCount { get; set; } 452 | public int PageSize { get; set; } 453 | public int CurrentPage { get; set; } 454 | public int TotalPages { get; set; } 455 | } 456 | ``` 457 | 458 | To test the result, you can create a `GET` method to something like this: 459 | 460 | ```csharp 461 | public async Task Get() 462 | { 463 | var data = await _personManager.GetAllAsync(); 464 | 465 | return new MyCustomApiResponse(DateTime.UtcNow, data, 466 | new Pagination 467 | { 468 | CurrentPage = 1, 469 | PageSize = 10, 470 | TotalItemsCount = 200, 471 | TotalPages = 20 472 | }); 473 | } 474 | ``` 475 | 476 | Running the code should give you now the following response format: 477 | 478 | ``` 479 | { 480 | "code": 200, 481 | "message": "Success", 482 | "payload": [ 483 | { 484 | "id": 1, 485 | "firstName": "Vianne Maverich", 486 | "lastName": "Durano", 487 | "dateOfBirth": "2018-11-01T00:00:00" 488 | }, 489 | { 490 | "id": 2, 491 | "firstName": "Vynn Markus", 492 | "lastName": "Durano", 493 | "dateOfBirth": "2018-11-01T00:00:00" 494 | }, 495 | { 496 | "id": 3, 497 | "firstName": "Mitch", 498 | "lastName": "Durano", 499 | "dateOfBirth": "2018-11-01T00:00:00" 500 | } 501 | ], 502 | "sentDate": "2019-10-17T02:26:32.5242353Z", 503 | "pagination": { 504 | "totalItemsCount": 200, 505 | "pageSize": 10, 506 | "currentPage": 1, 507 | "totalPages": 20 508 | } 509 | } 510 | ``` 511 | That’s it. One thing to note here is that once you use your own schema for your `API` response, you have the full ability to control how you would want to format your data, but at the same time losing some of the option configurations for the default `API` Response. The good thing is you can still take advantage of the `ApiException()` method to throw a user-defined error message. 512 | 513 | # Options 514 | The following properties are the available options that you can set: 515 | 516 | ### Version 4.5.x Additions 517 | * `ExcludePaths` 518 | 519 | ### Version 4.3.x Additions 520 | * `ShouldLogRequestData` 521 | * `ShowIsErrorFlagForSuccessfulResponse` 522 | 523 | ### Version 4.2.x Additions 524 | * `IgnoreWrapForOkRequests` 525 | 526 | ### Version 4.1.0 Additions 527 | * `LogRequestDataOnException` 528 | 529 | ### Version 4.0.0 Additions 530 | * `UseApiProblemDetailsException` 531 | * `UseCustomExceptionFormat` 532 | 533 | ### Version 3.0.0 Additions 534 | * `BypassHTMLValidation ` 535 | * `ReferenceLoopHandling ` 536 | 537 | ### Version 2.x.x Additions 538 | * `EnableResponseLogging` 539 | * `EnableExceptionLogging` 540 | 541 | ### Version 2.0.x Additions 542 | * `IgnoreNullValue` 543 | * `UseCamelCaseNamingStrategy` 544 | * `UseCustomSchema` 545 | 546 | ### Version 1.x.x Additions 547 | * `IsApiOnly` 548 | * `WrapWhenApiPathStartsWith` 549 | 550 | ### Version 1.0.0 551 | * `ApiVersion` 552 | * `ShowApiVersion` 553 | * `ShowStatusCode` 554 | * `IsDebug` 555 | 556 | #### ShowApiVersion 557 | if you want to show the `API` version in the response, then you can do: 558 | 559 | ```csharp 560 | app.UseApiResponseAndExceptionWrapper(new AutoWrapperOptions { ShowApiVersion = true }); 561 | ``` 562 | The default `API` version format is set to "`1.0.0.0`" 563 | 564 | #### ApiVersion 565 | If you wish to specify a different version format, then you can do: 566 | 567 | ```csharp 568 | app.UseApiResponseAndExceptionWrapper(new AutoWrapperOptions { ShowApiVersion = true, ApiVersion = "2.0" }); 569 | ``` 570 | 571 | #### ShowStatusCode 572 | if you want to show the `StatusCode` in the response, then you can do: 573 | 574 | ```csharp 575 | app.UseApiResponseAndExceptionWrapper(new AutoWrapperOptions { ShowStatusCode = true }); 576 | ``` 577 | 578 | #### IsDebug 579 | By default, `AutoWrapper` suppresses stack trace information. If you want to see the actual details of the error from the response during the development stage, then simply set the `AutoWrapperOptions` `IsDebug` to `true`: 580 | 581 | ```csharp 582 | app.UseApiResponseAndExceptionWrapper(new AutoWrapperOptions { IsDebug = true }); 583 | ``` 584 | 585 | #### IsApiOnly 586 | `AutoWrapper` is meant to be used for ASP.NET Core API project templates only. If you are combining `API Controllers` within your front-end projects like Angular, MVC, React, Blazor Server and other SPA frameworks that supports .NET Core, then use this property to enable it. 587 | 588 | ```csharp 589 | app.UseApiResponseAndExceptionWrapper(new AutoWrapperOptions { IsApiOnly = false} ); 590 | ``` 591 | 592 | #### WrapWhenApiPathStartsWith 593 | If you set the `IsApiOnly` option to `false`, you can also specify the segment of your `API` path for validation. By default it is set to `"/api"`. If you want to set it to something else, then you can do: 594 | 595 | ```csharp 596 | app.UseApiResponseAndExceptionWrapper( new AutoWrapperOptions { 597 | IsApiOnly = false, 598 | WrapWhenApiPathStartsWith = "/myapi" 599 | }); 600 | ``` 601 | This will activate the `AutoWrapper` to intercept HTTP responses when a request contains the `WrapWhenApiPathStartsWith` value. 602 | 603 | > Note that I would still recommend you to implement your `API Controllers` in a separate project to value the separation of concerns and to avoid mixing route configurations for your `SPAs` and `APIs`. 604 | 605 | #### IgnoreWrapForOkRequests 606 | If you want to completely ignore wrapping the response for successful requests to just output directly the data, you simply set the IgnoreWrapForOkRequests to true like in the following: 607 | 608 | ```csharp 609 | app.UseApiResponseAndExceptionWrapper(new AutoWrapperOptions { 610 | IgnoreWrapForOkRequests = true, 611 | }); 612 | ``` 613 | 614 | # AutoWrapIgnore Attribute 615 | You can use the `[AutoWrapIgnore]` filter attribute for endpoints that you don't need to be wrapped. 616 | 617 | For example: 618 | 619 | ```csharp 620 | [HttpGet] 621 | [AutoWrapIgnore] 622 | public async Task Get() 623 | { 624 | var data = await _personManager.GetAllAsync(); 625 | return Ok(data); 626 | } 627 | ``` 628 | or 629 | 630 | ```csharp 631 | [HttpGet] 632 | [AutoWrapIgnore] 633 | public async Task> Get() 634 | { 635 | return await _personManager.GetAllAsync(); 636 | } 637 | ``` 638 | 639 | # RequestDataLogIgnore Attribute 640 | You can use the `[RequestDataLogIgnore]` if you don't want certain endpoints to log the data in the requests: 641 | 642 | ```csharp 643 | [HttpGet] 644 | [RequestDataLogIgnore] 645 | public async Task Post([FromBody] CreatePersonRequest personRequest) 646 | { 647 | //Rest of the code here 648 | } 649 | ``` 650 | 651 | You can use the `[AutoWrapIgnore]` attribute and set `ShouldLogRequestData` property to `false` if you have an endpoint that don't need to be wrapped and also don't want to log the data in the requests: 652 | 653 | ```csharp 654 | [HttpGet] 655 | [AutoWrapIgnore(ShouldLogRequestData = false)] 656 | public async Task> Get() 657 | { 658 | //Rest of the code here 659 | } 660 | ``` 661 | # Support for Logging 662 | 663 | Another good thing about `AutoWrapper` is that logging is already pre-configured. .NET Core apps has built-in logging mechanism by default, and any requests and responses that has been intercepted by the wrapper will be automatically logged (thanks to Dependency Injection!). .NET Core supports a logging `API` that works with a variety of built-in and third-party logging providers. Depending on what supported .NET Core logging provider you use and how you configure the location to log the data (e.g text file, Cloud , etc. ), AutoWrapper will automatically write the logs there for you. 664 | 665 | You can turn off the default Logging by setting `EnableResponseLogging` and `EnableExceptionLogging` options to `false`. 666 | 667 | For example: 668 | 669 | ```csharp 670 | app.UseApiResponseAndExceptionWrapper(new AutoWrapperOptions { 671 | EnableResponseLogging = false, 672 | EnableExceptionLogging = false 673 | }); 674 | ``` 675 | 676 | You can set the `LogRequestDataOnException` option to `false` if you want to exclude the request body data in the logs when an exception occurs. 677 | 678 | ```csharp 679 | app.UseApiResponseAndExceptionWrapper(new AutoWrapperOptions { 680 | LogRequestDataOnException = false 681 | }); 682 | ``` 683 | 684 | # Support for Swagger 685 | [Swagger](https://swagger.io/) provides an advance documentation for your APIs where it allows developers to reference the details of your `API` endpoints and test them when necessary. This is very helpful especially when your `API` is public and you expect many developers to use it. 686 | 687 | `AutoWrapper` omit any request with “`/swagger`” in the `URL` so you can still be able to navigate to the Swagger UI for your API documentation. 688 | 689 | # Exclude Paths 690 | The ExcludePaths option enables you to set a collection of API paths to be ignored. This feature was added by chen1tian. Thank you so much for this great contribution! Here's how it works: 691 | 692 | Excluding Api paths/routes that do not need to be wrapped support three ExcludeMode: 693 | 694 | `Strict`: The request path must be exactly the same as the configured path. 695 | `StartWith`: The request path starts at the configuration path. 696 | `Regex`: If the requested path match the configured path regular expression, it will be excluded. 697 | The following is a quick example: 698 | 699 | # Support for SignalR 700 | If you have the following SignalR endpoint: 701 | 702 | ```csharp 703 | app.UseEndpoints(endpoints => 704 | { 705 | endpoints.MapControllers(); 706 | endpoints.MapHub("/notice"); 707 | }); 708 | ``` 709 | 710 | then you can use the ExcludePaths and set the the "/notice" path as AutoWrapperExcludePaths for the SignalR endpoint to work. For example: 711 | 712 | ```csharp 713 | app.UseApiResponseAndExceptionWrapper(new AutoWrapperOptions 714 | { 715 | ExcludePaths = new AutoWrapperExcludePath[] { 716 | new AutoWrapperExcludePath("/notice/.*|/notice", ExcludeMode.Regex) 717 | } 718 | }); 719 | ``` 720 | 721 | # Support for Dapr 722 | Prior to `4.5.x` version, the Dapr Pubsub request cannot reach the configured Controller Action after being wrapped by AutoWrapper. The series path starting with "/dapr/" needs to be excluded to make the dapr request take effect: 723 | 724 | ```csharp 725 | app.UseApiResponseAndExceptionWrapper(new AutoWrapperOptions 726 | { 727 | ExcludePaths = new AutoWrapperExcludePath[] { 728 | new AutoWrapperExcludePath("/dapr", ExcludeMode.StartWith) 729 | } 730 | }); 731 | ``` 732 | 733 | # Support for NetCoreApp2.1 and NetCoreApp2.2 734 | `AutoWrapper` version 2.x - 3.0 also supports both .NET Core 2.1 and 2.2. You just need to install the Nuget package `Newtonsoft.json` first before `AutoWrapper.Core`. 735 | 736 | # Unwrapping the Result from .NET Client 737 | [AutoWrapper.Server](https://github.com/proudmonkey/AutoWrapper.Server) is simple library that enables you unwrap the `Result` property of the AutoWrapper's `ApiResponse` object in your C# .NET Client code. The goal is to deserialize the `Result` object directly to your matching `Model` without having you to create the ApiResponse schema. 738 | 739 | For example: 740 | 741 | ```csharp 742 | [HttpGet] 743 | public async Task> Get() 744 | { 745 | var client = HttpClientFactory.Create(); 746 | var httpResponse = await client.GetAsync("https://localhost:5001/api/v1/persons"); 747 | 748 | IEnumerable persons = null; 749 | if (httpResponse.IsSuccessStatusCode) 750 | { 751 | var jsonString = await httpResponse.Content.ReadAsStringAsync(); 752 | persons = Unwrapper.Unwrap>(jsonString); 753 | } 754 | 755 | return persons; 756 | } 757 | ``` 758 | 759 | For more information, see: [AutoWrapper.Server](https://github.com/proudmonkey/AutoWrapper.Server) 760 | # Samples 761 | * [AutoWrapper: Prettify Your ASP.NET Core APIs with Meaningful Responses](http://vmsdurano.com/autowrapper-prettify-your-asp-net-core-apis-with-meaningful-responses/) 762 | * [AutoWrapper: Customizing the Default Response Output](http://vmsdurano.com/asp-net-core-with-autowrapper-customizing-the-default-response-output/) 763 | * [AutoWrapper Now Supports Problem Details For Your ASP.NET Core APIs](http://vmsdurano.com/autowrapper-now-supports-problemdetails/) 764 | * [AutoWrapper.Server: Sample Usage](http://vmsdurano.com/autowrapper-server-is-now-available/) 765 | 766 | # Feedback and Give a Star! :star: 767 | I’m pretty sure there are still lots of things to improve in this project. Try it out and let me know your thoughts. 768 | 769 | Feel free to submit a [ticket](https://github.com/proudmonkey/AutoWrapper/issues) if you find bugs or request a new feature. Your valuable feedback is much appreciated to better improve this project. If you find this useful, please give it a star to show your support for this project. 770 | 771 | # Contributors 772 | 773 | * **Vincent Maverick Durano** - [Blog](http://vmsdurano.com/) 774 | * **Huei Feng** - [Github Profile](https://github.com/hueifeng) 775 | * **ITninja04** - [Github Profile](https://github.com/ITninja04) 776 | * **Rahmat Slamet** - [Github Profile](https://github.com/arhen) 777 | * **abelfiore** - [Github Profile](https://github.com/abelfiore) 778 | * **chen1tian** - [Github Profile](https://github.com/chen1tian) 779 | 780 | Want to contribute? Please read the CONTRIBUTING docs [here](https://github.com/proudmonkey/AutoWrapper/blob/master/CONTRIBUTING.md). 781 | 782 | # Release History 783 | 784 | See: [Release Log](https://github.com/proudmonkey/AutoWrapper/blob/master/RELEASE.MD) 785 | 786 | # License 787 | 788 | This project is licensed under the MIT License - see the [LICENSE.md](LICENSE) file for details. 789 | 790 | # Donate 791 | If you find this project useful — or just feeling generous, consider buying me a beer or a coffee. Cheers! :beers: :coffee: 792 | | | | 793 | | ------------- |:-------------:| 794 | | | [![BMC](https://github.com/proudmonkey/Resources/blob/master/donate_coffee.png)](https://www.buymeacoffee.com/ImP9gONBW) | 795 | 796 | 797 | Thank you! 798 | -------------------------------------------------------------------------------- /README.zh-cn.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | # AutoWrapper [![Nuget](https://img.shields.io/nuget/v/AutoWrapper.Core?color=blue)](https://www.nuget.org/packages/AutoWrapper.Core) [![Nuget downloads](https://img.shields.io/nuget/dt/AutoWrapper.Core?color=green)](https://www.nuget.org/packages/AutoWrapper.Core) ![.NET Core](https://github.com/proudmonkey/AutoWrapper/workflows/.NET%20Core/badge.svg) 4 | 5 | `AutoWrapper`是一个简单,可自定义的全局`HTTP`异常处理程序和针对ASP.NET Core API的响应包装器。它使用ASP.NET Core `middleware` 来拦截传入的`HTTP`请求,并通过为成功和错误结果提供一致的响应格式来自动为您包装响应。目的是让您专注于特定于业务代码的要求,并让包装器自动处理`HTTP`响应。这可以加快构建API的开发时间,同时为`HTTP`响应强制执行自己的标准。 6 | 7 | #### 主要特点: 8 | 9 | * 异常处理 10 | * `ModelState` 验证错误机制 (同时支持 `Data Annotation` 和 `FluentValidation`) 11 | * 可配置`API`异常 12 | * `Result`和Errors一致性响应格式 13 | * 详细的`Result`响应 14 | * 详细的`Error`响应 15 | * 可配置`HTTP` `StatusCodes`和消息 16 | * 添加对`Swagger`的支持 17 | * 添加对`Request` `Response` 和`Exceptions`的日志支持 18 | * 一个可配置的中间件`选项`来配置包装器。有关详情,请参见下面**选项**部分。 19 | * 为默认`ApiResponse`属性启动属性名称映射 20 | * 添加支持以实现您自己的用户定义的`Response`和`Error` schema / object 21 | * 添加对问题详细信息的异常格式的支持 22 | * 添加对忽略不需要使用`[AutoWrapIgnore]`过滤器属性包装的操作方法的支持。 23 | * V3.x启用了对`netcoreapp2.1`和`netcoreapp2.2` .NET Core框架的向后兼容性支持 24 | * 增加排除的路径,依赖于此,增加了对`SignalR`的支持,同时也能支持`Dapr`方法。 25 | 26 | # 安装 27 | 28 | 1. AutoWrapper.Core从NuGet或通过CLI下载并安装: 29 | 30 | ``` 31 | PM> Install-Package AutoWrapper.Core -Version 4.5.0 32 | ``` 33 | 34 | 2. 在`Startup.cs`下声明命名空间 35 | 36 | ```csharp 37 | using AutoWrapper; 38 | ``` 39 | 40 | 3. 在`UseRouting()` 中间件之前的`Startup.cs`的`Configure()`方法中注册以下中间件: 41 | 42 | ```csharp 43 | app.UseApiResponseAndExceptionWrapper(); 44 | ``` 45 | 46 | 很简单!默认的ASP.NET Core API模板`WeatherForecastController` API的响应如下所示: 47 | 48 | ```json 49 | { 50 | "message": "GET Request successful.", 51 | "isError": false, 52 | "result": [ 53 | { 54 | "date": "2019-09-16T23:37:51.5544349-05:00", 55 | "temperatureC": 21, 56 | "temperatureF": 69, 57 | "summary": "Mild" 58 | }, 59 | { 60 | "date": "2019-09-17T23:37:51.554466-05:00", 61 | "temperatureC": 28, 62 | "temperatureF": 82, 63 | "summary": "Cool" 64 | }, 65 | { 66 | "date": "2019-09-18T23:37:51.554467-05:00", 67 | "temperatureC": 21, 68 | "temperatureF": 69, 69 | "summary": "Sweltering" 70 | }, 71 | { 72 | "date": "2019-09-19T23:37:51.5544676-05:00", 73 | "temperatureC": 53, 74 | "temperatureF": 127, 75 | "summary": "Chilly" 76 | }, 77 | { 78 | "date": "2019-09-20T23:37:51.5544681-05:00", 79 | "temperatureC": 22, 80 | "temperatureF": 71, 81 | "summary": "Bracing" 82 | } 83 | ] 84 | } 85 | ``` 86 | 87 | # 定义自己的自定义消息 88 | 89 | 要在响应中显示自定义消息,请使用`AutoWrapper.Wrappers`名称空间中的`ApiResponse`对象。例如,如果要在成功执行`POST`后显示一条消息,则可以执行以下操作: 90 | 91 | ```csharp 92 | [HttpPost] 93 | public async Task Post([FromBody]CreatePersonRequest createRequest) 94 | { 95 | try 96 | { 97 | var personId = await _personManager.CreateAsync(createRequest); 98 | return new ApiResponse("New record has been created in the database.", personId, Status201Created); 99 | } 100 | catch (Exception ex) 101 | { 102 | //TO DO: Log ex 103 | throw; 104 | } 105 | } 106 | ``` 107 | 108 | 成功运行代码将为您提供以下结果: 109 | 110 | ```json 111 | { 112 | "message": "New record has been created in the database.", 113 | "isError": false, 114 | "result": 100 115 | } 116 | ``` 117 | 118 | `ApiResponse`对象具有以下可使用的重载构造函数: 119 | 120 | ```csharp 121 | ApiResponse(string message, object result = null, int statusCode = 200, string apiVersion = "1.0.0.0") 122 | ApiResponse(object result, int statusCode = 200) 123 | ApiResponse(int statusCode, object apiError) 124 | ApiResponse() 125 | ``` 126 | 127 | # 定义自己的Api异常 128 | 129 | `AutoWrapper`提供了两种类型,您可以用来定义自己的自定义异常: 130 | 131 | * `ApiException`-默认 132 | 133 | * `ApiProblemDetailsException`-仅在 version 4及更高版本中可用 134 | 135 | 这里是一些抛出您自己的异常消息的示例。 136 | 137 | #### 捕获ModelState验证错误 138 | 139 | ```csharp 140 | if (!ModelState.IsValid) 141 | { 142 | throw new ApiException(ModelState.AllErrors()); 143 | } 144 | ``` 145 | 146 | 验证失败时,异常结果的格式如下所示: 147 | 148 | ```json 149 | { 150 | "isError": true, 151 | "responseException": { 152 | "exceptionMessage": "Request responded with one or more validation errors occurred.", 153 | "validationErrors": [ 154 | { 155 | "name": "LastName", 156 | "reason": "'Last Name' must not be empty." 157 | }, 158 | { 159 | "name": "FirstName", 160 | "reason": "'First Name' must not be empty." 161 | }, 162 | { 163 | "name": "DateOfBirth", 164 | "reason": "'Date Of Birth' must not be empty." 165 | } 166 | ] 167 | } 168 | } 169 | ``` 170 | 171 | 要将问题详细信息用作错误格式,只需将`UseApiProblemDetailsException`设置为`true`即可: 172 | 173 | ```csharp 174 | app.UseApiResponseAndExceptionWrapper(new AutoWrapperOptions { UseApiProblemDetailsException = true }); 175 | ``` 176 | 177 | 然后,您可以像下面这样使用`ApiProblemDetailsException`对象: 178 | 179 | ```csharp 180 | if (!ModelState.IsValid) 181 | { 182 | throw new ApiProblemDetailsException(ModelState); 183 | } 184 | 185 | ``` 186 | 187 | 验证失败时,异常结果的格式现在应类似于以下内容: 188 | 189 | ```json 190 | { 191 | "isError": true, 192 | "type": "https://httpstatuses.com/422", 193 | "title": "Unprocessable Entity", 194 | "status": 422, 195 | "detail": "Your request parameters didn't validate.", 196 | "instance": null, 197 | "extensions": {}, 198 | "validationErrors": [ 199 | { 200 | "name": "LastName", 201 | "reason": "'Last Name' must not be empty." 202 | }, 203 | { 204 | "name": "FirstName", 205 | "reason": "'First Name' must not be empty." 206 | }, 207 | { 208 | "name": "DateOfBirth", 209 | "reason": "'Date Of Birth' must not be empty." 210 | } 211 | ] 212 | } 213 | ``` 214 | 215 | 您可以看到如何使用模型中的`名称`自动填充`validationErrors`属性。 216 | 217 | #### 抛出自己的异常消息 218 | 219 | 一个使用`ApiException`的例子: 220 | 221 | ```csharp 222 | throw new ApiException($"Record with id: {id} does not exist.", Status404NotFound); 223 | ``` 224 | 225 | 结果看起来像这样: 226 | 227 | ```json 228 | { 229 | "isError": true, 230 | "responseException": { 231 | "exceptionMessage": "Record with id: 1001 does not exist.", 232 | } 233 | } 234 | ``` 235 | 236 | 一个使用`ApiProblemDetailsException`的例子: 237 | 238 | ```csharp 239 | throw new ApiProblemDetailsException($"Record with id: {id} does not exist.", Status404NotFound); 240 | ``` 241 | 242 | 结果看起来像这样: 243 | 244 | ```csharp 245 | throw new ApiProblemDetailsException($"Record with id: {id} does not exist.", Status404NotFound); 246 | ``` 247 | 248 | 结果看起来像这样: 249 | 250 | ```json 251 | { 252 | "isError": true, 253 | "type": "https://httpstatuses.com/404", 254 | "title": "Record with id: 1001 does not exist.", 255 | "status": 404, 256 | "detail": null, 257 | "instance": null, 258 | "extensions": {} 259 | } 260 | ``` 261 | 262 | `ApiException`对象包含以下重载构造函数,可用于定义异常: 263 | 264 | ```csharp 265 | ApiException(string message, int statusCode = 400, string errorCode = "", string refLink = "") 266 | ApiException(IEnumerable errors, int statusCode = 400) 267 | ApiException(System.Exception ex, int statusCode = 500) 268 | ApiException(object custom, int statusCode = 400) 269 | ``` 270 | 271 | `ApiProblemDetailsException`对象包含以下重载构造函数,可用于定义异常: 272 | 273 | ```csharp 274 | ApiProblemDetailsException(int statusCode) 275 | ApiProblemDetailsException(string title, int statusCode) 276 | ApiProblemDetailsException(ProblemDetails details) 277 | ApiProblemDetailsException(ModelStateDictionary modelState, int statusCode = Status422UnprocessableEntity) 278 | ``` 279 | 280 | 有关更多信息,请在下面的**示例**部分中查看链接。 281 | 282 | # 实施Model验证 283 | 284 | `Model`验证可让您在`class` /`property`级别强制执行预定义的验证规则。通常,您将使用此验证技术来保持关注点的清晰分离,因此验证代码的编写,维护和测试变得更加简单。 285 | 286 | 众所周知,从ASP.NET Core 2.1开始,它引入了`ApiController`属性,该属性针对`400 Bad Request`错误执行自动模型状态验证。当Controller用ApiController属性修饰时,框架将自动注册一个ModelStateInvalidFilter,它在OnActionExecuting事件上运行。这将检查`模型状态`的有效性,并相应地返回响应。这是一个很棒的功能,但是由于我们要返回自定义响应对象而不是`400 Bad Request`错误,因此我们将禁用此功能。 287 | 288 | 要禁用自动模型状态验证,只需在`Startup.cs`文件中的`ConfigureServices()`方法中添加以下代码: 289 | 290 | ```csharp 291 | public void ConfigureServices(IServiceCollection services) { 292 | services.Configure(options => 293 | { 294 | options.SuppressModelStateInvalidFilter = true; 295 | }); 296 | 297 | } 298 | ``` 299 | 300 | # 启用属性映射 301 | 302 | > Note: Property Mappings is not available for Problem Details attributes. 303 | 304 | 使用`AutoWrapperPropertyMap`属性将AutoWrapper的默认属性映射到其他属性。举例来说,假设您想将`result`属性的名称更改为诸如`data`之类的名称,然后只需定义自己的架构即可进行映射,如下所示: 305 | 306 | ```csharp 307 | public class MapResponseObject 308 | { 309 | [AutoWrapperPropertyMap(Prop.Result)] 310 | public object Data { get; set; } 311 | } 312 | ``` 313 | 然后可以将`apResponseObject`类传递给`AutoWrapper`中间件,如下所示: 314 | 315 | ```csharp 316 | app.UseApiResponseAndExceptionWrapper(); 317 | ``` 318 | 319 | 成功请求后,映射后,您的响应现在应如下所示: 320 | 321 | ```json 322 | { 323 | "message": "Request successful.", 324 | "isError": false, 325 | "data": { 326 | "id": 7002, 327 | "firstName": "Vianne", 328 | "lastName": "Durano", 329 | "dateOfBirth": "2018-11-01T00:00:00" 330 | } 331 | } 332 | ``` 333 | 334 | 注意,默认的`result`属性现在已被`data`属性取代。 335 | 336 | 请记住,您可以自由选择要映射的任何属性。这是可以映射的默认属性的列表: 337 | 338 | ```csharp 339 | [AutoWrapperPropertyMap(Prop.Version)] 340 | [AutoWrapperPropertyMap(Prop.StatusCode)] 341 | [AutoWrapperPropertyMap(Prop.Message)] 342 | [AutoWrapperPropertyMap(Prop.IsError)] 343 | [AutoWrapperPropertyMap(Prop.Result)] 344 | [AutoWrapperPropertyMap(Prop.ResponseException)] 345 | [AutoWrapperPropertyMap(Prop.ResponseException_ExceptionMessage)] 346 | [AutoWrapperPropertyMap(Prop.ResponseException_Details)] 347 | [AutoWrapperPropertyMap(Prop.ResponseException_ReferenceErrorCode)] 348 | [AutoWrapperPropertyMap(Prop.ResponseException_ReferenceDocumentLink)] 349 | [AutoWrapperPropertyMap(Prop.ResponseException_ValidationErrors)] 350 | [AutoWrapperPropertyMap(Prop.ResponseException_ValidationErrors_Field)] 351 | [AutoWrapperPropertyMap(Prop.ResponseException_ValidationErrors_Message)] 352 | ``` 353 | 354 | # 使用自己的错误架构 355 | 356 | 您可以定义自己的`Error`对象,并将其传递给`ApiException()`方法。例如,如果您具有配置了映射的以下`Error`模型: 357 | 358 | ```csharp 359 | public class MapResponseObject 360 | { 361 | [AutoWrapperPropertyMap(Prop.ResponseException)] 362 | public object Error { get; set; } 363 | } 364 | 365 | public class Error 366 | { 367 | public string Message { get; set; } 368 | 369 | public string Code { get; set; } 370 | public InnerError InnerError { get; set; } 371 | 372 | public Error(string message, string code, InnerError inner) 373 | { 374 | this.Message = message; 375 | this.Code = code; 376 | this.InnerError = inner; 377 | } 378 | 379 | } 380 | 381 | public class InnerError 382 | { 383 | public string RequestId { get; set; } 384 | public string Date { get; set; } 385 | 386 | public InnerError(string reqId, string reqDate) 387 | { 388 | this.RequestId = reqId; 389 | this.Date = reqDate; 390 | } 391 | } 392 | ``` 393 | 394 | 然后,您可以引发如下错误: 395 | 396 | ```csharp 397 | throw new ApiException( 398 | new Error("An error blah.", "InvalidRange", 399 | new InnerError("12345678", DateTime.Now.ToShortDateString()) 400 | )); 401 | ``` 402 | 403 | 现在,输出格式将如下所示: 404 | 405 | ```json 406 | { 407 | "isError": true, 408 | "error": { 409 | "message": "An error blah.", 410 | "code": "InvalidRange", 411 | "innerError": { 412 | "requestId": "12345678", 413 | "date": "10/16/2019" 414 | } 415 | } 416 | } 417 | ``` 418 | 419 | # 使用您自己的API响应架构 420 | 421 | 如果映射对您不起作用,并且您需要向默认的API响应模式中添加其他属性,则可以通过在AutoWrapperOptions中将UseCustomSchema设置为true来使用自己的自定义模式/模型来实现,如图所示。下面的代码: 422 | 423 | ```csharp 424 | app.UseApiResponseAndExceptionWrapper(new AutoWrapperOptions { UseCustomSchema = true }); 425 | ``` 426 | 427 | 现在,假设您想将属性`SentDate`和` Pagination`对象作为主要API响应的一部分,您可能希望将API响应架构定义为以下形式: 428 | 429 | ```csharp 430 | public class MyCustomApiResponse 431 | { 432 | public int Code { get; set; } 433 | public string Message { get; set; } 434 | public object Payload { get; set; } 435 | public DateTime SentDate { get; set; } 436 | public Pagination Pagination { get; set; } 437 | 438 | public MyCustomApiResponse(DateTime sentDate, 439 | object payload = null, 440 | string message = "", 441 | int statusCode = 200, 442 | Pagination pagination = null) 443 | { 444 | this.Code = statusCode; 445 | this.Message = message == string.Empty ? "Success" : message; 446 | this.Payload = payload; 447 | this.SentDate = sentDate; 448 | this.Pagination = pagination; 449 | } 450 | 451 | public MyCustomApiResponse(DateTime sentDate, 452 | object payload = null, 453 | Pagination pagination = null) 454 | { 455 | this.Code = 200; 456 | this.Message = "Success"; 457 | this.Payload = payload; 458 | this.SentDate = sentDate; 459 | this.Pagination = pagination; 460 | } 461 | 462 | public MyCustomApiResponse(object payload) 463 | { 464 | this.Code = 200; 465 | this.Payload = payload; 466 | } 467 | 468 | } 469 | 470 | public class Pagination 471 | { 472 | public int TotalItemsCount { get; set; } 473 | public int PageSize { get; set; } 474 | public int CurrentPage { get; set; } 475 | public int TotalPages { get; set; } 476 | } 477 | ``` 478 | 479 | 为了测试结果,您可以为以下内容创建一个`GET`方法: 480 | 481 | ```csharp 482 | public async Task Get() 483 | { 484 | var data = await _personManager.GetAllAsync(); 485 | 486 | return new MyCustomApiResponse(DateTime.UtcNow, data, 487 | new Pagination 488 | { 489 | CurrentPage = 1, 490 | PageSize = 10, 491 | TotalItemsCount = 200, 492 | TotalPages = 20 493 | }); 494 | } 495 | ``` 496 | 497 | 运行代码现在应该为您提供以下响应格式: 498 | 499 | ``` 500 | { 501 | "code": 200, 502 | "message": "Success", 503 | "payload": [ 504 | { 505 | "id": 1, 506 | "firstName": "Vianne Maverich", 507 | "lastName": "Durano", 508 | "dateOfBirth": "2018-11-01T00:00:00" 509 | }, 510 | { 511 | "id": 2, 512 | "firstName": "Vynn Markus", 513 | "lastName": "Durano", 514 | "dateOfBirth": "2018-11-01T00:00:00" 515 | }, 516 | { 517 | "id": 3, 518 | "firstName": "Mitch", 519 | "lastName": "Durano", 520 | "dateOfBirth": "2018-11-01T00:00:00" 521 | } 522 | ], 523 | "sentDate": "2019-10-17T02:26:32.5242353Z", 524 | "pagination": { 525 | "totalItemsCount": 200, 526 | "pageSize": 10, 527 | "currentPage": 1, 528 | "totalPages": 20 529 | } 530 | } 531 | ``` 532 | 533 | 而已。这里要注意的一件事是,一旦您对API响应使用了自己的模式,就可以完全控制想要格式化数据的方式,但是同时会丢失一些默认的选项配置。 API响应。好消息是,您仍然可以利用`ApiException()`方法抛出用户定义的错误消息。 534 | 535 | # 选项 536 | 537 | 以下属性是可以设置的可用选项: 538 | 539 | ### 版本4.3.x添加 540 | * `ShouldLogRequestData` 541 | * `ShowIsErrorFlagForSuccessfulResponse` 542 | 543 | ### 版本4.2.x添加 544 | * `IgnoreWrapForOkRequests` 545 | 546 | ### 版本4.1.0添加 547 | * `LogRequestDataOnException` 548 | 549 | ### 版本4.0.0添加 550 | * `UseApiProblemDetailsException` 551 | * `UseCustomExceptionFormat` 552 | 553 | ### 版本3.0.0添加 554 | * `BypassHTMLValidation` 555 | * `ReferenceLoopHandling` 556 | 557 | ### 版本2.x.x添加 558 | * `EnableResponseLogging` 559 | * `EnableExceptionLogging` 560 | 561 | ### 版本2.0.x添加 562 | * `IgnoreNullValue` 563 | * `UseCamelCaseNamingStrategy` 564 | * `UseCustomSchema` 565 | 566 | ### 版本1.x.x添加 567 | * `IsApiOnly` 568 | * `WrapWhenApiPathStartsWith` 569 | 570 | ### 版本1.0.0 571 | * `ApiVersion` 572 | * `ShowApiVersion` 573 | * `ShowStatusCode` 574 | * `IsDebug` 575 | 576 | #### ShowApiVersion 577 | 如果您想在响应中显示API版本,则可以执行以下操作: 578 | 579 | ```csharp 580 | app.UseApiResponseAndExceptionWrapper(new AutoWrapperOptions { ShowApiVersion = true }); 581 | ``` 582 | 583 | API的默认版本格式设置为`1.0.0.0` 584 | 585 | #### ApiVersion 586 | 如果您希望指定其他版本格式,则可以执行以下操作: 587 | 588 | ```csharp 589 | app.UseApiResponseAndExceptionWrapper(new AutoWrapperOptions { ShowApiVersion = true, ApiVersion = "2.0" }); 590 | ``` 591 | 592 | #### ShowStatusCode 593 | 如果您想在响应中显示`StatusCode`,则可以执行以下操作: 594 | 595 | ```csharp 596 | app.UseApiResponseAndExceptionWrapper(new AutoWrapperOptions { ShowStatusCode = true }); 597 | ``` 598 | 599 | #### IsDebug 600 | 默认情况下,`AutoWrapper`禁止显示堆栈跟踪信息。如果要在开发阶段从响应中查看错误的实际详细信息,只需将AutoWrapperOptions`IsDebug`设置为true: 601 | 602 | ```csharp 603 | app.UseApiResponseAndExceptionWrapper(new AutoWrapperOptions { IsDebug = true }); 604 | ``` 605 | 606 | #### IsApiOnly 607 | 608 | `AutoWrapper`仅可用于ASP.NET Core API项目模板。如果您要将`API Controllers`组合到您的前端项目(例如Angular,MVC,React,Blazor Server和其他支持.NET Core的SPA框架)中,请使用此属性将其启用。 609 | 610 | ```csharp 611 | app.UseApiResponseAndExceptionWrapper(new AutoWrapperOptions { IsApiOnly = false} ); 612 | ``` 613 | 614 | #### WrapWhenApiPathStartsWith 615 | 616 | 617 | 如果将`IsApiOnly`选项设置为`false`,则还可以指定 API路径的分段以进行验证。默认情况下,它设置为`/ api`。如果要将其设置为其他内容,则可以执行以下操作: 618 | 619 | ```csharp 620 | app.UseApiResponseAndExceptionWrapper( new AutoWrapperOptions { 621 | IsApiOnly = false, 622 | WrapWhenApiPathStartsWith = "/myapi" 623 | }); 624 | ``` 625 | 当请求包含`WrapWhenApiPathStartsWith`值时,这将激活`AutoWrapper`以拦截HTTP响应。 626 | 627 | 628 | >请注意,我仍然建议您在单独的项目中实现` API Controllers`,以重视关注点的分离,并避免将SPA和API的路由配置混合在一起。 629 | 630 | # AutoWrapIgnore Attribute 631 | 632 | 633 | 您可以使用`[AutoWrapIgnore]`过滤器属性来指定不需要包装的点。 634 | 635 | 例如: 636 | 637 | 638 | ```csharp 639 | [HttpGet] 640 | [AutoWrapIgnore] 641 | public async Task Get() 642 | { 643 | var data = await _personManager.GetAllAsync(); 644 | return Ok(data); 645 | } 646 | ``` 647 | or 648 | 649 | ```csharp 650 | [HttpGet] 651 | [AutoWrapIgnore] 652 | public async Task> Get() 653 | { 654 | return await _personManager.GetAllAsync(); 655 | } 656 | ``` 657 | 658 | # RequestDataLogIgnore Attribute 659 | 660 | 如果您不希望某些端点在请求中记录数据,则可以使用`[RequestDataLogIgnore]` 661 | 662 | ```csharp 663 | [HttpGet] 664 | [RequestDataLogIgnore] 665 | public async Task Post([FromBody] CreatePersonRequest personRequest) 666 | { 667 | //Rest of the code here 668 | } 669 | ``` 670 | 671 | 672 | 如果您有不需要包装的端点并且也不想在请求中记录数据,则可以使用`[AutoWrapIgnore]`属性并`将ShouldLogRequestData`属性设置为`false`。 673 | 674 | ```csharp 675 | [HttpGet] 676 | [AutoWrapIgnore(ShouldLogRequestData = false)] 677 | public async Task> Get() 678 | { 679 | //Rest of the code here 680 | } 681 | ``` 682 | 683 | # Support for Swagger 684 | [Swagger](https://swagger.io/) 提供API的高级文档,使开发人员可以引用API端点的详细信息并在必要时进行测试。这非常有用,特别是当您的`API`是公开的并且您希望许多开发人员使用它时。 685 | 686 | `AutoWrapper` 687 | 省略网址中带有`/swagger`的任何请求,因此您仍然可以导航到Swagger UI以获得API文档。 688 | 689 | # Exclude Paths 690 | 排除不需要包装的Api路径,支持三种排除方式: 691 | 692 | - 严格:请求路径与配置的路径必须完全一致才排除。 693 | - 起始于:请求路径开始于配置路径,便会被排除。 694 | - 正则:请求路径满足配置路径正则表达式的话,便会被排除。 695 | 696 | ```csharp 697 | app.UseApiResponseAndExceptionWrapper(new AutoWrapperOptions 698 | { 699 | ShowIsErrorFlagForSuccessfulResponse = true, 700 | ExcludePaths = new AutoWrapperExcludePaths[] { 701 | // 严格匹配 702 | new AutoWrapperExcludePaths("/Strict", ExcludeMode.Strict), 703 | // 匹配起始于路径 704 | new AutoWrapperExcludePaths("/dapr", ExcludeMode.StartWith), 705 | // 正则匹配 706 | new AutoWrapperExcludePaths("/notice/.*|/notice", ExcludeMode.Regex) 707 | } 708 | }); 709 | ``` 710 | 711 | 712 | # Support for SignalR 713 | 如果你有一个SigalR终结的,例如: 714 | ```csharp 715 | app.UseEndpoints(endpoints => 716 | { 717 | endpoints.MapControllers(); 718 | endpoints.MapHub("/notice"); 719 | }); 720 | ``` 721 | 那么可以使用ExcludePaths排除它,以便让SignalR终结点生效 722 | ```csharp 723 | app.UseApiResponseAndExceptionWrapper(new AutoWrapperOptions 724 | { 725 | ShowIsErrorFlagForSuccessfulResponse = true, 726 | ExcludePaths = new AutoWrapperExcludePaths[] { 727 | new AutoWrapperExcludePaths("/notice/.*|/notice", ExcludeMode.Regex) 728 | } 729 | }); 730 | ``` 731 | 732 | # Support for Dapr 733 | Dapr Pubsub请求被AutoWrapper包装后无法到达配置的Controller Action,需要排除`/dapr/`起始的系列路径,使dapr请求生效: 734 | ```csharp 735 | app.UseApiResponseAndExceptionWrapper(new AutoWrapperOptions 736 | { 737 | ShowIsErrorFlagForSuccessfulResponse = true, 738 | ExcludePaths = new AutoWrapperExcludePaths[] { 739 | new AutoWrapperExcludePaths("/dapr", ExcludeMode.StartWith) 740 | } 741 | }); 742 | ``` 743 | 744 | 745 | 746 | # Support for NetCoreApp2.1 and NetCoreApp2.2 747 | `AutoWrapper` 748 | 2.x-3.0版还支持.NET Core 2.1和2.2。您只需要先在`AutoWrapper.Core`之前安装Nuget包`Newtonsoft.json`即可。 749 | 750 | # Unwrapping the Result from .NET Client 751 | [AutoWrapper.Server](https://github.com/proudmonkey/AutoWrapper.Server) i 752 | 是一个简单的库,使您可以在C#.NET客户端代码中解开AutoWrapper的`ApiResponse`对象的`Result`属性。目的是将结果对象直接反序列化为匹配的模型,而无需创建`ApiResponse`模式。 753 | 754 | 例如: 755 | 756 | ```csharp 757 | [HttpGet] 758 | public async Task> Get() 759 | { 760 | var client = HttpClientFactory.Create(); 761 | var httpResponse = await client.GetAsync("https://localhost:5001/api/v1/persons"); 762 | 763 | IEnumerable persons = null; 764 | if (httpResponse.IsSuccessStatusCode) 765 | { 766 | var jsonString = await httpResponse.Content.ReadAsStringAsync(); 767 | persons = Unwrapper.Unwrap>(jsonString); 768 | } 769 | 770 | return persons; 771 | } 772 | ``` 773 | 774 | 775 | 有关更多信息,请参见: [AutoWrapper.Server](https://github.com/proudmonkey/AutoWrapper.Server) 776 | # Samples 777 | * [AutoWrapper: Prettify Your ASP.NET Core APIs with Meaningful Responses](http://vmsdurano.com/autowrapper-prettify-your-asp-net-core-apis-with-meaningful-responses/) 778 | * [AutoWrapper: Customizing the Default Response Output](http://vmsdurano.com/asp-net-core-with-autowrapper-customizing-the-default-response-output/) 779 | * [AutoWrapper Now Supports Problem Details For Your ASP.NET Core APIs](http://vmsdurano.com/autowrapper-now-supports-problemdetails/) 780 | * [AutoWrapper.Server: Sample Usage](http://vmsdurano.com/autowrapper-server-is-now-available/) 781 | 782 | # 反馈并给予好评!:star: 783 | 我敢肯定,这个项目还有很多事情需要改进。试试看,让我知道您的想法。 784 | 785 | 如果发现错误或要求新功能,请随时提交[ticket](https://github.com/proudmonkey/AutoWrapper/issues)。非常感谢您宝贵的反馈意见,以更好地改进该项目。如果您觉得此功能有用,请给它加星号,以表示您对该项目的支持。 786 | 787 | # 贡献者 788 | 789 | * **Vincent Maverick Durano** - [Blog](http://vmsdurano.com/) 790 | * **Huei Feng** - [Github Profile](https://github.com/hueifeng) 791 | * **ITninja04** - [Github Profile](https://github.com/ITninja04) 792 | * **Rahmat Slamet** - [Github Profile](https://github.com/arhen) 793 | * **abelfiore** - [Github Profile](https://github.com/abelfiore) 794 | * **chen1tian** - [Github Profile](https://github.com/chen1tian) 795 | 796 | 想要贡献?请阅读贡献文档 [here](https://github.com/proudmonkey/AutoWrapper/blob/master/CONTRIBUTING.md). 797 | 798 | # Release History 799 | 800 | See: [Release Log](https://github.com/proudmonkey/AutoWrapper/blob/master/RELEASE.MD) 801 | 802 | # License 803 | 804 | 该项目已获得MIT许可证的许可-有关详细信息,请参见[LICENSE.md](LICENSE)文件。 805 | 806 | # Donate 807 | 808 | 如果您觉得这个项目有用-或只是感到宽容,请考虑向我购买啤酒或咖啡。干杯! :beers: :coffee: 809 | | | | 810 | | ------------------------------------------------------------ | :----------------------------------------------------------: | 811 | | | [![BMC](https://github.com/proudmonkey/Resources/blob/master/donate_coffee.png)](https://www.buymeacoffee.com/ImP9gONBW) | 812 | 813 | 814 | 谢谢! 815 | -------------------------------------------------------------------------------- /RELEASE.MD: -------------------------------------------------------------------------------- 1 | 2 | # Release Log 3 | 4 | * 07/11/2023: AutoWrapper version `4.5.1` - added support for .NET 6. 5 | * 03/08/2021: AutoWrapper version `4.5.0` - added `ExcludePaths` option. 6 | * 02/04/2021: AutoWrapper version `4.4.0` - fixed bug on logging request data. 7 | * 01/12/2021: AutoWrapper version `4.3.2` - added `SwaggerPath` option. Default is `/swagger`. 8 | * 09/23/2020: AutoWrapper version `4.3.1` - added `ShowIsErrorFlagForSuccessfulResponse` option. 9 | * 08/03/2020: AutoWrapper version `4.3.0` - see [release notes](https://vmsdurano.com/autowrapper-4-3-0-released/). 10 | * 06/01/2020: AutoWrapper version `4.2.2` - fixed bug issued [here](https://github.com/proudmonkey/AutoWrapper/issues/55). 11 | * 06/01/2020: AutoWrapper version `4.2.1` - added Model Responses for ProblemDetails. 12 | * 06/01/2020: AutoWrapper version `4.2.0` - see [release notes](https://vmsdurano.com/autowrapper-4-2-0-released/). 13 | * 04/19/2020: AutoWrapper version `4.1.0` - see [release notes](https://vmsdurano.com/autowrapper-4-1-0-released/). 14 | * 03/15/2020: AutoWrapper version `4.1.0-rc` - added `[ResponseLogIgnore]` attribute and `ShouldLogResponse` property in `[AutoWrapIgnore]` attribute. 15 | * 03/15/2020: AutoWrapper version `4.0.1` - exposed `StatusCode` property in `ApiProblemDetailsException` object. 16 | * 03/03/2020: AutoWrapper version `4.0.0` - see [release notes](https://vmsdurano.com/autowrapper-now-supports-problemdetails/). 17 | * 02/26/2020: AutoWrapper version `4.0.0-rc` - added new options and support for Problem Details, bug fixes and code cleanup. 18 | * 02/02/2020: AutoWrapper version `3.0.0` - added new options, bug fix and code cleanup. More info, see [release notes](https://vmsdurano.com/autowrapper-version-3-0-0-released/). 19 | * 11/09/2019: AutoWrapper version `2.1.0` - added new options and features. More info, see [release notes](https://vmsdurano.com/autowrapper-version-2-1-0-released/). 20 | * 11/05/2019: AutoWrapper version `2.0.2` - added `UnAuthorize` and `BadRequest` method response. 21 | * 10/17/2019: AutoWrapper version `2.0.1` - added new features. More info, see [release notes](https://vmsdurano.com/autowrapper-version-2-is-now-released/). 22 | * 10/06/2019: AutoWrapper version `1.2.0` - refactor, cleanup and bugfixes for SPA support. 23 | * 10/04/2019: AutoWrapper version `1.1.0` - with newly added options. 24 | * 09/23/2019: AutoWrapper version `1.0.0` - offcial release. More info, see [release notes](https://vmsdurano.com/autowrapper-is-officially-released/). 25 | * 09/14/2019: AutoWrapper version `1.0.0-rc` - prerelease. 26 | -------------------------------------------------------------------------------- /src/AutoWrapper.Test/AutoWrapper.Test.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | netcoreapp3.0;netcoreapp3.1;net6.0 4 | false 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 3.1.2 24 | 25 | 26 | 27 | 28 | 29 | 30 | 3.0.3 31 | 32 | 33 | 34 | 35 | 36 | 37 | 3.1.2 38 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /src/AutoWrapper.Test/AutoWrapperMiddlewareTests.cs: -------------------------------------------------------------------------------- 1 | using AutoWrapper.Helpers; 2 | using AutoWrapper.Test.Helper; 3 | using AutoWrapper.Test.Models; 4 | using AutoWrapper.Wrappers; 5 | using Microsoft.AspNetCore.Builder; 6 | using Microsoft.AspNetCore.Hosting; 7 | using Microsoft.AspNetCore.Http; 8 | using Microsoft.AspNetCore.Mvc; 9 | using Microsoft.AspNetCore.Mvc.ModelBinding; 10 | using Microsoft.AspNetCore.TestHost; 11 | using Microsoft.Extensions.DependencyInjection; 12 | using Microsoft.Extensions.Logging; 13 | using Shouldly; 14 | using System; 15 | using System.Net.Http; 16 | using System.Threading.Tasks; 17 | using Xunit; 18 | 19 | namespace AutoWrapper.Test 20 | { 21 | public class AutoWrapperMiddlewareTests 22 | { 23 | 24 | [Fact(DisplayName = "DefaultTemplateNotResultData")] 25 | public async Task AutoWrapperDefaultTemplateNotResultData_Test() 26 | { 27 | var builder = new WebHostBuilder() 28 | .ConfigureServices(services => { services.AddMvcCore(); }) 29 | .Configure(app => 30 | { 31 | app.UseApiResponseAndExceptionWrapper(); 32 | app.Run(context => Task.FromResult(0)); 33 | }); 34 | var server = new TestServer(builder); 35 | var req = new HttpRequestMessage(HttpMethod.Get, ""); 36 | var rep = await server.CreateClient().SendAsync(req); 37 | var content = await rep.Content.ReadAsStringAsync(); 38 | var json = JsonHelper.ToJson(new ApiResponse("GET Request successful.", "", 0, null), null); 39 | Convert.ToInt32(rep.StatusCode).ShouldBe(200); 40 | content.ShouldBe(json); 41 | } 42 | 43 | 44 | 45 | [Fact(DisplayName = "DefaultTemplateWithResultData")] 46 | public async Task AutoWrapperDefaultTemplateWithResultData_Test() 47 | { 48 | var builder = new WebHostBuilder() 49 | .ConfigureServices(services => { services.AddMvcCore(); }) 50 | .Configure(app => 51 | { 52 | app.UseApiResponseAndExceptionWrapper(); 53 | app.Run(context => context.Response.WriteAsync("HueiFeng")); 54 | }); 55 | var server = new TestServer(builder); 56 | var req = new HttpRequestMessage(HttpMethod.Get, ""); 57 | var rep = await server.CreateClient().SendAsync(req); 58 | var content = await rep.Content.ReadAsStringAsync(); 59 | Convert.ToInt32(rep.StatusCode).ShouldBe(200); 60 | var json = JsonHelper.ToJson(new ApiResponse("GET Request successful.", "HueiFeng", 0, null), null); 61 | content.ShouldBe(json); 62 | } 63 | [Fact(DisplayName = "CustomMessage")] 64 | public async Task AutoWrapperCustomMessage_Test() 65 | { 66 | var builder = new WebHostBuilder() 67 | .ConfigureServices(services => { services.AddMvcCore(); }) 68 | .Configure(app => 69 | { 70 | app.UseApiResponseAndExceptionWrapper(); 71 | app.Run(context => context.Response.WriteAsync( 72 | new ApiResponse("customMessage.", "Test", 200).ToJson())); 73 | }); 74 | var server = new TestServer(builder); 75 | var req = new HttpRequestMessage(HttpMethod.Get, ""); 76 | var rep = await server.CreateClient().SendAsync(req); 77 | var content = await rep.Content.ReadAsStringAsync(); 78 | Convert.ToInt32(rep.StatusCode).ShouldBe(200); 79 | var json = JsonHelper.ToJson(new ApiResponse("customMessage.", "Test", 0, null), null); 80 | content.ShouldBe(json); 81 | } 82 | 83 | [Fact(DisplayName = "CustomMessageWithStatusCode")] 84 | public async Task AutoWrapperCustomMessageWithStatusCode_Test() 85 | { 86 | var builder = new WebHostBuilder() 87 | .ConfigureServices(services => { services.AddMvcCore(); }) 88 | .Configure(app => 89 | { 90 | app.UseApiResponseAndExceptionWrapper(options: new AutoWrapperOptions() 91 | { 92 | ShowStatusCode = true 93 | }); 94 | app.Run(context => context.Response.WriteAsync( 95 | new { firstName = "Test", lastName = "User", statusCode = 202 }.ToJson())); 96 | }); 97 | var server = new TestServer(builder); 98 | var req = new HttpRequestMessage(HttpMethod.Get, ""); 99 | var rep = await server.CreateClient().SendAsync(req); 100 | var content = await rep.Content.ReadAsStringAsync(); 101 | Convert.ToInt32(rep.StatusCode).ShouldBe(200); 102 | var json = JsonHelper.ToJson(new ApiResponse("GET Request successful.", new { firstName = "Test", lastName = "User", statusCode = 202 }, 200, null), null); 103 | content.ShouldBe(json); 104 | } 105 | 106 | [Fact(DisplayName = "CapturingModelStateApiException")] 107 | public async Task AutoWrapperCapturingModelState_ApiException_Test() 108 | { 109 | var dictionary = new ModelStateDictionary(); 110 | dictionary.AddModelError("name", "some error"); 111 | var builder = new WebHostBuilder() 112 | .ConfigureServices(services => { services.AddMvcCore(); }) 113 | .Configure(app => 114 | { 115 | app.UseApiResponseAndExceptionWrapper(); 116 | 117 | app.Run(context => throw new ApiException(dictionary["name"])); 118 | }); 119 | Exception ex; 120 | try 121 | { 122 | throw new ApiException(dictionary["name"]); 123 | } 124 | catch (Exception e) 125 | { 126 | ex = e; 127 | } 128 | var server = new TestServer(builder); 129 | var req = new HttpRequestMessage(HttpMethod.Get, ""); 130 | var rep = await server.CreateClient().SendAsync(req); 131 | var content = await rep.Content.ReadAsStringAsync(); 132 | Convert.ToInt32(rep.StatusCode).ShouldBe(400); 133 | var ex1 = ex as ApiException; 134 | var json = JsonHelper.ToJson(new ApiResponse(0, ex1.CustomError), null); 135 | content.ShouldBe(json); 136 | } 137 | [Fact(DisplayName = "CapturingModelStateApiProblemDetailsException")] 138 | public async Task AutoWrapperCapturingModelState_ApiProblemDetailsException_Test() 139 | { 140 | var dictionary = new ModelStateDictionary(); 141 | dictionary.AddModelError("name", "some error"); 142 | var builder = new WebHostBuilder() 143 | .ConfigureServices(services => { services.AddMvcCore(); }) 144 | .Configure(app => 145 | { 146 | app.UseApiResponseAndExceptionWrapper(new AutoWrapperOptions { UseApiProblemDetailsException = true }); 147 | app.Run(context => throw new ApiProblemDetailsException(dictionary)); 148 | }); 149 | var server = new TestServer(builder); 150 | var req = new HttpRequestMessage(HttpMethod.Get, ""); 151 | var rep = await server.CreateClient().SendAsync(req); 152 | var content = await rep.Content.ReadAsStringAsync(); 153 | Convert.ToInt32(rep.StatusCode).ShouldBe(422); 154 | var str = "{\"isError\":true,\"errors\":null,\"validationErrors\":[{\"name\":\"name\",\"reason\":\"some error\"}],\"details\":null,\"type\":\"https://httpstatuses.com/422\",\"title\":\"Unprocessable Entity\",\"status\":422,\"detail\":\"Your request parameters didn't validate.\",\"instance\":\"/\"}"; 155 | str.ShouldBe(content); 156 | } 157 | 158 | [Fact(DisplayName = "ThrowingExceptionMessageApiException")] 159 | public async Task AutoWrapperThrowingExceptionMessage_ApiException_Test() 160 | { 161 | var builder = new WebHostBuilder() 162 | .ConfigureServices(services => { services.AddMvcCore(); }) 163 | .Configure(app => 164 | { 165 | app.UseApiResponseAndExceptionWrapper(); 166 | app.Run(context => throw new ApiException("does not exist.", 404)); 167 | }); 168 | var server = new TestServer(builder); 169 | var req = new HttpRequestMessage(HttpMethod.Get, ""); 170 | var rep = await server.CreateClient().SendAsync(req); 171 | var content = await rep.Content.ReadAsStringAsync(); 172 | Convert.ToInt32(rep.StatusCode).ShouldBe(404); 173 | var ex1 = new ApiException("does not exist.", 404); 174 | var json = JsonHelper.ToJson( 175 | new ApiResponse(0, new ApiError(ex1.Message) { ReferenceErrorCode = ex1.ReferenceErrorCode, ReferenceDocumentLink = ex1.ReferenceDocumentLink }) 176 | , null); 177 | content.ShouldBe(json); 178 | 179 | } 180 | 181 | [Fact(DisplayName = "ThrowingExceptionMessageApiProblemDetailsException")] 182 | public async Task AutoWrapperThrowingExceptionMessage_ApiProblemDetailsException_Test() 183 | { 184 | var builder = new WebHostBuilder() 185 | .ConfigureServices(services => { services.AddMvcCore(); }) 186 | .Configure(app => 187 | { 188 | app.UseApiResponseAndExceptionWrapper(new AutoWrapperOptions { UseApiProblemDetailsException = true }); 189 | app.Run(context => throw new ApiProblemDetailsException("does not exist.", 404)); 190 | }); 191 | var server = new TestServer(builder); 192 | var req = new HttpRequestMessage(HttpMethod.Get, ""); 193 | var rep = await server.CreateClient().SendAsync(req); 194 | var content = await rep.Content.ReadAsStringAsync(); 195 | Convert.ToInt32(rep.StatusCode).ShouldBe(404); 196 | var str = "{\"isError\":true,\"errors\":null,\"validationErrors\":null,\"details\":null,\"type\":\"https://httpstatuses.com/404\",\"title\":\"does not exist.\",\"status\":404,\"detail\":null,\"instance\":\"/\"}"; 197 | str.ShouldBe(content); 198 | } 199 | 200 | [Fact(DisplayName = "ModelValidations")] 201 | public async Task AutoWrapperModelValidations_Test() 202 | { 203 | var builder = new WebHostBuilder() 204 | .ConfigureServices(services => 205 | { 206 | services.Configure(options => 207 | { 208 | options.SuppressModelStateInvalidFilter = true; 209 | }); 210 | services.AddMvcCore(); 211 | }) 212 | .Configure(app => 213 | { 214 | app.UseApiResponseAndExceptionWrapper(); 215 | app.Run(context => context.Response.WriteAsync( 216 | new ApiResponse("customMessage.", "Test", 200).ToJson())); 217 | }); 218 | var server = new TestServer(builder); 219 | var req = new HttpRequestMessage(HttpMethod.Get, ""); 220 | var rep = await server.CreateClient().SendAsync(req); 221 | var content = await rep.Content.ReadAsStringAsync(); 222 | Convert.ToInt32(rep.StatusCode).ShouldBe(200); 223 | var options = new AutoWrapperOptions(); 224 | var jsonSettings = JSONHelper.GetJSONSettings(options.IgnoreNullValue, options.ReferenceLoopHandling, options.UseCamelCaseNamingStrategy); 225 | var json = JsonHelper.ToJson(new ApiResponse("customMessage.", "Test", 0, null), jsonSettings.Settings); 226 | content.ShouldBe(json); 227 | } 228 | 229 | [Fact(DisplayName = "CustomErrorObject")] 230 | public async Task AutoWrapperCustomErrorObject_Test() 231 | { 232 | var builder = new WebHostBuilder() 233 | .ConfigureServices(services => 234 | { 235 | services.AddMvcCore(); 236 | }) 237 | .Configure(app => 238 | { 239 | app.UseApiResponseAndExceptionWrapper(); 240 | app.Run(context => 241 | throw new ApiException( 242 | new Error("An error blah.", "InvalidRange", 243 | new InnerError("12345678", "2020-03-20") 244 | ))); 245 | }); 246 | var server = new TestServer(builder); 247 | var req = new HttpRequestMessage(HttpMethod.Get, ""); 248 | var rep = await server.CreateClient().SendAsync(req); 249 | var content = await rep.Content.ReadAsStringAsync(); 250 | Convert.ToInt32(rep.StatusCode).ShouldBe(400); 251 | Exception ex; 252 | try 253 | { 254 | throw new ApiException( 255 | new Error("An error blah.", "InvalidRange", 256 | new InnerError("12345678", "2020-03-20") 257 | )); 258 | } 259 | catch (Exception e) 260 | { 261 | ex = e; 262 | } 263 | var ex1 = ex as ApiException; 264 | var options = new AutoWrapperOptions(); 265 | var jsonSettings = JSONHelper.GetJSONSettings(options.IgnoreNullValue, options.ReferenceLoopHandling, options.UseCamelCaseNamingStrategy); 266 | var json = JsonHelper.ToJson(new ApiResponse(0, ex1.CustomError), jsonSettings.Settings); 267 | json.ToJson().ShouldBe(content.ToJson()); 268 | } 269 | 270 | 271 | [Fact(DisplayName = "CustomResponse")] 272 | public async Task AutoWrapperCustomResponse_Test() 273 | { 274 | var builder = new WebHostBuilder() 275 | .ConfigureServices(services => 276 | { 277 | services.AddMvcCore(); 278 | }) 279 | .Configure(app => 280 | { 281 | app.UseApiResponseAndExceptionWrapper(new AutoWrapperOptions { UseCustomSchema = true }); 282 | app.Run(context => context.Response.WriteAsync(new MyCustomApiResponse("Mr.A").ToJson())); 283 | }); 284 | var server = new TestServer(builder); 285 | var req = new HttpRequestMessage(HttpMethod.Get, ""); 286 | var rep = await server.CreateClient().SendAsync(req); 287 | var content = await rep.Content.ReadAsStringAsync(); 288 | Convert.ToInt32(rep.StatusCode).ShouldBe(200); 289 | var str = "{\"Code\":200,\"Payload\":\"Mr.A\",\"SentDate\":\"0001-01-01 00:00:00\"}"; 290 | str.ShouldBe(content); 291 | } 292 | 293 | } 294 | } 295 | -------------------------------------------------------------------------------- /src/AutoWrapper.Test/Helper/JsonHelper.cs: -------------------------------------------------------------------------------- 1 | using AutoWrapper.Helpers; 2 | using Newtonsoft.Json; 3 | using Newtonsoft.Json.Converters; 4 | 5 | namespace AutoWrapper.Test.Helper 6 | { 7 | public static class JsonHelper 8 | { 9 | /// 10 | /// 11 | /// 12 | /// 13 | /// 14 | public static string ToJson(this object obj) 15 | { 16 | var timeConverter = new IsoDateTimeConverter { DateTimeFormat = "yyyy-MM-dd HH:mm:ss" }; 17 | return JsonConvert.SerializeObject(obj, timeConverter); 18 | } 19 | /// 20 | /// 21 | /// 22 | /// 23 | /// 24 | public static string ToJson(object obj, JsonSerializerSettings settings) 25 | { 26 | var options = new AutoWrapperOptions(); 27 | var jsonSettings = JSONHelper.GetJSONSettings(options.IgnoreNullValue, options.ReferenceLoopHandling, options.UseCamelCaseNamingStrategy); 28 | return JsonConvert.SerializeObject(obj, settings ?? jsonSettings); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/AutoWrapper.Test/Models/MapResponseCustomErrorObject.cs: -------------------------------------------------------------------------------- 1 | namespace AutoWrapper.Test.Models 2 | { 3 | public class MapResponseCustomErrorObject 4 | { 5 | [AutoWrapperPropertyMap(Prop.ResponseException)] 6 | public object Error { get; set; } 7 | } 8 | public class Error 9 | { 10 | public string Message { get; set; } 11 | 12 | public string Code { get; set; } 13 | public InnerError InnerError { get; set; } 14 | 15 | public Error(string message, string code, InnerError inner) 16 | { 17 | this.Message = message; 18 | this.Code = code; 19 | this.InnerError = inner; 20 | } 21 | 22 | } 23 | 24 | public class InnerError 25 | { 26 | public string RequestId { get; set; } 27 | public string Date { get; set; } 28 | 29 | public InnerError(string reqId, string reqDate) 30 | { 31 | this.RequestId = reqId; 32 | this.Date = reqDate; 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/AutoWrapper.Test/Models/MapResponseObject.cs: -------------------------------------------------------------------------------- 1 | namespace AutoWrapper.Test.Models 2 | { 3 | public class MapResponseObject 4 | { 5 | [AutoWrapperPropertyMap(Prop.Result)] 6 | public object Data { get; set; } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/AutoWrapper.Test/Models/MyCustomApiResponse.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace AutoWrapper.Test.Models 4 | { 5 | public class MyCustomApiResponse 6 | { 7 | public int Code { get; set; } 8 | public string Message { get; set; } 9 | public object Payload { get; set; } 10 | public DateTime SentDate { get; set; } 11 | public Pagination Pagination { get; set; } 12 | 13 | public MyCustomApiResponse(DateTime sentDate, 14 | object payload = null, 15 | string message = "", 16 | int statusCode = 200, 17 | Pagination pagination = null) 18 | { 19 | this.Code = statusCode; 20 | this.Message = message == string.Empty ? "Success" : message; 21 | this.Payload = payload; 22 | this.SentDate = sentDate; 23 | this.Pagination = pagination; 24 | } 25 | 26 | public MyCustomApiResponse(DateTime sentDate, 27 | object payload = null, 28 | Pagination pagination = null) 29 | { 30 | this.Code = 200; 31 | this.Message = "Success"; 32 | this.Payload = payload; 33 | this.SentDate = sentDate; 34 | this.Pagination = pagination; 35 | } 36 | 37 | public MyCustomApiResponse(object payload) 38 | { 39 | this.Code = 200; 40 | this.Payload = payload; 41 | } 42 | 43 | } 44 | 45 | public class Pagination 46 | { 47 | public int TotalItemsCount { get; set; } 48 | public int PageSize { get; set; } 49 | public int CurrentPage { get; set; } 50 | public int TotalPages { get; set; } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/AutoWrapper.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.29230.61 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AutoWrapper", "AutoWrapper\AutoWrapper.csproj", "{3CE65976-128D-486C-9454-A42055A361F2}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AutoWrapper.Test", "AutoWrapper.Test\AutoWrapper.Test.csproj", "{2E533B82-6A8C-466C-92AC-42C2346DB5C5}" 9 | EndProject 10 | Global 11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 12 | Debug|Any CPU = Debug|Any CPU 13 | Release|Any CPU = Release|Any CPU 14 | EndGlobalSection 15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 16 | {3CE65976-128D-486C-9454-A42055A361F2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 17 | {3CE65976-128D-486C-9454-A42055A361F2}.Debug|Any CPU.Build.0 = Debug|Any CPU 18 | {3CE65976-128D-486C-9454-A42055A361F2}.Release|Any CPU.ActiveCfg = Release|Any CPU 19 | {3CE65976-128D-486C-9454-A42055A361F2}.Release|Any CPU.Build.0 = Release|Any CPU 20 | {2E533B82-6A8C-466C-92AC-42C2346DB5C5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 21 | {2E533B82-6A8C-466C-92AC-42C2346DB5C5}.Debug|Any CPU.Build.0 = Debug|Any CPU 22 | {2E533B82-6A8C-466C-92AC-42C2346DB5C5}.Release|Any CPU.ActiveCfg = Release|Any CPU 23 | {2E533B82-6A8C-466C-92AC-42C2346DB5C5}.Release|Any CPU.Build.0 = Release|Any CPU 24 | EndGlobalSection 25 | GlobalSection(SolutionProperties) = preSolution 26 | HideSolutionNode = FALSE 27 | EndGlobalSection 28 | GlobalSection(ExtensibilityGlobals) = postSolution 29 | SolutionGuid = {D0276B23-82B9-4372-95E8-631B10BB65D8} 30 | EndGlobalSection 31 | EndGlobal 32 | -------------------------------------------------------------------------------- /src/AutoWrapper/AutoWrapper.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Builder; 2 | 3 | namespace AutoWrapper 4 | { 5 | public static class AutoWrapperExtension 6 | { 7 | public static IApplicationBuilder UseApiResponseAndExceptionWrapper(this IApplicationBuilder builder, AutoWrapperOptions options = default) 8 | { 9 | options ??= new AutoWrapperOptions(); 10 | return builder.UseMiddleware(options); 11 | } 12 | 13 | public static IApplicationBuilder UseApiResponseAndExceptionWrapper(this IApplicationBuilder builder, AutoWrapperOptions options = default) 14 | { 15 | options ??= new AutoWrapperOptions(); 16 | return builder.UseMiddleware>(options); 17 | } 18 | } 19 | 20 | } 21 | -------------------------------------------------------------------------------- /src/AutoWrapper/AutoWrapper.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netcoreapp3.0;netcoreapp3.1;net6.0 5 | Vincent Maverick Durano 6 | A simple yet customizable HTTP response wrapper and exception handler for ASP.NET Core APIs. 7 | 8 | https://github.com/proudmonkey/AutoWrapper 9 | https://github.com/proudmonkey/AutoWrapper 10 | Git 11 | 4.5.1 12 | AutoWrapper, REST, API, WebAPI, ASP.NETCore, Middleware, HttpResponseWrapper, NETCore, C#, ApiResponseAndExceptionWrapper, APIWrapper, ResponseWrapper, ExceptionWrapper, ProblemDetails 13 | See project repo for release notes. 14 | true 15 | MIT 16 | AutoWrapper.Core 17 | icon.png 18 | true 19 | README.md 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | -------------------------------------------------------------------------------- /src/AutoWrapper/AutoWrapperExcludePath.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace AutoWrapper 6 | { 7 | public enum ExcludeMode 8 | { 9 | Strict = 1, 10 | StartWith = 2, 11 | Regex = 3 12 | } 13 | 14 | public class AutoWrapperExcludePath 15 | { 16 | public AutoWrapperExcludePath(string path, ExcludeMode excludeMode = ExcludeMode.Strict) 17 | { 18 | Path = path; 19 | ExcludeMode = excludeMode; 20 | } 21 | 22 | public string Path { get; set; } 23 | 24 | public ExcludeMode ExcludeMode { get; set; } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/AutoWrapper/AutoWrapperMembers.cs: -------------------------------------------------------------------------------- 1 | using AutoWrapper.Extensions; 2 | using AutoWrapper.Helpers; 3 | using AutoWrapper.Wrappers; 4 | using Microsoft.AspNetCore.Http; 5 | using Microsoft.AspNetCore.Mvc; 6 | using Microsoft.AspNetCore.Mvc.Infrastructure; 7 | using Microsoft.Extensions.Logging; 8 | using Newtonsoft.Json; 9 | using Newtonsoft.Json.Linq; 10 | using System; 11 | using System.Collections.Generic; 12 | using System.IO; 13 | using System.Linq; 14 | using System.Text; 15 | using System.Text.RegularExpressions; 16 | using System.Threading.Tasks; 17 | using static Microsoft.AspNetCore.Http.StatusCodes; 18 | 19 | namespace AutoWrapper 20 | { 21 | internal class AutoWrapperMembers 22 | { 23 | 24 | private readonly AutoWrapperOptions _options; 25 | private readonly ILogger _logger; 26 | private readonly JsonSerializerSettings _jsonSettings; 27 | public readonly Dictionary _propertyMappings; 28 | private readonly bool _hasSchemaForMappping; 29 | public AutoWrapperMembers(AutoWrapperOptions options, 30 | ILogger logger, 31 | JsonSerializerSettings jsonSettings, 32 | Dictionary propertyMappings = null, 33 | bool hasSchemaForMappping = false) 34 | { 35 | _options = options; 36 | _logger = logger; 37 | _jsonSettings = jsonSettings; 38 | _propertyMappings = propertyMappings; 39 | _hasSchemaForMappping = hasSchemaForMappping; 40 | } 41 | 42 | public async Task GetRequestBodyAsync(HttpRequest request) 43 | { 44 | var httpMethodsWithRequestBody = new[] { "POST", "PUT", "PATCH" }; 45 | var hasRequestBody = httpMethodsWithRequestBody.Any(x => x.Equals(request.Method.ToUpper())); 46 | string requestBody = default; 47 | 48 | if (hasRequestBody) 49 | { 50 | request.EnableBuffering(); 51 | 52 | using var memoryStream = new MemoryStream(); 53 | await request.Body.CopyToAsync(memoryStream); 54 | requestBody = Encoding.UTF8.GetString(memoryStream.ToArray()); 55 | request.Body.Seek(0, SeekOrigin.Begin); 56 | } 57 | return requestBody; 58 | } 59 | 60 | public async Task ReadResponseBodyStreamAsync(Stream bodyStream) 61 | { 62 | bodyStream.Seek(0, SeekOrigin.Begin); 63 | var responseBody = await new StreamReader(bodyStream).ReadToEndAsync(); 64 | bodyStream.Seek(0, SeekOrigin.Begin); 65 | 66 | var (IsEncoded, ParsedText) = responseBody.VerifyBodyContent(); 67 | 68 | return IsEncoded ? ParsedText : responseBody; 69 | } 70 | 71 | public async Task RevertResponseBodyStreamAsync(Stream bodyStream, Stream orginalBodyStream) 72 | { 73 | bodyStream.Seek(0, SeekOrigin.Begin); 74 | await bodyStream.CopyToAsync(orginalBodyStream); 75 | } 76 | 77 | public async Task HandleExceptionAsync(HttpContext context, System.Exception exception) 78 | { 79 | if (_options.UseCustomExceptionFormat) 80 | { 81 | await WriteFormattedResponseToHttpContextAsync(context, context.Response.StatusCode, exception.GetBaseException().Message); 82 | return; 83 | } 84 | 85 | string exceptionMessage = default; 86 | object apiError; 87 | int httpStatusCode; 88 | 89 | if (exception is ApiException) 90 | { 91 | var ex = exception as ApiException; 92 | if (ex.IsModelValidatonError) 93 | { 94 | apiError = new ApiError(ResponseMessage.ValidationError, ex.Errors) { ReferenceErrorCode = ex.ReferenceErrorCode, ReferenceDocumentLink = ex.ReferenceDocumentLink }; 95 | } 96 | else if (ex.IsCustomErrorObject) 97 | { 98 | apiError = ex.CustomError; 99 | } 100 | else 101 | { 102 | apiError = new ApiError(ex.Message) { ReferenceErrorCode = ex.ReferenceErrorCode, ReferenceDocumentLink = ex.ReferenceDocumentLink }; 103 | } 104 | 105 | httpStatusCode = ex.StatusCode; 106 | } 107 | else if (exception is UnauthorizedAccessException) 108 | { 109 | apiError = new ApiError(ResponseMessage.UnAuthorized); 110 | httpStatusCode = Status401Unauthorized; 111 | } 112 | else 113 | { 114 | string stackTrace = null; 115 | 116 | if (_options.IsDebug) 117 | { 118 | exceptionMessage = $"{ exceptionMessage } { exception.GetBaseException().Message }"; 119 | stackTrace = exception.StackTrace; 120 | } 121 | else 122 | { 123 | exceptionMessage = ResponseMessage.Unhandled; 124 | } 125 | 126 | apiError = new ApiError(exceptionMessage) { Details = stackTrace }; 127 | httpStatusCode = Status500InternalServerError; 128 | } 129 | 130 | 131 | if (_options.EnableExceptionLogging) 132 | { 133 | var errorMessage = apiError is ApiError ? ((ApiError)apiError).ExceptionMessage : ResponseMessage.Exception; 134 | _logger.Log(LogLevel.Error, exception, $"[{httpStatusCode}]: { errorMessage }"); 135 | } 136 | 137 | var jsonString = ConvertToJSONString(GetErrorResponse(httpStatusCode, apiError)); 138 | 139 | await WriteFormattedResponseToHttpContextAsync(context, httpStatusCode, jsonString); 140 | } 141 | 142 | public async Task HandleUnsuccessfulRequestAsync(HttpContext context, object body, int httpStatusCode) 143 | { 144 | var (IsEncoded, ParsedText) = body.ToString().VerifyBodyContent(); 145 | 146 | if (IsEncoded && _options.UseCustomExceptionFormat) 147 | { 148 | await WriteFormattedResponseToHttpContextAsync(context, httpStatusCode, body.ToString()); 149 | return; 150 | } 151 | 152 | var bodyText = IsEncoded ? JsonConvert.DeserializeObject(ParsedText) : body.ToString(); 153 | ApiError apiError = !string.IsNullOrEmpty(body.ToString()) ? new ApiError(bodyText) : WrapUnsucessfulError(httpStatusCode); 154 | 155 | var jsonString = ConvertToJSONString(GetErrorResponse(httpStatusCode, apiError)); 156 | await WriteFormattedResponseToHttpContextAsync(context, httpStatusCode, jsonString); 157 | } 158 | 159 | public async Task HandleSuccessfulRequestAsync(HttpContext context, object body, int httpStatusCode) 160 | { 161 | var bodyText = !body.ToString().IsValidJson() ? ConvertToJSONString(body) : body.ToString(); 162 | 163 | dynamic bodyContent = JsonConvert.DeserializeObject(bodyText); 164 | 165 | Type type = bodyContent?.GetType(); 166 | 167 | string jsonString; 168 | if (type.Equals(typeof(JObject))) 169 | { 170 | ApiResponse apiResponse = new ApiResponse(); 171 | 172 | if (_options.UseCustomSchema) 173 | { 174 | var formatJson = _options.IgnoreNullValue ? JSONHelper.RemoveEmptyChildren(bodyContent) : bodyContent; 175 | await WriteFormattedResponseToHttpContextAsync(context, httpStatusCode, JsonConvert.SerializeObject(formatJson)); 176 | } 177 | else 178 | { 179 | if (_hasSchemaForMappping && (_propertyMappings.Count == 0 || _propertyMappings == null)) 180 | throw new ApiException(ResponseMessage.NoMappingFound); 181 | else if (bodyText.Contains(nameof(ApiResponse.Result))) 182 | apiResponse = JsonConvert.DeserializeObject(bodyText); 183 | else 184 | apiResponse = new ApiResponse(); 185 | } 186 | 187 | if (apiResponse.StatusCode == 0 && apiResponse.Result == null && apiResponse.ResponseException == null) 188 | jsonString = ConvertToJSONString(httpStatusCode, bodyContent, context.Request.Method); 189 | else if ((apiResponse.StatusCode != httpStatusCode || apiResponse.Result != null) || 190 | (apiResponse.StatusCode == httpStatusCode && apiResponse.Result == null)) 191 | { 192 | httpStatusCode = apiResponse.StatusCode; // in case response is not 200 (e.g 201) 193 | jsonString = ConvertToJSONString(GetSucessResponse(apiResponse, context.Request.Method)); 194 | 195 | } 196 | else 197 | jsonString = ConvertToJSONString(httpStatusCode, bodyContent, context.Request.Method); 198 | } 199 | else 200 | { 201 | var validated = ValidateSingleValueType(bodyContent); 202 | object result = validated.Item1 ? validated.Item2 : bodyContent; 203 | jsonString = ConvertToJSONString(httpStatusCode, result, context.Request.Method); 204 | } 205 | 206 | await WriteFormattedResponseToHttpContextAsync(context, httpStatusCode, jsonString); 207 | } 208 | 209 | public async Task HandleNotApiRequestAsync(HttpContext context) 210 | { 211 | string configErrorText = ResponseMessage.NotApiOnly; 212 | context.Response.ContentLength = configErrorText != null ? Encoding.UTF8.GetByteCount(configErrorText) : 0; 213 | await context.Response.WriteAsync(configErrorText); 214 | } 215 | 216 | public bool IsSwagger(HttpContext context, string swaggerPath) 217 | { 218 | return context.Request.Path.StartsWithSegments(new PathString(swaggerPath)); 219 | } 220 | 221 | public bool IsExclude(HttpContext context, IEnumerable excludePaths) 222 | { 223 | if (excludePaths == null || excludePaths.Count() == 0) 224 | { 225 | return false; 226 | } 227 | 228 | return excludePaths.Any(x => 229 | { 230 | switch (x.ExcludeMode) 231 | { 232 | default: 233 | case ExcludeMode.Strict: 234 | return context.Request.Path.Value == x.Path; 235 | case ExcludeMode.StartWith: 236 | return context.Request.Path.StartsWithSegments(new PathString(x.Path)); 237 | case ExcludeMode.Regex: 238 | Regex regExclude = new Regex(x.Path); 239 | return regExclude.IsMatch(context.Request.Path.Value); 240 | } 241 | }); 242 | } 243 | 244 | public bool IsApi(HttpContext context) 245 | { 246 | if (_options.IsApiOnly 247 | && !context.Request.Path.Value.Contains(".js") 248 | && !context.Request.Path.Value.Contains(".css") 249 | && !context.Request.Path.Value.Contains(".html")) 250 | return true; 251 | 252 | return context.Request.Path.StartsWithSegments(new PathString(_options.WrapWhenApiPathStartsWith)); 253 | } 254 | 255 | public async Task WrapIgnoreAsync(HttpContext context, object body) 256 | { 257 | var bodyText = body.ToString(); 258 | context.Response.ContentLength = bodyText != null ? Encoding.UTF8.GetByteCount(bodyText) : 0; 259 | await context.Response.WriteAsync(bodyText); 260 | } 261 | 262 | public async Task HandleProblemDetailsExceptionAsync(HttpContext context, IActionResultExecutor executor, object body, Exception exception = null) 263 | { 264 | await new ApiProblemDetailsMember().WriteProblemDetailsAsync(context, executor, body, exception, _options.IsDebug); 265 | 266 | if (_options.EnableExceptionLogging && exception != null) 267 | { 268 | _logger.Log(LogLevel.Error, exception, $"[{context.Response.StatusCode}]: { exception.GetBaseException().Message }"); 269 | } 270 | } 271 | 272 | public bool IsRequestSuccessful(int statusCode) 273 | { 274 | return (statusCode >= 200 && statusCode < 400); 275 | } 276 | 277 | #region Private Members 278 | 279 | private async Task WriteFormattedResponseToHttpContextAsync(HttpContext context, int httpStatusCode, string jsonString) 280 | { 281 | context.Response.StatusCode = httpStatusCode; 282 | context.Response.ContentType = TypeIdentifier.JSONHttpContentMediaType; 283 | context.Response.ContentLength = jsonString != null ? Encoding.UTF8.GetByteCount(jsonString) : 0; 284 | await context.Response.WriteAsync(jsonString); 285 | } 286 | 287 | private string ConvertToJSONString(int httpStatusCode, object content, string httpMethod) 288 | { 289 | var apiResponse = new ApiResponse($"{httpMethod} {ResponseMessage.Success}", content, !_options.ShowStatusCode ? 0 : httpStatusCode, GetApiVersion()); 290 | apiResponse.IsError = SetIsErrorValue(apiResponse.IsError); 291 | return JsonConvert.SerializeObject(apiResponse, _jsonSettings); 292 | } 293 | 294 | private string ConvertToJSONString(ApiResponse apiResponse) 295 | { 296 | apiResponse.StatusCode = !_options.ShowStatusCode ? 0 : apiResponse.StatusCode; 297 | apiResponse.IsError = SetIsErrorValue(apiResponse.IsError); 298 | return JsonConvert.SerializeObject(apiResponse, _jsonSettings); 299 | } 300 | 301 | private bool? SetIsErrorValue(bool? isError) 302 | { 303 | return isError.HasValue ? true : _options.ShowIsErrorFlagForSuccessfulResponse ? false : (bool?)null; 304 | } 305 | 306 | private string ConvertToJSONString(ApiError apiError) => JsonConvert.SerializeObject(apiError, _jsonSettings); 307 | 308 | private string ConvertToJSONString(object rawJSON) => JsonConvert.SerializeObject(rawJSON, _jsonSettings); 309 | 310 | private ApiError WrapUnsucessfulError(int statusCode) => 311 | statusCode switch 312 | { 313 | Status204NoContent => new ApiError(ResponseMessage.NotContent), 314 | Status400BadRequest => new ApiError(ResponseMessage.BadRequest), 315 | Status401Unauthorized => new ApiError(ResponseMessage.UnAuthorized), 316 | Status404NotFound => new ApiError(ResponseMessage.NotFound), 317 | Status405MethodNotAllowed => new ApiError(ResponseMessage.MethodNotAllowed), 318 | Status415UnsupportedMediaType => new ApiError(ResponseMessage.MediaTypeNotSupported), 319 | _ => new ApiError(ResponseMessage.Unknown) 320 | 321 | }; 322 | 323 | 324 | private ApiResponse GetErrorResponse(int httpStatusCode, object apiError) 325 | => new ApiResponse(!_options.ShowStatusCode ? 0 : httpStatusCode, apiError) { Version = GetApiVersion() }; 326 | 327 | private ApiResponse GetSucessResponse(ApiResponse apiResponse, string httpMethod) 328 | { 329 | apiResponse.Message ??= $"{httpMethod} {ResponseMessage.Success}"; 330 | apiResponse.Version = GetApiVersion(); 331 | return apiResponse; 332 | } 333 | 334 | private string GetApiVersion() => !_options.ShowApiVersion ? null : _options.ApiVersion; 335 | 336 | private (bool, object) ValidateSingleValueType(object value) 337 | { 338 | var result = value.ToString(); 339 | if (result.IsWholeNumber()) { return (true, result.ToInt64()); } 340 | if (result.IsDecimalNumber()) { return (true, result.ToDecimal()); } 341 | if (result.IsBoolean()) { return (true, result.ToBoolean()); } 342 | 343 | return (false, value); 344 | } 345 | 346 | #endregion 347 | } 348 | 349 | } 350 | -------------------------------------------------------------------------------- /src/AutoWrapper/AutoWrapperMiddleware.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Http; 2 | using Microsoft.Extensions.Logging; 3 | using System.Threading.Tasks; 4 | using AutoWrapper.Base; 5 | using Microsoft.AspNetCore.Mvc.Infrastructure; 6 | using Microsoft.AspNetCore.Mvc; 7 | 8 | namespace AutoWrapper 9 | { 10 | internal class AutoWrapperMiddleware : WrapperBase 11 | { 12 | private readonly AutoWrapperMembers _awm; 13 | public AutoWrapperMiddleware(RequestDelegate next, AutoWrapperOptions options, ILogger logger, IActionResultExecutor executor) : base(next, options, logger, executor) 14 | { 15 | var jsonSettings = Helpers.JSONHelper.GetJSONSettings(options.IgnoreNullValue, options.ReferenceLoopHandling, options.UseCamelCaseNamingStrategy); 16 | _awm = new AutoWrapperMembers(options, logger, jsonSettings); 17 | } 18 | 19 | public async Task InvokeAsync(HttpContext context) 20 | { 21 | await InvokeAsyncBase(context, _awm); 22 | } 23 | } 24 | 25 | internal class AutoWrapperMiddleware : WrapperBase 26 | { 27 | private readonly AutoWrapperMembers _awm; 28 | public AutoWrapperMiddleware(RequestDelegate next, AutoWrapperOptions options, ILogger logger, IActionResultExecutor executor) : base(next, options, logger, executor) 29 | { 30 | var (Settings, Mappings) = Helpers.JSONHelper.GetJSONSettings(options.IgnoreNullValue, options.ReferenceLoopHandling, options.UseCamelCaseNamingStrategy); 31 | _awm = new AutoWrapperMembers(options, logger, Settings, Mappings, true); 32 | } 33 | 34 | public async Task InvokeAsync(HttpContext context) 35 | { 36 | await InvokeAsyncBase(context, _awm); 37 | } 38 | 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/AutoWrapper/AutoWrapperOptions.cs: -------------------------------------------------------------------------------- 1 | using AutoWrapper.Base; 2 | using Newtonsoft.Json; 3 | using System.Collections.Generic; 4 | 5 | namespace AutoWrapper 6 | { 7 | public class AutoWrapperOptions : OptionBase 8 | { 9 | public bool UseCustomSchema { get; set; } = false; 10 | public ReferenceLoopHandling ReferenceLoopHandling { get; set; } = ReferenceLoopHandling.Ignore; 11 | public bool UseCustomExceptionFormat { get; set; } = false; 12 | public bool UseApiProblemDetailsException { get; set; } = false; 13 | public bool LogRequestDataOnException { get; set; } = true; 14 | public bool IgnoreWrapForOkRequests { get; set; } = false; 15 | public bool ShouldLogRequestData { get; set; } = true; 16 | 17 | public string SwaggerPath { get; set; } = "/swagger"; 18 | 19 | public IEnumerable ExcludePaths { get; set; } = null; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/AutoWrapper/AutoWrapperPropertyAttribute.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace AutoWrapper 4 | { 5 | 6 | [AttributeUsage(AttributeTargets.Field | AttributeTargets.Property | AttributeTargets.Parameter, AllowMultiple = false)] 7 | public sealed class AutoWrapperPropertyMapAttribute : Attribute 8 | { 9 | public string PropertyName { get; set; } = string.Empty; 10 | public AutoWrapperPropertyMapAttribute(){} 11 | /// 12 | /// Gets or sets the name of the property. 13 | /// 14 | /// The name of the property. 15 | 16 | public AutoWrapperPropertyMapAttribute(string propertyName) 17 | { 18 | PropertyName = propertyName; 19 | } 20 | } 21 | 22 | public class Prop 23 | { 24 | public const string Version = "Version"; 25 | public const string StatusCode = "StatusCode"; 26 | public const string Message = "Message"; 27 | public const string IsError = "IsError"; 28 | public const string Result = "Result"; 29 | public const string ResponseException = "ResponseException"; 30 | public const string ResponseException_ExceptionMessage = "ExceptionMessage"; 31 | public const string ResponseException_Details = "Details"; 32 | public const string ResponseException_ReferenceErrorCode = "ReferenceErrorCode"; 33 | public const string ResponseException_ReferenceDocumentLink = "ReferenceDocumentLink"; 34 | public const string ResponseException_ValidationErrors = "ValidationErrors"; 35 | public const string ResponseException_ValidationErrors_Field = "Field"; 36 | public const string ResponseException_ValidationErrors_Message = "Message"; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/AutoWrapper/Base/OptionBase.cs: -------------------------------------------------------------------------------- 1 | namespace AutoWrapper.Base 2 | { 3 | public abstract class OptionBase 4 | { 5 | /// 6 | /// Sets the Api version to be shown in the response. You must set the ShowApiVersion to true to see this value in the response. 7 | /// 8 | public string ApiVersion { get; set; } = "1.0.0.0"; 9 | /// 10 | /// Shows the stack trace information in the responseException details. 11 | /// 12 | public bool IsDebug { get; set; } = false; 13 | /// 14 | /// Shows the Api Version attribute in the response. 15 | /// 16 | public bool ShowApiVersion { get; set; } = false; 17 | 18 | /// 19 | /// Shows the StatusCode attribute in the response. 20 | /// 21 | public bool ShowStatusCode { get; set; } = false; 22 | 23 | /// 24 | /// Shows the IsError attribute in the response. 25 | /// 26 | public bool ShowIsErrorFlagForSuccessfulResponse { get; set; } = false; 27 | 28 | /// 29 | /// Use to indicate if the wrapper is used for API project only. 30 | /// Set this to false when you want to use the wrapper within an Angular, MVC, React or Blazor projects. 31 | /// 32 | public bool IsApiOnly { get; set; } = true; 33 | 34 | /// 35 | /// Tells the wrapper to ignore validation for string that contains HTML 36 | /// 37 | public bool BypassHTMLValidation { get; set; } = false; 38 | 39 | /// 40 | /// Set the Api path segment to validate. The default value is '/api'. Only works if IsApiOnly is set to false. 41 | /// 42 | public string WrapWhenApiPathStartsWith { get; set; } = "/api"; 43 | 44 | /// 45 | /// Tells the wrapper to ignore attributes with null values. Default is true. 46 | /// 47 | public bool IgnoreNullValue { get; set; } = true; 48 | 49 | /// 50 | /// Tells the wrapper to use camel case as the response format. Default is true. 51 | /// 52 | public bool UseCamelCaseNamingStrategy { get; set; } = true; 53 | 54 | /// 55 | /// Tells the wrapper whether to enable request and response logging. Default is true. 56 | /// 57 | public bool EnableResponseLogging { get; set; } = true; 58 | 59 | /// 60 | /// Tells the wrapper whether to enable exception logging. Default is true. 61 | /// 62 | public bool EnableExceptionLogging { get; set; } = true; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/AutoWrapper/Base/WrapperBase.cs: -------------------------------------------------------------------------------- 1 | using AutoWrapper.Extensions; 2 | using AutoWrapper.Helpers; 3 | using Microsoft.AspNetCore.Http; 4 | using Microsoft.AspNetCore.Mvc; 5 | using Microsoft.AspNetCore.Mvc.Infrastructure; 6 | using Microsoft.Extensions.Logging; 7 | using System; 8 | using System.Diagnostics; 9 | using System.IO; 10 | using System.Threading.Tasks; 11 | using AutoWrapper.Filters; 12 | using static Microsoft.AspNetCore.Http.StatusCodes; 13 | 14 | namespace AutoWrapper.Base 15 | { 16 | internal abstract class WrapperBase 17 | { 18 | private readonly RequestDelegate _next; 19 | private readonly AutoWrapperOptions _options; 20 | private readonly ILogger _logger; 21 | #pragma warning disable IDE1006 // 命名样式 22 | private IActionResultExecutor _executor { get; } 23 | #pragma warning restore IDE1006 // 命名样式 24 | public WrapperBase(RequestDelegate next, 25 | AutoWrapperOptions options, 26 | ILogger logger, 27 | IActionResultExecutor executor) 28 | { 29 | _next = next; 30 | _options = options; 31 | _logger = logger; 32 | _executor = executor; 33 | } 34 | 35 | public virtual async Task InvokeAsyncBase(HttpContext context, AutoWrapperMembers awm) 36 | { 37 | if (awm.IsSwagger(context, _options.SwaggerPath) || !awm.IsApi(context) || awm.IsExclude(context, _options.ExcludePaths)) 38 | await _next(context); 39 | else 40 | { 41 | var stopWatch = Stopwatch.StartNew(); 42 | var requestBody = await awm.GetRequestBodyAsync(context.Request); 43 | var originalResponseBodyStream = context.Response.Body; 44 | bool isRequestOk = false; 45 | 46 | using var memoryStream = new MemoryStream(); 47 | 48 | try 49 | { 50 | context.Response.Body = memoryStream; 51 | await _next.Invoke(context); 52 | 53 | if (context.Response.HasStarted) { LogResponseHasStartedError(); return; } 54 | 55 | var endpoint = context.GetEndpoint(); 56 | if (endpoint?.Metadata?.GetMetadata() is object) 57 | { 58 | await awm.RevertResponseBodyStreamAsync(memoryStream, originalResponseBodyStream); 59 | return; 60 | } 61 | 62 | var bodyAsText = await awm.ReadResponseBodyStreamAsync(memoryStream); 63 | context.Response.Body = originalResponseBodyStream; 64 | 65 | if (context.Response.StatusCode != Status304NotModified && context.Response.StatusCode != Status204NoContent) 66 | { 67 | 68 | if (!_options.IsApiOnly 69 | && (bodyAsText.IsHtml() 70 | && !_options.BypassHTMLValidation) 71 | && context.Response.StatusCode == Status200OK) 72 | { context.Response.StatusCode = Status404NotFound; } 73 | 74 | if (!context.Request.Path.StartsWithSegments(new PathString(_options.WrapWhenApiPathStartsWith)) 75 | && (bodyAsText.IsHtml() 76 | && !_options.BypassHTMLValidation) 77 | && context.Response.StatusCode == Status200OK) 78 | { 79 | if (memoryStream.Length > 0) { await awm.HandleNotApiRequestAsync(context); } 80 | return; 81 | } 82 | 83 | isRequestOk = awm.IsRequestSuccessful(context.Response.StatusCode); 84 | if (isRequestOk) 85 | { 86 | if (_options.IgnoreWrapForOkRequests) 87 | { 88 | await awm.WrapIgnoreAsync(context, bodyAsText); 89 | } 90 | else 91 | { 92 | await awm.HandleSuccessfulRequestAsync(context, bodyAsText, context.Response.StatusCode); 93 | } 94 | } 95 | else 96 | { 97 | if (_options.UseApiProblemDetailsException) 98 | { 99 | await awm.HandleProblemDetailsExceptionAsync(context, _executor, bodyAsText); 100 | return; 101 | } 102 | 103 | await awm.HandleUnsuccessfulRequestAsync(context, bodyAsText, context.Response.StatusCode); 104 | } 105 | } 106 | 107 | } 108 | catch (Exception exception) 109 | { 110 | if (context.Response.HasStarted) { LogResponseHasStartedError(); return; } 111 | 112 | if (_options.UseApiProblemDetailsException) 113 | { 114 | await awm.HandleProblemDetailsExceptionAsync(context, _executor, null, exception); 115 | } 116 | else 117 | { 118 | await awm.HandleExceptionAsync(context, exception); 119 | } 120 | 121 | await awm.RevertResponseBodyStreamAsync(memoryStream, originalResponseBodyStream); 122 | } 123 | finally 124 | { 125 | LogHttpRequest(context, requestBody, stopWatch, isRequestOk); 126 | } 127 | } 128 | } 129 | 130 | private bool ShouldLogRequestData(HttpContext context) 131 | { 132 | if (_options.ShouldLogRequestData) 133 | { 134 | var endpoint = context.GetEndpoint(); 135 | return !(endpoint?.Metadata?.GetMetadata() is object); 136 | } 137 | 138 | return false; 139 | } 140 | 141 | private void LogHttpRequest(HttpContext context, string requestBody, Stopwatch stopWatch, bool isRequestOk) 142 | { 143 | stopWatch.Stop(); 144 | if (_options.EnableResponseLogging) 145 | { 146 | bool shouldLogRequestData = ShouldLogRequestData(context); 147 | 148 | var request = shouldLogRequestData 149 | ? isRequestOk 150 | ? $"{context.Request.Method} {context.Request.Scheme} {context.Request.Host}{context.Request.Path} {context.Request.QueryString} {requestBody}" 151 | : (!isRequestOk && _options.LogRequestDataOnException) 152 | ? $"{context.Request.Method} {context.Request.Scheme} {context.Request.Host}{context.Request.Path} {context.Request.QueryString} {requestBody}" 153 | : $"{context.Request.Method} {context.Request.Scheme} {context.Request.Host}{context.Request.Path}" 154 | : $"{context.Request.Method} {context.Request.Scheme} {context.Request.Host}{context.Request.Path}"; 155 | 156 | _logger.Log(LogLevel.Information, $"Source:[{context.Connection.RemoteIpAddress }] " + 157 | $"Request: {request} " + 158 | $"Responded with [{context.Response.StatusCode}] in {stopWatch.ElapsedMilliseconds}ms"); 159 | } 160 | } 161 | 162 | private void LogResponseHasStartedError() 163 | { 164 | _logger.Log(LogLevel.Warning, "The response has already started, the AutoWrapper middleware will not be executed."); 165 | } 166 | } 167 | 168 | } 169 | -------------------------------------------------------------------------------- /src/AutoWrapper/Extensions/JsonExtension.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json.Linq; 2 | using System; 3 | 4 | namespace AutoWrapper.Extensions 5 | { 6 | public static class JsonExtensions 7 | { 8 | public static bool IsNullOrEmpty(this JToken token) 9 | { 10 | return (token == null) || 11 | (token.Type == JTokenType.Array && !token.HasValues) || 12 | (token.Type == JTokenType.Object && !token.HasValues) || 13 | (token.Type == JTokenType.String && token.ToString() == String.Empty) || 14 | (token.Type == JTokenType.Null); 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/AutoWrapper/Extensions/ModelStateExtension.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc.ModelBinding; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using AutoWrapper.Wrappers; 5 | using System.Collections; 6 | 7 | namespace AutoWrapper.Extensions 8 | { 9 | public static class ModelStateExtension 10 | { 11 | public static IEnumerable AllErrors(this ModelStateDictionary modelState) 12 | { 13 | return modelState.Keys.SelectMany(key => modelState[key].Errors.Select(x => new ValidationError(key, x.ErrorMessage))).ToList(); 14 | } 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /src/AutoWrapper/Extensions/StringExtension.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json.Linq; 2 | using System; 3 | using System.Text.RegularExpressions; 4 | 5 | namespace AutoWrapper.Extensions 6 | { 7 | internal static class StringExtension 8 | { 9 | public static bool IsValidJson(this string text) 10 | { 11 | text = text.Trim(); 12 | if ((text.StartsWith("{") && text.EndsWith("}")) || //For object 13 | (text.StartsWith("[") && text.EndsWith("]"))) //For array 14 | { 15 | try 16 | { 17 | var obj = JToken.Parse(text); 18 | return true; 19 | } 20 | catch(Exception) { 21 | return false; 22 | } 23 | } 24 | else 25 | { 26 | return false; 27 | } 28 | } 29 | 30 | public static (bool IsEncoded, string ParsedText) VerifyBodyContent(this string text) 31 | { 32 | try 33 | { 34 | var obj = JToken.Parse(text); 35 | return (true, obj.ToString()); 36 | } 37 | catch (Exception) 38 | { 39 | return (false, text); 40 | } 41 | } 42 | 43 | public static bool IsHtml(this string text) 44 | { 45 | Regex tagRegex = new Regex(@"<\s*([^ >]+)[^>]*>.*?<\s*/\s*\1\s*>"); 46 | 47 | return tagRegex.IsMatch(text); 48 | } 49 | 50 | 51 | public static string ToCamelCase(this string str) 52 | { 53 | if (!string.IsNullOrEmpty(str) && str.Length > 1) 54 | { 55 | return Char.ToLowerInvariant(str[0]) + str.Substring(1); 56 | } 57 | return str; 58 | } 59 | 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/AutoWrapper/Extensions/TypeConverterExtension.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using System.Text.RegularExpressions; 5 | 6 | namespace AutoWrapper.Extensions 7 | { 8 | public static class TypeConverterExtension 9 | { 10 | public static DateTime ToDateTime(this string value) 11 | => DateTime.TryParse(value, out var result)? result : default; 12 | 13 | public static short ToInt16(this string value) 14 | => short.TryParse(value, out var result) ? result : default; 15 | 16 | public static int ToInt32(this string value) 17 | => int.TryParse(value, out var result) ? result : default; 18 | 19 | public static long ToInt64(this string value) 20 | => long.TryParse(value, out var result) ? result : default; 21 | 22 | public static bool ToBoolean(this string value) 23 | => bool.TryParse(value, out var result) ? result : default; 24 | 25 | public static float ToFloat(this string value) 26 | => float.TryParse(value, out var result) ? result : default; 27 | 28 | public static decimal ToDecimal(this string value) 29 | => decimal.TryParse(value, out var result) ? result : default; 30 | 31 | public static double ToDouble(this string value) 32 | => double.TryParse(value, out var result) ? result : default; 33 | 34 | public static bool IsNumber(this string value) 35 | => Regex.IsMatch(value, @"^\d+$"); 36 | 37 | public static bool IsWholeNumber(this string value) 38 | => long.TryParse(value, out _); 39 | 40 | public static bool IsDecimalNumber(this string value) 41 | => decimal.TryParse(value, out _); 42 | 43 | public static bool IsBoolean(this string value) 44 | => bool.TryParse(value, out var _); 45 | 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/AutoWrapper/Filters/AutoWrapIgnore.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace AutoWrapper.Filters 4 | { 5 | public class AutoWrapIgnoreAttribute : Attribute 6 | { 7 | public bool ShouldLogRequestData{ get; set; } = true; 8 | 9 | public AutoWrapIgnoreAttribute(){} 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/AutoWrapper/Filters/RequestDataLogIgnore.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace AutoWrapper.Filters 4 | { 5 | public class RequestDataLogIgnoreAttribute: Attribute 6 | { 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/AutoWrapper/Helpers/CustomContractResolver.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using Newtonsoft.Json.Serialization; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Reflection; 6 | using AutoWrapper.Extensions; 7 | 8 | namespace AutoWrapper.Helpers 9 | { 10 | public class CustomContractResolver : DefaultContractResolver 11 | { 12 | public Dictionary _propertyMappings { get; set; } 13 | private readonly bool _useCamelCaseNaming; 14 | 15 | public CustomContractResolver(bool useCamelCaseNaming) 16 | { 17 | _propertyMappings = new Dictionary(); 18 | _useCamelCaseNaming = useCamelCaseNaming; 19 | SetObjectMappings(); 20 | } 21 | 22 | protected override string ResolvePropertyName(string propertyName) 23 | { 24 | var resolved = _propertyMappings.TryGetValue(propertyName, out string resolvedName); 25 | 26 | if (_useCamelCaseNaming) 27 | return (resolved) ? resolvedName.ToCamelCase() : base.ResolvePropertyName(propertyName.ToCamelCase()); 28 | 29 | return (resolved) ? resolvedName : base.ResolvePropertyName(propertyName); 30 | } 31 | 32 | protected override JsonProperty CreateProperty(MemberInfo member, MemberSerialization memberSerialization) 33 | { 34 | var prop = base.CreateProperty(member, memberSerialization); 35 | 36 | return prop; 37 | } 38 | 39 | private void SetObjectMappings() 40 | { 41 | SetObjectMappings(typeof(T)); 42 | } 43 | 44 | private void SetObjectMappings(Type classType) 45 | { 46 | foreach (PropertyInfo propertyInfo in classType.GetProperties(BindingFlags.DeclaredOnly | BindingFlags.Public | BindingFlags.Instance)) 47 | { 48 | 49 | var wrapperProperty = propertyInfo.GetCustomAttribute(); 50 | if (wrapperProperty != null) 51 | { 52 | _propertyMappings.Add(wrapperProperty.PropertyName, propertyInfo.Name); 53 | } 54 | 55 | var type = propertyInfo.PropertyType; 56 | if (type.IsClass) 57 | SetObjectMappings(propertyInfo.PropertyType); 58 | } 59 | } 60 | 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/AutoWrapper/Helpers/JsonHelper.cs: -------------------------------------------------------------------------------- 1 | using AutoWrapper.Extensions; 2 | using Newtonsoft.Json; 3 | using Newtonsoft.Json.Linq; 4 | using System.Collections.Generic; 5 | 6 | namespace AutoWrapper.Helpers 7 | { 8 | public static class JSONHelper 9 | { 10 | public static JsonSerializerSettings GetJSONSettings(bool ignoreNull = true, ReferenceLoopHandling referenceLoopHandling = ReferenceLoopHandling.Ignore, bool useCamelCaseNaming = true) 11 | { 12 | return new CamelCaseContractResolverJsonSettings().GetJSONSettings(ignoreNull, referenceLoopHandling, useCamelCaseNaming); 13 | } 14 | 15 | public static (JsonSerializerSettings Settings, Dictionary Mappings) GetJSONSettings(bool ignoreNull = true, ReferenceLoopHandling referenceLoopHandling = ReferenceLoopHandling.Ignore, bool useCamelCaseNaming = true) 16 | { 17 | return new CustomContractResolverJsonSettings().GetJSONSettings(ignoreNull, referenceLoopHandling, useCamelCaseNaming); 18 | } 19 | 20 | public static bool HasProperty(dynamic obj, string name) 21 | { 22 | if (obj is JObject) return ((JObject)obj).ContainsKey(name); 23 | return obj.GetType().GetProperty(name) != null; 24 | } 25 | public static JToken RemoveEmptyChildren(JToken token) 26 | { 27 | if (token.Type == JTokenType.Object) 28 | { 29 | JObject copy = new JObject(); 30 | foreach (JProperty prop in token.Children()) 31 | { 32 | JToken child = prop.Value; 33 | if (child.HasValues) 34 | { 35 | child = RemoveEmptyChildren(child); 36 | } 37 | 38 | if(!child.IsNullOrEmpty()) 39 | { 40 | copy.Add(prop.Name, child); 41 | } 42 | } 43 | return copy; 44 | } 45 | else if (token.Type == JTokenType.Array) 46 | { 47 | JArray copy = new JArray(); 48 | foreach (JToken item in token.Children()) 49 | { 50 | JToken child = item; 51 | if (child.HasValues) 52 | { 53 | child = RemoveEmptyChildren(child); 54 | } 55 | 56 | if (!child.IsNullOrEmpty()) 57 | { 58 | copy.Add(child); 59 | } 60 | } 61 | return copy; 62 | } 63 | return token; 64 | } 65 | 66 | public static bool IsEmpty(JToken token) 67 | { 68 | return (token.Type == JTokenType.Null); 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/AutoWrapper/Helpers/JsonSettings.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using Newtonsoft.Json.Converters; 3 | using Newtonsoft.Json.Serialization; 4 | using System.Collections.Generic; 5 | 6 | namespace AutoWrapper.Helpers 7 | { 8 | public class CamelCaseContractResolverJsonSettings 9 | { 10 | public JsonSerializerSettings GetJSONSettings(bool ignoreNull, ReferenceLoopHandling referenceLoopHandling = ReferenceLoopHandling.Ignore, bool useCamelCaseNaming = true) 11 | { 12 | return new JsonSerializerSettings 13 | { 14 | Formatting = Formatting.Indented, 15 | ContractResolver = useCamelCaseNaming ? new CamelCasePropertyNamesContractResolver() : new DefaultContractResolver(), 16 | Converters = new List { new StringEnumConverter() }, 17 | NullValueHandling = ignoreNull ? NullValueHandling.Ignore : NullValueHandling.Include, 18 | ReferenceLoopHandling = referenceLoopHandling 19 | }; 20 | } 21 | } 22 | 23 | public class CustomContractResolverJsonSettings 24 | { 25 | public (JsonSerializerSettings Settings, Dictionary Mappings) GetJSONSettings(bool ignoreNull, ReferenceLoopHandling referenceLoopHandling = ReferenceLoopHandling.Ignore, bool useCamelCaseNaming = true) 26 | { 27 | var resolver = new CustomContractResolver(useCamelCaseNaming); 28 | var propMappings = resolver._propertyMappings; 29 | 30 | var settings = new JsonSerializerSettings 31 | { 32 | Formatting = Formatting.Indented, 33 | ContractResolver = resolver, 34 | NullValueHandling = ignoreNull ? NullValueHandling.Ignore : NullValueHandling.Include, 35 | ReferenceLoopHandling = referenceLoopHandling 36 | }; 37 | 38 | return (settings, propMappings); 39 | 40 | } 41 | } 42 | 43 | 44 | 45 | } 46 | -------------------------------------------------------------------------------- /src/AutoWrapper/Helpers/ResponseMessage.cs: -------------------------------------------------------------------------------- 1 | namespace AutoWrapper.Helpers 2 | { 3 | internal class ResponseMessage 4 | { 5 | internal const string Success = "Request successful."; 6 | internal const string NotFound = "Request not found. The specified uri does not exist."; 7 | internal const string BadRequest = "Request invalid."; 8 | internal const string MethodNotAllowed = "Request responded with 'Method Not Allowed'."; 9 | internal const string NotContent = "Request no content. The specified uri does not contain any content."; 10 | internal const string Exception = "Request responded with exceptions."; 11 | internal const string UnAuthorized = "Request denied. Unauthorized access."; 12 | internal const string ValidationError = "Request responded with one or more validation errors."; 13 | internal const string Unknown = "Request cannot be processed. Please contact support."; 14 | internal const string Unhandled = "Unhandled Exception occurred. Unable to process the request."; 15 | internal const string MediaTypeNotSupported = "Unsupported Media Type."; 16 | internal const string NotApiOnly = @"HTML detected in the response body. 17 | If you are combining API Controllers within your front-end projects like Angular, MVC, React, Blazor and other SPA frameworks that supports .NET Core, then set the AutoWrapperOptions IsApiOnly property to false. 18 | If you are using pure API and want to output HTML as part of your JSON object, then set BypassHTMLValidation property to true."; 19 | internal const string NoMappingFound = "You must apply the [AutoWrapperPropertyMap] Attribute to map through the default ApiResponse properties. If you want to to define your own custom response, set UseCustomSchema = true in the AutoWrapper options."; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/AutoWrapper/Helpers/TypeIdentifier.cs: -------------------------------------------------------------------------------- 1 | namespace AutoWrapper.Helpers 2 | { 3 | internal class TypeIdentifier 4 | { 5 | internal const string JSONHttpContentMediaType = "application/json"; 6 | internal const string ProblemJSONHttpContentMediaType = "application/problem+json"; 7 | internal const string ProblemXMLHttpContentMediaType = "application/problem+xml"; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/AutoWrapper/Models/ApiProblemDetailsExceptionResponse.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | 3 | namespace AutoWrapper.Models 4 | { 5 | public class ApiProblemDetailsExceptionResponse: ProblemDetails 6 | { 7 | public bool IsError { get; set; } 8 | public ErrorDetails Errors { get; set; } 9 | public class ErrorDetails 10 | { 11 | public string Message { get; set; } 12 | public string Type { get; set; } 13 | public string Source { get; set; } 14 | public string Raw { get; set; } 15 | } 16 | } 17 | 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/AutoWrapper/Models/ApiProblemDetailsResponse.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | 3 | namespace AutoWrapper.Models 4 | { 5 | public class ApiProblemDetailsResponse: ProblemDetails 6 | { 7 | public bool IsError { get; set; } 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/AutoWrapper/Models/ApiProblemDetailsValidationErrorResponse.cs: -------------------------------------------------------------------------------- 1 | using AutoWrapper.Wrappers; 2 | using Microsoft.AspNetCore.Mvc; 3 | using System.Collections.Generic; 4 | 5 | namespace AutoWrapper.Models 6 | { 7 | public class ApiProblemDetailsValidationErrorResponse: ProblemDetails 8 | { 9 | public bool IsError { get; set; } 10 | public IEnumerable ValidationErrors { get; set; } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/AutoWrapper/Models/ApiResultResponse.cs: -------------------------------------------------------------------------------- 1 | namespace AutoWrapper.Models 2 | { 3 | public class ApiResultResponse where T : class 4 | { 5 | public string Message { get; set; } 6 | public T Result { get; set; } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/AutoWrapper/Wrappers/ApiError.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | 3 | namespace AutoWrapper.Wrappers 4 | { 5 | public class ApiError 6 | { 7 | public object ExceptionMessage { get; set; } 8 | public string Details { get; set; } 9 | public string ReferenceErrorCode { get; set; } 10 | public string ReferenceDocumentLink { get; set; } 11 | public IEnumerable ValidationErrors { get; set; } 12 | public ApiError(object message) 13 | { 14 | ExceptionMessage = message; 15 | } 16 | 17 | public ApiError(string message, IEnumerable validationErrors) 18 | { 19 | ExceptionMessage = message; 20 | ValidationErrors = validationErrors; 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/AutoWrapper/Wrappers/ApiException.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using static Microsoft.AspNetCore.Http.StatusCodes; 3 | 4 | namespace AutoWrapper.Wrappers 5 | { 6 | public class ApiException : System.Exception 7 | { 8 | public int StatusCode { get; set; } 9 | public bool IsModelValidatonError { get; set; } = false; 10 | public IEnumerable Errors { get; set; } 11 | public string ReferenceErrorCode { get; set; } 12 | public string ReferenceDocumentLink { get; set; } 13 | public object CustomError { get; set; } 14 | public bool IsCustomErrorObject { get; set; } = false; 15 | 16 | public ApiException(string message, 17 | int statusCode = Status400BadRequest, 18 | string errorCode = default, 19 | string refLink = default) : 20 | base(message) 21 | { 22 | StatusCode = statusCode; 23 | ReferenceErrorCode = errorCode; 24 | ReferenceDocumentLink = refLink; 25 | } 26 | 27 | public ApiException(object customError, int statusCode = Status400BadRequest) 28 | { 29 | IsCustomErrorObject = true; 30 | StatusCode = statusCode; 31 | CustomError = customError; 32 | } 33 | 34 | public ApiException(IEnumerable errors, int statusCode = Status400BadRequest) 35 | { 36 | IsModelValidatonError = true; 37 | StatusCode = statusCode; 38 | Errors = errors; 39 | } 40 | 41 | public ApiException(System.Exception ex, int statusCode = Status500InternalServerError) : base(ex.Message) 42 | { 43 | StatusCode = statusCode; 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/AutoWrapper/Wrappers/ApiProblemDetails.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | using Microsoft.AspNetCore.WebUtilities; 3 | using Newtonsoft.Json; 4 | using System.Collections.Generic; 5 | using static AutoWrapper.Wrappers.ApiProblemDetailsMember; 6 | 7 | namespace AutoWrapper.Wrappers 8 | { 9 | 10 | internal class ApiProblemDetails: ProblemDetails 11 | { 12 | public ApiProblemDetails(int statusCode) 13 | { 14 | IsError = true; 15 | Status = statusCode; 16 | Type = $"https://httpstatuses.com/{statusCode}"; 17 | Title = ReasonPhrases.GetReasonPhrase(statusCode); 18 | } 19 | 20 | public ApiProblemDetails(ProblemDetails details) 21 | { 22 | IsError = true; 23 | Details = details; 24 | } 25 | 26 | [JsonProperty(Order = -2)] 27 | public bool IsError { get; set; } 28 | [JsonProperty(NullValueHandling = NullValueHandling.Ignore, Order = 1)] 29 | public ErrorDetails Errors { get; set; } 30 | [JsonProperty(NullValueHandling = NullValueHandling.Ignore, Order = 1)] 31 | public IEnumerable ValidationErrors { get; set; } 32 | [JsonIgnore] 33 | public ProblemDetails Details { get; set; } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/AutoWrapper/Wrappers/ApiProblemDetailsException.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | using Microsoft.AspNetCore.Mvc.ModelBinding; 3 | using System; 4 | using System.Text; 5 | using AutoWrapper.Extensions; 6 | using static Microsoft.AspNetCore.Http.StatusCodes; 7 | 8 | namespace AutoWrapper.Wrappers 9 | { 10 | public class ApiProblemDetailsException : Exception 11 | { 12 | public ApiProblemDetailsException(int statusCode) 13 | : this(new ApiProblemDetails(statusCode)) 14 | { 15 | } 16 | 17 | public ApiProblemDetailsException(string title, int statusCode) 18 | : this(new ApiProblemDetails(statusCode) { Title = title }) 19 | { 20 | } 21 | 22 | public ApiProblemDetailsException(ProblemDetails details) 23 | : base($"{details.Type} : {details.Title}") 24 | { 25 | Problem = new ApiProblemDetails(details); 26 | } 27 | 28 | public ApiProblemDetailsException(ModelStateDictionary modelState, int statusCode = Status422UnprocessableEntity) 29 | : this(new ApiProblemDetails(statusCode) { Detail = "Your request parameters didn't validate.", ValidationErrors = modelState.AllErrors() }) 30 | { 31 | } 32 | 33 | public int StatusCode => Problem.Details.Status ?? 0; 34 | internal ApiProblemDetails Problem { get; } 35 | 36 | public override string ToString() 37 | { 38 | var stringBuilder = new StringBuilder(); 39 | 40 | stringBuilder.AppendLine($"Type : {Problem.Details.Type}"); 41 | stringBuilder.AppendLine($"Title : {Problem.Details.Title}"); 42 | stringBuilder.AppendLine($"Status : {Problem.Details.Status}"); 43 | stringBuilder.AppendLine($"Detail : {Problem.Details.Detail}"); 44 | stringBuilder.AppendLine($"Instance: {Problem.Details.Instance}"); 45 | 46 | return stringBuilder.ToString(); 47 | } 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/AutoWrapper/Wrappers/ApiProblemDetailsMember.cs: -------------------------------------------------------------------------------- 1 | using AutoWrapper.Extensions; 2 | using AutoWrapper.Helpers; 3 | using Microsoft.AspNetCore.Http; 4 | using Microsoft.AspNetCore.Mvc; 5 | using Microsoft.AspNetCore.Mvc.Abstractions; 6 | using Microsoft.AspNetCore.Mvc.Infrastructure; 7 | using Microsoft.AspNetCore.Routing; 8 | using Newtonsoft.Json; 9 | using System; 10 | using System.Threading.Tasks; 11 | using static Microsoft.AspNetCore.Http.StatusCodes; 12 | 13 | namespace AutoWrapper.Wrappers 14 | { 15 | internal class ApiProblemDetailsMember 16 | { 17 | private static readonly RouteData _emptyRouteData = new RouteData(); 18 | private static readonly ActionDescriptor _emptyActionDescriptor = new ActionDescriptor(); 19 | public Task WriteProblemDetailsAsync(HttpContext context, IActionResultExecutor executor, object body, Exception exception, bool isDebug = false) 20 | { 21 | var statusCode = context.Response.StatusCode; 22 | object details = exception == null ? DelegateResponse(body, statusCode) : GetProblemDetails(exception, isDebug); 23 | 24 | if (details is ProblemDetails) { (details as ProblemDetails).Instance = context.Request.Path; } 25 | 26 | var routeData = context.GetRouteData() ?? _emptyRouteData; 27 | 28 | var actionContext = new ActionContext(context, routeData, _emptyActionDescriptor); 29 | 30 | var result = new ObjectResult(details) 31 | { 32 | StatusCode = (details is ProblemDetails problem) ? problem.Status : statusCode, 33 | DeclaredType = details.GetType() 34 | }; 35 | 36 | result.ContentTypes.Add(TypeIdentifier.ProblemJSONHttpContentMediaType); 37 | result.ContentTypes.Add(TypeIdentifier.ProblemXMLHttpContentMediaType); 38 | 39 | return executor.ExecuteAsync(actionContext, result); 40 | } 41 | 42 | private object DelegateResponse(object body, int statusCode) 43 | { 44 | var content = body ?? string.Empty; 45 | var (IsEncoded, ParsedText) = content.ToString().VerifyBodyContent(); 46 | var result = IsEncoded ? JsonConvert.DeserializeObject(ParsedText) : new ApiProblemDetails(statusCode) { Detail = content.ToString() } ; 47 | 48 | return result; 49 | } 50 | private ProblemDetails GetProblemDetails(Exception exception, bool isDebug) 51 | { 52 | if (exception is ApiProblemDetailsException problem){ return problem.Problem.Details; } 53 | 54 | var defaultException = new ExceptionFallback(exception); 55 | 56 | if (isDebug) { return new DebugExceptionetails(defaultException); } 57 | 58 | return new ApiProblemDetails((int)defaultException.Status) { Detail = defaultException.Exception.Message }; 59 | } 60 | 61 | internal class ErrorDetails 62 | { 63 | public string Message { get; set; } 64 | public string Type { get; set; } 65 | public string Source { get; set; } 66 | public string Raw { get; set; } 67 | 68 | public ErrorDetails(ExceptionFallback detail) 69 | { 70 | Source = detail.Exception.Source; 71 | Raw = detail.Exception.StackTrace; 72 | Message = detail.Exception.Message; 73 | Type = detail.Exception.GetType().Name; 74 | } 75 | } 76 | 77 | internal class ExceptionFallback : ApiProblemDetails 78 | { 79 | public ExceptionFallback(Exception exception) : this(exception, Status500InternalServerError) 80 | { 81 | Detail = exception.Message; 82 | } 83 | 84 | public ExceptionFallback(Exception exception, int statusCode) : base(statusCode) 85 | { 86 | Exception = exception ?? throw new ArgumentNullException(nameof(exception)); 87 | } 88 | 89 | public Exception Exception { get; } 90 | } 91 | 92 | internal class DebugExceptionetails : ApiProblemDetails 93 | { 94 | public DebugExceptionetails(ExceptionFallback problem) 95 | : base(problem.Status ?? Status500InternalServerError) 96 | { 97 | Detail = problem.Detail ?? problem.Exception.Message; 98 | Title = problem.Title ?? problem.Exception.GetType().Name; 99 | Instance = problem.Instance ?? GetHelpLink(problem.Exception); 100 | 101 | if (!string.IsNullOrEmpty(problem.Type)) 102 | { 103 | Type = problem.Type; 104 | } 105 | 106 | Errors = new ErrorDetails(problem); 107 | } 108 | 109 | private static string GetHelpLink(Exception exception) 110 | { 111 | var link = exception.HelpLink; 112 | 113 | if (string.IsNullOrEmpty(link)) 114 | { 115 | return null; 116 | } 117 | 118 | if (Uri.TryCreate(link, UriKind.Absolute, out var result)) 119 | { 120 | return result.ToString(); 121 | } 122 | 123 | return null; 124 | } 125 | 126 | } 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/AutoWrapper/Wrappers/ApiResponse.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | 3 | namespace AutoWrapper.Wrappers 4 | { 5 | public class ApiResponse 6 | { 7 | public string Version { get; set; } 8 | 9 | [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] 10 | public int StatusCode { get; set; } 11 | 12 | public string Message { get; set; } 13 | 14 | [JsonProperty(DefaultValueHandling = DefaultValueHandling.Ignore)] 15 | public bool? IsError { get; set; } 16 | 17 | public object ResponseException { get; set; } 18 | 19 | public object Result { get; set; } 20 | 21 | [JsonConstructor] 22 | public ApiResponse(string message, object result = null, int statusCode = 200, string apiVersion = "1.0.0.0") 23 | { 24 | StatusCode = statusCode; 25 | Message = message; 26 | Result = result; 27 | Version = apiVersion; 28 | } 29 | public ApiResponse(object result, int statusCode = 200) 30 | { 31 | StatusCode = statusCode; 32 | Result = result; 33 | } 34 | 35 | public ApiResponse(int statusCode, object apiError) 36 | { 37 | StatusCode = statusCode; 38 | ResponseException = apiError; 39 | IsError = true; 40 | } 41 | 42 | public ApiResponse() { } 43 | } 44 | } 45 | 46 | 47 | -------------------------------------------------------------------------------- /src/AutoWrapper/Wrappers/ValidationError.cs: -------------------------------------------------------------------------------- 1 | namespace AutoWrapper.Wrappers 2 | { 3 | public class ValidationError 4 | { 5 | public string Name { get; } 6 | public string Reason { get; } 7 | public ValidationError(string name, string reason) 8 | { 9 | Name = name != string.Empty ? name : null; 10 | Reason = reason; 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/AutoWrapper/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/proudmonkey/AutoWrapper/529db8d18a1b244c2b6ab7862f0df55805c2e66c/src/AutoWrapper/icon.png -------------------------------------------------------------------------------- /src/AutoWrapper/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/proudmonkey/AutoWrapper/529db8d18a1b244c2b6ab7862f0df55805c2e66c/src/AutoWrapper/logo.png --------------------------------------------------------------------------------