├── 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: [](https://ci.appveyor.com/project/aspnetci/HttpClientFactory/branch/dev)
5 |
6 | Travis: [](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 |
--------------------------------------------------------------------------------