From 511c4e6f098a3c3d9075ca28fdf1f015cabbcb14 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Fri, 10 Jul 2026 11:21:46 -0700 Subject: [PATCH 1/3] Adopt ptr727.Utilities HttpClientFactory and drop local copies --- Directory.Packages.props | 1 + LanguageTagsCreate/AssemblyInfo.cs | 45 ------- LanguageTagsCreate/CreateTagData.cs | 4 +- LanguageTagsCreate/HttpClientFactory.cs | 120 ------------------- LanguageTagsCreate/LanguageTagsCreate.csproj | 2 +- LanguageTagsCreate/Program.cs | 1 + 6 files changed, 5 insertions(+), 168 deletions(-) delete mode 100644 LanguageTagsCreate/AssemblyInfo.cs delete mode 100644 LanguageTagsCreate/HttpClientFactory.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index 91b1f2c..1b17414 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -6,6 +6,7 @@ + diff --git a/LanguageTagsCreate/AssemblyInfo.cs b/LanguageTagsCreate/AssemblyInfo.cs deleted file mode 100644 index ac9e350..0000000 --- a/LanguageTagsCreate/AssemblyInfo.cs +++ /dev/null @@ -1,45 +0,0 @@ -using System.Reflection; -using System.Runtime.InteropServices; - -namespace ptr727.LanguageTags.Create; - -internal static class AssemblyInfo -{ - internal static string AppVersion => $"{AppName} : {FileVersion} ({BuildType})"; - - internal static string RuntimeVersion => - $"{RuntimeInformation.FrameworkDescription} : {RuntimeInformation.RuntimeIdentifier}"; - - internal static string BuildType => -#if DEBUG - "Debug"; -#else - "Release"; -#endif - - internal static string AppName => GetAssembly().GetName().Name ?? string.Empty; - - internal static string InformationalVersion => - // E.g. 1.2.3+abc123.abc123 - GetAssembly() - .GetCustomAttribute() - ?.InformationalVersion - ?? string.Empty; - - internal static string FileVersion => - // E.g. 1.2.3.4 - GetAssembly().GetCustomAttribute()?.Version - ?? string.Empty; - - internal static string ReleaseVersion => - // E.g. 1.2.3 part of 1.2.3+abc123.abc123 - // Use major.minor.build from informational version - InformationalVersion.Split('+', '-')[0]; - - private static Assembly GetAssembly() - { - Assembly? assembly = Assembly.GetEntryAssembly(); - assembly ??= Assembly.GetExecutingAssembly(); - return assembly; - } -} diff --git a/LanguageTagsCreate/CreateTagData.cs b/LanguageTagsCreate/CreateTagData.cs index 78fd33e..0adf845 100644 --- a/LanguageTagsCreate/CreateTagData.cs +++ b/LanguageTagsCreate/CreateTagData.cs @@ -127,8 +127,8 @@ private async Task DownloadFileAsync(Uri uri, string fileName) Log.Information("Downloading \"{Uri}\" to \"{FileName}\" ...", uri.ToString(), fileName); - using Stream httpStream = await HttpClientFactory - .GetHttpClient() + using Stream httpStream = await Utilities + .HttpClientFactory.GetClient() .GetStreamAsync(uri, cancellationToken) .ConfigureAwait(false); diff --git a/LanguageTagsCreate/HttpClientFactory.cs b/LanguageTagsCreate/HttpClientFactory.cs deleted file mode 100644 index da6063b..0000000 --- a/LanguageTagsCreate/HttpClientFactory.cs +++ /dev/null @@ -1,120 +0,0 @@ -using System.Net.Http.Headers; -using Microsoft.Extensions.Http.Resilience; -using Polly; -using Polly.CircuitBreaker; -using Polly.Retry; - -namespace ptr727.LanguageTags.Create; - -internal static class HttpClientFactory -{ - // Retry - private const int RetryMaxAttempts = 3; - private static readonly TimeSpan s_retryBaseDelay = TimeSpan.FromSeconds(1); - private static readonly TimeSpan s_retryMaxDelay = TimeSpan.FromSeconds(30); - - // Circuit breaker - private const double CircuitBreakerFailureRatio = 0.1; - private const int CircuitBreakerMinimumThroughput = 10; - private static readonly TimeSpan s_circuitBreakerSamplingDuration = TimeSpan.FromSeconds(60); - private static readonly TimeSpan s_circuitBreakerBreakDuration = TimeSpan.FromSeconds(30); - - // Connection pool - private static readonly TimeSpan s_connectionLifetime = TimeSpan.FromMinutes(15); - private static readonly TimeSpan s_connectionIdleTimeout = TimeSpan.FromMinutes(2); - - // HttpClient - private static readonly TimeSpan s_httpClientTimeout = TimeSpan.FromSeconds(120); - - private static readonly Lazy s_httpClient = new(CreateHttpClient); - - // Returns the shared singleton HttpClient; all callers share the connection pool and circuit breaker state. - internal static HttpClient GetHttpClient() => s_httpClient.Value; - - private static ResilienceHandler CreateResilienceHandler() => - new( - new ResiliencePipelineBuilder() - .AddRetry( - new RetryStrategyOptions - { - MaxRetryAttempts = RetryMaxAttempts, - BackoffType = DelayBackoffType.Exponential, - UseJitter = true, - Delay = s_retryBaseDelay, - MaxDelay = s_retryMaxDelay, - ShouldHandle = args => - ValueTask.FromResult(IsTransientFailure(args.Outcome)), - OnRetry = args => - { - Log.Logger.Warning( - "HTTP retry attempt {Attempt} after {Delay}ms: {Outcome}", - args.AttemptNumber, - args.RetryDelay.TotalMilliseconds, - args.Outcome - ); - return ValueTask.CompletedTask; - }, - } - ) - .AddCircuitBreaker( - new CircuitBreakerStrategyOptions - { - FailureRatio = CircuitBreakerFailureRatio, - MinimumThroughput = CircuitBreakerMinimumThroughput, - SamplingDuration = s_circuitBreakerSamplingDuration, - BreakDuration = s_circuitBreakerBreakDuration, - ShouldHandle = args => - ValueTask.FromResult(IsTransientFailure(args.Outcome)), - OnOpened = args => - { - Log.Logger.Warning( - "Circuit breaker opened for {Duration}s: {Outcome}", - args.BreakDuration.TotalSeconds, - args.Outcome - ); - return ValueTask.CompletedTask; - }, - OnClosed = _ => - { - Log.Logger.Information("Circuit breaker closed."); - return ValueTask.CompletedTask; - }, - OnHalfOpened = _ => - { - Log.Logger.Debug("Circuit breaker half-opened."); - return ValueTask.CompletedTask; - }, - } - ) - .Build() - ) - { - InnerHandler = new SocketsHttpHandler - { - PooledConnectionLifetime = s_connectionLifetime, - PooledConnectionIdleTimeout = s_connectionIdleTimeout, - AutomaticDecompression = System.Net.DecompressionMethods.All, - }, - }; - - private static bool IsTransientFailure(Outcome outcome) => - outcome.Exception is not null - ? outcome.Exception is not (OperationCanceledException or BrokenCircuitException) - : outcome.Result is not null && (int)outcome.Result.StatusCode is 408 or 429 or >= 500; - - // Creates a new HttpClient instance; each caller gets an independent resilience handler - // and circuit breaker state. Callers should store and reuse the returned instance. - [System.Diagnostics.CodeAnalysis.SuppressMessage( - "Reliability", - "CA2000:Dispose objects before losing scope", - Justification = "HttpClient takes ownership of the handler and disposes it when the client is disposed." - )] - internal static HttpClient CreateHttpClient() - { - HttpClient httpClient = new(CreateResilienceHandler()) { Timeout = s_httpClientTimeout }; - httpClient.DefaultRequestHeaders.UserAgent.Add( - new ProductInfoHeaderValue(AssemblyInfo.AppName, AssemblyInfo.ReleaseVersion) - ); - return httpClient; - } -} diff --git a/LanguageTagsCreate/LanguageTagsCreate.csproj b/LanguageTagsCreate/LanguageTagsCreate.csproj index 3900151..6b62657 100644 --- a/LanguageTagsCreate/LanguageTagsCreate.csproj +++ b/LanguageTagsCreate/LanguageTagsCreate.csproj @@ -15,7 +15,7 @@ - + diff --git a/LanguageTagsCreate/Program.cs b/LanguageTagsCreate/Program.cs index 66e8812..16f5c5f 100644 --- a/LanguageTagsCreate/Program.cs +++ b/LanguageTagsCreate/Program.cs @@ -28,6 +28,7 @@ internal static async Task Main(string[] args) commandLine.CreateOptions(commandLine.Result).LogOptions ); LogOptions.SetFactory(LoggerFactory.CreateLoggerFactory()); + Utilities.LogOptions.SetFactory(LoggerFactory.CreateLoggerFactory()); // Invoke command Log.Logger.LogOverrideContext().Information("Starting: {Args}", args); From 3256fae245aa9d0bec355b643cbd6339a9fbf849 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Fri, 10 Jul 2026 11:30:28 -0700 Subject: [PATCH 2/3] Capture shared logger factory once and drop unused package version --- Directory.Packages.props | 1 - LanguageTagsCreate/Program.cs | 5 +++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index 1b17414..be90f70 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -2,7 +2,6 @@ - diff --git a/LanguageTagsCreate/Program.cs b/LanguageTagsCreate/Program.cs index 16f5c5f..8057d66 100644 --- a/LanguageTagsCreate/Program.cs +++ b/LanguageTagsCreate/Program.cs @@ -27,8 +27,9 @@ internal static async Task Main(string[] args) Log.Logger = LoggerFactory.Create( commandLine.CreateOptions(commandLine.Result).LogOptions ); - LogOptions.SetFactory(LoggerFactory.CreateLoggerFactory()); - Utilities.LogOptions.SetFactory(LoggerFactory.CreateLoggerFactory()); + ILoggerFactory loggerFactory = LoggerFactory.CreateLoggerFactory(); + LogOptions.SetFactory(loggerFactory); + Utilities.LogOptions.SetFactory(loggerFactory); // Invoke command Log.Logger.LogOverrideContext().Information("Starting: {Args}", args); From f5d7ca3f876efe87e0a3c8e6c7a7f6b44363de28 Mon Sep 17 00:00:00 2001 From: Pieter Viljoen Date: Fri, 10 Jul 2026 13:03:20 -0700 Subject: [PATCH 3/3] Reference the released ptr727.Utilities 4.0.7 --- Directory.Packages.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index be90f70..96c740d 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -5,7 +5,7 @@ - +