reorganized project

This commit is contained in:
Flaminel
2025-06-17 18:11:18 +03:00
parent d76216665b
commit e5d7d8ed12
264 changed files with 1137 additions and 1123 deletions

View File

@@ -0,0 +1,116 @@
using System.Net;
using Cleanuparr.Infrastructure.Services;
using Infrastructure.Services;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Http;
using Microsoft.Extensions.Options;
using Polly;
using Polly.Extensions.Http;
namespace Cleanuparr.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,165 @@
using System.Net;
using Cleanuparr.Domain.Enums;
using Cleanuparr.Persistence.Models.Configuration.General;
using Microsoft.Extensions.Logging;
namespace Cleanuparr.Infrastructure.Http.DynamicHttpClientSystem;
/// <summary>
/// Implementation of the dynamic HttpClient factory
/// </summary>
public class DynamicHttpClientFactory : IDynamicHttpClientFactory
{
private readonly IHttpClientFactory _httpClientFactory;
private readonly IHttpClientConfigStore _configStore;
private readonly IHttpClientOptionsInvalidator _optionsInvalidator;
private readonly ILogger<DynamicHttpClientFactory> _logger;
public DynamicHttpClientFactory(
IHttpClientFactory httpClientFactory,
IHttpClientConfigStore configStore,
IHttpClientOptionsInvalidator optionsInvalidator,
ILogger<DynamicHttpClientFactory> logger)
{
_httpClientFactory = httpClientFactory;
_configStore = configStore;
_optionsInvalidator = optionsInvalidator;
_logger = logger;
}
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);
// Also invalidate the cached options for this client
_optionsInvalidator.InvalidateClient(clientName);
_logger.LogDebug("Unregistered and invalidated HTTP client configuration: {ClientName}", clientName);
}
public void UpdateAllClientsFromGeneralConfig(GeneralConfig generalConfig)
{
var allConfigurations = _configStore.GetAllConfigurations().ToList();
if (!allConfigurations.Any())
{
_logger.LogDebug("No HTTP client configurations to update");
return;
}
var updatedConfigurations = allConfigurations.Select(kvp =>
{
var config = kvp.Value;
// Update timeout and certificate validation for all clients
config.Timeout = generalConfig.HttpTimeout;
config.CertificateValidationType = generalConfig.HttpCertificateValidation;
// Update retry configuration if it exists
if (config.RetryConfig != null)
{
config.RetryConfig.MaxRetries = generalConfig.HttpMaxRetries;
}
return new KeyValuePair<string, HttpClientConfig>(kvp.Key, config);
}).ToList();
// Apply all updates to our configuration store
_configStore.UpdateConfigurations(updatedConfigurations);
// CRITICAL: Invalidate IHttpClientFactory's cached configurations
// This forces the factory to call our Configure() method again with updated settings
var clientNames = updatedConfigurations.Select(kvp => kvp.Key).ToList();
_optionsInvalidator.InvalidateClients(clientNames);
_logger.LogInformation("Updated and invalidated {Count} HTTP client configurations with new general settings: " +
"Timeout={Timeout}s, MaxRetries={MaxRetries}, CertificateValidation={CertValidation}",
updatedConfigurations.Count,
generalConfig.HttpTimeout,
generalConfig.HttpMaxRetries,
generalConfig.HttpCertificateValidation);
}
public IEnumerable<string> GetRegisteredClientNames()
{
return _configStore.GetAllConfigurations().Select(kvp => kvp.Key);
}
public void InvalidateAllCachedConfigurations()
{
_optionsInvalidator.InvalidateAllClients();
_logger.LogInformation("Force invalidated all HTTP client option caches");
}
}

View File

@@ -0,0 +1,34 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Http;
using Microsoft.Extensions.Options;
namespace Cleanuparr.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>();
// Register the cache invalidation service
services.AddSingleton<IHttpClientOptionsInvalidator, HttpClientOptionsInvalidator>();
// 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 Cleanuparr.Domain.Enums;
namespace Cleanuparr.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,51 @@
using System.Collections.Concurrent;
namespace Cleanuparr.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!);
}
public IEnumerable<KeyValuePair<string, HttpClientConfig>> GetAllConfigurations()
{
return _configurations.ToList(); // Return a snapshot to avoid collection modification issues
}
public void UpdateConfigurations(IEnumerable<KeyValuePair<string, HttpClientConfig>> configurations)
{
foreach (var kvp in configurations)
{
_configurations.AddOrUpdate(kvp.Key, kvp.Value, (_, _) => kvp.Value);
}
}
}

