Skip to content

Commit 8bea106

Browse files
committed
Caching handlers
1 parent 1edc786 commit 8bea106

File tree

2 files changed

+60
-22
lines changed

2 files changed

+60
-22
lines changed

src/HandlerKey.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
namespace Soenneker.Utils.HttpClientCache
2+
{
3+
internal readonly record struct HandlerKey(double LifetimeSeconds, int MaxConnections, bool UseCookies);
4+
}

src/HttpClientCache.cs

Lines changed: 56 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,17 @@
1-
using System;
1+
using Soenneker.Dtos.HttpClientOptions;
2+
using Soenneker.Extensions.Enumerable;
3+
using Soenneker.Extensions.ValueTask;
4+
using Soenneker.Utils.HttpClientCache.Abstract;
5+
using Soenneker.Utils.Runtime;
6+
using Soenneker.Utils.SingletonDictionary;
7+
using System;
8+
using System.Collections.Concurrent;
29
using System.Collections.Generic;
310
using System.Net;
411
using System.Net.Http;
12+
using System.Runtime.CompilerServices;
513
using System.Threading;
614
using System.Threading.Tasks;
7-
using Soenneker.Dtos.HttpClientOptions;
8-
using Soenneker.Extensions.ValueTask;
9-
using Soenneker.Utils.HttpClientCache.Abstract;
10-
using Soenneker.Utils.Runtime;
11-
using Soenneker.Utils.SingletonDictionary;
1215

1316
namespace Soenneker.Utils.HttpClientCache;
1417

@@ -17,6 +20,8 @@ public class HttpClientCache : IHttpClientCache
1720
{
1821
private readonly SingletonDictionary<HttpClient> _httpClients;
1922

23+
private readonly ConcurrentDictionary<HandlerKey, SocketsHttpHandler> _handlers = new();
24+
2025
public HttpClientCache()
2126
{
2227
_httpClients = new SingletonDictionary<HttpClient>(async args =>
@@ -43,14 +48,14 @@ public HttpClientCache()
4348
});
4449
}
4550

46-
private static HttpClient CreateHttpClient(HttpClientOptions? options)
51+
private HttpClient CreateHttpClient(HttpClientOptions? options)
4752
{
4853
if (RuntimeUtil.IsBrowser())
4954
{
5055
return options?.HttpClientHandler != null ? new HttpClient(options.HttpClientHandler) : new HttpClient();
5156
}
5257

53-
return options?.HttpClientHandler != null ? new HttpClient(options.HttpClientHandler) : new HttpClient(CreateSocketsHttpHandler(options));
58+
return options?.HttpClientHandler != null ? new HttpClient(options.HttpClientHandler) : new HttpClient(GetOrCreateHandler(options));
5459
}
5560

5661
public ValueTask<HttpClient> Get(string id, HttpClientOptions? options = null, CancellationToken cancellationToken = default)
@@ -85,38 +90,53 @@ public HttpClient GetSync(string id, Func<HttpClientOptions?> optionsFactory, Ca
8590
return _httpClients.GetSync(id, cancellationToken, options);
8691
}
8792

93+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
8894
private static async ValueTask ConfigureHttpClient(HttpClient httpClient, HttpClientOptions? options)
8995
{
9096
httpClient.Timeout = options?.Timeout ?? TimeSpan.FromSeconds(100);
9197

9298
if (options?.BaseAddress != null)
93-
httpClient.BaseAddress = new Uri(options.BaseAddress);
99+
{
100+
Uri baseUri = new(options.BaseAddress);
101+
102+
if (!Equals(httpClient.BaseAddress, baseUri))
103+
httpClient.BaseAddress = baseUri;
104+
}
94105

95106
AddDefaultRequestHeaders(httpClient, options?.DefaultRequestHeaders);
96107

97-
if (options?.ModifyClient != null)
98-
await options.ModifyClient.Invoke(httpClient).NoSync();
108+
Func<HttpClient, ValueTask>? modifyClient = options?.ModifyClient;
109+
110+
if (modifyClient is not null)
111+
await modifyClient(httpClient).NoSync();
99112
}
100113

101-
private static SocketsHttpHandler CreateSocketsHttpHandler(HttpClientOptions? options)
114+
private SocketsHttpHandler GetOrCreateHandler(HttpClientOptions? options)
102115
{
103-
var handler = new SocketsHttpHandler
104-
{
105-
PooledConnectionLifetime = options?.PooledConnectionLifetime ?? TimeSpan.FromMinutes(10),
106-
MaxConnectionsPerServer = options?.MaxConnectionsPerServer ?? 40
107-
};
116+
var key = new HandlerKey(
117+
options?.PooledConnectionLifetime?.TotalSeconds ?? 600,
118+
options?.MaxConnectionsPerServer ?? 40,
119+
options?.UseCookieContainer == true
120+
);
108121

109-
if (options?.UseCookieContainer == true)
122+
return _handlers.GetOrAdd(key, _ =>
110123
{
111-
handler.CookieContainer = new CookieContainer();
112-
}
124+
var handler = new SocketsHttpHandler
125+
{
126+
PooledConnectionLifetime = TimeSpan.FromSeconds(key.LifetimeSeconds),
127+
MaxConnectionsPerServer = key.MaxConnections
128+
};
129+
130+
if (key.UseCookies)
131+
handler.CookieContainer = new CookieContainer();
113132

114-
return handler;
133+
return handler;
134+
});
115135
}
116136

117137
private static void AddDefaultRequestHeaders(HttpClient httpClient, Dictionary<string, string>? headers)
118138
{
119-
if (headers == null)
139+
if (headers.IsNullOrEmpty())
120140
return;
121141

122142
foreach (KeyValuePair<string, string> header in headers)
@@ -138,12 +158,26 @@ public void RemoveSync(string id)
138158
public ValueTask DisposeAsync()
139159
{
140160
GC.SuppressFinalize(this);
161+
162+
foreach (KeyValuePair<HandlerKey, SocketsHttpHandler> kvp in _handlers.ToArray())
163+
{
164+
if (_handlers.TryRemove(kvp.Key, out SocketsHttpHandler? handler))
165+
handler.Dispose();
166+
}
167+
141168
return _httpClients.DisposeAsync();
142169
}
143170

144171
public void Dispose()
145172
{
146173
GC.SuppressFinalize(this);
174+
175+
foreach (KeyValuePair<HandlerKey, SocketsHttpHandler> kvp in _handlers.ToArray())
176+
{
177+
if (_handlers.TryRemove(kvp.Key, out SocketsHttpHandler? handler))
178+
handler.Dispose();
179+
}
180+
147181
_httpClients.Dispose();
148182
}
149183
}

0 commit comments

Comments
 (0)