Skip to content

Commit f6dd54a

Browse files
authored
Merge pull request #18 from hmlendea/cache
Implemented NetworkUtils caching
2 parents 8ea5c97 + 26faee8 commit f6dd54a

2 files changed

Lines changed: 82 additions & 46 deletions

File tree

NuciWeb.HTTP/NetworkUtils.cs

Lines changed: 81 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System;
2+
using System.Collections.Concurrent;
23
using System.Collections.Generic;
34
using System.Linq;
45
using 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

NuciWeb.HTTP/NuciWeb.HTTP.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
<PropertyGroup>
44
<TargetFramework>net10.0</TargetFramework>
55
<RootNamespace>NuciWeb.HTTP</RootNamespace>
6-
<Version>1.6.1</Version>
6+
<Version>1.7.0</Version>
77
<Description>HttpClient wrapper</Description>
88
<Authors>Horațiu Mlendea</Authors>
99
<Copyright>Copyright 2026 © Horațiu Mlendea</Copyright>

0 commit comments

Comments
 (0)