View File

@@ -0,0 +1,71 @@
using Cleanuparr.Persistence;
using Cleanuparr.Shared.Helpers;
using Microsoft.EntityFrameworkCore;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using DelugeService = Cleanuparr.Infrastructure.Features.DownloadClient.Deluge.DelugeService;
namespace Cleanuparr.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
.AsNoTracking()
.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,72 @@
using Microsoft.Extensions.Http;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
namespace Cleanuparr.Infrastructure.Http.DynamicHttpClientSystem;
/// <summary>
/// Implementation of HTTP client options invalidator using cache manipulation
/// </summary>
public class HttpClientOptionsInvalidator : IHttpClientOptionsInvalidator
{
private readonly IOptionsMonitorCache<HttpClientFactoryOptions> _optionsCache;
private readonly ILogger<HttpClientOptionsInvalidator> _logger;
public HttpClientOptionsInvalidator(
IOptionsMonitorCache<HttpClientFactoryOptions> optionsCache,
ILogger<HttpClientOptionsInvalidator> logger)
{
_optionsCache = optionsCache;
_logger = logger;
}
public void InvalidateClient(string clientName)
{
try
{
// Remove the cached configuration for this specific client
_optionsCache.TryRemove(clientName);
_logger.LogDebug("Invalidated HTTP client options cache for client: {ClientName}", clientName);
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to invalidate HTTP client options cache for client: {ClientName}", clientName);
}
}
public void InvalidateAllClients()
{
try
{
// Clear the entire options cache
_optionsCache.Clear();
_logger.LogDebug("Invalidated all HTTP client options cache entries");
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to invalidate all HTTP client options cache entries");
}
}
public void InvalidateClients(IEnumerable<string> clientNames)
{
var clientNamesList = clientNames.ToList();
try
{
foreach (var clientName in clientNamesList)
{
_optionsCache.TryRemove(clientName);
}
_logger.LogDebug("Invalidated HTTP client options cache for {Count} clients: {ClientNames}",
clientNamesList.Count, string.Join(", ", clientNamesList));
}
catch (Exception ex)
{
_logger.LogWarning(ex, "Failed to invalidate HTTP client options cache for multiple clients");
}
}
}

View File

@@ -0,0 +1,60 @@
using Cleanuparr.Domain.Enums;
using Cleanuparr.Persistence.Models.Configuration.General;
namespace Cleanuparr.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);
/// <summary>
/// Updates all registered HTTP client configurations with new general config settings
/// </summary>
void UpdateAllClientsFromGeneralConfig(GeneralConfig generalConfig);
/// <summary>
/// Gets all currently registered client names
/// </summary>
IEnumerable<string> GetRegisteredClientNames();
/// <summary>
/// Forces cache invalidation for all registered clients (for debugging/testing)
/// </summary>
void InvalidateAllCachedConfigurations();
}

View File

@@ -0,0 +1,37 @@
namespace Cleanuparr.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);
/// <summary>
/// Gets all currently registered configurations
/// </summary>
IEnumerable<KeyValuePair<string, HttpClientConfig>> GetAllConfigurations();
/// <summary>
/// Updates multiple configurations atomically
/// </summary>
void UpdateConfigurations(IEnumerable<KeyValuePair<string, HttpClientConfig>> configurations);
}

View File

@@ -0,0 +1,24 @@
namespace Cleanuparr.Infrastructure.Http.DynamicHttpClientSystem;
/// <summary>
/// Service for invalidating cached HTTP client configurations
/// </summary>
public interface IHttpClientOptionsInvalidator
{
/// <summary>
/// Invalidates the cached configuration for a specific client name
/// </summary>
/// <param name="clientName">The name of the client to invalidate</param>
void InvalidateClient(string clientName);
/// <summary>
/// Invalidates all cached HTTP client configurations
/// </summary>
void InvalidateAllClients();
/// <summary>
/// Invalidates multiple client configurations
/// </summary>
/// <param name="clientNames">The names of the clients to invalidate</param>
void InvalidateClients(IEnumerable<string> clientNames);
}