├── .appveyor.yml ├── .gitattributes ├── .gitignore ├── .travis.yml ├── .vsts-pipelines └── builds │ ├── ci-internal.yml │ └── ci-public.yml ├── CONTRIBUTING.md ├── Directory.Build.props ├── Directory.Build.targets ├── HttpClientFactory.sln ├── LICENSE.txt ├── NuGet.config ├── NuGetPackageVerifier.json ├── README.md ├── benchmarks └── Microsoft.Extensions.Http.Performance │ ├── Configs │ └── CoreConfig.cs │ ├── CreationOverheadBenchmark.cs │ ├── FakeClientHandler.cs │ ├── FakeLoggerProvider.cs │ ├── LoggingOverheadBenchmark.cs │ └── Microsoft.Extensions.Http.Performance.csproj ├── build.cmd ├── build.sh ├── build ├── Key.snk ├── dependencies.props ├── repo.props └── sources.props ├── korebuild-lock.txt ├── korebuild.json ├── run.cmd ├── run.ps1 ├── run.sh ├── samples └── HttpClientFactorySample │ ├── HttpClientFactorySample.csproj │ └── Program.cs ├── src ├── Directory.Build.props ├── Microsoft.Extensions.Http.Polly │ ├── DependencyInjection │ │ ├── PollyHttpClientBuilderExtensions.cs │ │ └── PollyServiceCollectionExtensions.cs │ ├── HttpRequestMessageExtensions.cs │ ├── Microsoft.Extensions.Http.Polly.csproj │ ├── PolicyHttpMessageHandler.cs │ ├── Properties │ │ ├── AssemblyInfo.cs │ │ └── Resources.Designer.cs │ ├── Resources.resx │ └── baseline.netcore.json └── Microsoft.Extensions.Http │ ├── ActiveHandlerTrackingEntry.cs │ ├── DefaultHttpClientFactory.cs │ ├── DefaultHttpMessageHandlerBuilder.cs │ ├── DefaultTypedHttpClientFactory.cs │ ├── DependencyInjection │ ├── DefaultHttpClientBuilder.cs │ ├── HttpClientBuilderExtensions.cs │ ├── HttpClientFactoryServiceCollectionExtensions.cs │ └── IHttpClientBuilder.cs │ ├── ExpiredHandlerTrackingEntry.cs │ ├── HttpClientFactoryExtensions.cs │ ├── HttpClientFactoryOptions.cs │ ├── HttpMessageHandlerBuilder.cs │ ├── HttpMessageHandlerFactoryExtensions.cs │ ├── IHttpClientFactory.cs │ ├── IHttpMessageHandlerBuilderFilter.cs │ ├── IHttpMessageHandlerFactory.cs │ ├── ITypedHttpClientFactory.cs │ ├── LifetimeTrackingHttpMessageHandler.cs │ ├── Logging │ ├── HttpHeadersLogValue.cs │ ├── LoggingHttpMessageHandler.cs │ ├── LoggingHttpMessageHandlerBuilderFilter.cs │ └── LoggingScopeHttpMessageHandler.cs │ ├── Microsoft.Extensions.Http.csproj │ ├── Properties │ ├── AssemblyInfo.cs │ └── Resources.Designer.cs │ ├── Resources.resx │ └── baseline.netcore.json ├── test ├── Directory.Build.props ├── Microsoft.Extensions.Http.Polly.Test │ ├── DependencyInjection │ │ └── PollyHttpClientBuilderExtensionsTest.cs │ ├── HttpRequestMessageExtensionsTest.cs │ ├── Microsoft.Extensions.Http.Polly.Test.csproj │ └── PolicyHttpMessageHandlerTest.cs └── Microsoft.Extensions.Http.Test │ ├── DefaultHttpClientFactoryTest.cs │ ├── DefaultHttpMessageHandlerBuilderTest.cs │ ├── DependencyInjection │ ├── HttpClientFactoryServiceCollectionExtensionsTest.cs │ └── OtherTestOptions.cs │ ├── HttpMessageHandlerBuilderTest.cs │ ├── ITestTypedClient.cs │ ├── Microsoft.Extensions.Http.Test.csproj │ └── TestTypedClient.cs └── version.props /.appveyor.yml: -------------------------------------------------------------------------------- 1 | init: 2 | - git config --global core.autocrlf true 3 | branches: 4 | only: 5 | - master 6 | - /^release\/.*$/ 7 | - /^(.*\/)?ci-.*$/ 8 | build_script: 9 | - ps: .\run.ps1 default-build 10 | clone_depth: 1 11 | environment: 12 | global: 13 | DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true 14 | DOTNET_CLI_TELEMETRY_OPTOUT: 1 15 | test: 'off' 16 | deploy: 'off' 17 | os: Visual Studio 2017 18 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | *.doc diff=astextplain 2 | *.DOC diff=astextplain 3 | *.docx diff=astextplain 4 | *.DOCX diff=astextplain 5 | *.dot diff=astextplain 6 | *.DOT diff=astextplain 7 | *.pdf diff=astextplain 8 | *.PDF diff=astextplain 9 | *.rtf diff=astextplain 10 | *.RTF diff=astextplain 11 | 12 | *.jpg binary 13 | *.png binary 14 | *.gif binary 15 | 16 | *.cs text=auto diff=csharp 17 | *.vb text=auto 18 | *.resx text=auto 19 | *.c text=auto 20 | *.cpp text=auto 21 | *.cxx text=auto 22 | *.h text=auto 23 | *.hxx text=auto 24 | *.py text=auto 25 | *.rb text=auto 26 | *.java text=auto 27 | *.html text=auto 28 | *.htm text=auto 29 | *.css text=auto 30 | *.scss text=auto 31 | *.sass text=auto 32 | *.less text=auto 33 | *.js text=auto 34 | *.lisp text=auto 35 | *.clj text=auto 36 | *.sql text=auto 37 | *.php text=auto 38 | *.lua text=auto 39 | *.m text=auto 40 | *.asm text=auto 41 | *.erl text=auto 42 | *.fs text=auto 43 | *.fsx text=auto 44 | *.hs text=auto 45 | 46 | *.csproj text=auto 47 | *.vbproj text=auto 48 | *.fsproj text=auto 49 | *.dbproj text=auto 50 | *.sln text=auto eol=crlf 51 | 52 | *.sh eol=lf 53 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | [Oo]bj/ 2 | [Bb]in/ 3 | TestResults/ 4 | .nuget/ 5 | *.sln.ide/ 6 | _ReSharper.*/ 7 | packages/ 8 | artifacts/ 9 | PublishProfiles/ 10 | .vs/ 11 | .vscode/ 12 | .build/ 13 | .idea/ 14 | .testPublish/ 15 | bower_components/ 16 | node_modules/ 17 | **/wwwroot/lib/ 18 | debugSettings.json 19 | project.lock.json 20 | *.user 21 | *.suo 22 | *.cache 23 | *.docstates 24 | _ReSharper.* 25 | nuget.exe 26 | *net45.csproj 27 | *net451.csproj 28 | *k10.csproj 29 | *.psess 30 | *.vsp 31 | *.pidb 32 | *.userprefs 33 | *DS_Store 34 | *.ncrunchsolution 35 | *.*sdf 36 | *.ipch 37 | .settings 38 | *.sln.ide 39 | node_modules 40 | **/[Cc]ompiler/[Rr]esources/**/*.js 41 | *launchSettings.json 42 | global.json 43 | BenchmarkDotNet.Artifacts/ 44 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: csharp 2 | sudo: false 3 | dist: trusty 4 | env: 5 | global: 6 | - DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true 7 | - DOTNET_CLI_TELEMETRY_OPTOUT: 1 8 | mono: none 9 | os: 10 | - linux 11 | - osx 12 | osx_image: xcode8.2 13 | addons: 14 | apt: 15 | packages: 16 | - libunwind8 17 | branches: 18 | only: 19 | - master 20 | - /^release\/.*$/ 21 | - /^(.*\/)?ci-.*$/ 22 | before_install: 23 | - if test "$TRAVIS_OS_NAME" == "osx"; then brew update; brew install openssl; ln -s 24 | /usr/local/opt/openssl/lib/libcrypto.1.0.0.dylib /usr/local/lib/; ln -s /usr/local/opt/openssl/lib/libssl.1.0.0.dylib 25 | /usr/local/lib/; fi 26 | script: 27 | - ./build.sh 28 | -------------------------------------------------------------------------------- /.vsts-pipelines/builds/ci-internal.yml: -------------------------------------------------------------------------------- 1 | trigger: 2 | - master 3 | - release/* 4 | 5 | resources: 6 | repositories: 7 | - repository: buildtools 8 | type: git 9 | name: aspnet-BuildTools 10 | ref: refs/heads/master 11 | 12 | phases: 13 | - template: .vsts-pipelines/templates/project-ci.yml@buildtools 14 | -------------------------------------------------------------------------------- /.vsts-pipelines/builds/ci-public.yml: -------------------------------------------------------------------------------- 1 | trigger: 2 | - master 3 | - release/* 4 | 5 | # See https://github.com/aspnet/BuildTools 6 | resources: 7 | repositories: 8 | - repository: buildtools 9 | type: github 10 | endpoint: DotNet-Bot GitHub Connection 11 | name: aspnet/BuildTools 12 | ref: refs/heads/master 13 | 14 | phases: 15 | - template: .vsts-pipelines/templates/project-ci.yml@buildtools 16 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Contributing 2 | ====== 3 | 4 | Information on contributing to this repo is in the [Contributing Guide](https://github.com/aspnet/Home/blob/master/CONTRIBUTING.md) in the Home repo. 5 | -------------------------------------------------------------------------------- /Directory.Build.props: -------------------------------------------------------------------------------- 1 |  2 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | Microsoft ASP.NET Core 12 | https://github.com/aspnet/HttpClientFactory 13 | git 14 | $(MSBuildThisFileDirectory) 15 | $(MSBuildThisFileDirectory)build\Key.snk 16 | true 17 | true 18 | 19 | 20 | -------------------------------------------------------------------------------- /Directory.Build.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | $(MicrosoftNETCoreApp20PackageVersion) 4 | $(MicrosoftNETCoreApp21PackageVersion) 5 | $(MicrosoftNETCoreApp22PackageVersion) 6 | $(NETStandardLibrary20PackageVersion) 7 | 8 | 99.9 9 | 10 | 11 | -------------------------------------------------------------------------------- /HttpClientFactory.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.27025.1 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{0ACA47C2-6B67-46B8-A661-C564E4450DE4}" 7 | EndProject 8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{A7C2238C-5C0F-4D33-BE66-4015985CE962}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Extensions.Http", "src\Microsoft.Extensions.Http\Microsoft.Extensions.Http.csproj", "{99E3492F-87BB-4506-8D2B-D1C7101655DC}" 11 | EndProject 12 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Extensions.Http.Test", "test\Microsoft.Extensions.Http.Test\Microsoft.Extensions.Http.Test.csproj", "{752F6163-AE48-46D3-8A6D-695BBF3629CB}" 13 | EndProject 14 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{FF6B150F-C423-41BB-9563-55A0DFEAE21C}" 15 | EndProject 16 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HttpClientFactorySample", "samples\HttpClientFactorySample\HttpClientFactorySample.csproj", "{42C81623-6316-4C15-9E41-84AF48608C47}" 17 | EndProject 18 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "benchmarks", "benchmarks", "{BAD8867C-73F7-4DB8-8E79-70287E67987A}" 19 | EndProject 20 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Extensions.Http.Performance", "benchmarks\Microsoft.Extensions.Http.Performance\Microsoft.Extensions.Http.Performance.csproj", "{0A0C4C1A-3A07-43C1-8A74-4DE193D50939}" 21 | EndProject 22 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{EBF25046-7C9B-4F84-ACEA-E836012F4459}" 23 | ProjectSection(SolutionItems) = preProject 24 | build\dependencies.props = build\dependencies.props 25 | build\repo.props = build\repo.props 26 | EndProjectSection 27 | EndProject 28 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Extensions.Http.Polly", "src\Microsoft.Extensions.Http.Polly\Microsoft.Extensions.Http.Polly.csproj", "{69D013E2-AC8C-4BE4-87AD-FCE0826FC1C5}" 29 | EndProject 30 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.Extensions.Http.Polly.Test", "test\Microsoft.Extensions.Http.Polly.Test\Microsoft.Extensions.Http.Polly.Test.csproj", "{6C61E727-6E0B-402D-8E6E-2FE4E7590822}" 31 | EndProject 32 | Global 33 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 34 | Debug|Any CPU = Debug|Any CPU 35 | Release|Any CPU = Release|Any CPU 36 | EndGlobalSection 37 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 38 | {99E3492F-87BB-4506-8D2B-D1C7101655DC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 39 | {99E3492F-87BB-4506-8D2B-D1C7101655DC}.Debug|Any CPU.Build.0 = Debug|Any CPU 40 | {99E3492F-87BB-4506-8D2B-D1C7101655DC}.Release|Any CPU.ActiveCfg = Release|Any CPU 41 | {99E3492F-87BB-4506-8D2B-D1C7101655DC}.Release|Any CPU.Build.0 = Release|Any CPU 42 | {752F6163-AE48-46D3-8A6D-695BBF3629CB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 43 | {752F6163-AE48-46D3-8A6D-695BBF3629CB}.Debug|Any CPU.Build.0 = Debug|Any CPU 44 | {752F6163-AE48-46D3-8A6D-695BBF3629CB}.Release|Any CPU.ActiveCfg = Release|Any CPU 45 | {752F6163-AE48-46D3-8A6D-695BBF3629CB}.Release|Any CPU.Build.0 = Release|Any CPU 46 | {42C81623-6316-4C15-9E41-84AF48608C47}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 47 | {42C81623-6316-4C15-9E41-84AF48608C47}.Debug|Any CPU.Build.0 = Debug|Any CPU 48 | {42C81623-6316-4C15-9E41-84AF48608C47}.Release|Any CPU.ActiveCfg = Release|Any CPU 49 | {42C81623-6316-4C15-9E41-84AF48608C47}.Release|Any CPU.Build.0 = Release|Any CPU 50 | {0A0C4C1A-3A07-43C1-8A74-4DE193D50939}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 51 | {0A0C4C1A-3A07-43C1-8A74-4DE193D50939}.Debug|Any CPU.Build.0 = Debug|Any CPU 52 | {0A0C4C1A-3A07-43C1-8A74-4DE193D50939}.Release|Any CPU.ActiveCfg = Release|Any CPU 53 | {0A0C4C1A-3A07-43C1-8A74-4DE193D50939}.Release|Any CPU.Build.0 = Release|Any CPU 54 | {69D013E2-AC8C-4BE4-87AD-FCE0826FC1C5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 55 | {69D013E2-AC8C-4BE4-87AD-FCE0826FC1C5}.Debug|Any CPU.Build.0 = Debug|Any CPU 56 | {69D013E2-AC8C-4BE4-87AD-FCE0826FC1C5}.Release|Any CPU.ActiveCfg = Release|Any CPU 57 | {69D013E2-AC8C-4BE4-87AD-FCE0826FC1C5}.Release|Any CPU.Build.0 = Release|Any CPU 58 | {6C61E727-6E0B-402D-8E6E-2FE4E7590822}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 59 | {6C61E727-6E0B-402D-8E6E-2FE4E7590822}.Debug|Any CPU.Build.0 = Debug|Any CPU 60 | {6C61E727-6E0B-402D-8E6E-2FE4E7590822}.Release|Any CPU.ActiveCfg = Release|Any CPU 61 | {6C61E727-6E0B-402D-8E6E-2FE4E7590822}.Release|Any CPU.Build.0 = Release|Any CPU 62 | EndGlobalSection 63 | GlobalSection(SolutionProperties) = preSolution 64 | HideSolutionNode = FALSE 65 | EndGlobalSection 66 | GlobalSection(NestedProjects) = preSolution 67 | {99E3492F-87BB-4506-8D2B-D1C7101655DC} = {0ACA47C2-6B67-46B8-A661-C564E4450DE4} 68 | {752F6163-AE48-46D3-8A6D-695BBF3629CB} = {A7C2238C-5C0F-4D33-BE66-4015985CE962} 69 | {42C81623-6316-4C15-9E41-84AF48608C47} = {FF6B150F-C423-41BB-9563-55A0DFEAE21C} 70 | {0A0C4C1A-3A07-43C1-8A74-4DE193D50939} = {BAD8867C-73F7-4DB8-8E79-70287E67987A} 71 | {69D013E2-AC8C-4BE4-87AD-FCE0826FC1C5} = {0ACA47C2-6B67-46B8-A661-C564E4450DE4} 72 | {6C61E727-6E0B-402D-8E6E-2FE4E7590822} = {A7C2238C-5C0F-4D33-BE66-4015985CE962} 73 | EndGlobalSection 74 | GlobalSection(ExtensibilityGlobals) = postSolution 75 | SolutionGuid = {002C7A25-D737-4B87-AFBB-B6E0FB2DB0D3} 76 | EndGlobalSection 77 | EndGlobal 78 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright (c) .NET Foundation and Contributors 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /NuGet.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /NuGetPackageVerifier.json: -------------------------------------------------------------------------------- 1 | { 2 | "Default": { 3 | "rules": [ 4 | "DefaultCompositeRule" 5 | ] 6 | } 7 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | HttpClient Factory [Archived] 2 | ============================= 3 | 4 | **This GitHub project has been archived.** Ongoing development on this project can be found in . 5 | 6 | Contains an opinionated factory for creating HttpClient instances. 7 | 8 | This project is part of ASP.NET Core. You can find samples, documentation and getting started instructions for ASP.NET Core at the [AspNetCore](https://github.com/aspnet/AspNetCore) repo. 9 | -------------------------------------------------------------------------------- /benchmarks/Microsoft.Extensions.Http.Performance/Configs/CoreConfig.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 BenchmarkDotNet.Columns; 5 | using BenchmarkDotNet.Configs; 6 | using BenchmarkDotNet.Diagnosers; 7 | using BenchmarkDotNet.Engines; 8 | using BenchmarkDotNet.Jobs; 9 | using BenchmarkDotNet.Validators; 10 | 11 | namespace Microsoft.Extensions.Http.Performance 12 | { 13 | public class CoreConfig : ManualConfig 14 | { 15 | public CoreConfig() 16 | : this(Job.Core 17 | .WithRemoveOutliers(false) 18 | .With(new GcMode() { Server = true }) 19 | .With(RunStrategy.Throughput) 20 | .WithLaunchCount(3) 21 | .WithWarmupCount(5) 22 | .WithTargetCount(10)) 23 | { 24 | Add(JitOptimizationsValidator.FailOnError); 25 | } 26 | 27 | public CoreConfig(Job job) 28 | { 29 | Add(DefaultConfig.Instance); 30 | 31 | Add(MemoryDiagnoser.Default); 32 | Add(StatisticColumn.OperationsPerSecond); 33 | 34 | Add(job); 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /benchmarks/Microsoft.Extensions.Http.Performance/CreationOverheadBenchmark.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.Net.Http.Headers; 7 | using System.Threading.Tasks; 8 | using BenchmarkDotNet.Attributes; 9 | using Microsoft.Extensions.DependencyInjection; 10 | 11 | namespace Microsoft.Extensions.Http.Performance 12 | { 13 | [ParameterizedJobConfig(typeof(CoreConfig))] 14 | public class CreationOverheadBenchmark 15 | { 16 | private const int Iterations = 100; 17 | 18 | public CreationOverheadBenchmark() 19 | { 20 | Handler = new FakeClientHandler(); 21 | 22 | var serviceCollection = new ServiceCollection(); 23 | serviceCollection.AddHttpClient("example", c => 24 | { 25 | c.BaseAddress = new Uri("http://example.com/"); 26 | c.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); 27 | }) 28 | .ConfigurePrimaryHttpMessageHandler(() => Handler); 29 | 30 | var services = serviceCollection.BuildServiceProvider(); 31 | Factory = services.GetRequiredService(); 32 | } 33 | 34 | public IHttpClientFactory Factory { get; } 35 | 36 | public HttpMessageHandler Handler { get; } 37 | 38 | [Benchmark( 39 | Description = "use IHttpClientFactory", 40 | OperationsPerInvoke = Iterations)] 41 | public async Task CreateClient() 42 | { 43 | for (var i = 0; i < Iterations; i++) 44 | { 45 | var client = Factory.CreateClient("example"); 46 | 47 | var response = await client.SendAsync(new HttpRequestMessage(HttpMethod.Get, "api/Products")); 48 | response.EnsureSuccessStatusCode(); 49 | } 50 | } 51 | 52 | [Benchmark( 53 | Description = "new HttpClient", 54 | Baseline = true, 55 | OperationsPerInvoke = Iterations)] 56 | public async Task Baseline() 57 | { 58 | for (var i = 0; i < Iterations; i++) 59 | { 60 | var client = new HttpClient(Handler, disposeHandler: false) 61 | { 62 | BaseAddress = new Uri("http://example.com/"), 63 | DefaultRequestHeaders = 64 | { 65 | Accept = 66 | { 67 | new MediaTypeWithQualityHeaderValue("application/json"), 68 | } 69 | }, 70 | }; 71 | 72 | var response = await client.SendAsync(new HttpRequestMessage(HttpMethod.Get, "api/Products")); 73 | response.EnsureSuccessStatusCode(); 74 | } 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /benchmarks/Microsoft.Extensions.Http.Performance/FakeClientHandler.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; 6 | using System.Net.Http; 7 | using System.Threading; 8 | using System.Threading.Tasks; 9 | 10 | namespace Microsoft.Extensions.Http.Performance 11 | { 12 | internal class FakeClientHandler : HttpMessageHandler 13 | { 14 | public TimeSpan Latency { get; set; } = TimeSpan.FromMilliseconds(10); 15 | 16 | protected async override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) 17 | { 18 | await Task.Yield(); 19 | 20 | var response = new HttpResponseMessage(HttpStatusCode.OK); 21 | response.RequestMessage = request; 22 | return response; 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /benchmarks/Microsoft.Extensions.Http.Performance/FakeLoggerProvider.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.Logging; 6 | 7 | namespace Microsoft.Extensions.Http.Performance 8 | { 9 | internal class FakeLoggerProvider : ILoggerProvider 10 | { 11 | public bool IsEnabled { get; set; } 12 | 13 | public ILogger CreateLogger(string categoryName) 14 | { 15 | return new Logger(this); 16 | } 17 | 18 | public void Dispose() 19 | { 20 | } 21 | 22 | private class Logger : ILogger 23 | { 24 | private FakeLoggerProvider _provider; 25 | 26 | public Logger(FakeLoggerProvider provider) 27 | { 28 | _provider = provider; 29 | } 30 | 31 | public IDisposable BeginScope(TState state) 32 | { 33 | return null; 34 | } 35 | 36 | public bool IsEnabled(LogLevel logLevel) 37 | { 38 | return _provider.IsEnabled; 39 | } 40 | 41 | public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter) 42 | { 43 | } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /benchmarks/Microsoft.Extensions.Http.Performance/LoggingOverheadBenchmark.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.Net.Http.Headers; 7 | using System.Threading.Tasks; 8 | using BenchmarkDotNet.Attributes; 9 | using Microsoft.Extensions.DependencyInjection; 10 | using Microsoft.Extensions.Logging; 11 | 12 | namespace Microsoft.Extensions.Http.Performance 13 | { 14 | [ParameterizedJobConfig(typeof(CoreConfig))] 15 | public class LoggingOverheadBenchmark 16 | { 17 | private const int Iterations = 100; 18 | 19 | public LoggingOverheadBenchmark() 20 | { 21 | Handler = new FakeClientHandler(); 22 | LoggerProvider = new FakeLoggerProvider(); 23 | 24 | var serviceCollection = new ServiceCollection(); 25 | serviceCollection.AddLogging(b => b.AddProvider(LoggerProvider)); 26 | serviceCollection.AddHttpClient("example", c => 27 | { 28 | c.BaseAddress = new Uri("http://example.com/"); 29 | c.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); 30 | }) 31 | .ConfigurePrimaryHttpMessageHandler(() => Handler); 32 | 33 | var services = serviceCollection.BuildServiceProvider(); 34 | Factory = services.GetRequiredService(); 35 | } 36 | 37 | private IHttpClientFactory Factory { get; } 38 | 39 | private HttpMessageHandler Handler { get; } 40 | 41 | private FakeLoggerProvider LoggerProvider { get; } 42 | 43 | [Benchmark( 44 | Description = "logging on", 45 | OperationsPerInvoke = Iterations)] 46 | public async Task LoggingOn() 47 | { 48 | LoggerProvider.IsEnabled = true; 49 | 50 | for (var i = 0; i < Iterations; i++) 51 | { 52 | var client = Factory.CreateClient("example"); 53 | 54 | var response = await client.SendAsync(new HttpRequestMessage(HttpMethod.Get, "api/Products")); 55 | response.EnsureSuccessStatusCode(); 56 | } 57 | } 58 | 59 | [Benchmark( 60 | Description = "logging off", 61 | OperationsPerInvoke = Iterations)] 62 | public async Task LoggingOff() 63 | { 64 | LoggerProvider.IsEnabled = false; 65 | 66 | for (var i = 0; i < Iterations; i++) 67 | { 68 | var client = Factory.CreateClient("example"); 69 | 70 | var response = await client.SendAsync(new HttpRequestMessage(HttpMethod.Get, "api/Products")); 71 | response.EnsureSuccessStatusCode(); 72 | } 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /benchmarks/Microsoft.Extensions.Http.Performance/Microsoft.Extensions.Http.Performance.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp2.0;net461 5 | netcoreapp2.0 6 | Exe 7 | true 8 | true 9 | false 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /build.cmd: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | PowerShell -NoProfile -NoLogo -ExecutionPolicy unrestricted -Command "[System.Threading.Thread]::CurrentThread.CurrentCulture = ''; [System.Threading.Thread]::CurrentThread.CurrentUICulture = '';& '%~dp0run.ps1' default-build %*; exit $LASTEXITCODE" 3 | -------------------------------------------------------------------------------- /build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 5 | 6 | # Call "sync" between "chmod" and execution to prevent "text file busy" error in Docker (aufs) 7 | chmod +x "$DIR/run.sh"; sync 8 | "$DIR/run.sh" default-build "$@" 9 | -------------------------------------------------------------------------------- /build/Key.snk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aspnet/HttpClientFactory/7c4f9479b4753a37c8b0669a883002e5e448cc94/build/Key.snk -------------------------------------------------------------------------------- /build/dependencies.props: -------------------------------------------------------------------------------- 1 |  2 | 3 | $(MSBuildAllProjects);$(MSBuildThisFileFullPath) 4 | 5 | 6 | 0.10.13 7 | 3.0.0-alpha1-20181004.7 8 | 3.0.0-alpha1-10584 9 | 3.0.0-alpha1-10584 10 | 5.2.6 11 | 3.0.0-alpha1-10584 12 | 3.0.0-alpha1-10584 13 | 3.0.0-alpha1-10584 14 | 3.0.0-alpha1-10584 15 | 3.0.0-alpha1-10584 16 | 3.0.0-alpha1-10584 17 | 3.0.0-alpha1-10584 18 | 3.0.0-alpha1-10584 19 | 2.0.9 20 | 2.1.3 21 | 2.2.0-preview2-26905-02 22 | 15.6.1 23 | 4.9.0 24 | 2.0.3 25 | 2.0.1 26 | 6.0.1 27 | 0.10.0 28 | 2.3.1 29 | 2.4.0 30 | 31 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /build/repo.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Internal.AspNetCore.Universe.Lineup 7 | https://dotnet.myget.org/F/aspnetcore-dev/api/v3/index.json 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /build/sources.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | $(DotNetRestoreSources) 6 | 7 | $(RestoreSources); 8 | https://dotnet.myget.org/F/dotnet-core/api/v3/index.json; 9 | https://dotnet.myget.org/F/aspnetcore-dev/api/v3/index.json; 10 | https://dotnet.myget.org/F/aspnetcore-tools/api/v3/index.json; 11 | 12 | 13 | $(RestoreSources); 14 | https://api.nuget.org/v3/index.json; 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /korebuild-lock.txt: -------------------------------------------------------------------------------- 1 | version:3.0.0-alpha1-20181004.7 2 | commithash:27fabdaf2b1d4753c3d2749581694ca65d78f7f2 3 | -------------------------------------------------------------------------------- /korebuild.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/aspnet/BuildTools/master/tools/korebuild.schema.json", 3 | "channel": "master" 4 | } 5 | -------------------------------------------------------------------------------- /run.cmd: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | PowerShell -NoProfile -NoLogo -ExecutionPolicy unrestricted -Command "[System.Threading.Thread]::CurrentThread.CurrentCulture = ''; [System.Threading.Thread]::CurrentThread.CurrentUICulture = '';& '%~dp0run.ps1' %*; exit $LASTEXITCODE" 3 | -------------------------------------------------------------------------------- /run.ps1: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env powershell 2 | #requires -version 4 3 | 4 | <# 5 | .SYNOPSIS 6 | Executes KoreBuild commands. 7 | 8 | .DESCRIPTION 9 | Downloads korebuild if required. Then executes the KoreBuild command. To see available commands, execute with `-Command help`. 10 | 11 | .PARAMETER Command 12 | The KoreBuild command to run. 13 | 14 | .PARAMETER Path 15 | The folder to build. Defaults to the folder containing this script. 16 | 17 | .PARAMETER Channel 18 | The channel of KoreBuild to download. Overrides the value from the config file. 19 | 20 | .PARAMETER DotNetHome 21 | The directory where .NET Core tools will be stored. 22 | 23 | .PARAMETER ToolsSource 24 | The base url where build tools can be downloaded. Overrides the value from the config file. 25 | 26 | .PARAMETER Update 27 | Updates KoreBuild to the latest version even if a lock file is present. 28 | 29 | .PARAMETER Reinstall 30 | Re-installs KoreBuild 31 | 32 | .PARAMETER ConfigFile 33 | The path to the configuration file that stores values. Defaults to korebuild.json. 34 | 35 | .PARAMETER ToolsSourceSuffix 36 | The Suffix to append to the end of the ToolsSource. Useful for query strings in blob stores. 37 | 38 | .PARAMETER CI 39 | Sets up CI specific settings and variables. 40 | 41 | .PARAMETER Arguments 42 | Arguments to be passed to the command 43 | 44 | .NOTES 45 | This function will create a file $PSScriptRoot/korebuild-lock.txt. This lock file can be committed to source, but does not have to be. 46 | When the lockfile is not present, KoreBuild will create one using latest available version from $Channel. 47 | 48 | The $ConfigFile is expected to be an JSON file. It is optional, and the configuration values in it are optional as well. Any options set 49 | in the file are overridden by command line parameters. 50 | 51 | .EXAMPLE 52 | Example config file: 53 | ```json 54 | { 55 | "$schema": "https://raw.githubusercontent.com/aspnet/BuildTools/master/tools/korebuild.schema.json", 56 | "channel": "master", 57 | "toolsSource": "https://aspnetcore.blob.core.windows.net/buildtools" 58 | } 59 | ``` 60 | #> 61 | [CmdletBinding(PositionalBinding = $false)] 62 | param( 63 | [Parameter(Mandatory = $true, Position = 0)] 64 | [string]$Command, 65 | [string]$Path = $PSScriptRoot, 66 | [Alias('c')] 67 | [string]$Channel, 68 | [Alias('d')] 69 | [string]$DotNetHome, 70 | [Alias('s')] 71 | [string]$ToolsSource, 72 | [Alias('u')] 73 | [switch]$Update, 74 | [switch]$Reinstall, 75 | [string]$ToolsSourceSuffix, 76 | [string]$ConfigFile = $null, 77 | [switch]$CI, 78 | [Parameter(ValueFromRemainingArguments = $true)] 79 | [string[]]$Arguments 80 | ) 81 | 82 | Set-StrictMode -Version 2 83 | $ErrorActionPreference = 'Stop' 84 | 85 | # 86 | # Functions 87 | # 88 | 89 | function Get-KoreBuild { 90 | 91 | $lockFile = Join-Path $Path 'korebuild-lock.txt' 92 | 93 | if (!(Test-Path $lockFile) -or $Update) { 94 | Get-RemoteFile "$ToolsSource/korebuild/channels/$Channel/latest.txt" $lockFile $ToolsSourceSuffix 95 | } 96 | 97 | $version = Get-Content $lockFile | Where-Object { $_ -like 'version:*' } | Select-Object -first 1 98 | if (!$version) { 99 | Write-Error "Failed to parse version from $lockFile. Expected a line that begins with 'version:'" 100 | } 101 | $version = $version.TrimStart('version:').Trim() 102 | $korebuildPath = Join-Paths $DotNetHome ('buildtools', 'korebuild', $version) 103 | 104 | if ($Reinstall -and (Test-Path $korebuildPath)) { 105 | Remove-Item -Force -Recurse $korebuildPath 106 | } 107 | 108 | if (!(Test-Path $korebuildPath)) { 109 | Write-Host -ForegroundColor Magenta "Downloading KoreBuild $version" 110 | New-Item -ItemType Directory -Path $korebuildPath | Out-Null 111 | $remotePath = "$ToolsSource/korebuild/artifacts/$version/korebuild.$version.zip" 112 | 113 | try { 114 | $tmpfile = Join-Path ([IO.Path]::GetTempPath()) "KoreBuild-$([guid]::NewGuid()).zip" 115 | Get-RemoteFile $remotePath $tmpfile $ToolsSourceSuffix 116 | if (Get-Command -Name 'Microsoft.PowerShell.Archive\Expand-Archive' -ErrorAction Ignore) { 117 | # Use built-in commands where possible as they are cross-plat compatible 118 | Microsoft.PowerShell.Archive\Expand-Archive -Path $tmpfile -DestinationPath $korebuildPath 119 | } 120 | else { 121 | # Fallback to old approach for old installations of PowerShell 122 | Add-Type -AssemblyName System.IO.Compression.FileSystem 123 | [System.IO.Compression.ZipFile]::ExtractToDirectory($tmpfile, $korebuildPath) 124 | } 125 | } 126 | catch { 127 | Remove-Item -Recurse -Force $korebuildPath -ErrorAction Ignore 128 | throw 129 | } 130 | finally { 131 | Remove-Item $tmpfile -ErrorAction Ignore 132 | } 133 | } 134 | 135 | return $korebuildPath 136 | } 137 | 138 | function Join-Paths([string]$path, [string[]]$childPaths) { 139 | $childPaths | ForEach-Object { $path = Join-Path $path $_ } 140 | return $path 141 | } 142 | 143 | function Get-RemoteFile([string]$RemotePath, [string]$LocalPath, [string]$RemoteSuffix) { 144 | if ($RemotePath -notlike 'http*') { 145 | Copy-Item $RemotePath $LocalPath 146 | return 147 | } 148 | 149 | $retries = 10 150 | while ($retries -gt 0) { 151 | $retries -= 1 152 | try { 153 | Invoke-WebRequest -UseBasicParsing -Uri $($RemotePath + $RemoteSuffix) -OutFile $LocalPath 154 | return 155 | } 156 | catch { 157 | Write-Verbose "Request failed. $retries retries remaining" 158 | } 159 | } 160 | 161 | Write-Error "Download failed: '$RemotePath'." 162 | } 163 | 164 | # 165 | # Main 166 | # 167 | 168 | # Load configuration or set defaults 169 | 170 | $Path = Resolve-Path $Path 171 | if (!$ConfigFile) { $ConfigFile = Join-Path $Path 'korebuild.json' } 172 | 173 | if (Test-Path $ConfigFile) { 174 | try { 175 | $config = Get-Content -Raw -Encoding UTF8 -Path $ConfigFile | ConvertFrom-Json 176 | if ($config) { 177 | if (!($Channel) -and (Get-Member -Name 'channel' -InputObject $config)) { [string] $Channel = $config.channel } 178 | if (!($ToolsSource) -and (Get-Member -Name 'toolsSource' -InputObject $config)) { [string] $ToolsSource = $config.toolsSource} 179 | } 180 | } 181 | catch { 182 | Write-Host -ForegroundColor Red $Error[0] 183 | Write-Error "$ConfigFile contains invalid JSON." 184 | exit 1 185 | } 186 | } 187 | 188 | if (!$DotNetHome) { 189 | $DotNetHome = if ($env:DOTNET_HOME) { $env:DOTNET_HOME } ` 190 | elseif ($env:USERPROFILE) { Join-Path $env:USERPROFILE '.dotnet'} ` 191 | elseif ($env:HOME) {Join-Path $env:HOME '.dotnet'}` 192 | else { Join-Path $PSScriptRoot '.dotnet'} 193 | } 194 | 195 | if (!$Channel) { $Channel = 'master' } 196 | if (!$ToolsSource) { $ToolsSource = 'https://aspnetcore.blob.core.windows.net/buildtools' } 197 | 198 | # Execute 199 | 200 | $korebuildPath = Get-KoreBuild 201 | Import-Module -Force -Scope Local (Join-Path $korebuildPath 'KoreBuild.psd1') 202 | 203 | try { 204 | Set-KoreBuildSettings -ToolsSource $ToolsSource -DotNetHome $DotNetHome -RepoPath $Path -ConfigFile $ConfigFile -CI:$CI 205 | Invoke-KoreBuildCommand $Command @Arguments 206 | } 207 | finally { 208 | Remove-Module 'KoreBuild' -ErrorAction Ignore 209 | } 210 | -------------------------------------------------------------------------------- /run.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | 5 | # 6 | # variables 7 | # 8 | 9 | RESET="\033[0m" 10 | RED="\033[0;31m" 11 | YELLOW="\033[0;33m" 12 | MAGENTA="\033[0;95m" 13 | DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" 14 | [ -z "${DOTNET_HOME:-}" ] && DOTNET_HOME="$HOME/.dotnet" 15 | verbose=false 16 | update=false 17 | reinstall=false 18 | repo_path="$DIR" 19 | channel='' 20 | tools_source='' 21 | tools_source_suffix='' 22 | ci=false 23 | 24 | # 25 | # Functions 26 | # 27 | __usage() { 28 | echo "Usage: $(basename "${BASH_SOURCE[0]}") command [options] [[--] ...]" 29 | echo "" 30 | echo "Arguments:" 31 | echo " command The command to be run." 32 | echo " ... Arguments passed to the command. Variable number of arguments allowed." 33 | echo "" 34 | echo "Options:" 35 | echo " --verbose Show verbose output." 36 | echo " -c|--channel The channel of KoreBuild to download. Overrides the value from the config file.." 37 | echo " --config-file The path to the configuration file that stores values. Defaults to korebuild.json." 38 | echo " -d|--dotnet-home The directory where .NET Core tools will be stored. Defaults to '\$DOTNET_HOME' or '\$HOME/.dotnet." 39 | echo " --path The directory to build. Defaults to the directory containing the script." 40 | echo " -s|--tools-source|-ToolsSource The base url where build tools can be downloaded. Overrides the value from the config file." 41 | echo " --tools-source-suffix|-ToolsSourceSuffix The suffix to append to tools-source. Useful for query strings." 42 | echo " -u|--update Update to the latest KoreBuild even if the lock file is present." 43 | echo " --reinstall Reinstall KoreBuild." 44 | echo " --ci Apply CI specific settings and environment variables." 45 | echo "" 46 | echo "Description:" 47 | echo " This function will create a file \$DIR/korebuild-lock.txt. This lock file can be committed to source, but does not have to be." 48 | echo " When the lockfile is not present, KoreBuild will create one using latest available version from \$channel." 49 | 50 | if [[ "${1:-}" != '--no-exit' ]]; then 51 | exit 2 52 | fi 53 | } 54 | 55 | get_korebuild() { 56 | local version 57 | local lock_file="$repo_path/korebuild-lock.txt" 58 | if [ ! -f "$lock_file" ] || [ "$update" = true ]; then 59 | __get_remote_file "$tools_source/korebuild/channels/$channel/latest.txt" "$lock_file" "$tools_source_suffix" 60 | fi 61 | version="$(grep 'version:*' -m 1 "$lock_file")" 62 | if [[ "$version" == '' ]]; then 63 | __error "Failed to parse version from $lock_file. Expected a line that begins with 'version:'" 64 | return 1 65 | fi 66 | version="$(echo "${version#version:}" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')" 67 | local korebuild_path="$DOTNET_HOME/buildtools/korebuild/$version" 68 | 69 | if [ "$reinstall" = true ] && [ -d "$korebuild_path" ]; then 70 | rm -rf "$korebuild_path" 71 | fi 72 | 73 | { 74 | if [ ! -d "$korebuild_path" ]; then 75 | mkdir -p "$korebuild_path" 76 | local remote_path="$tools_source/korebuild/artifacts/$version/korebuild.$version.zip" 77 | tmpfile="$(mktemp)" 78 | echo -e "${MAGENTA}Downloading KoreBuild ${version}${RESET}" 79 | if __get_remote_file "$remote_path" "$tmpfile" "$tools_source_suffix"; then 80 | unzip -q -d "$korebuild_path" "$tmpfile" 81 | fi 82 | rm "$tmpfile" || true 83 | fi 84 | 85 | source "$korebuild_path/KoreBuild.sh" 86 | } || { 87 | if [ -d "$korebuild_path" ]; then 88 | echo "Cleaning up after failed installation" 89 | rm -rf "$korebuild_path" || true 90 | fi 91 | return 1 92 | } 93 | } 94 | 95 | __error() { 96 | echo -e "${RED}error: $*${RESET}" 1>&2 97 | } 98 | 99 | __warn() { 100 | echo -e "${YELLOW}warning: $*${RESET}" 101 | } 102 | 103 | __machine_has() { 104 | hash "$1" > /dev/null 2>&1 105 | return $? 106 | } 107 | 108 | __get_remote_file() { 109 | local remote_path=$1 110 | local local_path=$2 111 | local remote_path_suffix=$3 112 | 113 | if [[ "$remote_path" != 'http'* ]]; then 114 | cp "$remote_path" "$local_path" 115 | return 0 116 | fi 117 | 118 | local failed=false 119 | if __machine_has wget; then 120 | wget --tries 10 --quiet -O "$local_path" "${remote_path}${remote_path_suffix}" || failed=true 121 | else 122 | failed=true 123 | fi 124 | 125 | if [ "$failed" = true ] && __machine_has curl; then 126 | failed=false 127 | curl --retry 10 -sSL -f --create-dirs -o "$local_path" "${remote_path}${remote_path_suffix}" || failed=true 128 | fi 129 | 130 | if [ "$failed" = true ]; then 131 | __error "Download failed: $remote_path" 1>&2 132 | return 1 133 | fi 134 | } 135 | 136 | # 137 | # main 138 | # 139 | 140 | command="${1:-}" 141 | shift 142 | 143 | while [[ $# -gt 0 ]]; do 144 | case $1 in 145 | -\?|-h|--help) 146 | __usage --no-exit 147 | exit 0 148 | ;; 149 | -c|--channel|-Channel) 150 | shift 151 | channel="${1:-}" 152 | [ -z "$channel" ] && __usage 153 | ;; 154 | --config-file|-ConfigFile) 155 | shift 156 | config_file="${1:-}" 157 | [ -z "$config_file" ] && __usage 158 | if [ ! -f "$config_file" ]; then 159 | __error "Invalid value for --config-file. $config_file does not exist." 160 | exit 1 161 | fi 162 | ;; 163 | -d|--dotnet-home|-DotNetHome) 164 | shift 165 | DOTNET_HOME="${1:-}" 166 | [ -z "$DOTNET_HOME" ] && __usage 167 | ;; 168 | --path|-Path) 169 | shift 170 | repo_path="${1:-}" 171 | [ -z "$repo_path" ] && __usage 172 | ;; 173 | -s|--tools-source|-ToolsSource) 174 | shift 175 | tools_source="${1:-}" 176 | [ -z "$tools_source" ] && __usage 177 | ;; 178 | --tools-source-suffix|-ToolsSourceSuffix) 179 | shift 180 | tools_source_suffix="${1:-}" 181 | [ -z "$tools_source_suffix" ] && __usage 182 | ;; 183 | -u|--update|-Update) 184 | update=true 185 | ;; 186 | --reinstall|-[Rr]einstall) 187 | reinstall=true 188 | ;; 189 | --ci|-[Cc][Ii]) 190 | ci=true 191 | ;; 192 | --verbose|-Verbose) 193 | verbose=true 194 | ;; 195 | --) 196 | shift 197 | break 198 | ;; 199 | *) 200 | break 201 | ;; 202 | esac 203 | shift 204 | done 205 | 206 | if ! __machine_has unzip; then 207 | __error 'Missing required command: unzip' 208 | exit 1 209 | fi 210 | 211 | if ! __machine_has curl && ! __machine_has wget; then 212 | __error 'Missing required command. Either wget or curl is required.' 213 | exit 1 214 | fi 215 | 216 | [ -z "${config_file:-}" ] && config_file="$repo_path/korebuild.json" 217 | if [ -f "$config_file" ]; then 218 | if __machine_has jq ; then 219 | if jq '.' "$config_file" >/dev/null ; then 220 | config_channel="$(jq -r 'select(.channel!=null) | .channel' "$config_file")" 221 | config_tools_source="$(jq -r 'select(.toolsSource!=null) | .toolsSource' "$config_file")" 222 | else 223 | __error "$config_file contains invalid JSON." 224 | exit 1 225 | fi 226 | elif __machine_has python ; then 227 | if python -c "import json,codecs;obj=json.load(codecs.open('$config_file', 'r', 'utf-8-sig'))" >/dev/null ; then 228 | config_channel="$(python -c "import json,codecs;obj=json.load(codecs.open('$config_file', 'r', 'utf-8-sig'));print(obj['channel'] if 'channel' in obj else '')")" 229 | config_tools_source="$(python -c "import json,codecs;obj=json.load(codecs.open('$config_file', 'r', 'utf-8-sig'));print(obj['toolsSource'] if 'toolsSource' in obj else '')")" 230 | else 231 | __error "$config_file contains invalid JSON." 232 | exit 1 233 | fi 234 | elif __machine_has python3 ; then 235 | if python3 -c "import json,codecs;obj=json.load(codecs.open('$config_file', 'r', 'utf-8-sig'))" >/dev/null ; then 236 | config_channel="$(python3 -c "import json,codecs;obj=json.load(codecs.open('$config_file', 'r', 'utf-8-sig'));print(obj['channel'] if 'channel' in obj else '')")" 237 | config_tools_source="$(python3 -c "import json,codecs;obj=json.load(codecs.open('$config_file', 'r', 'utf-8-sig'));print(obj['toolsSource'] if 'toolsSource' in obj else '')")" 238 | else 239 | __error "$config_file contains invalid JSON." 240 | exit 1 241 | fi 242 | else 243 | __error 'Missing required command: jq or python. Could not parse the JSON file.' 244 | exit 1 245 | fi 246 | 247 | [ ! -z "${config_channel:-}" ] && channel="$config_channel" 248 | [ ! -z "${config_tools_source:-}" ] && tools_source="$config_tools_source" 249 | fi 250 | 251 | [ -z "$channel" ] && channel='master' 252 | [ -z "$tools_source" ] && tools_source='https://aspnetcore.blob.core.windows.net/buildtools' 253 | 254 | get_korebuild 255 | set_korebuildsettings "$tools_source" "$DOTNET_HOME" "$repo_path" "$config_file" "$ci" 256 | invoke_korebuild_command "$command" "$@" 257 | -------------------------------------------------------------------------------- /samples/HttpClientFactorySample/HttpClientFactorySample.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp2.2;net461 5 | portable 6 | Exe 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /samples/HttpClientFactorySample/Program.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; 7 | using System.Threading.Tasks; 8 | using Microsoft.Extensions.DependencyInjection; 9 | using Microsoft.Extensions.Logging; 10 | using Newtonsoft.Json.Linq; 11 | using Polly; 12 | 13 | namespace HttpClientFactorySample 14 | { 15 | public class Program 16 | { 17 | public static void Main(string[] args) => Run().GetAwaiter().GetResult(); 18 | 19 | public static async Task Run() 20 | { 21 | var serviceCollection = new ServiceCollection(); 22 | serviceCollection.AddLogging(b => 23 | { 24 | b.AddFilter((category, level) => true); // Spam the world with logs. 25 | 26 | // Add console logger so we can see all the logging produced by the client by default. 27 | b.AddConsole(c => c.IncludeScopes = true); 28 | }); 29 | 30 | Configure(serviceCollection); 31 | 32 | var services = serviceCollection.BuildServiceProvider(); 33 | 34 | Console.WriteLine("Creating a client..."); 35 | var github = services.GetRequiredService(); 36 | 37 | Console.WriteLine("Sending a request..."); 38 | var response = await github.GetJson(); 39 | 40 | var data = await response.Content.ReadAsAsync(); 41 | Console.WriteLine("Response data:"); 42 | Console.WriteLine(data); 43 | 44 | Console.WriteLine("Press the ANY key to exit..."); 45 | Console.ReadKey(); 46 | } 47 | 48 | public static void Configure(IServiceCollection services) 49 | { 50 | var registry = services.AddPolicyRegistry(); 51 | 52 | var timeout = Policy.TimeoutAsync(TimeSpan.FromSeconds(10)); 53 | var longTimeout = Policy.TimeoutAsync(TimeSpan.FromSeconds(30)); 54 | 55 | registry.Add("regular", timeout); 56 | registry.Add("long", longTimeout); 57 | 58 | services.AddHttpClient("github", c => 59 | { 60 | c.BaseAddress = new Uri("https://api.github.com/"); 61 | 62 | c.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json"); // GitHub API versioning 63 | c.DefaultRequestHeaders.Add("User-Agent", "HttpClientFactory-Sample"); // GitHub requires a user-agent 64 | }) 65 | 66 | // Build a totally custom policy using any criteria 67 | .AddPolicyHandler(Policy.TimeoutAsync(TimeSpan.FromSeconds(10))) 68 | 69 | // Use a specific named policy from the registry. Simplest way, policy is cached for the 70 | // lifetime of the handler. 71 | .AddPolicyHandlerFromRegistry("regular") 72 | 73 | // Run some code to select a policy based on the request 74 | .AddPolicyHandler((request) => 75 | { 76 | return request.Method == HttpMethod.Get ? timeout : longTimeout; 77 | }) 78 | 79 | // Run some code to select a policy from the registry based on the request 80 | .AddPolicyHandlerFromRegistry((reg, request) => 81 | { 82 | return request.Method == HttpMethod.Get ? 83 | reg.Get>("regular") : 84 | reg.Get>("long"); 85 | }) 86 | 87 | // Build a policy that will handle exceptions, 408s, and 500s from the remote server 88 | .AddTransientHttpErrorPolicy(p => p.RetryAsync()) 89 | 90 | .AddHttpMessageHandler(() => new RetryHandler()) // Retry requests to github using our retry handler 91 | .AddTypedClient(); 92 | } 93 | 94 | private class GitHubClient 95 | { 96 | public GitHubClient(HttpClient httpClient) 97 | { 98 | HttpClient = httpClient; 99 | } 100 | 101 | public HttpClient HttpClient { get; } 102 | 103 | // Gets the list of services on github. 104 | public async Task GetJson() 105 | { 106 | var request = new HttpRequestMessage(HttpMethod.Get, "/"); 107 | 108 | var response = await HttpClient.SendAsync(request).ConfigureAwait(false); 109 | response.EnsureSuccessStatusCode(); 110 | 111 | return response; 112 | } 113 | } 114 | 115 | private class RetryHandler : DelegatingHandler 116 | { 117 | public int RetryCount { get; set; } = 5; 118 | 119 | protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) 120 | { 121 | for (var i = 0; i < RetryCount; i++) 122 | { 123 | try 124 | { 125 | return await base.SendAsync(request, cancellationToken); 126 | } 127 | catch (HttpRequestException) when (i == RetryCount - 1) 128 | { 129 | throw; 130 | } 131 | catch (HttpRequestException) 132 | { 133 | // Retry 134 | await Task.Delay(TimeSpan.FromMilliseconds(50)); 135 | } 136 | } 137 | 138 | // Unreachable. 139 | throw null; 140 | } 141 | } 142 | } 143 | } 144 | -------------------------------------------------------------------------------- /src/Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /src/Microsoft.Extensions.Http.Polly/DependencyInjection/PollyHttpClientBuilderExtensions.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 Microsoft.Extensions.Http; 7 | using Polly; 8 | using Polly.Extensions.Http; 9 | using Polly.Registry; 10 | 11 | namespace Microsoft.Extensions.DependencyInjection 12 | { 13 | /// 14 | /// Extensions methods for configuring message handlers as part of 15 | /// and message handler pipeline. 16 | /// 17 | public static class PollyHttpClientBuilderExtensions 18 | { 19 | /// 20 | /// Adds a which will surround request execution with the provided 21 | /// . 22 | /// 23 | /// The . 24 | /// The . 25 | /// An that can be used to configure the client. 26 | /// 27 | /// 28 | /// See the remarks on for guidance on configuring policies. 29 | /// 30 | /// 31 | public static IHttpClientBuilder AddPolicyHandler(this IHttpClientBuilder builder, IAsyncPolicy policy) 32 | { 33 | if (builder == null) 34 | { 35 | throw new ArgumentNullException(nameof(builder)); 36 | } 37 | 38 | if (policy == null) 39 | { 40 | throw new ArgumentNullException(nameof(policy)); 41 | } 42 | 43 | builder.AddHttpMessageHandler(() => new PolicyHttpMessageHandler(policy)); 44 | return builder; 45 | } 46 | 47 | /// 48 | /// Adds a which will surround request execution with a policy returned 49 | /// by the . 50 | /// 51 | /// The . 52 | /// 53 | /// Selects an to apply to the current request. 54 | /// 55 | /// An that can be used to configure the client. 56 | /// 57 | /// 58 | /// See the remarks on for guidance on configuring policies. 59 | /// 60 | /// 61 | public static IHttpClientBuilder AddPolicyHandler( 62 | this IHttpClientBuilder builder, 63 | Func> policySelector) 64 | { 65 | if (builder == null) 66 | { 67 | throw new ArgumentNullException(nameof(builder)); 68 | } 69 | 70 | if (policySelector == null) 71 | { 72 | throw new ArgumentNullException(nameof(policySelector)); 73 | } 74 | 75 | builder.AddHttpMessageHandler(() => new PolicyHttpMessageHandler(policySelector)); 76 | return builder; 77 | } 78 | 79 | /// 80 | /// Adds a which will surround request execution with a policy returned 81 | /// by the . 82 | /// 83 | /// The . 84 | /// 85 | /// Selects an to apply to the current request. 86 | /// 87 | /// An that can be used to configure the client. 88 | /// 89 | /// 90 | /// See the remarks on for guidance on configuring policies. 91 | /// 92 | /// 93 | public static IHttpClientBuilder AddPolicyHandler( 94 | this IHttpClientBuilder builder, 95 | Func> policySelector) 96 | { 97 | if (builder == null) 98 | { 99 | throw new ArgumentNullException(nameof(builder)); 100 | } 101 | 102 | if (policySelector == null) 103 | { 104 | throw new ArgumentNullException(nameof(policySelector)); 105 | } 106 | 107 | builder.AddHttpMessageHandler((services) => 108 | { 109 | return new PolicyHttpMessageHandler((request) => policySelector(services, request)); 110 | }); 111 | return builder; 112 | } 113 | 114 | /// 115 | /// Adds a which will surround request execution with a policy returned 116 | /// by the . 117 | /// 118 | /// The . 119 | /// 120 | /// The key used to resolve a policy from the . 121 | /// 122 | /// An that can be used to configure the client. 123 | /// 124 | /// 125 | /// See the remarks on for guidance on configuring policies. 126 | /// 127 | /// 128 | public static IHttpClientBuilder AddPolicyHandlerFromRegistry(this IHttpClientBuilder builder, string policyKey) 129 | { 130 | if (builder == null) 131 | { 132 | throw new ArgumentNullException(nameof(builder)); 133 | } 134 | 135 | if (policyKey == null) 136 | { 137 | throw new ArgumentNullException(nameof(policyKey)); 138 | } 139 | 140 | builder.AddHttpMessageHandler((services) => 141 | { 142 | var registry = services.GetRequiredService>(); 143 | 144 | var policy = registry.Get>(policyKey); 145 | 146 | return new PolicyHttpMessageHandler(policy); 147 | }); 148 | return builder; 149 | } 150 | 151 | /// 152 | /// Adds a which will surround request execution with a policy returned 153 | /// by the . 154 | /// 155 | /// The . 156 | /// 157 | /// Selects an to apply to the current request. 158 | /// 159 | /// An that can be used to configure the client. 160 | /// 161 | /// 162 | /// See the remarks on for guidance on configuring policies. 163 | /// 164 | /// 165 | public static IHttpClientBuilder AddPolicyHandlerFromRegistry( 166 | this IHttpClientBuilder builder, 167 | Func, HttpRequestMessage, IAsyncPolicy> policySelector) 168 | { 169 | if (builder == null) 170 | { 171 | throw new ArgumentNullException(nameof(builder)); 172 | } 173 | 174 | if (policySelector == null) 175 | { 176 | throw new ArgumentNullException(nameof(policySelector)); 177 | } 178 | 179 | builder.AddHttpMessageHandler((services) => 180 | { 181 | var registry = services.GetRequiredService>(); 182 | return new PolicyHttpMessageHandler((request) => policySelector(registry, request)); 183 | }); 184 | return builder; 185 | } 186 | 187 | /// 188 | /// Adds a which will surround request execution with a 189 | /// created by executing the provided configuration delegate. The policy builder will be preconfigured to trigger 190 | /// application of the policy for requests that fail with conditions that indicate a transient failure. 191 | /// 192 | /// The . 193 | /// A delegate used to create a . 194 | /// An that can be used to configure the client. 195 | /// 196 | /// 197 | /// See the remarks on for guidance on configuring policies. 198 | /// 199 | /// 200 | /// The provided to has been 201 | /// preconfigured errors to handle errors in the following categories: 202 | /// 203 | /// Network failures (as ) 204 | /// HTTP 5XX status codes (server errors) 205 | /// HTTP 408 status code (request timeout) 206 | /// 207 | /// 208 | /// 209 | /// The policy created by will be cached indefinitely per named client. Policies 210 | /// are generally designed to act as singletons, and can be shared when appropriate. To share a policy across multiple 211 | /// named clients, first create the policy and then pass it to multiple calls to 212 | /// as desired. 213 | /// 214 | /// 215 | public static IHttpClientBuilder AddTransientHttpErrorPolicy( 216 | this IHttpClientBuilder builder, 217 | Func, IAsyncPolicy> configurePolicy) 218 | { 219 | if (builder == null) 220 | { 221 | throw new ArgumentNullException(nameof(builder)); 222 | } 223 | 224 | if (configurePolicy == null) 225 | { 226 | throw new ArgumentNullException(nameof(configurePolicy)); 227 | } 228 | 229 | var policyBuilder = HttpPolicyExtensions.HandleTransientHttpError(); 230 | 231 | // Important - cache policy instances so that they are singletons per handler. 232 | var policy = configurePolicy(policyBuilder); 233 | 234 | builder.AddHttpMessageHandler(() => new PolicyHttpMessageHandler(policy)); 235 | return builder; 236 | } 237 | } 238 | } 239 | -------------------------------------------------------------------------------- /src/Microsoft.Extensions.Http.Polly/DependencyInjection/PollyServiceCollectionExtensions.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 Polly.Registry; 6 | 7 | namespace Microsoft.Extensions.DependencyInjection 8 | { 9 | /// 10 | /// Provides convenience extension methods to register and 11 | /// in the service collection. 12 | /// 13 | public static class PollyServiceCollectionExtensions 14 | { 15 | /// 16 | /// Registers an empty in the service collection with service types 17 | /// , and and returns 18 | /// the newly created registry. 19 | /// 20 | /// The . 21 | /// The newly created . 22 | public static IPolicyRegistry AddPolicyRegistry(this IServiceCollection services) 23 | { 24 | if (services == null) 25 | { 26 | throw new ArgumentNullException(nameof(services)); 27 | } 28 | 29 | // Create an empty registry, register and return it as an instance. This is the best way to get a 30 | // single instance registered using both interfaces. 31 | var registry = new PolicyRegistry(); 32 | services.AddSingleton>(registry); 33 | services.AddSingleton>(registry); 34 | 35 | return registry; 36 | } 37 | 38 | /// 39 | /// Registers the provided in the service collection with service types 40 | /// , and and returns 41 | /// the provided registry. 42 | /// 43 | /// The . 44 | /// The . 45 | /// The provided . 46 | public static IPolicyRegistry AddPolicyRegistry(this IServiceCollection services, IPolicyRegistry registry) 47 | { 48 | if (services == null) 49 | { 50 | throw new ArgumentNullException(nameof(services)); 51 | } 52 | 53 | if (registry == null) 54 | { 55 | throw new ArgumentNullException(nameof(registry)); 56 | } 57 | 58 | services.AddSingleton>(registry); 59 | services.AddSingleton>(registry); 60 | 61 | return registry; 62 | } 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/Microsoft.Extensions.Http.Polly/HttpRequestMessageExtensions.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 Microsoft.Extensions.Http; 5 | using System; 6 | using System.Net.Http; 7 | 8 | namespace Polly 9 | { 10 | /// 11 | /// Extension methods for Polly integration. 12 | /// 13 | public static class HttpRequestMessageExtensions 14 | { 15 | internal static readonly string PolicyExecutionContextKey = "PolicyExecutionContext"; 16 | 17 | /// 18 | /// Gets the associated with the provided . 19 | /// 20 | /// The . 21 | /// The if set, otherwise null. 22 | /// 23 | /// The will attach a context to the prior 24 | /// to executing a , if one does not already exist. The will be provided 25 | /// to the policy for use inside the and in other message handlers. 26 | /// 27 | public static Context GetPolicyExecutionContext(this HttpRequestMessage request) 28 | { 29 | if (request == null) 30 | { 31 | throw new ArgumentNullException(nameof(request)); 32 | } 33 | 34 | request.Properties.TryGetValue(PolicyExecutionContextKey, out var context); 35 | return context as Context; 36 | } 37 | 38 | /// 39 | /// Sets the associated with the provided . 40 | /// 41 | /// The . 42 | /// The , may be null. 43 | /// 44 | /// The will attach a context to the prior 45 | /// to executing a , if one does not already exist. The will be provided 46 | /// to the policy for use inside the and in other message handlers. 47 | /// 48 | public static void SetPolicyExecutionContext(this HttpRequestMessage request, Context context) 49 | { 50 | if (request == null) 51 | { 52 | throw new ArgumentNullException(nameof(request)); 53 | } 54 | 55 | request.Properties[PolicyExecutionContextKey] = context; 56 | } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/Microsoft.Extensions.Http.Polly/Microsoft.Extensions.Http.Polly.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | The HttpClient factory is a pattern for configuring and retrieving named HttpClients in a composable way. This package integrates IHttpClientFactory with the Polly library, to add transient-fault-handling and resiliency through fluent policies such as Retry, Circuit Breaker, Timeout, Bulkhead Isolation, and Fallback. 6 | 7 | netstandard2.0 8 | $(NoWarn);CS1591 9 | true 10 | aspnetcore;httpclient 11 | 12 | 13 | Microsoft.Extensions.Http 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/Microsoft.Extensions.Http.Polly/PolicyHttpMessageHandler.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; 7 | using System.Threading.Tasks; 8 | using Microsoft.Extensions.DependencyInjection; 9 | using Polly; 10 | 11 | namespace Microsoft.Extensions.Http 12 | { 13 | /// 14 | /// A implementation that executes request processing surrounded by a . 15 | /// 16 | /// 17 | /// 18 | /// This message handler implementation supports the use of policies provided by the Polly library for 19 | /// transient-fault-handling and resiliency. 20 | /// 21 | /// 22 | /// The documentation provided here is focused guidance for using Polly together with the . 23 | /// See the Polly project and its documentation (https://github.com/app-vnext/Polly) for authoritative information on Polly. 24 | /// 25 | /// 26 | /// The extension methods on are designed as a convenient and correct 27 | /// way to create a . 28 | /// 29 | /// 30 | /// The 31 | /// method supports the creation of a for any kind of policy. This includes 32 | /// non-reactive policies, such as Timeout or Cache, which don't require the underlying request to fail first. 33 | /// 34 | /// 35 | /// and the convenience methods 36 | /// only accept the generic . Generic policy instances can be created 37 | /// by using the generic methods on such as . 38 | /// 39 | /// 40 | /// To adapt an existing non-generic , use code like the following: 41 | /// 42 | /// Converting a non-generic IAsyncPolicy policy to . 43 | /// 44 | /// policy.AsAsyncPolicy<HttpResponseMessage>() 45 | /// 46 | /// 47 | /// 48 | /// 49 | /// The 50 | /// method is an opinionated convenience method that supports the application of a policy for requests that fail due 51 | /// to a connection failure or server error (5XX HTTP status code). This kind of method supports only reactive policies 52 | /// such as Retry, Circuit-Breaker or Fallback. This method is only provided for convenience; we recommend creating 53 | /// your own policies as needed if this does not meet your requirements. 54 | /// 55 | /// 56 | /// Take care when using policies such as Retry or Timeout together as HttpClient provides its own timeout via 57 | /// . When combining Retry and Timeout, will act as a 58 | /// timeout across all tries; a Polly Timeout policy can be configured after a Retry policy in the configuration sequence, 59 | /// to provide a timeout-per-try. 60 | /// 61 | /// 62 | /// All policies provided by Polly are designed to be efficient when used in a long-lived way. Certain policies such as the 63 | /// Bulkhead and Circuit-Breaker maintain state and should be scoped across calls you wish to share the Bulkhead or Circuit-Breaker state. 64 | /// Take care to ensure the correct lifetimes when using policies and message handlers together in custom scenarios. The extension 65 | /// methods provided by are designed to assign a long lifetime to policies 66 | /// and ensure that they can be used when the handler rotation feature is active. 67 | /// 68 | /// 69 | /// The will attach a context to the prior 70 | /// to executing a , if one does not already exist. The will be provided 71 | /// to the policy for use inside the and in other message handlers. 72 | /// 73 | /// 74 | public class PolicyHttpMessageHandler : DelegatingHandler 75 | { 76 | private readonly IAsyncPolicy _policy; 77 | private readonly Func> _policySelector; 78 | 79 | /// 80 | /// Creates a new . 81 | /// 82 | /// The policy. 83 | public PolicyHttpMessageHandler(IAsyncPolicy policy) 84 | { 85 | if (policy == null) 86 | { 87 | throw new ArgumentNullException(nameof(policy)); 88 | } 89 | 90 | _policy = policy; 91 | } 92 | 93 | /// 94 | /// Creates a new . 95 | /// 96 | /// A function which can select the desired policy for a given . 97 | public PolicyHttpMessageHandler(Func> policySelector) 98 | { 99 | if (policySelector == null) 100 | { 101 | throw new ArgumentNullException(nameof(policySelector)); 102 | } 103 | 104 | _policySelector = policySelector; 105 | } 106 | 107 | /// 108 | protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) 109 | { 110 | if (request == null) 111 | { 112 | throw new ArgumentNullException(nameof(request)); 113 | } 114 | 115 | // Guarantee the existence of a context for every policy execution, but only create a new one if needed. This 116 | // allows later handlers to flow state if desired. 117 | var cleanUpContext = false; 118 | var context = request.GetPolicyExecutionContext(); 119 | if (context == null) 120 | { 121 | context = new Context(); 122 | request.SetPolicyExecutionContext(context); 123 | cleanUpContext = true; 124 | } 125 | 126 | HttpResponseMessage response; 127 | try 128 | { 129 | var policy = _policy ?? SelectPolicy(request); 130 | response = await policy.ExecuteAsync((c, ct) => SendCoreAsync(request, c, ct), context, cancellationToken); 131 | } 132 | finally 133 | { 134 | if (cleanUpContext) 135 | { 136 | request.SetPolicyExecutionContext(null); 137 | } 138 | } 139 | 140 | return response; 141 | } 142 | 143 | /// 144 | /// Called inside the execution of the to perform request processing. 145 | /// 146 | /// The . 147 | /// The . 148 | /// The . 149 | /// Returns a that will yield a response when completed. 150 | protected virtual Task SendCoreAsync(HttpRequestMessage request, Context context, CancellationToken cancellationToken) 151 | { 152 | if (request == null) 153 | { 154 | throw new ArgumentNullException(nameof(request)); 155 | } 156 | 157 | if (context == null) 158 | { 159 | throw new ArgumentNullException(nameof(context)); 160 | } 161 | 162 | return base.SendAsync(request, cancellationToken); 163 | } 164 | 165 | private IAsyncPolicy SelectPolicy(HttpRequestMessage request) 166 | { 167 | var policy = _policySelector(request); 168 | if (policy == null) 169 | { 170 | var message = Resources.FormatPolicyHttpMessageHandler_PolicySelector_ReturnedNull( 171 | "policySelector", 172 | "Policy.NoOpAsync()"); 173 | throw new InvalidOperationException(message); 174 | } 175 | 176 | return policy; 177 | } 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /src/Microsoft.Extensions.Http.Polly/Properties/AssemblyInfo.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.Runtime.CompilerServices; 5 | 6 | [assembly: InternalsVisibleTo("Microsoft.Extensions.Http.Polly.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] 7 | [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7")] -------------------------------------------------------------------------------- /src/Microsoft.Extensions.Http.Polly/Properties/Resources.Designer.cs: -------------------------------------------------------------------------------- 1 | // 2 | namespace Microsoft.Extensions.Http 3 | { 4 | using System.Globalization; 5 | using System.Reflection; 6 | using System.Resources; 7 | 8 | internal static class Resources 9 | { 10 | private static readonly ResourceManager _resourceManager 11 | = new ResourceManager("Microsoft.Extensions.Http.Resources", typeof(Resources).GetTypeInfo().Assembly); 12 | 13 | /// 14 | /// The '{0}' function must return a non-null policy instance. To create a policy that takes no action, use '{1}'. 15 | /// 16 | internal static string PolicyHttpMessageHandler_PolicySelector_ReturnedNull 17 | { 18 | get => GetString("PolicyHttpMessageHandler_PolicySelector_ReturnedNull"); 19 | } 20 | 21 | /// 22 | /// The '{0}' function must return a non-null policy instance. To create a policy that takes no action, use '{1}'. 23 | /// 24 | internal static string FormatPolicyHttpMessageHandler_PolicySelector_ReturnedNull(object p0, object p1) 25 | => string.Format(CultureInfo.CurrentCulture, GetString("PolicyHttpMessageHandler_PolicySelector_ReturnedNull"), p0, p1); 26 | 27 | private static string GetString(string name, params string[] formatterNames) 28 | { 29 | var value = _resourceManager.GetString(name); 30 | 31 | System.Diagnostics.Debug.Assert(value != null); 32 | 33 | if (formatterNames != null) 34 | { 35 | for (var i = 0; i < formatterNames.Length; i++) 36 | { 37 | value = value.Replace("{" + formatterNames[i] + "}", "{" + i + "}"); 38 | } 39 | } 40 | 41 | return value; 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/Microsoft.Extensions.Http.Polly/Resources.resx: -------------------------------------------------------------------------------- 1 |  2 | 3 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | text/microsoft-resx 110 | 111 | 112 | 2.0 113 | 114 | 115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | 118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 119 | 120 | 121 | The '{0}' function must return a non-null policy instance. To create a policy that takes no action, use '{1}'. 122 | 123 | -------------------------------------------------------------------------------- /src/Microsoft.Extensions.Http.Polly/baseline.netcore.json: -------------------------------------------------------------------------------- 1 | { 2 | "AssemblyIdentity": "Microsoft.Extensions.Http.Polly, Version=2.1.1.0, Culture=neutral, PublicKeyToken=adb9793829ddae60", 3 | "Types": [ 4 | { 5 | "Name": "Polly.HttpRequestMessageExtensions", 6 | "Visibility": "Public", 7 | "Kind": "Class", 8 | "Abstract": true, 9 | "Static": true, 10 | "Sealed": true, 11 | "ImplementedInterfaces": [], 12 | "Members": [ 13 | { 14 | "Kind": "Method", 15 | "Name": "GetPolicyExecutionContext", 16 | "Parameters": [ 17 | { 18 | "Name": "request", 19 | "Type": "System.Net.Http.HttpRequestMessage" 20 | } 21 | ], 22 | "ReturnType": "Polly.Context", 23 | "Static": true, 24 | "Extension": true, 25 | "Visibility": "Public", 26 | "GenericParameter": [] 27 | }, 28 | { 29 | "Kind": "Method", 30 | "Name": "SetPolicyExecutionContext", 31 | "Parameters": [ 32 | { 33 | "Name": "request", 34 | "Type": "System.Net.Http.HttpRequestMessage" 35 | }, 36 | { 37 | "Name": "context", 38 | "Type": "Polly.Context" 39 | } 40 | ], 41 | "ReturnType": "System.Void", 42 | "Static": true, 43 | "Extension": true, 44 | "Visibility": "Public", 45 | "GenericParameter": [] 46 | } 47 | ], 48 | "GenericParameters": [] 49 | }, 50 | { 51 | "Name": "Microsoft.Extensions.Http.PolicyHttpMessageHandler", 52 | "Visibility": "Public", 53 | "Kind": "Class", 54 | "BaseType": "System.Net.Http.DelegatingHandler", 55 | "ImplementedInterfaces": [], 56 | "Members": [ 57 | { 58 | "Kind": "Method", 59 | "Name": "SendAsync", 60 | "Parameters": [ 61 | { 62 | "Name": "request", 63 | "Type": "System.Net.Http.HttpRequestMessage" 64 | }, 65 | { 66 | "Name": "cancellationToken", 67 | "Type": "System.Threading.CancellationToken" 68 | } 69 | ], 70 | "ReturnType": "System.Threading.Tasks.Task", 71 | "Virtual": true, 72 | "Override": true, 73 | "Visibility": "Protected", 74 | "GenericParameter": [] 75 | }, 76 | { 77 | "Kind": "Method", 78 | "Name": "SendCoreAsync", 79 | "Parameters": [ 80 | { 81 | "Name": "request", 82 | "Type": "System.Net.Http.HttpRequestMessage" 83 | }, 84 | { 85 | "Name": "context", 86 | "Type": "Polly.Context" 87 | }, 88 | { 89 | "Name": "cancellationToken", 90 | "Type": "System.Threading.CancellationToken" 91 | } 92 | ], 93 | "ReturnType": "System.Threading.Tasks.Task", 94 | "Virtual": true, 95 | "Visibility": "Protected", 96 | "GenericParameter": [] 97 | }, 98 | { 99 | "Kind": "Constructor", 100 | "Name": ".ctor", 101 | "Parameters": [ 102 | { 103 | "Name": "policy", 104 | "Type": "Polly.IAsyncPolicy" 105 | } 106 | ], 107 | "Visibility": "Public", 108 | "GenericParameter": [] 109 | }, 110 | { 111 | "Kind": "Constructor", 112 | "Name": ".ctor", 113 | "Parameters": [ 114 | { 115 | "Name": "policySelector", 116 | "Type": "System.Func>" 117 | } 118 | ], 119 | "Visibility": "Public", 120 | "GenericParameter": [] 121 | } 122 | ], 123 | "GenericParameters": [] 124 | }, 125 | { 126 | "Name": "Microsoft.Extensions.DependencyInjection.PollyHttpClientBuilderExtensions", 127 | "Visibility": "Public", 128 | "Kind": "Class", 129 | "Abstract": true, 130 | "Static": true, 131 | "Sealed": true, 132 | "ImplementedInterfaces": [], 133 | "Members": [ 134 | { 135 | "Kind": "Method", 136 | "Name": "AddPolicyHandler", 137 | "Parameters": [ 138 | { 139 | "Name": "builder", 140 | "Type": "Microsoft.Extensions.DependencyInjection.IHttpClientBuilder" 141 | }, 142 | { 143 | "Name": "policy", 144 | "Type": "Polly.IAsyncPolicy" 145 | } 146 | ], 147 | "ReturnType": "Microsoft.Extensions.DependencyInjection.IHttpClientBuilder", 148 | "Static": true, 149 | "Extension": true, 150 | "Visibility": "Public", 151 | "GenericParameter": [] 152 | }, 153 | { 154 | "Kind": "Method", 155 | "Name": "AddPolicyHandler", 156 | "Parameters": [ 157 | { 158 | "Name": "builder", 159 | "Type": "Microsoft.Extensions.DependencyInjection.IHttpClientBuilder" 160 | }, 161 | { 162 | "Name": "policySelector", 163 | "Type": "System.Func>" 164 | } 165 | ], 166 | "ReturnType": "Microsoft.Extensions.DependencyInjection.IHttpClientBuilder", 167 | "Static": true, 168 | "Extension": true, 169 | "Visibility": "Public", 170 | "GenericParameter": [] 171 | }, 172 | { 173 | "Kind": "Method", 174 | "Name": "AddPolicyHandler", 175 | "Parameters": [ 176 | { 177 | "Name": "builder", 178 | "Type": "Microsoft.Extensions.DependencyInjection.IHttpClientBuilder" 179 | }, 180 | { 181 | "Name": "policySelector", 182 | "Type": "System.Func>" 183 | } 184 | ], 185 | "ReturnType": "Microsoft.Extensions.DependencyInjection.IHttpClientBuilder", 186 | "Static": true, 187 | "Extension": true, 188 | "Visibility": "Public", 189 | "GenericParameter": [] 190 | }, 191 | { 192 | "Kind": "Method", 193 | "Name": "AddPolicyHandlerFromRegistry", 194 | "Parameters": [ 195 | { 196 | "Name": "builder", 197 | "Type": "Microsoft.Extensions.DependencyInjection.IHttpClientBuilder" 198 | }, 199 | { 200 | "Name": "policyKey", 201 | "Type": "System.String" 202 | } 203 | ], 204 | "ReturnType": "Microsoft.Extensions.DependencyInjection.IHttpClientBuilder", 205 | "Static": true, 206 | "Extension": true, 207 | "Visibility": "Public", 208 | "GenericParameter": [] 209 | }, 210 | { 211 | "Kind": "Method", 212 | "Name": "AddPolicyHandlerFromRegistry", 213 | "Parameters": [ 214 | { 215 | "Name": "builder", 216 | "Type": "Microsoft.Extensions.DependencyInjection.IHttpClientBuilder" 217 | }, 218 | { 219 | "Name": "policySelector", 220 | "Type": "System.Func, System.Net.Http.HttpRequestMessage, Polly.IAsyncPolicy>" 221 | } 222 | ], 223 | "ReturnType": "Microsoft.Extensions.DependencyInjection.IHttpClientBuilder", 224 | "Static": true, 225 | "Extension": true, 226 | "Visibility": "Public", 227 | "GenericParameter": [] 228 | }, 229 | { 230 | "Kind": "Method", 231 | "Name": "AddTransientHttpErrorPolicy", 232 | "Parameters": [ 233 | { 234 | "Name": "builder", 235 | "Type": "Microsoft.Extensions.DependencyInjection.IHttpClientBuilder" 236 | }, 237 | { 238 | "Name": "configurePolicy", 239 | "Type": "System.Func, Polly.IAsyncPolicy>" 240 | } 241 | ], 242 | "ReturnType": "Microsoft.Extensions.DependencyInjection.IHttpClientBuilder", 243 | "Static": true, 244 | "Extension": true, 245 | "Visibility": "Public", 246 | "GenericParameter": [] 247 | } 248 | ], 249 | "GenericParameters": [] 250 | }, 251 | { 252 | "Name": "Microsoft.Extensions.DependencyInjection.PollyServiceCollectionExtensions", 253 | "Visibility": "Public", 254 | "Kind": "Class", 255 | "Abstract": true, 256 | "Static": true, 257 | "Sealed": true, 258 | "ImplementedInterfaces": [], 259 | "Members": [ 260 | { 261 | "Kind": "Method", 262 | "Name": "AddPolicyRegistry", 263 | "Parameters": [ 264 | { 265 | "Name": "services", 266 | "Type": "Microsoft.Extensions.DependencyInjection.IServiceCollection" 267 | } 268 | ], 269 | "ReturnType": "Polly.Registry.IPolicyRegistry", 270 | "Static": true, 271 | "Extension": true, 272 | "Visibility": "Public", 273 | "GenericParameter": [] 274 | }, 275 | { 276 | "Kind": "Method", 277 | "Name": "AddPolicyRegistry", 278 | "Parameters": [ 279 | { 280 | "Name": "services", 281 | "Type": "Microsoft.Extensions.DependencyInjection.IServiceCollection" 282 | }, 283 | { 284 | "Name": "registry", 285 | "Type": "Polly.Registry.IPolicyRegistry" 286 | } 287 | ], 288 | "ReturnType": "Polly.Registry.IPolicyRegistry", 289 | "Static": true, 290 | "Extension": true, 291 | "Visibility": "Public", 292 | "GenericParameter": [] 293 | } 294 | ], 295 | "GenericParameters": [] 296 | } 297 | ] 298 | } -------------------------------------------------------------------------------- /src/Microsoft.Extensions.Http/ActiveHandlerTrackingEntry.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 | using System.Threading; 7 | using Microsoft.Extensions.DependencyInjection; 8 | using Microsoft.Extensions.Internal; 9 | 10 | namespace Microsoft.Extensions.Http 11 | { 12 | // Thread-safety: We treat this class as immutable except for the timer. Creating a new object 13 | // for the 'expiry' pool simplifies the threading requirements significantly. 14 | internal class ActiveHandlerTrackingEntry 15 | { 16 | private static readonly TimerCallback _timerCallback = (s) => ((ActiveHandlerTrackingEntry)s).Timer_Tick(); 17 | private readonly object _lock; 18 | private bool _timerInitialized; 19 | private Timer _timer; 20 | private TimerCallback _callback; 21 | 22 | public ActiveHandlerTrackingEntry( 23 | string name, 24 | LifetimeTrackingHttpMessageHandler handler, 25 | IServiceScope scope, 26 | TimeSpan lifetime) 27 | { 28 | Name = name; 29 | Handler = handler; 30 | Scope = scope; 31 | Lifetime = lifetime; 32 | 33 | _lock = new object(); 34 | } 35 | 36 | public LifetimeTrackingHttpMessageHandler Handler { get; private set; } 37 | 38 | public TimeSpan Lifetime { get; } 39 | 40 | public string Name { get; } 41 | 42 | public IServiceScope Scope { get; } 43 | 44 | public void StartExpiryTimer(TimerCallback callback) 45 | { 46 | if (Lifetime == Timeout.InfiniteTimeSpan) 47 | { 48 | return; // never expires. 49 | } 50 | 51 | if (Volatile.Read(ref _timerInitialized)) 52 | { 53 | return; 54 | } 55 | 56 | StartExpiryTimerSlow(callback); 57 | } 58 | 59 | private void StartExpiryTimerSlow(TimerCallback callback) 60 | { 61 | Debug.Assert(Lifetime != Timeout.InfiniteTimeSpan); 62 | 63 | lock (_lock) 64 | { 65 | if (Volatile.Read(ref _timerInitialized)) 66 | { 67 | return; 68 | } 69 | 70 | _callback = callback; 71 | _timer = NonCapturingTimer.Create(_timerCallback, this, Lifetime, Timeout.InfiniteTimeSpan); 72 | } 73 | } 74 | 75 | private void Timer_Tick() 76 | { 77 | Debug.Assert(_callback != null); 78 | Debug.Assert(_timer != null); 79 | 80 | lock (_lock) 81 | { 82 | _timer.Dispose(); 83 | _timer = null; 84 | 85 | _callback(this); 86 | } 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/Microsoft.Extensions.Http/DefaultHttpMessageHandlerBuilder.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 | 8 | namespace Microsoft.Extensions.Http 9 | { 10 | internal class DefaultHttpMessageHandlerBuilder : HttpMessageHandlerBuilder 11 | { 12 | public DefaultHttpMessageHandlerBuilder(IServiceProvider services) 13 | { 14 | Services = services; 15 | } 16 | 17 | private string _name; 18 | 19 | public override string Name 20 | { 21 | get => _name; 22 | set 23 | { 24 | if (value == null) 25 | { 26 | throw new ArgumentNullException(nameof(value)); 27 | } 28 | 29 | _name = value; 30 | } 31 | } 32 | 33 | public override HttpMessageHandler PrimaryHandler { get; set; } = new HttpClientHandler(); 34 | 35 | public override IList AdditionalHandlers { get; } = new List(); 36 | 37 | public override IServiceProvider Services { get; } 38 | 39 | public override HttpMessageHandler Build() 40 | { 41 | if (PrimaryHandler == null) 42 | { 43 | var message = Resources.FormatHttpMessageHandlerBuilder_PrimaryHandlerIsNull(nameof(PrimaryHandler)); 44 | throw new InvalidOperationException(message); 45 | } 46 | 47 | return CreateHandlerPipeline(PrimaryHandler, AdditionalHandlers); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/Microsoft.Extensions.Http/DefaultTypedHttpClientFactory.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; 7 | using Microsoft.Extensions.DependencyInjection; 8 | 9 | namespace Microsoft.Extensions.Http 10 | { 11 | internal class DefaultTypedHttpClientFactory : ITypedHttpClientFactory 12 | { 13 | private readonly Cache _cache; 14 | private readonly IServiceProvider _services; 15 | 16 | public DefaultTypedHttpClientFactory(Cache cache, IServiceProvider services) 17 | { 18 | if (cache == null) 19 | { 20 | throw new ArgumentNullException(nameof(cache)); 21 | } 22 | 23 | if (services == null) 24 | { 25 | throw new ArgumentNullException(nameof(services)); 26 | } 27 | 28 | _cache = cache; 29 | _services = services; 30 | } 31 | 32 | public TClient CreateClient(HttpClient httpClient) 33 | { 34 | if (httpClient == null) 35 | { 36 | throw new ArgumentNullException(nameof(httpClient)); 37 | } 38 | 39 | return (TClient)_cache.Activator(_services, new object[] { httpClient }); 40 | } 41 | 42 | // The Cache should be registered as a singleton, so it that it can 43 | // act as a cache for the Activator. This allows the outer class to be registered 44 | // as a transient, so that it doesn't close over the application root service provider. 45 | public class Cache 46 | { 47 | private readonly static Func _createActivator = () => ActivatorUtilities.CreateFactory(typeof(TClient), new Type[] { typeof(HttpClient), }); 48 | 49 | private ObjectFactory _activator; 50 | private bool _initialized; 51 | private object _lock; 52 | 53 | public ObjectFactory Activator => LazyInitializer.EnsureInitialized( 54 | ref _activator, 55 | ref _initialized, 56 | ref _lock, 57 | _createActivator); 58 | } 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/Microsoft.Extensions.Http/DependencyInjection/DefaultHttpClientBuilder.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.DependencyInjection 5 | { 6 | internal class DefaultHttpClientBuilder : IHttpClientBuilder 7 | { 8 | public DefaultHttpClientBuilder(IServiceCollection services, string name) 9 | { 10 | Services = services; 11 | Name = name; 12 | } 13 | 14 | public string Name { get; } 15 | 16 | public IServiceCollection Services { get; } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/Microsoft.Extensions.Http/DependencyInjection/IHttpClientBuilder.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.Net.Http; 5 | 6 | namespace Microsoft.Extensions.DependencyInjection 7 | { 8 | /// 9 | /// A builder for configuring named instances returned by . 10 | /// 11 | public interface IHttpClientBuilder 12 | { 13 | /// 14 | /// Gets the name of the client configured by this builder. 15 | /// 16 | string Name { get; } 17 | 18 | /// 19 | /// Gets the application service collection. 20 | /// 21 | IServiceCollection Services { get; } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Microsoft.Extensions.Http/ExpiredHandlerTrackingEntry.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 Microsoft.Extensions.DependencyInjection; 7 | 8 | namespace Microsoft.Extensions.Http 9 | { 10 | // Thread-safety: This class is immutable 11 | internal class ExpiredHandlerTrackingEntry 12 | { 13 | private readonly WeakReference _livenessTracker; 14 | 15 | // IMPORTANT: don't cache a reference to `other` or `other.Handler` here. 16 | // We need to allow it to be GC'ed. 17 | public ExpiredHandlerTrackingEntry(ActiveHandlerTrackingEntry other) 18 | { 19 | Name = other.Name; 20 | Scope = other.Scope; 21 | 22 | _livenessTracker = new WeakReference(other.Handler); 23 | InnerHandler = other.Handler.InnerHandler; 24 | } 25 | 26 | public bool CanDispose => !_livenessTracker.IsAlive; 27 | 28 | public HttpMessageHandler InnerHandler { get; } 29 | 30 | public string Name { get; } 31 | 32 | public IServiceScope Scope { get; } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/Microsoft.Extensions.Http/HttpClientFactoryExtensions.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 Microsoft.Extensions.Options; 5 | 6 | namespace System.Net.Http 7 | { 8 | /// 9 | /// Extensions methods for . 10 | /// 11 | public static class HttpClientFactoryExtensions 12 | { 13 | /// 14 | /// Creates a new using the default configuration. 15 | /// 16 | /// The . 17 | /// An configured using the default configuration. 18 | public static HttpClient CreateClient(this IHttpClientFactory factory) 19 | { 20 | if (factory == null) 21 | { 22 | throw new ArgumentNullException(nameof(factory)); 23 | } 24 | 25 | return factory.CreateClient(Options.DefaultName); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Microsoft.Extensions.Http/HttpClientFactoryOptions.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.Threading; 8 | using Microsoft.Extensions.DependencyInjection; 9 | 10 | namespace Microsoft.Extensions.Http 11 | { 12 | /// 13 | /// An options class for configuring the default . 14 | /// 15 | public class HttpClientFactoryOptions 16 | { 17 | // Establishing a minimum lifetime helps us avoid some possible destructive cases. 18 | // 19 | // IMPORTANT: This is used in a resource string. Update the resource if this changes. 20 | internal readonly static TimeSpan MinimumHandlerLifetime = TimeSpan.FromSeconds(1); 21 | 22 | private TimeSpan _handlerLifetime = TimeSpan.FromMinutes(2); 23 | 24 | /// 25 | /// Gets a list of operations used to configure an . 26 | /// 27 | public IList> HttpMessageHandlerBuilderActions { get; } = new List>(); 28 | 29 | /// 30 | /// Gets a list of operations used to configure an . 31 | /// 32 | public IList> HttpClientActions { get; } = new List>(); 33 | 34 | /// 35 | /// Gets or sets the length of time that a instance can be reused. Each named 36 | /// client can have its own configured handler lifetime value. The default value of this property is two minutes. 37 | /// Set the lifetime to to disable handler expiry. 38 | /// 39 | /// 40 | /// 41 | /// The default implementation of will pool the 42 | /// instances created by the factory to reduce resource consumption. This setting configures the amount of time 43 | /// a handler can be pooled before it is scheduled for removal from the pool and disposal. 44 | /// 45 | /// 46 | /// Pooling of handlers is desirable as each handler typically manages its own underlying HTTP connections; creating 47 | /// more handlers than necessary can result in connection delays. Some handlers also keep connections open indefinitely 48 | /// which can prevent the handler from reacting to DNS changes. The value of should be 49 | /// chosen with an understanding of the application's requirement to respond to changes in the network environment. 50 | /// 51 | /// 52 | /// Expiry of a handler will not immediately dispose the handler. An expired handler is placed in a separate pool 53 | /// which is processed at intervals to dispose handlers only when they become unreachable. Using long-lived 54 | /// instances will prevent the underlying from being 55 | /// disposed until all references are garbage-collected. 56 | /// 57 | /// 58 | public TimeSpan HandlerLifetime 59 | { 60 | get => _handlerLifetime; 61 | set 62 | { 63 | if (value != Timeout.InfiniteTimeSpan && value < MinimumHandlerLifetime) 64 | { 65 | throw new ArgumentException(Resources.HandlerLifetime_InvalidValue, nameof(value)); 66 | } 67 | 68 | _handlerLifetime = value; 69 | } 70 | } 71 | 72 | /// 73 | /// 74 | /// Gets or sets a value that determines whether the will 75 | /// create a dependency injection scope when building an . 76 | /// If false (default), a scope will be created, otherwise a scope will not be created. 77 | /// 78 | /// 79 | /// This option is provided for compatibility with existing applications. It is recommended 80 | /// to use the default setting for new applications. 81 | /// 82 | /// 83 | /// 84 | /// 85 | /// The will (by default) create a dependency injection scope 86 | /// each time it creates an . The created scope has the same 87 | /// lifetime as the message handler, and will be disposed when the message handler is disposed. 88 | /// 89 | /// 90 | /// When operations that are part of are executed 91 | /// they will be provided with the scoped via 92 | /// . This includes retrieving a message handler 93 | /// from dependency injection, such as one registered using 94 | /// . 95 | /// 96 | /// 97 | public bool SuppressHandlerScope { get; set; } 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/Microsoft.Extensions.Http/HttpMessageHandlerBuilder.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.Net.Http; 8 | 9 | namespace Microsoft.Extensions.Http 10 | { 11 | /// 12 | /// A builder abstraction for configuring instances. 13 | /// 14 | /// 15 | /// The is registered in the service collection as 16 | /// a transient service. Callers should retrieve a new instance for each to 17 | /// be created. Implementors should expect each instance to be used a single time. 18 | /// 19 | public abstract class HttpMessageHandlerBuilder 20 | { 21 | /// 22 | /// Gets or sets the name of the being created. 23 | /// 24 | /// 25 | /// The is set by the infrastructure 26 | /// and is public for unit testing purposes only. Setting the outside of 27 | /// testing scenarios may have unpredictable results. 28 | /// 29 | public abstract string Name { get; set; } 30 | 31 | /// 32 | /// Gets or sets the primary . 33 | /// 34 | public abstract HttpMessageHandler PrimaryHandler { get; set; } 35 | 36 | /// 37 | /// Gets a list of additional instances used to configure an 38 | /// pipeline. 39 | /// 40 | public abstract IList AdditionalHandlers { get; } 41 | 42 | /// 43 | /// Gets an which can be used to resolve services 44 | /// from the dependency injection container. 45 | /// 46 | /// 47 | /// This property is sensitive to the value of 48 | /// . If true this 49 | /// property will be a reference to the application's root service provider. If false 50 | /// (default) this will be a reference to a scoped service provider that has the same 51 | /// lifetime as the handler being created. 52 | /// 53 | public virtual IServiceProvider Services { get; } 54 | 55 | /// 56 | /// Creates an . 57 | /// 58 | /// 59 | /// An built from the and 60 | /// . 61 | /// 62 | public abstract HttpMessageHandler Build(); 63 | 64 | protected internal static HttpMessageHandler CreateHandlerPipeline(HttpMessageHandler primaryHandler, IEnumerable additionalHandlers) 65 | { 66 | // This is similar to https://github.com/aspnet/AspNetWebStack/blob/master/src/System.Net.Http.Formatting/HttpClientFactory.cs#L58 67 | // but we don't want to take that package as a dependency. 68 | 69 | if (primaryHandler == null) 70 | { 71 | throw new ArgumentNullException(nameof(primaryHandler)); 72 | } 73 | 74 | if (additionalHandlers == null) 75 | { 76 | throw new ArgumentNullException(nameof(additionalHandlers)); 77 | } 78 | 79 | var additionalHandlersList = additionalHandlers as IReadOnlyList ?? additionalHandlers.ToArray(); 80 | 81 | var next = primaryHandler; 82 | for (var i = additionalHandlersList.Count - 1; i >= 0; i--) 83 | { 84 | var handler = additionalHandlersList[i]; 85 | if (handler == null) 86 | { 87 | var message = Resources.FormatHttpMessageHandlerBuilder_AdditionalHandlerIsNull(nameof(additionalHandlers)); 88 | throw new InvalidOperationException(message); 89 | } 90 | 91 | // Checking for this allows us to catch cases where someone has tried to re-use a handler. That really won't 92 | // work the way you want and it can be tricky for callers to figure out. 93 | if (handler.InnerHandler != null) 94 | { 95 | var message = Resources.FormatHttpMessageHandlerBuilder_AdditionHandlerIsInvalid( 96 | nameof(DelegatingHandler.InnerHandler), 97 | nameof(DelegatingHandler), 98 | nameof(HttpMessageHandlerBuilder), 99 | Environment.NewLine, 100 | handler); 101 | throw new InvalidOperationException(message); 102 | } 103 | 104 | handler.InnerHandler = next; 105 | next = handler; 106 | } 107 | 108 | return next; 109 | } 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/Microsoft.Extensions.Http/HttpMessageHandlerFactoryExtensions.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 Microsoft.Extensions.Options; 5 | 6 | namespace System.Net.Http 7 | { 8 | /// 9 | /// Extensions methods for . 10 | /// 11 | public static class HttpMessageHandlerFactoryExtensions 12 | { 13 | /// 14 | /// Creates a new using the default configuration. 15 | /// 16 | /// The . 17 | /// An configured using the default configuration. 18 | public static HttpMessageHandler CreateHandler(this IHttpMessageHandlerFactory factory) 19 | { 20 | if (factory == null) 21 | { 22 | throw new ArgumentNullException(nameof(factory)); 23 | } 24 | 25 | return factory.CreateHandler(Options.DefaultName); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/Microsoft.Extensions.Http/IHttpClientFactory.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 Microsoft.Extensions.DependencyInjection; 5 | 6 | namespace System.Net.Http 7 | { 8 | /// 9 | /// A factory abstraction for a component that can create instances with custom 10 | /// configuration for a given logical name. 11 | /// 12 | /// 13 | /// A default can be registered in an 14 | /// by calling . 15 | /// The default will be registered in the service collection as a singleton. 16 | /// 17 | public interface IHttpClientFactory 18 | { 19 | /// 20 | /// Creates and configures an instance using the configuration that corresponds 21 | /// to the logical name specified by . 22 | /// 23 | /// The logical name of the client to create. 24 | /// A new instance. 25 | /// 26 | /// 27 | /// Each call to is guaranteed to return a new 28 | /// instance. Callers may cache the returned instance indefinitely or surround 29 | /// its use in a using block to dispose it when desired. 30 | /// 31 | /// 32 | /// The default implementation may cache the underlying 33 | /// instances to improve performance. 34 | /// 35 | /// 36 | /// Callers are also free to mutate the returned instance's public properties 37 | /// as desired. 38 | /// 39 | /// 40 | HttpClient CreateClient(string name); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/Microsoft.Extensions.Http/IHttpMessageHandlerBuilderFilter.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 | 6 | namespace Microsoft.Extensions.Http 7 | { 8 | /// 9 | /// Used by the to apply additional initialization to the configure the 10 | /// immediately before 11 | /// is called. 12 | /// 13 | public interface IHttpMessageHandlerBuilderFilter 14 | { 15 | /// 16 | /// Applies additional initialization to the 17 | /// 18 | /// A delegate which will run the next . 19 | Action Configure(Action next); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/Microsoft.Extensions.Http/IHttpMessageHandlerFactory.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 Microsoft.Extensions.DependencyInjection; 5 | 6 | namespace System.Net.Http 7 | { 8 | /// 9 | /// A factory abstraction for a component that can create instances with custom 10 | /// configuration for a given logical name. 11 | /// 12 | /// 13 | /// A default can be registered in an 14 | /// by calling . 15 | /// The default will be registered in the service collection as a singleton. 16 | /// 17 | public interface IHttpMessageHandlerFactory 18 | { 19 | /// 20 | /// Creates and configures an instance using the configuration that corresponds 21 | /// to the logical name specified by . 22 | /// 23 | /// The logical name of the message handler to create. 24 | /// A new instance. 25 | /// 26 | /// 27 | /// The default implementation may cache the underlying 28 | /// instances to improve performance. 29 | /// 30 | /// 31 | /// The default implementation also manages the lifetime of the 32 | /// handler created, so disposing of the returned by this method may 33 | /// have no effect. 34 | /// 35 | /// 36 | HttpMessageHandler CreateHandler(string name); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/Microsoft.Extensions.Http/ITypedHttpClientFactory.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 Microsoft.Extensions.DependencyInjection; 7 | 8 | namespace Microsoft.Extensions.Http 9 | { 10 | /// 11 | /// A factory abstraction for a component that can create typed client instances with custom 12 | /// configuration for a given logical name. 13 | /// 14 | /// The type of typed client to create. 15 | /// 16 | /// 17 | /// The is infrastructure that supports the 18 | /// and 19 | /// functionality. This type 20 | /// should rarely be used directly in application code, use instead 21 | /// to retrieve typed clients. 22 | /// 23 | /// 24 | /// A default can be registered in an 25 | /// by calling . 26 | /// The default will be registered in the service collection as a singleton 27 | /// open-generic service. 28 | /// 29 | /// 30 | /// The default uses type activation to create typed client instances. Typed 31 | /// client types are not retrieved directly from the . See 32 | /// for details. 33 | /// 34 | /// 35 | /// 36 | /// This sample shows the basic pattern for defining a typed client class. 37 | /// 38 | /// class ExampleClient 39 | /// { 40 | /// private readonly HttpClient _httpClient; 41 | /// private readonly ILogger _logger; 42 | /// 43 | /// // typed clients can use constructor injection to access additional services 44 | /// public ExampleClient(HttpClient httpClient, ILogger<ExampleClient> logger) 45 | /// { 46 | /// _httpClient = httpClient; 47 | /// _logger = logger; 48 | /// } 49 | /// 50 | /// // typed clients can expose the HttpClient for application code to call directly 51 | /// public HttpClient HttpClient => _httpClient; 52 | /// 53 | /// // typed clients can also define methods that abstract usage of the HttpClient 54 | /// public async Task SendHelloRequest() 55 | /// { 56 | /// var response = await _httpClient.GetAsync("/helloworld"); 57 | /// response.EnsureSuccessStatusCode(); 58 | /// } 59 | /// } 60 | /// 61 | /// 62 | /// 63 | /// This sample shows how to consume a typed client from an ASP.NET Core middleware. 64 | /// 65 | /// // in Startup.cs 66 | /// public void Configure(IApplicationBuilder app, ExampleClient exampleClient) 67 | /// { 68 | /// app.Run(async (context) => 69 | /// { 70 | /// var response = await _exampleClient.GetAsync("/helloworld"); 71 | /// await context.Response.WriteAsync("Remote server said: "); 72 | /// await response.Content.CopyToAsync(context.Response.Body); 73 | /// }); 74 | /// } 75 | /// 76 | /// 77 | /// 78 | /// This sample shows how to consume a typed client from an ASP.NET Core MVC Controller. 79 | /// 80 | /// // in Controllers/HomeController.cs 81 | /// public class HomeController : ControllerBase(IApplicationBuilder app, ExampleClient exampleClient) 82 | /// { 83 | /// private readonly ExampleClient _exampleClient; 84 | /// 85 | /// public HomeController(ExampleClient exampleClient) 86 | /// { 87 | /// _exampleClient = exampleClient; 88 | /// } 89 | /// 90 | /// public async Task<IActionResult> Index() 91 | /// { 92 | /// var response = await _exampleClient.GetAsync("/helloworld"); 93 | /// var text = await response.Content.ReadAsStringAsync(); 94 | /// return Content("Remote server said: " + text, "text/plain"); 95 | /// }; 96 | /// } 97 | /// 98 | /// 99 | public interface ITypedHttpClientFactory 100 | { 101 | /// 102 | /// Creates a typed client given an associated . 103 | /// 104 | /// 105 | /// An created by the for the named client 106 | /// associated with . 107 | /// 108 | /// An instance of . 109 | TClient CreateClient(HttpClient httpClient); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/Microsoft.Extensions.Http/LifetimeTrackingHttpMessageHandler.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.Net.Http; 5 | 6 | namespace Microsoft.Extensions.Http 7 | { 8 | // This a marker used to check if the underlying handler should be disposed. HttpClients 9 | // share a reference to an instance of this class, and when it goes out of scope the inner handler 10 | // is eligible to be disposed. 11 | internal class LifetimeTrackingHttpMessageHandler : DelegatingHandler 12 | { 13 | public LifetimeTrackingHttpMessageHandler(HttpMessageHandler innerHandler) 14 | : base(innerHandler) 15 | { 16 | } 17 | 18 | protected override void Dispose(bool disposing) 19 | { 20 | // The lifetime of this is tracked separately by ActiveHandlerTrackingEntry 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/Microsoft.Extensions.Http/Logging/HttpHeadersLogValue.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; 6 | using System.Collections.Generic; 7 | using System.Net.Http.Headers; 8 | using System.Text; 9 | 10 | namespace Microsoft.Extensions.Http.Logging 11 | { 12 | internal class HttpHeadersLogValue : IReadOnlyList> 13 | { 14 | private readonly Kind _kind; 15 | 16 | private string _formatted; 17 | private List> _values; 18 | 19 | public HttpHeadersLogValue(Kind kind, HttpHeaders headers, HttpHeaders contentHeaders) 20 | { 21 | _kind = kind; 22 | 23 | Headers = headers; 24 | ContentHeaders = contentHeaders; 25 | } 26 | 27 | public HttpHeaders Headers { get; } 28 | 29 | public HttpHeaders ContentHeaders { get; } 30 | 31 | private List> Values 32 | { 33 | get 34 | { 35 | if (_values == null) 36 | { 37 | var values = new List>(); 38 | 39 | foreach (var kvp in Headers) 40 | { 41 | values.Add(new KeyValuePair(kvp.Key, kvp.Value)); 42 | } 43 | 44 | if (ContentHeaders != null) 45 | { 46 | foreach (var kvp in ContentHeaders) 47 | { 48 | values.Add(new KeyValuePair(kvp.Key, kvp.Value)); 49 | } 50 | } 51 | 52 | _values = values; 53 | } 54 | 55 | return _values; 56 | } 57 | } 58 | 59 | public KeyValuePair this[int index] 60 | { 61 | get 62 | { 63 | if (index < 0 || index >= Count) 64 | { 65 | throw new IndexOutOfRangeException(nameof(index)); 66 | } 67 | 68 | return Values[index]; 69 | } 70 | } 71 | 72 | public int Count => Values.Count; 73 | 74 | public IEnumerator> GetEnumerator() 75 | { 76 | return Values.GetEnumerator(); 77 | } 78 | 79 | IEnumerator IEnumerable.GetEnumerator() 80 | { 81 | return Values.GetEnumerator(); 82 | } 83 | 84 | public override string ToString() 85 | { 86 | if (_formatted == null) 87 | { 88 | var builder = new StringBuilder(); 89 | builder.AppendLine(_kind == Kind.Request ? "Request Headers:" : "Response Headers:"); 90 | 91 | for (var i = 0; i < Values.Count; i++) 92 | { 93 | var kvp = Values[i]; 94 | builder.Append(kvp.Key); 95 | builder.Append(": "); 96 | 97 | foreach (var value in (IEnumerable)kvp.Value) 98 | { 99 | builder.Append(value); 100 | builder.Append(", "); 101 | } 102 | 103 | // Remove the extra ', ' 104 | builder.Remove(builder.Length - 2, 2); 105 | builder.AppendLine(); 106 | } 107 | 108 | _formatted = builder.ToString(); 109 | } 110 | 111 | return _formatted; 112 | } 113 | 114 | public enum Kind 115 | { 116 | Request, 117 | Response, 118 | } 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/Microsoft.Extensions.Http/Logging/LoggingHttpMessageHandler.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; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | using System.Net; 9 | using System.Net.Http; 10 | using System.Net.Http.Headers; 11 | using System.Text; 12 | using System.Threading; 13 | using System.Threading.Tasks; 14 | using Microsoft.Extensions.Internal; 15 | using Microsoft.Extensions.Logging; 16 | 17 | namespace Microsoft.Extensions.Http.Logging 18 | { 19 | public class LoggingHttpMessageHandler : DelegatingHandler 20 | { 21 | private ILogger _logger; 22 | 23 | public LoggingHttpMessageHandler(ILogger logger) 24 | { 25 | if (logger == null) 26 | { 27 | throw new ArgumentNullException(nameof(logger)); 28 | } 29 | 30 | _logger = logger; 31 | } 32 | 33 | protected async override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) 34 | { 35 | if (request == null) 36 | { 37 | throw new ArgumentNullException(nameof(request)); 38 | } 39 | 40 | var stopwatch = ValueStopwatch.StartNew(); 41 | 42 | // Not using a scope here because we always expect this to be at the end of the pipeline, thus there's 43 | // not really anything to surround. 44 | Log.RequestStart(_logger, request); 45 | var response = await base.SendAsync(request, cancellationToken); 46 | Log.RequestEnd(_logger, response, stopwatch.GetElapsedTime()); 47 | 48 | return response; 49 | } 50 | 51 | private static class Log 52 | { 53 | public static class EventIds 54 | { 55 | public static readonly EventId RequestStart = new EventId(100, "RequestStart"); 56 | public static readonly EventId RequestEnd = new EventId(101, "RequestEnd"); 57 | 58 | public static readonly EventId RequestHeader = new EventId(102, "RequestHeader"); 59 | public static readonly EventId ResponseHeader = new EventId(103, "ResponseHeader"); 60 | } 61 | 62 | private static readonly Action _requestStart = LoggerMessage.Define( 63 | LogLevel.Information, 64 | EventIds.RequestStart, 65 | "Sending HTTP request {HttpMethod} {Uri}"); 66 | 67 | private static readonly Action _requestEnd = LoggerMessage.Define( 68 | LogLevel.Information, 69 | EventIds.RequestEnd, 70 | "Received HTTP response after {ElapsedMilliseconds}ms - {StatusCode}"); 71 | 72 | public static void RequestStart(ILogger logger, HttpRequestMessage request) 73 | { 74 | _requestStart(logger, request.Method, request.RequestUri, null); 75 | 76 | if (logger.IsEnabled(LogLevel.Trace)) 77 | { 78 | logger.Log( 79 | LogLevel.Trace, 80 | EventIds.RequestHeader, 81 | new HttpHeadersLogValue(HttpHeadersLogValue.Kind.Request, request.Headers, request.Content?.Headers), 82 | null, 83 | (state, ex) => state.ToString()); 84 | } 85 | } 86 | 87 | public static void RequestEnd(ILogger logger, HttpResponseMessage response, TimeSpan duration) 88 | { 89 | _requestEnd(logger, duration.TotalMilliseconds, response.StatusCode, null); 90 | 91 | if (logger.IsEnabled(LogLevel.Trace)) 92 | { 93 | logger.Log( 94 | LogLevel.Trace, 95 | EventIds.ResponseHeader, 96 | new HttpHeadersLogValue(HttpHeadersLogValue.Kind.Response, response.Headers, response.Content?.Headers), 97 | null, 98 | (state, ex) => state.ToString()); 99 | } 100 | } 101 | } 102 | } 103 | } -------------------------------------------------------------------------------- /src/Microsoft.Extensions.Http/Logging/LoggingHttpMessageHandlerBuilderFilter.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.Http.Logging; 6 | using Microsoft.Extensions.Logging; 7 | 8 | namespace Microsoft.Extensions.Http 9 | { 10 | // Internal so we can change the requirements without breaking changes. 11 | internal class LoggingHttpMessageHandlerBuilderFilter : IHttpMessageHandlerBuilderFilter 12 | { 13 | private readonly ILoggerFactory _loggerFactory; 14 | 15 | public LoggingHttpMessageHandlerBuilderFilter(ILoggerFactory loggerFactory) 16 | { 17 | if (loggerFactory == null) 18 | { 19 | throw new ArgumentNullException(nameof(loggerFactory)); 20 | } 21 | 22 | _loggerFactory = loggerFactory; 23 | } 24 | 25 | public Action Configure(Action next) 26 | { 27 | if (next == null) 28 | { 29 | throw new ArgumentNullException(nameof(next)); 30 | } 31 | 32 | return (builder) => 33 | { 34 | // Run other configuration first, we want to decorate. 35 | next(builder); 36 | 37 | var loggerName = !string.IsNullOrEmpty(builder.Name) ? builder.Name : "Default"; 38 | 39 | // We want all of our logging message to show up as-if they are coming from HttpClient, 40 | // but also to include the name of the client for more fine-grained control. 41 | var outerLogger = _loggerFactory.CreateLogger($"System.Net.Http.HttpClient.{loggerName}.LogicalHandler"); 42 | var innerLogger = _loggerFactory.CreateLogger($"System.Net.Http.HttpClient.{loggerName}.ClientHandler"); 43 | 44 | // The 'scope' handler goes first so it can surround everything. 45 | builder.AdditionalHandlers.Insert(0, new LoggingScopeHttpMessageHandler(outerLogger)); 46 | 47 | // We want this handler to be last so we can log details about the request after 48 | // service discovery and security happen. 49 | builder.AdditionalHandlers.Add(new LoggingHttpMessageHandler(innerLogger)); 50 | 51 | }; 52 | } 53 | } 54 | } -------------------------------------------------------------------------------- /src/Microsoft.Extensions.Http/Logging/LoggingScopeHttpMessageHandler.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; 7 | using System.Net.Http; 8 | using System.Threading; 9 | using System.Threading.Tasks; 10 | using Microsoft.Extensions.Internal; 11 | using Microsoft.Extensions.Logging; 12 | 13 | namespace Microsoft.Extensions.Http.Logging 14 | { 15 | public class LoggingScopeHttpMessageHandler : DelegatingHandler 16 | { 17 | private ILogger _logger; 18 | 19 | public LoggingScopeHttpMessageHandler(ILogger logger) 20 | { 21 | if (logger == null) 22 | { 23 | throw new ArgumentNullException(nameof(logger)); 24 | } 25 | 26 | _logger = logger; 27 | } 28 | 29 | protected async override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) 30 | { 31 | if (request == null) 32 | { 33 | throw new ArgumentNullException(nameof(request)); 34 | } 35 | 36 | var stopwatch = ValueStopwatch.StartNew(); 37 | 38 | using (Log.BeginRequestPipelineScope(_logger, request)) 39 | { 40 | Log.RequestPipelineStart(_logger, request); 41 | var response = await base.SendAsync(request, cancellationToken); 42 | Log.RequestPipelineEnd(_logger, response, stopwatch.GetElapsedTime()); 43 | 44 | return response; 45 | } 46 | } 47 | 48 | private static class Log 49 | { 50 | public static class EventIds 51 | { 52 | public static readonly EventId PipelineStart = new EventId(100, "RequestPipelineStart"); 53 | public static readonly EventId PipelineEnd = new EventId(101, "RequestPipelineEnd"); 54 | 55 | public static readonly EventId RequestHeader = new EventId(102, "RequestPipelineRequestHeader"); 56 | public static readonly EventId ResponseHeader = new EventId(103, "RequestPipelineResponseHeader"); 57 | } 58 | 59 | private static readonly Func _beginRequestPipelineScope = LoggerMessage.DefineScope("HTTP {HttpMethod} {Uri}"); 60 | 61 | private static readonly Action _requestPipelineStart = LoggerMessage.Define( 62 | LogLevel.Information, 63 | EventIds.PipelineStart, 64 | "Start processing HTTP request {HttpMethod} {Uri}"); 65 | 66 | private static readonly Action _requestPipelineEnd = LoggerMessage.Define( 67 | LogLevel.Information, 68 | EventIds.PipelineEnd, 69 | "End processing HTTP request after {ElapsedMilliseconds}ms - {StatusCode}"); 70 | 71 | public static IDisposable BeginRequestPipelineScope(ILogger logger, HttpRequestMessage request) 72 | { 73 | return _beginRequestPipelineScope(logger, request.Method, request.RequestUri); 74 | } 75 | 76 | public static void RequestPipelineStart(ILogger logger, HttpRequestMessage request) 77 | { 78 | _requestPipelineStart(logger, request.Method, request.RequestUri, null); 79 | 80 | if (logger.IsEnabled(LogLevel.Trace)) 81 | { 82 | logger.Log( 83 | LogLevel.Trace, 84 | EventIds.RequestHeader, 85 | new HttpHeadersLogValue(HttpHeadersLogValue.Kind.Request, request.Headers, request.Content?.Headers), 86 | null, 87 | (state, ex) => state.ToString()); 88 | } 89 | } 90 | 91 | public static void RequestPipelineEnd(ILogger logger, HttpResponseMessage response, TimeSpan duration) 92 | { 93 | _requestPipelineEnd(logger, duration.TotalMilliseconds, response.StatusCode, null); 94 | 95 | if (logger.IsEnabled(LogLevel.Trace)) 96 | { 97 | logger.Log( 98 | LogLevel.Trace, 99 | EventIds.ResponseHeader, 100 | new HttpHeadersLogValue(HttpHeadersLogValue.Kind.Response, response.Headers, response.Content?.Headers), 101 | null, 102 | (state, ex) => state.ToString()); 103 | } 104 | } 105 | } 106 | } 107 | } -------------------------------------------------------------------------------- /src/Microsoft.Extensions.Http/Microsoft.Extensions.Http.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | The HttpClient factory is a pattern for configuring and retrieving named HttpClients in a composable way. The HttpClient factory provides extensibility to plug in DelegatingHandlers that address cross-cutting concerns such as service location, load balancing, and reliability. The default HttpClient factory provides built-in diagnostics and logging and manages the lifetimes of connections in a performant way. 6 | Commonly used types: 7 | System.Net.Http.IHttpClientFactory 8 | 9 | netstandard2.0 10 | $(NoWarn);CS1591 11 | true 12 | aspnetcore;httpclient 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | -------------------------------------------------------------------------------- /src/Microsoft.Extensions.Http/Properties/AssemblyInfo.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.Runtime.CompilerServices; 5 | 6 | [assembly: InternalsVisibleTo("Microsoft.Extensions.Http.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] 7 | [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7")] -------------------------------------------------------------------------------- /src/Microsoft.Extensions.Http/Properties/Resources.Designer.cs: -------------------------------------------------------------------------------- 1 | // 2 | namespace Microsoft.Extensions.Http 3 | { 4 | using System.Globalization; 5 | using System.Reflection; 6 | using System.Resources; 7 | 8 | internal static class Resources 9 | { 10 | private static readonly ResourceManager _resourceManager 11 | = new ResourceManager("Microsoft.Extensions.Http.Resources", typeof(Resources).GetTypeInfo().Assembly); 12 | 13 | /// 14 | /// The '{0}' must not contain a null entry. 15 | /// 16 | internal static string HttpMessageHandlerBuilder_AdditionalHandlerIsNull 17 | { 18 | get => GetString("HttpMessageHandlerBuilder_AdditionalHandlerIsNull"); 19 | } 20 | 21 | /// 22 | /// The '{0}' must not contain a null entry. 23 | /// 24 | internal static string FormatHttpMessageHandlerBuilder_AdditionalHandlerIsNull(object p0) 25 | => string.Format(CultureInfo.CurrentCulture, GetString("HttpMessageHandlerBuilder_AdditionalHandlerIsNull"), p0); 26 | 27 | /// 28 | /// The '{0}' property must be null. '{1}' instances provided to '{2}' must not be reused or cached.{3}Handler: '{4}' 29 | /// 30 | internal static string HttpMessageHandlerBuilder_AdditionHandlerIsInvalid 31 | { 32 | get => GetString("HttpMessageHandlerBuilder_AdditionHandlerIsInvalid"); 33 | } 34 | 35 | /// 36 | /// The '{0}' property must be null. '{1}' instances provided to '{2}' must not be reused or cached.{3}Handler: '{4}' 37 | /// 38 | internal static string FormatHttpMessageHandlerBuilder_AdditionHandlerIsInvalid(object p0, object p1, object p2, object p3, object p4) 39 | => string.Format(CultureInfo.CurrentCulture, GetString("HttpMessageHandlerBuilder_AdditionHandlerIsInvalid"), p0, p1, p2, p3, p4); 40 | 41 | /// 42 | /// The '{0}' must not be null. 43 | /// 44 | internal static string HttpMessageHandlerBuilder_PrimaryHandlerIsNull 45 | { 46 | get => GetString("HttpMessageHandlerBuilder_PrimaryHandlerIsNull"); 47 | } 48 | 49 | /// 50 | /// The '{0}' must not be null. 51 | /// 52 | internal static string FormatHttpMessageHandlerBuilder_PrimaryHandlerIsNull(object p0) 53 | => string.Format(CultureInfo.CurrentCulture, GetString("HttpMessageHandlerBuilder_PrimaryHandlerIsNull"), p0); 54 | 55 | /// 56 | /// The handler lifetime must be at least 1 second. 57 | /// 58 | internal static string HandlerLifetime_InvalidValue 59 | { 60 | get => GetString("HandlerLifetime_InvalidValue"); 61 | } 62 | 63 | /// 64 | /// The handler lifetime must be at least 1 second. 65 | /// 66 | internal static string FormatHandlerLifetime_InvalidValue() 67 | => GetString("HandlerLifetime_InvalidValue"); 68 | 69 | private static string GetString(string name, params string[] formatterNames) 70 | { 71 | var value = _resourceManager.GetString(name); 72 | 73 | System.Diagnostics.Debug.Assert(value != null); 74 | 75 | if (formatterNames != null) 76 | { 77 | for (var i = 0; i < formatterNames.Length; i++) 78 | { 79 | value = value.Replace("{" + formatterNames[i] + "}", "{" + i + "}"); 80 | } 81 | } 82 | 83 | return value; 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/Microsoft.Extensions.Http/Resources.resx: -------------------------------------------------------------------------------- 1 |  2 | 3 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | text/microsoft-resx 110 | 111 | 112 | 2.0 113 | 114 | 115 | System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 116 | 117 | 118 | System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 119 | 120 | 121 | The '{0}' must not contain a null entry. 122 | 123 | 124 | The '{0}' property must be null. '{1}' instances provided to '{2}' must not be reused or cached.{3}Handler: '{4}' 125 | 0 = nameof(DelegatingHandler.InnerHandler) 126 | 1 = nameof(DelegatingHandler) 127 | 2 = nameof(HttpMessageHandlerBuilder) 128 | 3 = Environment.NewLine 129 | 4 = handler.ToString() 130 | 131 | 132 | The '{0}' must not be null. 133 | 134 | 135 | The handler lifetime must be at least 1 second. 136 | 137 | -------------------------------------------------------------------------------- /test/Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | netcoreapp2.2 6 | $(DeveloperBuildTestTfms) 7 | $(StandardTestTfms) 8 | $(StandardTestTfms);net461 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /test/Microsoft.Extensions.Http.Polly.Test/DependencyInjection/PollyHttpClientBuilderExtensionsTest.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; 7 | using System.Net.Http; 8 | using System.Threading; 9 | using System.Threading.Tasks; 10 | using Microsoft.Extensions.Http; 11 | using Microsoft.Extensions.Http.Logging; 12 | using Polly; 13 | using Xunit; 14 | 15 | namespace Microsoft.Extensions.DependencyInjection 16 | { 17 | // These are integration tests that verify basic end-to-ends. 18 | public class PollyHttpClientBuilderExtensionsTest 19 | { 20 | public PollyHttpClientBuilderExtensionsTest() 21 | { 22 | PrimaryHandler = new FaultyMessageHandler(); 23 | 24 | NoOpPolicy = Policy.NoOpAsync(); 25 | RetryPolicy = Policy.Handle().OrResult(r => false).RetryAsync(); 26 | } 27 | 28 | private FaultyMessageHandler PrimaryHandler { get; } 29 | 30 | // Allows the exception from our handler to propegate 31 | private IAsyncPolicy NoOpPolicy { get; } 32 | 33 | // Matches what our client handler does 34 | private IAsyncPolicy RetryPolicy { get; } 35 | 36 | [Fact] 37 | public async Task AddPolicyHandler_Policy_AddsPolicyHandler() 38 | { 39 | var serviceCollection = new ServiceCollection(); 40 | 41 | HttpMessageHandlerBuilder builder = null; 42 | 43 | // Act1 44 | serviceCollection.AddHttpClient("example.com", c => c.BaseAddress = new Uri("http://example.com")) 45 | .AddPolicyHandler(RetryPolicy) 46 | .ConfigureHttpMessageHandlerBuilder(b => 47 | { 48 | b.PrimaryHandler = PrimaryHandler; 49 | builder = b; 50 | }); 51 | 52 | var services = serviceCollection.BuildServiceProvider(); 53 | var factory = services.GetRequiredService(); 54 | 55 | // Act2 56 | var client = factory.CreateClient("example.com"); 57 | 58 | // Assert 59 | Assert.NotNull(client); 60 | 61 | Assert.Collection( 62 | builder.AdditionalHandlers, 63 | h => Assert.IsType(h), 64 | h => Assert.IsType(h), 65 | h => Assert.IsType(h)); 66 | 67 | // Act 3 68 | var response = await client.SendAsync(new HttpRequestMessage()); 69 | 70 | // Assert 71 | Assert.Equal(HttpStatusCode.Created, response.StatusCode); 72 | } 73 | 74 | [Fact] 75 | public async Task AddPolicyHandler_PolicySelector_AddsPolicyHandler() 76 | { 77 | var serviceCollection = new ServiceCollection(); 78 | 79 | HttpMessageHandlerBuilder builder = null; 80 | 81 | // Act1 82 | serviceCollection.AddHttpClient("example.com", c => c.BaseAddress = new Uri("http://example.com")) 83 | .AddPolicyHandler((req) => req.RequestUri.AbsolutePath == "/" ? RetryPolicy : NoOpPolicy) 84 | .ConfigureHttpMessageHandlerBuilder(b => 85 | { 86 | b.PrimaryHandler = PrimaryHandler; 87 | builder = b; 88 | }); 89 | 90 | var services = serviceCollection.BuildServiceProvider(); 91 | var factory = services.GetRequiredService(); 92 | 93 | // Act2 94 | var client = factory.CreateClient("example.com"); 95 | 96 | // Assert 97 | Assert.NotNull(client); 98 | 99 | Assert.Collection( 100 | builder.AdditionalHandlers, 101 | h => Assert.IsType(h), 102 | h => Assert.IsType(h), 103 | h => Assert.IsType(h)); 104 | 105 | // Act 3 106 | var response = await client.SendAsync(new HttpRequestMessage()); 107 | 108 | // Assert 109 | Assert.Equal(HttpStatusCode.Created, response.StatusCode); 110 | 111 | // Act 4 112 | await Assert.ThrowsAsync(() => client.SendAsync(new HttpRequestMessage(HttpMethod.Get, "/throw"))); 113 | } 114 | 115 | [Fact] 116 | public async Task AddPolicyHandler_PolicySelectorWithServices_AddsPolicyHandler() 117 | { 118 | var serviceCollection = new ServiceCollection(); 119 | 120 | HttpMessageHandlerBuilder builder = null; 121 | 122 | // Act1 123 | serviceCollection.AddHttpClient("example.com", c => c.BaseAddress = new Uri("http://example.com")) 124 | .AddPolicyHandler((req) => req.RequestUri.AbsolutePath == "/" ? RetryPolicy : NoOpPolicy) 125 | .ConfigureHttpMessageHandlerBuilder(b => 126 | { 127 | b.PrimaryHandler = PrimaryHandler; 128 | builder = b; 129 | }); 130 | 131 | var services = serviceCollection.BuildServiceProvider(); 132 | var factory = services.GetRequiredService(); 133 | 134 | // Act2 135 | var client = factory.CreateClient("example.com"); 136 | 137 | // Assert 138 | Assert.NotNull(client); 139 | 140 | Assert.Collection( 141 | builder.AdditionalHandlers, 142 | h => Assert.IsType(h), 143 | h => Assert.IsType(h), 144 | h => Assert.IsType(h)); 145 | 146 | // Act 3 147 | var response = await client.SendAsync(new HttpRequestMessage()); 148 | 149 | // Assert 150 | Assert.Equal(HttpStatusCode.Created, response.StatusCode); 151 | 152 | // Act 4 153 | await Assert.ThrowsAsync(() => client.SendAsync(new HttpRequestMessage(HttpMethod.Get, "/throw"))); 154 | } 155 | 156 | [Fact] 157 | public async Task AddPolicyHandlerFromRegistry_Name_AddsPolicyHandler() 158 | { 159 | var serviceCollection = new ServiceCollection(); 160 | 161 | var registry = serviceCollection.AddPolicyRegistry(); 162 | registry.Add>("retry", RetryPolicy); 163 | 164 | HttpMessageHandlerBuilder builder = null; 165 | 166 | // Act1 167 | serviceCollection.AddHttpClient("example.com", c => c.BaseAddress = new Uri("http://example.com")) 168 | .AddPolicyHandlerFromRegistry("retry") 169 | .ConfigureHttpMessageHandlerBuilder(b => 170 | { 171 | b.PrimaryHandler = PrimaryHandler; 172 | 173 | builder = b; 174 | }); 175 | 176 | var services = serviceCollection.BuildServiceProvider(); 177 | var factory = services.GetRequiredService(); 178 | 179 | // Act2 180 | var client = factory.CreateClient("example.com"); 181 | 182 | // Assert 183 | Assert.NotNull(client); 184 | 185 | Assert.Collection( 186 | builder.AdditionalHandlers, 187 | h => Assert.IsType(h), 188 | h => Assert.IsType(h), 189 | h => Assert.IsType(h)); 190 | 191 | // Act 3 192 | var response = await client.SendAsync(new HttpRequestMessage()); 193 | 194 | // Assert 195 | Assert.Equal(HttpStatusCode.Created, response.StatusCode); 196 | } 197 | 198 | [Fact] 199 | public async Task AddPolicyHandlerFromRegistry_Dynamic_AddsPolicyHandler() 200 | { 201 | var serviceCollection = new ServiceCollection(); 202 | 203 | var registry = serviceCollection.AddPolicyRegistry(); 204 | registry.Add>("noop", NoOpPolicy); 205 | registry.Add>("retry", RetryPolicy); 206 | 207 | HttpMessageHandlerBuilder builder = null; 208 | 209 | // Act1 210 | serviceCollection.AddHttpClient("example.com", c => c.BaseAddress = new Uri("http://example.com")) 211 | .AddPolicyHandlerFromRegistry((reg, req) => 212 | { 213 | return req.RequestUri.AbsolutePath == "/" ? 214 | reg.Get>("retry") : 215 | reg.Get>("noop"); 216 | }) 217 | .ConfigureHttpMessageHandlerBuilder(b => 218 | { 219 | b.PrimaryHandler = PrimaryHandler; 220 | builder = b; 221 | }); 222 | 223 | var services = serviceCollection.BuildServiceProvider(); 224 | var factory = services.GetRequiredService(); 225 | 226 | // Act2 227 | var client = factory.CreateClient("example.com"); 228 | 229 | // Assert 230 | Assert.NotNull(client); 231 | 232 | Assert.Collection( 233 | builder.AdditionalHandlers, 234 | h => Assert.IsType(h), 235 | h => Assert.IsType(h), 236 | h => Assert.IsType(h)); 237 | 238 | // Act 3 239 | var response = await client.SendAsync(new HttpRequestMessage()); 240 | 241 | // Assert 242 | Assert.Equal(HttpStatusCode.Created, response.StatusCode); 243 | 244 | // Act 4 245 | await Assert.ThrowsAsync(() => client.SendAsync(new HttpRequestMessage(HttpMethod.Get, "/throw"))); 246 | } 247 | 248 | [Theory] 249 | [InlineData(HttpStatusCode.RequestTimeout)] 250 | [InlineData((HttpStatusCode)500)] 251 | [InlineData((HttpStatusCode)501)] 252 | [InlineData((HttpStatusCode)502)] 253 | [InlineData((HttpStatusCode)503)] 254 | public async Task AddTransientHttpErrorPolicy_AddsPolicyHandler_HandlesStatusCode(HttpStatusCode statusCode) 255 | { 256 | // Arrange 257 | var handler = new SequenceMessageHandler() 258 | { 259 | Responses = 260 | { 261 | (req) => new HttpResponseMessage(statusCode), 262 | (req) => new HttpResponseMessage(HttpStatusCode.OK), 263 | }, 264 | }; 265 | 266 | var serviceCollection = new ServiceCollection(); 267 | 268 | HttpMessageHandlerBuilder builder = null; 269 | 270 | // Act1 271 | serviceCollection.AddHttpClient("example.com", c => c.BaseAddress = new Uri("http://example.com")) 272 | .AddTransientHttpErrorPolicy(b => b.RetryAsync(5)) 273 | .ConfigureHttpMessageHandlerBuilder(b => 274 | { 275 | b.PrimaryHandler = handler; 276 | builder = b; 277 | }); 278 | 279 | var services = serviceCollection.BuildServiceProvider(); 280 | var factory = services.GetRequiredService(); 281 | 282 | // Act2 283 | var client = factory.CreateClient("example.com"); 284 | 285 | // Assert 286 | Assert.NotNull(client); 287 | 288 | Assert.Collection( 289 | builder.AdditionalHandlers, 290 | h => Assert.IsType(h), 291 | h => Assert.IsType(h), 292 | h => Assert.IsType(h)); 293 | 294 | // Act 3 295 | var response = await client.SendAsync(new HttpRequestMessage(HttpMethod.Get, "/")); 296 | 297 | // Assert 298 | Assert.Equal(HttpStatusCode.OK, response.StatusCode); 299 | } 300 | 301 | [Fact] 302 | public async Task AddTransientHttpErrorPolicy_AddsPolicyHandler_HandlesHttpRequestException() 303 | { 304 | // Arrange 305 | var handler = new SequenceMessageHandler() 306 | { 307 | Responses = 308 | { 309 | (req) => { throw new HttpRequestException("testing..."); }, 310 | (req) => new HttpResponseMessage(HttpStatusCode.OK), 311 | }, 312 | }; 313 | 314 | var serviceCollection = new ServiceCollection(); 315 | 316 | HttpMessageHandlerBuilder builder = null; 317 | 318 | // Act1 319 | serviceCollection.AddHttpClient("example.com", c => c.BaseAddress = new Uri("http://example.com")) 320 | .AddTransientHttpErrorPolicy(b => b.RetryAsync(5)) 321 | .ConfigureHttpMessageHandlerBuilder(b => 322 | { 323 | b.PrimaryHandler = handler; 324 | builder = b; 325 | }); 326 | 327 | var services = serviceCollection.BuildServiceProvider(); 328 | var factory = services.GetRequiredService(); 329 | 330 | // Act2 331 | var client = factory.CreateClient("example.com"); 332 | 333 | // Assert 334 | Assert.NotNull(client); 335 | 336 | Assert.Collection( 337 | builder.AdditionalHandlers, 338 | h => Assert.IsType(h), 339 | h => Assert.IsType(h), 340 | h => Assert.IsType(h)); 341 | 342 | // Act 3 343 | var response = await client.SendAsync(new HttpRequestMessage(HttpMethod.Get, "/")); 344 | 345 | // Assert 346 | Assert.Equal(HttpStatusCode.OK, response.StatusCode); 347 | } 348 | 349 | // Throws an exception or fails on even numbered requests, otherwise succeeds. 350 | private class FaultyMessageHandler : DelegatingHandler 351 | { 352 | public int CallCount { get; private set; } 353 | 354 | public Func CreateException { get; set; } = () => new OverflowException(); 355 | 356 | protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) 357 | { 358 | if (CallCount++ % 2 == 0) 359 | { 360 | throw CreateException(); 361 | } 362 | else 363 | { 364 | return Task.FromResult(new HttpResponseMessage(HttpStatusCode.Created)); 365 | } 366 | } 367 | } 368 | 369 | private class SequenceMessageHandler : DelegatingHandler 370 | { 371 | public int CallCount { get; private set; } 372 | 373 | public List> Responses { get; } = new List>(); 374 | 375 | protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) 376 | { 377 | var func = Responses[CallCount++ % Responses.Count]; 378 | return Task.FromResult(func(request)); 379 | } 380 | } 381 | } 382 | } 383 | -------------------------------------------------------------------------------- /test/Microsoft.Extensions.Http.Polly.Test/HttpRequestMessageExtensionsTest.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 Xunit; 7 | 8 | namespace Polly 9 | { 10 | public class HttpRequestMessageExtensionsTest 11 | { 12 | [Fact] 13 | public void GetPolicyExecutionContext_Found_SetsContext() 14 | { 15 | // Arrange 16 | var request = new HttpRequestMessage(); 17 | var expected = new Context(Guid.NewGuid().ToString()); 18 | request.Properties[HttpRequestMessageExtensions.PolicyExecutionContextKey] = expected; 19 | 20 | // Act 21 | var actual = request.GetPolicyExecutionContext(); 22 | 23 | // Assert 24 | Assert.Same(expected, actual); 25 | } 26 | 27 | [Fact] 28 | public void GetPolicyExecutionContext_NotFound_ReturnsNull() 29 | { 30 | // Arrange 31 | var request = new HttpRequestMessage(); 32 | 33 | // Act 34 | var actual = request.GetPolicyExecutionContext(); 35 | 36 | // Assert 37 | Assert.Null(actual); 38 | } 39 | 40 | [Fact] 41 | public void GetPolicyExecutionContext_Null_ReturnsNull() 42 | { 43 | // Arrange 44 | var request = new HttpRequestMessage(); 45 | request.Properties[HttpRequestMessageExtensions.PolicyExecutionContextKey] = null; 46 | 47 | // Act 48 | var actual = request.GetPolicyExecutionContext(); 49 | 50 | // Assert 51 | Assert.Null(actual); 52 | } 53 | 54 | [Fact] 55 | public void SetPolicyExecutionContext_WithValue_SetsContext() 56 | { 57 | // Arrange 58 | var request = new HttpRequestMessage(); 59 | var expected = new Context(Guid.NewGuid().ToString()); 60 | 61 | // Act 62 | request.SetPolicyExecutionContext(expected); 63 | 64 | // Assert 65 | var actual = request.Properties[HttpRequestMessageExtensions.PolicyExecutionContextKey]; 66 | Assert.Same(expected, actual); 67 | } 68 | 69 | [Fact] 70 | public void SetPolicyExecutionContext_WithNull_SetsNull() 71 | { 72 | // Arrange 73 | var request = new HttpRequestMessage(); 74 | request.Properties[HttpRequestMessageExtensions.PolicyExecutionContextKey] = new Context(Guid.NewGuid().ToString()); 75 | 76 | // Act 77 | request.SetPolicyExecutionContext(null); 78 | 79 | // Assert 80 | var actual = request.Properties[HttpRequestMessageExtensions.PolicyExecutionContextKey]; 81 | Assert.Null(actual); 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /test/Microsoft.Extensions.Http.Polly.Test/Microsoft.Extensions.Http.Polly.Test.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | $(StandardTestTfms) 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /test/Microsoft.Extensions.Http.Polly.Test/PolicyHttpMessageHandlerTest.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; 7 | using System.Threading.Tasks; 8 | using Polly; 9 | using Polly.Timeout; 10 | using Xunit; 11 | 12 | namespace Microsoft.Extensions.Http 13 | { 14 | public class PolicyHttpMessageHandlerTest 15 | { 16 | [Fact] 17 | public async Task SendAsync_StaticPolicy_PolicyTriggers_CanReexecuteSendAsync() 18 | { 19 | // Arrange 20 | var policy = Policy 21 | .Handle() 22 | .RetryAsync(retryCount: 5); 23 | 24 | var handler = new TestPolicyHttpMessageHandler(policy); 25 | 26 | var callCount = 0; 27 | var expected = new HttpResponseMessage(); 28 | handler.OnSendAsync = (req, c, ct) => 29 | { 30 | if (callCount == 0) 31 | { 32 | callCount++; 33 | throw new HttpRequestException(); 34 | } 35 | else if (callCount == 1) 36 | { 37 | callCount++; 38 | return expected; 39 | } 40 | else 41 | { 42 | throw new InvalidOperationException(); 43 | } 44 | }; 45 | 46 | // Act 47 | var response = await handler.SendAsync(new HttpRequestMessage(), CancellationToken.None); 48 | 49 | // Assert 50 | Assert.Equal(2, callCount); 51 | Assert.Same(expected, response); 52 | } 53 | 54 | [Fact] 55 | public async Task SendAsync_DynamicPolicy_PolicyTriggers_CanReexecuteSendAsync() 56 | { 57 | // Arrange 58 | var policy = Policy 59 | .Handle() 60 | .RetryAsync(retryCount: 5); 61 | 62 | var expectedRequest = new HttpRequestMessage(); 63 | 64 | HttpRequestMessage policySelectorRequest = null; 65 | var handler = new TestPolicyHttpMessageHandler((req) => 66 | { 67 | policySelectorRequest = req; 68 | return policy; 69 | }); 70 | 71 | var callCount = 0; 72 | var expected = new HttpResponseMessage(); 73 | handler.OnSendAsync = (req, c, ct) => 74 | { 75 | if (callCount == 0) 76 | { 77 | callCount++; 78 | throw new HttpRequestException(); 79 | } 80 | else if (callCount == 1) 81 | { 82 | callCount++; 83 | return expected; 84 | } 85 | else 86 | { 87 | throw new InvalidOperationException(); 88 | } 89 | }; 90 | 91 | // Act 92 | var response = await handler.SendAsync(expectedRequest, CancellationToken.None); 93 | 94 | // Assert 95 | Assert.Equal(2, callCount); 96 | Assert.Same(expected, response); 97 | Assert.Same(expectedRequest, policySelectorRequest); 98 | } 99 | 100 | [Fact] 101 | public async Task SendAsync_DynamicPolicy_PolicySelectorReturnsNull_ThrowsException() 102 | { 103 | // Arrange 104 | var handler = new TestPolicyHttpMessageHandler((req) => 105 | { 106 | return null; 107 | }); 108 | 109 | var expected = new HttpResponseMessage(); 110 | 111 | // Act 112 | var exception = await Assert.ThrowsAsync(async () => 113 | { 114 | await handler.SendAsync(new HttpRequestMessage(), CancellationToken.None); 115 | }); 116 | 117 | // Assert 118 | Assert.Equal( 119 | "The 'policySelector' function must return a non-null policy instance. To create a policy that takes no action, use 'Policy.NoOpAsync()'.", 120 | exception.Message); 121 | } 122 | 123 | [Fact] 124 | public async Task SendAsync_PolicyCancellation_CanTriggerRequestCancellation() 125 | { 126 | // Arrange 127 | var policy = Policy 128 | .Handle() // Handle timeouts by retrying 129 | .RetryAsync(retryCount: 5) 130 | .WrapAsync(Policy 131 | .TimeoutAsync(TimeSpan.FromMilliseconds(50)) // Apply a 50ms timeout 132 | .WrapAsync(Policy.NoOpAsync())); 133 | 134 | var handler = new TestPolicyHttpMessageHandler(policy); 135 | 136 | var @event = new ManualResetEventSlim(initialState: false); 137 | 138 | var callCount = 0; 139 | var expected = new HttpResponseMessage(); 140 | handler.OnSendAsync = (req, c, ct) => 141 | { 142 | // The inner cancellation token is created by Polly, it will trigger the timeout. 143 | Assert.True(ct.CanBeCanceled); 144 | if (callCount == 0) 145 | { 146 | callCount++; 147 | @event.Wait(ct); 148 | throw null; // unreachable, previous line should throw 149 | } 150 | else if (callCount == 1) 151 | { 152 | callCount++; 153 | return expected; 154 | } 155 | else 156 | { 157 | throw new InvalidOperationException(); 158 | } 159 | }; 160 | 161 | // Act 162 | var response = await handler.SendAsync(new HttpRequestMessage(), CancellationToken.None); 163 | 164 | // Assert 165 | Assert.Equal(2, callCount); 166 | Assert.Same(expected, response); 167 | } 168 | 169 | [Fact] 170 | public async Task SendAsync_NoContextSet_CreatesNewContext() 171 | { 172 | // Arrange 173 | var policy = Policy.TimeoutAsync(TimeSpan.FromSeconds(10)); 174 | var handler = new TestPolicyHttpMessageHandler(policy); 175 | 176 | Context context = null; 177 | var expected = new HttpResponseMessage(); 178 | handler.OnSendAsync = (req, c, ct) => 179 | { 180 | context = c; 181 | Assert.NotNull(context); 182 | Assert.Same(context, req.GetPolicyExecutionContext()); 183 | return expected; 184 | }; 185 | 186 | var request = new HttpRequestMessage(); 187 | 188 | // Act 189 | var response = await handler.SendAsync(request, CancellationToken.None); 190 | 191 | // Assert 192 | Assert.NotNull(context); 193 | Assert.Null(request.GetPolicyExecutionContext()); // We clean up the context if it was generated by the handler rather than caller supplied. 194 | Assert.Same(expected, response); 195 | } 196 | 197 | [Fact] 198 | public async Task SendAsync_ExistingContext_ReusesContext() 199 | { 200 | // Arrange 201 | var policy = Policy.TimeoutAsync(TimeSpan.FromSeconds(10)); 202 | var handler = new TestPolicyHttpMessageHandler(policy); 203 | 204 | var expected = new HttpResponseMessage(); 205 | var expectedContext = new Context(Guid.NewGuid().ToString()); 206 | 207 | Context context = null; 208 | handler.OnSendAsync = (req, c, ct) => 209 | { 210 | context = c; 211 | Assert.NotNull(c); 212 | Assert.Same(c, req.GetPolicyExecutionContext()); 213 | return expected; 214 | }; 215 | 216 | var request = new HttpRequestMessage(); 217 | request.SetPolicyExecutionContext(expectedContext); 218 | 219 | // Act 220 | var response = await handler.SendAsync(request, CancellationToken.None); 221 | 222 | // Assert 223 | Assert.Same(expectedContext, context); 224 | Assert.Same(expectedContext, request.GetPolicyExecutionContext()); // We don't clean up the context if the caller or earlier delegating handlers had supplied it. 225 | Assert.Same(expected, response); 226 | } 227 | 228 | [Fact] 229 | public async Task SendAsync_NoContextSet_DynamicPolicySelectorThrows_CleansUpContext() 230 | { 231 | // Arrange 232 | var handler = new TestPolicyHttpMessageHandler((req) => 233 | { 234 | throw new InvalidOperationException(); 235 | }); 236 | 237 | var request = new HttpRequestMessage(); 238 | 239 | // Act 240 | var exception = await Assert.ThrowsAsync(async () => 241 | { 242 | await handler.SendAsync(request, CancellationToken.None); 243 | }); 244 | 245 | // Assert 246 | Assert.Null(request.GetPolicyExecutionContext()); // We do clean up a context we generated, when the policy selector throws. 247 | } 248 | 249 | [Fact] 250 | public async Task SendAsync_NoContextSet_RequestThrows_CleansUpContext() 251 | { 252 | // Arrange 253 | var policy = Policy.TimeoutAsync(TimeSpan.FromSeconds(10)); 254 | var handler = new TestPolicyHttpMessageHandler(policy); 255 | 256 | Context context = null; 257 | handler.OnSendAsync = (req, c, ct) => 258 | { 259 | context = c; 260 | throw new OperationCanceledException(); 261 | }; 262 | 263 | var request = new HttpRequestMessage(); 264 | 265 | // Act 266 | var exception = await Assert.ThrowsAsync(async () => 267 | { 268 | await handler.SendAsync(request, CancellationToken.None); 269 | }); 270 | 271 | // Assert 272 | Assert.NotNull(context); // The handler did generate a context for the execution. 273 | Assert.Null(request.GetPolicyExecutionContext()); // We do clean up a context we generated, when the execution throws. 274 | } 275 | 276 | private class TestPolicyHttpMessageHandler : PolicyHttpMessageHandler 277 | { 278 | public Func OnSendAsync { get; set; } 279 | 280 | public TestPolicyHttpMessageHandler(IAsyncPolicy policy) 281 | : base(policy) 282 | { 283 | } 284 | 285 | public TestPolicyHttpMessageHandler(Func> policySelector) 286 | : base(policySelector) 287 | { 288 | } 289 | 290 | public new Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) 291 | { 292 | return base.SendAsync(request, cancellationToken); 293 | } 294 | 295 | protected override Task SendCoreAsync(HttpRequestMessage request, Context context, CancellationToken cancellationToken) 296 | { 297 | Assert.NotNull(OnSendAsync); 298 | return Task.FromResult(OnSendAsync(request, context, cancellationToken)); 299 | } 300 | } 301 | } 302 | } 303 | -------------------------------------------------------------------------------- /test/Microsoft.Extensions.Http.Test/DefaultHttpMessageHandlerBuilderTest.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 Microsoft.Extensions.DependencyInjection; 7 | using Moq; 8 | using Xunit; 9 | 10 | namespace Microsoft.Extensions.Http 11 | { 12 | public class DefaultHttpMessageHandlerBuilderTest 13 | { 14 | public DefaultHttpMessageHandlerBuilderTest() 15 | { 16 | Services = new ServiceCollection().BuildServiceProvider(); 17 | } 18 | 19 | public IServiceProvider Services { get; } 20 | 21 | // Testing this because it's an important design detail. If someone wants to globally replace the handler 22 | // they can do so by replacing this service. It's important that the Factory isn't the one to instantiate 23 | // the handler. The factory has no defaults - it only applies options. 24 | [Fact] 25 | public void Ctor_SetsPrimaryHandler() 26 | { 27 | // Arrange & Act 28 | var builder = new DefaultHttpMessageHandlerBuilder(Services); 29 | 30 | // Act 31 | Assert.IsType(builder.PrimaryHandler); 32 | } 33 | 34 | 35 | [Fact] 36 | public void Build_NoAdditionalHandlers_ReturnsPrimaryHandler() 37 | { 38 | // Arrange 39 | var builder = new DefaultHttpMessageHandlerBuilder(Services) 40 | { 41 | PrimaryHandler = Mock.Of(), 42 | }; 43 | 44 | // Act 45 | var handler = builder.Build(); 46 | 47 | // Assert 48 | Assert.Same(builder.PrimaryHandler, handler); 49 | } 50 | 51 | [Fact] 52 | public void Build_SomeAdditionalHandlers_PutsTogetherDelegatingHandlers() 53 | { 54 | // Arrange 55 | var builder = new DefaultHttpMessageHandlerBuilder(Services) 56 | { 57 | PrimaryHandler = Mock.Of(), 58 | AdditionalHandlers = 59 | { 60 | Mock.Of(), // Outer 61 | Mock.Of(), // Middle 62 | } 63 | }; 64 | 65 | // Act 66 | var handler = builder.Build(); 67 | 68 | // Assert 69 | Assert.Same(builder.AdditionalHandlers[0], handler); 70 | 71 | handler = Assert.IsAssignableFrom(handler).InnerHandler; 72 | Assert.Same(builder.AdditionalHandlers[1], handler); 73 | 74 | handler = Assert.IsAssignableFrom(handler).InnerHandler; 75 | Assert.Same(builder.PrimaryHandler, handler); 76 | } 77 | 78 | [Fact] 79 | public void Build_PrimaryHandlerIsNull_ThrowsException() 80 | { 81 | // Arrange 82 | var builder = new DefaultHttpMessageHandlerBuilder(Services) 83 | { 84 | PrimaryHandler = null, 85 | }; 86 | 87 | // Act & Assert 88 | var exception = Assert.Throws(() => builder.Build()); 89 | Assert.Equal("The 'PrimaryHandler' must not be null.", exception.Message); 90 | } 91 | 92 | [Fact] 93 | public void Build_AdditionalHandlerIsNull_ThrowsException() 94 | { 95 | // Arrange 96 | var builder = new DefaultHttpMessageHandlerBuilder(Services) 97 | { 98 | AdditionalHandlers = 99 | { 100 | null, 101 | } 102 | }; 103 | 104 | // Act & Assert 105 | var exception = Assert.Throws(() => builder.Build()); 106 | Assert.Equal("The 'additionalHandlers' must not contain a null entry.", exception.Message); 107 | } 108 | 109 | [Fact] 110 | public void Build_AdditionalHandlerHasNonNullInnerHandler_ThrowsException() 111 | { 112 | // Arrange 113 | var builder = new DefaultHttpMessageHandlerBuilder(Services) 114 | { 115 | AdditionalHandlers = 116 | { 117 | Mock.Of(h => h.InnerHandler == Mock.Of()), 118 | } 119 | }; 120 | 121 | // Act & Assert 122 | var exception = Assert.Throws(() => builder.Build()); 123 | Assert.Equal( 124 | "The 'InnerHandler' property must be null. " + 125 | "'DelegatingHandler' instances provided to 'HttpMessageHandlerBuilder' must not be reused or cached." + Environment.NewLine + 126 | $"Handler: '{builder.AdditionalHandlers[0].ToString()}'", 127 | exception.Message); 128 | } 129 | } 130 | } -------------------------------------------------------------------------------- /test/Microsoft.Extensions.Http.Test/DependencyInjection/OtherTestOptions.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 | 6 | namespace Microsoft.Extensions.DependencyInjection 7 | { 8 | public class OtherTestOptions 9 | { 10 | public string BaseAddress { get; set; } 11 | } 12 | } -------------------------------------------------------------------------------- /test/Microsoft.Extensions.Http.Test/HttpMessageHandlerBuilderTest.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 Moq; 7 | using Xunit; 8 | 9 | namespace Microsoft.Extensions.Http.Test 10 | { 11 | public class HttpMessageHandlerBuilderTest 12 | { 13 | [Fact] 14 | public void Build_AdditionalHandlerIsNull_ThrowsException() 15 | { 16 | // Arrange 17 | var primaryHandler = Mock.Of(); 18 | var additionalHandlers = new DelegatingHandler[] 19 | { 20 | null, 21 | }; 22 | 23 | // Act & Assert 24 | var exception = Assert.Throws(() => 25 | { 26 | HttpMessageHandlerBuilder.CreateHandlerPipeline(primaryHandler, additionalHandlers); 27 | }); 28 | Assert.Equal("The 'additionalHandlers' must not contain a null entry.", exception.Message); 29 | } 30 | 31 | [Fact] 32 | public void Build_AdditionalHandlerHasNonNullInnerHandler_ThrowsException() 33 | { 34 | // Arrange 35 | var primaryHandler = Mock.Of(); 36 | var additionalHandlers = new DelegatingHandler[] 37 | { 38 | Mock.Of(h => h.InnerHandler == Mock.Of()), 39 | }; 40 | 41 | // Act & Assert 42 | var exception = Assert.Throws(() => 43 | { 44 | HttpMessageHandlerBuilder.CreateHandlerPipeline(primaryHandler, additionalHandlers); 45 | }); 46 | Assert.Equal( 47 | "The 'InnerHandler' property must be null. " + 48 | "'DelegatingHandler' instances provided to 'HttpMessageHandlerBuilder' must not be reused or cached." + Environment.NewLine + 49 | $"Handler: '{additionalHandlers[0].ToString()}'", 50 | exception.Message); 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /test/Microsoft.Extensions.Http.Test/ITestTypedClient.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.Net.Http; 5 | 6 | namespace Microsoft.Extensions.Http 7 | { 8 | // Simple typed client for use in tests 9 | public interface ITestTypedClient 10 | { 11 | HttpClient HttpClient { get; } 12 | } 13 | } -------------------------------------------------------------------------------- /test/Microsoft.Extensions.Http.Test/Microsoft.Extensions.Http.Test.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | $(StandardTestTfms) 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /test/Microsoft.Extensions.Http.Test/TestTypedClient.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.Net.Http; 5 | 6 | namespace Microsoft.Extensions.Http 7 | { 8 | // Simple typed client for use in tests 9 | public class TestTypedClient : ITestTypedClient 10 | { 11 | public TestTypedClient(HttpClient httpClient) 12 | { 13 | HttpClient = httpClient; 14 | } 15 | 16 | public HttpClient HttpClient { get; } 17 | } 18 | } -------------------------------------------------------------------------------- /version.props: -------------------------------------------------------------------------------- 1 |  2 | 3 | 3.0.0 4 | alpha1 5 | $(VersionPrefix) 6 | $(VersionPrefix)-$(VersionSuffix)-final 7 | t000 8 | a- 9 | $(FeatureBranchVersionPrefix)$(VersionSuffix)-$([System.Text.RegularExpressions.Regex]::Replace('$(FeatureBranchVersionSuffix)', '[^\w-]', '-')) 10 | $(VersionSuffix)-$(BuildNumber) 11 | 12 | 13 | --------------------------------------------------------------------------------