try fix download client factory

This commit is contained in:
Flaminel
2025-06-16 21:38:27 +03:00
parent f651663fd3
commit 0a8d1450dd
12 changed files with 495 additions and 115 deletions

View File

@@ -0,0 +1,115 @@
using System.Net;
using Infrastructure.Services;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Http;
using Microsoft.Extensions.Options;
using Polly;
using Polly.Extensions.Http;
namespace Infrastructure.Http.DynamicHttpClientSystem;
/// <summary>
/// Dynamic configuration handler - this configures HttpClients on-demand based on stored configurations
/// </summary>
public class DynamicHttpClientConfiguration : IConfigureNamedOptions<HttpClientFactoryOptions>
{
private readonly IServiceProvider _serviceProvider;
public DynamicHttpClientConfiguration(IServiceProvider serviceProvider)
{
_serviceProvider = serviceProvider;
}
public void Configure(string name, HttpClientFactoryOptions options)
{
var configStore = _serviceProvider.GetRequiredService<IHttpClientConfigStore>();
if (!configStore.TryGetConfiguration(name, out HttpClientConfig? config))
return;
// Configure the HttpClient
options.HttpClientActions.Add(httpClient =>
{
httpClient.Timeout = TimeSpan.FromSeconds(config.Timeout);
});
// Configure the HttpMessageHandler based on type
options.HttpMessageHandlerBuilderActions.Add(builder =>
{
ConfigureHandler(builder, config);
// Add retry policy if configured
if (config.RetryConfig != null)
{
AddRetryPolicy(builder, config.RetryConfig);
}
});
}
private void ConfigureHandler(HttpMessageHandlerBuilder builder, HttpClientConfig config)
{
var certValidationService = _serviceProvider.GetRequiredService<CertificateValidationService>();
switch (config.Type)
{
case HttpClientType.WithRetry:
builder.PrimaryHandler = new HttpClientHandler
{
ServerCertificateCustomValidationCallback = (sender, certificate, chain, policy) =>
certValidationService.ShouldByPassValidationError(config.CertificateValidationType, sender, certificate, chain, policy)
};
break;
case HttpClientType.Deluge:
builder.PrimaryHandler = new HttpClientHandler
{
AllowAutoRedirect = config.AllowAutoRedirect,
UseCookies = true,
CookieContainer = new CookieContainer(),
AutomaticDecompression = config.AutomaticDecompression,
ServerCertificateCustomValidationCallback = (sender, certificate, chain, policy) =>
certValidationService.ShouldByPassValidationError(config.CertificateValidationType, sender, certificate, chain, policy),
};
break;
case HttpClientType.Default:
default:
// Use default handler with certificate validation
var defaultHandler = new HttpClientHandler
{
ServerCertificateCustomValidationCallback = (sender, certificate, chain, policy) =>
certValidationService.ShouldByPassValidationError(config.CertificateValidationType, sender, certificate, chain, policy)
};
builder.PrimaryHandler = defaultHandler;
break;
}
}
private void AddRetryPolicy(HttpMessageHandlerBuilder builder, RetryConfig retryConfig)
{
var retryPolicy = HttpPolicyExtensions
.HandleTransientHttpError();
if (retryConfig.ExcludeUnauthorized)
{
retryPolicy = retryPolicy.OrResult(response =>
!response.IsSuccessStatusCode && response.StatusCode != HttpStatusCode.Unauthorized);
}
else
{
retryPolicy = retryPolicy.OrResult(response => !response.IsSuccessStatusCode);
}
var policy = retryPolicy.WaitAndRetryAsync(
retryConfig.MaxRetries,
retryAttempt => TimeSpan.FromSeconds(Math.Pow(2, retryAttempt))
);
builder.AdditionalHandlers.Add(new PolicyHttpMessageHandler(policy));
}
public void Configure(HttpClientFactoryOptions options)
{
// This is called for unnamed clients - we don't need to do anything here
}
}

View File

