├── .gitignore ├── CHANGELOG.md ├── LICENSE ├── README.md ├── Umamimolecule.AzureFunctionsMiddleware.sln ├── assets ├── logo.pdn ├── logo.png └── obsolete.svg ├── samples ├── ConditionalMiddleware │ ├── .gitignore │ ├── ConditionalMiddleware.csproj │ ├── Functions │ │ └── Functions.cs │ ├── Pipelines │ │ ├── IMiddlewarePipelineFactory.cs │ │ ├── MiddlewareA.cs │ │ ├── MiddlewareB.cs │ │ └── MiddlewarePipelineFactory.cs │ ├── README.md │ ├── Startup.cs │ └── host.json ├── ExceptionHandler │ ├── .gitignore │ ├── ExceptionHandler.csproj │ ├── Exceptions │ │ └── ThrottledException.cs │ ├── Functions │ │ └── ExceptionHandler.cs │ ├── Pipelines │ │ ├── CustomExceptionHandler.cs │ │ ├── IMiddlewarePipelineFactory.cs │ │ └── MiddlewarePipelineFactory.cs │ ├── README.md │ ├── Startup.cs │ └── host.json ├── ModelValidation │ ├── .gitignore │ ├── Functions │ │ ├── BodyValidation.cs │ │ ├── BodyValidationBody.cs │ │ ├── BodyValidationWithCustomResponse.cs │ │ ├── QueryValidation.cs │ │ ├── QueryValidationQueryParameters.cs │ │ └── QueryValidationWithCustomResponse.cs │ ├── ModelValidation.csproj │ ├── Pipelines │ │ ├── IMiddlewarePipelineFactory.cs │ │ └── MiddlewarePipelineFactory.cs │ ├── README.md │ ├── Responses │ │ └── ResponseHelper.cs │ ├── Startup.cs │ └── host.json └── PipelineBranching │ ├── .gitignore │ ├── Functions │ └── Functions.cs │ ├── PipelineBranching.csproj │ ├── Pipelines │ ├── IMiddlewarePipelineFactory.cs │ ├── MiddlewareA.cs │ ├── MiddlewareB.cs │ └── MiddlewarePipelineFactory.cs │ ├── README.md │ ├── Startup.cs │ └── host.json ├── src └── Umamimolecule.AzureFunctionsMiddleware │ ├── BadRequestException.cs │ ├── BodyModelValidationMiddleware.cs │ ├── ConditionalMiddleware.cs │ ├── ContentTypes.cs │ ├── ContextItems.cs │ ├── CorrelationIdMiddleware.cs │ ├── ErrorCodes.cs │ ├── ExceptionHandlerMiddleware.cs │ ├── FunctionMiddleware.cs │ ├── GuardClauses.cs │ ├── Headers.cs │ ├── HttpContextExtensions.cs │ ├── HttpMiddleware.cs │ ├── HttpResponseResult.cs │ ├── IHttpMiddleware.cs │ ├── IMiddlewarePipeline.cs │ ├── MiddlewarePipeline.cs │ ├── MiddlewarePipelineException.cs │ ├── MiddlewarePipelineExtensions.cs │ ├── ModelValidationResult.cs │ ├── QueryModelValidationMiddleware.cs │ ├── RecursiveValidator.cs │ ├── RequestDelegateMiddleware.cs │ ├── Umamimolecule.AzureFunctionsMiddleware.csproj │ ├── Umamimolecule.AzureFunctionsMiddleware.nuspec │ ├── Umamimolecule.AzureFunctionsMiddleware.ruleset │ ├── Umamimolecule.AzureFunctionsMiddleware.xml │ └── ValidationMiddleware.cs ├── test.runsettings └── tests └── Umamimolecule.AzureFunctionsMiddleware.Tests ├── BodyModelValidationMiddlewareTests.cs ├── ConditionalMiddlewareTests.cs ├── ContextBuilder.cs ├── CorrelationIdMiddlewareTests.cs ├── DummyDisposable.cs ├── ErrorResponse.cs ├── ExceptionHandlerMiddlewareTests.cs ├── FunctionMiddlewareTests.cs ├── HttpContextAccessor.cs ├── Logger.cs ├── MiddlewarePipelineTests.cs ├── QueryModelValidationMiddlewareTests.cs ├── RecursiveValidatorTests.cs ├── StreamExtensions.cs └── Umamimolecule.AzureFunctionsMiddleware.Tests.csproj /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.suo 8 | *.user 9 | *.userosscache 10 | *.sln.docstates 11 | 12 | # User-specific files (MonoDevelop/Xamarin Studio) 13 | *.userprefs 14 | 15 | # Build results 16 | [Dd]ebug/ 17 | [Dd]ebugPublic/ 18 | [Rr]elease/ 19 | [Rr]eleases/ 20 | x64/ 21 | x86/ 22 | bld/ 23 | [Bb]in/ 24 | [Oo]bj/ 25 | [Ll]og/ 26 | 27 | # Visual Studio 2015/2017 cache/options directory 28 | .vs/ 29 | # Uncomment if you have tasks that create the project's static files in wwwroot 30 | #wwwroot/ 31 | 32 | # Visual Studio 2017 auto generated files 33 | Generated\ Files/ 34 | 35 | # MSTest test Results 36 | [Tt]est[Rr]esult*/ 37 | [Bb]uild[Ll]og.* 38 | 39 | # NUNIT 40 | *.VisualState.xml 41 | TestResult.xml 42 | 43 | # Build Results of an ATL Project 44 | [Dd]ebugPS/ 45 | [Rr]eleasePS/ 46 | dlldata.c 47 | 48 | # Benchmark Results 49 | BenchmarkDotNet.Artifacts/ 50 | 51 | # .NET Core 52 | project.lock.json 53 | project.fragment.lock.json 54 | artifacts/ 55 | **/Properties/launchSettings.json 56 | 57 | # StyleCop 58 | StyleCopReport.xml 59 | 60 | # Files built by Visual Studio 61 | *_i.c 62 | *_p.c 63 | *_i.h 64 | *.ilk 65 | *.meta 66 | *.obj 67 | *.iobj 68 | *.pch 69 | *.pdb 70 | *.ipdb 71 | *.pgc 72 | *.pgd 73 | *.rsp 74 | *.sbr 75 | *.tlb 76 | *.tli 77 | *.tlh 78 | *.tmp 79 | *.tmp_proj 80 | *.log 81 | *.vspscc 82 | *.vssscc 83 | .builds 84 | *.pidb 85 | *.svclog 86 | *.scc 87 | 88 | # Chutzpah Test files 89 | _Chutzpah* 90 | 91 | # Visual C++ cache files 92 | ipch/ 93 | *.aps 94 | *.ncb 95 | *.opendb 96 | *.opensdf 97 | *.sdf 98 | *.cachefile 99 | *.VC.db 100 | *.VC.VC.opendb 101 | 102 | # Visual Studio profiler 103 | *.psess 104 | *.vsp 105 | *.vspx 106 | *.sap 107 | 108 | # Visual Studio Trace Files 109 | *.e2e 110 | 111 | # TFS 2012 Local Workspace 112 | $tf/ 113 | 114 | # Guidance Automation Toolkit 115 | *.gpState 116 | 117 | # ReSharper is a .NET coding add-in 118 | _ReSharper*/ 119 | *.[Rr]e[Ss]harper 120 | *.DotSettings.user 121 | 122 | # JustCode is a .NET coding add-in 123 | .JustCode 124 | 125 | # TeamCity is a build add-in 126 | _TeamCity* 127 | 128 | # DotCover is a Code Coverage Tool 129 | *.dotCover 130 | 131 | # AxoCover is a Code Coverage Tool 132 | .axoCover/* 133 | !.axoCover/settings.json 134 | 135 | # Visual Studio code coverage results 136 | *.coverage 137 | *.coveragexml 138 | 139 | # NCrunch 140 | _NCrunch_* 141 | .*crunch*.local.xml 142 | nCrunchTemp_* 143 | 144 | # MightyMoose 145 | *.mm.* 146 | AutoTest.Net/ 147 | 148 | # Web workbench (sass) 149 | .sass-cache/ 150 | 151 | # Installshield output folder 152 | [Ee]xpress/ 153 | 154 | # DocProject is a documentation generator add-in 155 | DocProject/buildhelp/ 156 | DocProject/Help/*.HxT 157 | DocProject/Help/*.HxC 158 | DocProject/Help/*.hhc 159 | DocProject/Help/*.hhk 160 | DocProject/Help/*.hhp 161 | DocProject/Help/Html2 162 | DocProject/Help/html 163 | 164 | # Click-Once directory 165 | publish/ 166 | 167 | # Publish Web Output 168 | *.[Pp]ublish.xml 169 | *.azurePubxml 170 | # Note: Comment the next line if you want to checkin your web deploy settings, 171 | # but database connection strings (with potential passwords) will be unencrypted 172 | *.pubxml 173 | *.publishproj 174 | 175 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 176 | # checkin your Azure Web App publish settings, but sensitive information contained 177 | # in these scripts will be unencrypted 178 | PublishScripts/ 179 | 180 | # NuGet Packages 181 | *.nupkg 182 | # The packages folder can be ignored because of Package Restore 183 | **/[Pp]ackages/* 184 | # except build/, which is used as an MSBuild target. 185 | !**/[Pp]ackages/build/ 186 | # Uncomment if necessary however generally it will be regenerated when needed 187 | #!**/[Pp]ackages/repositories.config 188 | # NuGet v3's project.json files produces more ignorable files 189 | *.nuget.props 190 | *.nuget.targets 191 | 192 | # Microsoft Azure Build Output 193 | csx/ 194 | *.build.csdef 195 | 196 | # Microsoft Azure Emulator 197 | ecf/ 198 | rcf/ 199 | 200 | # Windows Store app package directories and files 201 | AppPackages/ 202 | BundleArtifacts/ 203 | Package.StoreAssociation.xml 204 | _pkginfo.txt 205 | *.appx 206 | 207 | # Visual Studio cache files 208 | # files ending in .cache can be ignored 209 | *.[Cc]ache 210 | # but keep track of directories ending in .cache 211 | !*.[Cc]ache/ 212 | 213 | # Others 214 | ClientBin/ 215 | ~$* 216 | *~ 217 | *.dbmdl 218 | *.dbproj.schemaview 219 | *.jfm 220 | *.pfx 221 | *.publishsettings 222 | orleans.codegen.cs 223 | 224 | # Including strong name files can present a security risk 225 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 226 | #*.snk 227 | 228 | # Since there are multiple workflows, uncomment next line to ignore bower_components 229 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 230 | #bower_components/ 231 | 232 | # RIA/Silverlight projects 233 | Generated_Code/ 234 | 235 | # Backup & report files from converting an old project file 236 | # to a newer Visual Studio version. Backup files are not needed, 237 | # because we have git ;-) 238 | _UpgradeReport_Files/ 239 | Backup*/ 240 | UpgradeLog*.XML 241 | UpgradeLog*.htm 242 | ServiceFabricBackup/ 243 | *.rptproj.bak 244 | 245 | # SQL Server files 246 | *.mdf 247 | *.ldf 248 | *.ndf 249 | 250 | # Business Intelligence projects 251 | *.rdl.data 252 | *.bim.layout 253 | *.bim_*.settings 254 | *.rptproj.rsuser 255 | 256 | # Microsoft Fakes 257 | FakesAssemblies/ 258 | 259 | # GhostDoc plugin setting file 260 | *.GhostDoc.xml 261 | 262 | # Node.js Tools for Visual Studio 263 | .ntvs_analysis.dat 264 | node_modules/ 265 | 266 | # Visual Studio 6 build log 267 | *.plg 268 | 269 | # Visual Studio 6 workspace options file 270 | *.opt 271 | 272 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 273 | *.vbw 274 | 275 | # Visual Studio LightSwitch build output 276 | **/*.HTMLClient/GeneratedArtifacts 277 | **/*.DesktopClient/GeneratedArtifacts 278 | **/*.DesktopClient/ModelManifest.xml 279 | **/*.Server/GeneratedArtifacts 280 | **/*.Server/ModelManifest.xml 281 | _Pvt_Extensions 282 | 283 | # Paket dependency manager 284 | .paket/paket.exe 285 | paket-files/ 286 | 287 | # FAKE - F# Make 288 | .fake/ 289 | 290 | # JetBrains Rider 291 | .idea/ 292 | *.sln.iml 293 | 294 | # CodeRush 295 | .cr/ 296 | 297 | # Python Tools for Visual Studio (PTVS) 298 | __pycache__/ 299 | *.pyc 300 | 301 | # Cake - Uncomment if you are using it 302 | # tools/** 303 | # !tools/packages.config 304 | 305 | # Tabs Studio 306 | *.tss 307 | 308 | # Telerik's JustMock configuration file 309 | *.jmconfig 310 | 311 | # BizTalk build output 312 | *.btp.cs 313 | *.btm.cs 314 | *.odx.cs 315 | *.xsd.cs 316 | 317 | # OpenCover UI analysis results 318 | OpenCover/ 319 | 320 | # Azure Stream Analytics local run output 321 | ASALocalRun/ 322 | 323 | # MSBuild Binary and Structured Log 324 | *.binlog 325 | 326 | # NVidia Nsight GPU debugger configuration file 327 | *.nvuser 328 | 329 | # MFractors (Xamarin productivity tool) working folder 330 | .mfractor/ 331 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | 4 | 5 | ## [2.1.0] - 2021-05-14 6 | 7 | ### Added 8 | - Custom response handlers for body and query parameter validation failures 9 | 10 | ## [2.0.0] - 2020-12-12 11 | 12 | ### Changed 13 | - Updated references to Azure Functions SDK version 3.0.11 and .Net Core 3.1 14 | 15 | 16 | 17 | ## [1.3.0] - 2019-12-28 18 | 19 | ### Added 20 | - Add pipeline branching and conditional middleware via `MapWhen` and `UseWhen`. 21 | 22 | 23 | 24 | ## [1.2.2] - 2019-12-26 25 | 26 | ### Added 27 | - Example code for exception handler middleware. 28 | - Include symbols package when publishing NuGet package. 29 | 30 | 31 | 32 | ## [1.2.1] - 2019-12-02 33 | 34 | ### Added 35 | - More tests for middleware and other classes. 36 | 37 | 38 | 39 | ## [1.2.0] - 2019-12-01 40 | 41 | ### Added 42 | - XML documentation for all public classes and members so that consumers get Intellisense. 43 | - Add licence file. 44 | - Add StyleCop analyzer to enforce standards. 45 | 46 | ### Changed 47 | - Fixed bug where body stream could not be read after validation middleware was run. 48 | - Update example code in readme. 49 | 50 | 51 | 52 | ## [1.1.0] - 2019-11-30 53 | 54 | ### Changed 55 | - Use `HttpContext` from ASP .Net Core instead of custom HttpFunctionContext object. 56 | 57 | 58 | 59 | ## [1.0.0] - 2019-11-24 60 | - Initial release. -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 umamimolecule 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 | -------------------------------------------------------------------------------- /Umamimolecule.AzureFunctionsMiddleware.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.29409.12 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ModelValidation", "samples\ModelValidation\ModelValidation.csproj", "{2045A40E-C62C-4079-8D04-1599FB4E808F}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Umamimolecule.AzureFunctionsMiddleware", "src\Umamimolecule.AzureFunctionsMiddleware\Umamimolecule.AzureFunctionsMiddleware.csproj", "{DB977461-5C89-40BB-99D4-534358BD0558}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Umamimolecule.AzureFunctionsMiddleware.Tests", "tests\Umamimolecule.AzureFunctionsMiddleware.Tests\Umamimolecule.AzureFunctionsMiddleware.Tests.csproj", "{E444B944-20E3-4A70-BF95-DB599C81FF0E}" 11 | EndProject 12 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{BB7CAA70-0929-4E40-940F-E9AC5B181C94}" 13 | ProjectSection(SolutionItems) = preProject 14 | .gitignore = .gitignore 15 | CHANGELOG.md = CHANGELOG.md 16 | LICENSE = LICENSE 17 | README.md = README.md 18 | test.runsettings = test.runsettings 19 | EndProjectSection 20 | EndProject 21 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Samples", "Samples", "{E9D699A4-F2CF-4E51-AC62-E1B0A64DBF72}" 22 | EndProject 23 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Tests", "Tests", "{C792CAB6-FED9-4EF3-B7C4-494C243A621A}" 24 | EndProject 25 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ExceptionHandler", "samples\ExceptionHandler\ExceptionHandler.csproj", "{5A1234AD-AC34-43BB-8DA3-7317C44E60D3}" 26 | EndProject 27 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConditionalMiddleware", "samples\ConditionalMiddleware\ConditionalMiddleware.csproj", "{90ABAE09-B126-40E8-97FA-E46384D42AC4}" 28 | EndProject 29 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PipelineBranching", "samples\PipelineBranching\PipelineBranching.csproj", "{30E26CFC-9982-4C6A-9C08-FF0CE2E93D59}" 30 | EndProject 31 | Global 32 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 33 | Debug|Any CPU = Debug|Any CPU 34 | Release|Any CPU = Release|Any CPU 35 | EndGlobalSection 36 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 37 | {2045A40E-C62C-4079-8D04-1599FB4E808F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 38 | {2045A40E-C62C-4079-8D04-1599FB4E808F}.Debug|Any CPU.Build.0 = Debug|Any CPU 39 | {2045A40E-C62C-4079-8D04-1599FB4E808F}.Release|Any CPU.ActiveCfg = Release|Any CPU 40 | {2045A40E-C62C-4079-8D04-1599FB4E808F}.Release|Any CPU.Build.0 = Release|Any CPU 41 | {DB977461-5C89-40BB-99D4-534358BD0558}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 42 | {DB977461-5C89-40BB-99D4-534358BD0558}.Debug|Any CPU.Build.0 = Debug|Any CPU 43 | {DB977461-5C89-40BB-99D4-534358BD0558}.Release|Any CPU.ActiveCfg = Release|Any CPU 44 | {DB977461-5C89-40BB-99D4-534358BD0558}.Release|Any CPU.Build.0 = Release|Any CPU 45 | {E444B944-20E3-4A70-BF95-DB599C81FF0E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 46 | {E444B944-20E3-4A70-BF95-DB599C81FF0E}.Debug|Any CPU.Build.0 = Debug|Any CPU 47 | {E444B944-20E3-4A70-BF95-DB599C81FF0E}.Release|Any CPU.ActiveCfg = Release|Any CPU 48 | {E444B944-20E3-4A70-BF95-DB599C81FF0E}.Release|Any CPU.Build.0 = Release|Any CPU 49 | {5A1234AD-AC34-43BB-8DA3-7317C44E60D3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 50 | {5A1234AD-AC34-43BB-8DA3-7317C44E60D3}.Debug|Any CPU.Build.0 = Debug|Any CPU 51 | {5A1234AD-AC34-43BB-8DA3-7317C44E60D3}.Release|Any CPU.ActiveCfg = Release|Any CPU 52 | {5A1234AD-AC34-43BB-8DA3-7317C44E60D3}.Release|Any CPU.Build.0 = Release|Any CPU 53 | {90ABAE09-B126-40E8-97FA-E46384D42AC4}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 54 | {90ABAE09-B126-40E8-97FA-E46384D42AC4}.Debug|Any CPU.Build.0 = Debug|Any CPU 55 | {90ABAE09-B126-40E8-97FA-E46384D42AC4}.Release|Any CPU.ActiveCfg = Release|Any CPU 56 | {90ABAE09-B126-40E8-97FA-E46384D42AC4}.Release|Any CPU.Build.0 = Release|Any CPU 57 | {30E26CFC-9982-4C6A-9C08-FF0CE2E93D59}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 58 | {30E26CFC-9982-4C6A-9C08-FF0CE2E93D59}.Debug|Any CPU.Build.0 = Debug|Any CPU 59 | {30E26CFC-9982-4C6A-9C08-FF0CE2E93D59}.Release|Any CPU.ActiveCfg = Release|Any CPU 60 | {30E26CFC-9982-4C6A-9C08-FF0CE2E93D59}.Release|Any CPU.Build.0 = Release|Any CPU 61 | EndGlobalSection 62 | GlobalSection(SolutionProperties) = preSolution 63 | HideSolutionNode = FALSE 64 | EndGlobalSection 65 | GlobalSection(NestedProjects) = preSolution 66 | {2045A40E-C62C-4079-8D04-1599FB4E808F} = {E9D699A4-F2CF-4E51-AC62-E1B0A64DBF72} 67 | {E444B944-20E3-4A70-BF95-DB599C81FF0E} = {C792CAB6-FED9-4EF3-B7C4-494C243A621A} 68 | {5A1234AD-AC34-43BB-8DA3-7317C44E60D3} = {E9D699A4-F2CF-4E51-AC62-E1B0A64DBF72} 69 | {90ABAE09-B126-40E8-97FA-E46384D42AC4} = {E9D699A4-F2CF-4E51-AC62-E1B0A64DBF72} 70 | {30E26CFC-9982-4C6A-9C08-FF0CE2E93D59} = {E9D699A4-F2CF-4E51-AC62-E1B0A64DBF72} 71 | EndGlobalSection 72 | GlobalSection(ExtensibilityGlobals) = postSolution 73 | SolutionGuid = {4D4606F1-A62A-417E-BD3D-92DE39CD96BD} 74 | EndGlobalSection 75 | EndGlobal 76 | -------------------------------------------------------------------------------- /assets/logo.pdn: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umamimolecule/azure-functions-http-middleware/9102b7e94f1a971452f4b938fd9e213ade903322/assets/logo.pdn -------------------------------------------------------------------------------- /assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umamimolecule/azure-functions-http-middleware/9102b7e94f1a971452f4b938fd9e213ade903322/assets/logo.png -------------------------------------------------------------------------------- /assets/obsolete.svg: -------------------------------------------------------------------------------- 1 | 8 | 9 |
10 | 39 |
40 |

Note: Azure Functions using the isolated process model with .Net 5 and above already have built-in support for middleware. This project is intended to provide middleware functionality to Azure Functions written in .Net Core 3.1 and below.

