diff --git a/Directory.Packages.props b/Directory.Packages.props
index 91b1f2c..96c740d 100644
--- a/Directory.Packages.props
+++ b/Directory.Packages.props
@@ -2,10 +2,10 @@
-
+
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..8057d66 100644
--- a/LanguageTagsCreate/Program.cs
+++ b/LanguageTagsCreate/Program.cs
@@ -27,7 +27,9 @@ internal static async Task Main(string[] args)
Log.Logger = LoggerFactory.Create(
commandLine.CreateOptions(commandLine.Result).LogOptions
);
- LogOptions.SetFactory(LoggerFactory.CreateLoggerFactory());
+ ILoggerFactory loggerFactory = LoggerFactory.CreateLoggerFactory();
+ LogOptions.SetFactory(loggerFactory);
+ Utilities.LogOptions.SetFactory(loggerFactory);
// Invoke command
Log.Logger.LogOverrideContext().Information("Starting: {Args}", args);