@@ -0,0 +1,96 @@
using System.Net;
using Common.Enums;
namespace Infrastructure.Http.DynamicHttpClientSystem;
/// <summary>
/// Implementation of the dynamic HttpClient factory
/// </summary>
public class DynamicHttpClientFactory : IDynamicHttpClientFactory
{
private readonly IHttpClientFactory _httpClientFactory;
private readonly IHttpClientConfigStore _configStore;
public DynamicHttpClientFactory(IHttpClientFactory httpClientFactory, IHttpClientConfigStore configStore)
{
_httpClientFactory = httpClientFactory;
_configStore = configStore;
}
public HttpClient CreateClient(string clientName, HttpClientConfig config)
{
_configStore.AddConfiguration(clientName, config);
return _httpClientFactory.CreateClient(clientName);
}
public HttpClient CreateClient(string clientName)
{
if (!_configStore.TryGetConfiguration(clientName, out _))
{
throw new InvalidOperationException($"No configuration found for client '{clientName}'. Register configuration first.");
}
return _httpClientFactory.CreateClient(clientName);
}
public void RegisterConfiguration(string clientName, HttpClientConfig config)
{
_configStore.AddConfiguration(clientName, config);
}
public void RegisterRetryClient(string clientName, int timeout, RetryConfig retryConfig, CertificateValidationType certificateType)
{
var config = new HttpClientConfig
{
Name = clientName,
Timeout = timeout,
Type = HttpClientType.WithRetry,
RetryConfig = retryConfig,
CertificateValidationType = certificateType
};
RegisterConfiguration(clientName, config);
}
public void RegisterDelugeClient(string clientName, int timeout, RetryConfig retryConfig, CertificateValidationType certificateType)
{
var config = new HttpClientConfig
{
Name = clientName,
Timeout = timeout,
Type = HttpClientType.Deluge,
RetryConfig = retryConfig,
AllowAutoRedirect = true,
CertificateValidationType = certificateType,
AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate
};
RegisterConfiguration(clientName, config);
}
public void RegisterDownloadClient(string clientName, int timeout, HttpClientType clientType, RetryConfig retryConfig, CertificateValidationType certificateType)
{
var config = new HttpClientConfig
{
Name = clientName,
Timeout = timeout,
Type = clientType,
RetryConfig = retryConfig,
CertificateValidationType = certificateType
};
// Configure Deluge-specific settings if needed
if (clientType == HttpClientType.Deluge)
{
config.AllowAutoRedirect = true;
config.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate;
}
RegisterConfiguration(clientName, config);
}
public void UnregisterConfiguration(string clientName)
{
_configStore.RemoveConfiguration(clientName);
}
}

View File

@@ -0,0 +1,31 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Http;
using Microsoft.Extensions.Options;
namespace Infrastructure.Http.DynamicHttpClientSystem;
/// <summary>
/// Service collection extensions for the dynamic HTTP client system
/// </summary>
public static class DynamicHttpClientServiceCollectionExtensions
{
/// <summary>
/// Adds the dynamic HTTP client system to the service collection
/// This replaces the traditional AddHttpClients method
/// </summary>
public static IServiceCollection AddDynamicHttpClients(this IServiceCollection services)
{
// Register the dynamic system components
services.AddSingleton<IHttpClientConfigStore, HttpClientConfigStore>();
services.AddSingleton<IConfigureOptions<HttpClientFactoryOptions>, DynamicHttpClientConfiguration>();
services.AddSingleton<IDynamicHttpClientFactory, DynamicHttpClientFactory>();
// Add base HttpClient factory
services.AddHttpClient();
// Pre-register standard configurations using a hosted service
services.AddHostedService<HttpClientConfigurationService>();
return services;
}
}

View File

@@ -0,0 +1,40 @@
using System.Net;
using Common.Enums;
namespace Infrastructure.Http.DynamicHttpClientSystem;
/// <summary>
/// Configuration for a dynamic HTTP client
/// </summary>
public class HttpClientConfig
{
public string Name { get; set; } = string.Empty;
public int Timeout { get; set; }
public HttpClientType Type { get; set; }
public RetryConfig? RetryConfig { get; set; }
// Deluge-specific settings
public bool AllowAutoRedirect { get; set; } = true;
public DecompressionMethods AutomaticDecompression { get; set; } = DecompressionMethods.GZip | DecompressionMethods.Deflate;
public CertificateValidationType CertificateValidationType { get; set; } = CertificateValidationType.Enabled;
}
/// <summary>
/// Retry configuration for HTTP clients
/// </summary>
public class RetryConfig
{
public int MaxRetries { get; set; }
public bool ExcludeUnauthorized { get; set; } = true;
}
/// <summary>
/// Types of HTTP clients that can be configured
/// </summary>
public enum HttpClientType
{
Default,
WithRetry,
Deluge
}

View File

