99using System . Collections . Generic ;
1010using System . Net ;
1111using System . Net . Http ;
12+ using System . Net . Security ;
1213using System . Runtime . CompilerServices ;
1314using System . Threading ;
1415using System . Threading . Tasks ;
@@ -18,61 +19,72 @@ namespace Soenneker.Utils.HttpClientCache;
1819///<inheritdoc cref="IHttpClientCache"/>
1920public sealed class HttpClientCache : IHttpClientCache
2021{
21- private readonly SingletonDictionary < HttpClient , Func < CancellationToken , ValueTask < HttpClientOptions ? > > > _httpClients ;
22+ private readonly SingletonDictionary < HttpClient , OptionsFactory > _httpClients ;
2223 private readonly ConcurrentDictionary < HandlerKey , SocketsHttpHandler > _handlers = new ( ) ;
2324
24- private static readonly Func < CancellationToken , ValueTask < HttpClientOptions ? > > _nullOptionsFactory = static _ => default ;
25+ private static readonly bool _isBrowser = RuntimeUtil . IsBrowser ( ) ;
26+
27+ private static readonly TimeSpan _defaultTimeout = TimeSpan . FromSeconds ( 100 ) ;
28+ private static readonly TimeSpan _defaultConnectTimeout = TimeSpan . FromSeconds ( 100 ) ;
29+ private static readonly TimeSpan _defaultPooledLifetime = TimeSpan . FromMinutes ( 10 ) ;
2530
2631 public HttpClientCache ( )
2732 {
28- // We need the token-aware init path so we can call optionsFactory(token).
29- _httpClients =
30- new SingletonDictionary < HttpClient , Func < CancellationToken , ValueTask < HttpClientOptions ? > > > ( async ( _ , cancellationToken , optionsFactory ) =>
31- {
32- HttpClientOptions ? options = await optionsFactory ( cancellationToken )
33- . NoSync ( ) ;
33+ // Use method group to avoid a closure and still access instance state.
34+ _httpClients = new SingletonDictionary < HttpClient , OptionsFactory > ( InitializeHttpClient ) ;
35+ }
36+
37+ private async ValueTask < HttpClient > InitializeHttpClient ( string _ , CancellationToken cancellationToken , OptionsFactory factory )
38+ {
39+ HttpClientOptions ? options = await factory . Invoke ( cancellationToken )
40+ . NoSync ( ) ;
3441
35- HttpClient httpClient = CreateHttpClient ( options ) ;
42+ HttpClient httpClient = CreateHttpClient ( options ) ;
3643
37- await ConfigureHttpClient ( httpClient , options )
38- . NoSync ( ) ;
44+ await ConfigureHttpClient ( httpClient , options )
45+ . NoSync ( ) ;
3946
40- return httpClient ;
41- } ) ;
47+ return httpClient ;
4248 }
4349
4450 [ MethodImpl ( MethodImplOptions . AggressiveInlining ) ]
4551 public ValueTask < HttpClient > Get ( string id , CancellationToken cancellationToken = default ) =>
46- _httpClients . Get ( id , ( ) => _nullOptionsFactory , cancellationToken ) ;
52+ _httpClients . Get ( id , OptionsFactory . Null , cancellationToken ) ;
4753
4854 [ MethodImpl ( MethodImplOptions . AggressiveInlining ) ]
4955 public ValueTask < HttpClient > Get ( string id , Func < CancellationToken , ValueTask < HttpClientOptions ? > > optionsFactory ,
50- CancellationToken cancellationToken = default ) => _httpClients . Get ( id , ( ) => optionsFactory , cancellationToken ) ;
56+ CancellationToken cancellationToken = default ) =>
57+ _httpClients . Get ( id , OptionsFactory . From ( optionsFactory ) , cancellationToken ) ;
5158
5259 [ MethodImpl ( MethodImplOptions . AggressiveInlining ) ]
5360 public ValueTask < HttpClient > Get ( string id , Func < HttpClientOptions ? > optionsFactory , CancellationToken cancellationToken = default ) =>
54- _httpClients . Get ( id , ( ) => _ => new ValueTask < HttpClientOptions ? > ( optionsFactory ( ) ) , cancellationToken ) ;
61+ _httpClients . Get ( id , OptionsFactory . From ( optionsFactory ) , cancellationToken ) ;
5562
5663 [ MethodImpl ( MethodImplOptions . AggressiveInlining ) ]
5764 public ValueTask < HttpClient > Get ( string id , Func < ValueTask < HttpClientOptions ? > > optionsFactory , CancellationToken cancellationToken = default ) =>
58- _httpClients . Get ( id , ( ) => _ => optionsFactory ( ) , cancellationToken ) ;
65+ _httpClients . Get ( id , OptionsFactory . From ( optionsFactory ) , cancellationToken ) ;
5966
6067 [ MethodImpl ( MethodImplOptions . AggressiveInlining ) ]
6168 public HttpClient GetSync ( string id , CancellationToken cancellationToken = default ) =>
62- _httpClients . GetSync ( id , ( ) => _nullOptionsFactory , cancellationToken ) ;
69+ _httpClients . GetSync ( id , OptionsFactory . Null , cancellationToken ) ;
6370
6471 [ MethodImpl ( MethodImplOptions . AggressiveInlining ) ]
65- public HttpClient GetSync (
66- string id , Func < CancellationToken , ValueTask < HttpClientOptions ? > > optionsFactory , CancellationToken cancellationToken = default ) =>
67- _httpClients . GetSync ( id , ( ) => optionsFactory , cancellationToken ) ;
72+ public HttpClient GetSync ( string id , Func < CancellationToken , ValueTask < HttpClientOptions ? > > optionsFactory ,
73+ CancellationToken cancellationToken = default ) =>
74+ _httpClients . GetSync ( id , OptionsFactory . From ( optionsFactory ) , cancellationToken ) ;
6875
6976 [ MethodImpl ( MethodImplOptions . AggressiveInlining ) ]
7077 public HttpClient GetSync ( string id , Func < HttpClientOptions ? > optionsFactory , CancellationToken cancellationToken = default ) =>
71- _httpClients . GetSync ( id , ( ) => _ => new ValueTask < HttpClientOptions ? > ( optionsFactory ( ) ) , cancellationToken ) ;
78+ _httpClients . GetSync ( id , OptionsFactory . From ( optionsFactory ) , cancellationToken ) ;
7279
80+ [ MethodImpl ( MethodImplOptions . AggressiveInlining ) ]
81+ public HttpClient GetSync ( string id , Func < ValueTask < HttpClientOptions ? > > optionsFactory , CancellationToken cancellationToken = default ) =>
82+ _httpClients . GetSync ( id , OptionsFactory . From ( optionsFactory ) , cancellationToken ) ;
83+
84+ [ MethodImpl ( MethodImplOptions . AggressiveInlining ) ]
7385 private HttpClient CreateHttpClient ( HttpClientOptions ? options )
7486 {
75- if ( RuntimeUtil . IsBrowser ( ) )
87+ if ( _isBrowser )
7688 {
7789 return options ? . HttpClientHandler != null ? new HttpClient ( options . HttpClientHandler , disposeHandler : false ) : new HttpClient ( ) ;
7890 }
@@ -82,96 +94,92 @@ private HttpClient CreateHttpClient(HttpClientOptions? options)
8294 : new HttpClient ( GetOrCreateHandler ( options ) , disposeHandler : false ) ;
8395 }
8496
97+ // Remove async state machine when ModifyClient is null.
8598 [ MethodImpl ( MethodImplOptions . AggressiveInlining ) ]
86- private static async ValueTask ConfigureHttpClient ( HttpClient httpClient , HttpClientOptions ? options )
99+ private static ValueTask ConfigureHttpClient ( HttpClient httpClient , HttpClientOptions ? options )
87100 {
88- httpClient . Timeout = options ? . Timeout ?? TimeSpan . FromSeconds ( 100 ) ;
101+ httpClient . Timeout = options ? . Timeout ?? _defaultTimeout ;
89102
90- if ( options ? . BaseAddress != null )
91- {
92- Uri baseUri = new ( options . BaseAddress ) ;
103+ // Prefer Uri to avoid parsing/allocation.
104+ Uri ? baseUri = options ? . BaseAddressUri ;
93105
94- if ( ! Equals ( httpClient . BaseAddress , baseUri ) )
95- httpClient . BaseAddress = baseUri ;
96- }
106+ if ( baseUri is not null && ! Equals ( httpClient . BaseAddress , baseUri ) )
107+ httpClient . BaseAddress = baseUri ;
97108
98109 AddDefaultRequestHeaders ( httpClient , options ? . DefaultRequestHeaders ) ;
99110
100111 Func < HttpClient , ValueTask > ? modifyClient = options ? . ModifyClient ;
101-
102- if ( modifyClient is not null )
103- await modifyClient ( httpClient )
104- . NoSync ( ) ;
112+ return modifyClient ? . Invoke ( httpClient ) ?? default ;
105113 }
106114
107115 private SocketsHttpHandler GetOrCreateHandler ( HttpClientOptions ? options )
108116 {
109- double connectTimeoutSeconds = options ? . Timeout ? . TotalSeconds ?? 100 ;
110-
111- var key = new HandlerKey (
112- LifetimeSeconds : options ? . PooledConnectionLifetime ? . TotalSeconds ?? 600 ,
113- MaxConnections : options ? . MaxConnectionsPerServer ?? 40 ,
114- UseCookies : options ? . UseCookieContainer == true ,
115- ConnectTimeoutSeconds : connectTimeoutSeconds ,
116- ResponseDrainTimeoutSeconds : options ? . ResponseDrainTimeout ? . TotalSeconds ,
117- AllowAutoRedirect : options ? . AllowAutoRedirect ,
118- AutomaticDecompression : ( int ? ) options ? . AutomaticDecompression ,
119- KeepAlivePingDelaySeconds : options ? . KeepAlivePingDelay ? . TotalSeconds ,
120- KeepAlivePingTimeoutSeconds : options ? . KeepAlivePingTimeout ? . TotalSeconds ,
121- KeepAlivePingPolicy : ( int ? ) options ? . KeepAlivePingPolicy ,
122- UseProxy : options ? . UseProxy ,
123- ProxyHashCode : options ? . Proxy ? . GetHashCode ( ) ,
124- MaxResponseDrainSize : options ? . MaxResponseDrainSize ,
125- MaxResponseHeadersLength : options ? . MaxResponseHeadersLength ,
126- SslOptionsHashCode : options ? . SslOptions ? . GetHashCode ( ) ) ;
127-
128- return _handlers . GetOrAdd ( key , _ =>
117+ // Do NOT tie connect timeout to request timeout.
118+ TimeSpan connectTimeout = options ? . ConnectTimeout ?? _defaultConnectTimeout ;
119+
120+ // Extract refs so they become part of the key (no closure, no hash-key risk)
121+ IWebProxy ? proxy = options ? . Proxy ;
122+ SslClientAuthenticationOptions ? sslOptions = options ? . SslOptions ;
123+
124+ var key = new HandlerKey ( PooledConnectionLifetimeTicks : ( options ? . PooledConnectionLifetime ?? _defaultPooledLifetime ) . Ticks ,
125+ MaxConnectionsPerServer : options ? . MaxConnectionsPerServer ?? 40 , UseCookies : options ? . UseCookieContainer == true ,
126+ ConnectTimeoutTicks : connectTimeout . Ticks , ResponseDrainTimeoutTicks : options ? . ResponseDrainTimeout ? . Ticks ,
127+ AllowAutoRedirect : options ? . AllowAutoRedirect , AutomaticDecompression : options ? . AutomaticDecompression ,
128+ KeepAlivePingDelayTicks : options ? . KeepAlivePingDelay ? . Ticks , KeepAlivePingTimeoutTicks : options ? . KeepAlivePingTimeout ? . Ticks ,
129+ KeepAlivePingPolicy : options ? . KeepAlivePingPolicy , UseProxy : options ? . UseProxy , Proxy : proxy , MaxResponseDrainSize : options ? . MaxResponseDrainSize ,
130+ MaxResponseHeadersLength : options ? . MaxResponseHeadersLength , SslOptions : sslOptions ) ;
131+
132+ // static factory => no closure allocation
133+ return _handlers . GetOrAdd ( key , static k => CreateHandlerFromKey ( k ) ) ;
134+ }
135+
136+ [ MethodImpl ( MethodImplOptions . AggressiveInlining ) ]
137+ private static SocketsHttpHandler CreateHandlerFromKey ( in HandlerKey key )
138+ {
139+ var handler = new SocketsHttpHandler
129140 {
130- var handler = new SocketsHttpHandler
131- {
132- PooledConnectionLifetime = TimeSpan . FromSeconds ( key . LifetimeSeconds ) ,
133- MaxConnectionsPerServer = key . MaxConnections ,
134- ConnectTimeout = TimeSpan . FromSeconds ( key . ConnectTimeoutSeconds )
135- } ;
141+ PooledConnectionLifetime = TimeSpan . FromTicks ( key . PooledConnectionLifetimeTicks ) ,
142+ MaxConnectionsPerServer = key . MaxConnectionsPerServer ,
143+ ConnectTimeout = TimeSpan . FromTicks ( key . ConnectTimeoutTicks )
144+ } ;
136145
137- if ( key . UseCookies )
138- handler . CookieContainer = new CookieContainer ( ) ;
146+ if ( key . UseCookies )
147+ handler . CookieContainer = new CookieContainer ( ) ;
139148
140- if ( key . ResponseDrainTimeoutSeconds . HasValue )
141- handler . ResponseDrainTimeout = TimeSpan . FromSeconds ( key . ResponseDrainTimeoutSeconds . Value ) ;
149+ if ( key . ResponseDrainTimeoutTicks . HasValue )
150+ handler . ResponseDrainTimeout = TimeSpan . FromTicks ( key . ResponseDrainTimeoutTicks . Value ) ;
142151
143- if ( key . AllowAutoRedirect . HasValue )
144- handler . AllowAutoRedirect = key . AllowAutoRedirect . Value ;
152+ if ( key . AllowAutoRedirect . HasValue )
153+ handler . AllowAutoRedirect = key . AllowAutoRedirect . Value ;
145154
146- if ( key . AutomaticDecompression . HasValue )
147- handler . AutomaticDecompression = ( DecompressionMethods ) key . AutomaticDecompression . Value ;
155+ if ( key . AutomaticDecompression . HasValue )
156+ handler . AutomaticDecompression = key . AutomaticDecompression . Value ;
148157
149- if ( key . KeepAlivePingDelaySeconds . HasValue )
150- handler . KeepAlivePingDelay = TimeSpan . FromSeconds ( key . KeepAlivePingDelaySeconds . Value ) ;
158+ if ( key . KeepAlivePingDelayTicks . HasValue )
159+ handler . KeepAlivePingDelay = TimeSpan . FromTicks ( key . KeepAlivePingDelayTicks . Value ) ;
151160
152- if ( key . KeepAlivePingTimeoutSeconds . HasValue )
153- handler . KeepAlivePingTimeout = TimeSpan . FromSeconds ( key . KeepAlivePingTimeoutSeconds . Value ) ;
161+ if ( key . KeepAlivePingTimeoutTicks . HasValue )
162+ handler . KeepAlivePingTimeout = TimeSpan . FromTicks ( key . KeepAlivePingTimeoutTicks . Value ) ;
154163
155- if ( key . KeepAlivePingPolicy . HasValue )
156- handler . KeepAlivePingPolicy = ( HttpKeepAlivePingPolicy ) key . KeepAlivePingPolicy . Value ;
164+ if ( key . KeepAlivePingPolicy . HasValue )
165+ handler . KeepAlivePingPolicy = key . KeepAlivePingPolicy . Value ;
157166
158- if ( key . UseProxy . HasValue )
159- handler . UseProxy = key . UseProxy . Value ;
167+ if ( key . UseProxy . HasValue )
168+ handler . UseProxy = key . UseProxy . Value ;
160169
161- if ( options ? . Proxy != null )
162- handler . Proxy = options . Proxy ;
170+ if ( key . Proxy is not null )
171+ handler . Proxy = key . Proxy ;
163172
164- if ( key . MaxResponseDrainSize . HasValue )
165- handler . MaxResponseDrainSize = key . MaxResponseDrainSize . Value ;
173+ if ( key . MaxResponseDrainSize . HasValue )
174+ handler . MaxResponseDrainSize = key . MaxResponseDrainSize . Value ;
166175
167- if ( key . MaxResponseHeadersLength . HasValue )
168- handler . MaxResponseHeadersLength = key . MaxResponseHeadersLength . Value ;
176+ if ( key . MaxResponseHeadersLength . HasValue )
177+ handler . MaxResponseHeadersLength = key . MaxResponseHeadersLength . Value ;
169178
170- if ( options ? . SslOptions != null )
171- handler . SslOptions = options . SslOptions ;
179+ if ( key . SslOptions is not null )
180+ handler . SslOptions = key . SslOptions ;
172181
173- return handler ;
174- } ) ;
182+ return handler ;
175183 }
176184
177185 private static void AddDefaultRequestHeaders ( HttpClient httpClient , Dictionary < string , string > ? headers )
@@ -183,13 +191,15 @@ private static void AddDefaultRequestHeaders(HttpClient httpClient, Dictionary<s
183191 httpClient . DefaultRequestHeaders . TryAddWithoutValidation ( header . Key , header . Value ) ;
184192 }
185193
186- public ValueTask Remove ( string id , CancellationToken cancellationToken = default ) => _httpClients . Remove ( id , cancellationToken ) ;
194+ public ValueTask Remove ( string id , CancellationToken cancellationToken = default ) =>
195+ _httpClients . Remove ( id , cancellationToken ) ;
187196
188- public void RemoveSync ( string id , CancellationToken cancellationToken = default ) => _httpClients . RemoveSync ( id , cancellationToken ) ;
197+ public void RemoveSync ( string id , CancellationToken cancellationToken = default ) =>
198+ _httpClients . RemoveSync ( id , cancellationToken ) ;
189199
190200 private void DisposeHandlers ( )
191201 {
192- foreach ( KeyValuePair < HandlerKey , SocketsHttpHandler > kvp in _handlers . ToArray ( ) )
202+ foreach ( KeyValuePair < HandlerKey , SocketsHttpHandler > kvp in _handlers )
193203 {
194204 if ( _handlers . TryRemove ( kvp . Key , out SocketsHttpHandler ? handler ) )
195205 handler . Dispose ( ) ;
0 commit comments