├── Arnath.StandaloneHttpClientFactory
├── AssemblyAttributes.cs
├── IHttpClientFactory.cs
├── Arnath.StandaloneHttpClientFactory.csproj
├── ServicePointHttpMessageHandler.cs
├── LoggingHttpMessageHandler.cs
└── StandaloneHttpClientFactory.cs
├── .github
└── workflows
│ └── publish-nuget.yaml
├── Arnath.StandaloneHttpClientFactory.DotNetFramework.UnitTests
├── Properties
│ └── AssemblyInfo.cs
├── app.config
├── packages.config
├── StandaloneHttpClientFactoryTests.cs
└── Arnath.StandaloneHttpClientFactory.DotNetFramework.UnitTests.csproj
├── Arnath.StandaloneHttpClientFactory.DotNetCore.UnitTests
├── Arnath.StandaloneHttpClientFactory.DotNetCore.UnitTests.csproj
└── StandaloneHttpClientFactoryTests.cs
├── StandaloneHttpClientFactory.sln
├── README.md
├── .gitignore
├── CodeAnalysis.ruleset
└── LICENSE
/Arnath.StandaloneHttpClientFactory/AssemblyAttributes.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Vijay Prakash. All rights reserved.
2 | // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
3 |
4 | using System.Runtime.CompilerServices;
5 |
6 | [assembly: InternalsVisibleTo("Arnath.StandaloneHttpClientFactory.DotNetCore.UnitTests")]
7 | [assembly: InternalsVisibleTo("Arnath.StandaloneHttpClientFactory.DotNetFramework.UnitTests")]
8 |
--------------------------------------------------------------------------------
/.github/workflows/publish-nuget.yaml:
--------------------------------------------------------------------------------
1 | name: Publish NuGet package
2 |
3 | on:
4 | push:
5 | branches:
6 | - master
7 |
8 | jobs:
9 | push:
10 | runs-on: windows-latest
11 | steps:
12 | - uses: actions/checkout@v3
13 | - name: Setup .NET Core
14 | uses: actions/setup-dotnet@v3
15 | with:
16 | dotnet-version: 6.0
17 | - name: Install dependencies
18 | run: dotnet restore
19 | - name: Build
20 | run: dotnet build --configuration Release --no-restore --output .\Drop\
21 | - name: Test
22 | run: dotnet test --no-restore --verbosity normal
23 | - name: Push
24 | run: dotnet nuget push Drop\*.nupkg --api-key ${{ secrets.NUGET_API_KEY }} --source https://api.nuget.org/v3/index.json
25 |
--------------------------------------------------------------------------------
/Arnath.StandaloneHttpClientFactory.DotNetFramework.UnitTests/Properties/AssemblyInfo.cs:
--------------------------------------------------------------------------------
1 | using System.Reflection;
2 | using System.Runtime.CompilerServices;
3 | using System.Runtime.InteropServices;
4 |
5 | [assembly: AssemblyTitle("Arnath.StandaloneHttpClientFactory.DotNetFramework.UnitTests")]
6 | [assembly: AssemblyDescription("")]
7 | [assembly: AssemblyConfiguration("")]
8 | [assembly: AssemblyCompany("")]
9 | [assembly: AssemblyProduct("Arnath.StandaloneHttpClientFactory.DotNetFramework.UnitTests")]
10 | [assembly: AssemblyCopyright("Copyright © 2020")]
11 | [assembly: AssemblyTrademark("")]
12 | [assembly: AssemblyCulture("")]
13 |
14 | [assembly: ComVisible(false)]
15 |
16 | [assembly: Guid("a9e5fbb7-f684-4d20-a79a-0e3ddc586959")]
17 |
18 | // [assembly: AssemblyVersion("1.0.*")]
19 | [assembly: AssemblyVersion("1.0.0.0")]
20 | [assembly: AssemblyFileVersion("1.0.0.0")]
21 |
--------------------------------------------------------------------------------
/Arnath.StandaloneHttpClientFactory/IHttpClientFactory.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Vijay Prakash. All rights reserved.
2 | // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
3 |
4 | namespace Arnath.StandaloneHttpClientFactory
5 | {
6 | using System.Net.Http;
7 |
8 | ///
9 | /// Factory for instances that follows the recommended
10 | /// best practices for creation and disposal. See
11 | /// https://github.com/arnath/standalonehttpclientfactory for more details.
12 | ///
13 | public interface IHttpClientFactory
14 | {
15 | ///
16 | /// Creates an HTTP client instance.
17 | ///
18 | ///
19 | HttpClient CreateClient();
20 | }
21 | }
22 |
--------------------------------------------------------------------------------
/Arnath.StandaloneHttpClientFactory.DotNetFramework.UnitTests/app.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
--------------------------------------------------------------------------------
/Arnath.StandaloneHttpClientFactory.DotNetCore.UnitTests/Arnath.StandaloneHttpClientFactory.DotNetCore.UnitTests.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | netcoreapp3.1
5 |
6 | false
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 | all
16 | runtime; build; native; contentfiles; analyzers; buildtransitive
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
--------------------------------------------------------------------------------
/Arnath.StandaloneHttpClientFactory.DotNetFramework.UnitTests/packages.config:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/Arnath.StandaloneHttpClientFactory/Arnath.StandaloneHttpClientFactory.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | netstandard2.0;netcoreapp3.1;net6.0
5 | ..\CodeAnalysis.ruleset
6 | true
7 | 1.0.0
8 | Vijay Prakash
9 | Vijay Prakash
10 | StandaloneHttpClientFactory
11 | Standalone HTTP client factory for .NET 6, .NET Core 3.1, and .NET Standard without all the ASP.NET dependencies.
12 | https://github.com/arnath/sydney
13 | LICENSE
14 |
15 |
16 |
17 |
18 | all
19 | runtime; build; native; contentfiles; analyzers; buildtransitive
20 |
21 |
22 |
23 |
24 |
25 |
26 | True
27 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/Arnath.StandaloneHttpClientFactory/ServicePointHttpMessageHandler.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Vijay Prakash. All rights reserved.
2 | // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
3 |
4 | namespace Arnath.StandaloneHttpClientFactory
5 | {
6 | using System;
7 | using System.Net;
8 | using System.Net.Http;
9 | using System.Threading;
10 | using System.Threading.Tasks;
11 |
12 | internal class ServicePointHttpMessageHandler : DelegatingHandler
13 | {
14 | public ServicePointHttpMessageHandler(TimeSpan connectionLeaseTimeout)
15 | : this(connectionLeaseTimeout, null)
16 | {
17 | }
18 |
19 | public ServicePointHttpMessageHandler(TimeSpan connectionTimeout, HttpMessageHandler innerHandler)
20 | : base(innerHandler)
21 | {
22 | this.ConnectionLeaseTimeout = connectionTimeout;
23 | }
24 |
25 | public TimeSpan ConnectionLeaseTimeout { get; }
26 |
27 | protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
28 | {
29 | if (request == null)
30 | {
31 | throw new ArgumentNullException(nameof(request));
32 | }
33 |
34 | ServicePoint servicePoint = ServicePointManager.FindServicePoint(request.RequestUri);
35 | servicePoint.ConnectionLeaseTimeout = (int)this.ConnectionLeaseTimeout.TotalMilliseconds;
36 |
37 | return base.SendAsync(request, cancellationToken);
38 | }
39 | }
40 | }
41 |
--------------------------------------------------------------------------------
/StandaloneHttpClientFactory.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio Version 16
4 | VisualStudioVersion = 16.0.29609.76
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Arnath.StandaloneHttpClientFactory", "Arnath.StandaloneHttpClientFactory\Arnath.StandaloneHttpClientFactory.csproj", "{EF792056-91F8-4832-911F-68EC6992A611}"
7 | EndProject
8 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{FB442585-D5D2-46FE-AF27-5A26EA8026BB}"
9 | ProjectSection(SolutionItems) = preProject
10 | .gitignore = .gitignore
11 | CodeAnalysis.ruleset = CodeAnalysis.ruleset
12 | LICENSE = LICENSE
13 | README.md = README.md
14 | EndProjectSection
15 | EndProject
16 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Arnath.StandaloneHttpClientFactory.DotNetCore.UnitTests", "Arnath.StandaloneHttpClientFactory.DotNetCore.UnitTests\Arnath.StandaloneHttpClientFactory.DotNetCore.UnitTests.csproj", "{8722AC6F-E590-499E-B046-24EF0B7F8A73}"
17 | EndProject
18 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Arnath.StandaloneHttpClientFactory.DotNetFramework.UnitTests", "Arnath.StandaloneHttpClientFactory.DotNetFramework.UnitTests\Arnath.StandaloneHttpClientFactory.DotNetFramework.UnitTests.csproj", "{A9E5FBB7-F684-4D20-A79A-0E3DDC586959}"
19 | EndProject
20 | Global
21 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
22 | Debug|Any CPU = Debug|Any CPU
23 | Release|Any CPU = Release|Any CPU
24 | EndGlobalSection
25 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
26 | {EF792056-91F8-4832-911F-68EC6992A611}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
27 | {EF792056-91F8-4832-911F-68EC6992A611}.Debug|Any CPU.Build.0 = Debug|Any CPU
28 | {EF792056-91F8-4832-911F-68EC6992A611}.Release|Any CPU.ActiveCfg = Release|Any CPU
29 | {EF792056-91F8-4832-911F-68EC6992A611}.Release|Any CPU.Build.0 = Release|Any CPU
30 | {8722AC6F-E590-499E-B046-24EF0B7F8A73}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
31 | {8722AC6F-E590-499E-B046-24EF0B7F8A73}.Debug|Any CPU.Build.0 = Debug|Any CPU
32 | {8722AC6F-E590-499E-B046-24EF0B7F8A73}.Release|Any CPU.ActiveCfg = Release|Any CPU
33 | {8722AC6F-E590-499E-B046-24EF0B7F8A73}.Release|Any CPU.Build.0 = Release|Any CPU
34 | {A9E5FBB7-F684-4D20-A79A-0E3DDC586959}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
35 | {A9E5FBB7-F684-4D20-A79A-0E3DDC586959}.Debug|Any CPU.Build.0 = Debug|Any CPU
36 | {A9E5FBB7-F684-4D20-A79A-0E3DDC586959}.Release|Any CPU.ActiveCfg = Release|Any CPU
37 | {A9E5FBB7-F684-4D20-A79A-0E3DDC586959}.Release|Any CPU.Build.0 = Release|Any CPU
38 | EndGlobalSection
39 | GlobalSection(SolutionProperties) = preSolution
40 | HideSolutionNode = FALSE
41 | EndGlobalSection
42 | GlobalSection(ExtensibilityGlobals) = postSolution
43 | SolutionGuid = {AB909983-A2B6-4F72-9074-315A47F40104}
44 | EndGlobalSection
45 | EndGlobal
46 |
--------------------------------------------------------------------------------
/Arnath.StandaloneHttpClientFactory.DotNetCore.UnitTests/StandaloneHttpClientFactoryTests.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Vijay Prakash. All rights reserved.
2 | // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
3 |
4 | namespace Arnath.StandaloneHttpClientFactory.DotNetCore.UnitTests
5 | {
6 | using System;
7 | using System.Net.Http;
8 | using System.Reflection;
9 | using System.Threading;
10 | using System.Threading.Tasks;
11 | using FakeItEasy;
12 | using Microsoft.Extensions.Logging.Abstractions;
13 | using Xunit;
14 |
15 | public class StandaloneHttpClientFactoryTests
16 | {
17 | [Fact]
18 | public void CreateClientUsesDefaultsWhenFactoryUsesDefaults()
19 | {
20 | using StandaloneHttpClientFactory httpClientFactory = new StandaloneHttpClientFactory();
21 | using HttpClient client = httpClientFactory.CreateClient();
22 | HttpMessageHandler handler = GetHandler(client);
23 |
24 | Assert.IsType(handler);
25 | Assert.Equal(StandaloneHttpClientFactory.DefaultConnectionLifetime, ((SocketsHttpHandler)handler).PooledConnectionLifetime);
26 | }
27 |
28 | [Fact]
29 | public void CreateClientUsesProvidedConnectionLifetime()
30 | {
31 | TimeSpan connectionLifetime = TimeSpan.FromMinutes(1);
32 | using StandaloneHttpClientFactory httpClientFactory = new StandaloneHttpClientFactory(connectionLifetime);
33 | using HttpClient client = httpClientFactory.CreateClient();
34 | HttpMessageHandler handler = GetHandler(client);
35 |
36 | Assert.IsType(handler);
37 | Assert.Equal(connectionLifetime, ((SocketsHttpHandler)handler).PooledConnectionLifetime);
38 | }
39 |
40 | [Fact]
41 | public void CreateClientUsesLoggerToCreateLoggingHandler()
42 | {
43 | using StandaloneHttpClientFactory httpClientFactory = new StandaloneHttpClientFactory(NullLogger.Instance);
44 | using HttpClient client = httpClientFactory.CreateClient();
45 | HttpMessageHandler handler = GetHandler(client);
46 |
47 | Assert.IsType(handler);
48 | LoggingHttpMessageHandler loggingHandler = (LoggingHttpMessageHandler)handler;
49 | Assert.IsType(loggingHandler.InnerHandler);
50 | }
51 |
52 | [Fact]
53 | public void CreateClientCreatesHandlerPipeline()
54 | {
55 | DelegatingHandler foo = A.Fake(options => options.CallsBaseMethods());
56 | DelegatingHandler bar = A.Fake(options => options.CallsBaseMethods());
57 |
58 | using StandaloneHttpClientFactory httpClientFactory = new StandaloneHttpClientFactory(foo, bar);
59 | using HttpClient client = httpClientFactory.CreateClient();
60 | HttpMessageHandler handler = GetHandler(client);
61 |
62 | Assert.Same(foo, handler);
63 | DelegatingHandler delegatingHandler = (DelegatingHandler)handler;
64 | Assert.Same(bar, delegatingHandler.InnerHandler);
65 | delegatingHandler = (DelegatingHandler)delegatingHandler.InnerHandler;
66 | Assert.IsType(delegatingHandler.InnerHandler);
67 | }
68 |
69 | [Fact]
70 | public void CreateClientCreatesNewClientWithSameHandler()
71 | {
72 | using StandaloneHttpClientFactory httpClientFactory = new StandaloneHttpClientFactory();
73 | using HttpClient client1 = httpClientFactory.CreateClient();
74 | using HttpClient client2 = httpClientFactory.CreateClient();
75 | HttpMessageHandler handler1 = GetHandler(client1);
76 | HttpMessageHandler handler2 = GetHandler(client2);
77 |
78 | Assert.NotSame(client1, client2);
79 | Assert.Same(handler1, handler2);
80 | }
81 |
82 | private HttpMessageHandler GetHandler(HttpClient client)
83 | {
84 | FieldInfo handlerField =
85 | typeof(HttpClient).BaseType.GetField(
86 | "_handler",
87 | BindingFlags.NonPublic | BindingFlags.Instance);
88 | object o = handlerField.GetValue(client);
89 |
90 | return o as HttpMessageHandler;
91 | }
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/Arnath.StandaloneHttpClientFactory/LoggingHttpMessageHandler.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Vijay Prakash. All rights reserved.
2 | // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
3 |
4 | namespace Arnath.StandaloneHttpClientFactory
5 | {
6 | using System;
7 | using System.Diagnostics;
8 | using System.Diagnostics.Contracts;
9 | using System.Net.Http;
10 | using System.Threading;
11 | using System.Threading.Tasks;
12 | using Microsoft.Extensions.Logging;
13 |
14 | ///
15 | /// Delegating HTTP message handler that logs some basic info about the
16 | /// request and response. The LogRequestStart and LogRequestEnd methods
17 | /// can be overridden to do custom logging. Largely based on the
18 | /// Microsoft.Extensions.Http.Logging.LoggingHttpMessageHandler" class
19 | /// but it's not used directly to avoid a bunch of ASP.NET dependencies.
20 | ///
21 | public class LoggingHttpMessageHandler : DelegatingHandler
22 | {
23 | ///
24 | /// Creates a new instance of the LoggingHttpMessageHandler class that
25 | /// uses the specified logger to log request information.
26 | ///
27 | /// The logger to which to log info about requests.
28 | public LoggingHttpMessageHandler(ILogger logger)
29 | : this(logger, innerHandler: null)
30 | {
31 | }
32 |
33 | ///
34 | /// Creates a new instance of the LoggingHttpMessageHandler class that
35 | /// uses the specified logger to log request information and calls the
36 | /// specified inner handler.
37 | ///
38 | /// The logger to which to log info about requests.
39 | /// The inner handler to call in-between logging.
40 | public LoggingHttpMessageHandler(ILogger logger, HttpMessageHandler innerHandler)
41 | {
42 | this.Logger = logger ?? throw new ArgumentNullException(nameof(logger));
43 | if (innerHandler != null)
44 | {
45 | this.InnerHandler = innerHandler;
46 | }
47 | }
48 |
49 | protected ILogger Logger { get; }
50 |
51 | protected override async Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
52 | {
53 | if (request == null)
54 | {
55 | throw new ArgumentNullException(nameof(request));
56 | }
57 |
58 | Stopwatch stopwatch = Stopwatch.StartNew();
59 |
60 | this.LogRequestStart(request);
61 | HttpResponseMessage response = await base.SendAsync(request, cancellationToken);
62 | this.LogRequestEnd(request, response, stopwatch.Elapsed);
63 |
64 | return response;
65 | }
66 |
67 | ///
68 | /// Called before the request is dispatched to log information about the request.
69 | ///
70 | /// The HTTP request that is about to be dispatched.
71 | protected virtual void LogRequestStart(HttpRequestMessage request)
72 | {
73 | Contract.Requires(request != null);
74 |
75 | this.Logger.LogInformation(
76 | "Sending HTTP request {HttpMethod} {Uri}",
77 | request.Method,
78 | request.RequestUri);
79 | }
80 |
81 | ///
82 | /// Called after the response has been received to log information about the
83 | /// request and response. Not called if an exception is thrown.
84 | ///
85 | /// The HTTP request that was sent to the server.
86 | /// The HTTP response from the server
87 | /// The duration of the request.
88 | protected virtual void LogRequestEnd(HttpRequestMessage request, HttpResponseMessage response, TimeSpan duration)
89 | {
90 | Contract.Requires(response != null);
91 |
92 | this.Logger.LogInformation(
93 | "Received HTTP response after {ElapsedMilliseconds}ms - {StatusCode}",
94 | duration.TotalMilliseconds,
95 | response.StatusCode);
96 | }
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | This project is a standalone HTTP client factory for .NET Core and .NET Standard that follows the best practices recommended by Microsoft for the creation and disposal of `HttpClient` instances.
2 |
3 | ## Motivation
4 |
5 | I wrote this because the .NET `HttpClient` class is great but has some quirks that make it difficult to use properly (see [here](https://docs.microsoft.com/en-us/dotnet/architecture/microservices/implement-resilient-applications/use-httpclientfactory-to-implement-resilient-http-requests) for an official explanation):
6 | 1. For one thing, the class is disposable but when you dispose it, the underlying socket is not immediately released. This means that if you are making a bunch of outgoing connections and you create and dispose it every time you make a call, you can run into socket exhaustion. For more info, see the [this](https://aspnetmonsters.com/2016/08/2016-08-27-httpclientwrong/) excellent blog post. As such, the class is intended to be instantiated once and reused throughout the life of your application.
7 | 2. If you do actually use a singleton `HttpClient` instance, you can run into a second issue where DNS changes aren't respected. By default, the connection close timeout is set to infinite so the client never refreshes its DNS cache so if an entry changes, your client will not update until it is disposed.
8 |
9 | There is a Microsoft official `IHttpClientFactory` in the [Microsoft.Extensions.Http](https://www.nuget.org/packages/Microsoft.Extensions.Http/) NuGet package that solves these issues. However, Microsoft has decided to follow a path with a lot of the .NET Core libraries where things are super tightly tied into ASP.NET Core and its dependency injection framework. If you don't want to use this, there's no way to use the official Microsoft package. ASP.NET Core is often great but I don't like being forced to use it and all the magic that comes with it.
10 |
11 | After doing a bunch of looking around for alternative implementations, I found a GitHub [issue](https://github.com/dotnet/extensions/issues/1345#issuecomment-480548175) that described the best practices for using `HttpClient` in both .NET Core and .NET Standard. However, I couldn't find any alternates so I decided to make my own.
12 |
13 | ## Installation
14 |
15 | You can find the latest release here: https://www.nuget.org/packages/Arnath.StandaloneHttpClientFactory
16 |
17 | ## Usage
18 |
19 | To create and use a simple `HttpClient` instance, just instantiate the factory and use the client in a using statement. The `HttpClient` instance returned by the factory will handle the proper disposal behavior for .NET Standard. The `StandaloneHttpClientFactory` itself is disposable but should only be created at the beginning of your application's lifetime and disposed at the end.
20 |
21 | ```csharp
22 | // This uses a default value of 15 minutes for the pooled connection lifetime
23 | // that was recommended by the .NET foundation.
24 | StandaloneHttpClientFactory factory = new StandaloneHttpClientFactory();
25 | using (HttpClient client = factory.CreateClient())
26 | {
27 | // Do stuff with client.
28 | }
29 |
30 | // The factory implements IDisposable. You should only dispose it at the end
31 | // of your application's lifetime.
32 | factory.Dispose();
33 | ```
34 |
35 | You can pass an `ILogger` instance to the factory to add a `LoggingHttpMessageHandler` to the handler chain that logs some basic information about requests.
36 | ```csharp
37 | // Adds a LoggingHttpMessageHandler to the handler chain.
38 | ILogger logger = new ConcreteLogger();
39 | StandaloneHttpClientFactory factory = new StandaloneHttpClientFactory(logger);
40 | ```
41 |
42 | You can also specify your own set of delegating handlers if desired. They must inherit from the `System.Net.Http.DelegatingHandler` class. For example, if you want to add a correlation ID header to every request, you could do something like this:
43 | ```csharp
44 | public class CorrelationIdMessageHandler : DelegatingHandler
45 | {
46 | protected override Task SendAsync(
47 | HttpRequestMessage request,
48 | CancellationToken cancellationToken)
49 | {
50 | if (request == null)
51 | {
52 | throw new ArgumentNullException(nameof(request));
53 | }
54 |
55 | request.Headers.Add("CorrelationId", Guid.NewGuid().ToString());
56 |
57 | return base.SendAsync(request, cancellationToken);
58 | }
59 | }
60 |
61 | StandaloneHttpClientFactory factory = new StandaloneHttpClientFactory(new CorrelationIdMessageHandler());
62 | ```
63 | You can look at `LoggingHttpMessageHandler` for an example delegating handler. The `LoggingHttpMessageHandler` can also be overridden to change the logging methods and then passed in as a delegating handler.
64 |
--------------------------------------------------------------------------------
/Arnath.StandaloneHttpClientFactory.DotNetFramework.UnitTests/StandaloneHttpClientFactoryTests.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Vijay Prakash. All rights reserved.
2 | // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
3 |
4 | namespace Arnath.StandaloneHttpClientFactory.DotNetFramework.UnitTests
5 | {
6 | using System;
7 | using System.Net.Http;
8 | using System.Reflection;
9 | using FakeItEasy;
10 | using Microsoft.Extensions.Logging.Abstractions;
11 | using Xunit;
12 |
13 | public class StandaloneHttpClientFactoryTests
14 | {
15 | [Fact]
16 | public void CreateClientUsesDefaultsWhenFactoryUsesDefaults()
17 | {
18 | using (StandaloneHttpClientFactory httpClientFactory = new StandaloneHttpClientFactory())
19 | {
20 | using (HttpClient client = httpClientFactory.CreateClient())
21 | {
22 | HttpMessageHandler handler = GetHandler(client);
23 |
24 | Assert.IsType(handler);
25 | Assert.Equal(StandaloneHttpClientFactory.DefaultConnectionLifetime, ((ServicePointHttpMessageHandler)handler).ConnectionLeaseTimeout);
26 | }
27 | }
28 | }
29 |
30 | [Fact]
31 | public void CreateClientUsesProvidedConnectionLifetime()
32 | {
33 | TimeSpan connectionLifetime = TimeSpan.FromMinutes(1);
34 | using (StandaloneHttpClientFactory httpClientFactory = new StandaloneHttpClientFactory(connectionLifetime))
35 | {
36 | using (HttpClient client = httpClientFactory.CreateClient())
37 | {
38 | HttpMessageHandler handler = GetHandler(client);
39 |
40 | Assert.IsType(handler);
41 | Assert.Equal(connectionLifetime, ((ServicePointHttpMessageHandler)handler).ConnectionLeaseTimeout);
42 | }
43 | }
44 | }
45 |
46 | [Fact]
47 | public void CreateClientUsesLoggerToCreateLoggingHandler()
48 | {
49 | using (StandaloneHttpClientFactory httpClientFactory = new StandaloneHttpClientFactory(NullLogger.Instance))
50 | {
51 | using (HttpClient client = httpClientFactory.CreateClient())
52 | {
53 | HttpMessageHandler handler = GetHandler(client);
54 |
55 | Assert.IsType(handler);
56 | LoggingHttpMessageHandler loggingHandler = (LoggingHttpMessageHandler)handler;
57 | Assert.IsType(loggingHandler.InnerHandler);
58 | }
59 | }
60 | }
61 |
62 | [Fact]
63 | public void CreateClientCreatesHandlerPipeline()
64 | {
65 | DelegatingHandler foo = A.Fake(options => options.CallsBaseMethods());
66 | DelegatingHandler bar = A.Fake(options => options.CallsBaseMethods());
67 |
68 | using (StandaloneHttpClientFactory httpClientFactory = new StandaloneHttpClientFactory(foo, bar))
69 | {
70 | using (HttpClient client = httpClientFactory.CreateClient())
71 | {
72 | HttpMessageHandler handler = GetHandler(client);
73 |
74 | Assert.Same(foo, handler);
75 | DelegatingHandler delegatingHandler = (DelegatingHandler)handler;
76 | Assert.Same(bar, delegatingHandler.InnerHandler);
77 | delegatingHandler = (DelegatingHandler)delegatingHandler.InnerHandler;
78 | Assert.IsType(delegatingHandler.InnerHandler);
79 | delegatingHandler = (DelegatingHandler)delegatingHandler.InnerHandler;
80 | Assert.IsType(delegatingHandler.InnerHandler);
81 | }
82 | }
83 | }
84 |
85 | [Fact]
86 | public void CreateClientCreatesSameClient()
87 | {
88 | using (StandaloneHttpClientFactory httpClientFactory = new StandaloneHttpClientFactory())
89 | {
90 | using (HttpClient client1 = httpClientFactory.CreateClient())
91 | {
92 | using (HttpClient client2 = httpClientFactory.CreateClient())
93 | {
94 | HttpMessageHandler handler1 = GetHandler(client1);
95 | HttpMessageHandler handler2 = GetHandler(client2);
96 |
97 | Assert.Same(client1, client2);
98 | Assert.Same(handler1, handler2);
99 | }
100 | }
101 | }
102 | }
103 |
104 | private HttpMessageHandler GetHandler(HttpClient client)
105 | {
106 | FieldInfo handlerField =
107 | typeof(HttpClient).BaseType.GetField(
108 | "handler",
109 | BindingFlags.NonPublic | BindingFlags.Instance);
110 | object o = handlerField.GetValue(client);
111 |
112 | return o as HttpMessageHandler;
113 | }
114 | }
115 | }
116 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ## Ignore Visual Studio temporary files, build results, and
2 | ## files generated by popular Visual Studio add-ons.
3 | ##
4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
5 |
6 | # User-specific files
7 | *.rsuser
8 | *.suo
9 | *.user
10 | *.userosscache
11 | *.sln.docstates
12 |
13 | # User-specific files (MonoDevelop/Xamarin Studio)
14 | *.userprefs
15 |
16 | # Mono auto generated files
17 | mono_crash.*
18 |
19 | # Build results
20 | [Dd]ebug/
21 | [Dd]ebugPublic/
22 | [Rr]elease/
23 | [Rr]eleases/
24 | x64/
25 | x86/
26 | [Aa][Rr][Mm]/
27 | [Aa][Rr][Mm]64/
28 | bld/
29 | [Bb]in/
30 | [Oo]bj/
31 | [Ll]og/
32 | [Ll]ogs/
33 |
34 | # Visual Studio 2015/2017 cache/options directory
35 | .vs/
36 | # Uncomment if you have tasks that create the project's static files in wwwroot
37 | #wwwroot/
38 |
39 | # Visual Studio 2017 auto generated files
40 | Generated\ Files/
41 |
42 | # MSTest test Results
43 | [Tt]est[Rr]esult*/
44 | [Bb]uild[Ll]og.*
45 |
46 | # NUnit
47 | *.VisualState.xml
48 | TestResult.xml
49 | nunit-*.xml
50 |
51 | # Build Results of an ATL Project
52 | [Dd]ebugPS/
53 | [Rr]eleasePS/
54 | dlldata.c
55 |
56 | # Benchmark Results
57 | BenchmarkDotNet.Artifacts/
58 |
59 | # .NET Core
60 | project.lock.json
61 | project.fragment.lock.json
62 | artifacts/
63 |
64 | # StyleCop
65 | StyleCopReport.xml
66 |
67 | # Files built by Visual Studio
68 | *_i.c
69 | *_p.c
70 | *_h.h
71 | *.ilk
72 | *.meta
73 | *.obj
74 | *.iobj
75 | *.pch
76 | *.pdb
77 | *.ipdb
78 | *.pgc
79 | *.pgd
80 | *.rsp
81 | *.sbr
82 | *.tlb
83 | *.tli
84 | *.tlh
85 | *.tmp
86 | *.tmp_proj
87 | *_wpftmp.csproj
88 | *.log
89 | *.vspscc
90 | *.vssscc
91 | .builds
92 | *.pidb
93 | *.svclog
94 | *.scc
95 |
96 | # Chutzpah Test files
97 | _Chutzpah*
98 |
99 | # Visual C++ cache files
100 | ipch/
101 | *.aps
102 | *.ncb
103 | *.opendb
104 | *.opensdf
105 | *.sdf
106 | *.cachefile
107 | *.VC.db
108 | *.VC.VC.opendb
109 |
110 | # Visual Studio profiler
111 | *.psess
112 | *.vsp
113 | *.vspx
114 | *.sap
115 |
116 | # Visual Studio Trace Files
117 | *.e2e
118 |
119 | # TFS 2012 Local Workspace
120 | $tf/
121 |
122 | # Guidance Automation Toolkit
123 | *.gpState
124 |
125 | # ReSharper is a .NET coding add-in
126 | _ReSharper*/
127 | *.[Rr]e[Ss]harper
128 | *.DotSettings.user
129 |
130 | # TeamCity is a build add-in
131 | _TeamCity*
132 |
133 | # DotCover is a Code Coverage Tool
134 | *.dotCover
135 |
136 | # AxoCover is a Code Coverage Tool
137 | .axoCover/*
138 | !.axoCover/settings.json
139 |
140 | # Coverlet is a free, cross platform Code Coverage Tool
141 | coverage*[.json, .xml, .info]
142 |
143 | # Visual Studio code coverage results
144 | *.coverage
145 | *.coveragexml
146 |
147 | # NCrunch
148 | _NCrunch_*
149 | .*crunch*.local.xml
150 | nCrunchTemp_*
151 |
152 | # MightyMoose
153 | *.mm.*
154 | AutoTest.Net/
155 |
156 | # Web workbench (sass)
157 | .sass-cache/
158 |
159 | # Installshield output folder
160 | [Ee]xpress/
161 |
162 | # DocProject is a documentation generator add-in
163 | DocProject/buildhelp/
164 | DocProject/Help/*.HxT
165 | DocProject/Help/*.HxC
166 | DocProject/Help/*.hhc
167 | DocProject/Help/*.hhk
168 | DocProject/Help/*.hhp
169 | DocProject/Help/Html2
170 | DocProject/Help/html
171 |
172 | # Click-Once directory
173 | publish/
174 |
175 | # Publish Web Output
176 | *.[Pp]ublish.xml
177 | *.azurePubxml
178 | # Note: Comment the next line if you want to checkin your web deploy settings,
179 | # but database connection strings (with potential passwords) will be unencrypted
180 | *.pubxml
181 | *.publishproj
182 |
183 | # Microsoft Azure Web App publish settings. Comment the next line if you want to
184 | # checkin your Azure Web App publish settings, but sensitive information contained
185 | # in these scripts will be unencrypted
186 | PublishScripts/
187 |
188 | # NuGet Packages
189 | *.nupkg
190 | # NuGet Symbol Packages
191 | *.snupkg
192 | # The packages folder can be ignored because of Package Restore
193 | **/[Pp]ackages/*
194 | # except build/, which is used as an MSBuild target.
195 | !**/[Pp]ackages/build/
196 | # Uncomment if necessary however generally it will be regenerated when needed
197 | #!**/[Pp]ackages/repositories.config
198 | # NuGet v3's project.json files produces more ignorable files
199 | *.nuget.props
200 | *.nuget.targets
201 |
202 | # Microsoft Azure Build Output
203 | csx/
204 | *.build.csdef
205 |
206 | # Microsoft Azure Emulator
207 | ecf/
208 | rcf/
209 |
210 | # Windows Store app package directories and files
211 | AppPackages/
212 | BundleArtifacts/
213 | Package.StoreAssociation.xml
214 | _pkginfo.txt
215 | *.appx
216 | *.appxbundle
217 | *.appxupload
218 |
219 | # Visual Studio cache files
220 | # files ending in .cache can be ignored
221 | *.[Cc]ache
222 | # but keep track of directories ending in .cache
223 | !?*.[Cc]ache/
224 |
225 | # Others
226 | ClientBin/
227 | ~$*
228 | *~
229 | *.dbmdl
230 | *.dbproj.schemaview
231 | *.jfm
232 | *.pfx
233 | *.publishsettings
234 | orleans.codegen.cs
235 |
236 | # Including strong name files can present a security risk
237 | # (https://github.com/github/gitignore/pull/2483#issue-259490424)
238 | #*.snk
239 |
240 | # Since there are multiple workflows, uncomment next line to ignore bower_components
241 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
242 | #bower_components/
243 |
244 | # RIA/Silverlight projects
245 | Generated_Code/
246 |
247 | # Backup & report files from converting an old project file
248 | # to a newer Visual Studio version. Backup files are not needed,
249 | # because we have git ;-)
250 | _UpgradeReport_Files/
251 | Backup*/
252 | UpgradeLog*.XML
253 | UpgradeLog*.htm
254 | ServiceFabricBackup/
255 | *.rptproj.bak
256 |
257 | # SQL Server files
258 | *.mdf
259 | *.ldf
260 | *.ndf
261 |
262 | # Business Intelligence projects
263 | *.rdl.data
264 | *.bim.layout
265 | *.bim_*.settings
266 | *.rptproj.rsuser
267 | *- [Bb]ackup.rdl
268 | *- [Bb]ackup ([0-9]).rdl
269 | *- [Bb]ackup ([0-9][0-9]).rdl
270 |
271 | # Microsoft Fakes
272 | FakesAssemblies/
273 |
274 | # GhostDoc plugin setting file
275 | *.GhostDoc.xml
276 |
277 | # Node.js Tools for Visual Studio
278 | .ntvs_analysis.dat
279 | node_modules/
280 |
281 | # Visual Studio 6 build log
282 | *.plg
283 |
284 | # Visual Studio 6 workspace options file
285 | *.opt
286 |
287 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
288 | *.vbw
289 |
290 | # Visual Studio LightSwitch build output
291 | **/*.HTMLClient/GeneratedArtifacts
292 | **/*.DesktopClient/GeneratedArtifacts
293 | **/*.DesktopClient/ModelManifest.xml
294 | **/*.Server/GeneratedArtifacts
295 | **/*.Server/ModelManifest.xml
296 | _Pvt_Extensions
297 |
298 | # Paket dependency manager
299 | .paket/paket.exe
300 | paket-files/
301 |
302 | # FAKE - F# Make
303 | .fake/
304 |
305 | # CodeRush personal settings
306 | .cr/personal
307 |
308 | # Python Tools for Visual Studio (PTVS)
309 | __pycache__/
310 | *.pyc
311 |
312 | # Cake - Uncomment if you are using it
313 | # tools/**
314 | # !tools/packages.config
315 |
316 | # Tabs Studio
317 | *.tss
318 |
319 | # Telerik's JustMock configuration file
320 | *.jmconfig
321 |
322 | # BizTalk build output
323 | *.btp.cs
324 | *.btm.cs
325 | *.odx.cs
326 | *.xsd.cs
327 |
328 | # OpenCover UI analysis results
329 | OpenCover/
330 |
331 | # Azure Stream Analytics local run output
332 | ASALocalRun/
333 |
334 | # MSBuild Binary and Structured Log
335 | *.binlog
336 |
337 | # NVidia Nsight GPU debugger configuration file
338 | *.nvuser
339 |
340 | # MFractors (Xamarin productivity tool) working folder
341 | .mfractor/
342 |
343 | # Local History for Visual Studio
344 | .localhistory/
345 |
346 | # BeatPulse healthcheck temp database
347 | healthchecksdb
348 |
349 | # Backup folder for Package Reference Convert tool in Visual Studio 2017
350 | MigrationBackup/
351 |
352 | # Ionide (cross platform F# VS Code tools) working folder
353 | .ionide/
354 |
355 | Drop/
356 |
--------------------------------------------------------------------------------
/Arnath.StandaloneHttpClientFactory.DotNetFramework.UnitTests/Arnath.StandaloneHttpClientFactory.DotNetFramework.UnitTests.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | Debug
8 | AnyCPU
9 | {A9E5FBB7-F684-4D20-A79A-0E3DDC586959}
10 | Library
11 | Properties
12 | Arnath.StandaloneHttpClientFactory.DotNetFramework.UnitTests
13 | Arnath.StandaloneHttpClientFactory.DotNetFramework.UnitTests
14 | v4.8
15 | 512
16 | {3AC096D0-A1C2-E12C-1390-A8335801FDAB};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}
17 | 15.0
18 | $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)
19 | $(ProgramFiles)\Common Files\microsoft shared\VSTT\$(VisualStudioVersion)\UITestExtensionPackages
20 | False
21 | UnitTest
22 |
23 |
24 |
25 |
26 |
27 | true
28 | full
29 | false
30 | bin\Debug\
31 | DEBUG;TRACE
32 | prompt
33 | 4
34 |
35 |
36 | pdbonly
37 | true
38 | bin\Release\
39 | TRACE
40 | prompt
41 | 4
42 |
43 |
44 |
45 | ..\packages\Castle.Core.5.1.0\lib\net462\Castle.Core.dll
46 |
47 |
48 | ..\packages\FakeItEasy.7.3.1\lib\net45\FakeItEasy.dll
49 |
50 |
51 | ..\packages\Microsoft.Extensions.Logging.Abstractions.6.0.2\lib\net461\Microsoft.Extensions.Logging.Abstractions.dll
52 |
53 |
54 |
55 | ..\packages\System.Buffers.4.5.1\lib\net461\System.Buffers.dll
56 |
57 |
58 |
59 |
60 | ..\packages\System.Memory.4.5.5\lib\net461\System.Memory.dll
61 |
62 |
63 | ..\..\..\..\..\..\Program Files (x86)\Reference Assemblies\Microsoft\Framework\.NETFramework\v4.7.2\System.Net.Http.dll
64 |
65 |
66 |
67 | ..\packages\System.Numerics.Vectors.4.5.0\lib\net46\System.Numerics.Vectors.dll
68 |
69 |
70 | ..\packages\System.Runtime.CompilerServices.Unsafe.6.0.0\lib\net461\System.Runtime.CompilerServices.Unsafe.dll
71 |
72 |
73 | ..\packages\xunit.abstractions.2.0.3\lib\net35\xunit.abstractions.dll
74 |
75 |
76 | ..\packages\xunit.assert.2.4.2\lib\netstandard1.1\xunit.assert.dll
77 |
78 |
79 | ..\packages\xunit.extensibility.core.2.4.2\lib\net452\xunit.core.dll
80 |
81 |
82 | ..\packages\xunit.extensibility.execution.2.4.2\lib\net452\xunit.execution.desktop.dll
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 | {ef792056-91f8-4832-911f-68ec6992a611}
96 | Arnath.StandaloneHttpClientFactory
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 | This project references NuGet package(s) that are missing on this computer. Use NuGet Package Restore to download them. For more information, see http://go.microsoft.com/fwlink/?LinkID=322105. The missing file is {0}.
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
--------------------------------------------------------------------------------
/CodeAnalysis.ruleset:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
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 |
110 |
111 |
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 |
135 |
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 |
160 |
161 |
162 |
163 |
164 |
165 |
166 |
167 |
168 |
169 |
170 |
171 |
172 |
173 |
174 |
175 |
176 |
177 |
178 |
179 |
180 |
181 |
182 |
183 |
184 |
185 |
186 |
187 |
188 |
189 |
190 |
191 |
192 |
193 |
194 |
195 |
196 |
197 |
198 |
199 |
200 |
201 |
202 |
203 |
204 |
205 |
206 |
207 |
208 |
209 |
210 |
211 |
212 |
213 |
214 |
215 |
216 |
217 |
218 |
--------------------------------------------------------------------------------
/Arnath.StandaloneHttpClientFactory/StandaloneHttpClientFactory.cs:
--------------------------------------------------------------------------------
1 | // Copyright (c) Vijay Prakash. All rights reserved.
2 | // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information.
3 |
4 | namespace Arnath.StandaloneHttpClientFactory
5 | {
6 | using System;
7 | using System.Net.Http;
8 | using System.Threading;
9 | using Microsoft.Extensions.Logging;
10 |
11 | ///
12 | /// Factory for instances that follows the recommended
13 | /// best practices for creation and disposal. See
14 | /// for more details.
15 | ///
16 | public class StandaloneHttpClientFactory : IHttpClientFactory, IDisposable
17 | {
18 | internal static readonly TimeSpan DefaultConnectionLifetime = TimeSpan.FromMinutes(15);
19 |
20 | private readonly IHttpClientFactory frameworkSpecificFactory;
21 |
22 | ///
23 | /// Creates a new instance of the StandaloneHttpClientFactory class with the
24 | /// default value of 15 mins for pooled connection lifetime and no delegating
25 | /// handlers.
26 | ///
27 | public StandaloneHttpClientFactory()
28 | : this(DefaultConnectionLifetime, delegatingHandlers: null)
29 | {
30 | }
31 |
32 | ///
33 | /// Creates an instance of the StandaloneHttpClientFactory class with the
34 | /// default value of 15 mins for pooled connection lifetime and a single
35 | /// delegating handler that uses the provided ILogger to create a
36 | /// LoggingHttpMessageHandler that logs some info about requests.
37 | ///
38 | /// The logger to which to log info about requests.
39 | public StandaloneHttpClientFactory(ILogger logger)
40 | : this(DefaultConnectionLifetime, logger)
41 | {
42 | }
43 |
44 | ///
45 | /// Creates an instance of the StandaloneHttpClientFactory class with the
46 | /// specified value for pooled connection lifetime and a single
47 | /// delegating handler that uses the provided ILogger to create a
48 | /// LoggingHttpMessageHandler that logs some info about requests.
49 | ///
50 | /// The lifetime of connections to each host.
51 | /// The logger to which to log info about requests.
52 | public StandaloneHttpClientFactory(TimeSpan connectionLifetime, ILogger logger)
53 | : this(connectionLifetime, new LoggingHttpMessageHandler(logger))
54 | {
55 | }
56 |
57 | ///
58 | /// Creates a new instance of the StandaloneHttpClientFactory class with the
59 | /// default value of 15 mins for pooled connection lifetime and the specified
60 | /// set of delegating handlers. The delegating handlers will be run in order.
61 | ///
62 | /// Array of DelegatingHandler instances that can be
63 | /// used for logging, etc. See LoggingHttpMessageHandler for an example.
64 | public StandaloneHttpClientFactory(params DelegatingHandler[] delegatingHandlers)
65 | : this(DefaultConnectionLifetime, delegatingHandlers)
66 | {
67 | }
68 |
69 | ///
70 | /// Creates an instance of the StandaloneHttpClientFactory class with the
71 | /// specified value for pooled connection lifetime and the specified
72 | /// set of delegating handlers. The delegating handlers will be run in order.
73 | ///
74 | /// The lifetime of connections to each host.
75 | /// Array of DelegatingHandler instances that can be
76 | /// used for logging, etc. See LoggingHttpMessageHandler for an example.
77 | public StandaloneHttpClientFactory(TimeSpan connectionLifetime, params DelegatingHandler[] delegatingHandlers)
78 | {
79 | #if NETCOREAPP
80 | this.frameworkSpecificFactory = new DotNetCoreHttpClientFactory(connectionLifetime, delegatingHandlers);
81 | #else
82 | this.frameworkSpecificFactory = new DotNetStandardHttpClientFactory(connectionLifetime, delegatingHandlers);
83 | #endif
84 | }
85 |
86 | ///
87 | /// Creates an HTTP client instance with the factory's pooled connection lifetime
88 | /// and delegating handlers.
89 | ///
90 | /// The HTTP client instance. This can be disposed freely; the instances
91 | /// returned by the factory will handle doing the right thing.
92 | public HttpClient CreateClient()
93 | {
94 | return this.frameworkSpecificFactory.CreateClient();
95 | }
96 |
97 | ///
98 | /// Releases the resources used by the StandaloneHttpClientFactory. Does nothing
99 | /// when called from .NET Core.
100 | ///
101 | public void Dispose()
102 | {
103 | this.Dispose(true);
104 | GC.SuppressFinalize(this);
105 | }
106 |
107 | ///
108 | /// Linsk together a set of DelegatingHandler instances with the framework specific
109 | /// handler to generate a handler pipeline that can be used to do things like log
110 | /// request and response information. The delegating handlers will be run in order.
111 | ///
112 | private static HttpMessageHandler CreateHandlerPipeline(
113 | HttpMessageHandler handler,
114 | params DelegatingHandler[] delegatingHandlers)
115 | {
116 | HttpMessageHandler next = handler;
117 | if (delegatingHandlers != null)
118 | {
119 | for (int i = delegatingHandlers.Length - 1; i >= 0; i--)
120 | {
121 | delegatingHandlers[i].InnerHandler = next;
122 | next = delegatingHandlers[i];
123 | }
124 | }
125 |
126 | return next;
127 | }
128 |
129 | protected virtual void Dispose(bool disposing)
130 | {
131 | if (disposing)
132 | {
133 | #if !NETCOREAPP
134 | if (this.frameworkSpecificFactory is DotNetStandardHttpClientFactory standardClientFactory)
135 | {
136 | standardClientFactory.Dispose();
137 | }
138 | #endif
139 | }
140 | }
141 |
142 | #if NETCOREAPP
143 | ///
144 | /// .NET Core version of the HTTP client factory. This uses a single
145 | /// shared instance of SocketsHttpHandler with a pooled connection
146 | /// lifetime set and generates new HttpClient instances on every request.
147 | ///
148 | private class DotNetCoreHttpClientFactory : IHttpClientFactory
149 | {
150 | private readonly Lazy lazyHandler;
151 |
152 | internal DotNetCoreHttpClientFactory(
153 | TimeSpan pooledConnectionLifetime,
154 | params DelegatingHandler[] delegatingHandlers)
155 | {
156 | this.lazyHandler = new Lazy(
157 | () => CreateLazyHandler(pooledConnectionLifetime, delegatingHandlers),
158 | LazyThreadSafetyMode.ExecutionAndPublication);
159 | }
160 |
161 | public HttpClient CreateClient()
162 | {
163 | return new HttpClient(this.lazyHandler.Value, disposeHandler: false);
164 | }
165 |
166 | private static HttpMessageHandler CreateLazyHandler(
167 | TimeSpan pooledConnectionLifetime,
168 | params DelegatingHandler[] delegatingHandlers)
169 | {
170 | SocketsHttpHandler handler = new SocketsHttpHandler();
171 | handler.PooledConnectionLifetime = pooledConnectionLifetime;
172 |
173 | return CreateHandlerPipeline(handler, delegatingHandlers);
174 | }
175 | }
176 | #else
177 | ///
178 | /// .NET Standard version of the HTTP client factory. This uses a single
179 | /// shared instance of HttpClient that does nothing when disposed. Every
180 | /// call to CreateClient returns this same instance.
181 | ///
182 | private class DotNetStandardHttpClientFactory : IHttpClientFactory
183 | {
184 | private readonly Lazy lazyClient;
185 |
186 | internal DotNetStandardHttpClientFactory(
187 | TimeSpan connectionLifetime,
188 | params DelegatingHandler[] delegatingHandlers)
189 | {
190 | this.lazyClient = new Lazy(
191 | () => CreateLazyClient(connectionLifetime, delegatingHandlers),
192 | LazyThreadSafetyMode.ExecutionAndPublication);
193 | }
194 |
195 | public HttpClient CreateClient()
196 | {
197 | return this.lazyClient.Value;
198 | }
199 |
200 | internal void Dispose()
201 | {
202 | if (this.lazyClient.IsValueCreated)
203 | {
204 | this.lazyClient.Value.DoDispose();
205 | }
206 | }
207 |
208 | private static NonDisposableHttpClient CreateLazyClient(
209 | TimeSpan connectionLifetime,
210 | params DelegatingHandler[] delegatingHandlers)
211 | {
212 | #pragma warning disable CA2000 // Dispose objects before losing scope
213 | // This handler object is disposed by the HttpClient instance
214 | // when the DotNetStandardHttpClientFactory is disposed.
215 | ServicePointHttpMessageHandler handler =
216 | new ServicePointHttpMessageHandler(
217 | connectionLifetime,
218 | new HttpClientHandler());
219 | #pragma warning restore CA2000 // Dispose objects before losing scope
220 |
221 | return new NonDisposableHttpClient(
222 | CreateHandlerPipeline(
223 | handler,
224 | delegatingHandlers));
225 | }
226 | }
227 |
228 | ///
229 | /// Non-disposable HTTP client wrapper that is used to stop clients from
230 | /// accidentally disposing the shared HTTP client instance. To actually
231 | /// dispose, call DoDispose().
232 | ///
233 | private class NonDisposableHttpClient : HttpClient
234 | {
235 | public NonDisposableHttpClient(HttpMessageHandler handler)
236 | : base(handler)
237 | {
238 | }
239 |
240 | protected override void Dispose(bool disposing)
241 | {
242 | // Don't do anything here because we don't want the singleton client instance
243 | // to be disposed.
244 | }
245 |
246 | internal void DoDispose()
247 | {
248 | base.Dispose(true);
249 | }
250 | }
251 | #endif
252 | }
253 | }
254 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright 2020 Vijay Prakash
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
--------------------------------------------------------------------------------