@@ -0,0 +1,38 @@
using System.Collections.Concurrent;
namespace Infrastructure.Http.DynamicHttpClientSystem;
/// <summary>
/// In-memory implementation of the HTTP client configuration store
/// </summary>
public class HttpClientConfigStore : IHttpClientConfigStore
{
private readonly ConcurrentDictionary<string, HttpClientConfig> _configurations = new();
private readonly ConcurrentDictionary<string, RetryConfig> _retryConfigurations = new();
public bool TryGetConfiguration(string clientName, out HttpClientConfig config)
{
return _configurations.TryGetValue(clientName, out config!);
}
public void AddConfiguration(string clientName, HttpClientConfig config)
{
_configurations.AddOrUpdate(clientName, config, (_, _) => config);
}
public void RemoveConfiguration(string clientName)
{
_configurations.TryRemove(clientName, out _);
_retryConfigurations.TryRemove(clientName, out _);
}
public void AddRetryConfiguration(string clientName, RetryConfig retryConfig)
{
_retryConfigurations.AddOrUpdate(clientName, retryConfig, (key, oldValue) => retryConfig);
}
public bool TryGetRetryConfiguration(string clientName, out RetryConfig retryConfig)
{
return _retryConfigurations.TryGetValue(clientName, out retryConfig!);
}
}

View File

@@ -0,0 +1,69 @@
using Common.Helpers;
using Data;
using Infrastructure.Verticals.DownloadClient.Deluge;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace Infrastructure.Http.DynamicHttpClientSystem;
/// <summary>
/// Background service to pre-register standard HttpClient configurations
/// </summary>
public class HttpClientConfigurationService : IHostedService
{
private readonly IDynamicHttpClientFactory _clientFactory;
private readonly DataContext _dataContext;
private readonly ILogger<HttpClientConfigurationService> _logger;
public HttpClientConfigurationService(
IDynamicHttpClientFactory clientFactory,
DataContext dataContext,
ILogger<HttpClientConfigurationService> logger)
{
_clientFactory = clientFactory;
_dataContext = dataContext;
_logger = logger;
}
public async Task StartAsync(CancellationToken cancellationToken)
{
try
{
var config = await _dataContext.GeneralConfigs.FirstAsync(cancellationToken);
// Register the retry client (equivalent to Constants.HttpClientWithRetryName)
_clientFactory.RegisterRetryClient(
Constants.HttpClientWithRetryName,
config.HttpTimeout,
new RetryConfig
{
MaxRetries = config.HttpMaxRetries,
ExcludeUnauthorized = true
},
config.HttpCertificateValidation
);
// Register the Deluge client
_clientFactory.RegisterDelugeClient(
nameof(DelugeService),
config.HttpTimeout,
new RetryConfig
{
MaxRetries = config.HttpMaxRetries,
ExcludeUnauthorized = true
},
config.HttpCertificateValidation
);
_logger.LogInformation("Pre-registered standard HTTP client configurations");
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to pre-register HTTP client configurations");
throw;
}
}
public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask;
}

View File

@@ -0,0 +1,44 @@
using Common.Enums;
namespace Infrastructure.Http.DynamicHttpClientSystem;
/// <summary>
/// Factory service to manage dynamic HttpClient creation
/// </summary>
public interface IDynamicHttpClientFactory
{
/// <summary>
/// Creates an HttpClient with the specified configuration and registers it for future use
/// </summary>
HttpClient CreateClient(string clientName, HttpClientConfig config);
/// <summary>
/// Creates an HttpClient using a previously registered configuration
/// </summary>
HttpClient CreateClient(string clientName);
/// <summary>
/// Registers a configuration for later use
/// </summary>
void RegisterConfiguration(string clientName, HttpClientConfig config);
/// <summary>
/// Registers a retry-enabled HttpClient configuration
/// </summary>
void RegisterRetryClient(string clientName, int timeout, RetryConfig retryConfig, CertificateValidationType certificateType);
/// <summary>
/// Registers a Deluge-specific HttpClient configuration
/// </summary>
void RegisterDelugeClient(string clientName, int timeout, RetryConfig retryConfig, CertificateValidationType certificateType);
/// <summary>
/// Registers a configuration for a download client
/// </summary>
void RegisterDownloadClient(string clientName, int timeout, HttpClientType clientType, RetryConfig retryConfig, CertificateValidationType certificateType);
/// <summary>
/// Unregisters a configuration
/// </summary>
void UnregisterConfiguration(string clientName);
}

View File

@@ -0,0 +1,27 @@
namespace Infrastructure.Http.DynamicHttpClientSystem;
/// <summary>
/// Store interface for managing HttpClient configurations dynamically
/// </summary>
public interface IHttpClientConfigStore
{
/// <summary>
/// Tries to get a configuration for the specified client name
/// </summary>
bool TryGetConfiguration(string clientName, out HttpClientConfig config);
/// <summary>
/// Adds or updates a configuration for the specified client name
/// </summary>
void AddConfiguration(string clientName, HttpClientConfig config);
/// <summary>
/// Removes a configuration for the specified client name
/// </summary>
void RemoveConfiguration(string clientName);
/// <summary>
/// Adds or updates a retry configuration for the specified client name
/// </summary>
void AddRetryConfiguration(string clientName, RetryConfig retryConfig);
}