diff --git a/ATI.Services.Consul/ATI.Services.Consul.csproj b/ATI.Services.Consul/ATI.Services.Consul.csproj index 2584fcc..a485f8a 100644 --- a/ATI.Services.Consul/ATI.Services.Consul.csproj +++ b/ATI.Services.Consul/ATI.Services.Consul.csproj @@ -17,7 +17,7 @@ - + \ No newline at end of file diff --git a/ATI.Services.Consul/Http/HttpClientBuilderExtensions.cs b/ATI.Services.Consul/Http/HttpClientBuilderExtensions.cs index c35ca9d..d97d288 100644 --- a/ATI.Services.Consul/Http/HttpClientBuilderExtensions.cs +++ b/ATI.Services.Consul/Http/HttpClientBuilderExtensions.cs @@ -1,10 +1,9 @@ using System.Threading; using ATI.Services.Common.Options; -using ATI.Services.Consul.Http; using JetBrains.Annotations; using Microsoft.Extensions.DependencyInjection; -namespace PassportVerification.test; +namespace ATI.Services.Consul.Http; [PublicAPI] public static class HttpClientBuilderExtensions @@ -16,7 +15,9 @@ public static IHttpClientBuilder WithConsul(this IHttpClientBui return httpClientBuilder .AddHttpMessageHandler>() - // infinite handler because we don't want to wait until ConsulServiceAddress will recreated and cache objects every 2 minutes (default) + // By default, handlers are alive for 2 minutes + // If we don't set InfiniteTimeSpan, every 2 minutes HttpConsulHandler will be recreated + // And it will lead to new ConsulServiceAddress instances, which constructor is pretty expensive and will stop http requests for some time .SetHandlerLifetime(Timeout.InfiniteTimeSpan); } diff --git a/ATI.Services.Consul/Http/ServiceCollectionHttpClientExtensions.cs b/ATI.Services.Consul/Http/ServiceCollectionHttpClientExtensions.cs index 23e5441..c997840 100644 --- a/ATI.Services.Consul/Http/ServiceCollectionHttpClientExtensions.cs +++ b/ATI.Services.Consul/Http/ServiceCollectionHttpClientExtensions.cs @@ -1,100 +1,64 @@ using System; -using System.Collections.Generic; -using System.Linq; using ATI.Services.Common.Http.Extensions; -using ATI.Services.Common.Logging; using ATI.Services.Common.Options; using ATI.Services.Common.Variables; using JetBrains.Annotations; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using NLog; -using PassportVerification.test; using ConfigurationManager = ATI.Services.Common.Behaviors.ConfigurationManager; -namespace ATI.Services.Common.Http; +namespace ATI.Services.Consul.Http; [PublicAPI] public static class ServiceCollectionHttpClientExtensions { - private static readonly HashSet RegisteredServiceNames = new (); - - /// - /// Dynamically add all inheritors of BaseServiceOptions as AddConsulHttpClient - /// - /// - /// - public static IServiceCollection AddConsulHttpClients(this IServiceCollection services) - { - var servicesOptionsTypes = AppDomain.CurrentDomain.GetAssemblies() - .SelectMany(assembly => assembly.GetTypes()) - .Where(type => type.IsSubclassOf(typeof(BaseServiceOptions))); - - foreach (var serviceOptionType in servicesOptionsTypes) - { - var method = typeof(ServiceCollectionHttpClientExtensions) - .GetMethod(nameof(AddConsulHttpClient), new[] { typeof(IServiceCollection) }); - var generic = method.MakeGenericMethod(serviceOptionType); - generic.Invoke(null, new[] { services }); - } - - return services; - } - /// /// Add HttpClient to HttpClientFactory with retry/cb/timeout policy /// Will add only if UseHttpClientFactory == true /// /// - /// + /// Type of the http adapter for typed HttpClient + /// /// s - public static IServiceCollection AddConsulHttpClient(this IServiceCollection services) where T : BaseServiceOptions + public static IServiceCollection AddConsulHttpClient(this IServiceCollection services) + where TAdapter : class + where TServiceOptions : BaseServiceOptions { - var className = typeof(T).Name; - var settings = ConfigurationManager.GetSection(className).Get(); + var className = typeof(TServiceOptions).Name; + var settings = ConfigurationManager.GetSection(className).Get(); if (settings == null) { throw new Exception($"Cannot find section for {className}"); } - - var serviceName = settings.ServiceName; - - var logger = LogManager.GetLogger(serviceName); - if (!settings.UseHttpClientFactory || string.IsNullOrEmpty(settings.ConsulName)) + if (string.IsNullOrEmpty(settings.ConsulName)) { - logger.WarnWithObject($"Class ${className} has UseHttpClientFactory == false OR ConsulName == null while AddCustomHttpClient"); - return services; + throw new Exception($"Class {className} has ConsulName == null while AddConsulHttpClient"); } - // Each HttpClient must be added only once, otherwise we will get exceptions like System.InvalidOperationException: The 'InnerHandler' property must be null. 'DelegatingHandler' instances provided to 'HttpMessageHandlerBuilder' must not be reused or cached. - // Handler: 'ATI.Services.Consul.Http.HttpConsulHandler - // Possible reason - because HttpConsulHandler is singleton (not transient) - // https://stackoverflow.com/questions/77542613/the-innerhandler-property-must-be-null-delegatinghandler-instances-provided - if (RegisteredServiceNames.Contains(serviceName)) - { - logger.WarnWithObject($"Class ${className} is already registered"); - return services; - } - + var serviceName = settings.ServiceName; + var logger = LogManager.GetLogger(serviceName); + var serviceVariablesOptions = ConfigurationManager.GetSection(nameof(ServiceVariablesOptions)).Get(); - services.AddHttpClient(serviceName, httpClient => + services.AddHttpClient(httpClient => { // We will override this url by consul, but we need to set it, otherwise we will get exception because HttpRequestMessage doesn't have baseUrl (only relative) httpClient.BaseAddress = new Uri("http://localhost"); httpClient.SetBaseFields(serviceVariablesOptions.GetServiceAsClientName(), serviceVariablesOptions.GetServiceAsClientHeaderName(), settings.AdditionalHeaders); }) - .WithLogging() - .WithProxyFields() + .WithLogging() + .WithProxyFields() .AddRetryPolicy(settings, logger) // Get new instance url for each retry (because 1 instance can be down) - .WithConsul() + .WithConsul() .AddHostSpecificCircuitBreakerPolicy(settings, logger) .AddTimeoutPolicy(settings.TimeOut) - .WithMetrics(); - - RegisteredServiceNames.Add(serviceName); + .WithMetrics(); + // we don't override PooledConnectionLifetime even we use HttpClient in static TAdapter + // because we are getting new host from consul for each request + // https://learn.microsoft.com/en-us/dotnet/fundamentals/networking/http/httpclient-guidelines return services; } diff --git a/ATI.Services.Consul/README.md b/ATI.Services.Consul/README.md index 89b9dbb..4a42d02 100644 --- a/ATI.Services.Consul/README.md +++ b/ATI.Services.Consul/README.md @@ -39,8 +39,7 @@ ### Http За основу взята работа с `HttClientFactory` из `atisu.services.common`, но добавлены следующие extensions: -1. `services.AddConsulHttpClient` -2. `services.AddConsulHttpClients()` - он автоматически соберет из проекта всех наследников `BaseServiceOptions`, где `ConsulName не NULL и UseHttpClientFactory = true` и для них сделает вызов `services.AddConsulHttpClient<>()` +1. `services.AddConsulHttpClient` Методы делают почти все то же самое, что и `services.AddCustomHttpClient<>`, но дополнительно: 1. Добавляется `ServiceAsClientName` хедер во все запросы