Skip to content

Commit a2172ae

Browse files
authored
Merge pull request #28 from silkfire/httpclienthandlerex-concurrent-collection
Use concurrent dictionary to cache visited addresses in HttpClientHandlerEx message handler
2 parents 416191a + fa5d10e commit a2172ae

File tree

2 files changed

+86
-18
lines changed

2 files changed

+86
-18
lines changed

Simple.HttpClientFactory.Tests/BasicClientBuilderTests.cs

Lines changed: 73 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,18 +18,27 @@ namespace Simple.HttpClientFactory.Tests
1818
public sealed class BasicClientBuilderTests : IDisposable
1919
{
2020
private const string _endpointUri = "/hello/world";
21+
private const string _endpointUri2 = "/hello/world2";
2122

2223
private readonly WireMockServer _server;
2324

2425
public BasicClientBuilderTests()
2526
{
2627
_server = WireMockServer.Start();
28+
2729
_server.Given(Request.Create().WithPath(_endpointUri).UsingAnyMethod())
2830
.RespondWith(
2931
Response.Create()
3032
.WithStatusCode(HttpStatusCode.OK)
3133
.WithHeader("Content-Type", "text/plain")
3234
.WithBody("Hello world!"));
35+
36+
_server.Given(Request.Create().WithPath(_endpointUri2).UsingAnyMethod())
37+
.RespondWith(
38+
Response.Create()
39+
.WithStatusCode(HttpStatusCode.OK)
40+
.WithHeader("Content-Type", "text/plain")
41+
.WithBody("Hello world 2!"));
3342
}
3443

3544

@@ -141,17 +150,76 @@ public async Task Will_send_default_headers()
141150

142151
#if NET472
143152
[Fact]
144-
public async Task HttpClient_will_cache_visited_urls()
153+
public async Task HttpClientHandlerEx_should_cache_visited_url()
154+
{
155+
var baseUri = _server.Urls[0];
156+
157+
var clientHandler = new HttpClientHandlerEx();
158+
var client = HttpClientFactory.Create().Build(clientHandler);
159+
160+
_ = await client.GetAsync($"{baseUri}{_endpointUri}");
161+
162+
var cachedUriKey = Assert.Single(clientHandler.AlreadySeenAddresses);
163+
Assert.Equal(new HttpClientHandlerEx.UriCacheKey($"{baseUri}{_endpointUri}"), cachedUriKey);
164+
}
165+
166+
[Fact]
167+
public async Task UriCacheKey_equal_comparison()
168+
{
169+
var baseUri = _server.Urls[0];
170+
171+
var clientHandler = new HttpClientHandlerEx();
172+
var client = HttpClientFactory.Create().Build(clientHandler);
173+
174+
_ = await client.GetAsync($"{baseUri}{_endpointUri}");
175+
176+
var cachedUriKey = Assert.Single(clientHandler.AlreadySeenAddresses);
177+
Assert.True(new HttpClientHandlerEx.UriCacheKey($"{baseUri}{_endpointUri}") == cachedUriKey);
178+
}
179+
180+
[Fact]
181+
public async Task UriCacheKey_not_equal_comparison()
145182
{
146183
var baseUri = _server.Urls[0];
147184

148185
var clientHandler = new HttpClientHandlerEx();
149-
var client = HttpClientFactory.Create(baseUri).Build(clientHandler);
186+
var client = HttpClientFactory.Create().Build(clientHandler);
150187

151-
_ = await client.GetAsync(_endpointUri);
188+
_ = await client.GetAsync($"{baseUri}{_endpointUri}");
189+
_ = await client.GetAsync($"{baseUri}{_endpointUri2}");
152190

153-
Assert.Single(clientHandler.AlreadySeenAddresses);
154-
Assert.True(clientHandler.AlreadySeenAddresses.First() == new HttpClientHandlerEx.UriCacheKey(new Uri($"{baseUri}{_endpointUri}")));
191+
Assert.Equal(2, clientHandler.AlreadySeenAddresses.Count);
192+
193+
var cachedUriKey = Assert.Single(clientHandler.AlreadySeenAddresses, uck => uck != new HttpClientHandlerEx.UriCacheKey($"{baseUri}{_endpointUri}"));
194+
Assert.Equal(new HttpClientHandlerEx.UriCacheKey($"{baseUri}{_endpointUri2}"), cachedUriKey);
195+
}
196+
197+
[Fact]
198+
public void UriCacheKey_equal_comparison_object()
199+
{
200+
object uriCacheKeyObject = new HttpClientHandlerEx.UriCacheKey($"{_server.Urls[0]}{_endpointUri}");
201+
202+
Assert.True(new HttpClientHandlerEx.UriCacheKey($"{_server.Urls[0]}{_endpointUri}").Equals(uriCacheKeyObject));
203+
}
204+
205+
[Fact]
206+
public void UriCacheKey_not_equal_comparison_object()
207+
{
208+
object notUriCacheKeyObject = 0;
209+
210+
Assert.False(new HttpClientHandlerEx.UriCacheKey($"{_server.Urls[0]}{_endpointUri2}").Equals(notUriCacheKeyObject));
211+
}
212+
213+
[Fact]
214+
public void UriCacheKey_ToString()
215+
{
216+
Assert.Equal($"{_server.Urls[0]}{_endpointUri}", new HttpClientHandlerEx.UriCacheKey($"{_server.Urls[0]}{_endpointUri}").ToString());
217+
}
218+
219+
[Fact]
220+
public void Two_UriCacheKeys_created_with_a_string_vs_a_uri_should_be_equal()
221+
{
222+
Assert.Equal(new HttpClientHandlerEx.UriCacheKey($"{_server.Urls[0]}{_endpointUri}"), new HttpClientHandlerEx.UriCacheKey(new Uri($"{_server.Urls[0]}{_endpointUri}")));
155223
}
156224
#endif
157225

Simple.HttpClientFactory/MessageHandlers/HttpClientHandlerEx.cs

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
#if NETSTANDARD2_0
22
using System;
3+
using System.Collections.Concurrent;
34
using System.Collections.Generic;
5+
using System.Linq;
46
using System.Net;
57
using System.Net.Http;
68
using System.Threading;
@@ -12,9 +14,9 @@ namespace Simple.HttpClientFactory.MessageHandlers
1214
//note: Easy.Common is licensed with MIT License (https://github.com/NimaAra/Easy.Common/blob/master/LICENSE)
1315
public class HttpClientHandlerEx : HttpClientHandler
1416
{
15-
private readonly HashSet<UriCacheKey> _alreadySeenAddresses = new HashSet<UriCacheKey>();
17+
private readonly ConcurrentDictionary<UriCacheKey, UriCacheKey> _alreadySeenAddresses = new ConcurrentDictionary<UriCacheKey, UriCacheKey>();
1618

17-
public IReadOnlyCollection<UriCacheKey> AlreadySeenAddresses => _alreadySeenAddresses;
19+
public IReadOnlyCollection<UriCacheKey> AlreadySeenAddresses => _alreadySeenAddresses.Values.ToList().AsReadOnly();
1820

1921
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
2022
{
@@ -30,23 +32,19 @@ protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage reques
3032

3133
private void EnsureConnectionLeaseTimeout(Uri endpoint)
3234
{
33-
if (!endpoint.IsAbsoluteUri) { return; }
34-
3535
var key = new UriCacheKey(endpoint);
36-
lock (_alreadySeenAddresses)
37-
{
38-
if (_alreadySeenAddresses.Contains(key)) { return; }
3936

40-
ServicePointManager.FindServicePoint(endpoint)
41-
.ConnectionLeaseTimeout = (int)Constants.ConnectionLifeTime.TotalMilliseconds;
42-
_alreadySeenAddresses.Add(key);
43-
}
37+
_alreadySeenAddresses.TryAdd(key, key);
38+
39+
ServicePointManager.FindServicePoint(endpoint).ConnectionLeaseTimeout = (int)Constants.ConnectionLifeTime.TotalMilliseconds;
4440
}
4541

46-
public struct UriCacheKey : IEquatable<UriCacheKey>
42+
public readonly struct UriCacheKey : IEquatable<UriCacheKey>
4743
{
4844
private readonly Uri _uri;
4945

46+
public UriCacheKey(string uri) => _uri = new Uri(uri);
47+
5048
public UriCacheKey(Uri uri) => _uri = uri;
5149

5250
public bool Equals(UriCacheKey other) => _uri == other._uri;
@@ -58,8 +56,10 @@ public struct UriCacheKey : IEquatable<UriCacheKey>
5856
public static bool operator ==(UriCacheKey left, UriCacheKey right) => left.Equals(right);
5957

6058
public static bool operator !=(UriCacheKey left, UriCacheKey right) => !left.Equals(right);
59+
60+
61+
public override string ToString() => _uri.ToString();
6162
}
6263
}
63-
6464
}
6565
#endif

0 commit comments

Comments
 (0)