├── .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 [](https://www.nuget.org/packages/AutoWrapper.Core) [](https://www.nuget.org/packages/AutoWrapper.Core) 
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 | |
| [](https://www.buymeacoffee.com/ImP9gONBW) |
795 |
796 |
797 | Thank you!
798 |
--------------------------------------------------------------------------------
/README.zh-cn.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # AutoWrapper [](https://www.nuget.org/packages/AutoWrapper.Core) [](https://www.nuget.org/packages/AutoWrapper.Core) 
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 | |
| [](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
--------------------------------------------------------------------------------