From 9359f3b729285426b33530419ae9edeb3dc0aee3 Mon Sep 17 00:00:00 2001 From: Hardi Shah Date: Thu, 26 Mar 2026 10:55:16 +0530 Subject: [PATCH 1/6] feat: implement response compression and enhance HTTP client configuration --- .../HttpClientRegistrationExtensions.cs | 20 ++++++++++++++++++- .../Helper/RegisterPluginHttpClients.cs | 5 ++++- source/AAS.TwinEngine.DataEngine/Program.cs | 15 ++++++++++++++ ...astructureDependencyInjectionExtensions.cs | 9 +++++---- .../Program.cs | 14 +++++++++++++ 5 files changed, 57 insertions(+), 6 deletions(-) diff --git a/source/AAS.TwinEngine.DataEngine/Infrastructure/Http/Extensions/HttpClientRegistrationExtensions.cs b/source/AAS.TwinEngine.DataEngine/Infrastructure/Http/Extensions/HttpClientRegistrationExtensions.cs index 72f1b915..d36378a8 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; @@ -14,7 +15,8 @@ public static IServiceCollection AddHttpClientWithResilience( IConfiguration configuration, string clientName, string retryPolicySectionKey, - Uri baseUrl) + Uri baseUrl, + IReadOnlyCollection? acceptEncodings = null) { _ = services.Configure(configuration.GetSection($"{HttpRetryPolicyOptions.Section}:{retryPolicySectionKey}")); @@ -22,9 +24,25 @@ public static IServiceCollection AddHttpClientWithResilience( { client.BaseAddress = baseUrl; client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + + if (acceptEncodings is not null) + { + foreach (var encoding in acceptEncodings.Where(value => !string.IsNullOrWhiteSpace(value))) + { + client.DefaultRequestHeaders.AcceptEncoding.Add(new StringWithQualityHeaderValue(encoding)); + } + } }) .AddStandardResilienceHandler(retryPolicySectionKey); + if (acceptEncodings is not null && acceptEncodings.Count > 0) + { + _ = httpClientBuilder.ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler + { + AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Brotli + }); + } + _ = httpClientBuilder.AddHttpMessageHandler(sp => new HeaderForwardingHandler( sp.GetRequiredService(), diff --git a/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Helper/RegisterPluginHttpClients.cs b/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Helper/RegisterPluginHttpClients.cs index fe1e12e0..decf8e24 100644 --- a/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Helper/RegisterPluginHttpClients.cs +++ b/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Helper/RegisterPluginHttpClients.cs @@ -12,13 +12,16 @@ public static void RegisterHttpClients( IConfiguration configuration, IReadOnlyCollection manifests) { + IReadOnlyCollection acceptEncodings = ["br", "gzip"]; + foreach (var manifest in manifests) { _ = services.AddHttpClientWithResilience( configuration, $"{PluginConfig.HttpClientNamePrefix}{manifest.PluginName}", HttpRetryPolicyOptions.PluginDataProvider, - manifest.PluginUrl! + manifest.PluginUrl!, + acceptEncodings ); } } diff --git a/source/AAS.TwinEngine.DataEngine/Program.cs b/source/AAS.TwinEngine.DataEngine/Program.cs index 22a23eed..ad46b2d2 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.Optimal); + _ = builder.Services.Configure(options => options.Level = CompressionLevel.Optimal); + _ = 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()); diff --git a/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/InfrastructureDependencyInjectionExtensions.cs b/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/InfrastructureDependencyInjectionExtensions.cs index a4a92298..45b06000 100644 --- a/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/InfrastructureDependencyInjectionExtensions.cs +++ b/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/InfrastructureDependencyInjectionExtensions.cs @@ -45,6 +45,7 @@ public static void ConfigureInfrastructure(this IServiceCollection services, ICo _ = services.Configure(configuration.GetSection(Semantics.Section)); var aasEnvironment = configuration.GetSection(AasEnvironmentConfig.Section).Get(); var plugins = configuration.GetSection(PluginConfig.Section).Get(); + IReadOnlyCollection acceptEncodings = ["br", "gzip"]; _ = services.AddOptions() .Bind(configuration.GetSection(MultiLanguagePropertySettings.Section)) @@ -52,9 +53,9 @@ public static void ConfigureInfrastructure(this IServiceCollection services, ICo _ = services.AddSingleton, MultiLanguagePropertySettingsValidator>(); _ = services.Configure(configuration.GetSection(HeaderForwardingOptions.Section)); - _ = services.AddHttpClientWithResilience(configuration, AasEnvironmentConfig.AasEnvironmentRepoHttpClientName, HttpRetryPolicyOptions.TemplateProvider, aasEnvironment?.AasEnvironmentRepositoryBaseUrl!); - _ = services.AddHttpClientWithResilience(configuration, AasEnvironmentConfig.AasRegistryHttpClientName, HttpRetryPolicyOptions.TemplateProvider, aasEnvironment?.AasRegistryBaseUrl!); - _ = services.AddHttpClientWithResilience(configuration, AasEnvironmentConfig.SubmodelRegistryHttpClientName, HttpRetryPolicyOptions.SubmodelDescriptorProvider, aasEnvironment?.SubModelRegistryBaseUrl!); + _ = services.AddHttpClientWithResilience(configuration, AasEnvironmentConfig.AasEnvironmentRepoHttpClientName, HttpRetryPolicyOptions.TemplateProvider, aasEnvironment?.AasEnvironmentRepositoryBaseUrl!, acceptEncodings); + _ = services.AddHttpClientWithResilience(configuration, AasEnvironmentConfig.AasRegistryHttpClientName, HttpRetryPolicyOptions.TemplateProvider, aasEnvironment?.AasRegistryBaseUrl!, acceptEncodings); + _ = services.AddHttpClientWithResilience(configuration, AasEnvironmentConfig.SubmodelRegistryHttpClientName, HttpRetryPolicyOptions.SubmodelDescriptorProvider, aasEnvironment?.SubModelRegistryBaseUrl!, acceptEncodings); _ = services.AddHttpClientWithoutResilience(AasEnvironmentConfig.AasEnvironmentRepoHealthCheckHttpClientName, aasEnvironment?.AasEnvironmentRepositoryBaseUrl!); _ = services.AddHttpClientWithoutResilience(AasEnvironmentConfig.AasRegistryHealthCheckHttpClientName, aasEnvironment?.AasRegistryBaseUrl!); @@ -62,7 +63,7 @@ public static void ConfigureInfrastructure(this IServiceCollection services, ICo foreach (var plugin in plugins.Plugins) { - _ = services.AddHttpClientWithResilience(configuration, PluginConfig.HttpClientNamePrefix + plugin.PluginName, HttpRetryPolicyOptions.PluginDataProvider, plugin.PluginUrl); + _ = services.AddHttpClientWithResilience(configuration, PluginConfig.HttpClientNamePrefix + plugin.PluginName, HttpRetryPolicyOptions.PluginDataProvider, plugin.PluginUrl, acceptEncodings); _ = services.AddHttpClientWithoutResilience(PluginConfig.HealthCheckHttpClientNamePrefix + plugin.PluginName, plugin.PluginUrl!); } diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin/Program.cs b/source/AAS.TwinEngine.Plugin.TestPlugin/Program.cs index 16f861d1..167192c0 100644 --- a/source/AAS.TwinEngine.Plugin.TestPlugin/Program.cs +++ b/source/AAS.TwinEngine.Plugin.TestPlugin/Program.cs @@ -4,6 +4,10 @@ using Asp.Versioning; +using Microsoft.AspNetCore.ResponseCompression; + +using System.IO.Compression; + namespace AAS.TwinEngine.Plugin.TestPlugin; public static class Program @@ -14,6 +18,14 @@ public static 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.Optimal); + _ = builder.Services.Configure(options => options.Level = CompressionLevel.Optimal); builder.ConfigureLogging(builder.Configuration); @@ -54,6 +66,8 @@ public static async Task Main(string[] args) app.UseExceptionHandler(); app.UseHttpsRedirection(); + _ = app.UseResponseCompression(); + app.UseAuthorization(); app.UseOpenApi(c => c.PostProcess = (d, _) => d.Servers.Clear()); app.MapControllers(); From 6dcab2a9703313f6b2988627e1383e10b8f5dbf6 Mon Sep 17 00:00:00 2001 From: Hardi Shah Date: Thu, 26 Mar 2026 14:04:46 +0530 Subject: [PATCH 2/6] Centralize Accept-Encoding config in HttpClient extensions Removed the acceptEncodings parameter from AddHttpClientWithResilience and related calls, using a static internal list ("br", "gzip") instead. This ensures consistent Accept-Encoding headers and simplifies the API. Updated all usages and removed redundant variables. --- .../Extensions/HttpClientRegistrationExtensions.cs | 14 ++++++-------- .../Helper/RegisterPluginHttpClients.cs | 5 +---- .../InfrastructureDependencyInjectionExtensions.cs | 9 ++++----- 3 files changed, 11 insertions(+), 17 deletions(-) diff --git a/source/AAS.TwinEngine.DataEngine/Infrastructure/Http/Extensions/HttpClientRegistrationExtensions.cs b/source/AAS.TwinEngine.DataEngine/Infrastructure/Http/Extensions/HttpClientRegistrationExtensions.cs index d36378a8..578f5701 100644 --- a/source/AAS.TwinEngine.DataEngine/Infrastructure/Http/Extensions/HttpClientRegistrationExtensions.cs +++ b/source/AAS.TwinEngine.DataEngine/Infrastructure/Http/Extensions/HttpClientRegistrationExtensions.cs @@ -10,13 +10,14 @@ 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, string clientName, string retryPolicySectionKey, - Uri baseUrl, - IReadOnlyCollection? acceptEncodings = null) + Uri baseUrl) { _ = services.Configure(configuration.GetSection($"{HttpRetryPolicyOptions.Section}:{retryPolicySectionKey}")); @@ -25,17 +26,14 @@ public static IServiceCollection AddHttpClientWithResilience( client.BaseAddress = baseUrl; client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); - if (acceptEncodings is not null) + foreach (var encoding in AcceptEncodings.Where(value => !string.IsNullOrWhiteSpace(value))) { - foreach (var encoding in acceptEncodings.Where(value => !string.IsNullOrWhiteSpace(value))) - { - client.DefaultRequestHeaders.AcceptEncoding.Add(new StringWithQualityHeaderValue(encoding)); - } + client.DefaultRequestHeaders.AcceptEncoding.Add(new StringWithQualityHeaderValue(encoding)); } }) .AddStandardResilienceHandler(retryPolicySectionKey); - if (acceptEncodings is not null && acceptEncodings.Count > 0) + if (AcceptEncodings.Count > 0) { _ = httpClientBuilder.ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler { diff --git a/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Helper/RegisterPluginHttpClients.cs b/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Helper/RegisterPluginHttpClients.cs index decf8e24..fe1e12e0 100644 --- a/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Helper/RegisterPluginHttpClients.cs +++ b/source/AAS.TwinEngine.DataEngine/Infrastructure/Providers/PluginDataProvider/Helper/RegisterPluginHttpClients.cs @@ -12,16 +12,13 @@ public static void RegisterHttpClients( IConfiguration configuration, IReadOnlyCollection manifests) { - IReadOnlyCollection acceptEncodings = ["br", "gzip"]; - foreach (var manifest in manifests) { _ = services.AddHttpClientWithResilience( configuration, $"{PluginConfig.HttpClientNamePrefix}{manifest.PluginName}", HttpRetryPolicyOptions.PluginDataProvider, - manifest.PluginUrl!, - acceptEncodings + manifest.PluginUrl! ); } } diff --git a/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/InfrastructureDependencyInjectionExtensions.cs b/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/InfrastructureDependencyInjectionExtensions.cs index 45b06000..a4a92298 100644 --- a/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/InfrastructureDependencyInjectionExtensions.cs +++ b/source/AAS.TwinEngine.DataEngine/ServiceConfiguration/InfrastructureDependencyInjectionExtensions.cs @@ -45,7 +45,6 @@ public static void ConfigureInfrastructure(this IServiceCollection services, ICo _ = services.Configure(configuration.GetSection(Semantics.Section)); var aasEnvironment = configuration.GetSection(AasEnvironmentConfig.Section).Get(); var plugins = configuration.GetSection(PluginConfig.Section).Get(); - IReadOnlyCollection acceptEncodings = ["br", "gzip"]; _ = services.AddOptions() .Bind(configuration.GetSection(MultiLanguagePropertySettings.Section)) @@ -53,9 +52,9 @@ public static void ConfigureInfrastructure(this IServiceCollection services, ICo _ = services.AddSingleton, MultiLanguagePropertySettingsValidator>(); _ = services.Configure(configuration.GetSection(HeaderForwardingOptions.Section)); - _ = services.AddHttpClientWithResilience(configuration, AasEnvironmentConfig.AasEnvironmentRepoHttpClientName, HttpRetryPolicyOptions.TemplateProvider, aasEnvironment?.AasEnvironmentRepositoryBaseUrl!, acceptEncodings); - _ = services.AddHttpClientWithResilience(configuration, AasEnvironmentConfig.AasRegistryHttpClientName, HttpRetryPolicyOptions.TemplateProvider, aasEnvironment?.AasRegistryBaseUrl!, acceptEncodings); - _ = services.AddHttpClientWithResilience(configuration, AasEnvironmentConfig.SubmodelRegistryHttpClientName, HttpRetryPolicyOptions.SubmodelDescriptorProvider, aasEnvironment?.SubModelRegistryBaseUrl!, acceptEncodings); + _ = services.AddHttpClientWithResilience(configuration, AasEnvironmentConfig.AasEnvironmentRepoHttpClientName, HttpRetryPolicyOptions.TemplateProvider, aasEnvironment?.AasEnvironmentRepositoryBaseUrl!); + _ = services.AddHttpClientWithResilience(configuration, AasEnvironmentConfig.AasRegistryHttpClientName, HttpRetryPolicyOptions.TemplateProvider, aasEnvironment?.AasRegistryBaseUrl!); + _ = services.AddHttpClientWithResilience(configuration, AasEnvironmentConfig.SubmodelRegistryHttpClientName, HttpRetryPolicyOptions.SubmodelDescriptorProvider, aasEnvironment?.SubModelRegistryBaseUrl!); _ = services.AddHttpClientWithoutResilience(AasEnvironmentConfig.AasEnvironmentRepoHealthCheckHttpClientName, aasEnvironment?.AasEnvironmentRepositoryBaseUrl!); _ = services.AddHttpClientWithoutResilience(AasEnvironmentConfig.AasRegistryHealthCheckHttpClientName, aasEnvironment?.AasRegistryBaseUrl!); @@ -63,7 +62,7 @@ public static void ConfigureInfrastructure(this IServiceCollection services, ICo foreach (var plugin in plugins.Plugins) { - _ = services.AddHttpClientWithResilience(configuration, PluginConfig.HttpClientNamePrefix + plugin.PluginName, HttpRetryPolicyOptions.PluginDataProvider, plugin.PluginUrl, acceptEncodings); + _ = services.AddHttpClientWithResilience(configuration, PluginConfig.HttpClientNamePrefix + plugin.PluginName, HttpRetryPolicyOptions.PluginDataProvider, plugin.PluginUrl); _ = services.AddHttpClientWithoutResilience(PluginConfig.HealthCheckHttpClientNamePrefix + plugin.PluginName, plugin.PluginUrl!); } From 66043e3bb4c03894f16821937f1a57e587755b69 Mon Sep 17 00:00:00 2001 From: Hardi Shah Date: Thu, 26 Mar 2026 15:41:24 +0530 Subject: [PATCH 3/6] feat: implement response compression and configure Accept-Encoding headers for HttpClient --- .../HttpClientRegistrationExtensionsTests.cs | 91 ++++++++++++++++++- .../HttpClientRegistrationExtensions.cs | 30 +++++- source/AAS.TwinEngine.DataEngine/Program.cs | 5 + .../Program.cs | 14 --- 4 files changed, 123 insertions(+), 17 deletions(-) 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 37fc8c17..a112782d 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 578f5701..20fdcdf0 100644 --- a/source/AAS.TwinEngine.DataEngine/Infrastructure/Http/Extensions/HttpClientRegistrationExtensions.cs +++ b/source/AAS.TwinEngine.DataEngine/Infrastructure/Http/Extensions/HttpClientRegistrationExtensions.cs @@ -12,6 +12,23 @@ public static class HttpClientRegistrationExtensions { private static readonly IReadOnlyCollection AcceptEncodings = ["br", "gzip"]; + private static readonly DecompressionMethods AcceptEncodingDecompressionMethods = BuildDecompressionMethods(AcceptEncodings); + + private static DecompressionMethods BuildDecompressionMethods(IReadOnlyCollection encodings) + { + var methods = DecompressionMethods.None; + foreach (var encoding in encodings) + { + methods |= encoding.ToLowerInvariant() switch + { + "gzip" => DecompressionMethods.GZip, + "br" => DecompressionMethods.Brotli, + _ => DecompressionMethods.None + }; + } + return methods; + } + public static IServiceCollection AddHttpClientWithResilience( this IServiceCollection services, IConfiguration configuration, @@ -33,11 +50,11 @@ public static IServiceCollection AddHttpClientWithResilience( }) .AddStandardResilienceHandler(retryPolicySectionKey); - if (AcceptEncodings.Count > 0) + if (AcceptEncodingDecompressionMethods != DecompressionMethods.None) { _ = httpClientBuilder.ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler { - AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Brotli + AutomaticDecompression = AcceptEncodingDecompressionMethods }); } @@ -61,6 +78,15 @@ public static IServiceCollection AddHttpClientWithoutResilience( client.BaseAddress = baseUrl; client.Timeout = timeout ?? TimeSpan.FromSeconds(5); client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); + + foreach (var encoding in AcceptEncodings.Where(value => !string.IsNullOrWhiteSpace(value))) + { + client.DefaultRequestHeaders.AcceptEncoding.Add(new StringWithQualityHeaderValue(encoding)); + } + }) + .ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler + { + AutomaticDecompression = AcceptEncodingDecompressionMethods }); return services; diff --git a/source/AAS.TwinEngine.DataEngine/Program.cs b/source/AAS.TwinEngine.DataEngine/Program.cs index ad46b2d2..6eede71f 100644 --- a/source/AAS.TwinEngine.DataEngine/Program.cs +++ b/source/AAS.TwinEngine.DataEngine/Program.cs @@ -27,6 +27,11 @@ public static async Task Main(string[] args) options.EnableForHttps = true; options.Providers.Add(); options.Providers.Add(); + options.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat( + [ + "application/problem+json", + "application/problem+xml" + ]); }); _ = builder.Services.Configure(options => options.Level = CompressionLevel.Optimal); _ = builder.Services.Configure(options => options.Level = CompressionLevel.Optimal); diff --git a/source/AAS.TwinEngine.Plugin.TestPlugin/Program.cs b/source/AAS.TwinEngine.Plugin.TestPlugin/Program.cs index 167192c0..16f861d1 100644 --- a/source/AAS.TwinEngine.Plugin.TestPlugin/Program.cs +++ b/source/AAS.TwinEngine.Plugin.TestPlugin/Program.cs @@ -4,10 +4,6 @@ using Asp.Versioning; -using Microsoft.AspNetCore.ResponseCompression; - -using System.IO.Compression; - namespace AAS.TwinEngine.Plugin.TestPlugin; public static class Program @@ -18,14 +14,6 @@ public static 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.Optimal); - _ = builder.Services.Configure(options => options.Level = CompressionLevel.Optimal); builder.ConfigureLogging(builder.Configuration); @@ -66,8 +54,6 @@ public static async Task Main(string[] args) app.UseExceptionHandler(); app.UseHttpsRedirection(); - _ = app.UseResponseCompression(); - app.UseAuthorization(); app.UseOpenApi(c => c.PostProcess = (d, _) => d.Servers.Clear()); app.MapControllers(); From ce55f3986e94dc5e7401338f5622b2e8f748d5c3 Mon Sep 17 00:00:00 2001 From: Hardi Shah Date: Thu, 26 Mar 2026 15:55:10 +0530 Subject: [PATCH 4/6] Refactor HTTP client compression/decompression setup Centralize and encapsulate logic for configuring HTTP client Accept-Encoding headers and decompression methods. Introduce helper methods for setting headers and creating handlers, improving code clarity and maintainability. Remove redundant static fields and streamline decompression method selection using LINQ. --- .../HttpClientRegistrationExtensions.cs | 69 +++++++++---------- 1 file changed, 31 insertions(+), 38 deletions(-) diff --git a/source/AAS.TwinEngine.DataEngine/Infrastructure/Http/Extensions/HttpClientRegistrationExtensions.cs b/source/AAS.TwinEngine.DataEngine/Infrastructure/Http/Extensions/HttpClientRegistrationExtensions.cs index 20fdcdf0..36256adb 100644 --- a/source/AAS.TwinEngine.DataEngine/Infrastructure/Http/Extensions/HttpClientRegistrationExtensions.cs +++ b/source/AAS.TwinEngine.DataEngine/Infrastructure/Http/Extensions/HttpClientRegistrationExtensions.cs @@ -12,23 +12,6 @@ public static class HttpClientRegistrationExtensions { private static readonly IReadOnlyCollection AcceptEncodings = ["br", "gzip"]; - private static readonly DecompressionMethods AcceptEncodingDecompressionMethods = BuildDecompressionMethods(AcceptEncodings); - - private static DecompressionMethods BuildDecompressionMethods(IReadOnlyCollection encodings) - { - var methods = DecompressionMethods.None; - foreach (var encoding in encodings) - { - methods |= encoding.ToLowerInvariant() switch - { - "gzip" => DecompressionMethods.GZip, - "br" => DecompressionMethods.Brotli, - _ => DecompressionMethods.None - }; - } - return methods; - } - public static IServiceCollection AddHttpClientWithResilience( this IServiceCollection services, IConfiguration configuration, @@ -42,21 +25,11 @@ public static IServiceCollection AddHttpClientWithResilience( { client.BaseAddress = baseUrl; client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); - - foreach (var encoding in AcceptEncodings.Where(value => !string.IsNullOrWhiteSpace(value))) - { - client.DefaultRequestHeaders.AcceptEncoding.Add(new StringWithQualityHeaderValue(encoding)); - } + ConfigureCompression(client); }) .AddStandardResilienceHandler(retryPolicySectionKey); - if (AcceptEncodingDecompressionMethods != DecompressionMethods.None) - { - _ = httpClientBuilder.ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler - { - AutomaticDecompression = AcceptEncodingDecompressionMethods - }); - } + httpClientBuilder.ConfigurePrimaryHttpMessageHandler(CreateHandler); _ = httpClientBuilder.AddHttpMessageHandler(sp => new HeaderForwardingHandler( @@ -78,17 +51,37 @@ public static IServiceCollection AddHttpClientWithoutResilience( client.BaseAddress = baseUrl; client.Timeout = timeout ?? TimeSpan.FromSeconds(5); client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); - - foreach (var encoding in AcceptEncodings.Where(value => !string.IsNullOrWhiteSpace(value))) - { - client.DefaultRequestHeaders.AcceptEncoding.Add(new StringWithQualityHeaderValue(encoding)); - } + ConfigureCompression(client); }) - .ConfigurePrimaryHttpMessageHandler(() => new HttpClientHandler - { - AutomaticDecompression = AcceptEncodingDecompressionMethods - }); + .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 + }); + } } From 4875e87cc812b2385b07f8d0038cb64db6834cfd Mon Sep 17 00:00:00 2001 From: Hardi Shah Date: Thu, 26 Mar 2026 16:47:01 +0530 Subject: [PATCH 5/6] Remove custom MIME types from response compression Removed explicit inclusion of "application/problem+json" and "application/problem+xml" from the response compression middleware. The middleware now uses only the default MIME types provided by ResponseCompressionDefaults.MimeTypes. --- source/AAS.TwinEngine.DataEngine/Program.cs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/source/AAS.TwinEngine.DataEngine/Program.cs b/source/AAS.TwinEngine.DataEngine/Program.cs index 6eede71f..ad46b2d2 100644 --- a/source/AAS.TwinEngine.DataEngine/Program.cs +++ b/source/AAS.TwinEngine.DataEngine/Program.cs @@ -27,11 +27,6 @@ public static async Task Main(string[] args) options.EnableForHttps = true; options.Providers.Add(); options.Providers.Add(); - options.MimeTypes = ResponseCompressionDefaults.MimeTypes.Concat( - [ - "application/problem+json", - "application/problem+xml" - ]); }); _ = builder.Services.Configure(options => options.Level = CompressionLevel.Optimal); _ = builder.Services.Configure(options => options.Level = CompressionLevel.Optimal); From 29c48c0cd4370f753223cf228ee379bd5d7d16ff Mon Sep 17 00:00:00 2001 From: Hardi Shah Date: Fri, 27 Mar 2026 12:24:03 +0530 Subject: [PATCH 6/6] Optimize response compression settings to use Fastest level for Brotli and Gzip providers --- source/AAS.TwinEngine.DataEngine/Program.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/source/AAS.TwinEngine.DataEngine/Program.cs b/source/AAS.TwinEngine.DataEngine/Program.cs index ad46b2d2..bcdf197a 100644 --- a/source/AAS.TwinEngine.DataEngine/Program.cs +++ b/source/AAS.TwinEngine.DataEngine/Program.cs @@ -28,8 +28,8 @@ public static async Task Main(string[] args) options.Providers.Add(); options.Providers.Add(); }); - _ = builder.Services.Configure(options => options.Level = CompressionLevel.Optimal); - _ = builder.Services.Configure(options => options.Level = CompressionLevel.Optimal); + _ = builder.Services.Configure(options => options.Level = CompressionLevel.Fastest); + _ = builder.Services.Configure(options => options.Level = CompressionLevel.Fastest); _ = builder.Host.UseSerilog(); builder.ConfigureLogging(builder.Configuration);