├── .gitignore ├── BuildWebAPIs.v1.json ├── BuildWebAPIs.v2.json ├── BuildingWebAPIsWorkshop.sln ├── Extensions ├── Microsoft.AspNetCore.HealthChecks │ ├── Guard.cs │ ├── HealthCheckMiddleware.cs │ ├── HealthCheckStartupFilter.cs │ ├── HealthCheckWebHostBuilderExtension.cs │ ├── HealthCheckWebHostExtensions.cs │ └── Microsoft.AspNetCore.HealthChecks.csproj └── Microsoft.Extensions.HealthChecks │ ├── CachedHealthCheck.cs │ ├── CachedHealthCheckExtensions.cs │ ├── CheckStatus.cs │ ├── Checks │ ├── AddCheck.cs │ ├── NumericChecks.cs │ ├── SystemChecks.cs │ └── UrlChecks.cs │ ├── CompositeHealthCheckResult.cs │ ├── HealthCheck.cs │ ├── HealthCheckBuilder.cs │ ├── HealthCheckGroup.cs │ ├── HealthCheckResult.cs │ ├── HealthCheckResults.cs │ ├── HealthCheckService.cs │ ├── HealthCheckServiceCollectionExtensions.cs │ ├── IHealthCheck.cs │ ├── IHealthCheckResult.cs │ ├── IHealthCheckService.cs │ ├── Internal │ ├── Guard.cs │ └── UrlChecker.cs │ └── Microsoft.Extensions.HealthChecks.csproj ├── GenealogyClientSdk.sln ├── GenealogyWebAPI.ClientSdk ├── FamilyName.cs ├── FamilyNameExtensions.cs ├── GenealogyAPI.cs ├── GenealogyAPI.partial.cs ├── GenealogyWebAPI.ClientSdk.csproj ├── IFamilyName.cs ├── IGenealogyAPI.cs └── Models │ └── FamilyProfile.cs ├── GenealogyWebAPI.IntegrationTests ├── GenealogyWebAPI.IntegrationTests.csproj ├── MockGenderizeClient.cs ├── ServiceContractIntegrationTests.cs └── appsettings.json ├── GenealogyWebAPI ├── Connected Services │ └── Application Insights │ │ └── ConnectedService.json ├── Controllers │ ├── FamilyNameV1Controller.cs │ ├── FamilyNameV2Controller.cs │ └── HomeController.cs ├── Features.cs ├── GenderizeApiOptions.cs ├── GenealogyWebAPI.csproj ├── Model │ ├── FamilyProfile.cs │ └── Gender.cs ├── Program.cs ├── Proxies │ ├── GenderizeResult.cs │ └── IGenderizeClient.cs ├── Startup.cs ├── appsettings.Development.json └── appsettings.json ├── GenerateClientSdk.ps1 └── Readme.md /.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 cache/options directory 28 | .vs/ 29 | .vscode/ 30 | 31 | # Uncomment if you have tasks that create the project's static files in wwwroot 32 | #wwwroot/ 33 | 34 | # MSTest test Results 35 | [Tt]est[Rr]esult*/ 36 | [Bb]uild[Ll]og.* 37 | 38 | # NUNIT 39 | *.VisualState.xml 40 | TestResult.xml 41 | 42 | # Build Results of an ATL Project 43 | [Dd]ebugPS/ 44 | [Rr]eleasePS/ 45 | dlldata.c 46 | 47 | # .NET Core 48 | project.lock.json 49 | project.fragment.lock.json 50 | artifacts/ 51 | **/Properties/launchSettings.json 52 | 53 | *_i.c 54 | *_p.c 55 | *_i.h 56 | *.ilk 57 | *.meta 58 | *.obj 59 | *.pch 60 | *.pdb 61 | *.pgc 62 | *.pgd 63 | *.rsp 64 | *.sbr 65 | *.tlb 66 | *.tli 67 | *.tlh 68 | *.tmp 69 | *.tmp_proj 70 | *.log 71 | *.vspscc 72 | *.vssscc 73 | .builds 74 | *.pidb 75 | *.svclog 76 | *.scc 77 | 78 | # Chutzpah Test files 79 | _Chutzpah* 80 | 81 | # Visual C++ cache files 82 | ipch/ 83 | *.aps 84 | *.ncb 85 | *.opendb 86 | *.opensdf 87 | *.sdf 88 | *.cachefile 89 | *.VC.db 90 | *.VC.VC.opendb 91 | 92 | # Visual Studio profiler 93 | *.psess 94 | *.vsp 95 | *.vspx 96 | *.sap 97 | 98 | # TFS 2012 Local Workspace 99 | $tf/ 100 | 101 | # Guidance Automation Toolkit 102 | *.gpState 103 | 104 | # ReSharper is a .NET coding add-in 105 | _ReSharper*/ 106 | *.[Rr]e[Ss]harper 107 | *.DotSettings.user 108 | 109 | # JustCode is a .NET coding add-in 110 | .JustCode 111 | 112 | # TeamCity is a build add-in 113 | _TeamCity* 114 | 115 | # DotCover is a Code Coverage Tool 116 | *.dotCover 117 | 118 | # Visual Studio code coverage results 119 | *.coverage 120 | *.coveragexml 121 | 122 | # NCrunch 123 | _NCrunch_* 124 | .*crunch*.local.xml 125 | nCrunchTemp_* 126 | 127 | # MightyMoose 128 | *.mm.* 129 | AutoTest.Net/ 130 | 131 | # Web workbench (sass) 132 | .sass-cache/ 133 | 134 | # Installshield output folder 135 | [Ee]xpress/ 136 | 137 | # DocProject is a documentation generator add-in 138 | DocProject/buildhelp/ 139 | DocProject/Help/*.HxT 140 | DocProject/Help/*.HxC 141 | DocProject/Help/*.hhc 142 | DocProject/Help/*.hhk 143 | DocProject/Help/*.hhp 144 | DocProject/Help/Html2 145 | DocProject/Help/html 146 | 147 | # Click-Once directory 148 | publish/ 149 | 150 | # Publish Web Output 151 | *.[Pp]ublish.xml 152 | *.azurePubxml 153 | # TODO: Comment the next line if you want to checkin your web deploy settings 154 | # but database connection strings (with potential passwords) will be unencrypted 155 | *.pubxml 156 | *.publishproj 157 | 158 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 159 | # checkin your Azure Web App publish settings, but sensitive information contained 160 | # in these scripts will be unencrypted 161 | PublishScripts/ 162 | 163 | # NuGet Packages 164 | *.nupkg 165 | # The packages folder can be ignored because of Package Restore 166 | **/packages/* 167 | # except build/, which is used as an MSBuild target. 168 | !**/packages/build/ 169 | # Uncomment if necessary however generally it will be regenerated when needed 170 | #!**/packages/repositories.config 171 | # NuGet v3's project.json files produces more ignorable files 172 | *.nuget.props 173 | *.nuget.targets 174 | 175 | # Microsoft Azure Build Output 176 | csx/ 177 | *.build.csdef 178 | 179 | # Microsoft Azure Emulator 180 | ecf/ 181 | rcf/ 182 | 183 | # Windows Store app package directories and files 184 | AppPackages/ 185 | BundleArtifacts/ 186 | Package.StoreAssociation.xml 187 | _pkginfo.txt 188 | 189 | # Visual Studio cache files 190 | # files ending in .cache can be ignored 191 | *.[Cc]ache 192 | # but keep track of directories ending in .cache 193 | !*.[Cc]ache/ 194 | 195 | # Others 196 | ClientBin/ 197 | ~$* 198 | *~ 199 | *.dbmdl 200 | *.dbproj.schemaview 201 | *.jfm 202 | *.pfx 203 | *.publishsettings 204 | orleans.codegen.cs 205 | 206 | # Since there are multiple workflows, uncomment next line to ignore bower_components 207 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 208 | #bower_components/ 209 | 210 | # RIA/Silverlight projects 211 | Generated_Code/ 212 | 213 | # Backup & report files from converting an old project file 214 | # to a newer Visual Studio version. Backup files are not needed, 215 | # because we have git ;-) 216 | _UpgradeReport_Files/ 217 | Backup*/ 218 | UpgradeLog*.XML 219 | UpgradeLog*.htm 220 | 221 | # SQL Server files 222 | *.mdf 223 | *.ldf 224 | *.ndf 225 | 226 | # Business Intelligence projects 227 | *.rdl.data 228 | *.bim.layout 229 | *.bim_*.settings 230 | 231 | # Microsoft Fakes 232 | FakesAssemblies/ 233 | 234 | # GhostDoc plugin setting file 235 | *.GhostDoc.xml 236 | 237 | # Node.js Tools for Visual Studio 238 | .ntvs_analysis.dat 239 | node_modules/ 240 | 241 | # Typescript v1 declaration files 242 | typings/ 243 | 244 | # Visual Studio 6 build log 245 | *.plg 246 | 247 | # Visual Studio 6 workspace options file 248 | *.opt 249 | 250 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 251 | *.vbw 252 | 253 | # Visual Studio LightSwitch build output 254 | **/*.HTMLClient/GeneratedArtifacts 255 | **/*.DesktopClient/GeneratedArtifacts 256 | **/*.DesktopClient/ModelManifest.xml 257 | **/*.Server/GeneratedArtifacts 258 | **/*.Server/ModelManifest.xml 259 | _Pvt_Extensions 260 | 261 | # Paket dependency manager 262 | .paket/paket.exe 263 | paket-files/ 264 | 265 | # FAKE - F# Make 266 | .fake/ 267 | 268 | # JetBrains Rider 269 | .idea/ 270 | *.sln.iml 271 | 272 | # CodeRush 273 | .cr/ 274 | 275 | # Python Tools for Visual Studio (PTVS) 276 | __pycache__/ 277 | *.pyc 278 | 279 | # Cake - Uncomment if you are using it 280 | # tools/** 281 | # !tools/packages.config 282 | 283 | # Telerik's JustMock configuration file 284 | *.jmconfig 285 | 286 | # BizTalk build output 287 | *.btp.cs 288 | *.btm.cs 289 | *.odx.cs 290 | *.xsd.cs 291 | /*.pdf 292 | /*.mp4 293 | /Alex_Thissen_sessionnotes.txt 294 | -------------------------------------------------------------------------------- /BuildWebAPIs.v1.json: -------------------------------------------------------------------------------- 1 | { 2 | "x-generator": "NSwag v11.17.0.0 (NJsonSchema v9.10.42.0 (Newtonsoft.Json v11.0.0.0))", 3 | "swagger": "2.0", 4 | "info": { 5 | "title": "Genealogy API", 6 | "description": "Building Web APIs Workshop Demo Web API", 7 | "version": "1.0" 8 | }, 9 | "host": "localhost:44383", 10 | "basePath": "/", 11 | "schemes": [ 12 | "https" 13 | ], 14 | "consumes": [ 15 | "application/json" 16 | ], 17 | "produces": [ 18 | "application/json" 19 | ], 20 | "paths": { 21 | "/api/v1.0/FamilyName/{name}": { 22 | "get": { 23 | "tags": [ 24 | "FamilyName" 25 | ], 26 | "summary": "Retrieve profile of person based on name.", 27 | "operationId": "FamilyName_Get", 28 | "parameters": [ 29 | { 30 | "type": "string", 31 | "name": "name", 32 | "in": "path", 33 | "required": true, 34 | "description": "Name of person.", 35 | "x-nullable": false 36 | } 37 | ], 38 | "responses": { 39 | "200": { 40 | "x-nullable": true, 41 | "description": "Detailed information regarding profile.", 42 | "schema": { 43 | "type": "string" 44 | } 45 | }, 46 | "400": { 47 | "description": "" 48 | } 49 | } 50 | } 51 | } 52 | }, 53 | "definitions": { 54 | "FamilyProfile": { 55 | "type": "object", 56 | "additionalProperties": false, 57 | "required": [ 58 | "gender" 59 | ], 60 | "properties": { 61 | "name": { 62 | "type": "string" 63 | }, 64 | "gender": { 65 | "$ref": "#/definitions/Gender" 66 | } 67 | } 68 | }, 69 | "Gender": { 70 | "type": "string", 71 | "description": "", 72 | "x-enumNames": [ 73 | "Male", 74 | "Female", 75 | "Unknown" 76 | ], 77 | "enum": [ 78 | "Male", 79 | "Female", 80 | "Unknown" 81 | ] 82 | } 83 | } 84 | } -------------------------------------------------------------------------------- /BuildWebAPIs.v2.json: -------------------------------------------------------------------------------- 1 | { 2 | "x-generator": "NSwag v11.17.0.0 (NJsonSchema v9.10.42.0 (Newtonsoft.Json v11.0.0.0))", 3 | "swagger": "2.0", 4 | "info": { 5 | "title": "Genealogy API", 6 | "description": "Building Web APIs Workshop Demo Web API", 7 | "version": "2.0" 8 | }, 9 | "host": "localhost:44383", 10 | "basePath": "/", 11 | "schemes": [ 12 | "https" 13 | ], 14 | "consumes": [ 15 | "application/json" 16 | ], 17 | "produces": [ 18 | "application/json" 19 | ], 20 | "paths": { 21 | "/api/v2.0/FamilyName/{name}": { 22 | "get": { 23 | "tags": [ 24 | "FamilyName" 25 | ], 26 | "summary": "Retrieve profile of person based on name.", 27 | "operationId": "FamilyName_Get", 28 | "parameters": [ 29 | { 30 | "type": "string", 31 | "name": "name", 32 | "in": "path", 33 | "required": true, 34 | "description": "Name of person.", 35 | "x-nullable": false 36 | } 37 | ], 38 | "responses": { 39 | "200": { 40 | "x-nullable": true, 41 | "description": "Detailed information regarding profile.", 42 | "schema": { 43 | "$ref": "#/definitions/FamilyProfile" 44 | } 45 | }, 46 | "400": { 47 | "description": "" 48 | } 49 | } 50 | } 51 | } 52 | }, 53 | "definitions": { 54 | "FamilyProfile": { 55 | "type": "object", 56 | "additionalProperties": false, 57 | "required": [ 58 | "gender" 59 | ], 60 | "properties": { 61 | "name": { 62 | "type": "string" 63 | }, 64 | "gender": { 65 | "$ref": "#/definitions/Gender" 66 | } 67 | } 68 | }, 69 | "Gender": { 70 | "type": "string", 71 | "description": "", 72 | "x-enumNames": [ 73 | "Male", 74 | "Female", 75 | "Unknown" 76 | ], 77 | "enum": [ 78 | "Male", 79 | "Female", 80 | "Unknown" 81 | ] 82 | } 83 | } 84 | } -------------------------------------------------------------------------------- /BuildingWebAPIsWorkshop.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.27428.2037 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GenealogyWebAPI", "GenealogyWebAPI\GenealogyWebAPI.csproj", "{F3FA8415-803E-48D2-82B0-5CE7F54A2442}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Extensions", "Extensions", "{F3A33C95-9387-4D71-AB9E-18521C94830F}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.HealthChecks", "Extensions\Microsoft.AspNetCore.HealthChecks\Microsoft.AspNetCore.HealthChecks.csproj", "{847945AB-F659-4985-998A-86AF7BAD490D}" 11 | EndProject 12 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Extensions.HealthChecks", "Extensions\Microsoft.Extensions.HealthChecks\Microsoft.Extensions.HealthChecks.csproj", "{BC72527D-5E98-446F-9522-8E9A33E7D371}" 13 | EndProject 14 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{85B85411-5948-4F58-AB38-2B7B58C1CF8A}" 15 | ProjectSection(SolutionItems) = preProject 16 | BuildWebAPIs.v1.json = BuildWebAPIs.v1.json 17 | BuildWebAPIs.v2.json = BuildWebAPIs.v2.json 18 | GenerateClientSdk.ps1 = GenerateClientSdk.ps1 19 | Readme.md = Readme.md 20 | EndProjectSection 21 | EndProject 22 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GenealogyWebAPI.ClientSdk", "GenealogyWebAPI.ClientSdk\GenealogyWebAPI.ClientSdk.csproj", "{7DD281B8-E70D-46A5-9488-16A76C3060F5}" 23 | EndProject 24 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "GenealogyWebAPI.IntegrationTests", "GenealogyWebAPI.IntegrationTests\GenealogyWebAPI.IntegrationTests.csproj", "{14011625-2270-4BB5-934A-ECFB075DF450}" 25 | EndProject 26 | Global 27 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 28 | Debug|Any CPU = Debug|Any CPU 29 | Release|Any CPU = Release|Any CPU 30 | EndGlobalSection 31 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 32 | {F3FA8415-803E-48D2-82B0-5CE7F54A2442}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 33 | {F3FA8415-803E-48D2-82B0-5CE7F54A2442}.Debug|Any CPU.Build.0 = Debug|Any CPU 34 | {F3FA8415-803E-48D2-82B0-5CE7F54A2442}.Release|Any CPU.ActiveCfg = Release|Any CPU 35 | {F3FA8415-803E-48D2-82B0-5CE7F54A2442}.Release|Any CPU.Build.0 = Release|Any CPU 36 | {847945AB-F659-4985-998A-86AF7BAD490D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 37 | {847945AB-F659-4985-998A-86AF7BAD490D}.Debug|Any CPU.Build.0 = Debug|Any CPU 38 | {847945AB-F659-4985-998A-86AF7BAD490D}.Release|Any CPU.ActiveCfg = Release|Any CPU 39 | {847945AB-F659-4985-998A-86AF7BAD490D}.Release|Any CPU.Build.0 = Release|Any CPU 40 | {BC72527D-5E98-446F-9522-8E9A33E7D371}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 41 | {BC72527D-5E98-446F-9522-8E9A33E7D371}.Debug|Any CPU.Build.0 = Debug|Any CPU 42 | {BC72527D-5E98-446F-9522-8E9A33E7D371}.Release|Any CPU.ActiveCfg = Release|Any CPU 43 | {BC72527D-5E98-446F-9522-8E9A33E7D371}.Release|Any CPU.Build.0 = Release|Any CPU 44 | {7DD281B8-E70D-46A5-9488-16A76C3060F5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 45 | {7DD281B8-E70D-46A5-9488-16A76C3060F5}.Debug|Any CPU.Build.0 = Debug|Any CPU 46 | {7DD281B8-E70D-46A5-9488-16A76C3060F5}.Release|Any CPU.ActiveCfg = Release|Any CPU 47 | {7DD281B8-E70D-46A5-9488-16A76C3060F5}.Release|Any CPU.Build.0 = Release|Any CPU 48 | {14011625-2270-4BB5-934A-ECFB075DF450}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 49 | {14011625-2270-4BB5-934A-ECFB075DF450}.Debug|Any CPU.Build.0 = Debug|Any CPU 50 | {14011625-2270-4BB5-934A-ECFB075DF450}.Release|Any CPU.ActiveCfg = Release|Any CPU 51 | {14011625-2270-4BB5-934A-ECFB075DF450}.Release|Any CPU.Build.0 = Release|Any CPU 52 | EndGlobalSection 53 | GlobalSection(SolutionProperties) = preSolution 54 | HideSolutionNode = FALSE 55 | EndGlobalSection 56 | GlobalSection(NestedProjects) = preSolution 57 | {847945AB-F659-4985-998A-86AF7BAD490D} = {F3A33C95-9387-4D71-AB9E-18521C94830F} 58 | {BC72527D-5E98-446F-9522-8E9A33E7D371} = {F3A33C95-9387-4D71-AB9E-18521C94830F} 59 | EndGlobalSection 60 | GlobalSection(ExtensibilityGlobals) = postSolution 61 | SolutionGuid = {F43B3905-DA84-4B0B-8760-B49A226CBDA9} 62 | EndGlobalSection 63 | EndGlobal 64 | -------------------------------------------------------------------------------- /Extensions/Microsoft.AspNetCore.HealthChecks/Guard.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | 7 | static class Guard 8 | { 9 | public static void ArgumentNotNull(string argumentName, object value) 10 | { 11 | if (value == null) 12 | { 13 | throw new ArgumentNullException(argumentName); 14 | } 15 | } 16 | 17 | public static void ArgumentNotNullOrEmpty(string argumentName, string value) 18 | { 19 | if (value == null) 20 | { 21 | throw new ArgumentNullException(argumentName); 22 | } 23 | if (string.IsNullOrEmpty(value)) 24 | { 25 | throw new ArgumentException("Value cannot be an empty string.", argumentName); 26 | } 27 | } 28 | 29 | // Use IReadOnlyCollection instead of IEnumerable to discourage double enumeration 30 | public static void ArgumentNotNullOrEmpty(string argumentName, IReadOnlyCollection items) 31 | { 32 | if (items == null) 33 | { 34 | throw new ArgumentNullException(argumentName); 35 | } 36 | if (items.Count == 0) 37 | { 38 | throw new ArgumentException("Collection must contain at least one item.", argumentName); 39 | } 40 | } 41 | 42 | public static void ArgumentValid(bool valid, string argumentName, string exceptionMessage) 43 | { 44 | if (!valid) 45 | { 46 | throw new ArgumentException(exceptionMessage, argumentName); 47 | } 48 | } 49 | 50 | public static void OperationValid(bool valid, string exceptionMessage) 51 | { 52 | if (!valid) 53 | { 54 | throw new InvalidOperationException(exceptionMessage); 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Extensions/Microsoft.AspNetCore.HealthChecks/HealthCheckMiddleware.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.Linq; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | using Microsoft.AspNetCore.Http; 9 | using Microsoft.AspNetCore.Http.Features; 10 | using Microsoft.Extensions.HealthChecks; 11 | using Newtonsoft.Json; 12 | 13 | namespace Microsoft.AspNetCore.HealthChecks 14 | { 15 | public class HealthCheckMiddleware 16 | { 17 | private readonly RequestDelegate _next; 18 | private readonly string _path; 19 | private readonly int? _port; 20 | private readonly IHealthCheckService _service; 21 | private readonly TimeSpan _timeout; 22 | 23 | public HealthCheckMiddleware(RequestDelegate next, IHealthCheckService service, int port, TimeSpan timeout) 24 | { 25 | _port = port; 26 | _service = service; 27 | _next = next; 28 | _timeout = timeout; 29 | } 30 | 31 | public HealthCheckMiddleware(RequestDelegate next, IHealthCheckService service, string path, TimeSpan timeout) 32 | { 33 | _path = path; 34 | _service = service; 35 | _next = next; 36 | _timeout = timeout; 37 | } 38 | 39 | public async Task Invoke(HttpContext context) 40 | { 41 | if (IsHealthCheckRequest(context)) 42 | { 43 | var timeoutTokenSource = new CancellationTokenSource(_timeout); 44 | var result = await _service.CheckHealthAsync(timeoutTokenSource.Token); 45 | var status = result.CheckStatus; 46 | 47 | if (status != CheckStatus.Healthy) 48 | context.Response.StatusCode = 503; 49 | 50 | context.Response.Headers.Add("content-type", "application/json"); 51 | await context.Response.WriteAsync(JsonConvert.SerializeObject(new { status = status.ToString() })); 52 | 53 | return; 54 | } 55 | else 56 | { 57 | await _next.Invoke(context); 58 | } 59 | } 60 | 61 | private bool IsHealthCheckRequest(HttpContext context) 62 | { 63 | if (_port.HasValue) 64 | { 65 | var connInfo = context.Features.Get(); 66 | if (connInfo.LocalPort == _port) 67 | return true; 68 | } 69 | 70 | if (context.Request.Path == _path) 71 | { 72 | return true; 73 | } 74 | 75 | return false; 76 | } 77 | } 78 | } -------------------------------------------------------------------------------- /Extensions/Microsoft.AspNetCore.HealthChecks/HealthCheckStartupFilter.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using Microsoft.AspNetCore.Builder; 6 | using Microsoft.AspNetCore.Hosting; 7 | 8 | namespace Microsoft.AspNetCore.HealthChecks 9 | { 10 | public class HealthCheckStartupFilter : IStartupFilter 11 | { 12 | private string _path; 13 | private int? _port; 14 | private TimeSpan _timeout; 15 | 16 | public HealthCheckStartupFilter(int port, TimeSpan timeout) 17 | { 18 | _port = port; 19 | _timeout = timeout; 20 | } 21 | 22 | public HealthCheckStartupFilter(string path, TimeSpan timeout) 23 | { 24 | _path = path; 25 | _timeout = timeout; 26 | } 27 | 28 | public Action Configure(Action next) 29 | { 30 | return app => 31 | { 32 | if (_port.HasValue) 33 | { 34 | app.UseMiddleware(_port, _timeout); 35 | } 36 | else 37 | { 38 | app.UseMiddleware(_path, _timeout); 39 | } 40 | 41 | next(app); 42 | }; 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /Extensions/Microsoft.AspNetCore.HealthChecks/HealthCheckWebHostBuilderExtension.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using Microsoft.AspNetCore.HealthChecks; 6 | using Microsoft.Extensions.DependencyInjection; 7 | 8 | namespace Microsoft.AspNetCore.Hosting 9 | { 10 | public static class HealthCheckWebHostBuilderExtension 11 | { 12 | public static readonly TimeSpan DefaultTimeout = TimeSpan.FromSeconds(10); 13 | 14 | public static IWebHostBuilder UseHealthChecks(this IWebHostBuilder builder, int port) 15 | => UseHealthChecks(builder, port, DefaultTimeout); 16 | 17 | public static IWebHostBuilder UseHealthChecks(this IWebHostBuilder builder, int port, TimeSpan timeout) 18 | { 19 | Guard.ArgumentValid(port > 0 && port < 65536, nameof(port), "Port must be a value between 1 and 65535."); 20 | Guard.ArgumentValid(timeout > TimeSpan.Zero, nameof(timeout), "Health check timeout must be a positive time span."); 21 | 22 | builder.ConfigureServices(services => 23 | { 24 | var existingUrl = builder.GetSetting(WebHostDefaults.ServerUrlsKey); 25 | builder.UseSetting(WebHostDefaults.ServerUrlsKey, $"{existingUrl};http://localhost:{port}"); 26 | 27 | services.AddSingleton(new HealthCheckStartupFilter(port, timeout)); 28 | }); 29 | return builder; 30 | } 31 | 32 | public static IWebHostBuilder UseHealthChecks(this IWebHostBuilder builder, string path) 33 | => UseHealthChecks(builder, path, DefaultTimeout); 34 | 35 | public static IWebHostBuilder UseHealthChecks(this IWebHostBuilder builder, string path, TimeSpan timeout) 36 | { 37 | Guard.ArgumentNotNull(nameof(path), path); 38 | // REVIEW: Is there a better URL path validator somewhere? 39 | Guard.ArgumentValid(!path.Contains("?"), nameof(path), "Path cannot contain query string values."); 40 | Guard.ArgumentValid(path.StartsWith("/"), nameof(path), "Path should start with '/'."); 41 | Guard.ArgumentValid(timeout > TimeSpan.Zero, nameof(timeout), "Health check timeout must be a positive time span."); 42 | 43 | builder.ConfigureServices(services => services.AddSingleton(new HealthCheckStartupFilter(path, timeout))); 44 | return builder; 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /Extensions/Microsoft.AspNetCore.HealthChecks/HealthCheckWebHostExtensions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using Microsoft.Extensions.HealthChecks; 6 | 7 | namespace Microsoft.AspNetCore.Hosting 8 | { 9 | public static class HealthCheckWebHostExtensions 10 | { 11 | private const int DEFAULT_TIMEOUT_SECONDS = 300; 12 | 13 | public static void RunWhenHealthy(this IWebHost webHost) 14 | { 15 | webHost.RunWhenHealthy(TimeSpan.FromSeconds(DEFAULT_TIMEOUT_SECONDS)); 16 | } 17 | 18 | public static void RunWhenHealthy(this IWebHost webHost, TimeSpan timeout) 19 | { 20 | var healthChecks = webHost.Services.GetService(typeof(IHealthCheckService)) as IHealthCheckService; 21 | 22 | var loops = 0; 23 | do 24 | { 25 | var checkResult = healthChecks.CheckHealthAsync().Result; 26 | if (checkResult.CheckStatus == CheckStatus.Healthy) 27 | { 28 | webHost.Run(); 29 | break; 30 | } 31 | 32 | System.Threading.Thread.Sleep(1000); 33 | loops++; 34 | 35 | } while (loops < timeout.TotalSeconds); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /Extensions/Microsoft.AspNetCore.HealthChecks/Microsoft.AspNetCore.HealthChecks.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netcoreapp2.1 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /Extensions/Microsoft.Extensions.HealthChecks/CachedHealthCheck.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.Reflection; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | using Microsoft.Extensions.DependencyInjection; 9 | 10 | namespace Microsoft.Extensions.HealthChecks 11 | { 12 | public abstract class CachedHealthCheck 13 | { 14 | private static readonly TypeInfo HealthCheckTypeInfo = typeof(IHealthCheck).GetTypeInfo(); 15 | 16 | private volatile int _writerCount; 17 | 18 | public CachedHealthCheck(string name, TimeSpan cacheDuration) 19 | { 20 | Guard.ArgumentNotNullOrEmpty(nameof(name), name); 21 | Guard.ArgumentValid(cacheDuration.TotalMilliseconds >= 0, nameof(cacheDuration), "Cache duration must be zero (disabled) or greater than zero."); 22 | 23 | Name = name; 24 | CacheDuration = cacheDuration; 25 | } 26 | 27 | public IHealthCheckResult CachedResult { get; internal set; } 28 | 29 | public TimeSpan CacheDuration { get; } 30 | 31 | public DateTimeOffset CacheExpiration { get; internal set; } 32 | 33 | public string Name { get; } 34 | 35 | protected virtual DateTimeOffset UtcNow => DateTimeOffset.UtcNow; 36 | 37 | protected abstract IHealthCheck Resolve(IServiceProvider serviceProvider); 38 | 39 | public async ValueTask RunAsync(IServiceProvider serviceProvider, CancellationToken cancellationToken = default(CancellationToken)) 40 | { 41 | while (CacheExpiration <= UtcNow) 42 | { 43 | // Can't use a standard lock here because of async, so we'll use this flag to determine when we should write a value, 44 | // and the waiters who aren't allowed to write will just spin wait for the new value. 45 | if (Interlocked.Exchange(ref _writerCount, 1) != 0) 46 | { 47 | await Task.Delay(5, cancellationToken).ConfigureAwait(false); 48 | continue; 49 | } 50 | 51 | try 52 | { 53 | var check = Resolve(serviceProvider); 54 | CachedResult = await check.CheckAsync(cancellationToken); 55 | } 56 | catch (OperationCanceledException) 57 | { 58 | CachedResult = HealthCheckResult.Unhealthy("The health check operation timed out"); 59 | } 60 | catch (Exception ex) 61 | { 62 | CachedResult = HealthCheckResult.Unhealthy($"Exception during check: {ex.GetType().FullName}"); 63 | } 64 | 65 | CacheExpiration = UtcNow + CacheDuration; 66 | _writerCount = 0; 67 | break; 68 | } 69 | 70 | return CachedResult; 71 | } 72 | 73 | public static CachedHealthCheck FromHealthCheck(string name, TimeSpan cacheDuration, IHealthCheck healthCheck) 74 | { 75 | Guard.ArgumentNotNull(nameof(healthCheck), healthCheck); 76 | 77 | return new TypeOrHealthCheck_HealthCheck(name, cacheDuration, healthCheck); 78 | } 79 | 80 | public static CachedHealthCheck FromType(string name, TimeSpan cacheDuration, Type healthCheckType) 81 | { 82 | Guard.ArgumentNotNull(nameof(healthCheckType), healthCheckType); 83 | Guard.ArgumentValid(HealthCheckTypeInfo.IsAssignableFrom(healthCheckType.GetTypeInfo()), nameof(healthCheckType), $"Health check must implement '{typeof(IHealthCheck).FullName}'."); 84 | 85 | return new TypeOrHealthCheck_Type(name, cacheDuration, healthCheckType); 86 | } 87 | 88 | class TypeOrHealthCheck_HealthCheck : CachedHealthCheck 89 | { 90 | private readonly IHealthCheck _healthCheck; 91 | 92 | public TypeOrHealthCheck_HealthCheck(string name, TimeSpan cacheDuration, IHealthCheck healthCheck) : base(name, cacheDuration) 93 | => _healthCheck = healthCheck; 94 | 95 | protected override IHealthCheck Resolve(IServiceProvider serviceProvider) => _healthCheck; 96 | } 97 | 98 | class TypeOrHealthCheck_Type : CachedHealthCheck 99 | { 100 | private readonly Type _healthCheckType; 101 | 102 | public TypeOrHealthCheck_Type(string name, TimeSpan cacheDuration, Type healthCheckType) : base(name, cacheDuration) 103 | => _healthCheckType = healthCheckType; 104 | 105 | protected override IHealthCheck Resolve(IServiceProvider serviceProvider) 106 | => (IHealthCheck)serviceProvider.GetRequiredService(_healthCheckType); 107 | } 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /Extensions/Microsoft.Extensions.HealthChecks/CachedHealthCheckExtensions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | 8 | namespace Microsoft.Extensions.HealthChecks 9 | { 10 | public static class CachedHealthCheckExtensions 11 | { 12 | public static ValueTask RunAsync(this CachedHealthCheck check, IServiceProvider serviceProvider) 13 | { 14 | Guard.ArgumentNotNull(nameof(check), check); 15 | 16 | return check.RunAsync(serviceProvider, CancellationToken.None); 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /Extensions/Microsoft.Extensions.HealthChecks/CheckStatus.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | namespace Microsoft.Extensions.HealthChecks 5 | { 6 | public enum CheckStatus 7 | { 8 | Unknown, 9 | Unhealthy, 10 | Healthy, 11 | Warning 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Extensions/Microsoft.Extensions.HealthChecks/Checks/AddCheck.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | 8 | namespace Microsoft.Extensions.HealthChecks 9 | { 10 | public static partial class HealthCheckBuilderExtensions 11 | { 12 | // Lambda versions of AddCheck for Func/Func/Func 13 | 14 | public static HealthCheckBuilder AddCheck(this HealthCheckBuilder builder, string name, Func check) 15 | { 16 | Guard.ArgumentNotNull(nameof(builder), builder); 17 | 18 | return builder.AddCheck(name, HealthCheck.FromCheck(check), builder.DefaultCacheDuration); 19 | } 20 | 21 | public static HealthCheckBuilder AddCheck(this HealthCheckBuilder builder, string name, Func check) 22 | { 23 | Guard.ArgumentNotNull(nameof(builder), builder); 24 | 25 | return builder.AddCheck(name, HealthCheck.FromCheck(check), builder.DefaultCacheDuration); 26 | } 27 | 28 | public static HealthCheckBuilder AddCheck(this HealthCheckBuilder builder, string name, Func check, TimeSpan cacheDuration) 29 | { 30 | Guard.ArgumentNotNull(nameof(builder), builder); 31 | 32 | return builder.AddCheck(name, HealthCheck.FromCheck(check), cacheDuration); 33 | } 34 | 35 | public static HealthCheckBuilder AddCheck(this HealthCheckBuilder builder, string name, Func check, TimeSpan cacheDuration) 36 | { 37 | Guard.ArgumentNotNull(nameof(builder), builder); 38 | 39 | return builder.AddCheck(name, HealthCheck.FromCheck(check), cacheDuration); 40 | } 41 | 42 | public static HealthCheckBuilder AddCheck(this HealthCheckBuilder builder, string name, Func> check) 43 | { 44 | Guard.ArgumentNotNull(nameof(builder), builder); 45 | 46 | return builder.AddCheck(name, HealthCheck.FromTaskCheck(check), builder.DefaultCacheDuration); 47 | } 48 | 49 | public static HealthCheckBuilder AddCheck(this HealthCheckBuilder builder, string name, Func> check) 50 | { 51 | Guard.ArgumentNotNull(nameof(builder), builder); 52 | 53 | return builder.AddCheck(name, HealthCheck.FromTaskCheck(check), builder.DefaultCacheDuration); 54 | } 55 | 56 | public static HealthCheckBuilder AddCheck(this HealthCheckBuilder builder, string name, Func> check, TimeSpan cacheDuration) 57 | { 58 | Guard.ArgumentNotNull(nameof(builder), builder); 59 | 60 | return builder.AddCheck(name, HealthCheck.FromTaskCheck(check), cacheDuration); 61 | } 62 | 63 | public static HealthCheckBuilder AddCheck(this HealthCheckBuilder builder, string name, Func> check, TimeSpan cacheDuration) 64 | { 65 | Guard.ArgumentNotNull(nameof(builder), builder); 66 | 67 | return builder.AddCheck(name, HealthCheck.FromTaskCheck(check), cacheDuration); 68 | } 69 | 70 | public static HealthCheckBuilder AddValueTaskCheck(this HealthCheckBuilder builder, string name, Func> check) 71 | { 72 | Guard.ArgumentNotNull(nameof(builder), builder); 73 | 74 | return builder.AddCheck(name, HealthCheck.FromValueTaskCheck(check), builder.DefaultCacheDuration); 75 | } 76 | 77 | public static HealthCheckBuilder AddValueTaskCheck(this HealthCheckBuilder builder, string name, Func> check) 78 | { 79 | Guard.ArgumentNotNull(nameof(builder), builder); 80 | 81 | return builder.AddCheck(name, HealthCheck.FromValueTaskCheck(check), builder.DefaultCacheDuration); 82 | } 83 | 84 | public static HealthCheckBuilder AddValueTaskCheck(this HealthCheckBuilder builder, string name, Func> check, TimeSpan cacheDuration) 85 | { 86 | Guard.ArgumentNotNull(nameof(builder), builder); 87 | 88 | return builder.AddCheck(name, HealthCheck.FromValueTaskCheck(check), cacheDuration); 89 | } 90 | 91 | public static HealthCheckBuilder AddValueTaskCheck(this HealthCheckBuilder builder, string name, Func> check, TimeSpan cacheDuration) 92 | { 93 | Guard.ArgumentNotNull(nameof(builder), builder); 94 | 95 | return builder.AddCheck(name, HealthCheck.FromValueTaskCheck(check), cacheDuration); 96 | } 97 | 98 | // IHealthCheck versions of AddCheck 99 | 100 | public static HealthCheckBuilder AddCheck(this HealthCheckBuilder builder, string checkName, IHealthCheck check) 101 | { 102 | Guard.ArgumentNotNull(nameof(builder), builder); 103 | 104 | return builder.AddCheck(checkName, check, builder.DefaultCacheDuration); 105 | } 106 | 107 | // Type versions of AddCheck 108 | 109 | public static HealthCheckBuilder AddCheck(this HealthCheckBuilder builder, string name) where TCheck : class, IHealthCheck 110 | { 111 | Guard.ArgumentNotNull(nameof(builder), builder); 112 | 113 | return builder.AddCheck(name, builder.DefaultCacheDuration); 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /Extensions/Microsoft.Extensions.HealthChecks/Checks/NumericChecks.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | 7 | namespace Microsoft.Extensions.HealthChecks 8 | { 9 | public static partial class HealthCheckBuilderExtensions 10 | { 11 | // Numeric checks 12 | 13 | public static HealthCheckBuilder AddMinValueCheck(this HealthCheckBuilder builder, string name, T minValue, Func currentValueFunc) where T : IComparable 14 | { 15 | Guard.ArgumentNotNull(nameof(builder), builder); 16 | 17 | return AddMinValueCheck(builder, name, minValue, currentValueFunc, builder.DefaultCacheDuration); 18 | } 19 | 20 | public static HealthCheckBuilder AddMinValueCheck(this HealthCheckBuilder builder, string name, T minValue, Func currentValueFunc, TimeSpan cacheDuration) 21 | where T : IComparable 22 | { 23 | Guard.ArgumentNotNull(nameof(builder), builder); 24 | Guard.ArgumentNotNullOrEmpty(nameof(name), name); 25 | Guard.ArgumentNotNull(nameof(currentValueFunc), currentValueFunc); 26 | 27 | builder.AddCheck(name, () => 28 | { 29 | var currentValue = currentValueFunc(); 30 | var status = currentValue.CompareTo(minValue) >= 0 ? CheckStatus.Healthy : CheckStatus.Unhealthy; 31 | return HealthCheckResult.FromStatus( 32 | status, 33 | $"min={minValue}, current={currentValue}", 34 | new Dictionary { { "min", minValue }, { "current", currentValue } } 35 | ); 36 | }, cacheDuration); 37 | 38 | return builder; 39 | } 40 | 41 | public static HealthCheckBuilder AddMaxValueCheck(this HealthCheckBuilder builder, string name, T maxValue, Func currentValueFunc) where T : IComparable 42 | { 43 | Guard.ArgumentNotNull(nameof(builder), builder); 44 | 45 | return AddMaxValueCheck(builder, name, maxValue, currentValueFunc, builder.DefaultCacheDuration); 46 | } 47 | 48 | public static HealthCheckBuilder AddMaxValueCheck(this HealthCheckBuilder builder, string name, T maxValue, Func currentValueFunc, TimeSpan cacheDuration) 49 | where T : IComparable 50 | { 51 | Guard.ArgumentNotNull(nameof(builder), builder); 52 | Guard.ArgumentNotNullOrEmpty(nameof(name), name); 53 | Guard.ArgumentNotNull(nameof(currentValueFunc), currentValueFunc); 54 | 55 | builder.AddCheck(name, () => 56 | { 57 | var currentValue = currentValueFunc(); 58 | var status = currentValue.CompareTo(maxValue) <= 0 ? CheckStatus.Healthy : CheckStatus.Unhealthy; 59 | return HealthCheckResult.FromStatus( 60 | status, 61 | $"max={maxValue}, current={currentValue}", 62 | new Dictionary { { "max", maxValue }, { "current", currentValue } } 63 | ); 64 | }, cacheDuration); 65 | 66 | return builder; 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /Extensions/Microsoft.Extensions.HealthChecks/Checks/SystemChecks.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.Diagnostics; 6 | 7 | namespace Microsoft.Extensions.HealthChecks 8 | { 9 | public static partial class HealthCheckBuilderExtensions 10 | { 11 | // System checks 12 | 13 | public static HealthCheckBuilder AddPrivateMemorySizeCheck(this HealthCheckBuilder builder, long maxSize) 14 | => AddMaxValueCheck(builder, $"PrivateMemorySize({maxSize})", maxSize, () => Process.GetCurrentProcess().PrivateMemorySize64); 15 | 16 | public static HealthCheckBuilder AddPrivateMemorySizeCheck(this HealthCheckBuilder builder, long maxSize, TimeSpan cacheDuration) 17 | => AddMaxValueCheck(builder, $"PrivateMemorySize({maxSize})", maxSize, () => Process.GetCurrentProcess().PrivateMemorySize64, cacheDuration); 18 | 19 | public static HealthCheckBuilder AddVirtualMemorySizeCheck(this HealthCheckBuilder builder, long maxSize) 20 | => AddMaxValueCheck(builder, $"VirtualMemorySize({maxSize})", maxSize, () => Process.GetCurrentProcess().VirtualMemorySize64); 21 | 22 | public static HealthCheckBuilder AddVirtualMemorySizeCheck(this HealthCheckBuilder builder, long maxSize, TimeSpan cacheDuration) 23 | => AddMaxValueCheck(builder, $"VirtualMemorySize({maxSize})", maxSize, () => Process.GetCurrentProcess().VirtualMemorySize64, cacheDuration); 24 | 25 | public static HealthCheckBuilder AddWorkingSetCheck(this HealthCheckBuilder builder, long maxSize) 26 | => AddMaxValueCheck(builder, $"WorkingSet({maxSize})", maxSize, () => Process.GetCurrentProcess().WorkingSet64); 27 | 28 | public static HealthCheckBuilder AddWorkingSetCheck(this HealthCheckBuilder builder, long maxSize, TimeSpan cacheDuration) 29 | => AddMaxValueCheck(builder, $"WorkingSet({maxSize})", maxSize, () => Process.GetCurrentProcess().WorkingSet64, cacheDuration); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /Extensions/Microsoft.Extensions.HealthChecks/Checks/UrlChecks.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.Net.Http; 6 | using System.Threading.Tasks; 7 | using Microsoft.Extensions.HealthChecks.Internal; 8 | 9 | namespace Microsoft.Extensions.HealthChecks 10 | { 11 | public static partial class HealthCheckBuilderExtensions 12 | { 13 | // Default URL check 14 | 15 | public static HealthCheckBuilder AddUrlCheck(this HealthCheckBuilder builder, string url) 16 | { 17 | Guard.ArgumentNotNull(nameof(builder), builder); 18 | 19 | return AddUrlCheck(builder, url, builder.DefaultCacheDuration); 20 | } 21 | 22 | public static HealthCheckBuilder AddUrlCheck(this HealthCheckBuilder builder, string url, TimeSpan cacheDuration) 23 | => AddUrlCheck(builder, url, response => UrlChecker.DefaultUrlCheck(response), cacheDuration); 24 | 25 | // Func returning IHealthCheckResult 26 | 27 | public static HealthCheckBuilder AddUrlCheck(this HealthCheckBuilder builder, string url, Func checkFunc) 28 | { 29 | Guard.ArgumentNotNull(nameof(builder), builder); 30 | 31 | return AddUrlCheck(builder, url, checkFunc, builder.DefaultCacheDuration); 32 | } 33 | 34 | public static HealthCheckBuilder AddUrlCheck(this HealthCheckBuilder builder, string url, 35 | Func checkFunc, 36 | TimeSpan cacheDuration) 37 | { 38 | Guard.ArgumentNotNull(nameof(checkFunc), checkFunc); 39 | 40 | return AddUrlCheck(builder, url, response => new ValueTask(checkFunc(response)), cacheDuration); 41 | } 42 | 43 | // Func returning Task 44 | 45 | public static HealthCheckBuilder AddUrlCheck(this HealthCheckBuilder builder, string url, Func> checkFunc) 46 | { 47 | Guard.ArgumentNotNull(nameof(builder), builder); 48 | 49 | return AddUrlCheck(builder, url, checkFunc, builder.DefaultCacheDuration); 50 | } 51 | 52 | public static HealthCheckBuilder AddUrlCheck(this HealthCheckBuilder builder, string url, 53 | Func> checkFunc, 54 | TimeSpan cacheDuration) 55 | { 56 | Guard.ArgumentNotNull(nameof(checkFunc), checkFunc); 57 | 58 | return AddUrlCheck(builder, url, response => new ValueTask(checkFunc(response)), cacheDuration); 59 | } 60 | 61 | // Func returning ValueTask 62 | 63 | public static HealthCheckBuilder AddUrlCheck(this HealthCheckBuilder builder, string url, Func> checkFunc) 64 | { 65 | Guard.ArgumentNotNull(nameof(builder), builder); 66 | 67 | return AddUrlCheck(builder, url, checkFunc, builder.DefaultCacheDuration); 68 | } 69 | 70 | public static HealthCheckBuilder AddUrlCheck(this HealthCheckBuilder builder, string url, 71 | Func> checkFunc, 72 | TimeSpan cacheDuration) 73 | { 74 | Guard.ArgumentNotNull(nameof(builder), builder); 75 | Guard.ArgumentNotNullOrEmpty(nameof(url), url); 76 | Guard.ArgumentNotNull(nameof(checkFunc), checkFunc); 77 | 78 | var urlCheck = new UrlChecker(checkFunc, url); 79 | builder.AddCheck($"UrlCheck({url})", () => urlCheck.CheckAsync(), cacheDuration); 80 | return builder; 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /Extensions/Microsoft.Extensions.HealthChecks/CompositeHealthCheckResult.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | 8 | namespace Microsoft.Extensions.HealthChecks 9 | { 10 | /// 11 | /// Represents a composite health check result built from several results. 12 | /// 13 | public class CompositeHealthCheckResult : IHealthCheckResult 14 | { 15 | private static readonly IReadOnlyDictionary _emptyData = new Dictionary(); 16 | private readonly CheckStatus _initialStatus; 17 | private readonly CheckStatus _partiallyHealthyStatus; 18 | private readonly Dictionary _results = new Dictionary(StringComparer.OrdinalIgnoreCase); 19 | 20 | public CompositeHealthCheckResult(CheckStatus partiallyHealthyStatus = CheckStatus.Warning, 21 | CheckStatus initialStatus = CheckStatus.Unknown) 22 | { 23 | _partiallyHealthyStatus = partiallyHealthyStatus; 24 | _initialStatus = initialStatus; 25 | } 26 | 27 | public CheckStatus CheckStatus 28 | { 29 | get 30 | { 31 | var checkStatuses = new HashSet(_results.Select(x => x.Value.CheckStatus)); 32 | if (checkStatuses.Count == 0) 33 | { 34 | return _initialStatus; 35 | } 36 | if (checkStatuses.Count == 1) 37 | { 38 | return checkStatuses.First(); 39 | } 40 | if (checkStatuses.Contains(CheckStatus.Healthy)) 41 | { 42 | return _partiallyHealthyStatus; 43 | } 44 | 45 | return CheckStatus.Unhealthy; 46 | } 47 | } 48 | 49 | public string Description => string.Join(Environment.NewLine, _results.Select(r => $"{r.Key}: {r.Value.Description}")); 50 | 51 | public IReadOnlyDictionary Data 52 | { 53 | get 54 | { 55 | var result = new Dictionary(); 56 | 57 | foreach (var kvp in _results) 58 | result.Add(kvp.Key, kvp.Value.Data); 59 | 60 | return result; 61 | } 62 | } 63 | 64 | public IReadOnlyDictionary Results => _results; 65 | 66 | public void Add(string name, CheckStatus status, string description) 67 | => Add(name, status, description, null); 68 | 69 | public void Add(string name, CheckStatus status, string description, Dictionary data) 70 | { 71 | Guard.ArgumentNotNullOrEmpty(nameof(name), name); 72 | Guard.ArgumentValid(status != CheckStatus.Unknown, nameof(status), "Cannot add 'Unknown' status to composite health check result."); 73 | Guard.ArgumentNotNullOrEmpty(nameof(description), description); 74 | 75 | _results.Add(name, HealthCheckResult.FromStatus(status, description, data)); 76 | } 77 | 78 | public void Add(string name, IHealthCheckResult checkResult) 79 | { 80 | Guard.ArgumentNotNullOrEmpty(nameof(name), name); 81 | Guard.ArgumentNotNull(nameof(checkResult), checkResult); 82 | 83 | _results.Add(name, checkResult); 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /Extensions/Microsoft.Extensions.HealthChecks/HealthCheck.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.Threading; 6 | using System.Threading.Tasks; 7 | 8 | namespace Microsoft.Extensions.HealthChecks 9 | { 10 | public class HealthCheck : IHealthCheck 11 | { 12 | protected HealthCheck(Func> check) 13 | { 14 | Guard.ArgumentNotNull(nameof(check), check); 15 | 16 | Check = check; 17 | } 18 | 19 | protected Func> Check { get; } 20 | 21 | public ValueTask CheckAsync(CancellationToken cancellationToken = default(CancellationToken)) 22 | => Check(cancellationToken); 23 | 24 | public static HealthCheck FromCheck(Func check) 25 | => new HealthCheck(token => new ValueTask(check())); 26 | 27 | public static HealthCheck FromCheck(Func check) 28 | => new HealthCheck(token => new ValueTask(check(token))); 29 | 30 | public static HealthCheck FromTaskCheck(Func> check) 31 | => new HealthCheck(token => new ValueTask(check())); 32 | 33 | public static HealthCheck FromTaskCheck(Func> check) 34 | => new HealthCheck(token => new ValueTask(check(token))); 35 | 36 | public static HealthCheck FromValueTaskCheck(Func> check) 37 | => new HealthCheck(token => check()); 38 | 39 | public static HealthCheck FromValueTaskCheck(Func> check) 40 | => new HealthCheck(check); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /Extensions/Microsoft.Extensions.HealthChecks/HealthCheckBuilder.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | 7 | namespace Microsoft.Extensions.HealthChecks 8 | { 9 | public class HealthCheckBuilder 10 | { 11 | private readonly Dictionary _checksByName; 12 | private readonly HealthCheckGroup _currentGroup; 13 | private readonly Dictionary _groups; 14 | 15 | public HealthCheckBuilder() 16 | { 17 | _checksByName = new Dictionary(StringComparer.OrdinalIgnoreCase); 18 | _currentGroup = new HealthCheckGroup(string.Empty, CheckStatus.Unhealthy); 19 | _groups = new Dictionary(StringComparer.OrdinalIgnoreCase) 20 | { 21 | [string.Empty] = _currentGroup 22 | }; 23 | 24 | DefaultCacheDuration = TimeSpan.FromMinutes(5); 25 | } 26 | 27 | /// 28 | /// This constructor should only be used when creating a grouped health check builder. 29 | /// 30 | public HealthCheckBuilder(HealthCheckBuilder rootBuilder, HealthCheckGroup currentGroup) 31 | { 32 | Guard.ArgumentNotNull(nameof(rootBuilder), rootBuilder); 33 | Guard.ArgumentNotNull(nameof(currentGroup), currentGroup); 34 | 35 | _checksByName = rootBuilder._checksByName; 36 | _currentGroup = currentGroup; 37 | _groups = rootBuilder._groups; 38 | 39 | DefaultCacheDuration = rootBuilder.DefaultCacheDuration; 40 | } 41 | 42 | /// 43 | /// Gets the registered checks, indexed by check name. 44 | /// 45 | public IReadOnlyDictionary ChecksByName => _checksByName; 46 | 47 | /// 48 | /// Gets the current default cache duration used when registering checks. 49 | /// 50 | public TimeSpan DefaultCacheDuration { get; private set; } 51 | 52 | /// 53 | /// Gets the registered groups, indexed by group name. The root group's name is . 54 | /// 55 | public IReadOnlyDictionary Groups => _groups; 56 | 57 | /// 58 | /// Registers a health check type that will later be resolved via dependency 59 | /// injection. 60 | /// 61 | public HealthCheckBuilder AddCheck(string checkName, TimeSpan cacheDuration) where TCheck : class, IHealthCheck 62 | { 63 | Guard.ArgumentNotNullOrEmpty(nameof(checkName), checkName); 64 | Guard.ArgumentValid(!_checksByName.ContainsKey(checkName), nameof(checkName), $"A check with name '{checkName}' has already been registered."); 65 | 66 | var namedCheck = CachedHealthCheck.FromType(checkName, cacheDuration, typeof(TCheck)); 67 | 68 | _checksByName.Add(checkName, namedCheck); 69 | _currentGroup.ChecksInternal.Add(namedCheck); 70 | 71 | return this; 72 | } 73 | 74 | /// 75 | /// Registers a concrete health check to the builder. 76 | /// 77 | public HealthCheckBuilder AddCheck(string checkName, IHealthCheck check, TimeSpan cacheDuration) 78 | { 79 | Guard.ArgumentNotNullOrEmpty(nameof(checkName), checkName); 80 | Guard.ArgumentNotNull(nameof(check), check); 81 | Guard.ArgumentValid(!_checksByName.ContainsKey(checkName), nameof(checkName), $"A check with name '{checkName}' has already been registered."); 82 | 83 | var namedCheck = CachedHealthCheck.FromHealthCheck(checkName, cacheDuration, check); 84 | 85 | _checksByName.Add(checkName, namedCheck); 86 | _currentGroup.ChecksInternal.Add(namedCheck); 87 | 88 | return this; 89 | } 90 | 91 | /// 92 | /// Creates a new health check group, to which you can add one or more health 93 | /// checks. Uses when the group is 94 | /// partially successful. 95 | /// 96 | public HealthCheckBuilder AddHealthCheckGroup(string groupName, Action groupChecks) 97 | => AddHealthCheckGroup(groupName, groupChecks, CheckStatus.Unhealthy); 98 | 99 | /// 100 | /// Creates a new health check group, to which you can add one or more health 101 | /// checks. 102 | /// 103 | public HealthCheckBuilder AddHealthCheckGroup(string groupName, Action groupChecks, CheckStatus partialSuccessStatus) 104 | { 105 | Guard.ArgumentNotNullOrEmpty(nameof(groupName), groupName); 106 | Guard.ArgumentNotNull(nameof(groupChecks), groupChecks); 107 | Guard.ArgumentValid(partialSuccessStatus != CheckStatus.Unknown, nameof(partialSuccessStatus), "Check status 'Unknown' is not valid for partial success."); 108 | Guard.ArgumentValid(!_groups.ContainsKey(groupName), nameof(groupName), $"A group with name '{groupName}' has already been registered."); 109 | Guard.OperationValid(_currentGroup.GroupName == string.Empty, "Nested groups are not supported by HealthCheckBuilder."); 110 | 111 | var group = new HealthCheckGroup(groupName, partialSuccessStatus); 112 | _groups.Add(groupName, group); 113 | 114 | var innerBuilder = new HealthCheckBuilder(this, group); 115 | groupChecks(innerBuilder); 116 | 117 | return this; 118 | } 119 | 120 | public HealthCheckBuilder WithDefaultCacheDuration(TimeSpan duration) 121 | { 122 | Guard.ArgumentValid(duration >= TimeSpan.Zero, nameof(duration), "Duration must be zero (disabled) or a positive duration."); 123 | 124 | DefaultCacheDuration = duration; 125 | return this; 126 | } 127 | 128 | public HealthCheckBuilder WithPartialSuccessStatus(CheckStatus partiallyHealthyStatus) 129 | { 130 | _currentGroup.PartiallyHealthyStatus = partiallyHealthyStatus; 131 | 132 | return this; 133 | } 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /Extensions/Microsoft.Extensions.HealthChecks/HealthCheckGroup.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System.Collections.Generic; 5 | 6 | namespace Microsoft.Extensions.HealthChecks 7 | { 8 | public class HealthCheckGroup 9 | { 10 | private CheckStatus _partialSuccessStatus; 11 | 12 | public HealthCheckGroup(string groupName, CheckStatus partialSuccessStatus) 13 | { 14 | Guard.ArgumentNotNull(nameof(groupName), groupName); 15 | 16 | GroupName = groupName; 17 | PartiallyHealthyStatus = partialSuccessStatus; 18 | } 19 | 20 | public IReadOnlyList Checks => ChecksInternal.AsReadOnly(); 21 | 22 | internal List ChecksInternal { get; } = new List(); 23 | 24 | public string GroupName { get; } 25 | 26 | public CheckStatus PartiallyHealthyStatus 27 | { 28 | get => _partialSuccessStatus; 29 | internal set 30 | { 31 | Guard.ArgumentValid(value != CheckStatus.Unknown, nameof(value), "Check status 'Unknown' is not valid for partial success."); 32 | 33 | _partialSuccessStatus = value; 34 | } 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Extensions/Microsoft.Extensions.HealthChecks/HealthCheckResult.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System.Collections.Generic; 5 | 6 | namespace Microsoft.Extensions.HealthChecks 7 | { 8 | public class HealthCheckResult : IHealthCheckResult 9 | { 10 | private static readonly IReadOnlyDictionary _emptyData = new Dictionary(); 11 | 12 | public CheckStatus CheckStatus { get; } 13 | public IReadOnlyDictionary Data { get; } 14 | public string Description { get; } 15 | 16 | private HealthCheckResult(CheckStatus checkStatus, string description, IReadOnlyDictionary data) 17 | { 18 | CheckStatus = checkStatus; 19 | Description = description; 20 | Data = data ?? _emptyData; 21 | } 22 | 23 | public static HealthCheckResult Unhealthy(string description) 24 | => new HealthCheckResult(CheckStatus.Unhealthy, description, null); 25 | 26 | public static HealthCheckResult Unhealthy(string description, IReadOnlyDictionary data) 27 | => new HealthCheckResult(CheckStatus.Unhealthy, description, data); 28 | 29 | public static HealthCheckResult Healthy(string description) 30 | => new HealthCheckResult(CheckStatus.Healthy, description, null); 31 | 32 | public static HealthCheckResult Healthy(string description, IReadOnlyDictionary data) 33 | => new HealthCheckResult(CheckStatus.Healthy, description, data); 34 | 35 | public static HealthCheckResult Warning(string description) 36 | => new HealthCheckResult(CheckStatus.Warning, description, null); 37 | 38 | public static HealthCheckResult Warning(string description, IReadOnlyDictionary data) 39 | => new HealthCheckResult(CheckStatus.Warning, description, data); 40 | 41 | public static HealthCheckResult Unknown(string description) 42 | => new HealthCheckResult(CheckStatus.Unknown, description, null); 43 | 44 | public static HealthCheckResult Unknown(string description, IReadOnlyDictionary data) 45 | => new HealthCheckResult(CheckStatus.Unknown, description, data); 46 | 47 | public static HealthCheckResult FromStatus(CheckStatus status, string description) 48 | => new HealthCheckResult(status, description, null); 49 | 50 | public static HealthCheckResult FromStatus(CheckStatus status, string description, IReadOnlyDictionary data) 51 | => new HealthCheckResult(status, description, data); 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /Extensions/Microsoft.Extensions.HealthChecks/HealthCheckResults.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System.Collections.Generic; 5 | 6 | namespace Microsoft.Extensions.HealthChecks 7 | { 8 | public class HealthCheckResults 9 | { 10 | public IList CheckResults { get; } = new List(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /Extensions/Microsoft.Extensions.HealthChecks/HealthCheckService.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Linq; 7 | using System.Threading; 8 | using System.Threading.Tasks; 9 | using Microsoft.Extensions.DependencyInjection; 10 | 11 | namespace Microsoft.Extensions.HealthChecks 12 | { 13 | public class HealthCheckService : IHealthCheckService 14 | { 15 | private readonly HealthCheckBuilder _builder; 16 | private readonly IReadOnlyList _groups; 17 | private readonly HealthCheckGroup _root; 18 | private readonly IServiceProvider _serviceProvider; 19 | private readonly IServiceScopeFactory _serviceScopeFactory; 20 | 21 | public HealthCheckService(HealthCheckBuilder builder, IServiceProvider serviceProvider, IServiceScopeFactory serviceScopeFactory) 22 | { 23 | _builder = builder; 24 | _groups = GetGroups().Where(group => group.GroupName != string.Empty).ToList(); 25 | _root = GetGroup(string.Empty); 26 | _serviceProvider = serviceProvider; 27 | _serviceScopeFactory = serviceScopeFactory; 28 | } 29 | 30 | public async Task CheckHealthAsync(CancellationToken cancellationToken = default(CancellationToken)) 31 | { 32 | using (var scope = GetServiceScope()) 33 | { 34 | var scopeServiceProvider = scope.ServiceProvider; 35 | var groupTasks = _groups.Select(group => new { Group = group, Task = RunGroupAsync(scopeServiceProvider, group, cancellationToken) }).ToList(); 36 | var result = await RunGroupAsync(scopeServiceProvider, _root, cancellationToken).ConfigureAwait(false); 37 | 38 | await Task.WhenAll(groupTasks.Select(x => x.Task)); 39 | 40 | foreach (var groupTask in groupTasks) 41 | { 42 | result.Add($"Group({groupTask.Group.GroupName})", groupTask.Task.Result); 43 | } 44 | 45 | return result; 46 | } 47 | } 48 | 49 | public IReadOnlyList GetAllChecks() 50 | => _builder.ChecksByName.Values.ToList().AsReadOnly(); 51 | 52 | public CachedHealthCheck GetCheck(string checkName) 53 | => _builder.ChecksByName[checkName]; 54 | 55 | public HealthCheckGroup GetGroup(string groupName) 56 | => _builder.Groups[groupName]; 57 | 58 | public IReadOnlyList GetGroups() 59 | => _builder.Groups.Values.ToList().AsReadOnly(); 60 | 61 | private IServiceScope GetServiceScope() 62 | => _serviceScopeFactory == null ? new UnscopedServiceProvider(_serviceProvider) : _serviceScopeFactory.CreateScope(); 63 | 64 | public async ValueTask RunCheckAsync(CachedHealthCheck healthCheck, CancellationToken cancellationToken = default(CancellationToken)) 65 | { 66 | using (var scope = GetServiceScope()) 67 | { 68 | return await RunCheckAsync(scope.ServiceProvider, healthCheck, cancellationToken).ConfigureAwait(false); 69 | } 70 | } 71 | 72 | /// 73 | /// Uses the provided service provider and executes the provided check. 74 | /// 75 | public ValueTask RunCheckAsync(IServiceProvider serviceProvider, CachedHealthCheck healthCheck, CancellationToken cancellationToken = default(CancellationToken)) 76 | { 77 | Guard.ArgumentNotNull(nameof(serviceProvider), serviceProvider); 78 | Guard.ArgumentNotNull(nameof(healthCheck), healthCheck); 79 | 80 | return healthCheck.RunAsync(serviceProvider, cancellationToken); 81 | } 82 | 83 | /// 84 | /// Creates a new resolution scope from the default service provider and executes the checks in the given group. 85 | /// 86 | public async Task RunGroupAsync(HealthCheckGroup group, CancellationToken cancellationToken = default(CancellationToken)) 87 | { 88 | using (var scope = GetServiceScope()) 89 | return await RunGroupAsync(scope.ServiceProvider, group, cancellationToken).ConfigureAwait(false); 90 | } 91 | 92 | /// 93 | /// Uses the provided service provider and executes the checks in the given group. 94 | /// 95 | public async Task RunGroupAsync(IServiceProvider serviceProvider, HealthCheckGroup group, CancellationToken cancellationToken = default(CancellationToken)) 96 | { 97 | var result = new CompositeHealthCheckResult(group.PartiallyHealthyStatus); 98 | var checkTasks = group.Checks.Select(check => new { Check = check, Task = check.RunAsync(serviceProvider, cancellationToken).AsTask() }).ToList(); 99 | await Task.WhenAll(checkTasks.Select(checkTask => checkTask.Task)); 100 | 101 | foreach (var checkTask in checkTasks) 102 | { 103 | result.Add(checkTask.Check.Name, checkTask.Task.Result); 104 | } 105 | 106 | return result; 107 | } 108 | 109 | private class UnscopedServiceProvider : IServiceScope 110 | { 111 | public UnscopedServiceProvider(IServiceProvider serviceProvider) 112 | => ServiceProvider = serviceProvider; 113 | 114 | public IServiceProvider ServiceProvider { get; } 115 | 116 | public void Dispose() { } 117 | } 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /Extensions/Microsoft.Extensions.HealthChecks/HealthCheckServiceCollectionExtensions.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.Linq; 6 | using Microsoft.Extensions.HealthChecks; 7 | 8 | namespace Microsoft.Extensions.DependencyInjection 9 | { 10 | public static class HealthCheckServiceCollectionExtensions 11 | { 12 | private static readonly Type HealthCheckServiceInterface = typeof(IHealthCheckService); 13 | 14 | public static IServiceCollection AddHealthChecks(this IServiceCollection services, Action checks) 15 | { 16 | Guard.OperationValid(!services.Any(descriptor => descriptor.ServiceType == HealthCheckServiceInterface), "AddHealthChecks may only be called once."); 17 | 18 | var builder = new HealthCheckBuilder(); 19 | 20 | services.AddSingleton(serviceProvider => 21 | { 22 | var serviceScopeFactory = serviceProvider.GetService(); 23 | return new HealthCheckService(builder, serviceProvider, serviceScopeFactory); 24 | }); 25 | 26 | checks(builder); 27 | return services; 28 | } 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /Extensions/Microsoft.Extensions.HealthChecks/IHealthCheck.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System.Threading; 5 | using System.Threading.Tasks; 6 | 7 | namespace Microsoft.Extensions.HealthChecks 8 | { 9 | public interface IHealthCheck 10 | { 11 | ValueTask CheckAsync(CancellationToken cancellationToken = default(CancellationToken)); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /Extensions/Microsoft.Extensions.HealthChecks/IHealthCheckResult.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System.Collections.Generic; 5 | 6 | namespace Microsoft.Extensions.HealthChecks 7 | { 8 | public interface IHealthCheckResult 9 | { 10 | CheckStatus CheckStatus { get; } 11 | string Description { get; } 12 | IReadOnlyDictionary Data { get; } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /Extensions/Microsoft.Extensions.HealthChecks/IHealthCheckService.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Threading; 7 | using System.Threading.Tasks; 8 | 9 | namespace Microsoft.Extensions.HealthChecks 10 | { 11 | public interface IHealthCheckService 12 | { 13 | /// 14 | /// Runs all registered health checks. 15 | /// 16 | Task CheckHealthAsync(CancellationToken cancellationToken = default(CancellationToken)); 17 | 18 | /// 19 | /// Gets all registered health checks as a flat list. 20 | /// 21 | IReadOnlyList GetAllChecks(); 22 | 23 | /// 24 | /// Gets a health check by name. 25 | /// 26 | CachedHealthCheck GetCheck(string checkName); 27 | 28 | /// 29 | /// Gets all health checks in a group. 30 | /// 31 | HealthCheckGroup GetGroup(string groupName); 32 | 33 | /// 34 | /// Gets all the health check groups. 35 | /// 36 | IReadOnlyList GetGroups(); 37 | 38 | /// 39 | /// Creates a new resolution scope from the default service provider and executes the provided check. 40 | /// 41 | ValueTask RunCheckAsync(CachedHealthCheck healthCheck, CancellationToken cancellationToken = default(CancellationToken)); 42 | 43 | /// 44 | /// Uses the provided service provider and executes the provided check. 45 | /// 46 | ValueTask RunCheckAsync(IServiceProvider serviceProvider, CachedHealthCheck healthCheck, CancellationToken cancellationToken = default(CancellationToken)); 47 | 48 | /// 49 | /// Creates a new resolution scope from the default service provider and executes the checks in the given group. 50 | /// 51 | Task RunGroupAsync(HealthCheckGroup group, CancellationToken cancellationToken = default(CancellationToken)); 52 | 53 | /// 54 | /// Uses the provided service provider and executes the checks in the given group. 55 | /// 56 | Task RunGroupAsync(IServiceProvider serviceProvider, HealthCheckGroup group, CancellationToken cancellationToken = default(CancellationToken)); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /Extensions/Microsoft.Extensions.HealthChecks/Internal/Guard.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | 7 | static class Guard 8 | { 9 | public static void ArgumentNotNull(string argumentName, object value) 10 | { 11 | if (value == null) 12 | { 13 | throw new ArgumentNullException(argumentName); 14 | } 15 | } 16 | 17 | public static void ArgumentNotNullOrEmpty(string argumentName, string value) 18 | { 19 | if (value == null) 20 | { 21 | throw new ArgumentNullException(argumentName); 22 | } 23 | if (string.IsNullOrEmpty(value)) 24 | { 25 | throw new ArgumentException("Value cannot be an empty string.", argumentName); 26 | } 27 | } 28 | 29 | // Use IReadOnlyCollection instead of IEnumerable to discourage double enumeration 30 | public static void ArgumentNotNullOrEmpty(string argumentName, IReadOnlyCollection items) 31 | { 32 | if (items == null) 33 | { 34 | throw new ArgumentNullException(argumentName); 35 | } 36 | if (items.Count == 0) 37 | { 38 | throw new ArgumentException("Collection must contain at least one item.", argumentName); 39 | } 40 | } 41 | 42 | public static void ArgumentValid(bool valid, string argumentName, string exceptionMessage) 43 | { 44 | if (!valid) 45 | { 46 | throw new ArgumentException(exceptionMessage, argumentName); 47 | } 48 | } 49 | 50 | public static void OperationValid(bool valid, string exceptionMessage) 51 | { 52 | if (!valid) 53 | { 54 | throw new InvalidOperationException(exceptionMessage); 55 | } 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /Extensions/Microsoft.Extensions.HealthChecks/Internal/UrlChecker.cs: -------------------------------------------------------------------------------- 1 | // Copyright (c) .NET Foundation. All rights reserved. 2 | // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. 3 | 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Net.Http; 7 | using System.Net.Http.Headers; 8 | using System.Threading.Tasks; 9 | 10 | namespace Microsoft.Extensions.HealthChecks.Internal 11 | { 12 | public class UrlChecker 13 | { 14 | private readonly Func> _checkFunc; 15 | private readonly string _url; 16 | 17 | public UrlChecker(Func> checkFunc, string url) 18 | { 19 | Guard.ArgumentNotNull(nameof(checkFunc), checkFunc); 20 | Guard.ArgumentNotNullOrEmpty(nameof(url), url); 21 | 22 | _checkFunc = checkFunc; 23 | _url = url; 24 | } 25 | 26 | public CheckStatus PartiallyHealthyStatus { get; set; } = CheckStatus.Warning; 27 | 28 | public async Task CheckAsync() 29 | { 30 | using (var httpClient = CreateHttpClient()) 31 | { 32 | try 33 | { 34 | var response = await httpClient.GetAsync(_url).ConfigureAwait(false); 35 | return await _checkFunc(response); 36 | } 37 | catch (Exception ex) 38 | { 39 | var data = new Dictionary { { "url", _url } }; 40 | return HealthCheckResult.Unhealthy($"Exception during check: {ex.GetType().FullName}", data); 41 | } 42 | } 43 | } 44 | 45 | private HttpClient CreateHttpClient() 46 | { 47 | var httpClient = GetHttpClient(); 48 | httpClient.DefaultRequestHeaders.CacheControl = new CacheControlHeaderValue { NoCache = true }; 49 | return httpClient; 50 | } 51 | 52 | public static async ValueTask DefaultUrlCheck(HttpResponseMessage response) 53 | { 54 | var status = response.IsSuccessStatusCode ? CheckStatus.Healthy : CheckStatus.Unhealthy; 55 | var data = new Dictionary 56 | { 57 | { "url", response.RequestMessage.RequestUri.ToString() }, 58 | { "status", (int)response.StatusCode }, 59 | { "reason", response.ReasonPhrase }, 60 | { "body", await response.Content?.ReadAsStringAsync() } 61 | }; 62 | return HealthCheckResult.FromStatus(status, $"status code {response.StatusCode} ({(int)response.StatusCode})", data); 63 | } 64 | 65 | protected virtual HttpClient GetHttpClient() 66 | => new HttpClient(); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /Extensions/Microsoft.Extensions.HealthChecks/Microsoft.Extensions.HealthChecks.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard1.3 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /GenealogyClientSdk.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.26124.0 5 | MinimumVisualStudioVersion = 15.0.26124.0 6 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GenealogyWebAPI.ClientSdk", "GenealogyWebAPI.ClientSdk\GenealogyWebAPI.ClientSdk.csproj", "{E197915D-47D9-4654-A3D4-EAD57C00F465}" 7 | EndProject 8 | Global 9 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 10 | Debug|Any CPU = Debug|Any CPU 11 | Debug|x64 = Debug|x64 12 | Debug|x86 = Debug|x86 13 | Release|Any CPU = Release|Any CPU 14 | Release|x64 = Release|x64 15 | Release|x86 = Release|x86 16 | EndGlobalSection 17 | GlobalSection(SolutionProperties) = preSolution 18 | HideSolutionNode = FALSE 19 | EndGlobalSection 20 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 21 | {E197915D-47D9-4654-A3D4-EAD57C00F465}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 22 | {E197915D-47D9-4654-A3D4-EAD57C00F465}.Debug|Any CPU.Build.0 = Debug|Any CPU 23 | {E197915D-47D9-4654-A3D4-EAD57C00F465}.Debug|x64.ActiveCfg = Debug|Any CPU 24 | {E197915D-47D9-4654-A3D4-EAD57C00F465}.Debug|x64.Build.0 = Debug|Any CPU 25 | {E197915D-47D9-4654-A3D4-EAD57C00F465}.Debug|x86.ActiveCfg = Debug|Any CPU 26 | {E197915D-47D9-4654-A3D4-EAD57C00F465}.Debug|x86.Build.0 = Debug|Any CPU 27 | {E197915D-47D9-4654-A3D4-EAD57C00F465}.Release|Any CPU.ActiveCfg = Release|Any CPU 28 | {E197915D-47D9-4654-A3D4-EAD57C00F465}.Release|Any CPU.Build.0 = Release|Any CPU 29 | {E197915D-47D9-4654-A3D4-EAD57C00F465}.Release|x64.ActiveCfg = Release|Any CPU 30 | {E197915D-47D9-4654-A3D4-EAD57C00F465}.Release|x64.Build.0 = Release|Any CPU 31 | {E197915D-47D9-4654-A3D4-EAD57C00F465}.Release|x86.ActiveCfg = Release|Any CPU 32 | {E197915D-47D9-4654-A3D4-EAD57C00F465}.Release|x86.Build.0 = Release|Any CPU 33 | EndGlobalSection 34 | EndGlobal 35 | -------------------------------------------------------------------------------- /GenealogyWebAPI.ClientSdk/FamilyName.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Code generated by Microsoft (R) AutoRest Code Generator. 3 | // Changes may cause incorrect behavior and will be lost if the code is 4 | // regenerated. 5 | // 6 | 7 | namespace GenealogyWebAPI.ClientSdk 8 | { 9 | using Microsoft.Rest; 10 | using Models; 11 | using Newtonsoft.Json; 12 | using System.Collections; 13 | using System.Collections.Generic; 14 | using System.IO; 15 | using System.Linq; 16 | using System.Net; 17 | using System.Net.Http; 18 | using System.Threading; 19 | using System.Threading.Tasks; 20 | 21 | /// 22 | /// FamilyName operations. 23 | /// 24 | public partial class FamilyName : IServiceOperations, IFamilyName 25 | { 26 | /// 27 | /// Initializes a new instance of the FamilyName class. 28 | /// 29 | /// 30 | /// Reference to the service client. 31 | /// 32 | /// 33 | /// Thrown when a required parameter is null 34 | /// 35 | public FamilyName(GenealogyAPI client) 36 | { 37 | if (client == null) 38 | { 39 | throw new System.ArgumentNullException("client"); 40 | } 41 | Client = client; 42 | } 43 | 44 | /// 45 | /// Gets a reference to the GenealogyAPI 46 | /// 47 | public GenealogyAPI Client { get; private set; } 48 | 49 | /// 50 | /// Retrieve profile of person based on name. 51 | /// 52 | /// 53 | /// Name of person. 54 | /// 55 | /// 56 | /// Headers that will be added to request. 57 | /// 58 | /// 59 | /// The cancellation token. 60 | /// 61 | /// 62 | /// Thrown when the operation returned an invalid status code 63 | /// 64 | /// 65 | /// Thrown when unable to deserialize the response 66 | /// 67 | /// 68 | /// Thrown when a required parameter is null 69 | /// 70 | /// 71 | /// Thrown when a required parameter is null 72 | /// 73 | /// 74 | /// A response object containing the response body and response headers. 75 | /// 76 | public async Task> GetWithHttpMessagesAsync(string name, Dictionary> customHeaders = null, CancellationToken cancellationToken = default(CancellationToken)) 77 | { 78 | if (name == null) 79 | { 80 | throw new ValidationException(ValidationRules.CannotBeNull, "name"); 81 | } 82 | // Tracing 83 | bool _shouldTrace = ServiceClientTracing.IsEnabled; 84 | string _invocationId = null; 85 | if (_shouldTrace) 86 | { 87 | _invocationId = ServiceClientTracing.NextInvocationId.ToString(); 88 | Dictionary tracingParameters = new Dictionary(); 89 | tracingParameters.Add("name", name); 90 | tracingParameters.Add("cancellationToken", cancellationToken); 91 | ServiceClientTracing.Enter(_invocationId, this, "Get", tracingParameters); 92 | } 93 | // Construct URL 94 | var _baseUrl = Client.BaseUri.AbsoluteUri; 95 | var _url = new System.Uri(new System.Uri(_baseUrl + (_baseUrl.EndsWith("/") ? "" : "/")), "api/v2.0/FamilyName/{name}").ToString(); 96 | _url = _url.Replace("{name}", System.Uri.EscapeDataString(name)); 97 | // Create HTTP transport objects 98 | var _httpRequest = new HttpRequestMessage(); 99 | HttpResponseMessage _httpResponse = null; 100 | _httpRequest.Method = new HttpMethod("GET"); 101 | _httpRequest.RequestUri = new System.Uri(_url); 102 | // Set Headers 103 | 104 | 105 | if (customHeaders != null) 106 | { 107 | foreach(var _header in customHeaders) 108 | { 109 | if (_httpRequest.Headers.Contains(_header.Key)) 110 | { 111 | _httpRequest.Headers.Remove(_header.Key); 112 | } 113 | _httpRequest.Headers.TryAddWithoutValidation(_header.Key, _header.Value); 114 | } 115 | } 116 | 117 | // Serialize Request 118 | string _requestContent = null; 119 | // Send Request 120 | if (_shouldTrace) 121 | { 122 | ServiceClientTracing.SendRequest(_invocationId, _httpRequest); 123 | } 124 | cancellationToken.ThrowIfCancellationRequested(); 125 | _httpResponse = await Client.HttpClient.SendAsync(_httpRequest, cancellationToken).ConfigureAwait(false); 126 | if (_shouldTrace) 127 | { 128 | ServiceClientTracing.ReceiveResponse(_invocationId, _httpResponse); 129 | } 130 | HttpStatusCode _statusCode = _httpResponse.StatusCode; 131 | cancellationToken.ThrowIfCancellationRequested(); 132 | string _responseContent = null; 133 | if ((int)_statusCode != 200 && (int)_statusCode != 400) 134 | { 135 | var ex = new HttpOperationException(string.Format("Operation returned an invalid status code '{0}'", _statusCode)); 136 | if (_httpResponse.Content != null) { 137 | _responseContent = await _httpResponse.Content.ReadAsStringAsync().ConfigureAwait(false); 138 | } 139 | else { 140 | _responseContent = string.Empty; 141 | } 142 | ex.Request = new HttpRequestMessageWrapper(_httpRequest, _requestContent); 143 | ex.Response = new HttpResponseMessageWrapper(_httpResponse, _responseContent); 144 | if (_shouldTrace) 145 | { 146 | ServiceClientTracing.Error(_invocationId, ex); 147 | } 148 | _httpRequest.Dispose(); 149 | if (_httpResponse != null) 150 | { 151 | _httpResponse.Dispose(); 152 | } 153 | throw ex; 154 | } 155 | // Create Result 156 | var _result = new HttpOperationResponse(); 157 | _result.Request = _httpRequest; 158 | _result.Response = _httpResponse; 159 | // Deserialize Response 160 | if ((int)_statusCode == 200) 161 | { 162 | _responseContent = await _httpResponse.Content.ReadAsStringAsync().ConfigureAwait(false); 163 | try 164 | { 165 | _result.Body = Microsoft.Rest.Serialization.SafeJsonConvert.DeserializeObject(_responseContent, Client.DeserializationSettings); 166 | } 167 | catch (JsonException ex) 168 | { 169 | _httpRequest.Dispose(); 170 | if (_httpResponse != null) 171 | { 172 | _httpResponse.Dispose(); 173 | } 174 | throw new SerializationException("Unable to deserialize the response.", _responseContent, ex); 175 | } 176 | } 177 | if (_shouldTrace) 178 | { 179 | ServiceClientTracing.Exit(_invocationId, _result); 180 | } 181 | return _result; 182 | } 183 | 184 | } 185 | } 186 | -------------------------------------------------------------------------------- /GenealogyWebAPI.ClientSdk/FamilyNameExtensions.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Code generated by Microsoft (R) AutoRest Code Generator. 3 | // Changes may cause incorrect behavior and will be lost if the code is 4 | // regenerated. 5 | // 6 | 7 | namespace GenealogyWebAPI.ClientSdk 8 | { 9 | using Models; 10 | using System.Threading; 11 | using System.Threading.Tasks; 12 | 13 | /// 14 | /// Extension methods for FamilyName. 15 | /// 16 | public static partial class FamilyNameExtensions 17 | { 18 | /// 19 | /// Retrieve profile of person based on name. 20 | /// 21 | /// 22 | /// The operations group for this extension method. 23 | /// 24 | /// 25 | /// Name of person. 26 | /// 27 | public static FamilyProfile Get(this IFamilyName operations, string name) 28 | { 29 | return operations.GetAsync(name).GetAwaiter().GetResult(); 30 | } 31 | 32 | /// 33 | /// Retrieve profile of person based on name. 34 | /// 35 | /// 36 | /// The operations group for this extension method. 37 | /// 38 | /// 39 | /// Name of person. 40 | /// 41 | /// 42 | /// The cancellation token. 43 | /// 44 | public static async Task GetAsync(this IFamilyName operations, string name, CancellationToken cancellationToken = default(CancellationToken)) 45 | { 46 | using (var _result = await operations.GetWithHttpMessagesAsync(name, null, cancellationToken).ConfigureAwait(false)) 47 | { 48 | return _result.Body; 49 | } 50 | } 51 | 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /GenealogyWebAPI.ClientSdk/GenealogyAPI.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Code generated by Microsoft (R) AutoRest Code Generator. 3 | // Changes may cause incorrect behavior and will be lost if the code is 4 | // regenerated. 5 | // 6 | 7 | namespace GenealogyWebAPI.ClientSdk 8 | { 9 | using Microsoft.Rest; 10 | using Microsoft.Rest.Serialization; 11 | using Models; 12 | using Newtonsoft.Json; 13 | using System.Collections; 14 | using System.Collections.Generic; 15 | using System.Net; 16 | using System.Net.Http; 17 | 18 | /// 19 | /// Building Web APIs Workshop Demo Web API 20 | /// 21 | public partial class GenealogyAPI : ServiceClient, IGenealogyAPI 22 | { 23 | /// 24 | /// The base URI of the service. 25 | /// 26 | public System.Uri BaseUri { get; set; } 27 | 28 | /// 29 | /// Gets or sets json serialization settings. 30 | /// 31 | public JsonSerializerSettings SerializationSettings { get; private set; } 32 | 33 | /// 34 | /// Gets or sets json deserialization settings. 35 | /// 36 | public JsonSerializerSettings DeserializationSettings { get; private set; } 37 | 38 | /// 39 | /// Gets the IFamilyName. 40 | /// 41 | public virtual IFamilyName FamilyName { get; private set; } 42 | 43 | /// 44 | /// Initializes a new instance of the GenealogyAPI class. 45 | /// 46 | /// 47 | /// Optional. The delegating handlers to add to the http client pipeline. 48 | /// 49 | public GenealogyAPI(params DelegatingHandler[] handlers) : base(handlers) 50 | { 51 | Initialize(); 52 | } 53 | 54 | /// 55 | /// Initializes a new instance of the GenealogyAPI class. 56 | /// 57 | /// 58 | /// Optional. The http client handler used to handle http transport. 59 | /// 60 | /// 61 | /// Optional. The delegating handlers to add to the http client pipeline. 62 | /// 63 | public GenealogyAPI(HttpClientHandler rootHandler, params DelegatingHandler[] handlers) : base(rootHandler, handlers) 64 | { 65 | Initialize(); 66 | } 67 | 68 | /// 69 | /// Initializes a new instance of the GenealogyAPI class. 70 | /// 71 | /// 72 | /// Optional. The base URI of the service. 73 | /// 74 | /// 75 | /// Optional. The delegating handlers to add to the http client pipeline. 76 | /// 77 | /// 78 | /// Thrown when a required parameter is null 79 | /// 80 | public GenealogyAPI(System.Uri baseUri, params DelegatingHandler[] handlers) : this(handlers) 81 | { 82 | if (baseUri == null) 83 | { 84 | throw new System.ArgumentNullException("baseUri"); 85 | } 86 | BaseUri = baseUri; 87 | } 88 | 89 | /// 90 | /// Initializes a new instance of the GenealogyAPI class. 91 | /// 92 | /// 93 | /// Optional. The base URI of the service. 94 | /// 95 | /// 96 | /// Optional. The http client handler used to handle http transport. 97 | /// 98 | /// 99 | /// Optional. The delegating handlers to add to the http client pipeline. 100 | /// 101 | /// 102 | /// Thrown when a required parameter is null 103 | /// 104 | public GenealogyAPI(System.Uri baseUri, HttpClientHandler rootHandler, params DelegatingHandler[] handlers) : this(rootHandler, handlers) 105 | { 106 | if (baseUri == null) 107 | { 108 | throw new System.ArgumentNullException("baseUri"); 109 | } 110 | BaseUri = baseUri; 111 | } 112 | 113 | /// 114 | /// An optional partial-method to perform custom initialization. 115 | /// 116 | partial void CustomInitialize(); 117 | /// 118 | /// Initializes client properties. 119 | /// 120 | private void Initialize() 121 | { 122 | FamilyName = new FamilyName(this); 123 | BaseUri = new System.Uri("https://localhost:44383"); 124 | SerializationSettings = new JsonSerializerSettings 125 | { 126 | Formatting = Newtonsoft.Json.Formatting.Indented, 127 | DateFormatHandling = Newtonsoft.Json.DateFormatHandling.IsoDateFormat, 128 | DateTimeZoneHandling = Newtonsoft.Json.DateTimeZoneHandling.Utc, 129 | NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore, 130 | ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Serialize, 131 | ContractResolver = new ReadOnlyJsonContractResolver(), 132 | Converters = new List 133 | { 134 | new Iso8601TimeSpanConverter() 135 | } 136 | }; 137 | DeserializationSettings = new JsonSerializerSettings 138 | { 139 | DateFormatHandling = Newtonsoft.Json.DateFormatHandling.IsoDateFormat, 140 | DateTimeZoneHandling = Newtonsoft.Json.DateTimeZoneHandling.Utc, 141 | NullValueHandling = Newtonsoft.Json.NullValueHandling.Ignore, 142 | ReferenceLoopHandling = Newtonsoft.Json.ReferenceLoopHandling.Serialize, 143 | ContractResolver = new ReadOnlyJsonContractResolver(), 144 | Converters = new List 145 | { 146 | new Iso8601TimeSpanConverter() 147 | } 148 | }; 149 | CustomInitialize(); 150 | } 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /GenealogyWebAPI.ClientSdk/GenealogyAPI.partial.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Net.Http; 4 | using System.Text; 5 | 6 | namespace GenealogyWebAPI.ClientSdk 7 | { 8 | public partial class GenealogyAPI 9 | { 10 | public GenealogyAPI(HttpClient client) : base(client, false) 11 | { 12 | Initialize(); 13 | BaseUri = client.BaseAddress; 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /GenealogyWebAPI.ClientSdk/GenealogyWebAPI.ClientSdk.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netstandard2.0 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /GenealogyWebAPI.ClientSdk/IFamilyName.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Code generated by Microsoft (R) AutoRest Code Generator. 3 | // Changes may cause incorrect behavior and will be lost if the code is 4 | // regenerated. 5 | // 6 | 7 | namespace GenealogyWebAPI.ClientSdk 8 | { 9 | using Microsoft.Rest; 10 | using Models; 11 | using System.Collections; 12 | using System.Collections.Generic; 13 | using System.Threading; 14 | using System.Threading.Tasks; 15 | 16 | /// 17 | /// FamilyName operations. 18 | /// 19 | public partial interface IFamilyName 20 | { 21 | /// 22 | /// Retrieve profile of person based on name. 23 | /// 24 | /// 25 | /// Name of person. 26 | /// 27 | /// 28 | /// The headers that will be added to request. 29 | /// 30 | /// 31 | /// The cancellation token. 32 | /// 33 | /// 34 | /// Thrown when the operation returned an invalid status code 35 | /// 36 | /// 37 | /// Thrown when unable to deserialize the response 38 | /// 39 | /// 40 | /// Thrown when a required parameter is null 41 | /// 42 | Task> GetWithHttpMessagesAsync(string name, Dictionary> customHeaders = null, CancellationToken cancellationToken = default(CancellationToken)); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /GenealogyWebAPI.ClientSdk/IGenealogyAPI.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Code generated by Microsoft (R) AutoRest Code Generator. 3 | // Changes may cause incorrect behavior and will be lost if the code is 4 | // regenerated. 5 | // 6 | 7 | namespace GenealogyWebAPI.ClientSdk 8 | { 9 | using Models; 10 | using Newtonsoft.Json; 11 | 12 | /// 13 | /// Building Web APIs Workshop Demo Web API 14 | /// 15 | public partial interface IGenealogyAPI : System.IDisposable 16 | { 17 | /// 18 | /// The base URI of the service. 19 | /// 20 | System.Uri BaseUri { get; set; } 21 | 22 | /// 23 | /// Gets or sets json serialization settings. 24 | /// 25 | JsonSerializerSettings SerializationSettings { get; } 26 | 27 | /// 28 | /// Gets or sets json deserialization settings. 29 | /// 30 | JsonSerializerSettings DeserializationSettings { get; } 31 | 32 | 33 | /// 34 | /// Gets the IFamilyName. 35 | /// 36 | IFamilyName FamilyName { get; } 37 | 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /GenealogyWebAPI.ClientSdk/Models/FamilyProfile.cs: -------------------------------------------------------------------------------- 1 | // 2 | // Code generated by Microsoft (R) AutoRest Code Generator. 3 | // Changes may cause incorrect behavior and will be lost if the code is 4 | // regenerated. 5 | // 6 | 7 | namespace GenealogyWebAPI.ClientSdk.Models 8 | { 9 | using Microsoft.Rest; 10 | using Newtonsoft.Json; 11 | using System.Linq; 12 | 13 | public partial class FamilyProfile 14 | { 15 | /// 16 | /// Initializes a new instance of the FamilyProfile class. 17 | /// 18 | public FamilyProfile() 19 | { 20 | CustomInit(); 21 | } 22 | 23 | /// 24 | /// Initializes a new instance of the FamilyProfile class. 25 | /// 26 | /// Possible values include: 'Male', 'Female', 27 | /// 'Unknown' 28 | public FamilyProfile(string gender, string name = default(string)) 29 | { 30 | Name = name; 31 | Gender = gender; 32 | CustomInit(); 33 | } 34 | 35 | /// 36 | /// An initialization method that performs custom operations like setting defaults 37 | /// 38 | partial void CustomInit(); 39 | 40 | /// 41 | /// 42 | [JsonProperty(PropertyName = "name")] 43 | public string Name { get; set; } 44 | 45 | /// 46 | /// Gets or sets possible values include: 'Male', 'Female', 'Unknown' 47 | /// 48 | [JsonProperty(PropertyName = "gender")] 49 | public string Gender { get; set; } 50 | 51 | /// 52 | /// Validate the object. 53 | /// 54 | /// 55 | /// Thrown if validation fails 56 | /// 57 | public virtual void Validate() 58 | { 59 | if (Gender == null) 60 | { 61 | throw new ValidationException(ValidationRules.CannotBeNull, "Gender"); 62 | } 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /GenealogyWebAPI.IntegrationTests/GenealogyWebAPI.IntegrationTests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp2.2 5 | 6 | false 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | PreserveNewest 16 | PreserveNewest 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /GenealogyWebAPI.IntegrationTests/MockGenderizeClient.cs: -------------------------------------------------------------------------------- 1 | using GenealogyWebAPI.Model; 2 | using GenealogyWebAPI.Proxies; 3 | using Refit; 4 | using System; 5 | using System.Collections.Generic; 6 | using System.Text; 7 | using System.Threading.Tasks; 8 | 9 | namespace GenealogyWebAPI.IntegrationTests 10 | { 11 | internal class MockGenderizeClient: IGenderizeClient 12 | { 13 | public Task GetGenderForName(string name, string key) 14 | { 15 | return Task.FromResult(new GenderizeResult() { Name = "alex", Gender = "male" }); 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /GenealogyWebAPI.IntegrationTests/ServiceContractIntegrationTests.cs: -------------------------------------------------------------------------------- 1 | using GenealogyWebAPI.ClientSdk; 2 | using GenealogyWebAPI.Model; 3 | using GenealogyWebAPI.Proxies; 4 | using Microsoft.AspNetCore.Hosting; 5 | using Microsoft.AspNetCore.TestHost; 6 | using Microsoft.Extensions.DependencyInjection; 7 | using Microsoft.VisualStudio.TestTools.UnitTesting; 8 | using System.Net; 9 | using System.Net.Http; 10 | using System.Threading.Tasks; 11 | 12 | namespace GenealogyWebAPI.IntegrationTests 13 | { 14 | [TestClass] 15 | public class ServiceContractIntegrationTests 16 | { 17 | TestServer server; 18 | HttpClient client; 19 | GenealogyAPI proxy; 20 | 21 | [TestInitialize] 22 | public void Initialize() 23 | { 24 | var builder = new WebHostBuilder() 25 | .UseEnvironment("Development") 26 | .UseStartup() 27 | .ConfigureTestServices(services => 28 | { 29 | services.AddTransient(); 30 | }); 31 | 32 | // Create test stack 33 | server = new TestServer(builder); 34 | client = server.CreateClient(); 35 | proxy = new GenealogyAPI(client); 36 | } 37 | 38 | [TestMethod] 39 | public async Task OpenApiDocumentationV2Available() 40 | { 41 | // Act 42 | var response = await client.GetAsync("/swagger/index.html?url=/swagger/v2/swagger.json"); 43 | 44 | // Assert 45 | response.EnsureSuccessStatusCode(); 46 | string responseHtml = await response.Content.ReadAsStringAsync(); 47 | Assert.IsTrue(responseHtml.Contains("swagger")); 48 | } 49 | 50 | [TestMethod] 51 | public async Task GetFamilyNameV2() 52 | { 53 | // Arrange 54 | string name = "alex"; 55 | 56 | // Act 57 | var result = await proxy.FamilyName.GetWithHttpMessagesAsync(name); 58 | 59 | // Assert 60 | Assert.IsNotNull(result, "Should have received a response."); 61 | Assert.AreEqual(HttpStatusCode.OK, result.Response.StatusCode, "Status code should be 200 OK"); 62 | GenderizeResult response = await result.Response.Content.ReadAsAsync(); 63 | Assert.AreEqual(name, response.Name, "Response body should contain original name"); 64 | } 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /GenealogyWebAPI.IntegrationTests/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "GenderizeApiOptions": { 3 | "BaseUrl": "https://api.genderize.io.local/" 4 | }, 5 | } 6 | -------------------------------------------------------------------------------- /GenealogyWebAPI/Connected Services/Application Insights/ConnectedService.json: -------------------------------------------------------------------------------- 1 | { 2 | "ProviderId": "Microsoft.ApplicationInsights.ConnectedService.ConnectedServiceProvider", 3 | "Version": "8.11.10402.2", 4 | "GettingStartedDocument": { 5 | "Uri": "https://go.microsoft.com/fwlink/?LinkID=798432" 6 | } 7 | } -------------------------------------------------------------------------------- /GenealogyWebAPI/Controllers/FamilyNameV1Controller.cs: -------------------------------------------------------------------------------- 1 | using GenealogyWebAPI.Model; 2 | using GenealogyWebAPI.Proxies; 3 | using Microsoft.AspNetCore.Http; 4 | using Microsoft.AspNetCore.Mvc; 5 | using Microsoft.Extensions.Options; 6 | using Newtonsoft.Json; 7 | using Polly.Timeout; 8 | using System; 9 | using System.Net.Http; 10 | using System.Threading.Tasks; 11 | 12 | namespace GenealogyWebAPI.Controllers 13 | { 14 | [Route("api/v{version:apiVersion}/[controller]")] 15 | [ApiController] 16 | [ApiVersion("1.0", Deprecated = true)] 17 | [AdvertiseApiVersions("2.0")] 18 | public class FamilyNameController : ControllerBase 19 | { 20 | // GET api/familyname/name 21 | /// 22 | /// Retrieve profile of person based on name. 23 | /// 24 | /// Name of person. 25 | /// Detailed information regarding profile. 26 | /// The profile was successfully retrieved. 27 | /// The request parameters were invalid or a timeout while retrieving profile occurred. 28 | [HttpGet("{name}")] 29 | [ProducesResponseType(typeof(string), 200)] 30 | [ProducesResponseType(400)] 31 | public ActionResult Get(string name) 32 | { 33 | var version = HttpContext.GetRequestedApiVersion(); 34 | return Ok(version); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /GenealogyWebAPI/Controllers/FamilyNameV2Controller.cs: -------------------------------------------------------------------------------- 1 | using GenealogyWebAPI.Model; 2 | using GenealogyWebAPI.Proxies; 3 | using Microsoft.AspNetCore.Http; 4 | using Microsoft.AspNetCore.Mvc; 5 | using Microsoft.Extensions.Logging; 6 | using Microsoft.Extensions.Options; 7 | using Polly.Timeout; 8 | using System; 9 | using System.Net.Http; 10 | using System.Threading.Tasks; 11 | 12 | namespace GenealogyWebAPI.Controllers.V2 13 | { 14 | [Route("api/v{version:apiVersion}/[controller]")] 15 | [ApiController] 16 | [ApiVersion("2.0")] 17 | [Produces("application/xml", "application/json")] 18 | public class FamilyNameController : ControllerBase 19 | { 20 | private readonly IGenderizeClient genderizeClient; 21 | private readonly ILogger logger; 22 | private readonly IOptionsSnapshot genderizeOptions; 23 | 24 | public FamilyNameController( 25 | IGenderizeClient genderizeClient, 26 | IOptionsSnapshot genderizeOptions, 27 | ILoggerFactory logger) 28 | { 29 | this.genderizeClient = genderizeClient; 30 | this.genderizeOptions = genderizeOptions; 31 | this.logger = logger.CreateLogger(); 32 | } 33 | 34 | // GET api/familyname/name 35 | /// 36 | /// Retrieve profile of person based on name. 37 | /// 38 | /// Name of person. 39 | /// Detailed information regarding profile. 40 | /// The profile was successfully retrieved. 41 | /// The request parameters were invalid or a timeout while retrieving profile occurred. 42 | [HttpGet("{name}")] 43 | [ProducesResponseType(typeof(FamilyProfile), 200)] 44 | [ProducesResponseType(400)] 45 | public async Task> Get(string name) 46 | { 47 | GenderizeResult result = null; 48 | FamilyProfile profile; 49 | 50 | try 51 | { 52 | string baseUrl = genderizeOptions.Value.BaseUrl; 53 | string key = genderizeOptions.Value.DeveloperApiKey; 54 | 55 | logger.LogInformation("Acquiring name details for {FamilyName}.", name); 56 | 57 | result = await genderizeClient.GetGenderForName(name, key); 58 | Gender gender; 59 | profile = new FamilyProfile() { 60 | Name = name, 61 | Gender = Enum.TryParse(result.Gender, true, out gender) 62 | ? gender : Gender.Unknown 63 | }; 64 | } 65 | catch (HttpRequestException ex) 66 | { 67 | logger.LogWarning(ex, "Http request failed."); 68 | return StatusCode(StatusCodes.Status502BadGateway, "Failed request to external resource."); 69 | } 70 | catch (TimeoutRejectedException ex) 71 | { 72 | logger.LogWarning(ex, "Timeout occurred when retrieving details for {FamilyName}.", name); 73 | return StatusCode(StatusCodes.Status504GatewayTimeout, "Timeout on external web request."); 74 | } 75 | catch (Exception ex) 76 | { 77 | logger.LogError(ex, "Unknown exception occurred while retrieving gender details."); 78 | 79 | // Exception shielding for all other exceptions 80 | return StatusCode(StatusCodes.Status500InternalServerError, "Request could not be processed."); 81 | } 82 | return Ok(profile); 83 | } 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /GenealogyWebAPI/Controllers/HomeController.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.AspNetCore.Mvc; 2 | using NSwag.Annotations; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Threading.Tasks; 7 | 8 | namespace GenealogyWebAPI.Controllers 9 | { 10 | [SwaggerIgnore] 11 | public class HomeController : Controller 12 | { 13 | // GET: // 14 | public IActionResult Index() 15 | { 16 | return new RedirectResult("~/swagger"); 17 | } 18 | } 19 | } -------------------------------------------------------------------------------- /GenealogyWebAPI/Features.cs: -------------------------------------------------------------------------------- 1 | using FeatureToggle; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | 7 | namespace GenealogyWebAPI 8 | { 9 | public class AdvancedHealthFeature : SimpleFeatureToggle { } 10 | } 11 | -------------------------------------------------------------------------------- /GenealogyWebAPI/GenderizeApiOptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | 6 | namespace GenealogyWebAPI 7 | { 8 | public class GenderizeApiOptions 9 | { 10 | public string BaseUrl { get; set; } 11 | public string DeveloperApiKey { get; set; } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /GenealogyWebAPI/GenealogyWebAPI.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp2.2 5 | 87c32959-3721-4d6b-8a72-9896f48fd46b 6 | 7 | 8 | 9 | bin\Debug\netcoreapp2.1\GenealogyWebAPI.xml 10 | 1701;1702;1705;CS1591 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /GenealogyWebAPI/Model/FamilyProfile.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using Newtonsoft.Json.Converters; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Threading.Tasks; 7 | 8 | namespace GenealogyWebAPI.Model 9 | { 10 | public class FamilyProfile 11 | { 12 | public string Name { get; set; } 13 | [JsonConverter(typeof(StringEnumConverter))] 14 | public Gender Gender { get; set; } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /GenealogyWebAPI/Model/Gender.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | 6 | namespace GenealogyWebAPI.Model 7 | { 8 | public enum Gender 9 | { 10 | Male, 11 | Female, 12 | Unknown 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /GenealogyWebAPI/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | using Microsoft.AspNetCore; 7 | using Microsoft.AspNetCore.Hosting; 8 | using Microsoft.Extensions.Configuration; 9 | using Microsoft.Extensions.Logging; 10 | 11 | namespace GenealogyWebAPI 12 | { 13 | public class Program 14 | { 15 | public static void Main(string[] args) 16 | { 17 | CreateWebHostBuilder(args).Build().Run(); 18 | } 19 | 20 | public static IWebHostBuilder CreateWebHostBuilder(string[] args) 21 | { 22 | //return WebHost.CreateDefaultBuilder(); 23 | var builder = new WebHostBuilder() 24 | .UseKestrel((builderContext, options) => 25 | { 26 | options.Configure(builderContext.Configuration.GetSection("Kestrel")); 27 | }) 28 | .UseContentRoot(Directory.GetCurrentDirectory()) 29 | .UseIISIntegration() 30 | .UseHealthChecks("/health", TimeSpan.FromSeconds(3)) // Or to host on a separate port: .UseHealthChecks(port) 31 | .UseDefaultServiceProvider((context, options) => 32 | { 33 | options.ValidateScopes = context.HostingEnvironment.IsDevelopment(); 34 | }) 35 | .UseStartup(); 36 | 37 | if (args != null) 38 | { 39 | builder.UseConfiguration(new ConfigurationBuilder().AddCommandLine(args).Build()); 40 | } 41 | 42 | return builder; 43 | 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /GenealogyWebAPI/Proxies/GenderizeResult.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Linq; 5 | using System.Threading.Tasks; 6 | 7 | namespace GenealogyWebAPI.Model 8 | { 9 | public class GenderizeResult 10 | { 11 | [JsonProperty(PropertyName = "name")] 12 | public string Name { get; set; } 13 | [JsonProperty(PropertyName = "gender")] 14 | public string Gender { get; set; } 15 | [JsonProperty(PropertyName = "probability")] 16 | public double Probability { get; set; } 17 | [JsonProperty(PropertyName = "count")] 18 | public int Count { get; set; } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /GenealogyWebAPI/Proxies/IGenderizeClient.cs: -------------------------------------------------------------------------------- 1 | using GenealogyWebAPI.Model; 2 | using Refit; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Linq; 6 | using System.Threading.Tasks; 7 | 8 | namespace GenealogyWebAPI.Proxies 9 | { 10 | // https://api.genderize.io/?name=igor&country_id=ua&apikey= 11 | // https://genderize.io/ 12 | 13 | [Headers("User-Agent: Genderize IO WebAPI Client 2.0")] 14 | public interface IGenderizeClient 15 | { 16 | [Get("/")] 17 | Task GetGenderForName(string name, [AliasAs("apikey")] string key); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /GenealogyWebAPI/Startup.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Net; 5 | using System.Net.Http; 6 | using System.Reflection; 7 | using System.Threading.Tasks; 8 | using FeatureToggle.Internal; 9 | using GenealogyWebAPI.Proxies; 10 | using Microsoft.AspNetCore.Builder; 11 | using Microsoft.AspNetCore.Hosting; 12 | using Microsoft.AspNetCore.Http; 13 | using Microsoft.AspNetCore.HttpsPolicy; 14 | using Microsoft.AspNetCore.Mvc; 15 | using Microsoft.Extensions.Configuration; 16 | using Microsoft.Extensions.DependencyInjection; 17 | using Microsoft.Extensions.HealthChecks; 18 | using Microsoft.Extensions.Logging; 19 | using Microsoft.Extensions.Options; 20 | using NSwag.AspNetCore; 21 | using NSwag.SwaggerGeneration.Processors; 22 | using NSwag.SwaggerGeneration.WebApi; 23 | using Polly; 24 | using Polly.Registry; 25 | using Refit; 26 | 27 | namespace GenealogyWebAPI 28 | { 29 | 30 | public class Startup 31 | { 32 | 33 | private IPolicyRegistry policyRegistry; 34 | 35 | public Startup(IConfiguration configuration, IHostingEnvironment env) 36 | { 37 | var builder = new ConfigurationBuilder() 38 | .SetBasePath(env.ContentRootPath) 39 | .AddJsonFile("appsettings.json", optional: true, reloadOnChange: true) 40 | .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true, reloadOnChange: true) 41 | .AddEnvironmentVariables(); 42 | 43 | if (env.IsDevelopment()) 44 | { 45 | var appAssembly = Assembly.Load(new AssemblyName(env.ApplicationName)); 46 | if (appAssembly != null) 47 | { 48 | builder.AddUserSecrets(appAssembly, optional: true); 49 | } 50 | } 51 | 52 | Configuration = builder.Build(); 53 | 54 | if (env.IsProduction()) 55 | { 56 | builder.AddAzureKeyVault(Configuration["KeyVaultName"]); 57 | Configuration = builder.Build(); 58 | } 59 | } 60 | 61 | public IConfigurationRoot Configuration { get; } 62 | 63 | // This method gets called by the runtime. Use this method to add services to the container. 64 | public void ConfigureServices(IServiceCollection services) 65 | { 66 | services.AddMvc() 67 | .AddXmlSerializerFormatters() 68 | .SetCompatibilityVersion(CompatibilityVersion.Version_2_1); 69 | 70 | // Options for particular external services 71 | services.Configure(Configuration.GetSection("GenderizeApiOptions")); 72 | 73 | ConfigurePolicies(services); 74 | ConfigureFeatures(services); 75 | ConfigureHealth(services); 76 | ConfigureOpenApi(services); 77 | ConfigureApiOptions(services); 78 | ConfigureHttpClients(services); 79 | ConfigureVersioning(services); 80 | ConfigureApplicationInsights(services); 81 | ConfigureHSTS(services); 82 | } 83 | 84 | private void ConfigureHSTS(IServiceCollection services) 85 | { 86 | services.AddHsts( 87 | options => 88 | { 89 | options.MaxAge = TimeSpan.FromDays(100); 90 | options.IncludeSubDomains = true; 91 | options.Preload = true; 92 | }); 93 | } 94 | 95 | private void ConfigureFeatures(IServiceCollection services) 96 | { 97 | var provider = new AppSettingsProvider { Configuration = Configuration }; 98 | services.AddSingleton(new AdvancedHealthFeature { ToggleValueProvider = provider }); 99 | } 100 | 101 | private void ConfigureApplicationInsights(IServiceCollection services) 102 | { 103 | IHostingEnvironment env = services.BuildServiceProvider().GetRequiredService(); 104 | services.AddApplicationInsightsTelemetry(options => { 105 | options.DeveloperMode = env.IsDevelopment(); 106 | options.InstrumentationKey = Configuration["ApplicationInsights:InstrumentationKey"]; 107 | }); 108 | } 109 | 110 | private void ConfigureVersioning(IServiceCollection services) 111 | { 112 | services.AddApiVersioning(options => 113 | { 114 | options.DefaultApiVersion = new ApiVersion(1, 0); 115 | options.AssumeDefaultVersionWhenUnspecified = true; 116 | // Includes headers "api-supported-versions" and "api-deprecated-versions" 117 | options.ReportApiVersions = true; 118 | }); 119 | 120 | // Alternative to attribute based versioning 121 | //options.Conventions.Controller() 122 | // .HasDeprecatedApiVersion(new ApiVersion(0, 9)) 123 | // .HasApiVersion(1) 124 | // .AdvertisesApiVersion(2) 125 | // .Action(a => a.Get(default(int))).MapToApiVersion(1); 126 | 127 | } 128 | 129 | private void ConfigurePolicies(IServiceCollection services) 130 | { 131 | policyRegistry = services.AddPolicyRegistry(); 132 | var timeoutPolicy = Policy.TimeoutAsync(TimeSpan.FromMilliseconds(1500)); 133 | policyRegistry.Add("timeout", timeoutPolicy); 134 | } 135 | 136 | private void ConfigureHttpClients(IServiceCollection services) 137 | { 138 | services.AddHttpClient("Genderize", options => 139 | { 140 | options.BaseAddress = new Uri(Configuration["GenderizeApiOptions:BaseUrl"]); 141 | options.Timeout = TimeSpan.FromMilliseconds(15000); 142 | options.DefaultRequestHeaders.Add("ClientFactory", "Check"); 143 | }) 144 | .AddPolicyHandlerFromRegistry("timeout") 145 | .AddTransientHttpErrorPolicy(p => p.RetryAsync(3)) 146 | .AddTypedClient(client => RestService.For(client)); 147 | } 148 | 149 | private void ConfigureApiOptions(IServiceCollection services) 150 | { 151 | services.Configure(options => 152 | { 153 | options.InvalidModelStateResponseFactory = context => 154 | { 155 | var problemDetails = new ValidationProblemDetails(context.ModelState) 156 | { 157 | Instance = context.HttpContext.Request.Path, 158 | Status = StatusCodes.Status400BadRequest, 159 | Type = "https://asp.net/core", 160 | Detail = "Please refer to the errors property for additional details." 161 | }; 162 | return new BadRequestObjectResult(problemDetails) 163 | { 164 | ContentTypes = { "application/problem+json", "application/problem+xml" } 165 | }; 166 | }; 167 | }); 168 | } 169 | 170 | private void ConfigureOpenApi(IServiceCollection services) 171 | { 172 | services.AddSwaggerDocument(); 173 | } 174 | 175 | private void ConfigureHealth(IServiceCollection services) 176 | { 177 | services.AddHealthChecks(checks => 178 | { 179 | checks 180 | .AddUrlCheck(Configuration["GenderizeApiOptions:BaseUrl"], 181 | response => 182 | { 183 | // Custom check for healthy service 184 | var status = response.StatusCode == HttpStatusCode.UnprocessableEntity ? CheckStatus.Healthy : CheckStatus.Unhealthy; 185 | return new ValueTask(HealthCheckResult.FromStatus(status, "Genderize API base URL reachable.")); 186 | } 187 | ); 188 | 189 | // Use feature toggle to add this functionality 190 | var feature = services.BuildServiceProvider().GetRequiredService(); 191 | if (feature.FeatureEnabled) 192 | { 193 | checks.AddHealthCheckGroup( 194 | "memory", 195 | group => group 196 | .AddPrivateMemorySizeCheck(200000000) // Maximum private memory 197 | .AddVirtualMemorySizeCheck(3000000000000) 198 | .AddWorkingSetCheck(200000000), 199 | CheckStatus.Unhealthy 200 | ); 201 | } 202 | }); 203 | } 204 | 205 | // This method gets called by the runtime. Use this method to configure the HTTP request pipeline. 206 | public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory) 207 | { 208 | loggerFactory.AddApplicationInsights(app.ApplicationServices, LogLevel.Information); 209 | loggerFactory.AddEventSourceLogger(); // ETW on Windows, dev/null on other platforms 210 | loggerFactory.AddConsole(); 211 | loggerFactory.AddDebug(); 212 | 213 | if (env.IsDevelopment()) 214 | { 215 | app.UseDeveloperExceptionPage(); 216 | // Do not expose Swagger interface in production 217 | app.UseSwaggerUi3(typeof(Startup).GetTypeInfo().Assembly, settings => 218 | { 219 | settings.DocumentPath = "/swagger/v2/swagger.json"; 220 | settings.EnableTryItOut = true; 221 | settings.DocExpansion = "list"; 222 | settings.PostProcess = document => 223 | { 224 | document.BasePath = "/"; 225 | }; 226 | settings.GeneratorSettings.Description = "Building Web APIs Workshop Demo Web API"; 227 | settings.GeneratorSettings.Title = "Genealogy API"; 228 | settings.GeneratorSettings.Version = "2.0"; 229 | settings.GeneratorSettings.OperationProcessors.Add( 230 | new ApiVersionProcessor() { IncludedVersions = new[] { "2.0" } } 231 | ); 232 | }); 233 | 234 | app.UseSwaggerUi3(typeof(Startup).GetTypeInfo().Assembly, settings => 235 | { 236 | settings.DocumentPath = "/swagger/v1/swagger.json"; 237 | settings.EnableTryItOut = true; 238 | settings.DocExpansion = "list"; 239 | settings.PostProcess = document => 240 | { 241 | document.BasePath = "/"; 242 | }; 243 | settings.GeneratorSettings.Description = "Building Web APIs Workshop Demo Web API"; 244 | settings.GeneratorSettings.Title = "Genealogy API"; 245 | settings.GeneratorSettings.Version = "1.0"; 246 | settings.GeneratorSettings.OperationProcessors.Add( 247 | new ApiVersionProcessor() { IncludedVersions = new[] { "1.0" } } 248 | ); 249 | }); 250 | } 251 | else 252 | { 253 | app.UseHsts(); 254 | app.UseHttpsRedirection(); 255 | } 256 | 257 | app.UseMvcWithDefaultRoute(); 258 | } 259 | } 260 | } 261 | -------------------------------------------------------------------------------- /GenealogyWebAPI/appsettings.Development.json: -------------------------------------------------------------------------------- 1 | { 2 | "GenderizeApiOptions": { 3 | "BaseUrl": "https://api.genderize.io" 4 | }, 5 | "Logging": { 6 | "IncludeScopes": false, 7 | "LogLevel": { 8 | "Default": "Information", 9 | "System": "Warning", 10 | "Microsoft": "Warning" 11 | }, 12 | "Debug": { 13 | "LogLevel": { 14 | "Default": "Information" 15 | } 16 | } 17 | }, 18 | } 19 | -------------------------------------------------------------------------------- /GenealogyWebAPI/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "GenderizeApiOptions": { 3 | "BaseUrl": "https://api.genderize.io/" 4 | }, 5 | "KeyVaultName": "https://{yourkeyvaultname}.vault.azure.net/", 6 | "Logging": { 7 | "LogLevel": { 8 | "Default": "Warning" 9 | } 10 | }, 11 | "AllowedHosts": "*", 12 | "ApplicationInsights": { 13 | "InstrumentationKey": "{yourapplicationsinsightskey}" 14 | }, 15 | "FeatureToggle": { 16 | "AdvancedHealthFeature": "true" 17 | } 18 | } -------------------------------------------------------------------------------- /GenerateClientSdk.ps1: -------------------------------------------------------------------------------- 1 | # TODO: Parametrize URL and name 2 | iwr https://localhost:44383/swagger/v1/swagger.json -o BuildWebAPIs.v1.json 3 | iwr https://localhost:44383/swagger/v2/swagger.json -o BuildWebAPIs.v2.json 4 | autorest --input-file=BuildWebAPIs.v2.json --csharp --output-folder=GenealogyWebAPI.ClientSdk --namespace=GenealogyWebAPI.ClientSdk 5 | 6 | dotnet new sln -n GenealogyClientSdk --force 7 | dotnet new classlib -o GenealogyWebAPI.ClientSdk --force 8 | dotnet sln GenealogyClientSdk.sln add GenealogyWebAPI.ClientSdk 9 | cd GenealogyWebAPI.ClientSdk 10 | del class1.cs 11 | 12 | dotnet add package Microsoft.AspNetCore --version 2.1-preview2-final 13 | dotnet add package Microsoft.Rest.ClientRuntime --version 2.3.11 14 | dotnet add package Newtonsoft.Json --version 11.0.2 15 | 16 | dotnet restore 17 | dotnet build 18 | dotnet pack --include-symbols -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # Workshop Building Web APIs using ASP.NET Core 2.2 2 | ## Robust and production ready 3 | 4 | Welcome to the workshop "Building Web APIs using ASP.NET Core 2.2". 5 | 6 | In this workshop you will learn about creating production-ready Web APIs with 7 | This workshop is a living workshop: evolving, maturing and improving. 8 | 9 | The general idea is that you will be able to observe building a ASP.NET Core Web API 10 | from "File, New Project" to include more and more features that will help running it reliably in production. 11 | The topics that are addressed are: 12 | 13 | - Service contract integration testing 14 | - Creating client SDKs 15 | - Health checks 16 | - Instrumentation and telemetry for monitoring 17 | - Versioning 18 | - OpenAPI documentation and Swagger tooling 19 | - Resiliency of external resources (using HttpClientFactory, Refit and Polly) 20 | - Security: User Secrets, HTTPS and HSTS 21 | 22 | To get the best experience from the workshop it is recommended that you prepare the following: 23 | - Laptop for browsing around in the code 24 | - Azure subscription to create and use App Services and Application Insights 25 | 26 | ### Preparing for workshop 27 | 28 | Even though you can certainly participate in this workshop without a laptop, 29 | it will be cool if you can install your development machine ahead of time. 30 | 31 | For hosting your API in Microsoft Azure you will need an Azure subscription. 32 | You can create a trial account if you do not have a subscription available. 33 | Visit https://azure.microsoft.com/free/ to get started with a trial 34 | 35 | ### Installation list 36 | - Visual Studio 15.6.6 or later. Try Visual Studio 15.7.0 Preview 3.0 if you are adventurous. 37 | - https://www.visualstudio.com/downloads/ 38 | Visual Studio Code (if you are not on Windows) 39 | - https://code.visualstudio.com/download 40 | - .NET Core SDK 2.1 Preview 2 (or later) 41 | - https://github.com/dotnet/core/blob/master/release-notes/download-archives/2.1.0-preview2-download.md 42 | - AutoRest for generation of REST API clients 43 | - https://github.com/Azure/autorest#installing-autorest 44 | - Postman to issue REST calls (optional) 45 | - https://www.getpostman.com/apps 46 | - Git for Windows and a GUI client 47 | - https://git-scm.com/download/win 48 | - https://git-scm.com/download/gui/windows 49 | 50 | ### Clone the repository 51 | 52 | The repository is located on GitHub at https://github.com/alexthissen/BuildWebAPIsWorkshop. 53 | The clone URL is https://github.com/alexthissen/BuildWebAPIsWorkshop.git. 54 | 55 | Your options: 56 | - Use Visual Studio 2017 57 | - Git UI tooling of your choice. 58 | - Git Command Line Interface (CLI) 59 | 60 | Start a command console and execute Git CLI commands: 61 | ``` 62 | cd c:\Sources 63 | git clone https://github.com/alexthissen/BuildWebAPIsWorkshop.git BuildWebAPIsWorkshop 64 | ``` 65 | 66 | You can switch to specific point in the version history. 67 | These points show individual features being implemented as increments. 68 | You can follow along on your laptop and inspect at a later moment in time as well. 69 | 70 | Some useful Git CLI commands: 71 | ``` 72 | # List available tags 73 | git tag -l 74 | 75 | # Checkout specific tag 76 | git checkout tags/{tagname} 77 | 78 | # Return to current version 79 | git checkout master 80 | ``` --------------------------------------------------------------------------------