11using System ;
2+ using System . Collections . Concurrent ;
23using System . Collections . Generic ;
34using System . Linq ;
45using System . Net ;
@@ -14,6 +15,8 @@ namespace NuciWeb.HTTP
1415 public static class NetworkUtils
1516 {
1617 private static readonly HttpClient HttpClient = CreateHttpClient ( ) ;
18+ private static readonly TimeSpan CacheDuration = TimeSpan . FromSeconds ( 60 ) ;
19+ private static readonly ConcurrentDictionary < string , CacheEntry > Cache = new ( ) ;
1720
1821 private static readonly List < string > PingHosts =
1922 [
@@ -205,6 +208,8 @@ public static class NetworkUtils
205208 "https://wtfismyip.com/text" ,
206209 ] ;
207210
211+ private sealed record CacheEntry ( object Value , DateTimeOffset ExpiresAt ) ;
212+
208213 /// <summary>
209214 /// Checks if the system has internet access.
210215 /// </summary>
@@ -249,6 +254,71 @@ public static async Task<bool> HasInternetAccessAsync()
249254 /// <returns>The public IP address as a string.</returns>
250255 /// <exception cref="InvalidOperationException">Thrown if no internet access is available.</exception>
251256 public static string GetPublicIpAddress ( )
257+ => GetOrCreateCachedValue ( "public-ip-address" , RetrievePublicIpAddress ) ;
258+
259+ /// <summary>
260+ /// Gets the known hostnames associated with the specified IP address using reverse DNS lookup.
261+ /// </summary>
262+ /// <param name="ipAddress">The IP address to resolve.</param>
263+ /// <returns>A list containing the primary hostname and aliases, if available.</returns>
264+ /// <exception cref="ArgumentNullException">Thrown if <paramref name="ipAddress"/> is null.</exception>
265+ public static List < string > GetHostnames ( IPAddress ipAddress )
266+ {
267+ ArgumentNullException . ThrowIfNull ( ipAddress ) ;
268+
269+ string [ ] cachedHostnames = GetOrCreateCachedValue (
270+ $ "hostnames:{ ipAddress } ",
271+ ( ) => ResolveHostnames ( ipAddress ) . ToArray ( ) ) ;
272+
273+ return [ .. cachedHostnames ] ;
274+ }
275+
276+ /// <summary>
277+ /// Gets the known hostnames associated with the specified IP address using reverse DNS lookup.
278+ /// </summary>
279+ /// <param name="ipAddress">The IP address to resolve.</param>
280+ /// <returns>A list containing the primary hostname and aliases, if available.</returns>
281+ /// <exception cref="ArgumentException">Thrown if <paramref name="ipAddress"/> is not a valid IP address.</exception>
282+ public static List < string > GetHostnames ( string ipAddress )
283+ {
284+ if ( ! IPAddress . TryParse ( ipAddress , out IPAddress parsedIpAddress ) )
285+ {
286+ throw new ArgumentException ( "The provided value is not a valid IP address." , nameof ( ipAddress ) ) ;
287+ }
288+
289+ return GetHostnames ( parsedIpAddress ) ;
290+ }
291+
292+ /// <summary>
293+ /// Waits for internet access to be available, with a default timeout of 30 seconds.
294+ /// </summary>
295+ /// <exception cref="TimeoutException">Thrown if internet access is not available within the specified timeout.</exception>
296+ public static void WaitForInternetAccess ( )
297+ => WaitForInternetAccess ( TimeSpan . FromSeconds ( 30 ) ) ;
298+
299+ /// <summary>
300+ /// Waits for internet access to be available.
301+ /// </summary>
302+ /// <param name="timeout">The maximum time to wait for internet access.</param>
303+ /// <exception cref="TimeoutException">Thrown if internet access is not available within the specified timeout.</exception>
304+ public static void WaitForInternetAccess ( TimeSpan timeout )
305+ {
306+ DateTime beginningDT = DateTime . Now ;
307+
308+ while ( DateTime . Now < beginningDT + timeout )
309+ {
310+ if ( HasInternetAccess ( ) )
311+ {
312+ return ;
313+ }
314+
315+ Thread . Sleep ( 1000 ) ;
316+ }
317+
318+ throw new TimeoutException ( "No internet access after the specified timeout." ) ;
319+ }
320+
321+ private static string RetrievePublicIpAddress ( )
252322 {
253323 if ( ! HasInternetAccess ( ) )
254324 {
@@ -308,16 +378,8 @@ private static bool TryNormalizePublicIpAddress(string response, out string publ
308378 return true ;
309379 }
310380
311- /// <summary>
312- /// Gets the known hostnames associated with the specified IP address using reverse DNS lookup.
313- /// </summary>
314- /// <param name="ipAddress">The IP address to resolve.</param>
315- /// <returns>A list containing the primary hostname and aliases, if available.</returns>
316- /// <exception cref="ArgumentNullException">Thrown if <paramref name="ipAddress"/> is null.</exception>
317- public static List < string > GetHostnames ( IPAddress ipAddress )
381+ private static List < string > ResolveHostnames ( IPAddress ipAddress )
318382 {
319- ArgumentNullException . ThrowIfNull ( ipAddress ) ;
320-
321383 IPHostEntry hostEntry ;
322384
323385 try
@@ -337,52 +399,26 @@ .. new[] { hostEntry.HostName }
337399 ] ;
338400 }
339401
340- /// <summary>
341- /// Gets the known hostnames associated with the specified IP address using reverse DNS lookup.
342- /// </summary>
343- /// <param name="ipAddress">The IP address to resolve.</param>
344- /// <returns>A list containing the primary hostname and aliases, if available.</returns>
345- /// <exception cref="ArgumentException">Thrown if <paramref name="ipAddress"/> is not a valid IP address.</exception>
346- public static List < string > GetHostnames ( string ipAddress )
402+ private static T GetOrCreateCachedValue < T > ( string cacheKey , Func < T > valueFactory )
347403 {
348- if ( ! IPAddress . TryParse ( ipAddress , out IPAddress parsedIpAddress ) )
404+ DateTimeOffset now = DateTimeOffset . UtcNow ;
405+
406+ if ( Cache . TryGetValue ( cacheKey , out CacheEntry cachedEntry )
407+ && cachedEntry . ExpiresAt > now )
349408 {
350- throw new ArgumentException ( "The provided value is not a valid IP address." , nameof ( ipAddress ) ) ;
409+ return ( T ) cachedEntry . Value ;
351410 }
352411
353- return GetHostnames ( parsedIpAddress ) ;
354- }
355-
356- /// <summary>
357- /// Waits for internet access to be available, with a default timeout of 30 seconds.
358- /// </summary>
359- /// <exception cref="TimeoutException">Thrown if internet access is not available within the specified timeout.</exception>
360- public static void WaitForInternetAccess ( )
361- => WaitForInternetAccess ( TimeSpan . FromSeconds ( 30 ) ) ;
362-
363- /// <summary>
364- /// Waits for internet access to be available.
365- /// </summary>
366- /// <param name="timeout">The maximum time to wait for internet access.</param>
367- /// <exception cref="TimeoutException">Thrown if internet access is not available within the specified timeout.</exception>
368- public static void WaitForInternetAccess ( TimeSpan timeout )
369- {
370- DateTime beginningDT = DateTime . Now ;
412+ T value = valueFactory ( ) ;
371413
372- while ( DateTime . Now < beginningDT + timeout )
414+ if ( value is not null )
373415 {
374- if ( HasInternetAccess ( ) )
375- {
376- return ;
377- }
378-
379- Thread . Sleep ( 1000 ) ;
416+ Cache [ cacheKey ] = new CacheEntry ( value , now . Add ( CacheDuration ) ) ;
380417 }
381418
382- throw new TimeoutException ( "No internet access after the specified timeout." ) ;
419+ return value ;
383420 }
384421
385-
386422 private static async Task < bool > TryPingAsync ( CancellationToken cancellationToken )
387423 {
388424 try
0 commit comments