diff --git a/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Http/Extensions/HttpClientRegistrationExtensionsTests.cs b/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Http/Extensions/HttpClientRegistrationExtensionsTests.cs index 37fc8c1..a112782 100644 --- a/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Http/Extensions/HttpClientRegistrationExtensionsTests.cs +++ b/source/AAS.TwinEngine.DataEngine.UnitTests/Infrastructure/Http/Extensions/HttpClientRegistrationExtensionsTests.cs @@ -1,4 +1,6 @@ -using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.Plugin.Config; +using System.Net; + +using AAS.TwinEngine.DataEngine.ApplicationLogic.Services.Plugin.Config; using AAS.TwinEngine.DataEngine.Infrastructure.Http.Authorization.Headers; using AAS.TwinEngine.DataEngine.Infrastructure.Http.Config; using AAS.TwinEngine.DataEngine.Infrastructure.Http.Extensions; @@ -154,6 +156,93 @@ public async Task AddHttpClientWithResilience_WithForwarding_AddsHeaderForwardin .ApplyMappings(httpContextAccessor.HttpContext, Arg.Any(), AasEnvironmentConfig.AasEnvironmentRepoHttpClientName); } + [Fact] + public void AddHttpClientWithResilience_WhenCalled_AddsAcceptEncodingHeaders() + { + var configValues = new Dictionary + { + { $"{HttpRetryPolicyOptions.Section}:{HttpRetryPolicyOptions.TemplateProvider}:MaxRetryAttempts", "3" }, + { $"{HttpRetryPolicyOptions.Section}:{HttpRetryPolicyOptions.TemplateProvider}:DelayInSeconds", "1" } + }; + IConfiguration configuration = new ConfigurationBuilder().AddInMemoryCollection(configValues!).Build(); + + var services = new ServiceCollection(); + services.Configure(HttpRetryPolicyOptions.TemplateProvider, configuration.GetSection($"{HttpRetryPolicyOptions.Section}:{HttpRetryPolicyOptions.TemplateProvider}")); + services.AddLogging(); + services.AddHttpContextAccessor(); + _ = services.AddScoped(_ => Substitute.For()); + + services.AddHttpClientWithResilience(configuration, AasEnvironmentConfig.AasEnvironmentRepoHttpClientName, HttpRetryPolicyOptions.TemplateProvider, new Uri("https://example.com")); + + var provider = services.BuildServiceProvider(); + var client = provider.GetRequiredService().CreateClient(AasEnvironmentConfig.AasEnvironmentRepoHttpClientName); + + Assert.Contains(client.DefaultRequestHeaders.AcceptEncoding, h => h.Value == "br"); + Assert.Contains(client.DefaultRequestHeaders.AcceptEncoding, h => h.Value == "gzip"); + } + + [Fact] + public void AddHttpClientWithResilience_WhenCalled_EnablesAutomaticDecompressionForGzipAndBrotli() + { + var configValues = new Dictionary + { + { $"{HttpRetryPolicyOptions.Section}:{HttpRetryPolicyOptions.TemplateProvider}:MaxRetryAttempts", "3" }, + { $"{HttpRetryPolicyOptions.Section}:{HttpRetryPolicyOptions.TemplateProvider}:DelayInSeconds", "1" } + }; + IConfiguration configuration = new ConfigurationBuilder().AddInMemoryCollection(configValues!).Build(); + + var services = new ServiceCollection(); + services.Configure(HttpRetryPolicyOptions.TemplateProvider, configuration.GetSection($"{HttpRetryPolicyOptions.Section}:{HttpRetryPolicyOptions.TemplateProvider}")); + services.AddLogging(); + services.AddHttpContextAccessor(); + _ = services.AddScoped(_ => Substitute.For()); + + services.AddHttpClientWithResilience(configuration, AasEnvironmentConfig.AasEnvironmentRepoHttpClientName, HttpRetryPolicyOptions.TemplateProvider, new Uri("https://example.com")); + + HttpMessageHandler? capturedHandler = null; + services.Configure(AasEnvironmentConfig.AasEnvironmentRepoHttpClientName, + options => options.HttpMessageHandlerBuilderActions.Add(b => capturedHandler = b.PrimaryHandler)); + + var provider = services.BuildServiceProvider(); + _ = provider.GetRequiredService().CreateClient(AasEnvironmentConfig.AasEnvironmentRepoHttpClientName); + + var handler = Assert.IsType(capturedHandler); + Assert.True(handler.AutomaticDecompression.HasFlag(DecompressionMethods.GZip)); + Assert.True(handler.AutomaticDecompression.HasFlag(DecompressionMethods.Brotli)); + } + + [Fact] + public void AddHttpClientWithoutResilience_WhenCalled_AddsAcceptEncodingHeaders() + { + var services = new ServiceCollection(); + services.AddHttpClientWithoutResilience("health-check", new Uri("https://example.com")); + + var provider = services.BuildServiceProvider(); + var client = provider.GetRequiredService().CreateClient("health-check"); + + Assert.Contains(client.DefaultRequestHeaders.AcceptEncoding, h => h.Value == "br"); + Assert.Contains(client.DefaultRequestHeaders.AcceptEncoding, h => h.Value == "gzip"); + } + + [Fact] + public void AddHttpClientWithoutResilience_WhenCalled_EnablesAutomaticDecompressionForGzipAndBrotli() + { + const string clientName = "health-check"; + var services = new ServiceCollection(); + services.AddHttpClientWithoutResilience(clientName, new Uri("https://example.com")); + + HttpMessageHandler? capturedHandler = null; + services.Configure(clientName, + options => options.HttpMessageHandlerBuilderActions.Add(b => capturedHandler = b.PrimaryHandler)); + + var provider = services.BuildServiceProvider(); + _ = provider.GetRequiredService().CreateClient(clientName); + + var handler = Assert.IsType(capturedHandler); + Assert.True(handler.AutomaticDecompression.HasFlag(DecompressionMethods.GZip)); + Assert.True(handler.AutomaticDecompression.HasFlag(DecompressionMethods.Brotli)); + } + private sealed class FaultyHttpMessageHandler : HttpMessageHandler { public int CallCount { get; private set; } diff --git a/source/AAS.TwinEngine.DataEngine/Infrastructure/Http/Extensions/HttpClientRegistrationExtensions.cs b/source/AAS.TwinEngine.DataEngine/Infrastructure/Http/Extensions/HttpClientRegistrationExtensions.cs index 72f1b91..36256ad 100644 --- a/source/AAS.TwinEngine.DataEngine/Infrastructure/Http/Extensions/HttpClientRegistrationExtensions.cs +++ b/source/AAS.TwinEngine.DataEngine/Infrastructure/Http/Extensions/HttpClientRegistrationExtensions.cs @@ -1,4 +1,5 @@ using System.Net.Http.Headers; +using System.Net; using AAS.TwinEngine.DataEngine.Infrastructure.Http.Authorization; using AAS.TwinEngine.DataEngine.Infrastructure.Http.Authorization.Headers; @@ -9,6 +10,8 @@ namespace AAS.TwinEngine.DataEngine.Infrastructure.Http.Extensions; public static class HttpClientRegistrationExtensions { + private static readonly IReadOnlyCollection AcceptEncodings = ["br", "gzip"]; + public static IServiceCollection AddHttpClientWithResilience( this IServiceCollection services, IConfiguration configuration, @@ -22,9 +25,12 @@ public static IServiceCollection AddHttpClientWithResilience( { client.BaseAddress = baseUrl; client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + ConfigureCompression(client); }) .AddStandardResilienceHandler(retryPolicySectionKey); + httpClientBuilder.ConfigurePrimaryHttpMessageHandler(CreateHandler); + _ = httpClientBuilder.AddHttpMessageHandler(sp => new HeaderForwardingHandler( sp.GetRequiredService(), @@ -45,8 +51,37 @@ public static IServiceCollection AddHttpClientWithoutResilience( client.BaseAddress = baseUrl; client.Timeout = timeout ?? TimeSpan.FromSeconds(5); client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); - }); + ConfigureCompression(client); + }) + .ConfigurePrimaryHttpMessageHandler(CreateHandler); return services; } + + private static void ConfigureCompression(HttpClient client) + { + foreach (var encoding in AcceptEncodings.Where(e => !string.IsNullOrWhiteSpace(e))) + { + client.DefaultRequestHeaders.AcceptEncoding.Add( + new StringWithQualityHeaderValue(encoding)); + } + } + + private static HttpMessageHandler CreateHandler() + { + return new HttpClientHandler + { + AutomaticDecompression = GetDecompressionMethods() + }; + } + + private static DecompressionMethods GetDecompressionMethods() + { + return AcceptEncodings.Aggregate(DecompressionMethods.None, (current, encoding) => current | encoding switch + { + "gzip" => DecompressionMethods.GZip, + "br" => DecompressionMethods.Brotli, + _ => DecompressionMethods.None + }); + } } diff --git a/source/AAS.TwinEngine.DataEngine/Program.cs b/source/AAS.TwinEngine.DataEngine/Program.cs index 22a23ee..bcdf197 100644 --- a/source/AAS.TwinEngine.DataEngine/Program.cs +++ b/source/AAS.TwinEngine.DataEngine/Program.cs @@ -6,8 +6,12 @@ using Asp.Versioning; +using Microsoft.AspNetCore.ResponseCompression; + using Serilog; +using System.IO.Compression; + namespace AAS.TwinEngine.DataEngine; public class Program @@ -18,6 +22,15 @@ public class Program public static async Task Main(string[] args) { var builder = WebApplication.CreateBuilder(args); + _ = builder.Services.AddResponseCompression(options => + { + options.EnableForHttps = true; + options.Providers.Add(); + options.Providers.Add(); + }); + _ = builder.Services.Configure(options => options.Level = CompressionLevel.Fastest); + _ = builder.Services.Configure(options => options.Level = CompressionLevel.Fastest); + _ = builder.Host.UseSerilog(); builder.ConfigureLogging(builder.Configuration); builder.ConfigureCorsServices(); @@ -71,6 +84,8 @@ public static async Task Main(string[] args) _ = app.UseExceptionHandler(); _ = app.UseMiddleware(); _ = app.UseHttpsRedirection(); + _ = app.UseResponseCompression(); + _ = app.UseAuthorization(); app.UseCorsServices(); _ = app.UseOpenApi(c => c.PostProcess = (d, _) => d.Servers.Clear());