├── src ├── Microsoft.Extensions.Http │ ├── baseline.netcore.json │ ├── DependencyInjection │ │ ├── DefaultHttpClientBuilder.cs │ │ ├── IHttpClientBuilder.cs │ │ ├── HttpClientBuilderExtensions.cs │ │ └── HttpClientFactoryServiceCollectionExtensions.cs │ ├── Microsoft.Extensions.Http.csproj │ ├── Logging │ │ ├── EventIds.cs │ │ ├── LoggingHttpMessageHandlerBuilderFilter.cs │ │ ├── LoggingHttpMessageHandler.cs │ │ └── LoggingScopeHttpMessageHandler.cs │ ├── Properties │ │ ├── AssemblyInfo.cs │ │ └── Resources.Designer.cs │ ├── IHttpMessageHandlerBuilderFilter.cs │ ├── HttpClientFactoryOptions.cs │ ├── HttpClientFactoryExtensions.cs │ ├── DefaultHttpMessageHandlerBuilder.cs │ ├── IHttpClientFactory.cs │ ├── HttpMessageHandlerBuilder.cs │ ├── DefaultHttpClientFactory.cs │ └── Resources.resx └── Directory.Build.props ├── korebuild-lock.txt ├── NuGetPackageVerifier.json ├── korebuild.json ├── CONTRIBUTING.md ├── Directory.Build.targets ├── run.cmd ├── .appveyor.yml ├── NuGet.config ├── README.md ├── Directory.Build.props ├── LICENSE.txt ├── version.props ├── test ├── Microsoft.Extensions.Http.Test │ ├── Microsoft.Extensions.Http.Test.csproj │ ├── HttpMessageHandlerBuilderTest.cs │ ├── DependencyInjection │ │ └── HttpClientFactoryServiceCollectionExtensionsTest.cs │ ├── DefaultHttpMessageHandlerBuilderTest.cs │ └── DefaultHttpClientFactoryTest.cs └── Directory.Build.props ├── .travis.yml ├── .gitignore ├── .gitattributes ├── samples └── HttpClientFactorySample │ ├── HttpClientFactorySample.csproj │ └── Program.cs ├── HttpClientFactory.sln ├── run.ps1 └── run.sh /src/Microsoft.Extensions.Http/baseline.netcore.json: -------------------------------------------------------------------------------- 1 | { 2 | 3 | } -------------------------------------------------------------------------------- /korebuild-lock.txt: -------------------------------------------------------------------------------- 1 | version:2.1.0-preview1-15549 2 | commithash:f570e08585fec510dd60cd4bfe8795388b757a95 3 | -------------------------------------------------------------------------------- /NuGetPackageVerifier.json: -------------------------------------------------------------------------------- 1 | { 2 | "Default": { 3 | "rules": [ 4 | "DefaultCompositeRule" 5 | ] 6 | } 7 | } -------------------------------------------------------------------------------- /korebuild.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/aspnet/BuildTools/dev/tools/korebuild.schema.json", 3 | "channel": "dev" 4 | } 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Contributing 2 | ====== 3 | 4 | Information on contributing to this repo is in the [Contributing Guide](https://github.com/aspnet/Home/blob/dev/CONTRIBUTING.md) in the Home repo. 5 | -------------------------------------------------------------------------------- /Directory.Build.targets: -------------------------------------------------------------------------------- 1 | 2 | 3 | $(MicrosoftNETCoreApp20PackageVersion) 4 | 5 | 6 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /src/Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.appveyor.yml: -------------------------------------------------------------------------------- 1 | init: 2 | - git config --global core.autocrlf true 3 | branches: 4 | only: 5 | - master 6 | - release 7 | - dev 8 | - /^(.*\/)?ci-.*$/ 9 | build_script: 10 | - ps: .\run.ps1 default-build 11 | clone_depth: 1 12 | environment: 13 | global: 14 | DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true 15 | DOTNET_CLI_TELEMETRY_OPTOUT: 1 16 | test: off 17 | deploy: off 18 | 19 | os: Visual Studio 2017 20 | -------------------------------------------------------------------------------- /NuGet.config: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | HttpClient Factory 2 | === 3 | 4 | AppVeyor: [![AppVeyor](https://ci.appveyor.com/api/projects/status/github/aspnet/HttpClientFactory?branch=dev&svg=true)](https://ci.appveyor.com/project/aspnetci/HttpClientFactory/branch/dev) 5 | 6 | Travis: [![Travis](https://travis-ci.org/aspnet/HttpClientFactory.svg?branch=dev)](https://travis-ci.org/aspnet/HttpClientFactory) 7 | 8 | Contains an opinionated factory for creating HttpClient instances. 9 | 10 | This project is part of ASP.NET Core. You can find samples, documentation and getting started instructions for ASP.NET Core at the [Home](https://github.com/aspnet/home) repo. 11 | -------------------------------------------------------------------------------- /Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Microsoft ASP.NET Core 7 | https://github.com/aspnet/HttpClientFactory 8 | git 9 | $(MSBuildThisFileDirectory)build\Key.snk 10 | true 11 | true 12 | true 13 | 14 | 15 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) .NET Foundation and Contributors 2 | 3 | All rights reserved. 4 | 5 | Licensed under the Apache License, Version 2.0 (the "License"); you may not use 6 | this file except in compliance with the License. You may obtain a copy of the 7 | License at 8 | 9 | http://www.apache.org/licenses/LICENSE-2.0 10 | 11 | Unless required by applicable law or agreed to in writing, software distributed 12 | under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR 13 | CONDITIONS OF ANY KIND, either express or implied. See the License for the 14 | specific language governing permissions and limitations under the License. 15 | -------------------------------------------------------------------------------- /version.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 2.1.0 4 | preview1 5 | $(VersionPrefix) 6 | $(VersionPrefix)-$(VersionSuffix)-final 7 | t000 8 | $(VersionSuffix)-$(BuildNumber) 9 | 10 | 11 | -------------------------------------------------------------------------------- /test/Microsoft.Extensions.Http.Test/Microsoft.Extensions.Http.Test.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp2.0;net461 5 | netcoreapp2.0 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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 | - dev 22 | - /^(.*\/)?ci-.*$/ 23 | before_install: 24 | - if test "$TRAVIS_OS_NAME" == "osx"; then brew update; brew install openssl; ln -s /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 /usr/local/lib/; fi 25 | script: 26 | - ./build.sh 27 | -------------------------------------------------------------------------------- /.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 | .testPublish/ 14 | bower_components/ 15 | node_modules/ 16 | **/wwwroot/lib/ 17 | debugSettings.json 18 | project.lock.json 19 | *.user 20 | *.suo 21 | *.cache 22 | *.docstates 23 | _ReSharper.* 24 | nuget.exe 25 | *net45.csproj 26 | *net451.csproj 27 | *k10.csproj 28 | *.psess 29 | *.vsp 30 | *.pidb 31 | *.userprefs 32 | *DS_Store 33 | *.ncrunchsolution 34 | *.*sdf 35 | *.ipch 36 | .settings 37 | *.sln.ide 38 | node_modules 39 | **/[Cc]ompiler/[Rr]esources/**/*.js 40 | *launchSettings.json 41 | global.json 42 | BenchmarkDotNet.Artifacts/ 43 | -------------------------------------------------------------------------------- /test/Directory.Build.props: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /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/Microsoft.Extensions.Http.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Provides API for creating HttpClient instances. 5 | netstandard2.0 6 | $(NoWarn);CS1591 7 | true 8 | aspnetcore;httpclient 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/Microsoft.Extensions.Http/Logging/EventIds.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.Logging; 5 | 6 | namespace Microsoft.Extensions.Http.Logging 7 | { 8 | internal static class EventIds 9 | { 10 | // Logging done by LoggingScopeHttpMessageHandler - this surrounds the whole pipeline 11 | public static readonly EventId RequestPipelineStart = new EventId(100, "RequestPipelineStart"); 12 | public static readonly EventId RequestPipelineEnd = new EventId(101, "RequestPipelineEnd"); 13 | 14 | // Logging done by LoggingHttpMessageHandler - this surrounds the actual HTTP request/response 15 | public static readonly EventId RequestStart = new EventId(102, "RequestStart"); 16 | public static readonly EventId RequestEnd = new EventId(103, "RequestEnd"); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /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/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 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /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 | 8 | namespace Microsoft.Extensions.Http 9 | { 10 | /// 11 | /// An options class for configuring the default . 12 | /// 13 | public class HttpClientFactoryOptions 14 | { 15 | /// 16 | /// Gets a list of operations used to configure an . 17 | /// 18 | public IList> HttpMessageHandlerBuilderActions { get; } = new List>(); 19 | 20 | /// 21 | /// Gets a list of operations used to configure an . 22 | /// 23 | public IList> HttpClientActions { get; } = new List>(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /samples/HttpClientFactorySample/HttpClientFactorySample.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp2.0;net461 5 | portable 6 | Exe 7 | 8 | 9 | $(PackageTargetFallback);portable-net451+win8 10 | true 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /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.Linq; 7 | using System.Net.Http; 8 | 9 | namespace Microsoft.Extensions.Http 10 | { 11 | internal class DefaultHttpMessageHandlerBuilder : HttpMessageHandlerBuilder 12 | { 13 | public override HttpMessageHandler PrimaryHandler { get; set; } = new HttpClientHandler(); 14 | 15 | public override IList AdditionalHandlers { get; } = new List(); 16 | 17 | public override HttpMessageHandler Build() 18 | { 19 | if (PrimaryHandler == null) 20 | { 21 | var message = Resources.FormatHttpMessageHandlerBuilder_PrimaryHandlerIsNull(nameof(PrimaryHandler)); 22 | throw new InvalidOperationException(message); 23 | } 24 | 25 | return CreateHandlerPipeline(PrimaryHandler, AdditionalHandlers); 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /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 System.Net.Http; 6 | using Microsoft.Extensions.Http.Logging; 7 | using Microsoft.Extensions.Logging; 8 | 9 | namespace Microsoft.Extensions.Http 10 | { 11 | // Internal so we can change the requirements without breaking changes. 12 | internal class LoggingHttpMessageHandlerBuilderFilter : IHttpMessageHandlerBuilderFilter 13 | { 14 | private readonly ILoggerFactory _loggerFactory; 15 | 16 | public LoggingHttpMessageHandlerBuilderFilter(ILoggerFactory loggerFactory) 17 | { 18 | if (loggerFactory == null) 19 | { 20 | throw new ArgumentNullException(nameof(loggerFactory)); 21 | } 22 | 23 | _loggerFactory = loggerFactory; 24 | } 25 | 26 | public Action Configure(Action next) 27 | { 28 | if (next == null) 29 | { 30 | throw new ArgumentNullException(nameof(next)); 31 | } 32 | 33 | return (builder) => 34 | { 35 | // Run other configuration first, we want to decorate. 36 | next(builder); 37 | 38 | // We want all of our logging message to show up as-if they are coming from HttpClient 39 | var logger = _loggerFactory.CreateLogger(); 40 | 41 | // The 'scope' handler goes first so it can surround everything. 42 | builder.AdditionalHandlers.Insert(0, new LoggingScopeHttpMessageHandler(logger)); 43 | 44 | // We want this handler to be last so we can log details about the request after 45 | // service discovery and security happen. 46 | builder.AdditionalHandlers.Add(new LoggingHttpMessageHandler(logger)); 47 | 48 | }; 49 | } 50 | } 51 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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.Net; 6 | using System.Net.Http; 7 | using System.Threading; 8 | using System.Threading.Tasks; 9 | using Microsoft.Extensions.Logging; 10 | 11 | namespace Microsoft.Extensions.Http.Logging 12 | { 13 | public class LoggingHttpMessageHandler : DelegatingHandler 14 | { 15 | private ILogger _logger; 16 | 17 | public LoggingHttpMessageHandler(ILogger logger) 18 | { 19 | if (logger == null) 20 | { 21 | throw new ArgumentNullException(nameof(logger)); 22 | } 23 | 24 | _logger = logger; 25 | } 26 | 27 | protected async override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) 28 | { 29 | if (request == null) 30 | { 31 | throw new ArgumentNullException(nameof(request)); 32 | } 33 | 34 | // Not using a scope here because we always expect this to be at the end of the pipeline, thus there's 35 | // not really anything to surround. 36 | Log.RequestStart(_logger, request); 37 | var response = await base.SendAsync(request, cancellationToken); 38 | Log.RequestEnd(_logger, response); 39 | 40 | return response; 41 | } 42 | 43 | private static class Log 44 | { 45 | private static readonly Action _requestStart = LoggerMessage.Define(LogLevel.Information, EventIds.RequestStart, "Sending HTTP request {HttpMethod} {Uri}"); 46 | private static readonly Action _requestEnd = LoggerMessage.Define(LogLevel.Information, EventIds.RequestEnd, "Recieved HTTP response {HttpMethod} {Uri} - {StatusCode}"); 47 | 48 | public static void RequestStart(ILogger logger, HttpRequestMessage request) 49 | { 50 | _requestStart(logger, request.Method, request.RequestUri, null); 51 | } 52 | 53 | public static void RequestEnd(ILogger logger, HttpResponseMessage response) 54 | { 55 | _requestEnd(logger, response.RequestMessage.Method, response.RequestMessage.RequestUri, response.StatusCode, null); 56 | } 57 | } 58 | } 59 | } -------------------------------------------------------------------------------- /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.Net; 6 | using System.Net.Http; 7 | using System.Threading; 8 | using System.Threading.Tasks; 9 | using Microsoft.Extensions.Logging; 10 | 11 | namespace Microsoft.Extensions.Http.Logging 12 | { 13 | public class LoggingScopeHttpMessageHandler : DelegatingHandler 14 | { 15 | private ILogger _logger; 16 | 17 | public LoggingScopeHttpMessageHandler(ILogger logger) 18 | { 19 | if (logger == null) 20 | { 21 | throw new ArgumentNullException(nameof(logger)); 22 | } 23 | 24 | _logger = logger; 25 | } 26 | 27 | protected async override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) 28 | { 29 | if (request == null) 30 | { 31 | throw new ArgumentNullException(nameof(request)); 32 | } 33 | 34 | using (Log.BeginRequestPipelineScope(_logger, request)) 35 | { 36 | Log.RequestPipelineStart(_logger, request); 37 | var response = await base.SendAsync(request, cancellationToken); 38 | Log.RequestPipelineEnd(_logger, response); 39 | 40 | return response; 41 | } 42 | } 43 | 44 | private static class Log 45 | { 46 | private static readonly Func _beginRequestPipelineScope = LoggerMessage.DefineScope("HTTP {HttpMethod} {Uri}"); 47 | private static readonly Action _requestPipelineStart = LoggerMessage.Define(LogLevel.Information, EventIds.RequestPipelineStart, "Start processing HTTP request {HttpMethod} {Uri}"); 48 | private static readonly Action _requestPipelineEnd = LoggerMessage.Define(LogLevel.Information, EventIds.RequestPipelineEnd, "End processing HTTP request {HttpMethod} {Uri} - {StatusCode}"); 49 | 50 | public static IDisposable BeginRequestPipelineScope(ILogger logger, HttpRequestMessage request) 51 | { 52 | return _beginRequestPipelineScope(logger, request.Method, request.RequestUri); 53 | } 54 | 55 | public static void RequestPipelineStart(ILogger logger, HttpRequestMessage request) 56 | { 57 | _requestPipelineStart(logger, request.Method, request.RequestUri, null); 58 | } 59 | 60 | public static void RequestPipelineEnd(ILogger logger, HttpResponseMessage response) 61 | { 62 | _requestPipelineEnd(logger, response.RequestMessage.Method, response.RequestMessage.RequestUri, response.StatusCode, null); 63 | } 64 | } 65 | } 66 | } -------------------------------------------------------------------------------- /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 | private static string GetString(string name, params string[] formatterNames) 56 | { 57 | var value = _resourceManager.GetString(name); 58 | 59 | System.Diagnostics.Debug.Assert(value != null); 60 | 61 | if (formatterNames != null) 62 | { 63 | for (var i = 0; i < formatterNames.Length; i++) 64 | { 65 | value = value.Replace("{" + formatterNames[i] + "}", "{" + i + "}"); 66 | } 67 | } 68 | 69 | return value; 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /HttpClientFactory.sln: -------------------------------------------------------------------------------- 1 | 2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio 15 4 | VisualStudioVersion = 15.0.27004.2008 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}") = "build", "build", "{6A3B322A-47A4-48F9-B11B-471B1B591D0B}" 19 | ProjectSection(SolutionItems) = preProject 20 | build\dependencies.props = build\dependencies.props 21 | build\repo.props = build\repo.props 22 | EndProjectSection 23 | EndProject 24 | Global 25 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 26 | Debug|Any CPU = Debug|Any CPU 27 | Release|Any CPU = Release|Any CPU 28 | EndGlobalSection 29 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 30 | {99E3492F-87BB-4506-8D2B-D1C7101655DC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 31 | {99E3492F-87BB-4506-8D2B-D1C7101655DC}.Debug|Any CPU.Build.0 = Debug|Any CPU 32 | {99E3492F-87BB-4506-8D2B-D1C7101655DC}.Release|Any CPU.ActiveCfg = Release|Any CPU 33 | {99E3492F-87BB-4506-8D2B-D1C7101655DC}.Release|Any CPU.Build.0 = Release|Any CPU 34 | {752F6163-AE48-46D3-8A6D-695BBF3629CB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 35 | {752F6163-AE48-46D3-8A6D-695BBF3629CB}.Debug|Any CPU.Build.0 = Debug|Any CPU 36 | {752F6163-AE48-46D3-8A6D-695BBF3629CB}.Release|Any CPU.ActiveCfg = Release|Any CPU 37 | {752F6163-AE48-46D3-8A6D-695BBF3629CB}.Release|Any CPU.Build.0 = Release|Any CPU 38 | {42C81623-6316-4C15-9E41-84AF48608C47}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 39 | {42C81623-6316-4C15-9E41-84AF48608C47}.Debug|Any CPU.Build.0 = Debug|Any CPU 40 | {42C81623-6316-4C15-9E41-84AF48608C47}.Release|Any CPU.ActiveCfg = Release|Any CPU 41 | {42C81623-6316-4C15-9E41-84AF48608C47}.Release|Any CPU.Build.0 = Release|Any CPU 42 | EndGlobalSection 43 | GlobalSection(SolutionProperties) = preSolution 44 | HideSolutionNode = FALSE 45 | EndGlobalSection 46 | GlobalSection(NestedProjects) = preSolution 47 | {99E3492F-87BB-4506-8D2B-D1C7101655DC} = {0ACA47C2-6B67-46B8-A661-C564E4450DE4} 48 | {752F6163-AE48-46D3-8A6D-695BBF3629CB} = {A7C2238C-5C0F-4D33-BE66-4015985CE962} 49 | {42C81623-6316-4C15-9E41-84AF48608C47} = {FF6B150F-C423-41BB-9563-55A0DFEAE21C} 50 | EndGlobalSection 51 | GlobalSection(ExtensibilityGlobals) = postSolution 52 | SolutionGuid = {002C7A25-D737-4B87-AFBB-B6E0FB2DB0D3} 53 | EndGlobalSection 54 | EndGlobal 55 | -------------------------------------------------------------------------------- /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 | 12 | namespace HttpClientFactorySample 13 | { 14 | public class Program 15 | { 16 | public static void Main(string[] args) => Run().GetAwaiter().GetResult(); 17 | 18 | public static async Task Run() 19 | { 20 | var serviceCollection = new ServiceCollection(); 21 | serviceCollection.AddLogging(b => 22 | { 23 | b.AddFilter((category, level) => true); // Spam the world with logs. 24 | 25 | // Add console logger so we can see all the logging produced by the client by default. 26 | b.AddConsole(c => c.IncludeScopes = true); 27 | }); 28 | 29 | Configure(serviceCollection); 30 | 31 | var services = serviceCollection.BuildServiceProvider(); 32 | 33 | var factory = services.GetRequiredService(); 34 | 35 | Console.WriteLine("Creating an HttpClient"); 36 | var client = factory.CreateClient("github"); 37 | 38 | Console.WriteLine("Creating and sending a request"); 39 | var request = new HttpRequestMessage(HttpMethod.Get, "/"); 40 | 41 | var response = await client.SendAsync(request).ConfigureAwait(false); 42 | response.EnsureSuccessStatusCode(); 43 | 44 | var data = await response.Content.ReadAsAsync(); 45 | Console.WriteLine("Response data:"); 46 | Console.WriteLine(data); 47 | 48 | Console.WriteLine("Press the ANY key to exit..."); 49 | Console.ReadKey(); 50 | } 51 | 52 | public static void Configure(IServiceCollection services) 53 | { 54 | services.AddHttpClient("github", c => 55 | { 56 | c.BaseAddress = new Uri("https://api.github.com/"); 57 | 58 | c.DefaultRequestHeaders.Add("Accept", "application/vnd.github.v3+json"); // Github API versioning 59 | c.DefaultRequestHeaders.Add("User-Agent", "HttpClientFactory-Sample"); // Github requires a user-agent 60 | }) 61 | .AddHttpMessageHandler(() => new RetryHandler()); // Retry requests to github using our retry handler 62 | } 63 | 64 | private class RetryHandler : DelegatingHandler 65 | { 66 | public int RetryCount { get; set; } = 5; 67 | 68 | protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) 69 | { 70 | for (var i = 0; i < RetryCount; i++) 71 | { 72 | try 73 | { 74 | return base.SendAsync(request, cancellationToken); 75 | } 76 | catch (HttpRequestException) when (i == RetryCount - 1) 77 | { 78 | throw; 79 | } 80 | catch (HttpRequestException) 81 | { 82 | // Retry 83 | Task.Delay(TimeSpan.FromMilliseconds(50)); 84 | } 85 | } 86 | 87 | // Unreachable. 88 | throw null; 89 | } 90 | } 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /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 primary . 23 | /// 24 | public abstract HttpMessageHandler PrimaryHandler { get; set; } 25 | 26 | /// 27 | /// Gets a list of additional instances used to configure an 28 | /// pipeline. 29 | /// 30 | public abstract IList AdditionalHandlers { get; } 31 | 32 | /// 33 | /// Creates an . 34 | /// 35 | /// 36 | /// An built from the and 37 | /// . 38 | /// 39 | public abstract HttpMessageHandler Build(); 40 | 41 | protected internal static HttpMessageHandler CreateHandlerPipeline(HttpMessageHandler primaryHandler, IEnumerable additionalHandlers) 42 | { 43 | // This is similar to https://github.com/aspnet/AspNetWebStack/blob/master/src/System.Net.Http.Formatting/HttpClientFactory.cs#L58 44 | // but we don't want to take that package as a dependency. 45 | 46 | if (primaryHandler == null) 47 | { 48 | throw new ArgumentNullException(nameof(primaryHandler)); 49 | } 50 | 51 | if (additionalHandlers == null) 52 | { 53 | throw new ArgumentNullException(nameof(additionalHandlers)); 54 | } 55 | 56 | var additionalHandlersList = additionalHandlers as IReadOnlyList ?? additionalHandlers.ToArray(); 57 | 58 | var next = primaryHandler; 59 | for (var i = additionalHandlersList.Count - 1; i >= 0; i--) 60 | { 61 | var handler = additionalHandlersList[i]; 62 | if (handler == null) 63 | { 64 | var message = Resources.FormatHttpMessageHandlerBuilder_AdditionalHandlerIsNull(nameof(additionalHandlers)); 65 | throw new InvalidOperationException(message); 66 | } 67 | 68 | // Checking for this allows us to catch cases where someone has tried to re-use a handler. That really won't 69 | // work the way you want and it can be tricky for callers to figure out. 70 | if (handler.InnerHandler != null) 71 | { 72 | var message = Resources.FormatHttpMessageHandlerBuilder_AdditionHandlerIsInvalid( 73 | nameof(DelegatingHandler.InnerHandler), 74 | nameof(DelegatingHandler), 75 | nameof(HttpMessageHandlerBuilder), 76 | Environment.NewLine, 77 | handler); 78 | throw new InvalidOperationException(message); 79 | } 80 | 81 | handler.InnerHandler = next; 82 | next = handler; 83 | } 84 | 85 | return next; 86 | } 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/Microsoft.Extensions.Http/DefaultHttpClientFactory.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.Concurrent; 6 | using System.Collections.Generic; 7 | using System.Linq; 8 | using System.Net.Http; 9 | using System.Threading; 10 | using Microsoft.Extensions.DependencyInjection; 11 | using Microsoft.Extensions.Options; 12 | 13 | namespace Microsoft.Extensions.Http 14 | { 15 | internal class DefaultHttpClientFactory : IHttpClientFactory 16 | { 17 | private readonly IServiceProvider _services; 18 | private readonly IOptionsMonitor _optionsMonitor; 19 | private readonly IHttpMessageHandlerBuilderFilter[] _filters; 20 | 21 | // Lazy, because we're using a subtle pattern here to ensure that only one instance of 22 | // HttpMessageHandler is created for each name. 23 | private readonly ConcurrentDictionary> _cache; 24 | private readonly Func> _valueFactory; 25 | 26 | public DefaultHttpClientFactory( 27 | IServiceProvider services, 28 | IOptionsMonitor optionsMonitor, 29 | IEnumerable filters) 30 | { 31 | if (services == null) 32 | { 33 | throw new ArgumentNullException(nameof(services)); 34 | } 35 | 36 | if (optionsMonitor == null) 37 | { 38 | throw new ArgumentNullException(nameof(optionsMonitor)); 39 | } 40 | 41 | if (filters ==null) 42 | { 43 | throw new ArgumentNullException(nameof(filters)); 44 | } 45 | 46 | _services = services; 47 | _optionsMonitor = optionsMonitor; 48 | _filters = filters.ToArray(); 49 | 50 | // case-sensitive because named options is. 51 | _cache = new ConcurrentDictionary>(StringComparer.Ordinal); 52 | _valueFactory = (name) => new Lazy(() => CreateHandler(name), LazyThreadSafetyMode.ExecutionAndPublication); 53 | } 54 | 55 | public HttpClient CreateClient(string name) 56 | { 57 | if (name == null) 58 | { 59 | throw new ArgumentNullException(nameof(name)); 60 | } 61 | 62 | var handler = _cache.GetOrAdd(name, _valueFactory); 63 | var client = new HttpClient(handler.Value, disposeHandler: false); 64 | 65 | var options = _optionsMonitor.Get(name); 66 | for (var i = 0; i < options.HttpClientActions.Count; i++) 67 | { 68 | options.HttpClientActions[i](client); 69 | } 70 | 71 | return client; 72 | } 73 | 74 | // Internal for tests 75 | internal HttpMessageHandler CreateHandler(string name) 76 | { 77 | var builder = _services.GetRequiredService(); 78 | 79 | // This is similar to the initialization pattern in: 80 | // https://github.com/aspnet/Hosting/blob/e892ed8bbdcd25a0dafc1850033398dc57f65fe1/src/Microsoft.AspNetCore.Hosting/Internal/WebHost.cs#L188 81 | Action configure = Configure; 82 | for (var i = _filters.Length -1; i >= 0; i--) 83 | { 84 | configure = _filters[i].Configure(configure); 85 | } 86 | 87 | configure(builder); 88 | 89 | return builder.Build(); 90 | 91 | void Configure(HttpMessageHandlerBuilder b) 92 | { 93 | var options = _optionsMonitor.Get(name); 94 | for (var i = 0; i < options.HttpMessageHandlerBuilderActions.Count; i++) 95 | { 96 | options.HttpMessageHandlerBuilderActions[i](b); 97 | } 98 | } 99 | } 100 | } 101 | } -------------------------------------------------------------------------------- /src/Microsoft.Extensions.Http/DependencyInjection/HttpClientBuilderExtensions.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 | 8 | namespace Microsoft.Extensions.DependencyInjection 9 | { 10 | /// 11 | /// Extension methods for configuring an 12 | /// 13 | public static class HttpClientBuilderExtensions 14 | { 15 | /// 16 | /// Adds a delegate that will be used to configure a named . 17 | /// 18 | /// The . 19 | /// A delegate that is used to configure an . 20 | /// An that can be used to configure the client. 21 | public static IHttpClientBuilder AddHttpClientOptions(this IHttpClientBuilder builder, Action configureClient) 22 | { 23 | if (builder == null) 24 | { 25 | throw new ArgumentNullException(nameof(builder)); 26 | } 27 | 28 | if (configureClient == null) 29 | { 30 | throw new ArgumentNullException(nameof(configureClient)); 31 | } 32 | 33 | builder.Services.Configure(builder.Name, options => options.HttpClientActions.Add(configureClient)); 34 | 35 | return builder; 36 | } 37 | 38 | /// 39 | /// Adds a delegate that will be used to create an additional message handler for a named . 40 | /// 41 | /// The . 42 | /// A delegate that is used to configure an . 43 | /// An that can be used to configure the client. 44 | public static IHttpClientBuilder AddHttpMessageHandler(this IHttpClientBuilder builder, Func configureHandler) 45 | { 46 | if (builder == null) 47 | { 48 | throw new ArgumentNullException(nameof(builder)); 49 | } 50 | 51 | if (configureHandler == null) 52 | { 53 | throw new ArgumentNullException(nameof(configureHandler)); 54 | } 55 | 56 | builder.Services.Configure(builder.Name, options => 57 | { 58 | options.HttpMessageHandlerBuilderActions.Add(b => b.AdditionalHandlers.Add(configureHandler())); 59 | }); 60 | 61 | return builder; 62 | } 63 | 64 | 65 | /// 66 | /// Adds a delegate that will be used to configure message handlers using 67 | /// for a named . 68 | /// 69 | /// The . 70 | /// A delegate that is used to configure an . 71 | /// An that can be used to configure the client. 72 | public static IHttpClientBuilder AddHttpMessageHandlerBuilderOptions(this IHttpClientBuilder builder, Action configureBuilder) 73 | { 74 | if (builder == null) 75 | { 76 | throw new ArgumentNullException(nameof(builder)); 77 | } 78 | 79 | if (configureBuilder == null) 80 | { 81 | throw new ArgumentNullException(nameof(configureBuilder)); 82 | } 83 | 84 | builder.Services.Configure(builder.Name, options => options.HttpMessageHandlerBuilderActions.Add(configureBuilder)); 85 | 86 | return builder; 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /test/Microsoft.Extensions.Http.Test/DependencyInjection/HttpClientFactoryServiceCollectionExtensionsTest.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 Microsoft.Extensions.Http.Logging; 8 | using Microsoft.Extensions.Options; 9 | using Moq; 10 | using Xunit; 11 | 12 | namespace Microsoft.Extensions.DependencyInjection 13 | { 14 | // These are mostly integration tests that verify the configuration experience. 15 | public class HttpClientFactoryServiceCollectionExtensionsTest 16 | { 17 | [Fact] // Verifies that AddHttpClient is enough to get the factory and make clients. 18 | public void AddHttpClient_IsSelfContained_CanCreateClient() 19 | { 20 | // Arrange 21 | var serviceCollection = new ServiceCollection(); 22 | 23 | // Act1 24 | serviceCollection.AddHttpClient(); 25 | 26 | var services = serviceCollection.BuildServiceProvider(); 27 | var options = services.GetRequiredService>(); 28 | 29 | var factory = services.GetRequiredService(); 30 | 31 | // Act2 32 | var client = factory.CreateClient(); 33 | 34 | // Assert 35 | Assert.NotNull(client); 36 | } 37 | 38 | [Fact] 39 | public void AddHttpClient_WithDefaultName_ConfiguresDefaultClient() 40 | { 41 | var serviceCollection = new ServiceCollection(); 42 | 43 | // Act1 44 | serviceCollection.AddHttpClient(Options.Options.DefaultName, c => c.BaseAddress = new Uri("http://example.com/")); 45 | 46 | var services = serviceCollection.BuildServiceProvider(); 47 | var options = services.GetRequiredService>(); 48 | 49 | var factory = services.GetRequiredService(); 50 | 51 | // Act2 52 | var client = factory.CreateClient(); 53 | 54 | // Assert 55 | Assert.NotNull(client); 56 | Assert.Equal("http://example.com/", client.BaseAddress.AbsoluteUri); 57 | } 58 | 59 | [Fact] 60 | public void AddHttpClient_WithName_ConfiguresNamedClient() 61 | { 62 | var serviceCollection = new ServiceCollection(); 63 | 64 | // Act1 65 | serviceCollection.AddHttpClient("example.com", c => c.BaseAddress = new Uri("http://example.com/")); 66 | 67 | var services = serviceCollection.BuildServiceProvider(); 68 | var options = services.GetRequiredService>(); 69 | 70 | var factory = services.GetRequiredService(); 71 | 72 | // Act2 73 | var client = factory.CreateClient("example.com"); 74 | 75 | // Assert 76 | Assert.NotNull(client); 77 | Assert.Equal("http://example.com/", client.BaseAddress.AbsoluteUri); 78 | } 79 | 80 | [Fact] 81 | public void AddHttpMessageHandler_WithName_NewHandlerIsSurroundedByLogging() 82 | { 83 | var serviceCollection = new ServiceCollection(); 84 | 85 | HttpMessageHandlerBuilder builder = null; 86 | 87 | // Act1 88 | serviceCollection.AddHttpClient("example.com").AddHttpMessageHandlerBuilderOptions(b => 89 | { 90 | builder = b; 91 | 92 | b.AdditionalHandlers.Add(Mock.Of()); 93 | }); 94 | 95 | var services = serviceCollection.BuildServiceProvider(); 96 | var options = services.GetRequiredService>(); 97 | 98 | var factory = services.GetRequiredService(); 99 | 100 | // Act2 101 | var client = factory.CreateClient("example.com"); 102 | 103 | // Assert 104 | Assert.NotNull(client); 105 | 106 | Assert.Collection( 107 | builder.AdditionalHandlers, 108 | h => Assert.IsType(h), 109 | h => Assert.NotNull(h), 110 | h => Assert.IsType(h)); 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /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 Moq; 7 | using Xunit; 8 | 9 | namespace Microsoft.Extensions.Http 10 | { 11 | public class DefaultHttpMessageHandlerBuilderTest 12 | { 13 | // Testing this because it's an important design detail. If someone wants to globally replace the handler 14 | // they can do so by replacing this service. It's important that the Factory isn't the one to instantiate 15 | // the handler. The factory has no defaults - it only applies options. 16 | [Fact] 17 | public void Ctor_SetsPrimaryHandler() 18 | { 19 | // Arrange & Act 20 | var builder = new DefaultHttpMessageHandlerBuilder(); 21 | 22 | // Act 23 | Assert.IsType(builder.PrimaryHandler); 24 | } 25 | 26 | 27 | [Fact] 28 | public void Build_NoAdditionalHandlers_ReturnsPrimaryHandler() 29 | { 30 | // Arrange 31 | var builder = new DefaultHttpMessageHandlerBuilder() 32 | { 33 | PrimaryHandler = Mock.Of(), 34 | }; 35 | 36 | // Act 37 | var handler = builder.Build(); 38 | 39 | // Assert 40 | Assert.Same(builder.PrimaryHandler, handler); 41 | } 42 | 43 | [Fact] 44 | public void Build_SomeAdditionalHandlers_PutsTogetherDelegatingHandlers() 45 | { 46 | // Arrange 47 | var builder = new DefaultHttpMessageHandlerBuilder() 48 | { 49 | PrimaryHandler = Mock.Of(), 50 | AdditionalHandlers = 51 | { 52 | Mock.Of(), // Outer 53 | Mock.Of(), // Middle 54 | } 55 | }; 56 | 57 | // Act 58 | var handler = builder.Build(); 59 | 60 | // Assert 61 | Assert.Same(builder.AdditionalHandlers[0], handler); 62 | 63 | handler = Assert.IsAssignableFrom(handler).InnerHandler; 64 | Assert.Same(builder.AdditionalHandlers[1], handler); 65 | 66 | handler = Assert.IsAssignableFrom(handler).InnerHandler; 67 | Assert.Same(builder.PrimaryHandler, handler); 68 | } 69 | 70 | [Fact] 71 | public void Build_PrimaryHandlerIsNull_ThrowsException() 72 | { 73 | // Arrange 74 | var builder = new DefaultHttpMessageHandlerBuilder() 75 | { 76 | PrimaryHandler = null, 77 | }; 78 | 79 | // Act & Assert 80 | var exception = Assert.Throws(() => builder.Build()); 81 | Assert.Equal("The 'PrimaryHandler' must not be null.", exception.Message); 82 | } 83 | 84 | [Fact] 85 | public void Build_AdditionalHandlerIsNull_ThrowsException() 86 | { 87 | // Arrange 88 | var builder = new DefaultHttpMessageHandlerBuilder() 89 | { 90 | AdditionalHandlers = 91 | { 92 | null, 93 | } 94 | }; 95 | 96 | // Act & Assert 97 | var exception = Assert.Throws(() => builder.Build()); 98 | Assert.Equal("The 'additionalHandlers' must not contain a null entry.", exception.Message); 99 | } 100 | 101 | [Fact] 102 | public void Build_AdditionalHandlerHasNonNullInnerHandler_ThrowsException() 103 | { 104 | // Arrange 105 | var builder = new DefaultHttpMessageHandlerBuilder() 106 | { 107 | AdditionalHandlers = 108 | { 109 | Mock.Of(h => h.InnerHandler == Mock.Of()), 110 | } 111 | }; 112 | 113 | // Act & Assert 114 | var exception = Assert.Throws(() => builder.Build()); 115 | Assert.Equal( 116 | "The 'InnerHandler' property must be null. " + 117 | "'DelegatingHandler' instances provided to 'HttpMessageHandlerBuilder' must not be reused or cached." + Environment.NewLine + 118 | $"Handler: '{builder.AdditionalHandlers[0].ToString()}'", 119 | exception.Message); 120 | } 121 | } 122 | } -------------------------------------------------------------------------------- /src/Microsoft.Extensions.Http/DependencyInjection/HttpClientFactoryServiceCollectionExtensions.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.Extensions; 7 | using Microsoft.Extensions.Http; 8 | 9 | namespace Microsoft.Extensions.DependencyInjection 10 | { 11 | /// 12 | /// Extensions methods to configure an for . 13 | /// 14 | public static class HttpClientFactoryServiceCollectionExtensions 15 | { 16 | /// 17 | /// Adds the and related services to the . 18 | /// 19 | /// The . 20 | /// The . 21 | public static IServiceCollection AddHttpClient(this IServiceCollection services) 22 | { 23 | if (services == null) 24 | { 25 | throw new ArgumentNullException(nameof(services)); 26 | } 27 | 28 | services.AddLogging(); 29 | services.AddOptions(); 30 | 31 | // 32 | // Core abstractions 33 | // 34 | services.TryAddTransient(); 35 | services.TryAddSingleton(); 36 | 37 | // 38 | // Misc infrastrure 39 | // 40 | services.TryAddEnumerable(ServiceDescriptor.Singleton()); 41 | 42 | return services; 43 | } 44 | 45 | /// 46 | /// Adds the and related services to the and configures 47 | /// a named . 48 | /// 49 | /// The . 50 | /// The logical name of the to configure. 51 | /// An that can be used to configure the client. 52 | /// 53 | /// 54 | /// instances that apply the provided configuration can be retrieved using 55 | /// and providing the matching name. 56 | /// 57 | /// 58 | /// Use as the name to configure the default client. 59 | /// 60 | /// 61 | public static IHttpClientBuilder AddHttpClient(this IServiceCollection services, string name) 62 | { 63 | if (services == null) 64 | { 65 | throw new ArgumentNullException(nameof(services)); 66 | } 67 | 68 | AddHttpClient(services); 69 | 70 | return new DefaultHttpClientBuilder(services, name); 71 | } 72 | 73 | /// 74 | /// Adds the and related services to the and configures 75 | /// a named . 76 | /// 77 | /// The . 78 | /// The logical name of the to configure. 79 | /// A delegate that is used to configure an . 80 | /// An that can be used to configure the client. 81 | /// 82 | /// 83 | /// instances that apply the provided configuration can be retrieved using 84 | /// and providing the matching name. 85 | /// 86 | /// 87 | /// Use as the name to configure the default client. 88 | /// 89 | /// 90 | public static IHttpClientBuilder AddHttpClient(this IServiceCollection services, string name, Action configureClient) 91 | { 92 | if (services == null) 93 | { 94 | throw new ArgumentNullException(nameof(services)); 95 | } 96 | 97 | if (name == null) 98 | { 99 | throw new ArgumentNullException(nameof(name)); 100 | } 101 | 102 | if (configureClient == null) 103 | { 104 | throw new ArgumentNullException(nameof(configureClient)); 105 | } 106 | 107 | AddHttpClient(services); 108 | services.Configure(name, options => options.HttpClientActions.Add(configureClient)); 109 | 110 | return new DefaultHttpClientBuilder(services, name); 111 | } 112 | } 113 | } -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 ConfigFile 30 | The path to the configuration file that stores values. Defaults to korebuild.json. 31 | 32 | .PARAMETER Arguments 33 | Arguments to be passed to the command 34 | 35 | .NOTES 36 | This function will create a file $PSScriptRoot/korebuild-lock.txt. This lock file can be committed to source, but does not have to be. 37 | When the lockfile is not present, KoreBuild will create one using latest available version from $Channel. 38 | 39 | 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 40 | in the file are overridden by command line parameters. 41 | 42 | .EXAMPLE 43 | Example config file: 44 | ```json 45 | { 46 | "$schema": "https://raw.githubusercontent.com/aspnet/BuildTools/dev/tools/korebuild.schema.json", 47 | "channel": "dev", 48 | "toolsSource": "https://aspnetcore.blob.core.windows.net/buildtools" 49 | } 50 | ``` 51 | #> 52 | [CmdletBinding(PositionalBinding = $false)] 53 | param( 54 | [Parameter(Mandatory=$true, Position = 0)] 55 | [string]$Command, 56 | [string]$Path = $PSScriptRoot, 57 | [Alias('c')] 58 | [string]$Channel, 59 | [Alias('d')] 60 | [string]$DotNetHome, 61 | [Alias('s')] 62 | [string]$ToolsSource, 63 | [Alias('u')] 64 | [switch]$Update, 65 | [string]$ConfigFile, 66 | [Parameter(ValueFromRemainingArguments = $true)] 67 | [string[]]$Arguments 68 | ) 69 | 70 | Set-StrictMode -Version 2 71 | $ErrorActionPreference = 'Stop' 72 | 73 | # 74 | # Functions 75 | # 76 | 77 | function Get-KoreBuild { 78 | 79 | $lockFile = Join-Path $Path 'korebuild-lock.txt' 80 | 81 | if (!(Test-Path $lockFile) -or $Update) { 82 | Get-RemoteFile "$ToolsSource/korebuild/channels/$Channel/latest.txt" $lockFile 83 | } 84 | 85 | $version = Get-Content $lockFile | Where-Object { $_ -like 'version:*' } | Select-Object -first 1 86 | if (!$version) { 87 | Write-Error "Failed to parse version from $lockFile. Expected a line that begins with 'version:'" 88 | } 89 | $version = $version.TrimStart('version:').Trim() 90 | $korebuildPath = Join-Paths $DotNetHome ('buildtools', 'korebuild', $version) 91 | 92 | if (!(Test-Path $korebuildPath)) { 93 | Write-Host -ForegroundColor Magenta "Downloading KoreBuild $version" 94 | New-Item -ItemType Directory -Path $korebuildPath | Out-Null 95 | $remotePath = "$ToolsSource/korebuild/artifacts/$version/korebuild.$version.zip" 96 | 97 | try { 98 | $tmpfile = Join-Path ([IO.Path]::GetTempPath()) "KoreBuild-$([guid]::NewGuid()).zip" 99 | Get-RemoteFile $remotePath $tmpfile 100 | if (Get-Command -Name 'Expand-Archive' -ErrorAction Ignore) { 101 | # Use built-in commands where possible as they are cross-plat compatible 102 | Expand-Archive -Path $tmpfile -DestinationPath $korebuildPath 103 | } 104 | else { 105 | # Fallback to old approach for old installations of PowerShell 106 | Add-Type -AssemblyName System.IO.Compression.FileSystem 107 | [System.IO.Compression.ZipFile]::ExtractToDirectory($tmpfile, $korebuildPath) 108 | } 109 | } 110 | catch { 111 | Remove-Item -Recurse -Force $korebuildPath -ErrorAction Ignore 112 | throw 113 | } 114 | finally { 115 | Remove-Item $tmpfile -ErrorAction Ignore 116 | } 117 | } 118 | 119 | return $korebuildPath 120 | } 121 | 122 | function Join-Paths([string]$path, [string[]]$childPaths) { 123 | $childPaths | ForEach-Object { $path = Join-Path $path $_ } 124 | return $path 125 | } 126 | 127 | function Get-RemoteFile([string]$RemotePath, [string]$LocalPath) { 128 | if ($RemotePath -notlike 'http*') { 129 | Copy-Item $RemotePath $LocalPath 130 | return 131 | } 132 | 133 | $retries = 10 134 | while ($retries -gt 0) { 135 | $retries -= 1 136 | try { 137 | Invoke-WebRequest -UseBasicParsing -Uri $RemotePath -OutFile $LocalPath 138 | return 139 | } 140 | catch { 141 | Write-Verbose "Request failed. $retries retries remaining" 142 | } 143 | } 144 | 145 | Write-Error "Download failed: '$RemotePath'." 146 | } 147 | 148 | # 149 | # Main 150 | # 151 | 152 | # Load configuration or set defaults 153 | 154 | $Path = Resolve-Path $Path 155 | if (!$ConfigFile) { $ConfigFile = Join-Path $Path 'korebuild.json' } 156 | 157 | if (Test-Path $ConfigFile) { 158 | try { 159 | $config = Get-Content -Raw -Encoding UTF8 -Path $ConfigFile | ConvertFrom-Json 160 | if ($config) { 161 | if (!($Channel) -and (Get-Member -Name 'channel' -InputObject $config)) { [string] $Channel = $config.channel } 162 | if (!($ToolsSource) -and (Get-Member -Name 'toolsSource' -InputObject $config)) { [string] $ToolsSource = $config.toolsSource} 163 | } 164 | } catch { 165 | Write-Warning "$ConfigFile could not be read. Its settings will be ignored." 166 | Write-Warning $Error[0] 167 | } 168 | } 169 | 170 | if (!$DotNetHome) { 171 | $DotNetHome = if ($env:DOTNET_HOME) { $env:DOTNET_HOME } ` 172 | elseif ($env:USERPROFILE) { Join-Path $env:USERPROFILE '.dotnet'} ` 173 | elseif ($env:HOME) {Join-Path $env:HOME '.dotnet'}` 174 | else { Join-Path $PSScriptRoot '.dotnet'} 175 | } 176 | 177 | if (!$Channel) { $Channel = 'dev' } 178 | if (!$ToolsSource) { $ToolsSource = 'https://aspnetcore.blob.core.windows.net/buildtools' } 179 | 180 | # Execute 181 | 182 | $korebuildPath = Get-KoreBuild 183 | Import-Module -Force -Scope Local (Join-Path $korebuildPath 'KoreBuild.psd1') 184 | 185 | try { 186 | Set-KoreBuildSettings -ToolsSource $ToolsSource -DotNetHome $DotNetHome -RepoPath $Path -ConfigFile $ConfigFile 187 | Invoke-KoreBuildCommand $Command @Arguments 188 | } 189 | finally { 190 | Remove-Module 'KoreBuild' -ErrorAction Ignore 191 | } 192 | -------------------------------------------------------------------------------- /test/Microsoft.Extensions.Http.Test/DefaultHttpClientFactoryTest.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 Microsoft.Extensions.DependencyInjection; 8 | using Microsoft.Extensions.Options; 9 | using Moq; 10 | using Moq.Protected; 11 | using Xunit; 12 | 13 | namespace Microsoft.Extensions.Http 14 | { 15 | public class DefaultHttpClientFactoryTest 16 | { 17 | public DefaultHttpClientFactoryTest() 18 | { 19 | Services = new ServiceCollection().AddHttpClient().BuildServiceProvider(); 20 | Options = Services.GetRequiredService>(); 21 | } 22 | 23 | public IServiceProvider Services { get; } 24 | 25 | public IOptionsMonitor Options { get; } 26 | 27 | public IEnumerable EmptyFilters = Array.Empty(); 28 | 29 | [Fact] 30 | public void Factory_MultipleCalls_DoesNotCacheHttpClient() 31 | { 32 | // Arrange 33 | var count = 0; 34 | Options.CurrentValue.HttpClientActions.Add(c => 35 | { 36 | count++; 37 | }); 38 | 39 | var factory = new DefaultHttpClientFactory(Services, Options, EmptyFilters); 40 | 41 | // Act 1 42 | var client1 = factory.CreateClient(); 43 | 44 | // Act 2 45 | var client2 = factory.CreateClient(); 46 | 47 | // Assert 48 | Assert.Equal(2, count); 49 | Assert.NotSame(client1, client2); 50 | } 51 | 52 | [Fact] 53 | public void Factory_MultipleCalls_CachesHandler() 54 | { 55 | // Arrange 56 | var count = 0; 57 | Options.CurrentValue.HttpMessageHandlerBuilderActions.Add(b => 58 | { 59 | count++; 60 | }); 61 | 62 | var factory = new DefaultHttpClientFactory(Services, Options, EmptyFilters); 63 | 64 | // Act 1 65 | var client1 = factory.CreateClient(); 66 | 67 | // Act 2 68 | var client2 = factory.CreateClient(); 69 | 70 | // Assert 71 | Assert.Equal(1, count); 72 | Assert.NotSame(client1, client2); 73 | } 74 | 75 | [Fact] 76 | public void Factory_DisposeClient_DoesNotDisposeHandler() 77 | { 78 | // Arrange 79 | Options.CurrentValue.HttpMessageHandlerBuilderActions.Add(b => 80 | { 81 | var mockHandler = new Mock(); 82 | mockHandler 83 | .Protected() 84 | .Setup("Dispose", true) 85 | .Throws(new Exception("Dispose should not be called")); 86 | 87 | b.PrimaryHandler = mockHandler.Object; 88 | }); 89 | 90 | var factory = new DefaultHttpClientFactory(Services, Options, EmptyFilters); 91 | 92 | // Act 93 | using (factory.CreateClient()) 94 | { 95 | } 96 | 97 | // Assert (does not throw) 98 | } 99 | 100 | [Fact] 101 | public void Factory_CreateClient_WithoutName_UsesDefaultOptions() 102 | { 103 | // Arrange 104 | var count = 0; 105 | Options.CurrentValue.HttpClientActions.Add(b => 106 | { 107 | count++; 108 | }); 109 | 110 | var factory = new DefaultHttpClientFactory(Services, Options, EmptyFilters); 111 | 112 | // Act 113 | var client = factory.CreateClient(); 114 | 115 | // Assert 116 | Assert.Equal(1, count); 117 | } 118 | 119 | [Fact] 120 | public void Factory_CreateClient_WithName_UsesNamedOptions() 121 | { 122 | // Arrange 123 | var count = 0; 124 | Options.Get("github").HttpClientActions.Add(b => 125 | { 126 | count++; 127 | }); 128 | 129 | var factory = new DefaultHttpClientFactory(Services, Options, EmptyFilters); 130 | 131 | // Act 132 | var client = factory.CreateClient("github"); 133 | 134 | // Assert 135 | Assert.Equal(1, count); 136 | } 137 | 138 | [Fact] 139 | public void Factory_CreateClient_FiltersCanDecorateBuilder() 140 | { 141 | // Arrange 142 | var expected = new HttpMessageHandler[] 143 | { 144 | Mock.Of(), // Added by filter1 145 | Mock.Of(), // Added by filter2 146 | Mock.Of(), // Added by filter3 147 | Mock.Of(), // Added in options 148 | Mock.Of(), // Added by filter3 149 | Mock.Of(), // Added by filter2 150 | Mock.Of(), // Added by filter1 151 | 152 | Mock.Of(), // Set as primary handler by options 153 | }; 154 | 155 | Options.Get("github").HttpMessageHandlerBuilderActions.Add(b => 156 | { 157 | b.PrimaryHandler = expected[7]; 158 | 159 | b.AdditionalHandlers.Add((DelegatingHandler)expected[3]); 160 | }); 161 | 162 | var filter1 = new Mock(); 163 | filter1 164 | .Setup(f => f.Configure(It.IsAny>())) 165 | .Returns>(next => (b) => 166 | { 167 | next(b); // Calls filter2 168 | b.AdditionalHandlers.Insert(0, (DelegatingHandler)expected[0]); 169 | b.AdditionalHandlers.Add((DelegatingHandler)expected[6]); 170 | }); 171 | 172 | var filter2 = new Mock(); 173 | filter2 174 | .Setup(f => f.Configure(It.IsAny>())) 175 | .Returns>(next => (b) => 176 | { 177 | next(b); // Calls filter3 178 | b.AdditionalHandlers.Insert(0, (DelegatingHandler)expected[1]); 179 | b.AdditionalHandlers.Add((DelegatingHandler)expected[5]); 180 | }); 181 | 182 | var filter3 = new Mock(); 183 | filter3 184 | .Setup(f => f.Configure(It.IsAny>())) 185 | .Returns>(next => (b) => 186 | { 187 | b.AdditionalHandlers.Add((DelegatingHandler)expected[2]); 188 | next(b); // Calls options 189 | b.AdditionalHandlers.Add((DelegatingHandler)expected[4]); 190 | }); 191 | 192 | var factory = new DefaultHttpClientFactory(Services, Options, new[] { filter1.Object, filter2.Object, filter3.Object, }); 193 | 194 | // Act 195 | var handler = factory.CreateHandler("github"); 196 | 197 | // Assert 198 | for (var i = 0; i < expected.Length - 1; i++) 199 | { 200 | Assert.Same(expected[i], handler); 201 | handler = Assert.IsAssignableFrom(handler).InnerHandler; 202 | } 203 | 204 | Assert.Same(expected[7], handler); 205 | } 206 | } 207 | } 208 | -------------------------------------------------------------------------------- /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 | repo_path="$DIR" 18 | channel='' 19 | tools_source='' 20 | 21 | # 22 | # Functions 23 | # 24 | __usage() { 25 | echo "Usage: $(basename "${BASH_SOURCE[0]}") command [options] [[--] ...]" 26 | echo "" 27 | echo "Arguments:" 28 | echo " command The command to be run." 29 | echo " ... Arguments passed to the command. Variable number of arguments allowed." 30 | echo "" 31 | echo "Options:" 32 | echo " --verbose Show verbose output." 33 | echo " -c|--channel The channel of KoreBuild to download. Overrides the value from the config file.." 34 | echo " --config-file The path to the configuration file that stores values. Defaults to korebuild.json." 35 | echo " -d|--dotnet-home The directory where .NET Core tools will be stored. Defaults to '\$DOTNET_HOME' or '\$HOME/.dotnet." 36 | echo " --path The directory to build. Defaults to the directory containing the script." 37 | echo " -s|--tools-source|-ToolsSource The base url where build tools can be downloaded. Overrides the value from the config file." 38 | echo " -u|--update Update to the latest KoreBuild even if the lock file is present." 39 | echo "" 40 | echo "Description:" 41 | 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." 42 | echo " When the lockfile is not present, KoreBuild will create one using latest available version from \$channel." 43 | 44 | if [[ "${1:-}" != '--no-exit' ]]; then 45 | exit 2 46 | fi 47 | } 48 | 49 | get_korebuild() { 50 | local version 51 | local lock_file="$repo_path/korebuild-lock.txt" 52 | if [ ! -f "$lock_file" ] || [ "$update" = true ]; then 53 | __get_remote_file "$tools_source/korebuild/channels/$channel/latest.txt" "$lock_file" 54 | fi 55 | version="$(grep 'version:*' -m 1 "$lock_file")" 56 | if [[ "$version" == '' ]]; then 57 | __error "Failed to parse version from $lock_file. Expected a line that begins with 'version:'" 58 | return 1 59 | fi 60 | version="$(echo "${version#version:}" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')" 61 | local korebuild_path="$DOTNET_HOME/buildtools/korebuild/$version" 62 | 63 | { 64 | if [ ! -d "$korebuild_path" ]; then 65 | mkdir -p "$korebuild_path" 66 | local remote_path="$tools_source/korebuild/artifacts/$version/korebuild.$version.zip" 67 | tmpfile="$(mktemp)" 68 | echo -e "${MAGENTA}Downloading KoreBuild ${version}${RESET}" 69 | if __get_remote_file "$remote_path" "$tmpfile"; then 70 | unzip -q -d "$korebuild_path" "$tmpfile" 71 | fi 72 | rm "$tmpfile" || true 73 | fi 74 | 75 | source "$korebuild_path/KoreBuild.sh" 76 | } || { 77 | if [ -d "$korebuild_path" ]; then 78 | echo "Cleaning up after failed installation" 79 | rm -rf "$korebuild_path" || true 80 | fi 81 | return 1 82 | } 83 | } 84 | 85 | __error() { 86 | echo -e "${RED}error: $*${RESET}" 1>&2 87 | } 88 | 89 | __warn() { 90 | echo -e "${YELLOW}warning: $*${RESET}" 91 | } 92 | 93 | __machine_has() { 94 | hash "$1" > /dev/null 2>&1 95 | return $? 96 | } 97 | 98 | __get_remote_file() { 99 | local remote_path=$1 100 | local local_path=$2 101 | 102 | if [[ "$remote_path" != 'http'* ]]; then 103 | cp "$remote_path" "$local_path" 104 | return 0 105 | fi 106 | 107 | local failed=false 108 | if __machine_has wget; then 109 | wget --tries 10 --quiet -O "$local_path" "$remote_path" || failed=true 110 | else 111 | failed=true 112 | fi 113 | 114 | if [ "$failed" = true ] && __machine_has curl; then 115 | failed=false 116 | curl --retry 10 -sSL -f --create-dirs -o "$local_path" "$remote_path" || failed=true 117 | fi 118 | 119 | if [ "$failed" = true ]; then 120 | __error "Download failed: $remote_path" 1>&2 121 | return 1 122 | fi 123 | } 124 | 125 | # 126 | # main 127 | # 128 | 129 | command="${1:-}" 130 | shift 131 | 132 | while [[ $# -gt 0 ]]; do 133 | case $1 in 134 | -\?|-h|--help) 135 | __usage --no-exit 136 | exit 0 137 | ;; 138 | -c|--channel|-Channel) 139 | shift 140 | channel="${1:-}" 141 | [ -z "$channel" ] && __usage 142 | ;; 143 | --config-file|-ConfigFile) 144 | shift 145 | config_file="${1:-}" 146 | [ -z "$config_file" ] && __usage 147 | if [ ! -f "$config_file" ]; then 148 | __error "Invalid value for --config-file. $config_file does not exist." 149 | exit 1 150 | fi 151 | ;; 152 | -d|--dotnet-home|-DotNetHome) 153 | shift 154 | DOTNET_HOME="${1:-}" 155 | [ -z "$DOTNET_HOME" ] && __usage 156 | ;; 157 | --path|-Path) 158 | shift 159 | repo_path="${1:-}" 160 | [ -z "$repo_path" ] && __usage 161 | ;; 162 | -s|--tools-source|-ToolsSource) 163 | shift 164 | tools_source="${1:-}" 165 | [ -z "$tools_source" ] && __usage 166 | ;; 167 | -u|--update|-Update) 168 | update=true 169 | ;; 170 | --verbose|-Verbose) 171 | verbose=true 172 | ;; 173 | --) 174 | shift 175 | break 176 | ;; 177 | *) 178 | break 179 | ;; 180 | esac 181 | shift 182 | done 183 | 184 | if ! __machine_has unzip; then 185 | __error 'Missing required command: unzip' 186 | exit 1 187 | fi 188 | 189 | if ! __machine_has curl && ! __machine_has wget; then 190 | __error 'Missing required command. Either wget or curl is required.' 191 | exit 1 192 | fi 193 | 194 | [ -z "${config_file:-}" ] && config_file="$repo_path/korebuild.json" 195 | if [ -f "$config_file" ]; then 196 | if __machine_has jq ; then 197 | if jq '.' "$config_file" >/dev/null ; then 198 | config_channel="$(jq -r 'select(.channel!=null) | .channel' "$config_file")" 199 | config_tools_source="$(jq -r 'select(.toolsSource!=null) | .toolsSource' "$config_file")" 200 | else 201 | __warn "$config_file is invalid JSON. Its settings will be ignored." 202 | fi 203 | elif __machine_has python ; then 204 | if python -c "import json,codecs;obj=json.load(codecs.open('$config_file', 'r', 'utf-8-sig'))" >/dev/null ; then 205 | 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 '')")" 206 | 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 '')")" 207 | else 208 | __warn "$config_file is invalid JSON. Its settings will be ignored." 209 | fi 210 | else 211 | __warn 'Missing required command: jq or pyton. Could not parse the JSON file. Its settings will be ignored.' 212 | fi 213 | 214 | [ ! -z "${config_channel:-}" ] && channel="$config_channel" 215 | [ ! -z "${config_tools_source:-}" ] && tools_source="$config_tools_source" 216 | fi 217 | 218 | [ -z "$channel" ] && channel='dev' 219 | [ -z "$tools_source" ] && tools_source='https://aspnetcore.blob.core.windows.net/buildtools' 220 | 221 | get_korebuild 222 | set_korebuildsettings "$tools_source" "$DOTNET_HOME" "$repo_path" "$config_file" 223 | invoke_korebuild_command "$command" "$@" 224 | --------------------------------------------------------------------------------