├── .gitattributes
├── .gitignore
├── HttpClientFactory.sln
├── MIT-LICENSE.txt
├── README.md
├── src
├── HttpClientFactory.csproj
├── IHttpClient.cs
├── IHttpClientFactory.cs
└── Impl
│ ├── HttpClientFactoryBase.cs
│ ├── HttpSettings.cs
│ ├── PerHostHttpClientFactory.cs
│ ├── PerUrlHttpClientFactory.cs
│ └── SafeHttpClient.cs
└── test
└── UnitTest
├── PerHostHttpClientFactoryTests.cs
├── PerUrlHttpClientFactoryTests.cs
├── UnitTest.csproj
└── UnitTest1.cs
/.gitattributes:
--------------------------------------------------------------------------------
1 | ###############################################################################
2 | # Set default behavior to automatically normalize line endings.
3 | ###############################################################################
4 | * text=auto
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | bin
2 | obj
3 | packages
4 | *.suo
5 | *.nupkg
6 | *.DotSettings.user
7 | *.xproj.user
8 | .vs
9 | *.lock.json
10 | *.log
11 | .vscode
12 | publish
13 | TestResult.xml
14 | *.bak
15 | .idea
16 | src/bin
17 | _ReSharper.Caches/
18 |
--------------------------------------------------------------------------------
/HttpClientFactory.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | # Visual Studio 15
4 | VisualStudioVersion = 15.0.28307.168
5 | MinimumVisualStudioVersion = 10.0.40219.1
6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "HttpClientFactory", "src\HttpClientFactory.csproj", "{75A6862A-78BC-4473-B555-EA88C2F9F044}"
7 | EndProject
8 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "UnitTest", "test\UnitTest\UnitTest.csproj", "{41D74C0E-7C61-4553-A603-CFDC57F12A96}"
9 | EndProject
10 | Global
11 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
12 | Debug|Any CPU = Debug|Any CPU
13 | Release|Any CPU = Release|Any CPU
14 | EndGlobalSection
15 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
16 | {75A6862A-78BC-4473-B555-EA88C2F9F044}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
17 | {75A6862A-78BC-4473-B555-EA88C2F9F044}.Debug|Any CPU.Build.0 = Debug|Any CPU
18 | {75A6862A-78BC-4473-B555-EA88C2F9F044}.Release|Any CPU.ActiveCfg = Release|Any CPU
19 | {75A6862A-78BC-4473-B555-EA88C2F9F044}.Release|Any CPU.Build.0 = Release|Any CPU
20 | {41D74C0E-7C61-4553-A603-CFDC57F12A96}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
21 | {41D74C0E-7C61-4553-A603-CFDC57F12A96}.Debug|Any CPU.Build.0 = Debug|Any CPU
22 | {41D74C0E-7C61-4553-A603-CFDC57F12A96}.Release|Any CPU.ActiveCfg = Release|Any CPU
23 | {41D74C0E-7C61-4553-A603-CFDC57F12A96}.Release|Any CPU.Build.0 = Release|Any CPU
24 | EndGlobalSection
25 | GlobalSection(SolutionProperties) = preSolution
26 | HideSolutionNode = FALSE
27 | EndGlobalSection
28 | GlobalSection(ExtensibilityGlobals) = postSolution
29 | SolutionGuid = {51213CA1-0A4C-4821-A426-BE1243A2CB83}
30 | EndGlobalSection
31 | EndGlobal
32 |
--------------------------------------------------------------------------------
/MIT-LICENSE.txt:
--------------------------------------------------------------------------------
1 | Permission is hereby granted, free of charge, to any person obtaining a copy
2 | of this software and associated documentation files (the "Software"), to deal
3 | in the Software without restriction, including without limitation the rights
4 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
5 | copies of the Software, and to permit persons to whom the Software is
6 | furnished to do so, subject to the following conditions:
7 |
8 | The above copyright notice and this permission notice shall be included in
9 | all copies or substantial portions of the Software.
10 |
11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
12 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
13 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
14 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
15 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
16 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
17 | THE SOFTWARE.
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # HttpClientFactory For netcore And framework
2 | ### NUGET Install-Package HttpClientFactory
3 |
4 | supported netcore2.0 and framework4.5+
5 |
6 | ## Why need HttpClientFactory?
7 |
8 | As you know, HttpClient has a [trap](https://aspnetmonsters.com/2016/08/2016-08-27-httpclientwrong/) for using!
9 |
10 | 1. if not Singleton,
11 | each instance of HttpClient will open a new socket connection
12 | and on high traffic sites you can exhaust the available pool and receive a System.Net.Sockets.SocketException
13 | 2. if Singleton,HttpClient doesn't respect DNS changes!
14 |
15 |
16 | ## So how to use HttpClient correctly?
17 |
18 | ```
19 | //useage 1: same Host use same HttpClient
20 | PerHostHttpClientFactory perHostHttpClientFactory = new PerHostHttpClientFactory();// can be static
21 | //or you can change default timeout for perFactory
22 | PerHostHttpClientFactory perHostHttpClientFactory = new PerHostHttpClientFactory(TimeSpan.FromSeconds(10));
23 | HttpClient client = perHostHttpClientFactory.GetHttpClient("http://www.baidu.com");
24 |
25 | //useage 2: per url use per HttpClient
26 | PerUrlHttpClientFactory perUrlHttpClientFactory = new PerUrlHttpClientFactory();
27 | //or you can change default timeout for perFactory
28 | PerUrlHttpClientFactory perUrlHttpClientFactory = new PerUrlHttpClientFactory(TimeSpan.FromSeconds(10));
29 | HttpClient client = perUrlHttpClientFactory.GetHttpClient("http://www.baidu.com");
30 |
31 | //useage 3: per proxy use per HttpClient
32 | PerHostHttpClientFactory perHostHttpClientFactory = new PerHostHttpClientFactory();
33 | //or you can change default timeout for perFactory
34 | PerHostHttpClientFactory perHostHttpClientFactory = new PerHostHttpClientFactory(TimeSpan.FromSeconds(10));
35 | HttpClient client = perUrlHttpClientFactory.GetProxiedHttpClient("http://127.0.0.1:8080");
36 | ```
37 |
38 | ## Easy to implement
39 | ```
40 | public class XXXHttpClientFactory : HttpClientFactoryBase
41 | {
42 | protected override string GetCacheKey(string key)
43 | {
44 | return key;
45 | }
46 |
47 |
48 | protected override HttpClient CreateHttpClient(HttpMessageHandler handler)
49 | {
50 | return new HttpClient(handler)
51 | {
52 | Timeout = TimeSpan.FromSeconds(20),
53 | };
54 | }
55 |
56 | protected override HttpMessageHandler CreateMessageHandler()
57 | {
58 | var handler = new HttpClientHandler();
59 | handler.Proxy = new WebProxy("xxx");
60 | return handler;
61 | }
62 | }
63 | ```
64 |
65 | ## Remark
66 | 1. Default Timeout is TimeSpan.FromSeconds(100)
67 | 2. Default [ConnectionLeaseExpired](http://byterot.blogspot.com/2016/07/singleton-httpclient-dns.html) is TimeSpan.FromMinutes(1)
68 |
--------------------------------------------------------------------------------
/src/HttpClientFactory.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net45;netstandard2.0
5 | true
6 | yuzd
7 | safe HttpClient for netcore and netframework
8 | zdyu
9 | https://github.com/yuzd/HttpClientFactory
10 | 1.0.5
11 | Allow setting default HttpClient timeout per factory
12 | MIT
13 | False
14 |
15 |
16 |
17 | portable
18 | TRACE;DEBUG
19 |
20 |
21 |
22 | RELEASE
23 |
24 |
25 |
26 | $(DefineConstants);NETCORE
27 |
28 |
29 |
30 | TRACE;DEBUG;NET45
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
--------------------------------------------------------------------------------
/src/IHttpClient.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Net.Http;
3 |
4 | namespace HttpClientFactory
5 | {
6 | public interface IHttpClient : IDisposable
7 | {
8 | HttpClient HttpClient { get; }
9 |
10 | HttpMessageHandler HttpMessageHandler { get; }
11 |
12 | string BaseUrl { get; set; }
13 | bool IsDisposed { get; }
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/src/IHttpClientFactory.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Net.Http;
3 |
4 | namespace HttpClientFactory
5 | {
6 | public interface IHttpClientFactory : IDisposable
7 | {
8 | HttpClient GetHttpClient(string url);
9 |
10 | HttpClient GetProxiedHttpClient(string proxyUrl);
11 |
12 | }
13 |
14 | }
15 |
--------------------------------------------------------------------------------
/src/Impl/HttpClientFactoryBase.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Concurrent;
3 | using System.Net;
4 | using System.Net.Http;
5 |
6 | namespace HttpClientFactory.Impl
7 | {
8 | public abstract class HttpClientFactoryBase : IHttpClientFactory
9 | {
10 | private readonly ConcurrentDictionary _clients = new ConcurrentDictionary();
11 | private readonly TimeSpan _defaultClientTimeout = TimeSpan.FromSeconds(100);// same as HttpClient default value
12 |
13 | protected HttpClientFactoryBase()
14 | {
15 | }
16 | protected HttpClientFactoryBase(TimeSpan defaultClientTimeout)
17 | {
18 | _defaultClientTimeout = defaultClientTimeout;
19 | }
20 |
21 | public virtual HttpClient GetHttpClient(string url)
22 | {
23 | if (string.IsNullOrEmpty(url))
24 | throw new ArgumentNullException(nameof(url));
25 |
26 | var safeClient = _clients.AddOrUpdate(
27 | GetCacheKey(url),
28 | Create,
29 | (u, client) => client.IsDisposed ? Create(u) : client);
30 |
31 | return safeClient.HttpClient;
32 | }
33 |
34 | public virtual HttpClient GetProxiedHttpClient(string proxyUrl)
35 | {
36 | if (string.IsNullOrEmpty(proxyUrl))
37 | throw new ArgumentNullException(nameof(proxyUrl));
38 |
39 | var safeClient = _clients.AddOrUpdate(
40 | proxyUrl,
41 | CreateProxied,
42 | (u, client) => client.IsDisposed ? CreateProxied(u) : client);
43 |
44 | return safeClient.HttpClient;
45 | }
46 |
47 | public void Dispose()
48 | {
49 | foreach (var kv in _clients)
50 | {
51 | if (!kv.Value.IsDisposed)
52 | kv.Value.Dispose();
53 | }
54 | _clients.Clear();
55 | }
56 |
57 |
58 | protected abstract string GetCacheKey(string url);
59 |
60 | protected virtual IHttpClient Create(string url) => new SafeHttpClient(this,url);
61 | protected virtual IHttpClient CreateProxied(string proxyUrl) => new SafeHttpClient(this, proxyUrl, true);
62 |
63 |
64 | internal HttpClient CreateHttpClientInternal(HttpMessageHandler handler)
65 | {
66 | return CreateHttpClient(handler);
67 | }
68 |
69 | internal HttpMessageHandler CreateMessageHandlerInternal(string proxyUrl = null)
70 | {
71 | return CreateMessageHandler(proxyUrl);
72 | }
73 |
74 | protected virtual HttpClient CreateHttpClient(HttpMessageHandler handler)
75 | {
76 | return new HttpClient(handler)
77 | {
78 | Timeout = _defaultClientTimeout
79 | };
80 | }
81 |
82 | protected virtual HttpMessageHandler CreateMessageHandler(string proxyUrl = null)
83 | {
84 | if (!string.IsNullOrEmpty(proxyUrl))
85 | {
86 | return new HttpClientHandler
87 | {
88 | UseProxy = true,
89 | Proxy = new WebProxy(proxyUrl),
90 | AutomaticDecompression = DecompressionMethods.None
91 | };
92 | }
93 | return new HttpClientHandler
94 | {
95 | UseProxy = false,
96 | AutomaticDecompression = DecompressionMethods.None
97 | };
98 | }
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/src/Impl/HttpSettings.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Net.Http;
3 |
4 | namespace HttpClientFactory.Impl
5 | {
6 |
7 |
8 | #if NET45
9 | [Serializable]
10 | #endif
11 | public class HttpRunTimeSeetings : IDisposable
12 | {
13 | public HttpRunTimeSeetings()
14 | {
15 | SetCurrentTest(this);
16 | }
17 |
18 | public void Dispose()
19 | {
20 | SetCurrentTest(null);
21 | }
22 |
23 | public static HttpRunTimeSeetings Current => GetCurrentTest();
24 |
25 | public TimeSpan? Timeout { get; set; }
26 | public TimeSpan? ConnectionLeaseTimeout { get; set; }
27 |
28 | public HttpMessageHandler HttpMessageHandler { get; set; }
29 |
30 |
31 | #if NET45
32 | private static void SetCurrentTest(HttpRunTimeSeetings runTimeSeetings) => System.Runtime.Remoting.Messaging.CallContext.LogicalSetData("HttpRunTimeSeetings", runTimeSeetings);
33 | private static HttpRunTimeSeetings GetCurrentTest() => System.Runtime.Remoting.Messaging.CallContext.LogicalGetData("HttpRunTimeSeetings") as HttpRunTimeSeetings;
34 | #else
35 | private static readonly System.Threading.AsyncLocal _test = new System.Threading.AsyncLocal();
36 | private static void SetCurrentTest(HttpRunTimeSeetings test) => _test.Value = test;
37 | private static HttpRunTimeSeetings GetCurrentTest() => _test.Value;
38 | #endif
39 |
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/src/Impl/PerHostHttpClientFactory.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace HttpClientFactory.Impl
4 | {
5 | ///
6 | /// same Host use same HttpClient
7 | ///
8 | public class PerHostHttpClientFactory : HttpClientFactoryBase
9 | {
10 | public PerHostHttpClientFactory()
11 | {
12 | }
13 |
14 | public PerHostHttpClientFactory(TimeSpan defaultClientTimeout) : base(defaultClientTimeout)
15 | {
16 | }
17 |
18 | protected override string GetCacheKey(string url)
19 | {
20 | return new Uri(url).Host;
21 | }
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/src/Impl/PerUrlHttpClientFactory.cs:
--------------------------------------------------------------------------------
1 | using System;
2 |
3 | namespace HttpClientFactory.Impl
4 | {
5 | ///
6 | /// same url use same HttpClient
7 | ///
8 | public class PerUrlHttpClientFactory : HttpClientFactoryBase
9 | {
10 | public PerUrlHttpClientFactory()
11 | {
12 | }
13 |
14 | public PerUrlHttpClientFactory(TimeSpan defaultClientTimeout) : base(defaultClientTimeout)
15 | {
16 | }
17 |
18 | protected override string GetCacheKey(string url)
19 | {
20 | return url;
21 | }
22 |
23 | protected override IHttpClient Create(string url)
24 | {
25 | return new SafeHttpClient(this, url);
26 | }
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/src/Impl/SafeHttpClient.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Net.Http;
3 |
4 | namespace HttpClientFactory.Impl
5 | {
6 | internal class SafeHttpClient : IHttpClient
7 | {
8 | private readonly TimeSpan? _connectionLeaseTimeout;
9 |
10 | private readonly HttpClientFactoryBase _baseFactory;
11 |
12 | private Lazy _httpClient;
13 | private Lazy _httpMessageHandler;
14 |
15 |
16 | private DateTime? _clientCreatedAt;
17 | private HttpClient _zombieClient;
18 |
19 | private readonly object _connectionLeaseLock = new object();
20 |
21 | public HttpClient HttpClient => GetHttpClient();
22 |
23 | public HttpMessageHandler HttpMessageHandler => HttpRunTimeSeetings.Current?.HttpMessageHandler ?? _httpMessageHandler?.Value;
24 | public string BaseUrl { get; set; }
25 | public bool IsProxy { get; set; }
26 |
27 | public SafeHttpClient(HttpClientFactoryBase baseFactory,string baseUrl = null,bool isProxy = false)
28 | {
29 | _baseFactory = baseFactory;
30 | BaseUrl = baseUrl;
31 | IsProxy = isProxy;
32 | _connectionLeaseTimeout = HttpRunTimeSeetings.Current?.ConnectionLeaseTimeout ?? TimeSpan.FromMinutes(1);
33 | _httpClient = new Lazy(CreateHttpClient);
34 | _httpMessageHandler = new Lazy(()=>baseFactory.CreateMessageHandlerInternal(isProxy?baseUrl:null));
35 | }
36 |
37 | public bool IsDisposed { get; private set; }
38 |
39 | public virtual void Dispose()
40 | {
41 | if (IsDisposed)
42 | return;
43 |
44 | if (_httpMessageHandler?.IsValueCreated == true)
45 | _httpMessageHandler.Value.Dispose();
46 | if (_httpClient?.IsValueCreated == true)
47 | _httpClient.Value.Dispose();
48 |
49 | IsDisposed = true;
50 | }
51 |
52 | private HttpClient GetHttpClient()
53 | {
54 | if (ConnectionLeaseExpired())
55 | {
56 | lock (_connectionLeaseLock)
57 | {
58 | if (ConnectionLeaseExpired())
59 | {
60 | _zombieClient?.Dispose();
61 | _zombieClient = _httpClient.Value;
62 | _httpClient = new Lazy(CreateHttpClient);
63 | _httpMessageHandler = new Lazy(()=>_baseFactory.CreateMessageHandlerInternal(IsProxy ? BaseUrl : null));
64 | _clientCreatedAt = DateTime.UtcNow;
65 | }
66 | }
67 | }
68 | return _httpClient.Value;
69 | }
70 |
71 | private HttpClient CreateHttpClient()
72 | {
73 | var cli = _baseFactory.CreateHttpClientInternal(HttpMessageHandler);
74 | _clientCreatedAt = DateTime.UtcNow;
75 | return cli;
76 | }
77 |
78 | private bool ConnectionLeaseExpired()
79 | {
80 | // for thread safety, capture these to temp variables
81 | var createdAt = _clientCreatedAt;
82 | var timeout = _connectionLeaseTimeout;
83 |
84 | return
85 | _httpClient.IsValueCreated &&
86 | createdAt.HasValue &&
87 | timeout.HasValue &&
88 | DateTime.UtcNow - createdAt.Value > timeout.Value;
89 | }
90 |
91 |
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/test/UnitTest/PerHostHttpClientFactoryTests.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using HttpClientFactory.Impl;
3 | using Xunit;
4 |
5 | namespace UnitTest
6 | {
7 | public class PerHostHttpClientFactoryTests
8 | {
9 | [Fact]
10 | public void Should_Set_Client_Timeout_To_Default()
11 | {
12 |
13 | var factory = new PerHostHttpClientFactory();
14 | var client = factory.GetHttpClient("http://www.baidu.com");
15 | Assert.Equal(client.Timeout, TimeSpan.FromSeconds(100));
16 | }
17 |
18 | [Fact]
19 | public void Should_Set_Client_Timeout()
20 | {
21 |
22 | var factory = new PerHostHttpClientFactory(TimeSpan.FromMinutes(15));
23 | var client = factory.GetHttpClient("http://www.baidu.com");
24 | Assert.Equal(client.Timeout, TimeSpan.FromMinutes(15));
25 | }
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/test/UnitTest/PerUrlHttpClientFactoryTests.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Net;
3 | using System.Threading.Tasks;
4 | using HttpClientFactory.Impl;
5 | using Xunit;
6 |
7 | namespace UnitTest
8 | {
9 | public class PerUrlHttpClientFactoryTests
10 | {
11 | [Fact]
12 | public void Should_Set_Client_Timeout_To_Default()
13 | {
14 |
15 | var factory = new PerUrlHttpClientFactory();
16 | var client = factory.GetHttpClient("http://www.baidu.com");
17 | Assert.Equal(client.Timeout, TimeSpan.FromSeconds(100));
18 | }
19 |
20 | [Fact]
21 | public void Should_Set_Client_Timeout()
22 | {
23 |
24 | var factory = new PerUrlHttpClientFactory(TimeSpan.FromMinutes(15));
25 | var client = factory.GetHttpClient("http://www.baidu.com");
26 | Assert.Equal(client.Timeout, TimeSpan.FromMinutes(15));
27 | }
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/test/UnitTest/UnitTest.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | netcoreapp2.1
5 |
6 | false
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/test/UnitTest/UnitTest1.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Net;
3 | using System.Threading.Tasks;
4 | using HttpClientFactory.Impl;
5 | using Xunit;
6 |
7 | namespace UnitTest
8 | {
9 | public class UnitTest1
10 | {
11 | [Fact]
12 | public async Task Test_PerHostHttpClientFactory()
13 | {
14 |
15 | var factory = new PerHostHttpClientFactory();
16 | var client = factory.GetHttpClient("http://www.baidu.com");
17 | var str = await client.GetStringAsync("http://www.baidu.com");
18 | }
19 | }
20 | }
21 |
--------------------------------------------------------------------------------