11using Soenneker . Dtos . HttpClientOptions ;
22using Soenneker . Extensions . Enumerable ;
33using Soenneker . Extensions . ValueTask ;
4+ using Soenneker . Dictionaries . SingletonKeys ;
45using Soenneker . Utils . HttpClientCache . Abstract ;
56using Soenneker . Utils . Runtime ;
6- using Soenneker . Utils . SingletonDictionary ;
77using System ;
8- using System . Collections . Concurrent ;
98using System . Collections . Generic ;
109using System . Net ;
1110using System . Net . Http ;
1211using System . Net . Security ;
1312using System . Runtime . CompilerServices ;
1413using System . Threading ;
1514using System . Threading . Tasks ;
15+ using Soenneker . Dictionaries . Singletons ;
1616
1717namespace Soenneker . Utils . HttpClientCache ;
1818
1919///<inheritdoc cref="IHttpClientCache"/>
2020public sealed class HttpClientCache : IHttpClientCache
2121{
2222 private readonly SingletonDictionary < HttpClient , OptionsFactory > _httpClients ;
23- private readonly ConcurrentDictionary < HandlerKey , SocketsHttpHandler > _handlers = new ( ) ;
23+ private readonly SingletonKeyDictionary < HandlerKey , SocketsHttpHandler > _handlers ;
2424
2525 private static readonly bool _isBrowser = RuntimeUtil . IsBrowser ( ) ;
2626
@@ -32,9 +32,12 @@ public HttpClientCache()
3232 {
3333 // Use method group to avoid a closure and still access instance state.
3434 _httpClients = new SingletonDictionary < HttpClient , OptionsFactory > ( InitializeHttpClient ) ;
35+
36+ // Handlers are expensive and should be reused; use a keyed singleton dictionary to guarantee a single handler per key.
37+ _handlers = new SingletonKeyDictionary < HandlerKey , SocketsHttpHandler > ( static k => CreateHandlerFromKey ( in k ) ) ;
3538 }
3639
37- private async ValueTask < HttpClient > InitializeHttpClient ( string _ , CancellationToken cancellationToken , OptionsFactory factory )
40+ private async ValueTask < HttpClient > InitializeHttpClient ( string _ , OptionsFactory factory , CancellationToken cancellationToken )
3841 {
3942 HttpClientOptions ? options = await factory . Invoke ( cancellationToken )
4043 . NoSync ( ) ;
@@ -144,8 +147,7 @@ private SocketsHttpHandler GetOrCreateHandler(HttpClientOptions? options)
144147 KeepAlivePingPolicy : options ? . KeepAlivePingPolicy , UseProxy : options ? . UseProxy , Proxy : proxy , MaxResponseDrainSize : options ? . MaxResponseDrainSize ,
145148 MaxResponseHeadersLength : options ? . MaxResponseHeadersLength , SslOptions : sslOptions ) ;
146149
147- // static factory => no closure allocation
148- return _handlers . GetOrAdd ( key , static k => CreateHandlerFromKey ( k ) ) ;
150+ return _handlers . GetSync ( key ) ;
149151 }
150152
151153 [ MethodImpl ( MethodImplOptions . AggressiveInlining ) ]
@@ -212,24 +214,15 @@ public ValueTask Remove(string id, CancellationToken cancellationToken = default
212214 public void RemoveSync ( string id , CancellationToken cancellationToken = default ) =>
213215 _httpClients . RemoveSync ( id , cancellationToken ) ;
214216
215- private void DisposeHandlers ( )
216- {
217- foreach ( KeyValuePair < HandlerKey , SocketsHttpHandler > kvp in _handlers )
218- {
219- if ( _handlers . TryRemove ( kvp . Key , out SocketsHttpHandler ? handler ) )
220- handler . Dispose ( ) ;
221- }
222- }
223-
224- public ValueTask DisposeAsync ( )
217+ public async ValueTask DisposeAsync ( )
225218 {
226- DisposeHandlers ( ) ;
227- return _httpClients . DisposeAsync ( ) ;
219+ await _handlers . DisposeAsync ( ) ;
220+ await _httpClients . DisposeAsync ( ) ;
228221 }
229222
230223 public void Dispose ( )
231224 {
232- DisposeHandlers ( ) ;
225+ _handlers . Dispose ( ) ;
233226 _httpClients . Dispose ( ) ;
234227 }
235228}
0 commit comments