42 |
43 |
44 |
45 |
46 | -------------------------------------------------------------------------------- /samples/ConditionalMiddleware/.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # Azure Functions localsettings file 5 | local.settings.json 6 | 7 | # User-specific files 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Build results 17 | [Dd]ebug/ 18 | [Dd]ebugPublic/ 19 | [Rr]elease/ 20 | [Rr]eleases/ 21 | x64/ 22 | x86/ 23 | bld/ 24 | [Bb]in/ 25 | [Oo]bj/ 26 | [Ll]og/ 27 | 28 | # Visual Studio 2015 cache/options directory 29 | .vs/ 30 | # Uncomment if you have tasks that create the project's static files in wwwroot 31 | #wwwroot/ 32 | 33 | # MSTest test Results 34 | [Tt]est[Rr]esult*/ 35 | [Bb]uild[Ll]og.* 36 | 37 | # NUNIT 38 | *.VisualState.xml 39 | TestResult.xml 40 | 41 | # Build Results of an ATL Project 42 | [Dd]ebugPS/ 43 | [Rr]eleasePS/ 44 | dlldata.c 45 | 46 | # DNX 47 | project.lock.json 48 | project.fragment.lock.json 49 | artifacts/ 50 | 51 | *_i.c 52 | *_p.c 53 | *_i.h 54 | *.ilk 55 | *.meta 56 | *.obj 57 | *.pch 58 | *.pdb 59 | *.pgc 60 | *.pgd 61 | *.rsp 62 | *.sbr 63 | *.tlb 64 | *.tli 65 | *.tlh 66 | *.tmp 67 | *.tmp_proj 68 | *.log 69 | *.vspscc 70 | *.vssscc 71 | .builds 72 | *.pidb 73 | *.svclog 74 | *.scc 75 | 76 | # Chutzpah Test files 77 | _Chutzpah* 78 | 79 | # Visual C++ cache files 80 | ipch/ 81 | *.aps 82 | *.ncb 83 | *.opendb 84 | *.opensdf 85 | *.sdf 86 | *.cachefile 87 | *.VC.db 88 | *.VC.VC.opendb 89 | 90 | # Visual Studio profiler 91 | *.psess 92 | *.vsp 93 | *.vspx 94 | *.sap 95 | 96 | # TFS 2012 Local Workspace 97 | $tf/ 98 | 99 | # Guidance Automation Toolkit 100 | *.gpState 101 | 102 | # ReSharper is a .NET coding add-in 103 | _ReSharper*/ 104 | *.[Rr]e[Ss]harper 105 | *.DotSettings.user 106 | 107 | # JustCode is a .NET coding add-in 108 | .JustCode 109 | 110 | # TeamCity is a build add-in 111 | _TeamCity* 112 | 113 | # DotCover is a Code Coverage Tool 114 | *.dotCover 115 | 116 | # NCrunch 117 | _NCrunch_* 118 | .*crunch*.local.xml 119 | nCrunchTemp_* 120 | 121 | # MightyMoose 122 | *.mm.* 123 | AutoTest.Net/ 124 | 125 | # Web workbench (sass) 126 | .sass-cache/ 127 | 128 | # Installshield output folder 129 | [Ee]xpress/ 130 | 131 | # DocProject is a documentation generator add-in 132 | DocProject/buildhelp/ 133 | DocProject/Help/*.HxT 134 | DocProject/Help/*.HxC 135 | DocProject/Help/*.hhc 136 | DocProject/Help/*.hhk 137 | DocProject/Help/*.hhp 138 | DocProject/Help/Html2 139 | DocProject/Help/html 140 | 141 | # Click-Once directory 142 | publish/ 143 | 144 | # Publish Web Output 145 | *.[Pp]ublish.xml 146 | *.azurePubxml 147 | # TODO: Comment the next line if you want to checkin your web deploy settings 148 | # but database connection strings (with potential passwords) will be unencrypted 149 | #*.pubxml 150 | *.publishproj 151 | 152 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 153 | # checkin your Azure Web App publish settings, but sensitive information contained 154 | # in these scripts will be unencrypted 155 | PublishScripts/ 156 | 157 | # NuGet Packages 158 | *.nupkg 159 | # The packages folder can be ignored because of Package Restore 160 | **/packages/* 161 | # except build/, which is used as an MSBuild target. 162 | !**/packages/build/ 163 | # Uncomment if necessary however generally it will be regenerated when needed 164 | #!**/packages/repositories.config 165 | # NuGet v3's project.json files produces more ignoreable files 166 | *.nuget.props 167 | *.nuget.targets 168 | 169 | # Microsoft Azure Build Output 170 | csx/ 171 | *.build.csdef 172 | 173 | # Microsoft Azure Emulator 174 | ecf/ 175 | rcf/ 176 | 177 | # Windows Store app package directories and files 178 | AppPackages/ 179 | BundleArtifacts/ 180 | Package.StoreAssociation.xml 181 | _pkginfo.txt 182 | 183 | # Visual Studio cache files 184 | # files ending in .cache can be ignored 185 | *.[Cc]ache 186 | # but keep track of directories ending in .cache 187 | !*.[Cc]ache/ 188 | 189 | # Others 190 | ClientBin/ 191 | ~$* 192 | *~ 193 | *.dbmdl 194 | *.dbproj.schemaview 195 | *.jfm 196 | *.pfx 197 | *.publishsettings 198 | node_modules/ 199 | orleans.codegen.cs 200 | 201 | # Since there are multiple workflows, uncomment next line to ignore bower_components 202 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 203 | #bower_components/ 204 | 205 | # RIA/Silverlight projects 206 | Generated_Code/ 207 | 208 | # Backup & report files from converting an old project file 209 | # to a newer Visual Studio version. Backup files are not needed, 210 | # because we have git ;-) 211 | _UpgradeReport_Files/ 212 | Backup*/ 213 | UpgradeLog*.XML 214 | UpgradeLog*.htm 215 | 216 | # SQL Server files 217 | *.mdf 218 | *.ldf 219 | 220 | # Business Intelligence projects 221 | *.rdl.data 222 | *.bim.layout 223 | *.bim_*.settings 224 | 225 | # Microsoft Fakes 226 | FakesAssemblies/ 227 | 228 | # GhostDoc plugin setting file 229 | *.GhostDoc.xml 230 | 231 | # Node.js Tools for Visual Studio 232 | .ntvs_analysis.dat 233 | 234 | # Visual Studio 6 build log 235 | *.plg 236 | 237 | # Visual Studio 6 workspace options file 238 | *.opt 239 | 240 | # Visual Studio LightSwitch build output 241 | **/*.HTMLClient/GeneratedArtifacts 242 | **/*.DesktopClient/GeneratedArtifacts 243 | **/*.DesktopClient/ModelManifest.xml 244 | **/*.Server/GeneratedArtifacts 245 | **/*.Server/ModelManifest.xml 246 | _Pvt_Extensions 247 | 248 | # Paket dependency manager 249 | .paket/paket.exe 250 | paket-files/ 251 | 252 | # FAKE - F# Make 253 | .fake/ 254 | 255 | # JetBrains Rider 256 | .idea/ 257 | *.sln.iml 258 | 259 | # CodeRush 260 | .cr/ 261 | 262 | # Python Tools for Visual Studio (PTVS) 263 | __pycache__/ 264 | *.pyc 265 | 266 | .DS_Store -------------------------------------------------------------------------------- /samples/ConditionalMiddleware/ConditionalMiddleware.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | netcoreapp3.1 4 | v3 5 | Samples.ConditionalMiddleware 6 | Samples.ConditionalMiddleware 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | PreserveNewest 18 | 19 | 20 | PreserveNewest 21 | Never 22 | 23 | 24 | -------------------------------------------------------------------------------- /samples/ConditionalMiddleware/Functions/Functions.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Microsoft.AspNetCore.Http; 3 | using Microsoft.AspNetCore.Mvc; 4 | using Microsoft.Azure.WebJobs; 5 | using Microsoft.Azure.WebJobs.Extensions.Http; 6 | using Samples.ConditionalMiddleware.Pipelines; 7 | 8 | namespace Samples.ConditionalMiddleware.Functions 9 | { 10 | /// 11 | /// An HTTP-triggered Azure Function to demonstrate conditional middleware. 12 | /// 13 | public class Functions 14 | { 15 | private readonly IMiddlewarePipelineFactory pipelineFactory; 16 | 17 | /// 18 | /// Initializes a new instance of the class. 19 | /// 20 | /// The middleware pipeline factory. 21 | public Functions(IMiddlewarePipelineFactory pipelineFactory) 22 | { 23 | this.pipelineFactory = pipelineFactory; 24 | } 25 | 26 | /// 27 | /// The HTTP trigger entrypoint for the first function. 28 | /// 29 | /// The HTTP request. 30 | /// 31 | [FunctionName("Function1")] 32 | public async Task Function1( 33 | [HttpTrigger(AuthorizationLevel.Function, "get")] HttpRequest req) 34 | { 35 | var pipeline = this.pipelineFactory.Create(this.ExecuteFunction1Async); 36 | return await pipeline.RunAsync(); 37 | } 38 | 39 | /// 40 | /// The HTTP trigger entrypoint for the second function. 41 | /// 42 | /// The HTTP request. 43 | /// 44 | [FunctionName("Function2")] 45 | public async Task Function2( 46 | [HttpTrigger(AuthorizationLevel.Function, "get")] HttpRequest req) 47 | { 48 | var pipeline = this.pipelineFactory.Create(this.ExecuteFunction2Async); 49 | return await pipeline.RunAsync(); 50 | } 51 | 52 | /// 53 | /// Executes the function 1's business logic. 54 | /// 55 | /// The HTTP context for the request. 56 | /// The result. 57 | private Task ExecuteFunction1Async(HttpContext context) 58 | { 59 | var payload = new 60 | { 61 | message = "OK", 62 | functionName = "Function1" 63 | }; 64 | 65 | return Task.FromResult(new OkObjectResult(payload)); 66 | } 67 | 68 | /// 69 | /// Executes the function 2's business logic. 70 | /// 71 | /// The HTTP context for the request. 72 | /// The result. 73 | private Task ExecuteFunction2Async(HttpContext context) 74 | { 75 | var payload = new 76 | { 77 | message = "OK", 78 | functionName = "Function2" 79 | }; 80 | 81 | return Task.FromResult(new OkObjectResult(payload)); 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /samples/ConditionalMiddleware/Pipelines/IMiddlewarePipelineFactory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Microsoft.AspNetCore.Http; 4 | using Microsoft.AspNetCore.Mvc; 5 | using Umamimolecule.AzureFunctionsMiddleware; 6 | 7 | namespace Samples.ConditionalMiddleware.Pipelines 8 | { 9 | /// 10 | /// Represents a component to creates middleware pipeline instances used by this project. 11 | /// 12 | public interface IMiddlewarePipelineFactory 13 | { 14 | /// 15 | /// Creates a pipeline. 16 | /// 17 | /// The method containing the Azure Function business logic implementation. 18 | /// The middleware pipeline. 19 | IMiddlewarePipeline Create(Func> func); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /samples/ConditionalMiddleware/Pipelines/MiddlewareA.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Microsoft.AspNetCore.Http; 3 | using Umamimolecule.AzureFunctionsMiddleware; 4 | 5 | namespace Samples.ConditionalMiddleware.Pipelines 6 | { 7 | public class MiddlewareA : HttpMiddleware 8 | { 9 | public override async Task InvokeAsync(HttpContext context) 10 | { 11 | context.Response.Headers["x-middleware-a"] = "Hello from middleware A"; 12 | 13 | if (this.Next != null) 14 | { 15 | await this.Next.InvokeAsync(context); 16 | } 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /samples/ConditionalMiddleware/Pipelines/MiddlewareB.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Microsoft.AspNetCore.Http; 3 | using Umamimolecule.AzureFunctionsMiddleware; 4 | 5 | namespace Samples.ConditionalMiddleware.Pipelines 6 | { 7 | public class MiddlewareB : HttpMiddleware 8 | { 9 | public override async Task InvokeAsync(HttpContext context) 10 | { 11 | context.Response.Headers["x-middleware-b"] = "Hello from middleware B"; 12 | 13 | if (this.Next != null) 14 | { 15 | await this.Next.InvokeAsync(context); 16 | } 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /samples/ConditionalMiddleware/Pipelines/MiddlewarePipelineFactory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Microsoft.AspNetCore.Http; 4 | using Microsoft.AspNetCore.Mvc; 5 | using Umamimolecule.AzureFunctionsMiddleware; 6 | 7 | namespace Samples.ConditionalMiddleware.Pipelines 8 | { 9 | /// 10 | /// A component to creates middleware pipeline instances used by this project. 11 | /// 12 | public class MiddlewarePipelineFactory : IMiddlewarePipelineFactory 13 | { 14 | private readonly IHttpContextAccessor httpContextAccessor; 15 | private readonly MiddlewareA middlewareA; 16 | private readonly MiddlewareB middlewareB; 17 | 18 | /// 19 | /// Initializes a new instance of the class. 20 | /// 21 | /// The HTTP context accessor. 22 | /// Middleware A 23 | /// Middleware B 24 | public MiddlewarePipelineFactory( 25 | IHttpContextAccessor httpContextAccessor, 26 | MiddlewareA middlewareA, 27 | MiddlewareB middlewareB) 28 | { 29 | this.httpContextAccessor = httpContextAccessor; 30 | this.middlewareA = middlewareA; 31 | this.middlewareB = middlewareB; 32 | } 33 | 34 | /// 35 | /// Creates a pipeline to validate query parameters. 36 | /// 37 | /// The object type representing the query parameters. 38 | /// The method containing the Azure Function business logic implementation. 39 | /// The middleware pipeline. 40 | public IMiddlewarePipeline Create(Func> func) 41 | { 42 | MiddlewarePipeline pipeline = new MiddlewarePipeline(this.httpContextAccessor); 43 | 44 | // If Function1 is called, then use MiddlewareA and B, else use MiddlewareB only 45 | return pipeline.UseWhen(ctx => ctx.Request.Path.StartsWithSegments("/api/Function1"), 46 | p => p.Use(middlewareA)) 47 | .Use(middlewareB) 48 | .Use(func); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /samples/ConditionalMiddleware/README.md: -------------------------------------------------------------------------------- 1 | # Sample: Conditional middleware 2 | 3 | This example project shows how to conditionally use middleware using the `UseWhen` extension method for a pipeline. 4 | 5 | ## ConditionalMiddleware 6 | 7 | This project contains two functions: `http://localhost:7071/api/Function1` and `http://localhost:7071/api/Function2`. 8 | 9 | Function1 will execute middleware A then B, whilst Function2 will only execute middleware B. 10 | 11 | The pipeline is set up using the following code, which inspects the inbound request to decide which middleware is invoked: 12 | 13 | ``` 14 | return pipeline.UseWhen(ctx => ctx.Request.Path.StartsWithSegments("/api/Function1"), 15 | p => p.Use(middlewareA)) 16 | .Use(middlewareB) 17 | .Use(func); 18 | ``` 19 | -------------------------------------------------------------------------------- /samples/ConditionalMiddleware/Startup.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Azure.Functions.Extensions.DependencyInjection; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using Samples.ConditionalMiddleware.Pipelines; 4 | 5 | [assembly: FunctionsStartup(typeof(Samples.ConditionalMiddleware.Startup))] 6 | 7 | namespace Samples.ConditionalMiddleware 8 | { 9 | public class Startup : FunctionsStartup 10 | { 11 | public override void Configure(IFunctionsHostBuilder builder) 12 | { 13 | builder.Services.AddHttpContextAccessor(); 14 | builder.Services.AddTransient(); 15 | builder.Services.AddTransient(); 16 | builder.Services.AddTransient(); 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /samples/ConditionalMiddleware/host.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0" 3 | } -------------------------------------------------------------------------------- /samples/ExceptionHandler/.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # Azure Functions localsettings file 5 | local.settings.json 6 | 7 | # User-specific files 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Build results 17 | [Dd]ebug/ 18 | [Dd]ebugPublic/ 19 | [Rr]elease/ 20 | [Rr]eleases/ 21 | x64/ 22 | x86/ 23 | bld/ 24 | [Bb]in/ 25 | [Oo]bj/ 26 | [Ll]og/ 27 | 28 | # Visual Studio 2015 cache/options directory 29 | .vs/ 30 | # Uncomment if you have tasks that create the project's static files in wwwroot 31 | #wwwroot/ 32 | 33 | # MSTest test Results 34 | [Tt]est[Rr]esult*/ 35 | [Bb]uild[Ll]og.* 36 | 37 | # NUNIT 38 | *.VisualState.xml 39 | TestResult.xml 40 | 41 | # Build Results of an ATL Project 42 | [Dd]ebugPS/ 43 | [Rr]eleasePS/ 44 | dlldata.c 45 | 46 | # DNX 47 | project.lock.json 48 | project.fragment.lock.json 49 | artifacts/ 50 | 51 | *_i.c 52 | *_p.c 53 | *_i.h 54 | *.ilk 55 | *.meta 56 | *.obj 57 | *.pch 58 | *.pdb 59 | *.pgc 60 | *.pgd 61 | *.rsp 62 | *.sbr 63 | *.tlb 64 | *.tli 65 | *.tlh 66 | *.tmp 67 | *.tmp_proj 68 | *.log 69 | *.vspscc 70 | *.vssscc 71 | .builds 72 | *.pidb 73 | *.svclog 74 | *.scc 75 | 76 | # Chutzpah Test files 77 | _Chutzpah* 78 | 79 | # Visual C++ cache files 80 | ipch/ 81 | *.aps 82 | *.ncb 83 | *.opendb 84 | *.opensdf 85 | *.sdf 86 | *.cachefile 87 | *.VC.db 88 | *.VC.VC.opendb 89 | 90 | # Visual Studio profiler 91 | *.psess 92 | *.vsp 93 | *.vspx 94 | *.sap 95 | 96 | # TFS 2012 Local Workspace 97 | $tf/ 98 | 99 | # Guidance Automation Toolkit 100 | *.gpState 101 | 102 | # ReSharper is a .NET coding add-in 103 | _ReSharper*/ 104 | *.[Rr]e[Ss]harper 105 | *.DotSettings.user 106 | 107 | # JustCode is a .NET coding add-in 108 | .JustCode 109 | 110 | # TeamCity is a build add-in 111 | _TeamCity* 112 | 113 | # DotCover is a Code Coverage Tool 114 | *.dotCover 115 | 116 | # NCrunch 117 | _NCrunch_* 118 | .*crunch*.local.xml 119 | nCrunchTemp_* 120 | 121 | # MightyMoose 122 | *.mm.* 123 | AutoTest.Net/ 124 | 125 | # Web workbench (sass) 126 | .sass-cache/ 127 | 128 | # Installshield output folder 129 | [Ee]xpress/ 130 | 131 | # DocProject is a documentation generator add-in 132 | DocProject/buildhelp/ 133 | DocProject/Help/*.HxT 134 | DocProject/Help/*.HxC 135 | DocProject/Help/*.hhc 136 | DocProject/Help/*.hhk 137 | DocProject/Help/*.hhp 138 | DocProject/Help/Html2 139 | DocProject/Help/html 140 | 141 | # Click-Once directory 142 | publish/ 143 | 144 | # Publish Web Output 145 | *.[Pp]ublish.xml 146 | *.azurePubxml 147 | # TODO: Comment the next line if you want to checkin your web deploy settings 148 | # but database connection strings (with potential passwords) will be unencrypted 149 | #*.pubxml 150 | *.publishproj 151 | 152 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 153 | # checkin your Azure Web App publish settings, but sensitive information contained 154 | # in these scripts will be unencrypted 155 | PublishScripts/ 156 | 157 | # NuGet Packages 158 | *.nupkg 159 | # The packages folder can be ignored because of Package Restore 160 | **/packages/* 161 | # except build/, which is used as an MSBuild target. 162 | !**/packages/build/ 163 | # Uncomment if necessary however generally it will be regenerated when needed 164 | #!**/packages/repositories.config 165 | # NuGet v3's project.json files produces more ignoreable files 166 | *.nuget.props 167 | *.nuget.targets 168 | 169 | # Microsoft Azure Build Output 170 | csx/ 171 | *.build.csdef 172 | 173 | # Microsoft Azure Emulator 174 | ecf/ 175 | rcf/ 176 | 177 | # Windows Store app package directories and files 178 | AppPackages/ 179 | BundleArtifacts/ 180 | Package.StoreAssociation.xml 181 | _pkginfo.txt 182 | 183 | # Visual Studio cache files 184 | # files ending in .cache can be ignored 185 | *.[Cc]ache 186 | # but keep track of directories ending in .cache 187 | !*.[Cc]ache/ 188 | 189 | # Others 190 | ClientBin/ 191 | ~$* 192 | *~ 193 | *.dbmdl 194 | *.dbproj.schemaview 195 | *.jfm 196 | *.pfx 197 | *.publishsettings 198 | node_modules/ 199 | orleans.codegen.cs 200 | 201 | # Since there are multiple workflows, uncomment next line to ignore bower_components 202 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 203 | #bower_components/ 204 | 205 | # RIA/Silverlight projects 206 | Generated_Code/ 207 | 208 | # Backup & report files from converting an old project file 209 | # to a newer Visual Studio version. Backup files are not needed, 210 | # because we have git ;-) 211 | _UpgradeReport_Files/ 212 | Backup*/ 213 | UpgradeLog*.XML 214 | UpgradeLog*.htm 215 | 216 | # SQL Server files 217 | *.mdf 218 | *.ldf 219 | 220 | # Business Intelligence projects 221 | *.rdl.data 222 | *.bim.layout 223 | *.bim_*.settings 224 | 225 | # Microsoft Fakes 226 | FakesAssemblies/ 227 | 228 | # GhostDoc plugin setting file 229 | *.GhostDoc.xml 230 | 231 | # Node.js Tools for Visual Studio 232 | .ntvs_analysis.dat 233 | 234 | # Visual Studio 6 build log 235 | *.plg 236 | 237 | # Visual Studio 6 workspace options file 238 | *.opt 239 | 240 | # Visual Studio LightSwitch build output 241 | **/*.HTMLClient/GeneratedArtifacts 242 | **/*.DesktopClient/GeneratedArtifacts 243 | **/*.DesktopClient/ModelManifest.xml 244 | **/*.Server/GeneratedArtifacts 245 | **/*.Server/ModelManifest.xml 246 | _Pvt_Extensions 247 | 248 | # Paket dependency manager 249 | .paket/paket.exe 250 | paket-files/ 251 | 252 | # FAKE - F# Make 253 | .fake/ 254 | 255 | # JetBrains Rider 256 | .idea/ 257 | *.sln.iml 258 | 259 | # CodeRush 260 | .cr/ 261 | 262 | # Python Tools for Visual Studio (PTVS) 263 | __pycache__/ 264 | *.pyc -------------------------------------------------------------------------------- /samples/ExceptionHandler/ExceptionHandler.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | netcoreapp3.1 4 | v3 5 | Samples.ExceptionHandler 6 | Samples.ModelValidation 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | PreserveNewest 18 | 19 | 20 | PreserveNewest 21 | Never 22 | 23 | 24 | -------------------------------------------------------------------------------- /samples/ExceptionHandler/Exceptions/ThrottledException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Samples.ModelValidation.Exceptions 4 | { 5 | class ThrottledException : Exception 6 | { 7 | public ThrottledException(TimeSpan tryAgain) 8 | { 9 | this.TryAgain = tryAgain; 10 | } 11 | 12 | public TimeSpan TryAgain { get; private set; } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /samples/ExceptionHandler/Functions/ExceptionHandler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Microsoft.AspNetCore.Mvc; 4 | using Microsoft.Azure.WebJobs; 5 | using Microsoft.Azure.WebJobs.Extensions.Http; 6 | using Microsoft.AspNetCore.Http; 7 | using Umamimolecule.AzureFunctionsMiddleware; 8 | using Samples.ModelValidation.Pipelines; 9 | using Samples.ModelValidation.Exceptions; 10 | 11 | namespace Samples.ModelValidation.Functions 12 | { 13 | /// 14 | /// An HTTP-triggered Azure Function to demonstrate exception handling middleware. 15 | /// 16 | public class ExceptionHandler 17 | { 18 | private readonly IMiddlewarePipeline pipeline; 19 | 20 | /// 21 | /// Initializes a new instance of the class. 22 | /// 23 | /// The middleware pipeline factory. 24 | public ExceptionHandler(IMiddlewarePipelineFactory pipelineFactory) 25 | { 26 | this.pipeline = pipelineFactory.Create(this.ExecuteAsync); 27 | } 28 | 29 | /// 30 | /// The HTTP trigger entrypoint for the function. 31 | /// 32 | /// The HTTP request. 33 | /// 34 | [FunctionName(nameof(ExceptionHandler))] 35 | public async Task Run( 36 | [HttpTrigger(AuthorizationLevel.Function, "get")] HttpRequest req) 37 | { 38 | return await this.pipeline.RunAsync(); 39 | } 40 | 41 | /// 42 | /// Executes the function's business logic. At this point, the body model 43 | /// has been validated correctly. 44 | /// 45 | /// The HTTP context for the request. 46 | /// The result. 47 | private Task ExecuteAsync(HttpContext context) 48 | { 49 | // Throw an exception to test to handler 50 | throw new ThrottledException(TimeSpan.FromMinutes(5)); 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /samples/ExceptionHandler/Pipelines/CustomExceptionHandler.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net; 3 | using System.Threading.Tasks; 4 | using Microsoft.AspNetCore.Http; 5 | using Microsoft.AspNetCore.Mvc; 6 | using Samples.ModelValidation.Exceptions; 7 | 8 | namespace Samples.ModelValidation.Pipelines 9 | { 10 | class CustomExceptionHandler 11 | { 12 | public static Task HandleAsync(Exception exception, HttpContext context) 13 | { 14 | IActionResult result; 15 | switch (exception) 16 | { 17 | case ThrottledException t: 18 | context.Response.Headers["Retry-After"] = ((int)t.TryAgain.TotalSeconds).ToString(); 19 | result = new StatusCodeResult(429); 20 | break; 21 | 22 | default: 23 | var body = new 24 | { 25 | correlationId = context.TraceIdentifier, 26 | error = new 27 | { 28 | code = "INTERNAL_SERVER_ERROR", 29 | message = "An unexpected error occurred." 30 | } 31 | }; 32 | 33 | result = new ObjectResult(body) 34 | { 35 | StatusCode = (int)HttpStatusCode.InternalServerError 36 | }; 37 | break; 38 | } 39 | 40 | return Task.FromResult(result); 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /samples/ExceptionHandler/Pipelines/IMiddlewarePipelineFactory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Microsoft.AspNetCore.Http; 4 | using Microsoft.AspNetCore.Mvc; 5 | using Umamimolecule.AzureFunctionsMiddleware; 6 | 7 | namespace Samples.ModelValidation.Pipelines 8 | { 9 | /// 10 | /// Represents a component to creates middleware pipeline instances used by this project. 11 | /// 12 | public interface IMiddlewarePipelineFactory 13 | { 14 | /// 15 | /// Creates a pipeline to demonstrate exception handling query parameters. 16 | /// 17 | /// The method containing the Azure Function business logic implementation. 18 | /// The middleware pipeline. 19 | IMiddlewarePipeline Create(Func> func); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /samples/ExceptionHandler/Pipelines/MiddlewarePipelineFactory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Microsoft.AspNetCore.Http; 4 | using Microsoft.AspNetCore.Mvc; 5 | using Umamimolecule.AzureFunctionsMiddleware; 6 | 7 | namespace Samples.ModelValidation.Pipelines 8 | { 9 | /// 10 | /// A component to creates middleware pipeline instances used by this project. 11 | /// 12 | public class MiddlewarePipelineFactory : IMiddlewarePipelineFactory 13 | { 14 | private readonly IHttpContextAccessor httpContextAccessor; 15 | 16 | /// 17 | /// The request headers containing correlation identifiers. 18 | /// 19 | private readonly static string[] correlationIdHeaders = new string[] 20 | { 21 | "ms-request-id", 22 | "request-id" 23 | }; 24 | 25 | /// 26 | /// Initializes a new instance of the class. 27 | /// 28 | /// The HTTP context accessor. 29 | public MiddlewarePipelineFactory(IHttpContextAccessor httpContextAccessor) 30 | { 31 | this.httpContextAccessor = httpContextAccessor; 32 | } 33 | 34 | /// 35 | /// Creates a pipeline to demonstrate exception handling. 36 | /// 37 | /// The method containing the Azure Function business logic implementation. 38 | /// The middleware pipeline. 39 | public IMiddlewarePipeline Create(Func> func) 40 | { 41 | MiddlewarePipeline pipeline = new MiddlewarePipeline(this.httpContextAccessor); 42 | return pipeline.UseCorrelationId(correlationIdHeaders) 43 | .UseExceptionHandling(CustomExceptionHandler.HandleAsync) 44 | .Use(func); 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /samples/ExceptionHandler/README.md: -------------------------------------------------------------------------------- 1 | # Sample: Model validation 2 | 3 | This example project shows how to configure the built-in middleware to perform exception handling. 4 | 5 | ## ExceptionHandler 6 | 7 | Submit a GET request to the endpoint `http://localhost:7071/api/ExceptionHandler`: 8 | 9 | This will return 429 Too Many Requests. -------------------------------------------------------------------------------- /samples/ExceptionHandler/Startup.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Azure.Functions.Extensions.DependencyInjection; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using Samples.ModelValidation.Pipelines; 4 | 5 | [assembly: FunctionsStartup(typeof(Samples.ModelValidation.Startup))] 6 | 7 | namespace Samples.ModelValidation 8 | { 9 | public class Startup : FunctionsStartup 10 | { 11 | public override void Configure(IFunctionsHostBuilder builder) 12 | { 13 | builder.Services.AddHttpContextAccessor(); 14 | builder.Services.AddTransient(); 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /samples/ExceptionHandler/host.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0" 3 | } -------------------------------------------------------------------------------- /samples/ModelValidation/.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # Azure Functions localsettings file 5 | local.settings.json 6 | 7 | # User-specific files 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Build results 17 | [Dd]ebug/ 18 | [Dd]ebugPublic/ 19 | [Rr]elease/ 20 | [Rr]eleases/ 21 | x64/ 22 | x86/ 23 | bld/ 24 | [Bb]in/ 25 | [Oo]bj/ 26 | [Ll]og/ 27 | 28 | # Visual Studio 2015 cache/options directory 29 | .vs/ 30 | # Uncomment if you have tasks that create the project's static files in wwwroot 31 | #wwwroot/ 32 | 33 | # MSTest test Results 34 | [Tt]est[Rr]esult*/ 35 | [Bb]uild[Ll]og.* 36 | 37 | # NUNIT 38 | *.VisualState.xml 39 | TestResult.xml 40 | 41 | # Build Results of an ATL Project 42 | [Dd]ebugPS/ 43 | [Rr]eleasePS/ 44 | dlldata.c 45 | 46 | # DNX 47 | project.lock.json 48 | project.fragment.lock.json 49 | artifacts/ 50 | 51 | *_i.c 52 | *_p.c 53 | *_i.h 54 | *.ilk 55 | *.meta 56 | *.obj 57 | *.pch 58 | *.pdb 59 | *.pgc 60 | *.pgd 61 | *.rsp 62 | *.sbr 63 | *.tlb 64 | *.tli 65 | *.tlh 66 | *.tmp 67 | *.tmp_proj 68 | *.log 69 | *.vspscc 70 | *.vssscc 71 | .builds 72 | *.pidb 73 | *.svclog 74 | *.scc 75 | 76 | # Chutzpah Test files 77 | _Chutzpah* 78 | 79 | # Visual C++ cache files 80 | ipch/ 81 | *.aps 82 | *.ncb 83 | *.opendb 84 | *.opensdf 85 | *.sdf 86 | *.cachefile 87 | *.VC.db 88 | *.VC.VC.opendb 89 | 90 | # Visual Studio profiler 91 | *.psess 92 | *.vsp 93 | *.vspx 94 | *.sap 95 | 96 | # TFS 2012 Local Workspace 97 | $tf/ 98 | 99 | # Guidance Automation Toolkit 100 | *.gpState 101 | 102 | # ReSharper is a .NET coding add-in 103 | _ReSharper*/ 104 | *.[Rr]e[Ss]harper 105 | *.DotSettings.user 106 | 107 | # JustCode is a .NET coding add-in 108 | .JustCode 109 | 110 | # TeamCity is a build add-in 111 | _TeamCity* 112 | 113 | # DotCover is a Code Coverage Tool 114 | *.dotCover 115 | 116 | # NCrunch 117 | _NCrunch_* 118 | .*crunch*.local.xml 119 | nCrunchTemp_* 120 | 121 | # MightyMoose 122 | *.mm.* 123 | AutoTest.Net/ 124 | 125 | # Web workbench (sass) 126 | .sass-cache/ 127 | 128 | # Installshield output folder 129 | [Ee]xpress/ 130 | 131 | # DocProject is a documentation generator add-in 132 | DocProject/buildhelp/ 133 | DocProject/Help/*.HxT 134 | DocProject/Help/*.HxC 135 | DocProject/Help/*.hhc 136 | DocProject/Help/*.hhk 137 | DocProject/Help/*.hhp 138 | DocProject/Help/Html2 139 | DocProject/Help/html 140 | 141 | # Click-Once directory 142 | publish/ 143 | 144 | # Publish Web Output 145 | *.[Pp]ublish.xml 146 | *.azurePubxml 147 | # TODO: Comment the next line if you want to checkin your web deploy settings 148 | # but database connection strings (with potential passwords) will be unencrypted 149 | #*.pubxml 150 | *.publishproj 151 | 152 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 153 | # checkin your Azure Web App publish settings, but sensitive information contained 154 | # in these scripts will be unencrypted 155 | PublishScripts/ 156 | 157 | # NuGet Packages 158 | *.nupkg 159 | # The packages folder can be ignored because of Package Restore 160 | **/packages/* 161 | # except build/, which is used as an MSBuild target. 162 | !**/packages/build/ 163 | # Uncomment if necessary however generally it will be regenerated when needed 164 | #!**/packages/repositories.config 165 | # NuGet v3's project.json files produces more ignoreable files 166 | *.nuget.props 167 | *.nuget.targets 168 | 169 | # Microsoft Azure Build Output 170 | csx/ 171 | *.build.csdef 172 | 173 | # Microsoft Azure Emulator 174 | ecf/ 175 | rcf/ 176 | 177 | # Windows Store app package directories and files 178 | AppPackages/ 179 | BundleArtifacts/ 180 | Package.StoreAssociation.xml 181 | _pkginfo.txt 182 | 183 | # Visual Studio cache files 184 | # files ending in .cache can be ignored 185 | *.[Cc]ache 186 | # but keep track of directories ending in .cache 187 | !*.[Cc]ache/ 188 | 189 | # Others 190 | ClientBin/ 191 | ~$* 192 | *~ 193 | *.dbmdl 194 | *.dbproj.schemaview 195 | *.jfm 196 | *.pfx 197 | *.publishsettings 198 | node_modules/ 199 | orleans.codegen.cs 200 | 201 | # Since there are multiple workflows, uncomment next line to ignore bower_components 202 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 203 | #bower_components/ 204 | 205 | # RIA/Silverlight projects 206 | Generated_Code/ 207 | 208 | # Backup & report files from converting an old project file 209 | # to a newer Visual Studio version. Backup files are not needed, 210 | # because we have git ;-) 211 | _UpgradeReport_Files/ 212 | Backup*/ 213 | UpgradeLog*.XML 214 | UpgradeLog*.htm 215 | 216 | # SQL Server files 217 | *.mdf 218 | *.ldf 219 | 220 | # Business Intelligence projects 221 | *.rdl.data 222 | *.bim.layout 223 | *.bim_*.settings 224 | 225 | # Microsoft Fakes 226 | FakesAssemblies/ 227 | 228 | # GhostDoc plugin setting file 229 | *.GhostDoc.xml 230 | 231 | # Node.js Tools for Visual Studio 232 | .ntvs_analysis.dat 233 | 234 | # Visual Studio 6 build log 235 | *.plg 236 | 237 | # Visual Studio 6 workspace options file 238 | *.opt 239 | 240 | # Visual Studio LightSwitch build output 241 | **/*.HTMLClient/GeneratedArtifacts 242 | **/*.DesktopClient/GeneratedArtifacts 243 | **/*.DesktopClient/ModelManifest.xml 244 | **/*.Server/GeneratedArtifacts 245 | **/*.Server/ModelManifest.xml 246 | _Pvt_Extensions 247 | 248 | # Paket dependency manager 249 | .paket/paket.exe 250 | paket-files/ 251 | 252 | # FAKE - F# Make 253 | .fake/ 254 | 255 | # JetBrains Rider 256 | .idea/ 257 | *.sln.iml 258 | 259 | # CodeRush 260 | .cr/ 261 | 262 | # Python Tools for Visual Studio (PTVS) 263 | __pycache__/ 264 | *.pyc -------------------------------------------------------------------------------- /samples/ModelValidation/Functions/BodyValidation.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Microsoft.AspNetCore.Mvc; 3 | using Microsoft.Azure.WebJobs; 4 | using Microsoft.Azure.WebJobs.Extensions.Http; 5 | using Microsoft.AspNetCore.Http; 6 | using Umamimolecule.AzureFunctionsMiddleware; 7 | using Samples.ModelValidation.Pipelines; 8 | 9 | namespace Samples.ModelValidation.Functions 10 | { 11 | /// 12 | /// An HTTP-triggered Azure Function to demonstrate body payload parameter 13 | /// validation using middleware. 14 | /// 15 | public class BodyValidation 16 | { 17 | private readonly IMiddlewarePipeline pipeline; 18 | 19 | /// 20 | /// Initializes a new instance of the class. 21 | /// 22 | /// The middleware pipeline factory. 23 | public BodyValidation(IMiddlewarePipelineFactory pipelineFactory) 24 | { 25 | // Note: You could simply new-up a MiddlewarePipeline instance here and build it, 26 | // but this example uses a pipeline factory so you can share pipelines between 27 | // Azure Functions that have common requirements, without having to duplicate 28 | // code. 29 | 30 | this.pipeline = pipelineFactory.CreateForBody(this.ExecuteAsync); 31 | } 32 | 33 | /// 34 | /// The HTTP trigger entrypoint for the function. 35 | /// 36 | /// The HTTP request. 37 | /// 38 | [FunctionName(nameof(BodyValidation))] 39 | public async Task Run( 40 | [HttpTrigger(AuthorizationLevel.Function, "post")] HttpRequest req) 41 | { 42 | return await this.pipeline.RunAsync(); 43 | } 44 | 45 | /// 46 | /// Executes the function's business logic. At this point, the body model 47 | /// has been validated correctly. 48 | /// 49 | /// The HTTP context for the request. 50 | /// The result. 51 | private Task ExecuteAsync(HttpContext context) 52 | { 53 | var payload = new 54 | { 55 | correlationId = context.TraceIdentifier, 56 | message = "OK", 57 | body = context.Items[ContextItems.Body] 58 | }; 59 | 60 | return Task.FromResult(new OkObjectResult(payload)); 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /samples/ModelValidation/Functions/BodyValidationBody.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace Samples.ModelValidation.Functions 4 | { 5 | /// 6 | /// An object representing the body payload for the function. 7 | /// 8 | /// 9 | /// The following JSON structures are valid examples of this payload: 10 | /// 11 | /// { 12 | /// "name": "Test" 13 | /// } 14 | /// 15 | /// { 16 | /// "name": "Test", 17 | /// "user": { 18 | /// "id": 1 19 | /// } 20 | /// } 21 | /// 22 | /// { 23 | /// "name": "Test", 24 | /// "user": { 25 | /// "id": 1, 26 | /// "name": "Fred" 27 | /// } 28 | /// } 29 | /// 30 | /// 31 | public class BodyValidationBody 32 | { 33 | /// 34 | /// Gets or sets the name. 35 | /// 36 | /// 37 | /// This is marked with the attribute, so it is mandatory. 38 | /// 39 | [Required] 40 | public string Name { get; set; } 41 | 42 | /// 43 | /// Gets or sets the user data. 44 | /// 45 | /// 46 | /// This is marked with the attribute, so it is mandatory. 47 | /// However if it is supplied, then an ID must be supplied. 48 | /// 49 | public UserInfo User { get; set; } 50 | 51 | public class UserInfo 52 | { 53 | [Required] 54 | public int ID { get; set; } 55 | 56 | [Required] 57 | public string Name { get; set; } 58 | 59 | public string Description { get; set; } 60 | } 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /samples/ModelValidation/Functions/BodyValidationWithCustomResponse.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Microsoft.AspNetCore.Mvc; 3 | using Microsoft.Azure.WebJobs; 4 | using Microsoft.Azure.WebJobs.Extensions.Http; 5 | using Microsoft.AspNetCore.Http; 6 | using Samples.ModelValidation.Pipelines; 7 | using Samples.ModelValidation.Responses; 8 | using Umamimolecule.AzureFunctionsMiddleware; 9 | 10 | namespace Samples.ModelValidation.Functions 11 | { 12 | /// 13 | /// An HTTP-triggered Azure Function to demonstrate body payload parameter 14 | /// validation using middleware. 15 | /// 16 | public class BodyValidationWithCustomResponse 17 | { 18 | private readonly IMiddlewarePipeline pipeline; 19 | 20 | /// 21 | /// Initializes a new instance of the class. 22 | /// 23 | /// The middleware pipeline factory. 24 | public BodyValidationWithCustomResponse(IMiddlewarePipelineFactory pipelineFactory) 25 | { 26 | // Note: You could simply new-up a MiddlewarePipeline instance here and build it, 27 | // but this example uses a pipeline factory so you can share pipelines between 28 | // Azure Functions that have common requirements, without having to duplicate 29 | // code. 30 | 31 | this.pipeline = pipelineFactory.CreateForBody( 32 | this.ExecuteAsync, 33 | ResponseHelper.HandleValidationFailure); 34 | } 35 | 36 | /// 37 | /// The HTTP trigger entrypoint for the function. 38 | /// 39 | /// The HTTP request. 40 | /// 41 | [FunctionName(nameof(BodyValidationWithCustomResponse))] 42 | public async Task Run( 43 | [HttpTrigger(AuthorizationLevel.Function, "post")] HttpRequest req) 44 | { 45 | return await this.pipeline.RunAsync(); 46 | } 47 | 48 | /// 49 | /// Executes the function's business logic. At this point, the body model 50 | /// has been validated correctly. 51 | /// 52 | /// The HTTP context for the request. 53 | /// The result. 54 | private Task ExecuteAsync(HttpContext context) 55 | { 56 | var payload = new 57 | { 58 | correlationId = context.TraceIdentifier, 59 | message = "OK", 60 | body = context.Items[ContextItems.Body] 61 | }; 62 | 63 | return Task.FromResult(new OkObjectResult(payload)); 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /samples/ModelValidation/Functions/QueryValidation.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Microsoft.AspNetCore.Mvc; 3 | using Microsoft.Azure.WebJobs; 4 | using Microsoft.Azure.WebJobs.Extensions.Http; 5 | using Microsoft.AspNetCore.Http; 6 | using Samples.ModelValidation.Pipelines; 7 | using Umamimolecule.AzureFunctionsMiddleware; 8 | 9 | namespace Samples.ModelValidation.Functions 10 | { 11 | /// 12 | /// An HTTP-triggered Azure Function to demonstrate query parameter validation 13 | /// using middleware. 14 | /// 15 | public class QueryValidation 16 | { 17 | private readonly IMiddlewarePipeline pipeline; 18 | 19 | /// 20 | /// Initializes a new instance of the class. 21 | /// 22 | /// The middleware pipeline factory. 23 | public QueryValidation(IMiddlewarePipelineFactory pipelineFactory) 24 | { 25 | // Note: You could simply new-up a MiddlewarePipeline instance here and build it, 26 | // but this example uses a pipeline factory so you can share pipelines between 27 | // Azure Functions that have common requirements, without having to duplicate 28 | // code. 29 | 30 | this.pipeline = pipelineFactory.CreateForQuery(this.ExecuteAsync); 31 | } 32 | 33 | /// 34 | /// The HTTP trigger entrypoint for the function. 35 | /// 36 | /// The HTTP request. 37 | /// The result. 38 | [FunctionName(nameof(QueryValidation))] 39 | public async Task Run( 40 | [HttpTrigger(AuthorizationLevel.Function, "get")] HttpRequest req) 41 | { 42 | // Note that we don't need to use HttpRequest parameter here as the 43 | // pipeline is using IHttpContextAccessor. 44 | return await this.pipeline.RunAsync(); 45 | } 46 | 47 | /// 48 | /// Executes the function's business logic. At this point, the query model 49 | /// has been validated correctly. 50 | /// 51 | /// The HTTP context for the request. 52 | /// The result. 53 | private Task ExecuteAsync(HttpContext context) 54 | { 55 | var payload = new 56 | { 57 | correlationId = context.TraceIdentifier, 58 | message = "OK", 59 | query = context.Items[ContextItems.Query] 60 | }; 61 | 62 | return Task.FromResult(new OkObjectResult(payload)); 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /samples/ModelValidation/Functions/QueryValidationQueryParameters.cs: -------------------------------------------------------------------------------- 1 | using System.ComponentModel.DataAnnotations; 2 | 3 | namespace Samples.ModelValidation.Functions 4 | { 5 | /// 6 | /// An object representing the query parameters for the function. 7 | /// 8 | public class QueryValidationQueryParameters 9 | { 10 | /// 11 | /// Gets or sets the name. 12 | /// 13 | /// 14 | /// This is marked with the attribute, so it is mandatory. 15 | /// 16 | [Required] 17 | public string Name { get; set; } 18 | 19 | /// 20 | /// Gets or sets the description. 21 | /// 22 | /// 23 | /// Since this is not marked with the attribute, it is optional. 24 | /// 25 | public string Description { get; set; } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /samples/ModelValidation/Functions/QueryValidationWithCustomResponse.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Microsoft.AspNetCore.Mvc; 3 | using Microsoft.Azure.WebJobs; 4 | using Microsoft.Azure.WebJobs.Extensions.Http; 5 | using Microsoft.AspNetCore.Http; 6 | using Samples.ModelValidation.Pipelines; 7 | using Samples.ModelValidation.Responses; 8 | using Umamimolecule.AzureFunctionsMiddleware; 9 | 10 | namespace Samples.ModelValidation.Functions 11 | { 12 | /// 13 | /// An HTTP-triggered Azure Function to demonstrate query parameter validation 14 | /// using middleware. 15 | /// 16 | public class QueryValidationWithCustomResponse 17 | { 18 | private readonly IMiddlewarePipeline pipeline; 19 | 20 | /// 21 | /// Initializes a new instance of the class. 22 | /// 23 | /// The middleware pipeline factory. 24 | public QueryValidationWithCustomResponse(IMiddlewarePipelineFactory pipelineFactory) 25 | { 26 | // Note: You could simply new-up a MiddlewarePipeline instance here and build it, 27 | // but this example uses a pipeline factory so you can share pipelines between 28 | // Azure Functions that have common requirements, without having to duplicate 29 | // code. 30 | 31 | this.pipeline = pipelineFactory.CreateForQuery( 32 | this.ExecuteAsync, 33 | ResponseHelper.HandleValidationFailure); 34 | } 35 | 36 | /// 37 | /// The HTTP trigger entrypoint for the function. 38 | /// 39 | /// The HTTP request. 40 | /// The result. 41 | [FunctionName(nameof(QueryValidationWithCustomResponse))] 42 | public async Task Run( 43 | [HttpTrigger(AuthorizationLevel.Function, "get")] HttpRequest req) 44 | { 45 | // Note that we don't need to use HttpRequest parameter here as the 46 | // pipeline is using IHttpContextAccessor. 47 | return await this.pipeline.RunAsync(); 48 | } 49 | 50 | /// 51 | /// Executes the function's business logic. At this point, the query model 52 | /// has been validated correctly. 53 | /// 54 | /// The HTTP context for the request. 55 | /// The result. 56 | private Task ExecuteAsync(HttpContext context) 57 | { 58 | var payload = new 59 | { 60 | correlationId = context.TraceIdentifier, 61 | message = "OK", 62 | query = context.Items[ContextItems.Query] 63 | }; 64 | 65 | return Task.FromResult(new OkObjectResult(payload)); 66 | } 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /samples/ModelValidation/ModelValidation.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | netcoreapp3.1 4 | v3 5 | Samples.ModelValidation 6 | Samples.ModelValidation 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | PreserveNewest 18 | 19 | 20 | PreserveNewest 21 | Never 22 | 23 | 24 | -------------------------------------------------------------------------------- /samples/ModelValidation/Pipelines/IMiddlewarePipelineFactory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Microsoft.AspNetCore.Http; 4 | using Microsoft.AspNetCore.Mvc; 5 | using Umamimolecule.AzureFunctionsMiddleware; 6 | 7 | namespace Samples.ModelValidation.Pipelines 8 | { 9 | /// 10 | /// Represents a component to creates middleware pipeline instances used by this project. 11 | /// 12 | public interface IMiddlewarePipelineFactory 13 | { 14 | /// 15 | /// Creates a pipeline to validate query parameters. 16 | /// 17 | /// The object type representing the query parameters. 18 | /// The method containing the Azure Function business logic implementation. 19 | /// Optional handler to return a custom response when validation is unsuccessful. 20 | /// The middleware pipeline. 21 | IMiddlewarePipeline CreateForQuery( 22 | Func> func, 23 | Func handleValidationFailure = null) 24 | where TQuery : new(); 25 | 26 | /// 27 | /// Creates a pipeline to validate a body payload. 28 | /// 29 | /// The object type representing the body. 30 | /// The method containing the Azure Function business logic implementation. 31 | /// Optional handler to return a custom response when validation is unsuccessful. 32 | /// The middleware pipeline. 33 | IMiddlewarePipeline CreateForBody( 34 | Func> func, 35 | Func handleValidationFailure = null) 36 | where TBody : new(); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /samples/ModelValidation/Pipelines/MiddlewarePipelineFactory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Microsoft.AspNetCore.Http; 4 | using Microsoft.AspNetCore.Mvc; 5 | using Umamimolecule.AzureFunctionsMiddleware; 6 | 7 | namespace Samples.ModelValidation.Pipelines 8 | { 9 | /// 10 | /// A component to creates middleware pipeline instances used by this project. 11 | /// 12 | public class MiddlewarePipelineFactory : IMiddlewarePipelineFactory 13 | { 14 | private readonly IHttpContextAccessor httpContextAccessor; 15 | 16 | /// 17 | /// The request headers containing correlation identifiers. 18 | /// 19 | private readonly static string[] correlationIdHeaders = new string[] 20 | { 21 | "ms-request-id", 22 | "request-id" 23 | }; 24 | 25 | /// 26 | /// Initializes a new instance of the class. 27 | /// 28 | /// The HTTP context accessor. 29 | public MiddlewarePipelineFactory(IHttpContextAccessor httpContextAccessor) 30 | { 31 | this.httpContextAccessor = httpContextAccessor; 32 | } 33 | 34 | /// 35 | /// Creates a pipeline to validate query parameters. 36 | /// 37 | /// The object type representing the query parameters. 38 | /// The method containing the Azure Function business logic implementation. 39 | /// Optional handler to return a custom response when validation is unsuccessful. 40 | /// The middleware pipeline. 41 | public IMiddlewarePipeline CreateForQuery( 42 | Func> func, 43 | Func handleValidationFailure = null) 44 | where TQuery : new() 45 | { 46 | MiddlewarePipeline pipeline = new MiddlewarePipeline(this.httpContextAccessor); 47 | return pipeline.UseCorrelationId(correlationIdHeaders) 48 | .UseQueryValidation(handleValidationFailure) 49 | .Use(func); 50 | } 51 | 52 | /// 53 | /// Creates a pipeline to validate a body payload. 54 | /// 55 | /// The object type representing the body. 56 | /// The method containing the Azure Function business logic implementation. 57 | /// Optional handler to return a custom response when validation is unsuccessful. 58 | /// The middleware pipeline. 59 | public IMiddlewarePipeline CreateForBody( 60 | Func> func, 61 | Func handleValidationFailure = null) 62 | where TBody : new() 63 | { 64 | MiddlewarePipeline pipeline = new MiddlewarePipeline(this.httpContextAccessor); 65 | return pipeline.UseCorrelationId(correlationIdHeaders) 66 | .UseBodyValidation(handleValidationFailure) 67 | .Use(func); 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /samples/ModelValidation/README.md: -------------------------------------------------------------------------------- 1 | # Sample: Model validation 2 | 3 | This example project shows how to configure the built-in middleware to perform model validation for query parameters and body payload. 4 | 5 | It shows the default response sent when validation is unsuccessful, and also how to customise the response. 6 | 7 | ### Table of contents 8 | - [Body validation](#bodyvalidation) 9 | - [Default validation failure response](#defaultbodyvalidationresponse) 10 | - [Custom validation failure response](#custombodyvalidationresponse) 11 | - [Query validation](#queryvalidation) 12 | - [Default validation failure response](#defaultqueryvalidationresponse) 13 | - [Custom validation failure response](#customqueryvalidationresponse) 14 | 15 | 16 |
17 | 18 | ## Body validation 19 | 20 | 21 | 22 | ### Default validation failure response 23 | 24 | Submit a POST request with the following JSON payload to the endpoint `http://localhost:7071/api/BodyValidation`: 25 | 26 | ``` 27 | { 28 | "name": "A", 29 | "user": { 30 | "id": 1, 31 | "name": "Fred" 32 | } 33 | } 34 | ``` 35 | 36 | This will return 200 OK and send back the received body: 37 | 38 | ``` 39 | { 40 | "correlationId": "...", 41 | "message": "OK", 42 | "body": { 43 | "name": "Test", 44 | "user": { 45 | "id": 1, 46 | "name": "Fred", 47 | "description": null 48 | } 49 | } 50 | } 51 | ``` 52 | 53 | Now try the following payload: 54 | 55 | ``` 56 | { 57 | "name": null, 58 | "user": { 59 | "id": 1, 60 | "name": "Fred" 61 | } 62 | } 63 | ``` 64 | 65 | and observe it returns 400 Bad Request with a description of the error: 66 | 67 | ``` 68 | { 69 | "correlationId": "...", 70 | "error": { 71 | "code": "INVALID_BODY", 72 | "message": "The Name field is required." 73 | } 74 | } 75 | ``` 76 | 77 | This is the default validation failure response structure. 78 | 79 | Note that body validation can handle a complex nested object. Submit the following request: 80 | 81 | ``` 82 | { 83 | "name": "Test", 84 | "user": { 85 | "id": 1 86 | } 87 | } 88 | ``` 89 | 90 | and the response indicates a nested child property is invalid: 91 | 92 | ``` 93 | { 94 | "correlationId": "...", 95 | "error": { 96 | "code": "INVALID_BODY", 97 | "message": "User.Name: The Name field is required." 98 | } 99 | } 100 | ``` 101 | 102 | 103 | 104 | ### Custom validation failure response 105 | Custom responses can returned when body payload validation is unsuccessful by passing in a function to the `IMiddlewarePipeline.UseBodyValidation` extension method: 106 | 107 | ``` 108 | Func handleValidationFailure 109 | ``` 110 | 111 | This function takes in two arguments: 112 | - `HttpContext` - The HTTP context for the current request 113 | - `ModelValidationResult` - The result of the validation 114 | 115 | and returns an `IActionResult` containing the response to be returned to the caller. 116 | 117 | To see this in action in this sample, Submit a POST request with the following JSON payload to the endpoint `http://localhost:7071/api/BodyValidationWithCustomResponse`: 118 | 119 | 120 | ``` 121 | { 122 | "name": null, 123 | "user": { 124 | "id": 1, 125 | "name": "Fred" 126 | } 127 | } 128 | ``` 129 | 130 | and observe it returns 400 Bad Request with a description of the error: 131 | 132 | ``` 133 | { 134 | "customResponse": { 135 | "errorMessage": "Name: The Name field is required." 136 | } 137 | } 138 | ``` 139 | 140 | 141 | 142 | ## Query validation 143 | 144 | 145 | 146 | ### Default validation failure response 147 | 148 | Submit a GET request to `http://localhost:7071/api/QueryValidation?name=Fred`: 149 | 150 | This will return 200 OK and send back the received body: 151 | 152 | ``` 153 | { 154 | "correlationId": "...", 155 | "message": "OK", 156 | "query": { 157 | "name": "Fred", 158 | "description": null 159 | } 160 | } 161 | ``` 162 | 163 | Now try the following request `http://localhost:7071/api/QueryValidation?name2=Fred` 164 | 165 | and observe it returns 400 Bad Request with a description of the error: 166 | 167 | ``` 168 | { 169 | "correlationId": "...", 170 | "error": { 171 | "code": "INVALID_QUERY_PARAMETERS", 172 | "message": "The Name field is required." 173 | } 174 | } 175 | ``` 176 | 177 | 178 | 179 | ### Custom validation failure response 180 | Custom responses can returned when query parameter validation is unsuccessful by by passing in a function to the `IMiddlewarePipeline.UseQueryValidation` extension method: 181 | 182 | ``` 183 | Func handleValidationFailure 184 | ``` 185 | 186 | This function takes in two arguments: 187 | - `HttpContext` - The HTTP context for the current request 188 | - `ModelValidationResult` - The result of the validation 189 | 190 | and returns an `IActionResult` containing the response to be returned to the caller. 191 | 192 | To see this in action in this sample, Submit a GET request with the following JSON payload to the endpoint `http://localhost:7071/api/QueryValidationWithCustomResponse?name2=Fred` 193 | 194 | 195 | and observe it returns 400 Bad Request with a description of the error: 196 | 197 | ``` 198 | { 199 | "customResponse": { 200 | "errorMessage": "Name: The Name field is required." 201 | } 202 | } 203 | ``` -------------------------------------------------------------------------------- /samples/ModelValidation/Responses/ResponseHelper.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Http; 2 | using Microsoft.AspNetCore.Mvc; 3 | using Umamimolecule.AzureFunctionsMiddleware; 4 | 5 | namespace Samples.ModelValidation.Responses 6 | { 7 | /// 8 | /// Contains utility methods related to responses. 9 | /// 10 | public static class ResponseHelper 11 | { 12 | /// 13 | /// Example of a custom response handler for valiation failures. 14 | /// 15 | /// 16 | /// 17 | /// 18 | public static IActionResult HandleValidationFailure( 19 | HttpContext _, 20 | ModelValidationResult validationResult) 21 | { 22 | var response = new 23 | { 24 | customResponse = new 25 | { 26 | errorMessage = validationResult.Error 27 | } 28 | }; 29 | 30 | return new BadRequestObjectResult(response); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /samples/ModelValidation/Startup.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Azure.Functions.Extensions.DependencyInjection; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using Samples.ModelValidation.Pipelines; 4 | 5 | [assembly: FunctionsStartup(typeof(Samples.ModelValidation.Startup))] 6 | 7 | namespace Samples.ModelValidation 8 | { 9 | public class Startup : FunctionsStartup 10 | { 11 | public override void Configure(IFunctionsHostBuilder builder) 12 | { 13 | builder.Services.AddHttpContextAccessor(); 14 | builder.Services.AddTransient(); 15 | } 16 | } 17 | } -------------------------------------------------------------------------------- /samples/ModelValidation/host.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0" 3 | } -------------------------------------------------------------------------------- /samples/PipelineBranching/.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # Azure Functions localsettings file 5 | local.settings.json 6 | 7 | # User-specific files 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Build results 17 | [Dd]ebug/ 18 | [Dd]ebugPublic/ 19 | [Rr]elease/ 20 | [Rr]eleases/ 21 | x64/ 22 | x86/ 23 | bld/ 24 | [Bb]in/ 25 | [Oo]bj/ 26 | [Ll]og/ 27 | 28 | # Visual Studio 2015 cache/options directory 29 | .vs/ 30 | # Uncomment if you have tasks that create the project's static files in wwwroot 31 | #wwwroot/ 32 | 33 | # MSTest test Results 34 | [Tt]est[Rr]esult*/ 35 | [Bb]uild[Ll]og.* 36 | 37 | # NUNIT 38 | *.VisualState.xml 39 | TestResult.xml 40 | 41 | # Build Results of an ATL Project 42 | [Dd]ebugPS/ 43 | [Rr]eleasePS/ 44 | dlldata.c 45 | 46 | # DNX 47 | project.lock.json 48 | project.fragment.lock.json 49 | artifacts/ 50 | 51 | *_i.c 52 | *_p.c 53 | *_i.h 54 | *.ilk 55 | *.meta 56 | *.obj 57 | *.pch 58 | *.pdb 59 | *.pgc 60 | *.pgd 61 | *.rsp 62 | *.sbr 63 | *.tlb 64 | *.tli 65 | *.tlh 66 | *.tmp 67 | *.tmp_proj 68 | *.log 69 | *.vspscc 70 | *.vssscc 71 | .builds 72 | *.pidb 73 | *.svclog 74 | *.scc 75 | 76 | # Chutzpah Test files 77 | _Chutzpah* 78 | 79 | # Visual C++ cache files 80 | ipch/ 81 | *.aps 82 | *.ncb 83 | *.opendb 84 | *.opensdf 85 | *.sdf 86 | *.cachefile 87 | *.VC.db 88 | *.VC.VC.opendb 89 | 90 | # Visual Studio profiler 91 | *.psess 92 | *.vsp 93 | *.vspx 94 | *.sap 95 | 96 | # TFS 2012 Local Workspace 97 | $tf/ 98 | 99 | # Guidance Automation Toolkit 100 | *.gpState 101 | 102 | # ReSharper is a .NET coding add-in 103 | _ReSharper*/ 104 | *.[Rr]e[Ss]harper 105 | *.DotSettings.user 106 | 107 | # JustCode is a .NET coding add-in 108 | .JustCode 109 | 110 | # TeamCity is a build add-in 111 | _TeamCity* 112 | 113 | # DotCover is a Code Coverage Tool 114 | *.dotCover 115 | 116 | # NCrunch 117 | _NCrunch_* 118 | .*crunch*.local.xml 119 | nCrunchTemp_* 120 | 121 | # MightyMoose 122 | *.mm.* 123 | AutoTest.Net/ 124 | 125 | # Web workbench (sass) 126 | .sass-cache/ 127 | 128 | # Installshield output folder 129 | [Ee]xpress/ 130 | 131 | # DocProject is a documentation generator add-in 132 | DocProject/buildhelp/ 133 | DocProject/Help/*.HxT 134 | DocProject/Help/*.HxC 135 | DocProject/Help/*.hhc 136 | DocProject/Help/*.hhk 137 | DocProject/Help/*.hhp 138 | DocProject/Help/Html2 139 | DocProject/Help/html 140 | 141 | # Click-Once directory 142 | publish/ 143 | 144 | # Publish Web Output 145 | *.[Pp]ublish.xml 146 | *.azurePubxml 147 | # TODO: Comment the next line if you want to checkin your web deploy settings 148 | # but database connection strings (with potential passwords) will be unencrypted 149 | #*.pubxml 150 | *.publishproj 151 | 152 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 153 | # checkin your Azure Web App publish settings, but sensitive information contained 154 | # in these scripts will be unencrypted 155 | PublishScripts/ 156 | 157 | # NuGet Packages 158 | *.nupkg 159 | # The packages folder can be ignored because of Package Restore 160 | **/packages/* 161 | # except build/, which is used as an MSBuild target. 162 | !**/packages/build/ 163 | # Uncomment if necessary however generally it will be regenerated when needed 164 | #!**/packages/repositories.config 165 | # NuGet v3's project.json files produces more ignoreable files 166 | *.nuget.props 167 | *.nuget.targets 168 | 169 | # Microsoft Azure Build Output 170 | csx/ 171 | *.build.csdef 172 | 173 | # Microsoft Azure Emulator 174 | ecf/ 175 | rcf/ 176 | 177 | # Windows Store app package directories and files 178 | AppPackages/ 179 | BundleArtifacts/ 180 | Package.StoreAssociation.xml 181 | _pkginfo.txt 182 | 183 | # Visual Studio cache files 184 | # files ending in .cache can be ignored 185 | *.[Cc]ache 186 | # but keep track of directories ending in .cache 187 | !*.[Cc]ache/ 188 | 189 | # Others 190 | ClientBin/ 191 | ~$* 192 | *~ 193 | *.dbmdl 194 | *.dbproj.schemaview 195 | *.jfm 196 | *.pfx 197 | *.publishsettings 198 | node_modules/ 199 | orleans.codegen.cs 200 | 201 | # Since there are multiple workflows, uncomment next line to ignore bower_components 202 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 203 | #bower_components/ 204 | 205 | # RIA/Silverlight projects 206 | Generated_Code/ 207 | 208 | # Backup & report files from converting an old project file 209 | # to a newer Visual Studio version. Backup files are not needed, 210 | # because we have git ;-) 211 | _UpgradeReport_Files/ 212 | Backup*/ 213 | UpgradeLog*.XML 214 | UpgradeLog*.htm 215 | 216 | # SQL Server files 217 | *.mdf 218 | *.ldf 219 | 220 | # Business Intelligence projects 221 | *.rdl.data 222 | *.bim.layout 223 | *.bim_*.settings 224 | 225 | # Microsoft Fakes 226 | FakesAssemblies/ 227 | 228 | # GhostDoc plugin setting file 229 | *.GhostDoc.xml 230 | 231 | # Node.js Tools for Visual Studio 232 | .ntvs_analysis.dat 233 | 234 | # Visual Studio 6 build log 235 | *.plg 236 | 237 | # Visual Studio 6 workspace options file 238 | *.opt 239 | 240 | # Visual Studio LightSwitch build output 241 | **/*.HTMLClient/GeneratedArtifacts 242 | **/*.DesktopClient/GeneratedArtifacts 243 | **/*.DesktopClient/ModelManifest.xml 244 | **/*.Server/GeneratedArtifacts 245 | **/*.Server/ModelManifest.xml 246 | _Pvt_Extensions 247 | 248 | # Paket dependency manager 249 | .paket/paket.exe 250 | paket-files/ 251 | 252 | # FAKE - F# Make 253 | .fake/ 254 | 255 | # JetBrains Rider 256 | .idea/ 257 | *.sln.iml 258 | 259 | # CodeRush 260 | .cr/ 261 | 262 | # Python Tools for Visual Studio (PTVS) 263 | __pycache__/ 264 | *.pyc -------------------------------------------------------------------------------- /samples/PipelineBranching/Functions/Functions.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Microsoft.AspNetCore.Http; 3 | using Microsoft.AspNetCore.Mvc; 4 | using Microsoft.Azure.WebJobs; 5 | using Microsoft.Azure.WebJobs.Extensions.Http; 6 | using Samples.PipelineBranching.Pipelines; 7 | 8 | namespace Samples.PipelineBranching.Functions 9 | { 10 | /// 11 | /// An HTTP-triggered Azure Function to demonstrate pipeline branching. 12 | /// 13 | public class Functions 14 | { 15 | private readonly IMiddlewarePipelineFactory pipelineFactory; 16 | 17 | /// 18 | /// Initializes a new instance of the class. 19 | /// 20 | /// The middleware pipeline factory. 21 | public Functions(IMiddlewarePipelineFactory pipelineFactory) 22 | { 23 | this.pipelineFactory = pipelineFactory; 24 | } 25 | 26 | /// 27 | /// The HTTP trigger entrypoint for the first function. 28 | /// 29 | /// The HTTP request. 30 | /// 31 | [FunctionName("Function1")] 32 | public async Task Function1( 33 | [HttpTrigger(AuthorizationLevel.Function, "get")] HttpRequest req) 34 | { 35 | var pipeline = this.pipelineFactory.Create(this.ExecuteFunction1Async); 36 | return await pipeline.RunAsync(); 37 | } 38 | 39 | /// 40 | /// The HTTP trigger entrypoint for the second function. 41 | /// 42 | /// The HTTP request. 43 | /// 44 | [FunctionName("Function2")] 45 | public async Task Function2( 46 | [HttpTrigger(AuthorizationLevel.Function, "get")] HttpRequest req) 47 | { 48 | var pipeline = this.pipelineFactory.Create(this.ExecuteFunction2Async); 49 | return await pipeline.RunAsync(); 50 | } 51 | 52 | /// 53 | /// Executes the function 1's business logic. 54 | /// 55 | /// The HTTP context for the request. 56 | /// The result. 57 | private Task ExecuteFunction1Async(HttpContext context) 58 | { 59 | var payload = new 60 | { 61 | message = "OK", 62 | functionName = "Function1" 63 | }; 64 | 65 | return Task.FromResult(new OkObjectResult(payload)); 66 | } 67 | 68 | /// 69 | /// Executes the function 2's business logic. 70 | /// 71 | /// The HTTP context for the request. 72 | /// The result. 73 | private Task ExecuteFunction2Async(HttpContext context) 74 | { 75 | var payload = new 76 | { 77 | message = "OK", 78 | functionName = "Function2" 79 | }; 80 | 81 | return Task.FromResult(new OkObjectResult(payload)); 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /samples/PipelineBranching/PipelineBranching.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | netcoreapp3.1 4 | v3 5 | Samples.PipelineBranching 6 | Samples.PipelineBranching 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | PreserveNewest 18 | 19 | 20 | PreserveNewest 21 | Never 22 | 23 | 24 | -------------------------------------------------------------------------------- /samples/PipelineBranching/Pipelines/IMiddlewarePipelineFactory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Microsoft.AspNetCore.Http; 4 | using Microsoft.AspNetCore.Mvc; 5 | using Umamimolecule.AzureFunctionsMiddleware; 6 | 7 | namespace Samples.PipelineBranching.Pipelines 8 | { 9 | /// 10 | /// Represents a component to creates middleware pipeline instances used by this project. 11 | /// 12 | public interface IMiddlewarePipelineFactory 13 | { 14 | /// 15 | /// Creates a pipeline. 16 | /// 17 | /// The method containing the Azure Function business logic implementation. 18 | /// The middleware pipeline. 19 | IMiddlewarePipeline Create(Func> func); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /samples/PipelineBranching/Pipelines/MiddlewareA.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Microsoft.AspNetCore.Http; 3 | using Umamimolecule.AzureFunctionsMiddleware; 4 | 5 | namespace Samples.PipelineBranching.Pipelines 6 | { 7 | public class MiddlewareA : HttpMiddleware 8 | { 9 | public override async Task InvokeAsync(HttpContext context) 10 | { 11 | context.Response.Headers["x-middleware-a"] = "Hello from middleware A"; 12 | 13 | if (this.Next != null) 14 | { 15 | await this.Next.InvokeAsync(context); 16 | } 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /samples/PipelineBranching/Pipelines/MiddlewareB.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Microsoft.AspNetCore.Http; 3 | using Umamimolecule.AzureFunctionsMiddleware; 4 | 5 | namespace Samples.PipelineBranching.Pipelines 6 | { 7 | public class MiddlewareB : HttpMiddleware 8 | { 9 | public override async Task InvokeAsync(HttpContext context) 10 | { 11 | context.Response.Headers["x-middleware-b"] = "Hello from middleware B"; 12 | 13 | if (this.Next != null) 14 | { 15 | await this.Next.InvokeAsync(context); 16 | } 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /samples/PipelineBranching/Pipelines/MiddlewarePipelineFactory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Microsoft.AspNetCore.Http; 4 | using Microsoft.AspNetCore.Mvc; 5 | using Umamimolecule.AzureFunctionsMiddleware; 6 | 7 | namespace Samples.PipelineBranching.Pipelines 8 | { 9 | /// 10 | /// A component to creates middleware pipeline instances used by this project. 11 | /// 12 | public class MiddlewarePipelineFactory : IMiddlewarePipelineFactory 13 | { 14 | private readonly IHttpContextAccessor httpContextAccessor; 15 | private readonly MiddlewareA middlewareA; 16 | private readonly MiddlewareB middlewareB; 17 | 18 | /// 19 | /// Initializes a new instance of the class. 20 | /// 21 | /// The HTTP context accessor. 22 | /// Middleware A 23 | /// Middleware B 24 | public MiddlewarePipelineFactory( 25 | IHttpContextAccessor httpContextAccessor, 26 | MiddlewareA middlewareA, 27 | MiddlewareB middlewareB) 28 | { 29 | this.httpContextAccessor = httpContextAccessor; 30 | this.middlewareA = middlewareA; 31 | this.middlewareB = middlewareB; 32 | } 33 | 34 | /// 35 | /// Creates a pipeline to validate query parameters. 36 | /// 37 | /// The object type representing the query parameters. 38 | /// The method containing the Azure Function business logic implementation. 39 | /// The middleware pipeline. 40 | public IMiddlewarePipeline Create(Func> func) 41 | { 42 | MiddlewarePipeline pipeline = new MiddlewarePipeline(this.httpContextAccessor); 43 | 44 | // If Function1 is called, then use MiddlewareA, else use MiddlewareB 45 | return pipeline.MapWhen(ctx => ctx.Request.Path.StartsWithSegments("/api/Function1"), 46 | p => p.Use(middlewareA) 47 | .Use(func)) 48 | .Use(middlewareB) 49 | .Use(func); 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /samples/PipelineBranching/README.md: -------------------------------------------------------------------------------- 1 | # Sample: Pipeline branching 2 | 3 | This example project shows how to conditionally branch a pipeline using the `MapWhen` extension method for a pipeline. 4 | 5 | ## PipelineBranching 6 | 7 | This project contains two functions: `http://localhost:7071/api/Function1` and `http://localhost:7071/api/Function2`. 8 | 9 | Each function will execute a different pipeline branch, which sets a response header `x-middleware-a` or `x-middleware-b`. 10 | 11 | The pipeline is set up using the following code, which inspects the inbound request to decide which middleware is invoked: 12 | 13 | ``` 14 | pipeline.MapWhen(ctx => ctx.Request.Path.StartsWithSegments("/api/Function1"), 15 | p => p.Use(middlewareA) 16 | .Use(func)); 17 | .Use(middlewareB)) 18 | .Use(func); 19 | ``` 20 | -------------------------------------------------------------------------------- /samples/PipelineBranching/Startup.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Azure.Functions.Extensions.DependencyInjection; 2 | using Microsoft.Extensions.DependencyInjection; 3 | using Samples.PipelineBranching.Pipelines; 4 | 5 | [assembly: FunctionsStartup(typeof(Samples.PipelineBranching.Startup))] 6 | 7 | namespace Samples.PipelineBranching 8 | { 9 | public class Startup : FunctionsStartup 10 | { 11 | public override void Configure(IFunctionsHostBuilder builder) 12 | { 13 | builder.Services.AddHttpContextAccessor(); 14 | builder.Services.AddTransient(); 15 | builder.Services.AddTransient(); 16 | builder.Services.AddTransient(); 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /samples/PipelineBranching/host.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0" 3 | } -------------------------------------------------------------------------------- /src/Umamimolecule.AzureFunctionsMiddleware/BadRequestException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Umamimolecule.AzureFunctionsMiddleware 4 | { 5 | /// 6 | /// Exception that is thrown within validators. 7 | /// 8 | [Serializable] 9 | public class BadRequestException : Exception 10 | { 11 | /// 12 | /// Initializes a new instance of the class. 13 | /// 14 | /// The message describing the error. 15 | public BadRequestException(string message) 16 | : base(message) 17 | { 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/Umamimolecule.AzureFunctionsMiddleware/BodyModelValidationMiddleware.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.DataAnnotations; 4 | using System.IO; 5 | using System.Linq; 6 | using System.Threading.Tasks; 7 | using Microsoft.AspNetCore.Http; 8 | using Newtonsoft.Json; 9 | 10 | namespace Umamimolecule.AzureFunctionsMiddleware 11 | { 12 | /// 13 | /// Middleware to perform validation body payload. 14 | /// 15 | /// The body payload type. 16 | public class BodyModelValidationMiddleware : ValidationMiddleware 17 | where T : new() 18 | { 19 | /// 20 | /// Gets the error code to use when validation fails. 21 | /// 22 | public override string ErrorCode => ErrorCodes.InvalidBody; 23 | 24 | /// 25 | /// Validates the body payload. 26 | /// 27 | /// The context. 28 | /// The validation results. 29 | protected override async Task ValidateAsync(HttpContext context) 30 | { 31 | (var model, var error) = await this.CreateModelAsync(context); 32 | if (!string.IsNullOrEmpty(error)) 33 | { 34 | return new ModelValidationResult() 35 | { 36 | Success = false, 37 | Error = error, 38 | }; 39 | } 40 | 41 | List validationResults = new List(); 42 | 43 | if (!RecursiveValidator.TryValidateObject(model, validationResults, true)) 44 | { 45 | return new ModelValidationResult() 46 | { 47 | Success = false, 48 | Error = string.Join(", ", validationResults.Select(x => string.Join(", ", x.MemberNames) + ": " + x.ErrorMessage)), 49 | }; 50 | } 51 | 52 | context.Items[ContextItems.Body] = model; 53 | 54 | return new ModelValidationResult() 55 | { 56 | Success = true, 57 | }; 58 | } 59 | 60 | private async Task<(T model, string error)> CreateModelAsync(HttpContext context) 61 | { 62 | if (context.Request.Body != null) 63 | { 64 | context.Request.EnableBuffering(); 65 | #pragma warning disable IDE0067 // Dispose objects before losing scope 66 | var reader = new StreamReader(context.Request.Body); 67 | #pragma warning restore IDE0067 // Dispose objects before losing scope 68 | var json = await reader.ReadToEndAsync(); 69 | if (context.Request.Body.CanSeek) 70 | { 71 | context.Request.Body.Seek(0, SeekOrigin.Begin); 72 | } 73 | 74 | try 75 | { 76 | var model = JsonConvert.DeserializeObject(json); 77 | if (model != null) 78 | { 79 | return (model, null); 80 | } 81 | } 82 | catch (Exception e) 83 | { 84 | return (default(T), e.Message); 85 | } 86 | } 87 | 88 | return (new T(), null); 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /src/Umamimolecule.AzureFunctionsMiddleware/ConditionalMiddleware.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Microsoft.AspNetCore.Http; 4 | 5 | namespace Umamimolecule.AzureFunctionsMiddleware 6 | { 7 | /// 8 | /// Middleware to conditionally execute a branch. 9 | /// 10 | public class ConditionalMiddleware : HttpMiddleware 11 | { 12 | private readonly IMiddlewarePipeline pipeline; 13 | private readonly Action configure; 14 | private readonly Func condition; 15 | private readonly bool rejoinPipeline; 16 | 17 | /// 18 | /// Initializes a new instance of the class. 19 | /// 20 | /// The pipeline. Required. 21 | /// The condition to evaluate. Required. 22 | /// Configures the branch. Optional. 23 | /// Determines if the branch should rejoin the main pipeline or not. 24 | public ConditionalMiddleware( 25 | IMiddlewarePipeline pipeline, 26 | Func condition, 27 | Action configure, 28 | bool rejoinPipeline) 29 | { 30 | GuardClauses.IsNotNull(nameof(pipeline), pipeline); 31 | GuardClauses.IsNotNull(nameof(condition), condition); 32 | 33 | this.pipeline = pipeline; 34 | this.configure = configure; 35 | this.condition = condition; 36 | this.rejoinPipeline = rejoinPipeline; 37 | } 38 | 39 | /// 40 | /// Runs the middleware. 41 | /// 42 | /// The context. 43 | /// A task representing the asynchronous operation. 44 | public override async Task InvokeAsync(HttpContext context) 45 | { 46 | if (this.condition(context)) 47 | { 48 | // Create new pipeline for branch 49 | var branch = this.pipeline.New(); 50 | this.configure?.Invoke(branch); 51 | await branch.RunAsync(); 52 | 53 | if (!this.rejoinPipeline) 54 | { 55 | return; 56 | } 57 | } 58 | 59 | if (this.Next != null) 60 | { 61 | await this.Next.InvokeAsync(context); 62 | } 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/Umamimolecule.AzureFunctionsMiddleware/ContentTypes.cs: -------------------------------------------------------------------------------- 1 | namespace Umamimolecule.AzureFunctionsMiddleware 2 | { 3 | /// 4 | /// Contains constants for known content types. 5 | /// 6 | internal static class ContentTypes 7 | { 8 | /// 9 | /// The content type value for application/json. 10 | /// 11 | public const string ApplicationJson = "application/json"; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Umamimolecule.AzureFunctionsMiddleware/ContextItems.cs: -------------------------------------------------------------------------------- 1 | namespace Umamimolecule.AzureFunctionsMiddleware 2 | { 3 | /// 4 | /// Contains keys for HTTP context items. 5 | /// 6 | public static class ContextItems 7 | { 8 | /// 9 | /// The key for the query parameter model. 10 | /// 11 | public const string Query = "Query"; 12 | 13 | /// 14 | /// The key for the body payload model. 15 | /// 16 | public const string Body = "Body"; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Umamimolecule.AzureFunctionsMiddleware/CorrelationIdMiddleware.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading.Tasks; 4 | using Microsoft.AspNetCore.Http; 5 | using Microsoft.Extensions.Primitives; 6 | 7 | namespace Umamimolecule.AzureFunctionsMiddleware 8 | { 9 | /// 10 | /// Middleware to extract correlation identifier from a request header. 11 | /// 12 | public class CorrelationIdMiddleware : HttpMiddleware 13 | { 14 | private readonly IEnumerable correlationIdHeaders; 15 | 16 | /// 17 | /// Initializes a new instance of the class. 18 | /// 19 | /// The collection of headers which will be inspected, in order. The first matching header found will be used for the correlation ID. 20 | public CorrelationIdMiddleware(IEnumerable correlationIdHeaders) 21 | { 22 | this.correlationIdHeaders = correlationIdHeaders; 23 | } 24 | 25 | /// 26 | /// Runs the middleware. 27 | /// 28 | /// The context. 29 | /// A task representing the asynchronous operation. 30 | public override async Task InvokeAsync(HttpContext context) 31 | { 32 | var correlationId = this.GetCorrelationId(context.Request); 33 | context.TraceIdentifier = correlationId; 34 | 35 | if (this.Next != null) 36 | { 37 | await this.Next.InvokeAsync(context); 38 | } 39 | } 40 | 41 | private string GetCorrelationId(HttpRequest request) 42 | { 43 | foreach (var correlationIdHeader in this.correlationIdHeaders) 44 | { 45 | if (request.Headers.TryGetValue(correlationIdHeader, out StringValues value) && 46 | !StringValues.IsNullOrEmpty(value)) 47 | { 48 | return value.ToString(); 49 | } 50 | } 51 | 52 | return Guid.NewGuid().ToString(); 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/Umamimolecule.AzureFunctionsMiddleware/ErrorCodes.cs: -------------------------------------------------------------------------------- 1 | namespace Umamimolecule.AzureFunctionsMiddleware 2 | { 3 | /// 4 | /// Contains error codes. 5 | /// 6 | internal static class ErrorCodes 7 | { 8 | /// 9 | /// The error code for invalid query parameters. 10 | /// 11 | public const string InvalidQueryParameters = "INVALID_QUERY_PARAMETERS"; 12 | 13 | /// 14 | /// The error code for invalid body payload. 15 | /// 16 | public const string InvalidBody = "INVALID_BODY"; 17 | 18 | /// 19 | /// The error code for bad request. 20 | /// 21 | public const string BadRequest = "BAD_REQUEST"; 22 | 23 | /// 24 | /// The error code for internal server error. 25 | /// 26 | public const string InternalServerError = "INTERNAL_SERVER_ERROR"; 27 | 28 | /// 29 | /// The error code for unauthorized. 30 | /// 31 | public const string Unauthorized = "UNAUTHORIZED"; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/Umamimolecule.AzureFunctionsMiddleware/ExceptionHandlerMiddleware.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net; 3 | using System.Threading.Tasks; 4 | using Microsoft.AspNetCore.Http; 5 | using Microsoft.AspNetCore.Mvc; 6 | using Newtonsoft.Json; 7 | 8 | namespace Umamimolecule.AzureFunctionsMiddleware 9 | { 10 | /// 11 | /// Exception handling middleware. 12 | /// 13 | public class ExceptionHandlerMiddleware : HttpMiddleware 14 | { 15 | /// 16 | /// Gets a default exception handler to provide basic support for model validation failure and unexpected exceptions. 17 | /// 18 | /// The response to return from the Azure function. 19 | public static Func> DefaultExceptionHandler 20 | { 21 | get 22 | { 23 | return (Exception exception, HttpContext context) => 24 | { 25 | IActionResult result; 26 | 27 | if (exception is BadRequestException) 28 | { 29 | var response = new 30 | { 31 | correlationId = context.TraceIdentifier, 32 | error = new 33 | { 34 | code = ErrorCodes.BadRequest, 35 | message = exception.Message, 36 | }, 37 | }; 38 | 39 | result = new BadRequestObjectResult(response); 40 | } 41 | else 42 | { 43 | var response = new 44 | { 45 | correlationId = context.TraceIdentifier, 46 | error = new 47 | { 48 | code = ErrorCodes.InternalServerError, 49 | message = "An unexpected error occurred in the application", 50 | }, 51 | }; 52 | 53 | result = new ObjectResult(response) 54 | { 55 | StatusCode = (int)HttpStatusCode.InternalServerError, 56 | }; 57 | } 58 | 59 | return Task.FromResult(result); 60 | }; 61 | } 62 | } 63 | 64 | /// 65 | /// Gets or sets the function to determine what response should be returned by the exception handler. 66 | /// 67 | public Func> ExceptionHandler { get; set; } 68 | 69 | /// 70 | /// Gets or sets the handler function to log exceptions. Optional. 71 | /// 72 | public Func LogExceptionAsync { get; set; } 73 | 74 | /// 75 | /// Runs the middleware. 76 | /// 77 | /// The context. 78 | /// A task representing the asynchronous operation. 79 | public override async Task InvokeAsync(HttpContext context) 80 | { 81 | if (this.Next == null) 82 | { 83 | throw new MiddlewarePipelineException($"{this.GetType().FullName} must have a Next middleware"); 84 | } 85 | 86 | try 87 | { 88 | await this.Next.InvokeAsync(context); 89 | } 90 | catch (Exception e) 91 | { 92 | if (this.LogExceptionAsync != null) 93 | { 94 | await this.LogExceptionAsync(e); 95 | } 96 | 97 | if (this.ExceptionHandler == null) 98 | { 99 | context.Response.StatusCode = (int)HttpStatusCode.InternalServerError; 100 | context.Response.ContentType = ContentTypes.ApplicationJson; 101 | var content = new 102 | { 103 | error = new 104 | { 105 | code = ErrorCodes.InternalServerError, 106 | message = "An internal server error occurred", 107 | }, 108 | correlationId = context.TraceIdentifier, 109 | }; 110 | 111 | await HttpResponseWritingExtensions.WriteAsync(context.Response, JsonConvert.SerializeObject(content)); 112 | } 113 | else 114 | { 115 | var result = await this.ExceptionHandler(e, context); 116 | await context.ProcessActionResultAsync(result); 117 | } 118 | } 119 | } 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /src/Umamimolecule.AzureFunctionsMiddleware/FunctionMiddleware.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Microsoft.AspNetCore.Http; 4 | using Microsoft.AspNetCore.Mvc; 5 | 6 | namespace Umamimolecule.AzureFunctionsMiddleware 7 | { 8 | /// 9 | /// Middleware to allow an asynchronous operation to be executed. 10 | /// 11 | public class FunctionMiddleware : HttpMiddleware 12 | { 13 | private readonly Func> func; 14 | 15 | /// 16 | /// Initializes a new instance of the class. 17 | /// 18 | /// The task to be executed. 19 | public FunctionMiddleware(Func> func) 20 | { 21 | GuardClauses.IsNotNull(nameof(func), func); 22 | this.func = func; 23 | } 24 | 25 | /// 26 | /// Runs the middleware. 27 | /// 28 | /// The context. 29 | /// A task representing the asynchronous operation. 30 | public override async Task InvokeAsync(HttpContext context) 31 | { 32 | var result = await this.func(context); 33 | await context.ProcessActionResultAsync(result); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Umamimolecule.AzureFunctionsMiddleware/GuardClauses.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Umamimolecule.AzureFunctionsMiddleware 4 | { 5 | /// 6 | /// Contains methods to validate parameter values. 7 | /// 8 | internal static class GuardClauses 9 | { 10 | /// 11 | /// Validates a parameter value is not null. 12 | /// 13 | /// The parameter name. 14 | /// The parameter value. 15 | public static void IsNotNull(string paramName, object value) 16 | { 17 | if (value == null) 18 | { 19 | throw new ArgumentNullException(paramName); 20 | } 21 | } 22 | 23 | /// 24 | /// Validates a parameter value is not null or whitespace. 25 | /// 26 | /// The parameter name. 27 | /// The parameter value. 28 | public static void IsNotNullOrWhitespace(string paramName, string value) 29 | { 30 | if (string.IsNullOrWhiteSpace(value)) 31 | { 32 | throw new ArgumentException($"The parameter {paramName} must not be null or whitespace.", paramName); 33 | } 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Umamimolecule.AzureFunctionsMiddleware/Headers.cs: -------------------------------------------------------------------------------- 1 | namespace Umamimolecule.AzureFunctionsMiddleware 2 | { 3 | /// 4 | /// Contains header names. 5 | /// 6 | internal static class Headers 7 | { 8 | /// 9 | /// The Content-Type header name. 10 | /// 11 | public const string ContentType = "Content-Type"; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/Umamimolecule.AzureFunctionsMiddleware/HttpContextExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Microsoft.AspNetCore.Http; 3 | using Microsoft.AspNetCore.Mvc; 4 | 5 | namespace Umamimolecule.AzureFunctionsMiddleware 6 | { 7 | /// 8 | /// Contains extension methods for instances. 9 | /// 10 | public static class HttpContextExtensions 11 | { 12 | /// 13 | /// Processes an action result and applies to the instance. 14 | /// 15 | /// The HTTP context. 16 | /// The aciton result to process. 17 | /// A task representing the asynchronous operation. 18 | public static Task ProcessActionResultAsync(this HttpContext context, IActionResult result) 19 | { 20 | return result.ExecuteResultAsync(new ActionContext(context, new Microsoft.AspNetCore.Routing.RouteData(), new Microsoft.AspNetCore.Mvc.Abstractions.ActionDescriptor())); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Umamimolecule.AzureFunctionsMiddleware/HttpMiddleware.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Microsoft.AspNetCore.Http; 3 | 4 | namespace Umamimolecule.AzureFunctionsMiddleware 5 | { 6 | /// 7 | /// Base class for all middleware. 8 | /// 9 | public abstract class HttpMiddleware : IHttpMiddleware 10 | { 11 | /// 12 | /// Initializes a new instance of the class. 13 | /// 14 | protected HttpMiddleware() 15 | { 16 | } 17 | 18 | /// 19 | /// Initializes a new instance of the class. 20 | /// 21 | /// The next middleware to be run. 22 | protected HttpMiddleware(IHttpMiddleware next) 23 | { 24 | this.Next = next; 25 | } 26 | 27 | /// 28 | /// Gets or sets the next middleware to be executed after this one. 29 | /// 30 | public IHttpMiddleware Next { get; set; } 31 | 32 | /// 33 | /// Runs the middleware. 34 | /// 35 | /// The context. 36 | /// A task representing the asynchronous operation. 37 | public abstract Task InvokeAsync(HttpContext context); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/Umamimolecule.AzureFunctionsMiddleware/HttpResponseResult.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Microsoft.AspNetCore.Http; 3 | using Microsoft.AspNetCore.Mvc; 4 | 5 | namespace Umamimolecule.AzureFunctionsMiddleware 6 | { 7 | /// 8 | /// An implementation to convert HTTP context. 9 | /// 10 | public class HttpResponseResult : IActionResult 11 | { 12 | private readonly HttpContext context; 13 | 14 | /// 15 | /// Initializes a new instance of the class. 16 | /// 17 | /// The HTTP context. 18 | public HttpResponseResult(HttpContext context) 19 | { 20 | this.context = context; 21 | } 22 | 23 | /// 24 | /// Gets the HTTP context. 25 | /// 26 | public HttpContext Context => this.context; 27 | 28 | /// 29 | /// Executes the result operation of the action method asynchronously. 30 | /// 31 | /// 32 | /// The context in which the result is executed. The context information 33 | /// includes information about the action that was executed and request 34 | /// information. 35 | /// 36 | /// A task representing the asynchronous operation. 37 | public Task ExecuteResultAsync(ActionContext context) 38 | { 39 | context.HttpContext = this.context; 40 | return Task.CompletedTask; 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/Umamimolecule.AzureFunctionsMiddleware/IHttpMiddleware.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Microsoft.AspNetCore.Http; 3 | 4 | namespace Umamimolecule.AzureFunctionsMiddleware 5 | { 6 | /// 7 | /// Represents a middleware component. 8 | /// 9 | public interface IHttpMiddleware 10 | { 11 | /// 12 | /// Gets or sets the next middleware to be executed after this one. 13 | /// 14 | IHttpMiddleware Next { get; set; } 15 | 16 | /// 17 | /// Runs the middleware. 18 | /// 19 | /// The context. 20 | /// A task representing the asynchronous operation. 21 | Task InvokeAsync(HttpContext context); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Umamimolecule.AzureFunctionsMiddleware/IMiddlewarePipeline.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Microsoft.AspNetCore.Mvc; 3 | 4 | namespace Umamimolecule.AzureFunctionsMiddleware 5 | { 6 | /// 7 | /// Represents the middleware pipeline. 8 | /// 9 | public interface IMiddlewarePipeline 10 | { 11 | /// 12 | /// Adds middleware to the pipeline. 13 | /// 14 | /// The middleware to add. 15 | /// The pipeline. 16 | IMiddlewarePipeline Use(IHttpMiddleware middleware); 17 | 18 | /// 19 | /// Executes the pipeline. 20 | /// 21 | /// The value to returned from the Azure function. 22 | Task RunAsync(); 23 | 24 | /// 25 | /// Creates a new pipeline with the same configuration as the current instance. 26 | /// 27 | /// The new pipeline. 28 | IMiddlewarePipeline New(); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Umamimolecule.AzureFunctionsMiddleware/MiddlewarePipeline.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using System.Threading.Tasks; 4 | using Microsoft.AspNetCore.Http; 5 | using Microsoft.AspNetCore.Mvc; 6 | 7 | namespace Umamimolecule.AzureFunctionsMiddleware 8 | { 9 | /// 10 | /// The middleware pipeline. 11 | /// 12 | public class MiddlewarePipeline : IMiddlewarePipeline 13 | { 14 | private readonly List pipeline = new List(); 15 | private readonly IHttpContextAccessor httpContextAccessor; 16 | 17 | /// 18 | /// Initializes a new instance of the class. 19 | /// 20 | /// The HTTP context accessor. 21 | public MiddlewarePipeline(IHttpContextAccessor httpContextAccessor) 22 | { 23 | GuardClauses.IsNotNull(nameof(httpContextAccessor), httpContextAccessor); 24 | this.httpContextAccessor = httpContextAccessor; 25 | } 26 | 27 | /// 28 | /// Adds middleware to the pipeline. 29 | /// 30 | /// The middleware to add. 31 | /// The pipeline. 32 | public IMiddlewarePipeline Use(IHttpMiddleware middleware) 33 | { 34 | if (this.pipeline.Any()) 35 | { 36 | this.pipeline.Last().Next = middleware; 37 | } 38 | 39 | this.pipeline.Add(middleware); 40 | 41 | return this; 42 | } 43 | 44 | /// 45 | /// Executes the pipeline. 46 | /// 47 | /// The value to returned from the Azure function. 48 | public async Task RunAsync() 49 | { 50 | var context = this.httpContextAccessor.HttpContext; 51 | 52 | if (this.pipeline.Any()) 53 | { 54 | await this.pipeline.First().InvokeAsync(context); 55 | } 56 | else 57 | { 58 | throw new MiddlewarePipelineException($"No middleware configured"); 59 | } 60 | 61 | return new HttpResponseResult(context); 62 | } 63 | 64 | /// 65 | /// Creates a new pipeline with the same configuration as the current instance. 66 | /// 67 | /// The new pipeline. 68 | public IMiddlewarePipeline New() 69 | { 70 | return new MiddlewarePipeline(this.httpContextAccessor); 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/Umamimolecule.AzureFunctionsMiddleware/MiddlewarePipelineException.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Umamimolecule.AzureFunctionsMiddleware 4 | { 5 | /// 6 | /// Exception which is thrown when the pipeline is misconfigured. 7 | /// 8 | [Serializable] 9 | public class MiddlewarePipelineException : Exception 10 | { 11 | /// 12 | /// Initializes a new instance of the class. 13 | /// 14 | public MiddlewarePipelineException() 15 | : base("Middleware pipeline must be configured with at least one middleware and the final middleware must return a response") 16 | { 17 | } 18 | 19 | /// 20 | /// Initializes a new instance of the class. 21 | /// 22 | /// The message describing the error. 23 | public MiddlewarePipelineException(string message) 24 | : base(message) 25 | { 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Umamimolecule.AzureFunctionsMiddleware/MiddlewarePipelineExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading.Tasks; 4 | using Microsoft.AspNetCore.Http; 5 | using Microsoft.AspNetCore.Mvc; 6 | 7 | namespace Umamimolecule.AzureFunctionsMiddleware 8 | { 9 | /// 10 | /// Contains extension methods for instances. 11 | /// 12 | public static class MiddlewarePipelineExtensions 13 | { 14 | /// 15 | /// Adds an Azure Function middleware to the pipeline. 16 | /// 17 | /// The pipeline. 18 | /// The function to add. 19 | /// The pipeline instance. 20 | public static IMiddlewarePipeline Use(this IMiddlewarePipeline pipeline, Func> func) 21 | { 22 | return pipeline.Use(new FunctionMiddleware(func)); 23 | } 24 | 25 | /// 26 | /// Adds a request delegate middleware to the pipeline. 27 | /// 28 | /// The pipeline. 29 | /// The request delegate to add. 30 | /// The pipeline instance. 31 | public static IMiddlewarePipeline Use(this IMiddlewarePipeline pipeline, RequestDelegate requestDelegate) 32 | { 33 | return pipeline.Use(new RequestDelegateMiddleware(requestDelegate)); 34 | } 35 | 36 | /// 37 | /// Adds correlation ID middleware to the pipeline. 38 | /// 39 | /// The pipeline. 40 | /// The colleciton of request headers that contain the correlation ID. The first match is used. 41 | /// The pipeline instance. 42 | public static IMiddlewarePipeline UseCorrelationId(this IMiddlewarePipeline pipeline, IEnumerable correlationIdHeaders) 43 | { 44 | return pipeline.Use(new CorrelationIdMiddleware(correlationIdHeaders)); 45 | } 46 | 47 | /// 48 | /// Adds query parameter validation middleware to the pipeline. 49 | /// 50 | /// The query model type. 51 | /// The pipeline. 52 | /// Optional handler to return a custom response when validation is unsuccessful. 53 | /// The pipeline instance. 54 | public static IMiddlewarePipeline UseQueryValidation( 55 | this IMiddlewarePipeline pipeline, 56 | Func handleValidationFailure = null) 57 | where T : new() 58 | { 59 | return pipeline.Use(new QueryModelValidationMiddleware() { HandleValidationFailure = handleValidationFailure }); 60 | } 61 | 62 | /// 63 | /// Adds body payload validation middleware to the pipeline. 64 | /// 65 | /// The body model type. 66 | /// The pipeline. 67 | /// Optional handler to return a custom response when validation is unsuccessful. 68 | /// The pipeline instance. 69 | public static IMiddlewarePipeline UseBodyValidation( 70 | this IMiddlewarePipeline pipeline, 71 | Func handleValidationFailure = null) 72 | where T : new() 73 | { 74 | return pipeline.Use(new BodyModelValidationMiddleware() { HandleValidationFailure = handleValidationFailure }); 75 | } 76 | 77 | /// 78 | /// Adds exception handling middleware to the pipeline. 79 | /// 80 | /// The pipeline. 81 | /// An optional handler to process exceptions. Leave null to use the default exception handler. 82 | /// An optional handler to log exceptions. 83 | /// The pipeline instance. 84 | public static IMiddlewarePipeline UseExceptionHandling( 85 | this IMiddlewarePipeline pipeline, 86 | Func> exceptionHandler = null, 87 | Func loggerHandler = null) 88 | { 89 | var middleware = new ExceptionHandlerMiddleware() 90 | { 91 | ExceptionHandler = exceptionHandler ?? ExceptionHandlerMiddleware.DefaultExceptionHandler, 92 | LogExceptionAsync = loggerHandler, 93 | }; 94 | 95 | return pipeline.Use(middleware); 96 | } 97 | 98 | /// 99 | /// Conditionally creates a branch in the request pipeline that is rejoined to the main pipeline. 100 | /// 101 | /// The pipeline. 102 | /// The function which is invoked to determine if the branch should be taken. 103 | /// Configures the branch. 104 | /// The pipeline instance. 105 | public static IMiddlewarePipeline UseWhen( 106 | this IMiddlewarePipeline pipeline, 107 | Func condition, 108 | Action configure) 109 | { 110 | var middleware = new ConditionalMiddleware(pipeline, condition, configure, true); 111 | return pipeline.Use(middleware); 112 | } 113 | 114 | /// 115 | /// Conditionally creates a branch in the request pipeline. 116 | /// 117 | /// The pipeline. 118 | /// The function which is invoked to determine if the branch should be taken. 119 | /// Configures the branch. 120 | /// The pipeline instance. 121 | public static IMiddlewarePipeline MapWhen( 122 | this IMiddlewarePipeline pipeline, 123 | Func condition, 124 | Action configure) 125 | { 126 | var middleware = new ConditionalMiddleware(pipeline, condition, configure, false); 127 | return pipeline.Use(middleware); 128 | } 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/Umamimolecule.AzureFunctionsMiddleware/ModelValidationResult.cs: -------------------------------------------------------------------------------- 1 | namespace Umamimolecule.AzureFunctionsMiddleware 2 | { 3 | /// 4 | /// Represents the validation result for a model. 5 | /// 6 | public class ModelValidationResult 7 | { 8 | /// 9 | /// Gets or sets a value indicating whether the validation was successful. 10 | /// 11 | public bool Success { get; set; } 12 | 13 | /// 14 | /// Gets or sets the error message when the validation was unsuccessful. 15 | /// 16 | public string Error { get; set; } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Umamimolecule.AzureFunctionsMiddleware/QueryModelValidationMiddleware.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.DataAnnotations; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | using Microsoft.AspNetCore.Http; 7 | 8 | namespace Umamimolecule.AzureFunctionsMiddleware 9 | { 10 | /// 11 | /// Middleware to perform validation of the query parameters. 12 | /// 13 | /// The query parameter type. 14 | public class QueryModelValidationMiddleware : ValidationMiddleware 15 | where T : new() 16 | { 17 | /// 18 | /// Gets the error code to use when validation fails. 19 | /// 20 | public override string ErrorCode => ErrorCodes.InvalidQueryParameters; 21 | 22 | /// 23 | /// Validates the query parameters. 24 | /// 25 | /// The context. 26 | /// The validation results. 27 | protected override async Task ValidateAsync(HttpContext context) 28 | { 29 | var model = await this.CreateModelAsync(context); 30 | List validationResults = new List(); 31 | if (!Validator.TryValidateObject(model, new ValidationContext(model), validationResults, true)) 32 | { 33 | return new ModelValidationResult() 34 | { 35 | Success = false, 36 | Error = string.Join(", ", validationResults.Select(x => x.ErrorMessage)), 37 | }; 38 | } 39 | 40 | context.Items[ContextItems.Query] = model; 41 | 42 | return new ModelValidationResult() 43 | { 44 | Success = true, 45 | }; 46 | } 47 | 48 | private Task CreateModelAsync(HttpContext context) 49 | { 50 | var type = typeof(T); 51 | var ctor = type.GetConstructor(System.Type.EmptyTypes); 52 | if (ctor == null) 53 | { 54 | throw new ApplicationException($"Could not find a parameterless public constructor for the type {type.FullName}"); 55 | } 56 | 57 | var model = (T)ctor.Invoke(null); 58 | 59 | if (context.Request.Query != null) 60 | { 61 | var properties = type.GetProperties(); 62 | foreach (var parameter in context.Request.Query) 63 | { 64 | var property = properties.FirstOrDefault(x => string.Compare(x.Name, parameter.Key, true) == 0); 65 | if (property != null) 66 | { 67 | property.SetValue(model, parameter.Value.ToString()); 68 | } 69 | } 70 | } 71 | 72 | return Task.FromResult(model); 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/Umamimolecule.AzureFunctionsMiddleware/RecursiveValidator.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.DataAnnotations; 4 | using System.Linq; 5 | 6 | namespace Umamimolecule.AzureFunctionsMiddleware 7 | { 8 | /// 9 | /// Recursively validates all properties and child properties of a model. 10 | /// 11 | public static class RecursiveValidator 12 | { 13 | /// 14 | /// Recursively validates . 15 | /// 16 | /// The object to validate. 17 | /// A collection to hold each failed validation. 18 | /// true to validate all properties; if false, only required attributes are validated. 19 | /// true if the object validates; otherwise, false. 20 | /// is null. 21 | public static bool TryValidateObject(object instance, ICollection results, bool validateAllProperties) 22 | { 23 | return TryValidateObject(instance, results, validateAllProperties, null); 24 | } 25 | 26 | /// 27 | /// Recursively validates . 28 | /// 29 | /// The object to validate. 30 | /// A collection to hold each failed validation. 31 | /// true to validate all properties; if false, only required attributes are validated. 32 | /// The prefix to append to the field name when validation fails. 33 | /// true if the object validates; otherwise, false. 34 | /// is null. 35 | private static bool TryValidateObject(object instance, ICollection results, bool validateAllProperties, string prefix) 36 | { 37 | GuardClauses.IsNotNull(nameof(instance), instance); 38 | 39 | var tempResults = new List(); 40 | 41 | ValidationContext validationContext = new ValidationContext(instance); 42 | var isValid = Validator.TryValidateObject(instance, validationContext, tempResults, validateAllProperties: validateAllProperties); 43 | 44 | foreach (var item in tempResults) 45 | { 46 | IEnumerable memberNames = item.MemberNames.Select(name => (!string.IsNullOrEmpty(prefix) ? prefix + "." : string.Empty) + name); 47 | results.Add(new ValidationResult(item.ErrorMessage, memberNames)); 48 | } 49 | 50 | foreach (var prop in instance.GetType().GetProperties()) 51 | { 52 | if (prop.PropertyType != typeof(string)) 53 | { 54 | var value = prop.GetValue(instance); 55 | if (value == null) 56 | { 57 | continue; 58 | } 59 | else if (value is IEnumerable list) 60 | { 61 | var memberPrefix = (!string.IsNullOrEmpty(prefix) ? prefix + "." : string.Empty) + prop.Name; 62 | int i = 0; 63 | foreach (var item in list) 64 | { 65 | if (!TryValidateObject(item, results, validateAllProperties, $"{memberPrefix}[{i}]")) 66 | { 67 | isValid = false; 68 | } 69 | 70 | i++; 71 | } 72 | } 73 | else 74 | { 75 | var memberPrefix = (!string.IsNullOrEmpty(prefix) ? prefix + "." : string.Empty) + prop.Name; 76 | if (!TryValidateObject(value, results, validateAllProperties, memberPrefix)) 77 | { 78 | isValid = false; 79 | } 80 | } 81 | } 82 | } 83 | 84 | return isValid; 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/Umamimolecule.AzureFunctionsMiddleware/RequestDelegateMiddleware.cs: -------------------------------------------------------------------------------- 1 | using System.Threading.Tasks; 2 | using Microsoft.AspNetCore.Http; 3 | 4 | namespace Umamimolecule.AzureFunctionsMiddleware 5 | { 6 | /// 7 | /// Middleware to allow a request delegate to be executed. 8 | /// 9 | public class RequestDelegateMiddleware : HttpMiddleware 10 | { 11 | private readonly RequestDelegate requestDelegate; 12 | 13 | /// 14 | /// Initializes a new instance of the class. 15 | /// 16 | /// The request delegate to be executed. 17 | public RequestDelegateMiddleware(RequestDelegate requestDelegate) 18 | { 19 | this.requestDelegate = requestDelegate; 20 | } 21 | 22 | /// 23 | /// Runs the middleware. 24 | /// 25 | /// The context. 26 | /// A task representing the asynchronous operation. 27 | public override Task InvokeAsync(HttpContext context) 28 | { 29 | return this.requestDelegate(context); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/Umamimolecule.AzureFunctionsMiddleware/Umamimolecule.AzureFunctionsMiddleware.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.1 5 | Umamimolecule 6 | 7 | An extensible middleware implementation for HTTP-triggered Azure Functions in .Net. 8 | https://github.com/umamimolecule/azure-functions-http-middleware 9 | https://github.com/umamimolecule/azure-functions-http-middleware 10 | git 11 | Azure Functions Http Middleware 12 | true 13 | snupkg 14 | true 15 | https://github.com/umamimolecule/azure-functions-http-middleware/blob/master/CHANGELOG.md#2.1.0 16 | true 17 | 2.1.0 18 | 2.1.0.0 19 | 2.1.0.0 20 | 21 | 22 | 23 | Umamimolecule.AzureFunctionsMiddleware.ruleset 24 | true 25 | 26 | Umamimolecule.AzureFunctionsMiddleware.xml 27 | 28 | 29 | 30 | Umamimolecule.AzureFunctionsMiddleware.ruleset 31 | true 32 | 33 | Umamimolecule.AzureFunctionsMiddleware.xml 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | all 44 | runtime; build; native; contentfiles; analyzers; buildtransitive 45 | 46 | 47 | all 48 | runtime; build; native; contentfiles; analyzers; buildtransitive 49 | 50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /src/Umamimolecule.AzureFunctionsMiddleware/Umamimolecule.AzureFunctionsMiddleware.nuspec: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Umamimolecule.AzureFunctionsMiddleware 5 | 2.1.0 6 | Umamimolecule.AzureFunctionsMiddleware 7 | Umamimolecule 8 | Umamimolecule 9 | https://github.com/umamimolecule/azure-functions-http-middleware 10 | false 11 | An extensible middleware implementation for HTTP-triggered Azure Functions in .Net. 12 | https://github.com/umamimolecule/azure-functions-http-middleware/blob/master/CHANGELOG.md#2.1.0 13 | Azure Functions Http Middleware 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/Umamimolecule.AzureFunctionsMiddleware/Umamimolecule.AzureFunctionsMiddleware.ruleset: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /src/Umamimolecule.AzureFunctionsMiddleware/ValidationMiddleware.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Net; 3 | using System.Threading.Tasks; 4 | using Microsoft.AspNetCore.Http; 5 | using Microsoft.AspNetCore.Mvc; 6 | using Newtonsoft.Json; 7 | 8 | namespace Umamimolecule.AzureFunctionsMiddleware 9 | { 10 | /// 11 | /// Base class for validation middleware. 12 | /// 13 | /// The object type containing the model to validate. 14 | public abstract class ValidationMiddleware : HttpMiddleware 15 | where T : new() 16 | { 17 | /// 18 | /// Gets the error code to use when validation fails. 19 | /// 20 | public abstract string ErrorCode { get; } 21 | 22 | /// 23 | /// Gets or sets a function to handle a validation failure and provide a custom response. If not set, a default response object will be sent. 24 | /// 25 | public Func HandleValidationFailure { get; set; } 26 | 27 | private Func DefaultFailureResponse => (HttpContext context, ModelValidationResult validationResult) => 28 | { 29 | var response = new 30 | { 31 | correlationId = context.TraceIdentifier, 32 | error = new 33 | { 34 | code = this.ErrorCode, 35 | message = validationResult.Error, 36 | }, 37 | }; 38 | 39 | return new BadRequestObjectResult(response); 40 | }; 41 | 42 | /// 43 | /// Runs the middleware. 44 | /// 45 | /// The context. 46 | /// A task representing the asynchronous operation. 47 | public override async Task InvokeAsync(HttpContext context) 48 | { 49 | #pragma warning disable IDE0042 // Deconstruct variable declaration 50 | var validationResult = await this.ValidateAsync(context); 51 | #pragma warning restore IDE0042 // Deconstruct variable declaration 52 | if (validationResult.Success) 53 | { 54 | if (this.Next != null) 55 | { 56 | await this.Next.InvokeAsync(context); 57 | } 58 | } 59 | else 60 | { 61 | var result = this.HandleValidationFailure ?? this.DefaultFailureResponse; 62 | await context.ProcessActionResultAsync(result(context, validationResult)); 63 | } 64 | } 65 | 66 | /// 67 | /// Validates the model. 68 | /// 69 | /// The HTTP context. 70 | /// The validation results. 71 | protected abstract Task ValidateAsync(HttpContext context); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /test.runsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | .*\Umamimolecule.AzureFunctionsMiddleware.dll$ 14 | 15 | 16 | True 17 | True 18 | True 19 | False 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /tests/Umamimolecule.AzureFunctionsMiddleware.Tests/BodyModelValidationMiddlewareTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.ComponentModel.DataAnnotations; 3 | using System.Threading.Tasks; 4 | using Microsoft.AspNetCore.Http; 5 | using Microsoft.AspNetCore.Mvc; 6 | using Microsoft.Extensions.DependencyInjection; 7 | using Microsoft.Extensions.Logging; 8 | using Microsoft.Extensions.ObjectPool; 9 | using Moq; 10 | using Newtonsoft.Json; 11 | using Shouldly; 12 | using Xunit; 13 | 14 | namespace Umamimolecule.AzureFunctionsMiddleware.Tests 15 | { 16 | public class BodyModelValidationMiddlewareTests 17 | { 18 | [Fact(DisplayName = "InvokeAsync should correctly set the Body item for the context")] 19 | public async Task InvokeAsync() 20 | { 21 | var body = new Body() 22 | { 23 | Id = "1", 24 | Child = new Child() 25 | { 26 | Id = "2", 27 | Name = "Fred" 28 | } 29 | }; 30 | 31 | var context = this.CreateContext(body); 32 | var instance = this.CreateInstance(); 33 | await instance.InvokeAsync(context); 34 | 35 | context.Items["Body"].ShouldNotBeNull(); 36 | var model = context.Items["Body"].ShouldBeOfType(); 37 | model.Id.ShouldBe("1"); 38 | model.Child.ShouldNotBeNull(); 39 | model.Child.Id.ShouldBe("2"); 40 | model.Child.Name.ShouldBe("Fred"); 41 | } 42 | 43 | [Fact(DisplayName = "InvokeAsync should correctly call the next middleware")] 44 | public async Task InvokeAsync_CallsNextMiddleware() 45 | { 46 | var body = new Body() 47 | { 48 | Id = "1", 49 | Child = new Child() 50 | { 51 | Id = "2", 52 | Name = "Fred" 53 | } 54 | }; 55 | 56 | var context = this.CreateContext(body); 57 | var instance = this.CreateInstance(); 58 | 59 | var nextMiddleware = new Mock(); 60 | instance.Next = nextMiddleware.Object; 61 | await instance.InvokeAsync(context); 62 | 63 | nextMiddleware.Verify(x => x.InvokeAsync(It.IsAny()), Times.Once); 64 | } 65 | 66 | [Theory(DisplayName = "InvokeAsync should throw the expected exception when a required property is missing for the default response handler")] 67 | [InlineData(null)] 68 | [InlineData("")] 69 | public async Task InvokeAsync_Fail_MissingRequiredProperties_DefaultResponseHandler(string id) 70 | { 71 | var body = new Body() 72 | { 73 | Id = null, 74 | Child = new Child() 75 | { 76 | Id = id, 77 | Name = "Fred" 78 | } 79 | }; 80 | 81 | var context = this.CreateContext(body); 82 | var instance = this.CreateInstance(); 83 | 84 | await instance.InvokeAsync(context); 85 | 86 | context.Response.StatusCode.ShouldBe(400); 87 | context.Response.Body.Position = 0; 88 | var contents = context.Response.Body.ReadAsString(); 89 | var response = JsonConvert.DeserializeObject(contents); 90 | response.ShouldNotBeNull(); 91 | response.CorrelationId.ShouldNotBeNullOrWhiteSpace(); 92 | response.Error.ShouldNotBeNull(); 93 | response.Error.Code.ShouldBe("INVALID_BODY"); 94 | response.Error.Message.ShouldContain("The Id field is required."); 95 | } 96 | 97 | [Theory(DisplayName = "InvokeAsync should throw the expected exception when a required property is missing for a custom response handler")] 98 | [InlineData(null)] 99 | [InlineData("")] 100 | public async Task InvokeAsync_Fail_MissingRequiredProperties_CustomResponseHandler(string id) 101 | { 102 | var body = new Body() 103 | { 104 | Id = null, 105 | Child = new Child() 106 | { 107 | Id = id, 108 | Name = "Fred" 109 | } 110 | }; 111 | 112 | static IActionResult handler(HttpContext context, ModelValidationResult validationResult) 113 | { 114 | CustomErrorResponse response = new CustomErrorResponse() 115 | { 116 | CustomErrorMessage = "Custom error: The Id field is required", 117 | }; 118 | 119 | return new ObjectResult(response) 120 | { 121 | StatusCode = 409, 122 | }; 123 | } 124 | 125 | var context = this.CreateContext(body); 126 | var instance = this.CreateInstance(); 127 | instance.HandleValidationFailure = handler; 128 | 129 | await instance.InvokeAsync(context); 130 | 131 | context.Response.StatusCode.ShouldBe(409); 132 | context.Response.Body.Position = 0; 133 | var contents = context.Response.Body.ReadAsString(); 134 | var response = JsonConvert.DeserializeObject(contents); 135 | response.ShouldNotBeNull(); 136 | response.CustomErrorMessage.ShouldBe("Custom error: The Id field is required"); 137 | } 138 | 139 | private BodyModelValidationMiddleware CreateInstance() 140 | where T : new() 141 | { 142 | return new BodyModelValidationMiddleware(); 143 | } 144 | 145 | private HttpContext CreateContext(T body) 146 | { 147 | Logger logger = new Logger(); 148 | Mock loggerFactory = new Mock(); 149 | loggerFactory.Setup(x => x.CreateLogger(It.IsAny())) 150 | .Returns(logger); 151 | 152 | ServiceCollection services = new ServiceCollection(); 153 | services.AddMvcCore().AddJsonFormatters(); 154 | services.AddOptions(); 155 | 156 | services.AddTransient((IServiceProvider p) => loggerFactory.Object); 157 | services.AddSingleton(); 158 | var serviceProvider = services.BuildServiceProvider(); 159 | 160 | return new ContextBuilder().AddServiceProvider(serviceProvider) 161 | .AddJsonBody(body) 162 | .Build(); 163 | } 164 | 165 | class Body 166 | { 167 | [Required] 168 | public string Id { get; set; } 169 | 170 | public Child Child { get; set; } 171 | } 172 | 173 | class Child 174 | { 175 | [Required] 176 | public string Id { get; set; } 177 | 178 | public string Name { get; set; } 179 | } 180 | 181 | class CustomErrorResponse 182 | { 183 | public string CustomErrorMessage { get; set; } 184 | } 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /tests/Umamimolecule.AzureFunctionsMiddleware.Tests/ConditionalMiddlewareTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Microsoft.AspNetCore.Http; 4 | using Moq; 5 | using Shouldly; 6 | using Xunit; 7 | 8 | namespace Umamimolecule.AzureFunctionsMiddleware.Tests 9 | { 10 | public class ConditionalMiddlewareTests 11 | { 12 | private readonly Mock context = new Mock(); 13 | 14 | private readonly Mock pipeline = new Mock(); 15 | 16 | private readonly Mock branch = new Mock(); 17 | 18 | private readonly Func condition; 19 | 20 | private readonly Mock next = new Mock(); 21 | 22 | private readonly Action configure; 23 | 24 | private bool conditionResult = true; 25 | 26 | private bool configured = false; 27 | 28 | public ConditionalMiddlewareTests() 29 | { 30 | this.condition = (context) => this.conditionResult; 31 | this.configure = (pipeline) => { this.configured = true; }; 32 | this.pipeline.Setup(x => x.New()).Returns(branch.Object); 33 | } 34 | 35 | [Fact(DisplayName = "Creating new instance with null pipeline should throw expected exception")] 36 | public void NullPipelineShouldThrowExpectedException() 37 | { 38 | var exception = ShouldThrowExtensions.ShouldThrow(() => new ConditionalMiddleware(null, condition, configure, true)); 39 | exception.ParamName.ShouldBe("pipeline"); 40 | } 41 | 42 | [Fact(DisplayName = "Creating new instance with null condition should throw expected exception")] 43 | public void NullConditionShouldThrowExpectedException() 44 | { 45 | var exception = ShouldThrowExtensions.ShouldThrow(() => new ConditionalMiddleware(pipeline.Object, null, configure, true)); 46 | exception.ParamName.ShouldBe("condition"); 47 | } 48 | 49 | [Fact(DisplayName = "Creating new instance with null configure should not throw exception")] 50 | public void NullConfigureShouldNotThrowExpectedException() 51 | { 52 | new ConditionalMiddleware(pipeline.Object, condition, null, true); 53 | } 54 | 55 | [Fact(DisplayName = "InvokeAsync should execute the branch when condition evaluates to true but not rejoin pipeline")] 56 | public async Task InvokeAsync_ConditionTrue_DoNotRejoinPipeline() 57 | { 58 | var instance = new ConditionalMiddleware(pipeline.Object, condition, configure, false) 59 | { 60 | Next = next.Object 61 | }; 62 | await instance.InvokeAsync(this.context.Object); 63 | this.configured.ShouldBeTrue(); 64 | this.branch.Verify(x => x.RunAsync(), Times.Once); 65 | this.next.Verify(x => x.InvokeAsync(this.context.Object), Times.Never); 66 | } 67 | 68 | [Fact(DisplayName = "InvokeAsync should execute the branch when condition evaluates to true and rejoin pipeline")] 69 | public async Task InvokeAsync_ConditionTrue_RejoinPipeline() 70 | { 71 | var instance = new ConditionalMiddleware(pipeline.Object, condition, configure, true) 72 | { 73 | Next = next.Object 74 | }; 75 | await instance.InvokeAsync(this.context.Object); 76 | this.configured.ShouldBeTrue(); 77 | this.branch.Verify(x => x.RunAsync(), Times.Once); 78 | this.next.Verify(x => x.InvokeAsync(this.context.Object), Times.Once); 79 | } 80 | 81 | [Fact(DisplayName = "InvokeAsync should not execute the branch when condition evaluates to false")] 82 | public async Task InvokeAsync_ConditionFalse_RejoinPipeline() 83 | { 84 | this.conditionResult = false; 85 | var instance = new ConditionalMiddleware(pipeline.Object, condition, configure, true) 86 | { 87 | Next = next.Object 88 | }; 89 | await instance.InvokeAsync(this.context.Object); 90 | this.configured.ShouldBeFalse(); 91 | this.branch.Verify(x => x.RunAsync(), Times.Never); 92 | this.next.Verify(x => x.InvokeAsync(this.context.Object), Times.Once); 93 | } 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /tests/Umamimolecule.AzureFunctionsMiddleware.Tests/ContextBuilder.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using Microsoft.AspNetCore.Http; 4 | using Microsoft.AspNetCore.Http.Features; 5 | using Newtonsoft.Json; 6 | 7 | namespace Umamimolecule.AzureFunctionsMiddleware.Tests 8 | { 9 | public class ContextBuilder 10 | { 11 | private readonly HttpContext context; 12 | 13 | public ContextBuilder() 14 | { 15 | this.context = new DefaultHttpContext(); 16 | context.Request.Body = new MemoryStream(); 17 | context.Response.Body = new MemoryStream(); 18 | } 19 | 20 | public HttpContext Build() 21 | { 22 | return this.context; 23 | } 24 | 25 | public ContextBuilder Accepts(string contentType) 26 | { 27 | this.context.Request.Headers["Accepts"] = contentType; 28 | return this; 29 | } 30 | 31 | public ContextBuilder AddServiceProvider(IServiceProvider serviceProvider) 32 | { 33 | this.context.RequestServices = serviceProvider; 34 | return this; 35 | } 36 | 37 | public ContextBuilder AddRequestHeaders(IHeaderDictionary headers) 38 | { 39 | foreach (var header in headers) 40 | { 41 | this.context.Request.Headers.Add(header.Key, header.Value); 42 | } 43 | 44 | return this; 45 | } 46 | 47 | public ContextBuilder AddQuery(IQueryCollection query) 48 | { 49 | this.context.Request.Query = query; 50 | return this; 51 | } 52 | 53 | public ContextBuilder AddJsonBody(T body) 54 | { 55 | StreamWriter writer = new StreamWriter(this.context.Request.Body); 56 | writer.Write(JsonConvert.SerializeObject(body)); 57 | writer.Flush(); 58 | this.context.Request.Body.Seek(0, SeekOrigin.Begin); 59 | this.context.Request.ContentType = "application/json"; 60 | return this; 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /tests/Umamimolecule.AzureFunctionsMiddleware.Tests/CorrelationIdMiddlewareTests.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using System.Threading.Tasks; 3 | using Microsoft.AspNetCore.Http; 4 | using Microsoft.AspNetCore.Mvc; 5 | using Moq; 6 | using Shouldly; 7 | using Xunit; 8 | 9 | namespace Umamimolecule.AzureFunctionsMiddleware.Tests 10 | { 11 | public class CorrelationIdMiddlewareTests 12 | { 13 | private const string correlationIdHeader1 = "a"; 14 | 15 | private const string correlationIdHeader2 = "b"; 16 | 17 | public CorrelationIdMiddlewareTests() 18 | { 19 | var types = typeof(StatusCodeResult).Assembly.GetExportedTypes(); 20 | var actionResults = types.Where(x => x.GetInterfaces().Contains(typeof(IActionResult))).ToArray(); 21 | } 22 | 23 | [Fact(DisplayName = "Should invoke next middleware")] 24 | public async Task InvokeNextMiddleware() 25 | { 26 | HeaderDictionary headers = new HeaderDictionary 27 | { 28 | { "c", "abc123" } 29 | }; 30 | 31 | var context = new ContextBuilder().AddRequestHeaders(headers) 32 | .Build(); 33 | 34 | var instance = this.CreateInstance(); 35 | 36 | var nextMiddleWare = new Mock(); 37 | instance.Next = nextMiddleWare.Object; 38 | 39 | await instance.InvokeAsync(context); 40 | nextMiddleWare.Verify(x => x.InvokeAsync(context), Times.Once); 41 | } 42 | 43 | [Fact(DisplayName = "Should generate correlation identifier when no matching headers are found")] 44 | public async Task NoMatchingCorrelationHeaders() 45 | { 46 | HeaderDictionary headers = new HeaderDictionary 47 | { 48 | { "c", "abc123" } 49 | }; 50 | 51 | var context = new ContextBuilder().AddRequestHeaders(headers) 52 | .Build(); 53 | 54 | var instance = this.CreateInstance(); 55 | await instance.InvokeAsync(context); 56 | context.TraceIdentifier.ShouldNotBeNullOrWhiteSpace(); 57 | } 58 | 59 | [Fact(DisplayName = "Should use supplied correlation identifier")] 60 | public async Task FirstMatchingCorrelationHeader() 61 | { 62 | HeaderDictionary headers = new HeaderDictionary 63 | { 64 | { correlationIdHeader1, "abc123" }, 65 | { correlationIdHeader2, "abc124" } 66 | }; 67 | 68 | var context = new ContextBuilder().AddRequestHeaders(headers) 69 | .Build(); 70 | 71 | var instance = this.CreateInstance(); 72 | await instance.InvokeAsync(context); 73 | context.TraceIdentifier.ShouldBe("abc123"); 74 | } 75 | 76 | [Fact(DisplayName = "Should use supplied correlation identifier")] 77 | public async Task SecondMatchingCorrelationHeaders() 78 | { 79 | HeaderDictionary headers = new HeaderDictionary 80 | { 81 | { correlationIdHeader1, "" }, 82 | { correlationIdHeader2, "abc124" } 83 | }; 84 | 85 | var context = new ContextBuilder().AddRequestHeaders(headers) 86 | .Build(); 87 | 88 | var instance = this.CreateInstance(); 89 | await instance.InvokeAsync(context); 90 | context.TraceIdentifier.ShouldBe("abc124"); 91 | } 92 | 93 | private CorrelationIdMiddleware CreateInstance() 94 | { 95 | return new CorrelationIdMiddleware(new string[] { correlationIdHeader1, correlationIdHeader2 }); 96 | } 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /tests/Umamimolecule.AzureFunctionsMiddleware.Tests/DummyDisposable.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace Umamimolecule.AzureFunctionsMiddleware.Tests 4 | { 5 | public class DummyDisposable : IDisposable 6 | { 7 | public void Dispose() 8 | { 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /tests/Umamimolecule.AzureFunctionsMiddleware.Tests/ErrorResponse.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | 5 | namespace Umamimolecule.AzureFunctionsMiddleware.Tests 6 | { 7 | class ErrorResponse 8 | { 9 | public Error Error { get; set; } 10 | 11 | public string CorrelationId { get; set; } 12 | } 13 | 14 | class Error 15 | { 16 | public string Code { get; set; } 17 | 18 | public string Message { get; set; } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /tests/Umamimolecule.AzureFunctionsMiddleware.Tests/ExceptionHandlerMiddlewareTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Text; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Http; 6 | using Microsoft.Extensions.DependencyInjection; 7 | using Microsoft.Extensions.Logging; 8 | using Microsoft.Extensions.ObjectPool; 9 | using Moq; 10 | using Newtonsoft.Json; 11 | using Shouldly; 12 | using Xunit; 13 | 14 | namespace Umamimolecule.AzureFunctionsMiddleware.Tests 15 | { 16 | public class ExceptionHandlerMiddlewareTests 17 | { 18 | [Fact(DisplayName = "InvokeAsync should throw the expected exception when Next middleware is not set")] 19 | public async Task InvokeAsync_ThrowsExceptionWhenNextIsNotSet() 20 | { 21 | ExceptionHandlerMiddleware middleware = new ExceptionHandlerMiddleware(); 22 | var context = this.GetContext(); 23 | var exception = await ShouldThrowAsyncExtensions.ShouldThrowAsync(() => middleware.InvokeAsync(context)); 24 | exception.Message.ShouldContain("must have a Next middleware"); 25 | } 26 | 27 | [Fact(DisplayName = "InvokeAsync should succeed when no exceptions are thrown by the next middleware")] 28 | public async Task InvokeAsync_NoExceptionFromNextMiddleware() 29 | { 30 | ExceptionHandlerMiddleware middleware = new ExceptionHandlerMiddleware() 31 | { 32 | Next = new DummyMiddleware() 33 | }; 34 | var context = this.GetContext(); 35 | await middleware.InvokeAsync(context); 36 | context.Response.StatusCode.ShouldBe(202); 37 | } 38 | 39 | [Fact(DisplayName = "InvokeAsync should call LogExceptionAsync when an exception is thrown by the next middleware")] 40 | public async Task InvokeAsync_CallLogExceptionAsync() 41 | { 42 | Exception loggedException = null; 43 | ExceptionHandlerMiddleware middleware = new ExceptionHandlerMiddleware() 44 | { 45 | Next = new FaultyMiddleware(), 46 | LogExceptionAsync = (Exception e) => 47 | { 48 | loggedException = e; 49 | return Task.CompletedTask; 50 | } 51 | }; 52 | var context = this.GetContext(); 53 | await middleware.InvokeAsync(context); 54 | var typedLoggedException = loggedException.ShouldBeOfType(); 55 | typedLoggedException.Message.ShouldBe("oh no!"); 56 | } 57 | 58 | [Fact(DisplayName = "InvokeAsync should set the expected response when no exception handler is defined")] 59 | public async Task InvokeAsync_NoExceptionHandler() 60 | { 61 | ExceptionHandlerMiddleware middleware = new ExceptionHandlerMiddleware() 62 | { 63 | Next = new FaultyMiddleware() 64 | }; 65 | var context = this.GetContext(); 66 | await middleware.InvokeAsync(context); 67 | context.Response.StatusCode.ShouldBe(500); 68 | context.Response.Body.Position = 0; 69 | var contents = context.Response.Body.ReadAsString(); 70 | var response = JsonConvert.DeserializeObject(contents); 71 | response.ShouldNotBeNull(); 72 | response.CorrelationId.ShouldNotBeNullOrWhiteSpace(); 73 | response.Error.ShouldNotBeNull(); 74 | response.Error.Code.ShouldBe("INTERNAL_SERVER_ERROR"); 75 | response.Error.Message.ShouldBe("An internal server error occurred"); 76 | } 77 | 78 | [Fact(DisplayName = "InvokeAsync should set the expected response when the default exception handler is used")] 79 | public async Task InvokeAsync_DefaultExceptionHandler() 80 | { 81 | ExceptionHandlerMiddleware middleware = new ExceptionHandlerMiddleware() 82 | { 83 | Next = new FaultyMiddleware(), 84 | ExceptionHandler = ExceptionHandlerMiddleware.DefaultExceptionHandler 85 | }; 86 | var context = this.GetContext(); 87 | await middleware.InvokeAsync(context); 88 | context.Response.StatusCode.ShouldBe(500); 89 | context.Response.Body.Position = 0; 90 | var contents = context.Response.Body.ReadAsString(); 91 | var response = JsonConvert.DeserializeObject(contents); 92 | response.ShouldNotBeNull(); 93 | response.CorrelationId.ShouldNotBeNullOrWhiteSpace(); 94 | response.Error.ShouldNotBeNull(); 95 | response.Error.Code.ShouldBe("INTERNAL_SERVER_ERROR"); 96 | response.Error.Message.ShouldBe("An unexpected error occurred in the application"); 97 | } 98 | 99 | [Fact(DisplayName = "InvokeAsync should set the expected response when the default exception handler is used")] 100 | public async Task InvokeAsync_DefaultExceptionHandler_BadRequest() 101 | { 102 | ExceptionHandlerMiddleware middleware = new ExceptionHandlerMiddleware() 103 | { 104 | Next = new BadRequestMiddleware(), 105 | ExceptionHandler = ExceptionHandlerMiddleware.DefaultExceptionHandler 106 | }; 107 | var context = this.GetContext(); 108 | await middleware.InvokeAsync(context); 109 | context.Response.StatusCode.ShouldBe(400); 110 | context.Response.Body.Position = 0; 111 | var contents = context.Response.Body.ReadAsString(); 112 | var response = JsonConvert.DeserializeObject(contents); 113 | response.ShouldNotBeNull(); 114 | response.CorrelationId.ShouldNotBeNullOrWhiteSpace(); 115 | response.Error.ShouldNotBeNull(); 116 | response.Error.Code.ShouldBe("BAD_REQUEST"); 117 | response.Error.Message.ShouldBe("oh no!"); 118 | } 119 | 120 | private HttpContext GetContext() 121 | { 122 | Logger logger = new Logger(); 123 | Mock loggerFactory = new Mock(); 124 | loggerFactory.Setup(x => x.CreateLogger(It.IsAny())) 125 | .Returns(logger); 126 | 127 | ServiceCollection services = new ServiceCollection(); 128 | services.AddMvcCore().AddJsonFormatters(); 129 | services.AddOptions(); 130 | 131 | services.AddTransient((IServiceProvider p) => loggerFactory.Object); 132 | services.AddSingleton(); 133 | var serviceProvider = services.BuildServiceProvider(); 134 | 135 | return new ContextBuilder().AddServiceProvider(serviceProvider) 136 | .Build(); 137 | } 138 | 139 | class DummyMiddleware : HttpMiddleware 140 | { 141 | public override Task InvokeAsync(HttpContext context) 142 | { 143 | context.Response.StatusCode = 202; 144 | return Task.CompletedTask; 145 | } 146 | } 147 | 148 | class FaultyMiddleware : HttpMiddleware 149 | { 150 | public override Task InvokeAsync(HttpContext context) 151 | { 152 | throw new ApplicationException("oh no!"); 153 | } 154 | } 155 | 156 | class BadRequestMiddleware : HttpMiddleware 157 | { 158 | public override Task InvokeAsync(HttpContext context) 159 | { 160 | throw new BadRequestException("oh no!"); 161 | } 162 | } 163 | } 164 | } 165 | -------------------------------------------------------------------------------- /tests/Umamimolecule.AzureFunctionsMiddleware.Tests/FunctionMiddlewareTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Threading.Tasks; 3 | using Microsoft.AspNetCore.Http; 4 | using Microsoft.AspNetCore.Mvc; 5 | using Moq; 6 | using Shouldly; 7 | using Xunit; 8 | 9 | namespace Umamimolecule.AzureFunctionsMiddleware.Tests 10 | { 11 | public class FunctionMiddlewareTests 12 | { 13 | private readonly Mock context = new Mock(); 14 | 15 | [Fact(DisplayName = "Creating new instance with null function should throw expected exception")] 16 | public void NullFuncShouldThrowExpectedException() 17 | { 18 | var exception = ShouldThrowExtensions.ShouldThrow(() => new FunctionMiddleware(null)); 19 | exception.ParamName.ShouldBe("func"); 20 | } 21 | 22 | [Fact(DisplayName = "Invoking the middleware should call the function.")] 23 | public async Task FunctionShouldBeCalledOnInvoke() 24 | { 25 | Mock result = new Mock(); 26 | 27 | int callCount = 0; 28 | Task func(HttpContext ctx) 29 | { 30 | callCount++; 31 | return Task.FromResult(result.Object); 32 | } 33 | 34 | FunctionMiddleware instance = new FunctionMiddleware(func); 35 | await instance.InvokeAsync(context.Object); 36 | callCount.ShouldBe(1); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /tests/Umamimolecule.AzureFunctionsMiddleware.Tests/HttpContextAccessor.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Http; 2 | 3 | namespace Umamimolecule.AzureFunctionsMiddleware.Tests 4 | { 5 | public class HttpContextAccessor : IHttpContextAccessor 6 | { 7 | public HttpContextAccessor(HttpContext context) 8 | { 9 | this.HttpContext = context; 10 | } 11 | 12 | public HttpContext HttpContext { get; set; } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /tests/Umamimolecule.AzureFunctionsMiddleware.Tests/Logger.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Microsoft.Extensions.Logging; 4 | 5 | namespace Umamimolecule.AzureFunctionsMiddleware.Tests 6 | { 7 | public class Logger : ILogger 8 | { 9 | public List EnabledLogLevels { get; set; } 10 | 11 | public Logger() 12 | { 13 | this.EnabledLogLevels = new List 14 | { 15 | LogLevel.Trace, 16 | LogLevel.Debug, 17 | LogLevel.Information, 18 | LogLevel.Warning, 19 | LogLevel.Error, 20 | LogLevel.Critical 21 | }; 22 | } 23 | 24 | public IDisposable BeginScope(TState state) 25 | { 26 | return new DummyDisposable(); 27 | } 28 | 29 | public bool IsEnabled(LogLevel logLevel) 30 | { 31 | return this.EnabledLogLevels.Contains(logLevel); 32 | } 33 | 34 | public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) 35 | { 36 | // TODO: Maybe want to track the log values but for now do nothing 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /tests/Umamimolecule.AzureFunctionsMiddleware.Tests/MiddlewarePipelineTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Net; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Http; 6 | using Microsoft.AspNetCore.Mvc; 7 | using Microsoft.Extensions.DependencyInjection; 8 | using Microsoft.Extensions.Logging; 9 | using Microsoft.Extensions.ObjectPool; 10 | using Moq; 11 | using Shouldly; 12 | using Xunit; 13 | 14 | namespace Umamimolecule.AzureFunctionsMiddleware.Tests 15 | { 16 | public class MiddlewarePipelineTests 17 | { 18 | private readonly HttpContext context; 19 | private readonly HttpContextAccessor contextAccessor; 20 | private readonly Mock loggerFactory = new Mock(); 21 | private readonly Logger logger = new Logger(); 22 | 23 | public MiddlewarePipelineTests() 24 | { 25 | this.loggerFactory.Setup(x => x.CreateLogger(It.IsAny())) 26 | .Returns(this.logger); 27 | 28 | ServiceCollection c = new ServiceCollection(); 29 | c.AddMvcCore().AddJsonFormatters(); 30 | c.AddOptions(); 31 | 32 | c.AddTransient((IServiceProvider p) => this.loggerFactory.Object); 33 | c.AddSingleton(); 34 | var serviceProvider = c.BuildServiceProvider(); 35 | //var serviceProvider = new DefaultServiceProvider(); 36 | 37 | this.context = new ContextBuilder().AddServiceProvider(serviceProvider) 38 | .Accepts("application/json") 39 | .Build(); 40 | this.contextAccessor = new HttpContextAccessor(this.context); 41 | } 42 | 43 | [Fact(DisplayName = "Creating new instance with null context accessor should throw expected exception")] 44 | public void NullContextAccessorThrowsExpectedException() 45 | { 46 | var exception = ShouldThrowExtensions.ShouldThrow(() => new MiddlewarePipeline(null)); 47 | exception.ParamName.ShouldBe("httpContextAccessor"); 48 | } 49 | 50 | [Fact(DisplayName = "Use should add the first middleware successfully")] 51 | public void Use_FirstMiddleWare() 52 | { 53 | var instance = this.CreateInstance(); 54 | var middleware = new Mock(); 55 | instance.Use(middleware.Object); 56 | middleware.Verify(x => x.Next, Times.Never); 57 | } 58 | 59 | [Fact(DisplayName = "Use should add the second middleware successfully")] 60 | public void Use_SecondMiddleWare() 61 | { 62 | var instance = this.CreateInstance(); 63 | var middleware1 = new Mock(); 64 | var middleware2 = new Mock(); 65 | instance.Use(middleware1.Object); 66 | instance.Use(middleware2.Object); 67 | middleware1.VerifySet(x => x.Next = middleware2.Object, Times.Once); 68 | middleware2.VerifySet(x => x.Next = It.IsAny(), Times.Never); 69 | } 70 | 71 | [Fact(DisplayName = "RunAsync should return expected response when no middleware is configured")] 72 | public async Task RunAsync_NoMiddleware_NoCustomExceptionHandler() 73 | { 74 | var instance = this.CreateInstance(); 75 | var exception = await Should.ThrowAsync(() => instance.RunAsync()); 76 | } 77 | 78 | //[Fact(DisplayName = "RunAsync should return expected response when last middleware does not return a response")] 79 | //public async Task RunAsync_LastMiddlewareNoResponse_NoCustomExceptionHandler() 80 | //{ 81 | // var instance = this.CreateInstance(); 82 | // var middleware = new Mock(); 83 | // instance.Use(middleware.Object); 84 | // var result = await instance.RunAsync(); 85 | // var httpResponseResult = result.ShouldBeOfType(); 86 | // await httpResponseResult.ExecuteResultAsync(new ActionContext(this.context, new Microsoft.AspNetCore.Routing.RouteData(), new Microsoft.AspNetCore.Mvc.Abstractions.ActionDescriptor())); 87 | // this.context.Response.StatusCode.ShouldBe((int)HttpStatusCode.InternalServerError); 88 | //} 89 | 90 | [Fact(DisplayName = "RunAsync should return expected response when middleware throws BadRequestException")] 91 | public async Task RunAsync_MiddlewareThrowsBadRequestException_NoExceptionHandler() 92 | { 93 | var instance = this.CreateInstance(); 94 | var middleware = new Mock(); 95 | middleware.Setup(x => x.InvokeAsync(It.IsAny())) 96 | .ThrowsAsync(new BadRequestException("oh no")); 97 | instance.Use(middleware.Object); 98 | var exception = await Should.ThrowAsync(() => instance.RunAsync()); 99 | } 100 | 101 | [Fact(DisplayName = "RunAsync should return expected response")] 102 | public async Task RunAsync() 103 | { 104 | OkObjectResult middlewareResponse = new OkObjectResult("hello"); 105 | 106 | var instance = this.CreateInstance(); 107 | var middleware = new FunctionMiddleware((HttpContext context) => 108 | { 109 | var obj = new 110 | { 111 | message = "hello" 112 | }; 113 | 114 | return Task.FromResult(new OkObjectResult(obj)); 115 | }); 116 | 117 | instance.Use(middleware); 118 | var result = await instance.RunAsync(); 119 | var httpResponseResult = result.ShouldBeOfType(); 120 | await httpResponseResult.ExecuteResultAsync(new ActionContext() { HttpContext = this.context }); 121 | this.context.Response.StatusCode.ShouldBe((int)HttpStatusCode.OK); 122 | //var content = this.context.Response.result.Content.ReadAsStringAsync(); 123 | //content.ShouldBe("hello"); 124 | } 125 | 126 | [Fact(DisplayName = "New should create a new pipeline with the same properties as the original pipeline.")] 127 | public void New() 128 | { 129 | var instance = this.CreateInstance(); 130 | var result = instance.New().ShouldBeOfType(); 131 | 132 | var httpContextAccessorField = typeof(MiddlewarePipeline).GetField("httpContextAccessor", System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.NonPublic); 133 | var originalHttpContextAccessor = (IHttpContextAccessor)httpContextAccessorField.GetValue(instance); 134 | var newHttpContextAccessor = (IHttpContextAccessor)httpContextAccessorField.GetValue(result); 135 | originalHttpContextAccessor.ShouldBeSameAs(newHttpContextAccessor); 136 | } 137 | 138 | [Fact(DisplayName = "UseWhen should execute the branch when the predicate returns true")] 139 | public async Task UseWhen_ConditionTrue() 140 | { 141 | var instance = this.CreateInstance(); 142 | DummyLogger logger = new DummyLogger(); 143 | instance.UseWhen(ctx => true, 144 | p => p.Use(new LogMiddleware(logger, "a1")) 145 | .Use(new LogMiddleware(logger, "a2"))); 146 | instance.Use(new LogMiddleware(logger, "b1")) 147 | .Use(new LogMiddleware(logger, "b2")); 148 | 149 | var result = await instance.RunAsync(); 150 | logger.Data.Count.ShouldBe(4); 151 | logger.Data.ToArray().ShouldBe(new string[] { "a1", "a2", "b1", "b2" }); 152 | } 153 | 154 | [Fact(DisplayName = "UseWhen should not execute the branch when the predicate returns false")] 155 | public async Task UseWhen_ConditionFalse() 156 | { 157 | var instance = this.CreateInstance(); 158 | DummyLogger logger = new DummyLogger(); 159 | instance.UseWhen(ctx => false, 160 | p => p.Use(new LogMiddleware(logger, "a1")) 161 | .Use(new LogMiddleware(logger, "a2"))); 162 | instance.Use(new LogMiddleware(logger, "b1")) 163 | .Use(new LogMiddleware(logger, "b2")); 164 | 165 | var result = await instance.RunAsync(); 166 | logger.Data.Count.ShouldBe(2); 167 | logger.Data.ToArray().ShouldBe(new string[] { "b1", "b2" }); 168 | } 169 | 170 | [Fact(DisplayName = "MapWhen should execute the branch when the predicate returns true")] 171 | public async Task MapWhen_ConditionTrue() 172 | { 173 | var instance = this.CreateInstance(); 174 | DummyLogger logger = new DummyLogger(); 175 | instance.MapWhen(ctx => true, 176 | p => p.Use(new LogMiddleware(logger, "a1")) 177 | .Use(new LogMiddleware(logger, "a2"))); 178 | instance.Use(new LogMiddleware(logger, "b1")) 179 | .Use(new LogMiddleware(logger, "b2")); 180 | 181 | var result = await instance.RunAsync(); 182 | logger.Data.Count.ShouldBe(2); 183 | logger.Data.ToArray().ShouldBe(new string[] { "a1", "a2" }); 184 | } 185 | 186 | [Fact(DisplayName = "MapWhen should not execute the branch when the predicate returns false")] 187 | public async Task MapWhen_ConditionFalse() 188 | { 189 | var instance = this.CreateInstance(); 190 | DummyLogger logger = new DummyLogger(); 191 | instance.MapWhen(ctx => false, 192 | p => p.Use(new LogMiddleware(logger, "a1")) 193 | .Use(new LogMiddleware(logger, "a2"))); 194 | instance.Use(new LogMiddleware(logger, "b1")) 195 | .Use(new LogMiddleware(logger, "b2")); 196 | 197 | var result = await instance.RunAsync(); 198 | logger.Data.Count.ShouldBe(2); 199 | logger.Data.ToArray().ShouldBe(new string[] { "b1", "b2" }); 200 | } 201 | 202 | private MiddlewarePipeline CreateInstance() 203 | { 204 | return new MiddlewarePipeline(this.contextAccessor); 205 | } 206 | 207 | class DummyLogger 208 | { 209 | public DummyLogger() 210 | { 211 | this.Data = new List(); 212 | } 213 | 214 | public List Data { get; private set; } 215 | 216 | public void Log(string message) 217 | { 218 | this.Data.Add(message); 219 | } 220 | } 221 | 222 | class LogMiddleware : HttpMiddleware 223 | { 224 | private readonly DummyLogger logger; 225 | private readonly string id; 226 | 227 | public LogMiddleware(DummyLogger logger, string id) 228 | { 229 | this.logger = logger; 230 | this.id = id; 231 | } 232 | 233 | public override async Task InvokeAsync(HttpContext context) 234 | { 235 | this.logger.Log(this.id); 236 | 237 | if (this.Next != null) 238 | { 239 | await this.Next.InvokeAsync(context); 240 | } 241 | } 242 | } 243 | } 244 | } 245 | -------------------------------------------------------------------------------- /tests/Umamimolecule.AzureFunctionsMiddleware.Tests/QueryModelValidationMiddlewareTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.DataAnnotations; 4 | using System.Threading.Tasks; 5 | using Microsoft.AspNetCore.Http; 6 | using Microsoft.AspNetCore.Http.Internal; 7 | using Microsoft.AspNetCore.Mvc; 8 | using Microsoft.Extensions.DependencyInjection; 9 | using Microsoft.Extensions.Logging; 10 | using Microsoft.Extensions.ObjectPool; 11 | using Microsoft.Extensions.Primitives; 12 | using Moq; 13 | using Newtonsoft.Json; 14 | using Shouldly; 15 | using Xunit; 16 | 17 | namespace Umamimolecule.AzureFunctionsMiddleware.Tests 18 | { 19 | public class QueryModelValidationMiddlewareTests 20 | { 21 | [Fact(DisplayName = "InvokeAsync should correctly set the Query item property for the context")] 22 | public async Task InvokeAsync() 23 | { 24 | var queryParameters = new QueryCollection(new Dictionary 25 | { 26 | { "id", "1" }, 27 | { "description", "hello" } 28 | }); 29 | 30 | var context = this.CreateContext(queryParameters); 31 | 32 | var instance = this.CreateInstance(); 33 | await instance.InvokeAsync(context); 34 | 35 | context.Items["Query"].ShouldNotBeNull(); 36 | var model = context.Items["Query"].ShouldBeOfType(); 37 | model.Id.ShouldBe("1"); 38 | model.Description.ShouldBe("hello"); 39 | } 40 | 41 | [Fact(DisplayName = "InvokeAsync should correctly call the next middleware")] 42 | public async Task InvokeAsync_CallsNextMiddleware() 43 | { 44 | var queryParameters = new QueryCollection(new Dictionary 45 | { 46 | { "id", "1" }, 47 | { "description", "hello" } 48 | }); 49 | 50 | var context = this.CreateContext(queryParameters); 51 | 52 | var instance = this.CreateInstance(); 53 | 54 | var nextMiddleware = new Mock(); 55 | instance.Next = nextMiddleware.Object; 56 | await instance.InvokeAsync(context); 57 | 58 | nextMiddleware.Verify(x => x.InvokeAsync(It.IsAny()), Times.Once); 59 | } 60 | 61 | [Theory(DisplayName = "InvokeAsync should throw the expected exception when a require property is missing")] 62 | [InlineData(null)] 63 | [InlineData("")] 64 | public async Task InvokeAsync_Fail_MissingRequiredProperties(string id) 65 | { 66 | var queryParameters = new QueryCollection(new Dictionary 67 | { 68 | { "id", id }, 69 | { "description", "hello" } 70 | }); 71 | 72 | var context = this.CreateContext(queryParameters); 73 | 74 | var instance = this.CreateInstance(); 75 | await instance.InvokeAsync(context); 76 | context.Response.StatusCode.ShouldBe(400); 77 | context.Response.Body.Position = 0; 78 | var contents = context.Response.Body.ReadAsString(); 79 | var response = JsonConvert.DeserializeObject(contents); 80 | response.ShouldNotBeNull(); 81 | response.CorrelationId.ShouldNotBeNullOrWhiteSpace(); 82 | response.Error.ShouldNotBeNull(); 83 | response.Error.Code.ShouldBe("INVALID_QUERY_PARAMETERS"); 84 | response.Error.Message.ShouldBe("The Id field is required."); 85 | } 86 | 87 | [Theory(DisplayName = "InvokeAsync should throw the expected exception when a required property is missing for a custom response handler")] 88 | [InlineData(null)] 89 | [InlineData("")] 90 | public async Task InvokeAsync_Fail_MissingRequiredProperties_CustomResponseHandler(string id) 91 | { 92 | var queryParameters = new QueryCollection(new Dictionary 93 | { 94 | { "id", id }, 95 | { "description", "hello" } 96 | }); 97 | 98 | static IActionResult handler(HttpContext context, ModelValidationResult validationResult) 99 | { 100 | CustomErrorResponse response = new CustomErrorResponse() 101 | { 102 | CustomErrorMessage = "Custom error: The Id field is required", 103 | }; 104 | 105 | return new ObjectResult(response) 106 | { 107 | StatusCode = 409, 108 | }; 109 | } 110 | 111 | var context = this.CreateContext(queryParameters); 112 | var instance = this.CreateInstance(); 113 | instance.HandleValidationFailure = handler; 114 | 115 | await instance.InvokeAsync(context); 116 | 117 | context.Response.StatusCode.ShouldBe(409); 118 | context.Response.Body.Position = 0; 119 | var contents = context.Response.Body.ReadAsString(); 120 | var response = JsonConvert.DeserializeObject(contents); 121 | response.ShouldNotBeNull(); 122 | response.CustomErrorMessage.ShouldBe("Custom error: The Id field is required"); 123 | } 124 | 125 | private QueryModelValidationMiddleware CreateInstance() 126 | where T : new() 127 | { 128 | return new QueryModelValidationMiddleware(); 129 | } 130 | 131 | private HttpContext CreateContext(IQueryCollection queryParameters) 132 | { 133 | Logger logger = new Logger(); 134 | Mock loggerFactory = new Mock(); 135 | loggerFactory.Setup(x => x.CreateLogger(It.IsAny())) 136 | .Returns(logger); 137 | 138 | ServiceCollection services = new ServiceCollection(); 139 | services.AddMvcCore().AddJsonFormatters(); 140 | services.AddOptions(); 141 | 142 | services.AddTransient((IServiceProvider p) => loggerFactory.Object); 143 | services.AddSingleton(); 144 | var serviceProvider = services.BuildServiceProvider(); 145 | 146 | return new ContextBuilder().AddServiceProvider(serviceProvider) 147 | .AddQuery(queryParameters) 148 | .Build(); 149 | } 150 | 151 | class QueryParameters 152 | { 153 | [Required] 154 | public string Id { get; set; } 155 | 156 | public string Description { get; set; } 157 | } 158 | 159 | class CustomErrorResponse 160 | { 161 | public string CustomErrorMessage { get; set; } 162 | } 163 | 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /tests/Umamimolecule.AzureFunctionsMiddleware.Tests/RecursiveValidatorTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.ComponentModel.DataAnnotations; 4 | using System.IO; 5 | using System.Linq; 6 | using Microsoft.AspNetCore.Http; 7 | using Moq; 8 | using Newtonsoft.Json; 9 | using Shouldly; 10 | using Xunit; 11 | 12 | namespace Umamimolecule.AzureFunctionsMiddleware.Tests 13 | { 14 | public class BodyValidatorTests 15 | { 16 | [Fact(DisplayName = "Validation should succeed when supplied an empty instance without any required fields")] 17 | public void NoRequiredFieldsNoValuesSupplied() 18 | { 19 | List validationResults = new List(); 20 | var result = RecursiveValidator.TryValidateObject(new BodySimple(), validationResults, true); 21 | result.ShouldBeTrue(); 22 | validationResults.ShouldBeEmpty(); 23 | } 24 | 25 | [Fact(DisplayName = "Validation should succeed when supplied an instance without any required fields")] 26 | public void NoRequiredFieldsAllValuesSupplied() 27 | { 28 | BodySimple body = new BodySimple() 29 | { 30 | Param1 = "value1", 31 | Param2 = "value2" 32 | }; 33 | 34 | List validationResults = new List(); 35 | var result = RecursiveValidator.TryValidateObject(body, validationResults, true); 36 | result.ShouldBeTrue(); 37 | validationResults.ShouldBeEmpty(); 38 | } 39 | 40 | [Theory(DisplayName = "Validation should fail when supplied an instance missing required fields")] 41 | [InlineData(null)] 42 | [InlineData("")] 43 | public void RequiredFieldsNotSuppliedOrEmpty(string param1) 44 | { 45 | BodyWithRequiredFields body = new BodyWithRequiredFields() 46 | { 47 | Param1 = param1, 48 | Param2 = "value2" 49 | }; 50 | 51 | List validationResults = new List(); 52 | var result = RecursiveValidator.TryValidateObject(body, validationResults, true); 53 | result.ShouldBeFalse(); 54 | validationResults.ShouldContain(x => x.ErrorMessage.Contains("Param1 is required")); 55 | } 56 | 57 | [Fact(DisplayName = "Validation should succeed when supplied an instance containing all required fields")] 58 | public void RequiredFieldsAllValuesSupplied() 59 | { 60 | BodyWithRequiredFields body = new BodyWithRequiredFields() 61 | { 62 | Param1 = "value1", 63 | Param2 = "value2" 64 | }; 65 | 66 | List validationResults = new List(); 67 | var result = RecursiveValidator.TryValidateObject(body, validationResults, true); 68 | result.ShouldBeTrue(); 69 | validationResults.ShouldBeEmpty(); 70 | } 71 | 72 | [Fact(DisplayName = "Validation should fail when a nested collection field is required but is null")] 73 | public void NestedCollectionFieldMissingRequiredValue() 74 | { 75 | BodyWithNestedRequiredUrlField body = new BodyWithNestedRequiredUrlField() 76 | { 77 | UrlParam = new BodyWithUrlField() 78 | { 79 | Param1 = "https://google.com" 80 | }, 81 | Items = new BodyWithRequiredFields[] 82 | { 83 | new BodyWithRequiredFields() 84 | { 85 | Param1 = null, 86 | Param2 = "blah" 87 | } 88 | } 89 | }; 90 | 91 | List validationResults = new List(); 92 | var result = RecursiveValidator.TryValidateObject(body, validationResults, true); 93 | result.ShouldBeFalse(); 94 | validationResults.ShouldContain(x => x.MemberNames.Contains("Items[0].Param1") && x.ErrorMessage.Contains("Param1 is required")); 95 | } 96 | 97 | [Fact(DisplayName = "Validation should fail when a nested object field is required but is null")] 98 | public void NestedChildFieldMissingRequiredValue() 99 | { 100 | BodyWithNestedRequiredUrlField body = new BodyWithNestedRequiredUrlField() 101 | { 102 | UrlParam = new BodyWithUrlField() 103 | { 104 | Param1 = null 105 | } 106 | }; 107 | 108 | List validationResults = new List(); 109 | var result = RecursiveValidator.TryValidateObject(body, validationResults, true); 110 | result.ShouldBeFalse(); 111 | validationResults.ShouldContain(x => x.MemberNames.Contains("UrlParam.Param1") && x.ErrorMessage.Contains("Param1 is required")); 112 | } 113 | 114 | [Fact(DisplayName = "Validation should fail when required readonly properties are null")] 115 | public void ShouldValidateReadOnlyProperties() 116 | { 117 | ClassWithRequiredReadonlyProperty body = new ClassWithRequiredReadonlyProperty() 118 | { 119 | Child = new ClassWithRequiredReadonlyProperty() 120 | }; 121 | 122 | List validationResults = new List(); 123 | var result = RecursiveValidator.TryValidateObject(body, validationResults, true); 124 | result.ShouldBeFalse(); 125 | validationResults.ShouldContain(x => x.MemberNames.Contains("ReadOnlyProperty") && x.ErrorMessage.Contains("ReadOnlyProperty field is required")); 126 | validationResults.ShouldContain(x => x.MemberNames.Contains("Child.ReadOnlyProperty") && x.ErrorMessage.Contains("ReadOnlyProperty field is required")); 127 | } 128 | 129 | [Fact(DisplayName = "Should throw the expected exception when attempting to validate a null instance.")] 130 | public void ThrowsArgumentNullException() 131 | { 132 | List validationResults = new List(); 133 | var exception = ShouldThrowExtensions.ShouldThrow(() => RecursiveValidator.TryValidateObject(null, validationResults, true)); 134 | exception.ParamName.ShouldBe("instance"); 135 | } 136 | 137 | private BodyModelValidationMiddleware CreateInstance() where T : new() 138 | { 139 | return new BodyModelValidationMiddleware(); 140 | } 141 | 142 | private Mock CreateContext(T body) 143 | { 144 | Mock context = new Mock(); 145 | 146 | MemoryStream stream = new MemoryStream(); 147 | StreamWriter writer = new StreamWriter(stream); 148 | writer.Write(JsonConvert.SerializeObject(body)); 149 | writer.Flush(); 150 | stream.Position = 0; 151 | 152 | Mock request = new Mock(); 153 | request.Setup(x => x.Body) 154 | .Returns(stream); 155 | 156 | context.Setup(x => x.Request) 157 | .Returns(request.Object); 158 | 159 | return context; 160 | } 161 | 162 | class BodySimple 163 | { 164 | public string Param1 { get; set; } 165 | 166 | public string Param2 { get; set; } 167 | } 168 | 169 | class BodyWithRequiredFields 170 | { 171 | [Required(AllowEmptyStrings = false, ErrorMessage = "Param1 is required")] 172 | public string Param1 { get; set; } 173 | 174 | public string Param2 { get; set; } 175 | } 176 | 177 | class BodyWithUrlField 178 | { 179 | [Required(ErrorMessage = "Param1 is required")] 180 | [UrlValidation(ErrorMessage = "Should be a valid URL")] 181 | public string Param1 { get; set; } 182 | } 183 | 184 | class BodyWithNestedRequiredUrlField 185 | { 186 | [Required(ErrorMessage = "UrlParam is required")] 187 | public BodyWithUrlField UrlParam { get; set; } 188 | 189 | public BodyWithUrlField NonRequiredUrlParam { get; set; } 190 | 191 | public IEnumerable Items { get; set; } 192 | } 193 | 194 | class UrlValidationAttribute : ValidationAttribute 195 | { 196 | /// 197 | /// Determines whether the specified value of the object is valid. 198 | /// 199 | /// The value of the object to validate. 200 | /// true if the specified value is valid; otherwise, false. 201 | public override bool IsValid(object value) 202 | { 203 | if (value == null) 204 | { 205 | return true; 206 | } 207 | 208 | return Uri.TryCreate(value.ToString(), UriKind.Absolute, out Uri _); 209 | } 210 | } 211 | 212 | class ClassWithRequiredReadonlyProperty 213 | { 214 | [Required] 215 | public string ReadOnlyProperty => null; 216 | 217 | public ClassWithRequiredReadonlyProperty Child { get; set; } 218 | } 219 | } 220 | } 221 | -------------------------------------------------------------------------------- /tests/Umamimolecule.AzureFunctionsMiddleware.Tests/StreamExtensions.cs: -------------------------------------------------------------------------------- 1 | using System.IO; 2 | 3 | namespace Umamimolecule.AzureFunctionsMiddleware.Tests 4 | { 5 | public static class StreamExtensions 6 | { 7 | public static string ReadAsString(this Stream stream) 8 | { 9 | StreamReader reader = new StreamReader(stream); 10 | var result = reader.ReadToEnd(); 11 | stream.Seek(0, SeekOrigin.Begin); 12 | return result; 13 | } 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /tests/Umamimolecule.AzureFunctionsMiddleware.Tests/Umamimolecule.AzureFunctionsMiddleware.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netcoreapp3.1 5 | false 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | all 17 | runtime; build; native; contentfiles; analyzers; buildtransitive 18 | 19 | 20 | all 21 | runtime; build; native; contentfiles; analyzers; buildtransitive 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | --------------------------------------------------------------------------------