├── .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 | --------------------------------------------------------------------------------