From 506b31153d5d15257658babcb2ec3785fa2f938e Mon Sep 17 00:00:00 2001 From: Bart Koelman <104792814+bart-vmware@users.noreply.github.com> Date: Mon, 23 Mar 2026 06:14:49 +0100 Subject: [PATCH 01/29] Remove duplicate tests --- ...erverConfigurationBuilderExtensionsTest.cs | 16 -------------- ...igServerServiceCollectionExtensionsTest.cs | 21 ------------------- 2 files changed, 37 deletions(-) diff --git a/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationBuilderExtensionsTest.cs b/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationBuilderExtensionsTest.cs index 57ed14e112..bc57a9e0da 100644 --- a/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationBuilderExtensionsTest.cs +++ b/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationBuilderExtensionsTest.cs @@ -3,7 +3,6 @@ // See the LICENSE file in the project root for more information. using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Steeltoe.Common.TestResources; using Steeltoe.Configuration.CloudFoundry; @@ -172,21 +171,6 @@ public void AddConfigServer_AddsConfigServerSourceToList() source.Should().NotBeNull(); } - [Fact] - public void AddConfigServer_WithLoggerFactorySucceeds() - { - CapturingLoggerProvider loggerProvider = new(); - using var loggerFactory = new LoggerFactory([loggerProvider]); - - var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.AddConfigServer(loggerFactory); - _ = configurationBuilder.Build(); - - IList logMessages = loggerProvider.GetAll(); - - logMessages.Should().Contain("DBUG Steeltoe.Configuration.ConfigServer.ConfigServerConfigurationProvider: Fetching configuration from server(s)."); - } - [Theory] [InlineData(VcapServicesV2)] [InlineData(VcapServicesV3)] diff --git a/src/Configuration/test/ConfigServer.Test/ConfigServerServiceCollectionExtensionsTest.cs b/src/Configuration/test/ConfigServer.Test/ConfigServerServiceCollectionExtensionsTest.cs index 05fe12d3a4..9bf40b414a 100644 --- a/src/Configuration/test/ConfigServer.Test/ConfigServerServiceCollectionExtensionsTest.cs +++ b/src/Configuration/test/ConfigServer.Test/ConfigServerServiceCollectionExtensionsTest.cs @@ -2,10 +2,7 @@ // The .NET Foundation licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information. -using System.Reflection; using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyInjection; -using Microsoft.Extensions.Options; using Steeltoe.Configuration.Encryption; using Steeltoe.Configuration.Placeholder; @@ -13,24 +10,6 @@ namespace Steeltoe.Configuration.ConfigServer.Test; public sealed class ConfigServerServiceCollectionExtensionsTest { - [Fact] - public async Task ConfigureConfigServerClientOptions_ConfiguresConfigServerClientOptions_WithDefaults() - { - var builder = new ConfigurationBuilder(); - builder.AddConfigServer(); - IConfiguration configuration = builder.Build(); - - var services = new ServiceCollection(); - services.AddSingleton(configuration); - services.ConfigureConfigServerClientOptions(); - - await using ServiceProvider serviceProvider = services.BuildServiceProvider(true); - var optionsMonitor = serviceProvider.GetRequiredService>(); - - string? expectedAppName = Assembly.GetEntryAssembly()!.GetName().Name; - TestHelper.VerifyDefaults(optionsMonitor.CurrentValue, expectedAppName); - } - [Fact] public void DoesNotAddConfigServerMultipleTimes() { From de045115eb42ff983b3dea8bc55efd42dd8f7081 Mon Sep 17 00:00:00 2001 From: Bart Koelman <104792814+bart-vmware@users.noreply.github.com> Date: Wed, 25 Mar 2026 15:28:23 +0100 Subject: [PATCH 02/29] Update tests using Sandbox to use MemoryFileProvider (and without delay), to reduce test flakiness --- .../ConfigureCertificateOptionsTest.cs | 4 +- .../ApplicationInstanceInfoTest.cs | 13 +-- .../test/TestResources/MemoryFileProvider.cs | 2 - ...MemoryFileProviderAppSettingsExtensions.cs | 36 +++++++ ...eProviderConfigurationBuilderExtensions.cs | 77 ++++++++++++++ ...rConfigurationExtensionsIntegrationTest.cs | 64 +++-------- .../ConfigServerClientOptionsTest.cs | 11 +- ...rConfigurationBuilderExtensionsCoreTest.cs | 96 ++++++----------- .../PlaceholderConfigurationTest.cs | 30 +++--- .../PlaceholderWebApplicationTest.cs | 74 ++++++------- .../ConfigurationChangeDetectionTest.cs | 25 +++-- .../ConfigurationDiscoveryClientTest.cs | 11 +- .../Eureka.Test/EurekaClientOptionsTest.cs | 12 +-- .../Eureka.Test/EurekaInstanceOptionsTest.cs | 12 +-- .../RegisterMultipleDiscoveryClientsTest.cs | 27 ++--- .../DynamicConsoleLoggerProviderTest.cs | 38 ++++--- .../DynamicSerilogLoggerProviderTest.cs | 52 +++++---- .../CloudFoundry/CloudFoundryActuatorTest.cs | 38 +++---- .../Environment/EnvironmentActuatorTest.cs | 82 +++++++------- .../Actuators/Health/HealthActuatorTest.cs | 48 ++++----- .../HttpExchangesActuatorTest.cs | 38 +++---- .../Hypermedia/HypermediaActuatorTest.cs | 38 +++---- .../Loggers/LoggersActuatorSerilogTest.cs | 46 ++++---- .../Actuators/Loggers/LoggersActuatorTest.cs | 42 ++++---- .../HttpVerbInConventionalRoutingTest.cs | 48 ++++----- .../Refresh/HttpVerbInEndpointRoutingTest.cs | 48 ++++----- .../Actuators/Refresh/RefreshActuatorTest.cs | 28 ++--- .../RouteMappingsActuatorTest.cs | 28 ++--- .../test/Endpoint.Test/EndpointOptionsTest.cs | 100 +++++++++--------- .../SpringBootAdminClient/HostBuilderTest.cs | 24 ++--- 30 files changed, 600 insertions(+), 592 deletions(-) create mode 100644 src/Common/test/TestResources/MemoryFileProviderAppSettingsExtensions.cs create mode 100644 src/Common/test/TestResources/MemoryFileProviderConfigurationBuilderExtensions.cs diff --git a/src/Common/test/Certificates.Test/ConfigureCertificateOptionsTest.cs b/src/Common/test/Certificates.Test/ConfigureCertificateOptionsTest.cs index 2a93ec848b..24e9b70cf8 100644 --- a/src/Common/test/Certificates.Test/ConfigureCertificateOptionsTest.cs +++ b/src/Common/test/Certificates.Test/ConfigureCertificateOptionsTest.cs @@ -148,7 +148,7 @@ public async Task CertificateOptions_update_on_changed_contents(string certifica string secondPrivateKeyContent = await File.ReadAllTextAsync("secondInstance.key", TestContext.Current.CancellationToken); using var secondX509 = X509Certificate2.CreateFromPemFile("secondInstance.crt", "secondInstance.key"); string appSettings = BuildAppSettingsJson(certificateName, certificateFilePath, privateKeyFilePath); - string appSettingsPath = sandbox.CreateFile(MemoryFileProvider.DefaultAppSettingsFileName, appSettings); + string appSettingsPath = sandbox.CreateFile("appsettings.json", appSettings); var configurationBuilder = new ConfigurationBuilder(); configurationBuilder.AddJsonFile(appSettingsPath, false, true); IConfiguration configuration = configurationBuilder.Build(); @@ -185,7 +185,7 @@ public async Task CertificateOptions_update_on_changed_path(string certificateNa string firstPrivateKeyFilePath = sandbox.CreateFile(Guid.NewGuid() + ".key", firstPrivateKeyContent); using var secondX509 = X509Certificate2.CreateFromPemFile("secondInstance.crt", "secondInstance.key"); string appSettings = BuildAppSettingsJson(certificateName, firstCertificateFilePath, firstPrivateKeyFilePath); - string appSettingsPath = sandbox.CreateFile(MemoryFileProvider.DefaultAppSettingsFileName, appSettings); + string appSettingsPath = sandbox.CreateFile("appsettings.json", appSettings); var configurationBuilder = new ConfigurationBuilder(); configurationBuilder.AddJsonFile(appSettingsPath, false, true); IConfiguration configuration = configurationBuilder.Build(); diff --git a/src/Common/test/Common.Test/ApplicationInstanceInfoTest.cs b/src/Common/test/Common.Test/ApplicationInstanceInfoTest.cs index 94f5c04d4b..b38457aaf7 100644 --- a/src/Common/test/Common.Test/ApplicationInstanceInfoTest.cs +++ b/src/Common/test/Common.Test/ApplicationInstanceInfoTest.cs @@ -7,7 +7,6 @@ using Microsoft.Extensions.DependencyInjection; using Steeltoe.Common.Extensions; using Steeltoe.Common.TestResources; -using Steeltoe.Common.TestResources.IO; namespace Steeltoe.Common.Test; @@ -24,7 +23,7 @@ public void ConstructorSetsDefaults() [Fact] public async Task ReadsApplicationConfiguration() { - const string configJson = """ + const string appSettings = """ { "Spring": { "Application": { @@ -34,13 +33,11 @@ public async Task ReadsApplicationConfiguration() } """; - using var sandbox = new Sandbox(); - string path = sandbox.CreateFile(MemoryFileProvider.DefaultAppSettingsFileName, configJson); - string directory = Path.GetDirectoryName(path)!; - string fileName = Path.GetFileName(path); + var fileProvider = new MemoryFileProvider(); + fileProvider.IncludeAppSettingsJsonFile(appSettings); + var builder = new ConfigurationBuilder(); - builder.SetBasePath(directory); - builder.AddJsonFile(fileName); + builder.AddInMemoryAppSettingsJsonFile(fileProvider); IConfiguration configuration = builder.Build(); var services = new ServiceCollection(); diff --git a/src/Common/test/TestResources/MemoryFileProvider.cs b/src/Common/test/TestResources/MemoryFileProvider.cs index d8141e3a8a..e28f45a91d 100644 --- a/src/Common/test/TestResources/MemoryFileProvider.cs +++ b/src/Common/test/TestResources/MemoryFileProvider.cs @@ -12,8 +12,6 @@ namespace Steeltoe.Common.TestResources; public sealed class MemoryFileProvider : IFileProvider { - public const string DefaultAppSettingsFileName = "appsettings.json"; - private static readonly char[] DirectorySeparators = [ Path.DirectorySeparatorChar, diff --git a/src/Common/test/TestResources/MemoryFileProviderAppSettingsExtensions.cs b/src/Common/test/TestResources/MemoryFileProviderAppSettingsExtensions.cs new file mode 100644 index 0000000000..de791453a4 --- /dev/null +++ b/src/Common/test/TestResources/MemoryFileProviderAppSettingsExtensions.cs @@ -0,0 +1,36 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information. + +namespace Steeltoe.Common.TestResources; + +public static class MemoryFileProviderAppSettingsExtensions +{ + public static void IncludeAppSettingsJsonFile(this MemoryFileProvider fileProvider, string contents) + { + ArgumentNullException.ThrowIfNull(fileProvider); + + fileProvider.IncludeFile(MemoryFileProviderConfigurationBuilderExtensions.AppSettingsJsonFileName, contents); + } + + public static void IncludeAppSettingsXmlFile(this MemoryFileProvider fileProvider, string contents) + { + ArgumentNullException.ThrowIfNull(fileProvider); + + fileProvider.IncludeFile(MemoryFileProviderConfigurationBuilderExtensions.AppSettingsXmlFileName, contents); + } + + public static void IncludeAppSettingsIniFile(this MemoryFileProvider fileProvider, string contents) + { + ArgumentNullException.ThrowIfNull(fileProvider); + + fileProvider.IncludeFile(MemoryFileProviderConfigurationBuilderExtensions.AppSettingsIniFileName, contents); + } + + public static void ReplaceAppSettingsJsonFile(this MemoryFileProvider fileProvider, string contents) + { + ArgumentNullException.ThrowIfNull(fileProvider); + + fileProvider.ReplaceFile(MemoryFileProviderConfigurationBuilderExtensions.AppSettingsJsonFileName, contents); + } +} diff --git a/src/Common/test/TestResources/MemoryFileProviderConfigurationBuilderExtensions.cs b/src/Common/test/TestResources/MemoryFileProviderConfigurationBuilderExtensions.cs new file mode 100644 index 0000000000..fcec5b0ef4 --- /dev/null +++ b/src/Common/test/TestResources/MemoryFileProviderConfigurationBuilderExtensions.cs @@ -0,0 +1,77 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information. + +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Configuration.Ini; +using Microsoft.Extensions.Configuration.Json; +using Microsoft.Extensions.Configuration.Xml; + +namespace Steeltoe.Common.TestResources; + +public static class MemoryFileProviderConfigurationBuilderExtensions +{ + internal const string AppSettingsJsonFileName = "appsettings.json"; + internal const string AppSettingsXmlFileName = "appsettings.xml"; + internal const string AppSettingsIniFileName = "appsettings.ini"; + + public static void AddInMemoryAppSettingsJsonFile(this IConfigurationBuilder builder, MemoryFileProvider fileProvider) + { + AddInMemoryJsonFile(builder, fileProvider, AppSettingsJsonFileName); + } + + public static void AddInMemoryJsonFile(this IConfigurationBuilder builder, MemoryFileProvider fileProvider, string path) + { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(fileProvider); + ArgumentException.ThrowIfNullOrEmpty(path); + + var source = new JsonConfigurationSource + { + FileProvider = fileProvider, + Path = path, + Optional = false, + ReloadOnChange = true, + // Turn off debounce, so the change token triggers immediately. Then we don't need to sleep in tests. + ReloadDelay = 0 + }; + + builder.Add(source); + } + + public static void AddInMemoryAppSettingsXmlFile(this IConfigurationBuilder builder, MemoryFileProvider fileProvider) + { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(fileProvider); + + var source = new XmlConfigurationSource + { + FileProvider = fileProvider, + Path = AppSettingsXmlFileName, + Optional = false, + ReloadOnChange = true, + // Turn off debounce, so the change token triggers immediately. Then we don't need to sleep in tests. + ReloadDelay = 0 + }; + + builder.Add(source); + } + + public static void AddInMemoryAppSettingsIniFile(this IConfigurationBuilder builder, MemoryFileProvider fileProvider) + { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(fileProvider); + + var source = new IniConfigurationSource + { + FileProvider = fileProvider, + Path = AppSettingsIniFileName, + Optional = false, + ReloadOnChange = true, + // Turn off debounce, so the change token triggers immediately. Then we don't need to sleep in tests. + ReloadDelay = 0 + }; + + builder.Add(source); + } +} diff --git a/src/Configuration/test/ConfigServer.Integration.Test/ConfigServerConfigurationExtensionsIntegrationTest.cs b/src/Configuration/test/ConfigServer.Integration.Test/ConfigServerConfigurationExtensionsIntegrationTest.cs index f06a592b64..f8457950fe 100644 --- a/src/Configuration/test/ConfigServer.Integration.Test/ConfigServerConfigurationExtensionsIntegrationTest.cs +++ b/src/Configuration/test/ConfigServer.Integration.Test/ConfigServerConfigurationExtensionsIntegrationTest.cs @@ -6,7 +6,6 @@ using Microsoft.AspNetCore.TestHost; using Microsoft.Extensions.Configuration; using Steeltoe.Common.TestResources; -using Steeltoe.Common.TestResources.IO; namespace Steeltoe.Configuration.ConfigServer.Integration.Test; @@ -41,15 +40,11 @@ public void SpringCloudConfigServer_ReturnsExpectedDefaultData() } """; - using var sandbox = new Sandbox(); - string path = sandbox.CreateFile(MemoryFileProvider.DefaultAppSettingsFileName, appSettings); - string directory = Path.GetDirectoryName(path)!; - string fileName = Path.GetFileName(path); - var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.SetBasePath(directory); - - configurationBuilder.AddJsonFile(fileName); + var fileProvider = new MemoryFileProvider(); + fileProvider.IncludeAppSettingsJsonFile(appSettings); + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryAppSettingsJsonFile(fileProvider); configurationBuilder.AddConfigServer(); IConfigurationRoot root = configurationBuilder.Build(); @@ -85,21 +80,13 @@ public async Task SpringCloudConfigServer_ReturnsExpectedDefaultData_AsInjectedO } """; - using var sandbox = new Sandbox(); - string path = sandbox.CreateFile(MemoryFileProvider.DefaultAppSettingsFileName, appSettings); - string directory = Path.GetDirectoryName(path)!; - string fileName = Path.GetFileName(path); + var fileProvider = new MemoryFileProvider(); + fileProvider.IncludeAppSettingsJsonFile(appSettings); WebHostBuilder builder = TestWebHostBuilderFactory.Create(); builder.UseEnvironment("development"); builder.UseStartup(); - - builder.ConfigureAppConfiguration(configurationBuilder => - { - configurationBuilder.SetBasePath(directory); - configurationBuilder.AddJsonFile(fileName); - }); - + builder.ConfigureAppConfiguration(configurationBuilder => configurationBuilder.AddInMemoryAppSettingsJsonFile(fileProvider)); builder.AddConfigServer(); using IWebHost host = builder.Build(); @@ -176,21 +163,13 @@ public async Task SpringCloudConfigServer_ConfiguredViaCloudfoundryEnv_ReturnsEx } """; - using var sandbox = new Sandbox(); - string path = sandbox.CreateFile(MemoryFileProvider.DefaultAppSettingsFileName, appSettings); - string directory = Path.GetDirectoryName(path)!; - string fileName = Path.GetFileName(path); + var fileProvider = new MemoryFileProvider(); + fileProvider.IncludeAppSettingsJsonFile(appSettings); WebHostBuilder builder = TestWebHostBuilderFactory.Create(); builder.UseEnvironment("development"); builder.UseStartup(); - - builder.ConfigureAppConfiguration(configurationBuilder => - { - configurationBuilder.SetBasePath(directory); - configurationBuilder.AddJsonFile(fileName); - }); - + builder.ConfigureAppConfiguration(configurationBuilder => configurationBuilder.AddInMemoryAppSettingsJsonFile(fileProvider)); builder.AddConfigServer(); using IWebHost host = builder.Build(); @@ -231,15 +210,12 @@ public void SpringCloudConfigServer_DiscoveryFirst_ReturnsExpectedDefaultData() } """; - using var sandbox = new Sandbox(); - string path = sandbox.CreateFile(MemoryFileProvider.DefaultAppSettingsFileName, appSettings); - string directory = Path.GetDirectoryName(path)!; - string fileName = Path.GetFileName(path); + var fileProvider = new MemoryFileProvider(); + fileProvider.IncludeAppSettingsJsonFile(appSettings); var configurationBuilder = new ConfigurationBuilder(); configurationBuilder.Add(FastTestConfigurations.Discovery); - configurationBuilder.SetBasePath(directory); - configurationBuilder.AddJsonFile(fileName); + configurationBuilder.AddInMemoryAppSettingsJsonFile(fileProvider); configurationBuilder.AddConfigServer(); IConfigurationRoot root = configurationBuilder.Build(); @@ -275,20 +251,12 @@ public async Task SpringCloudConfigServer_WithHealthEnabled_ReturnsHealth() } """; - using var sandbox = new Sandbox(); - string path = sandbox.CreateFile(MemoryFileProvider.DefaultAppSettingsFileName, appSettings); - string directory = Path.GetDirectoryName(path)!; - string fileName = Path.GetFileName(path); + var fileProvider = new MemoryFileProvider(); + fileProvider.IncludeAppSettingsJsonFile(appSettings); WebHostBuilder builder = TestWebHostBuilderFactory.Create(); builder.UseStartup(); - - builder.ConfigureAppConfiguration(configurationBuilder => - { - configurationBuilder.SetBasePath(directory); - configurationBuilder.AddJsonFile(fileName); - }); - + builder.ConfigureAppConfiguration(configurationBuilder => configurationBuilder.AddInMemoryAppSettingsJsonFile(fileProvider)); builder.AddConfigServer(); using IWebHost host = builder.Build(); diff --git a/src/Configuration/test/ConfigServer.Test/ConfigServerClientOptionsTest.cs b/src/Configuration/test/ConfigServer.Test/ConfigServerClientOptionsTest.cs index 828b6dd46f..fe76426583 100644 --- a/src/Configuration/test/ConfigServer.Test/ConfigServerClientOptionsTest.cs +++ b/src/Configuration/test/ConfigServer.Test/ConfigServerClientOptionsTest.cs @@ -7,7 +7,6 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Steeltoe.Common.TestResources; -using Steeltoe.Common.TestResources.IO; namespace Steeltoe.Configuration.ConfigServer.Test; @@ -69,13 +68,11 @@ public async Task ConfigureConfigServerClientOptions_WithValues() } """; - using var sandbox = new Sandbox(); - string path = sandbox.CreateFile(MemoryFileProvider.DefaultAppSettingsFileName, appSettings); - string directory = Path.GetDirectoryName(path)!; - string fileName = Path.GetFileName(path); + var fileProvider = new MemoryFileProvider(); + fileProvider.IncludeAppSettingsJsonFile(appSettings); + var builder = new ConfigurationBuilder(); - builder.SetBasePath(directory); - builder.AddJsonFile(fileName); + builder.AddInMemoryAppSettingsJsonFile(fileProvider); IConfiguration configuration = builder.Build(); services.AddSingleton(configuration); diff --git a/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationBuilderExtensionsCoreTest.cs b/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationBuilderExtensionsCoreTest.cs index b78ec586fa..6706391ae3 100644 --- a/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationBuilderExtensionsCoreTest.cs +++ b/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationBuilderExtensionsCoreTest.cs @@ -5,7 +5,6 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using Steeltoe.Common.TestResources; -using Steeltoe.Common.TestResources.IO; using Steeltoe.Configuration.Placeholder; namespace Steeltoe.Configuration.ConfigServer.Test; @@ -81,14 +80,11 @@ public void AddConfigServer_JsonAppSettingsConfiguresClient() } """; - using var sandbox = new Sandbox(); - string path = sandbox.CreateFile(MemoryFileProvider.DefaultAppSettingsFileName, appSettings); - string directory = Path.GetDirectoryName(path)!; - string fileName = Path.GetFileName(path); + var fileProvider = new MemoryFileProvider(); + fileProvider.IncludeAppSettingsJsonFile(appSettings); var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.SetBasePath(directory); - configurationBuilder.AddJsonFile(fileName); + configurationBuilder.AddInMemoryAppSettingsJsonFile(fileProvider); configurationBuilder.AddConfigServer(); IConfigurationRoot configurationRoot = configurationBuilder.Build(); @@ -135,14 +131,11 @@ public void AddConfigServer_ValidateCertificates_DisablesCertValidation() } """; - using var sandbox = new Sandbox(); - string path = sandbox.CreateFile(MemoryFileProvider.DefaultAppSettingsFileName, appSettings); - string directory = Path.GetDirectoryName(path)!; - string fileName = Path.GetFileName(path); + var fileProvider = new MemoryFileProvider(); + fileProvider.IncludeAppSettingsJsonFile(appSettings); var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.SetBasePath(directory); - configurationBuilder.AddJsonFile(fileName); + configurationBuilder.AddInMemoryAppSettingsJsonFile(fileProvider); configurationBuilder.AddConfigServer(); IConfigurationRoot configurationRoot = configurationBuilder.Build(); @@ -169,14 +162,11 @@ public void AddConfigServer_Validate_Certificates_DisablesCertValidation() } """; - using var sandbox = new Sandbox(); - string path = sandbox.CreateFile(MemoryFileProvider.DefaultAppSettingsFileName, appSettings); - string directory = Path.GetDirectoryName(path)!; - string fileName = Path.GetFileName(path); + var fileProvider = new MemoryFileProvider(); + fileProvider.IncludeAppSettingsJsonFile(appSettings); var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.SetBasePath(directory); - configurationBuilder.AddJsonFile(fileName); + configurationBuilder.AddInMemoryAppSettingsJsonFile(fileProvider); configurationBuilder.AddConfigServer(); IConfigurationRoot configurationRoot = configurationBuilder.Build(); @@ -208,14 +198,11 @@ public void AddConfigServer_XmlAppSettingsConfiguresClient() """; - using var sandbox = new Sandbox(); - string path = sandbox.CreateFile("appsettings.xml", appSettings); - string directory = Path.GetDirectoryName(path)!; - string fileName = Path.GetFileName(path); + var fileProvider = new MemoryFileProvider(); + fileProvider.IncludeAppSettingsXmlFile(appSettings); var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.SetBasePath(directory); - configurationBuilder.AddXmlFile(fileName); + configurationBuilder.AddInMemoryAppSettingsXmlFile(fileProvider); configurationBuilder.AddConfigServer(); IConfigurationRoot configurationRoot = configurationBuilder.Build(); @@ -250,14 +237,11 @@ public void AddConfigServer_IniAppSettingsConfiguresClient() password=myPassword """; - using var sandbox = new Sandbox(); - string path = sandbox.CreateFile("appsettings.ini", appSettings); - string directory = Path.GetDirectoryName(path)!; - string fileName = Path.GetFileName(path); + var fileProvider = new MemoryFileProvider(); + fileProvider.IncludeAppSettingsIniFile(appSettings); var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.SetBasePath(directory); - configurationBuilder.AddIniFile(fileName); + configurationBuilder.AddInMemoryAppSettingsIniFile(fileProvider); configurationBuilder.AddConfigServer(); IConfigurationRoot configurationRoot = configurationBuilder.Build(); @@ -347,15 +331,11 @@ public void AddConfigServer_SubstitutesPlaceholders() } """; - using var sandbox = new Sandbox(); - string path = sandbox.CreateFile(MemoryFileProvider.DefaultAppSettingsFileName, appSettings); - - string directory = Path.GetDirectoryName(path)!; - string fileName = Path.GetFileName(path); + var fileProvider = new MemoryFileProvider(); + fileProvider.IncludeAppSettingsJsonFile(appSettings); var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.SetBasePath(directory); - configurationBuilder.AddJsonFile(fileName); + configurationBuilder.AddInMemoryAppSettingsJsonFile(fileProvider); configurationBuilder.AddPlaceholderResolver(); configurationBuilder.AddConfigServer(); IConfigurationRoot configurationRoot = configurationBuilder.Build(); @@ -442,21 +422,15 @@ public void AddConfigServer_WithCloudfoundryEnvironment_ConfiguresClientCorrectl } """; - using var sandbox = new Sandbox(); - string appSettingsPath = sandbox.CreateFile(MemoryFileProvider.DefaultAppSettingsFileName, appSettings); - string appSettingsFileName = Path.GetFileName(appSettingsPath); - - string vcapAppPath = sandbox.CreateFile("vcapapp.json", vcapApplication); - string vcapAppFileName = Path.GetFileName(vcapAppPath); - - string vcapServicesPath = sandbox.CreateFile("vcapservices.json", vcapServices); - string vcapServicesFileName = Path.GetFileName(vcapServicesPath); + var fileProvider = new MemoryFileProvider(); + fileProvider.IncludeAppSettingsJsonFile(appSettings); + fileProvider.IncludeFile("vcapapp.json", vcapApplication); + fileProvider.IncludeFile("vcapservices.json", vcapServices); var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.SetBasePath(sandbox.FullPath); - configurationBuilder.AddJsonFile(appSettingsFileName); - configurationBuilder.AddJsonFile(vcapAppFileName); - configurationBuilder.AddJsonFile(vcapServicesFileName); + configurationBuilder.AddInMemoryAppSettingsJsonFile(fileProvider); + configurationBuilder.AddInMemoryJsonFile(fileProvider, "vcapapp.json"); + configurationBuilder.AddInMemoryJsonFile(fileProvider, "vcapservices.json"); configurationBuilder.AddConfigServer(); IConfigurationRoot configurationRoot = configurationBuilder.Build(); @@ -550,21 +524,15 @@ public void AddConfigServer_WithCloudfoundryEnvironmentSCS3_ConfiguresClientCorr } """; - using var sandbox = new Sandbox(); - string appSettingsPath = sandbox.CreateFile(MemoryFileProvider.DefaultAppSettingsFileName, appSettings); - string appSettingsFileName = Path.GetFileName(appSettingsPath); - - string vcapAppPath = sandbox.CreateFile("vcapapp.json", vcapApplication); - string vcapAppFileName = Path.GetFileName(vcapAppPath); - - string vcapServicesPath = sandbox.CreateFile("vcapservices.json", vcapServices); - string vcapServicesFileName = Path.GetFileName(vcapServicesPath); + var fileProvider = new MemoryFileProvider(); + fileProvider.IncludeAppSettingsJsonFile(appSettings); + fileProvider.IncludeFile("vcapapp.json", vcapApplication); + fileProvider.IncludeFile("vcapservices.json", vcapServices); var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.SetBasePath(sandbox.FullPath); - configurationBuilder.AddJsonFile(appSettingsFileName); - configurationBuilder.AddJsonFile(vcapAppFileName); - configurationBuilder.AddJsonFile(vcapServicesFileName); + configurationBuilder.AddInMemoryAppSettingsJsonFile(fileProvider); + configurationBuilder.AddInMemoryJsonFile(fileProvider, "vcapapp.json"); + configurationBuilder.AddInMemoryJsonFile(fileProvider, "vcapservices.json"); configurationBuilder.AddConfigServer(); IConfigurationRoot configurationRoot = configurationBuilder.Build(); diff --git a/src/Configuration/test/Placeholder.Test/PlaceholderConfigurationTest.cs b/src/Configuration/test/Placeholder.Test/PlaceholderConfigurationTest.cs index 7869c46492..f9f74b68a2 100644 --- a/src/Configuration/test/Placeholder.Test/PlaceholderConfigurationTest.cs +++ b/src/Configuration/test/Placeholder.Test/PlaceholderConfigurationTest.cs @@ -336,16 +336,16 @@ public void Reloads_options_on_change(int placeholderCount) { MemoryFileProvider fileProvider = new(); - fileProvider.IncludeFile(MemoryFileProvider.DefaultAppSettingsFileName, """ - { - "TestRoot": { - "Value": "valueA" - } - } - """); + fileProvider.IncludeAppSettingsJsonFile(""" + { + "TestRoot": { + "Value": "valueA" + } + } + """); var builder = new ConfigurationBuilder(); - builder.AddJsonFile(fileProvider, MemoryFileProvider.DefaultAppSettingsFileName, false, true); + builder.AddInMemoryAppSettingsJsonFile(fileProvider); #pragma warning disable SA1312 // Variable names should begin with lower-case letter foreach (int _ in Enumerable.Repeat(0, placeholderCount)) @@ -375,13 +375,13 @@ public void Reloads_options_on_change(int placeholderCount) _ = optionsMonitor.CurrentValue; configurer.ConfigureCount.Should().Be(1); - fileProvider.ReplaceFile(MemoryFileProvider.DefaultAppSettingsFileName, """ - { - "TestRoot": { - "Value": "valueB" - } - } - """); + fileProvider.ReplaceAppSettingsJsonFile(""" + { + "TestRoot": { + "Value": "valueB" + } + } + """); fileProvider.NotifyChanged(); diff --git a/src/Configuration/test/Placeholder.Test/PlaceholderWebApplicationTest.cs b/src/Configuration/test/Placeholder.Test/PlaceholderWebApplicationTest.cs index 544688e850..3dcb81fb43 100644 --- a/src/Configuration/test/Placeholder.Test/PlaceholderWebApplicationTest.cs +++ b/src/Configuration/test/Placeholder.Test/PlaceholderWebApplicationTest.cs @@ -2,14 +2,12 @@ // The .NET Foundation licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information. -using FluentAssertions.Extensions; using Microsoft.AspNetCore.Builder; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using Steeltoe.Common.TestResources; -using Steeltoe.Common.TestResources.IO; namespace Steeltoe.Configuration.Placeholder.Test; @@ -26,16 +24,15 @@ public PlaceholderWebApplicationTest(ITestOutputHelper testOutputHelper) [Fact] public async Task Reloads_options_on_change() { - const string appSettings = """ + var fileProvider = new MemoryFileProvider(); + + fileProvider.IncludeAppSettingsJsonFile(""" { "TestRoot": { "AppName": "AppOne" } } - """; - - using var sandbox = new Sandbox(); - string path = sandbox.CreateFile(MemoryFileProvider.DefaultAppSettingsFileName, appSettings); + """); var memorySettings = new Dictionary { @@ -44,9 +41,8 @@ public async Task Reloads_options_on_change() WebApplicationBuilder builder = TestWebApplicationBuilderFactory.Create(); builder.Services.AddSingleton(_loggerFactory); - builder.Configuration.SetBasePath(sandbox.FullPath); builder.Configuration.AddInMemoryCollection(memorySettings); - builder.Configuration.AddJsonFile(MemoryFileProvider.DefaultAppSettingsFileName, false, true); + builder.Configuration.AddInMemoryAppSettingsJsonFile(fileProvider); builder.Configuration.AddPlaceholderResolver(_loggerFactory); builder.Services.Configure(builder.Configuration.GetSection("TestRoot")); builder.Services.AddSingleton, ConfigureTestOptions>(); @@ -55,27 +51,27 @@ public async Task Reloads_options_on_change() var optionsMonitor = app.Services.GetRequiredService>(); optionsMonitor.CurrentValue.Value.Should().Be("AppOne"); - await File.WriteAllTextAsync(path, """ - { - "TestRoot": { - "AppName": "AppTwo" - } - } - """, TestContext.Current.CancellationToken); + fileProvider.ReplaceAppSettingsJsonFile(""" + { + "TestRoot": { + "AppName": "AppTwo" + } + } + """); - await Task.Delay(2.Seconds(), TestContext.Current.CancellationToken); + fileProvider.NotifyChanged(); optionsMonitor.CurrentValue.Value.Should().Be("AppTwo"); - await File.WriteAllTextAsync(path, """ - { - "TestRoot": { - "AppName": "AppThree" - } - } - """, TestContext.Current.CancellationToken); + fileProvider.ReplaceAppSettingsJsonFile(""" + { + "TestRoot": { + "AppName": "AppThree" + } + } + """); - await Task.Delay(2.Seconds(), TestContext.Current.CancellationToken); + fileProvider.NotifyChanged(); optionsMonitor.CurrentValue.Value.Should().Be("AppThree"); } @@ -121,11 +117,9 @@ public void Can_rebuild_sources() [Fact] public async Task Can_substitute_across_multiple_sources() { - const string appSettingsJsonFileName = "appsettings.json"; - const string appSettingsXmlFileName = "appsettings.xml"; - const string appSettingsIniFileName = "appsettings.ini"; + var fileProvider = new MemoryFileProvider(); - const string appSettingsJsonContent = """ + fileProvider.IncludeAppSettingsJsonFile(""" { "JsonTestRoot": { "JsonSubLevel": { @@ -134,9 +128,9 @@ public async Task Can_substitute_across_multiple_sources() } } } - """; + """); - const string appSettingsXmlContent = """ + fileProvider.IncludeAppSettingsXmlFile(""" @@ -145,13 +139,13 @@ public async Task Can_substitute_across_multiple_sources() - """; + """); - const string appSettingsIniContent = """ + fileProvider.IncludeAppSettingsIniFile(""" [IniTestRoot:IniSubLevel] IniKey=IniValue CmdSource=IniTo${CmdTestRoot:CmdSubLevel:CmdKey} - """; + """); string[] appSettingsCommandLine = [ @@ -159,16 +153,10 @@ public async Task Can_substitute_across_multiple_sources() "--CmdTestRoot:CmdSubLevel:JsonSource=CmdTo${JsonTestRoot:JsonSubLevel:JsonKey}" ]; - using var sandbox = new Sandbox(); - sandbox.CreateFile(appSettingsJsonFileName, appSettingsJsonContent); - sandbox.CreateFile(appSettingsXmlFileName, appSettingsXmlContent); - sandbox.CreateFile(appSettingsIniFileName, appSettingsIniContent); - WebApplicationBuilder builder = TestWebApplicationBuilderFactory.Create(); - builder.Configuration.SetBasePath(sandbox.FullPath); - builder.Configuration.AddJsonFile(appSettingsJsonFileName); - builder.Configuration.AddXmlFile(appSettingsXmlFileName); - builder.Configuration.AddIniFile(appSettingsIniFileName); + builder.Configuration.AddInMemoryAppSettingsJsonFile(fileProvider); + builder.Configuration.AddInMemoryAppSettingsXmlFile(fileProvider); + builder.Configuration.AddInMemoryAppSettingsIniFile(fileProvider); builder.Configuration.AddCommandLine(appSettingsCommandLine); builder.Configuration.AddPlaceholderResolver(_loggerFactory); diff --git a/src/Connectors/test/Connectors.Test/ConfigurationChangeDetectionTest.cs b/src/Connectors/test/Connectors.Test/ConfigurationChangeDetectionTest.cs index 9d9aca9d3e..c16d6df3bb 100644 --- a/src/Connectors/test/Connectors.Test/ConfigurationChangeDetectionTest.cs +++ b/src/Connectors/test/Connectors.Test/ConfigurationChangeDetectionTest.cs @@ -4,7 +4,6 @@ using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; -using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Npgsql; @@ -35,9 +34,9 @@ public async Task Applies_local_configuration_changes_using_WebApplicationBuilde """; var fileProvider = new MemoryFileProvider(); - fileProvider.IncludeFile(MemoryFileProvider.DefaultAppSettingsFileName, fileContents); + fileProvider.IncludeAppSettingsJsonFile(fileContents); - builder.Configuration.AddJsonFile(fileProvider, MemoryFileProvider.DefaultAppSettingsFileName, false, true); + builder.Configuration.AddInMemoryAppSettingsJsonFile(fileProvider); builder.AddPostgreSql(configureOptions => configureOptions.DetectConfigurationChanges = true, null); await using WebApplication app = builder.Build(); @@ -63,7 +62,7 @@ public async Task Applies_local_configuration_changes_using_WebApplicationBuilde } """; - fileProvider.ReplaceFile(MemoryFileProvider.DefaultAppSettingsFileName, fileContents); + fileProvider.ReplaceAppSettingsJsonFile(fileContents); fileProvider.NotifyChanged(); connectionString = connectorFactory.Get("examplePostgreSqlService").Options.ConnectionString; @@ -83,7 +82,7 @@ public async Task Applies_local_configuration_changes_using_WebApplicationBuilde } """; - fileProvider.ReplaceFile(MemoryFileProvider.DefaultAppSettingsFileName, fileContents); + fileProvider.ReplaceAppSettingsJsonFile(fileContents); fileProvider.NotifyChanged(); connectionString = connectorFactory.Get("examplePostgreSqlService").Options.ConnectionString; @@ -110,11 +109,11 @@ public void Applies_local_configuration_changes_using_WebHostBuilder() """; var fileProvider = new MemoryFileProvider(); - fileProvider.IncludeFile(MemoryFileProvider.DefaultAppSettingsFileName, fileContents); + fileProvider.IncludeAppSettingsJsonFile(fileContents); builder.ConfigureAppConfiguration(configurationBuilder => { - configurationBuilder.AddJsonFile(fileProvider, MemoryFileProvider.DefaultAppSettingsFileName, false, true); + configurationBuilder.AddInMemoryAppSettingsJsonFile(fileProvider); configurationBuilder.ConfigurePostgreSql(options => options.DetectConfigurationChanges = true); }); @@ -142,7 +141,7 @@ public void Applies_local_configuration_changes_using_WebHostBuilder() } """; - fileProvider.ReplaceFile(MemoryFileProvider.DefaultAppSettingsFileName, fileContents); + fileProvider.ReplaceAppSettingsJsonFile(fileContents); fileProvider.NotifyChanged(); connectionString = connectorFactory.Get("examplePostgreSqlService").Options.ConnectionString; @@ -162,7 +161,7 @@ public void Applies_local_configuration_changes_using_WebHostBuilder() } """; - fileProvider.ReplaceFile(MemoryFileProvider.DefaultAppSettingsFileName, fileContents); + fileProvider.ReplaceAppSettingsJsonFile(fileContents); fileProvider.NotifyChanged(); connectionString = connectorFactory.Get("examplePostgreSqlService").Options.ConnectionString; @@ -189,11 +188,11 @@ public void Applies_local_configuration_changes_using_HostBuilder() """; var fileProvider = new MemoryFileProvider(); - fileProvider.IncludeFile(MemoryFileProvider.DefaultAppSettingsFileName, fileContents); + fileProvider.IncludeAppSettingsJsonFile(fileContents); builder.ConfigureAppConfiguration(configurationBuilder => { - configurationBuilder.AddJsonFile(fileProvider, MemoryFileProvider.DefaultAppSettingsFileName, false, true); + configurationBuilder.AddInMemoryAppSettingsJsonFile(fileProvider); configurationBuilder.ConfigurePostgreSql(options => options.DetectConfigurationChanges = true); }); @@ -221,7 +220,7 @@ public void Applies_local_configuration_changes_using_HostBuilder() } """; - fileProvider.ReplaceFile(MemoryFileProvider.DefaultAppSettingsFileName, fileContents); + fileProvider.ReplaceAppSettingsJsonFile(fileContents); fileProvider.NotifyChanged(); connectionString = connectorFactory.Get("examplePostgreSqlService").Options.ConnectionString; @@ -241,7 +240,7 @@ public void Applies_local_configuration_changes_using_HostBuilder() } """; - fileProvider.ReplaceFile(MemoryFileProvider.DefaultAppSettingsFileName, fileContents); + fileProvider.ReplaceAppSettingsJsonFile(fileContents); fileProvider.NotifyChanged(); connectionString = connectorFactory.Get("examplePostgreSqlService").Options.ConnectionString; diff --git a/src/Discovery/test/Configuration.Test/ConfigurationDiscoveryClientTest.cs b/src/Discovery/test/Configuration.Test/ConfigurationDiscoveryClientTest.cs index 90f12d9797..dcbb000c47 100644 --- a/src/Discovery/test/Configuration.Test/ConfigurationDiscoveryClientTest.cs +++ b/src/Discovery/test/Configuration.Test/ConfigurationDiscoveryClientTest.cs @@ -7,7 +7,6 @@ using Microsoft.Extensions.Hosting; using Steeltoe.Common.Discovery; using Steeltoe.Common.TestResources; -using Steeltoe.Common.TestResources.IO; namespace Steeltoe.Discovery.Configuration.Test; @@ -119,13 +118,11 @@ public async Task AddConfigurationDiscoveryClient_AddsClientWithOptions() } """; - using var sandbox = new Sandbox(); - string path = sandbox.CreateFile(MemoryFileProvider.DefaultAppSettingsFileName, appSettings); - string directory = Path.GetDirectoryName(path)!; - string fileName = Path.GetFileName(path); + var fileProvider = new MemoryFileProvider(); + fileProvider.IncludeAppSettingsJsonFile(appSettings); + var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.SetBasePath(directory); - configurationBuilder.AddJsonFile(fileName); + configurationBuilder.AddInMemoryAppSettingsJsonFile(fileProvider); IConfiguration configuration = configurationBuilder.Build(); var services = new ServiceCollection(); diff --git a/src/Discovery/test/Eureka.Test/EurekaClientOptionsTest.cs b/src/Discovery/test/Eureka.Test/EurekaClientOptionsTest.cs index 8194b53020..dabd6d4039 100644 --- a/src/Discovery/test/Eureka.Test/EurekaClientOptionsTest.cs +++ b/src/Discovery/test/Eureka.Test/EurekaClientOptionsTest.cs @@ -6,7 +6,6 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Options; using Steeltoe.Common.TestResources; -using Steeltoe.Common.TestResources.IO; using Steeltoe.Discovery.Eureka.Configuration; namespace Steeltoe.Discovery.Eureka.Test; @@ -98,14 +97,11 @@ public void Constructor_ConfiguresEurekaDiscovery_Correctly() } """; - using var sandbox = new Sandbox(); - string path = sandbox.CreateFile(MemoryFileProvider.DefaultAppSettingsFileName, appSettings); - string directory = Path.GetDirectoryName(path)!; - string fileName = Path.GetFileName(path); - var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.SetBasePath(directory); + var fileProvider = new MemoryFileProvider(); + fileProvider.IncludeAppSettingsJsonFile(appSettings); - configurationBuilder.AddJsonFile(fileName); + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryAppSettingsJsonFile(fileProvider); IConfiguration configuration = configurationBuilder.Build(); IConfigurationSection clientSection = configuration.GetSection(EurekaClientOptions.ConfigurationPrefix); diff --git a/src/Discovery/test/Eureka.Test/EurekaInstanceOptionsTest.cs b/src/Discovery/test/Eureka.Test/EurekaInstanceOptionsTest.cs index 180fe7fbf9..445262fbb4 100644 --- a/src/Discovery/test/Eureka.Test/EurekaInstanceOptionsTest.cs +++ b/src/Discovery/test/Eureka.Test/EurekaInstanceOptionsTest.cs @@ -4,7 +4,6 @@ using Microsoft.Extensions.Configuration; using Steeltoe.Common.TestResources; -using Steeltoe.Common.TestResources.IO; using Steeltoe.Discovery.Eureka.AppInfo; using Steeltoe.Discovery.Eureka.Configuration; @@ -104,14 +103,11 @@ public void Constructor_ConfiguresEurekaDiscovery_Correctly() } """; - using var sandbox = new Sandbox(); - string path = sandbox.CreateFile(MemoryFileProvider.DefaultAppSettingsFileName, appSettings); - string directory = Path.GetDirectoryName(path)!; - string fileName = Path.GetFileName(path); - var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.SetBasePath(directory); + var fileProvider = new MemoryFileProvider(); + fileProvider.IncludeAppSettingsJsonFile(appSettings); - configurationBuilder.AddJsonFile(fileName); + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryAppSettingsJsonFile(fileProvider); IConfiguration configuration = configurationBuilder.Build(); IConfigurationSection instanceSection = configuration.GetSection(EurekaInstanceOptions.ConfigurationPrefix); diff --git a/src/Discovery/test/HttpClients.Test/RegisterMultipleDiscoveryClientsTest.cs b/src/Discovery/test/HttpClients.Test/RegisterMultipleDiscoveryClientsTest.cs index 07ef73750c..a4a3a846f1 100644 --- a/src/Discovery/test/HttpClients.Test/RegisterMultipleDiscoveryClientsTest.cs +++ b/src/Discovery/test/HttpClients.Test/RegisterMultipleDiscoveryClientsTest.cs @@ -15,7 +15,6 @@ using Steeltoe.Common.HealthChecks; using Steeltoe.Common.Http.HttpClientPooling; using Steeltoe.Common.TestResources; -using Steeltoe.Common.TestResources.IO; using Steeltoe.Configuration.CloudFoundry; using Steeltoe.Configuration.CloudFoundry.ServiceBindings; using Steeltoe.Configuration.CloudFoundry.ServiceBindings.PostProcessors; @@ -57,14 +56,11 @@ public async Task WithEurekaConfiguration_AddsDiscoveryClient() } """; - using var sandbox = new Sandbox(); - string path = sandbox.CreateFile(MemoryFileProvider.DefaultAppSettingsFileName, appSettings); - string directory = Path.GetDirectoryName(path)!; - string fileName = Path.GetFileName(path); + var fileProvider = new MemoryFileProvider(); + fileProvider.IncludeAppSettingsJsonFile(appSettings); var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.SetBasePath(directory); - configurationBuilder.AddJsonFile(fileName); + configurationBuilder.AddInMemoryAppSettingsJsonFile(fileProvider); IConfiguration configuration = configurationBuilder.Build(); IServiceCollection services = new ServiceCollection(); @@ -656,14 +652,11 @@ public async Task WithConsulConfiguration_AddsDiscoveryClient() } """; - using var sandbox = new Sandbox(); - string path = sandbox.CreateFile(MemoryFileProvider.DefaultAppSettingsFileName, appSettings); - string directory = Path.GetDirectoryName(path)!; - string fileName = Path.GetFileName(path); + var fileProvider = new MemoryFileProvider(); + fileProvider.IncludeAppSettingsJsonFile(appSettings); var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.SetBasePath(directory); - configurationBuilder.AddJsonFile(fileName); + configurationBuilder.AddInMemoryAppSettingsJsonFile(fileProvider); IConfiguration configuration = configurationBuilder.Build(); var services = new ServiceCollection(); @@ -829,10 +822,12 @@ public async Task WithAppConfiguration_AddsAndWorks() } """; - using var sandbox = new Sandbox(); - string path = sandbox.CreateFile(MemoryFileProvider.DefaultAppSettingsFileName, appSettings); + var fileProvider = new MemoryFileProvider(); + fileProvider.IncludeAppSettingsJsonFile(appSettings); - IConfiguration configuration = new ConfigurationBuilder().SetBasePath(Path.GetDirectoryName(path)!).AddJsonFile(Path.GetFileName(path)).Build(); + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryAppSettingsJsonFile(fileProvider); + IConfiguration configuration = configurationBuilder.Build(); IServiceCollection services = new ServiceCollection(); services.AddOptions(); diff --git a/src/Logging/test/DynamicConsole.Test/DynamicConsoleLoggerProviderTest.cs b/src/Logging/test/DynamicConsole.Test/DynamicConsoleLoggerProviderTest.cs index 3e85796499..dce86c3f4e 100644 --- a/src/Logging/test/DynamicConsole.Test/DynamicConsoleLoggerProviderTest.cs +++ b/src/Logging/test/DynamicConsole.Test/DynamicConsoleLoggerProviderTest.cs @@ -400,19 +400,17 @@ public async Task AppliesChangedConfiguration() { MemoryFileProvider fileProvider = new(); - fileProvider.IncludeFile(MemoryFileProvider.DefaultAppSettingsFileName, """ - { - "Logging": { - "LogLevel": { - "A": "Warning" + fileProvider.IncludeAppSettingsJsonFile(""" + { + "Logging": { + "LogLevel": { + "A": "Warning" + } + } } - } - } - """); - - using IDynamicLoggerProvider provider = CreateLoggerProvider(configurationBuilder => - configurationBuilder.AddJsonFile(fileProvider, MemoryFileProvider.DefaultAppSettingsFileName, false, true)); + """); + using IDynamicLoggerProvider provider = CreateLoggerProvider(configurationBuilder => configurationBuilder.AddInMemoryAppSettingsJsonFile(fileProvider)); DynamicLoggingTestContext testContext = new(provider, _consoleOutput); await testContext.Parent.AssertMinLevelAsync(LogLevel.Warning); @@ -426,16 +424,16 @@ public async Task AppliesChangedConfiguration() await testContext.Self.AssertMinLevelAsync(LogLevel.Error, LogLevel.Warning); await testContext.Child.AssertMinLevelAsync(LogLevel.Error); - fileProvider.ReplaceFile(MemoryFileProvider.DefaultAppSettingsFileName, """ - { - "Logging": { - "LogLevel": { - "A": "Trace", - "A.B.C": "Debug" + fileProvider.ReplaceAppSettingsJsonFile(""" + { + "Logging": { + "LogLevel": { + "A": "Trace", + "A.B.C": "Debug" + } + } } - } - } - """); + """); fileProvider.NotifyChanged(); testContext.Refresh(); diff --git a/src/Logging/test/DynamicSerilog.Test/DynamicSerilogLoggerProviderTest.cs b/src/Logging/test/DynamicSerilog.Test/DynamicSerilogLoggerProviderTest.cs index 682a90fdc4..41545a488f 100644 --- a/src/Logging/test/DynamicSerilog.Test/DynamicSerilogLoggerProviderTest.cs +++ b/src/Logging/test/DynamicSerilog.Test/DynamicSerilogLoggerProviderTest.cs @@ -198,24 +198,22 @@ public void AppliesChangedConfiguration() { MemoryFileProvider fileProvider = new(); - fileProvider.IncludeFile(MemoryFileProvider.DefaultAppSettingsFileName, """ - { - "Serilog": { - "MinimumLevel": { - "Override": { - "A": "Warning" + fileProvider.IncludeAppSettingsJsonFile(""" + { + "Serilog": { + "MinimumLevel": { + "Override": { + "A": "Warning" + } + }, + "WriteTo": { + "Name": "Console" + } } - }, - "WriteTo": { - "Name": "Console" } - } - } - """); - - using IDynamicLoggerProvider provider = CreateLoggerProvider(configurationBuilder => - configurationBuilder.AddJsonFile(fileProvider, MemoryFileProvider.DefaultAppSettingsFileName, false, true)); + """); + using IDynamicLoggerProvider provider = CreateLoggerProvider(configurationBuilder => configurationBuilder.AddInMemoryAppSettingsJsonFile(fileProvider)); DynamicLoggingTestContext testContext = new(provider, _consoleOutput); testContext.Parent.AssertMinLevel(LogLevel.Warning); @@ -229,19 +227,19 @@ public void AppliesChangedConfiguration() testContext.Self.AssertMinLevel(LogLevel.Error, LogLevel.Warning); testContext.Child.AssertMinLevel(LogLevel.Error); - fileProvider.ReplaceFile(MemoryFileProvider.DefaultAppSettingsFileName, """ - { - "Serilog": { - "MinimumLevel": { - "Override": { - "A": "Verbose", - "A.B.C": "Debug" + fileProvider.ReplaceAppSettingsJsonFile(""" + { + "Serilog": { + "MinimumLevel": { + "Override": { + "A": "Verbose", + "A.B.C": "Debug" + } + }, + "WriteTo": "Console" } - }, - "WriteTo": "Console" - } - } - """); + } + """); fileProvider.NotifyChanged(); testContext.Refresh(); diff --git a/src/Management/test/Endpoint.Test/Actuators/CloudFoundry/CloudFoundryActuatorTest.cs b/src/Management/test/Endpoint.Test/Actuators/CloudFoundry/CloudFoundryActuatorTest.cs index 14f42128c6..6a4e565aea 100644 --- a/src/Management/test/Endpoint.Test/Actuators/CloudFoundry/CloudFoundryActuatorTest.cs +++ b/src/Management/test/Endpoint.Test/Actuators/CloudFoundry/CloudFoundryActuatorTest.cs @@ -522,21 +522,21 @@ public async Task Can_change_configuration_at_runtime() var fileProvider = new MemoryFileProvider(); - fileProvider.IncludeFile(MemoryFileProvider.DefaultAppSettingsFileName, """ - { - "Management": { - "Endpoints": { - "Info": { - "Enabled": false + fileProvider.IncludeAppSettingsJsonFile(""" + { + "Management": { + "Endpoints": { + "Info": { + "Enabled": false + } + } } } - } - } - """); + """); WebApplicationBuilder builder = TestWebApplicationBuilderFactory.Create(); builder.Configuration.AddCloudFoundry(); - builder.Configuration.AddJsonFile(fileProvider, MemoryFileProvider.DefaultAppSettingsFileName, false, true); + builder.Configuration.AddInMemoryAppSettingsJsonFile(fileProvider); builder.Services.AddCloudFoundryActuator(); builder.Services.AddInfoActuator(); builder.Services.AddHealthActuator(); @@ -568,17 +568,17 @@ public async Task Can_change_configuration_at_runtime() } """); - fileProvider.ReplaceFile(MemoryFileProvider.DefaultAppSettingsFileName, """ - { - "Management": { - "Endpoints": { - "Health": { - "Enabled": false + fileProvider.ReplaceAppSettingsJsonFile(""" + { + "Management": { + "Endpoints": { + "Health": { + "Enabled": false + } + } } } - } - } - """); + """); fileProvider.NotifyChanged(); diff --git a/src/Management/test/Endpoint.Test/Actuators/Environment/EnvironmentActuatorTest.cs b/src/Management/test/Endpoint.Test/Actuators/Environment/EnvironmentActuatorTest.cs index 13fe635f8b..897202ec36 100644 --- a/src/Management/test/Endpoint.Test/Actuators/Environment/EnvironmentActuatorTest.cs +++ b/src/Management/test/Endpoint.Test/Actuators/Environment/EnvironmentActuatorTest.cs @@ -323,34 +323,34 @@ public async Task Can_change_configuration_at_runtime() { var fileProvider = new MemoryFileProvider(); - fileProvider.IncludeFile(MemoryFileProvider.DefaultAppSettingsFileName, """ - { - "Management": { - "Endpoints": { - "Actuator": { - "Exposure": { - "Include": [ - "env" - ] + fileProvider.IncludeAppSettingsJsonFile(""" + { + "Management": { + "Endpoints": { + "Actuator": { + "Exposure": { + "Include": [ + "env" + ] + } + }, + "Env": { + "KeysToSanitize": [ + "Password" + ] + } } }, - "Env": { - "KeysToSanitize": [ - "Password" - ] + "TestSettings": { + "Password": "secret-password", + "AccessToken": "secret-token" } } - }, - "TestSettings": { - "Password": "secret-password", - "AccessToken": "secret-token" - } - } - """); + """); WebApplicationBuilder builder = TestWebApplicationBuilderFactory.Create(); builder.Configuration.Sources.Clear(); - builder.Configuration.AddJsonFile(fileProvider, MemoryFileProvider.DefaultAppSettingsFileName, false, true); + builder.Configuration.AddInMemoryAppSettingsJsonFile(fileProvider); builder.Services.AddEnvironmentActuator(); await using WebApplication host = builder.Build(); @@ -390,30 +390,30 @@ public async Task Can_change_configuration_at_runtime() } """); - fileProvider.ReplaceFile(MemoryFileProvider.DefaultAppSettingsFileName, """ - { - "Management": { - "Endpoints": { - "Actuator": { - "Exposure": { - "Include": [ - "env" - ] + fileProvider.ReplaceAppSettingsJsonFile(""" + { + "Management": { + "Endpoints": { + "Actuator": { + "Exposure": { + "Include": [ + "env" + ] + } + }, + "Env": { + "KeysToSanitize": [ + "AccessToken" + ] + } } }, - "Env": { - "KeysToSanitize": [ - "AccessToken" - ] + "TestSettings": { + "Password": "secret-password", + "AccessToken": "secret-token" } } - }, - "TestSettings": { - "Password": "secret-password", - "AccessToken": "secret-token" - } - } - """); + """); fileProvider.NotifyChanged(); diff --git a/src/Management/test/Endpoint.Test/Actuators/Health/HealthActuatorTest.cs b/src/Management/test/Endpoint.Test/Actuators/Health/HealthActuatorTest.cs index 2310840bea..4a5e408466 100644 --- a/src/Management/test/Endpoint.Test/Actuators/Health/HealthActuatorTest.cs +++ b/src/Management/test/Endpoint.Test/Actuators/Health/HealthActuatorTest.cs @@ -578,26 +578,26 @@ public async Task Can_change_configuration_at_runtime() { var fileProvider = new MemoryFileProvider(); - fileProvider.IncludeFile(MemoryFileProvider.DefaultAppSettingsFileName, """ - { - "Management": { - "Endpoints": { - "Health": { - "Groups": { - "ping-group": { - "include": "ping", - "ShowComponents": "Always" + fileProvider.IncludeAppSettingsJsonFile(""" + { + "Management": { + "Endpoints": { + "Health": { + "Groups": { + "ping-group": { + "include": "ping", + "ShowComponents": "Always" + } + } } } } } - } - } - """); + """); WebApplicationBuilder builder = TestWebApplicationBuilderFactory.Create(); builder.Configuration.AddInMemoryCollection(AppSettings); - builder.Configuration.AddJsonFile(fileProvider, MemoryFileProvider.DefaultAppSettingsFileName, false, true); + builder.Configuration.AddInMemoryAppSettingsJsonFile(fileProvider); builder.Services.AddHealthActuator(); WebApplication host = builder.Build(); @@ -622,21 +622,21 @@ public async Task Can_change_configuration_at_runtime() } """); - fileProvider.ReplaceFile(MemoryFileProvider.DefaultAppSettingsFileName, """ - { - "Management": { - "Endpoints": { - "Health": { - "Groups": { - "ping-group": { - "include": "ping" + fileProvider.ReplaceAppSettingsJsonFile(""" + { + "Management": { + "Endpoints": { + "Health": { + "Groups": { + "ping-group": { + "include": "ping" + } + } } } } } - } - } - """); + """); fileProvider.NotifyChanged(); diff --git a/src/Management/test/Endpoint.Test/Actuators/HttpExchanges/HttpExchangesActuatorTest.cs b/src/Management/test/Endpoint.Test/Actuators/HttpExchanges/HttpExchangesActuatorTest.cs index 25f49eb277..476a670815 100644 --- a/src/Management/test/Endpoint.Test/Actuators/HttpExchanges/HttpExchangesActuatorTest.cs +++ b/src/Management/test/Endpoint.Test/Actuators/HttpExchanges/HttpExchangesActuatorTest.cs @@ -422,17 +422,17 @@ public async Task Can_change_configuration_at_runtime() { var fileProvider = new MemoryFileProvider(); - fileProvider.IncludeFile(MemoryFileProvider.DefaultAppSettingsFileName, """ - { - "Management": { - "Endpoints": { - "HttpExchanges": { - "IncludeQueryString": false + fileProvider.IncludeAppSettingsJsonFile(""" + { + "Management": { + "Endpoints": { + "HttpExchanges": { + "IncludeQueryString": false + } + } } } - } - } - """); + """); DateTime currentTime = 19.September(2024); @@ -446,7 +446,7 @@ public async Task Can_change_configuration_at_runtime() WebApplicationBuilder builder = TestWebApplicationBuilderFactory.Create(); builder.Configuration.AddInMemoryCollection(AppSettings); - builder.Configuration.AddJsonFile(fileProvider, MemoryFileProvider.DefaultAppSettingsFileName, false, true); + builder.Configuration.AddInMemoryAppSettingsJsonFile(fileProvider); builder.Services.AddSingleton(new FakeHttpExchangeRecorder(httpExchanges)); builder.Services.AddHttpExchangesActuator(); await using WebApplication host = builder.Build(); @@ -487,17 +487,17 @@ public async Task Can_change_configuration_at_runtime() } """); - fileProvider.ReplaceFile(MemoryFileProvider.DefaultAppSettingsFileName, """ - { - "Management": { - "Endpoints": { - "HttpExchanges": { - "Reverse": false + fileProvider.ReplaceAppSettingsJsonFile(""" + { + "Management": { + "Endpoints": { + "HttpExchanges": { + "Reverse": false + } + } } } - } - } - """); + """); fileProvider.NotifyChanged(); diff --git a/src/Management/test/Endpoint.Test/Actuators/Hypermedia/HypermediaActuatorTest.cs b/src/Management/test/Endpoint.Test/Actuators/Hypermedia/HypermediaActuatorTest.cs index abba10aa7b..7325d9b4b1 100644 --- a/src/Management/test/Endpoint.Test/Actuators/Hypermedia/HypermediaActuatorTest.cs +++ b/src/Management/test/Endpoint.Test/Actuators/Hypermedia/HypermediaActuatorTest.cs @@ -452,20 +452,20 @@ public async Task Can_change_configuration_at_runtime() { var fileProvider = new MemoryFileProvider(); - fileProvider.IncludeFile(MemoryFileProvider.DefaultAppSettingsFileName, """ - { - "Management": { - "Endpoints": { - "Info": { - "Enabled": false + fileProvider.IncludeAppSettingsJsonFile(""" + { + "Management": { + "Endpoints": { + "Info": { + "Enabled": false + } + } } } - } - } - """); + """); WebApplicationBuilder builder = TestWebApplicationBuilderFactory.Create(); - builder.Configuration.AddJsonFile(fileProvider, MemoryFileProvider.DefaultAppSettingsFileName, false, true); + builder.Configuration.AddInMemoryAppSettingsJsonFile(fileProvider); builder.Services.AddHypermediaActuator(); builder.Services.AddInfoActuator(); builder.Services.AddHealthActuator(); @@ -496,17 +496,17 @@ public async Task Can_change_configuration_at_runtime() } """); - fileProvider.ReplaceFile(MemoryFileProvider.DefaultAppSettingsFileName, """ - { - "Management": { - "Endpoints": { - "Health": { - "Enabled": false + fileProvider.ReplaceAppSettingsJsonFile(""" + { + "Management": { + "Endpoints": { + "Health": { + "Enabled": false + } + } } } - } - } - """); + """); fileProvider.NotifyChanged(); diff --git a/src/Management/test/Endpoint.Test/Actuators/Loggers/LoggersActuatorSerilogTest.cs b/src/Management/test/Endpoint.Test/Actuators/Loggers/LoggersActuatorSerilogTest.cs index 70e96e837a..d7d800c214 100644 --- a/src/Management/test/Endpoint.Test/Actuators/Loggers/LoggersActuatorSerilogTest.cs +++ b/src/Management/test/Endpoint.Test/Actuators/Loggers/LoggersActuatorSerilogTest.cs @@ -272,23 +272,23 @@ public async Task Can_change_serilog_configuration_at_runtime() { var fileProvider = new MemoryFileProvider(); - fileProvider.IncludeFile(MemoryFileProvider.DefaultAppSettingsFileName, """ - { - "Serilog": { - "MinimumLevel": { - "Default": "Error", - "Override": { - "Fake": "Warning", - "Fake.Category.AtDebugLevel": "Debug" + fileProvider.IncludeAppSettingsJsonFile(""" + { + "Serilog": { + "MinimumLevel": { + "Default": "Error", + "Override": { + "Fake": "Warning", + "Fake.Category.AtDebugLevel": "Debug" + } + } } } - } - } - """); + """); WebApplicationBuilder builder = TestWebApplicationBuilderFactory.Create(); builder.Configuration.AddInMemoryCollection(AppSettings); - builder.Configuration.AddJsonFile(fileProvider, MemoryFileProvider.DefaultAppSettingsFileName, false, true); + builder.Configuration.AddInMemoryAppSettingsJsonFile(fileProvider); builder.Services.AddSingleton(); builder.Logging.AddDynamicSerilog(); builder.Services.AddLoggersActuator(); @@ -342,19 +342,19 @@ public async Task Can_change_serilog_configuration_at_runtime() } """); - fileProvider.ReplaceFile(MemoryFileProvider.DefaultAppSettingsFileName, """ - { - "Serilog": { - "MinimumLevel": { - "Default": "Information", - "Override": { - "Fake.Some": "Error", - "Fake.Category": "Warning" + fileProvider.ReplaceAppSettingsJsonFile(""" + { + "Serilog": { + "MinimumLevel": { + "Default": "Information", + "Override": { + "Fake.Some": "Error", + "Fake.Category": "Warning" + } + } } } - } - } - """); + """); fileProvider.NotifyChanged(); diff --git a/src/Management/test/Endpoint.Test/Actuators/Loggers/LoggersActuatorTest.cs b/src/Management/test/Endpoint.Test/Actuators/Loggers/LoggersActuatorTest.cs index bde4c60857..0e28b20870 100644 --- a/src/Management/test/Endpoint.Test/Actuators/Loggers/LoggersActuatorTest.cs +++ b/src/Management/test/Endpoint.Test/Actuators/Loggers/LoggersActuatorTest.cs @@ -588,21 +588,21 @@ public async Task Can_change_configuration_at_runtime() { var fileProvider = new MemoryFileProvider(); - fileProvider.IncludeFile(MemoryFileProvider.DefaultAppSettingsFileName, """ - { - "Logging": { - "LogLevel": { - "Default": "Error", - "Fake": "Warning", - "Fake.Category.AtDebugLevel": "Debug" + fileProvider.IncludeAppSettingsJsonFile(""" + { + "Logging": { + "LogLevel": { + "Default": "Error", + "Fake": "Warning", + "Fake.Category.AtDebugLevel": "Debug" + } + } } - } - } - """); + """); WebApplicationBuilder builder = TestWebApplicationBuilderFactory.Create(); builder.Configuration.AddInMemoryCollection(AppSettings); - builder.Configuration.AddJsonFile(fileProvider, MemoryFileProvider.DefaultAppSettingsFileName, false, true); + builder.Configuration.AddInMemoryAppSettingsJsonFile(fileProvider); EnsureLoggingConfigurationIsBound(builder.Logging, builder.Configuration); builder.Services.AddSingleton(); builder.Services.AddLoggersActuator(); @@ -656,17 +656,17 @@ public async Task Can_change_configuration_at_runtime() } """); - fileProvider.ReplaceFile(MemoryFileProvider.DefaultAppSettingsFileName, """ - { - "Logging": { - "LogLevel": { - "Default": "Information", - "Fake.Some": "Error", - "Fake.Category": "Warning" + fileProvider.ReplaceAppSettingsJsonFile(""" + { + "Logging": { + "LogLevel": { + "Default": "Information", + "Fake.Some": "Error", + "Fake.Category": "Warning" + } + } } - } - } - """); + """); fileProvider.NotifyChanged(); diff --git a/src/Management/test/Endpoint.Test/Actuators/Refresh/HttpVerbInConventionalRoutingTest.cs b/src/Management/test/Endpoint.Test/Actuators/Refresh/HttpVerbInConventionalRoutingTest.cs index 902db72709..bb571357e7 100644 --- a/src/Management/test/Endpoint.Test/Actuators/Refresh/HttpVerbInConventionalRoutingTest.cs +++ b/src/Management/test/Endpoint.Test/Actuators/Refresh/HttpVerbInConventionalRoutingTest.cs @@ -186,22 +186,22 @@ public async Task Can_change_allowed_verbs_at_runtime() { MemoryFileProvider fileProvider = new(); - fileProvider.IncludeFile(MemoryFileProvider.DefaultAppSettingsFileName, """ - { - "Management": { - "Endpoints": { - "Actuator": { - "Exposure": { - "Include": ["refresh"] + fileProvider.IncludeAppSettingsJsonFile(""" + { + "Management": { + "Endpoints": { + "Actuator": { + "Exposure": { + "Include": ["refresh"] + } + } } } } - } - } - """); + """); WebApplicationBuilder builder = TestWebApplicationBuilderFactory.Create(); - builder.Configuration.AddJsonFile(fileProvider, MemoryFileProvider.DefaultAppSettingsFileName, false, true); + builder.Configuration.AddInMemoryAppSettingsJsonFile(fileProvider); builder.Services.AddControllersWithViews(options => options.EnableEndpointRouting = false); builder.Services.AddRefreshActuator(); @@ -218,22 +218,22 @@ public async Task Can_change_allowed_verbs_at_runtime() HttpResponseMessage postResponse = await httpClient.PostAsync(requestUri, null, TestContext.Current.CancellationToken); postResponse.StatusCode.Should().Be(HttpStatusCode.OK); - fileProvider.ReplaceFile(MemoryFileProvider.DefaultAppSettingsFileName, """ - { - "Management": { - "Endpoints": { - "Actuator": { - "Exposure": { - "Include": ["refresh"] + fileProvider.ReplaceAppSettingsJsonFile(""" + { + "Management": { + "Endpoints": { + "Actuator": { + "Exposure": { + "Include": ["refresh"] + } + }, + "Refresh": { + "AllowedVerbs": ["GET"] + } } - }, - "Refresh": { - "AllowedVerbs": ["GET"] } } - } - } - """); + """); fileProvider.NotifyChanged(); diff --git a/src/Management/test/Endpoint.Test/Actuators/Refresh/HttpVerbInEndpointRoutingTest.cs b/src/Management/test/Endpoint.Test/Actuators/Refresh/HttpVerbInEndpointRoutingTest.cs index 3687723972..80610b9771 100644 --- a/src/Management/test/Endpoint.Test/Actuators/Refresh/HttpVerbInEndpointRoutingTest.cs +++ b/src/Management/test/Endpoint.Test/Actuators/Refresh/HttpVerbInEndpointRoutingTest.cs @@ -186,22 +186,22 @@ public async Task Can_change_allowed_verbs_at_runtime() { MemoryFileProvider fileProvider = new(); - fileProvider.IncludeFile(MemoryFileProvider.DefaultAppSettingsFileName, """ - { - "Management": { - "Endpoints": { - "Actuator": { - "Exposure": { - "Include": ["refresh"] + fileProvider.IncludeAppSettingsJsonFile(""" + { + "Management": { + "Endpoints": { + "Actuator": { + "Exposure": { + "Include": ["refresh"] + } + } } } } - } - } - """); + """); WebApplicationBuilder builder = TestWebApplicationBuilderFactory.Create(); - builder.Configuration.AddJsonFile(fileProvider, MemoryFileProvider.DefaultAppSettingsFileName, false, true); + builder.Configuration.AddInMemoryAppSettingsJsonFile(fileProvider); builder.Services.AddControllersWithViews(); builder.Services.AddRefreshActuator(); @@ -218,22 +218,22 @@ public async Task Can_change_allowed_verbs_at_runtime() HttpResponseMessage postResponse = await httpClient.PostAsync(requestUri, null, TestContext.Current.CancellationToken); postResponse.StatusCode.Should().Be(HttpStatusCode.OK); - fileProvider.ReplaceFile(MemoryFileProvider.DefaultAppSettingsFileName, """ - { - "Management": { - "Endpoints": { - "Actuator": { - "Exposure": { - "Include": ["refresh"] + fileProvider.ReplaceAppSettingsJsonFile(""" + { + "Management": { + "Endpoints": { + "Actuator": { + "Exposure": { + "Include": ["refresh"] + } + }, + "Refresh": { + "AllowedVerbs": ["GET"] + } } - }, - "Refresh": { - "AllowedVerbs": ["GET"] } } - } - } - """); + """); fileProvider.NotifyChanged(); diff --git a/src/Management/test/Endpoint.Test/Actuators/Refresh/RefreshActuatorTest.cs b/src/Management/test/Endpoint.Test/Actuators/Refresh/RefreshActuatorTest.cs index 0faf8a75af..7ede0f15bd 100644 --- a/src/Management/test/Endpoint.Test/Actuators/Refresh/RefreshActuatorTest.cs +++ b/src/Management/test/Endpoint.Test/Actuators/Refresh/RefreshActuatorTest.cs @@ -200,21 +200,21 @@ public async Task Can_change_configuration_at_runtime() { var fileProvider = new MemoryFileProvider(); - fileProvider.IncludeFile(MemoryFileProvider.DefaultAppSettingsFileName, """ - { - "Management": { - "Endpoints": { - "Refresh": { - "ReturnConfiguration": false + fileProvider.IncludeAppSettingsJsonFile(""" + { + "Management": { + "Endpoints": { + "Refresh": { + "ReturnConfiguration": false + } + } } } - } - } - """); + """); WebApplicationBuilder builder = TestWebApplicationBuilderFactory.Create(); builder.Configuration.AddInMemoryCollection(AppSettings); - builder.Configuration.AddJsonFile(fileProvider, MemoryFileProvider.DefaultAppSettingsFileName, false, true); + builder.Configuration.AddInMemoryAppSettingsJsonFile(fileProvider); builder.Services.AddRefreshActuator(); await using WebApplication host = builder.Build(); @@ -229,10 +229,10 @@ public async Task Can_change_configuration_at_runtime() responseBody1.Should().BeJson("[]"); - fileProvider.ReplaceFile(MemoryFileProvider.DefaultAppSettingsFileName, """ - { - } - """); + fileProvider.ReplaceAppSettingsJsonFile(""" + { + } + """); fileProvider.NotifyChanged(); diff --git a/src/Management/test/Endpoint.Test/Actuators/RouteMappings/RouteMappingsActuatorTest.cs b/src/Management/test/Endpoint.Test/Actuators/RouteMappings/RouteMappingsActuatorTest.cs index 3262df4951..6c63c31926 100644 --- a/src/Management/test/Endpoint.Test/Actuators/RouteMappings/RouteMappingsActuatorTest.cs +++ b/src/Management/test/Endpoint.Test/Actuators/RouteMappings/RouteMappingsActuatorTest.cs @@ -373,21 +373,21 @@ public async Task Can_change_configuration_at_runtime() { var fileProvider = new MemoryFileProvider(); - fileProvider.IncludeFile(MemoryFileProvider.DefaultAppSettingsFileName, """ - { - "Management": { - "Endpoints": { - "Mappings": { - "IncludeActuators": false + fileProvider.IncludeAppSettingsJsonFile(""" + { + "Management": { + "Endpoints": { + "Mappings": { + "IncludeActuators": false + } + } } } - } - } - """); + """); WebApplicationBuilder builder = TestWebApplicationBuilderFactory.Create(); builder.Configuration.AddInMemoryCollection(AppSettings); - builder.Configuration.AddJsonFile(fileProvider, MemoryFileProvider.DefaultAppSettingsFileName, false, true); + builder.Configuration.AddInMemoryAppSettingsJsonFile(fileProvider); builder.Services.AddAllActuators(); await using WebApplication host = builder.Build(); @@ -400,10 +400,10 @@ public async Task Can_change_configuration_at_runtime() responseNode1["contexts"]!["application"]!["mappings"]!["dispatcherServlets"]!["dispatcherServlet"].Should().BeOfType().Subject.Should() .BeEmpty(); - fileProvider.ReplaceFile(MemoryFileProvider.DefaultAppSettingsFileName, """ - { - } - """); + fileProvider.ReplaceAppSettingsJsonFile(""" + { + } + """); fileProvider.NotifyChanged(); diff --git a/src/Management/test/Endpoint.Test/EndpointOptionsTest.cs b/src/Management/test/Endpoint.Test/EndpointOptionsTest.cs index 5610cf05e4..f5d9858575 100644 --- a/src/Management/test/Endpoint.Test/EndpointOptionsTest.cs +++ b/src/Management/test/Endpoint.Test/EndpointOptionsTest.cs @@ -86,14 +86,14 @@ public async Task CanTurnOffEndpointAtRuntimeFromExposureConfiguration() { MemoryFileProvider fileProvider = new(); - fileProvider.IncludeFile(MemoryFileProvider.DefaultAppSettingsFileName, """ - { - "Management:Endpoints:Actuator:Exposure:Include:0": "env" - } - """); + fileProvider.IncludeAppSettingsJsonFile(""" + { + "Management:Endpoints:Actuator:Exposure:Include:0": "env" + } + """); WebApplicationBuilder builder = TestWebApplicationBuilderFactory.Create(); - builder.Configuration.AddJsonFile(fileProvider, MemoryFileProvider.DefaultAppSettingsFileName, false, true); + builder.Configuration.AddInMemoryAppSettingsJsonFile(fileProvider); builder.Services.AddAllActuators(); await using WebApplication app = builder.Build(); @@ -103,12 +103,12 @@ public async Task CanTurnOffEndpointAtRuntimeFromExposureConfiguration() HttpResponseMessage response1 = await httpClient.GetAsync(new Uri("/actuator/env", UriKind.Relative), TestContext.Current.CancellationToken); response1.StatusCode.Should().Be(HttpStatusCode.OK); - fileProvider.ReplaceFile(MemoryFileProvider.DefaultAppSettingsFileName, """ - { - "Management:Endpoints:Actuator:Exposure:Include:0": "env", - "Management:Endpoints:Actuator:Exposure:Exclude:0": "*" - } - """); + fileProvider.ReplaceAppSettingsJsonFile(""" + { + "Management:Endpoints:Actuator:Exposure:Include:0": "env", + "Management:Endpoints:Actuator:Exposure:Exclude:0": "*" + } + """); fileProvider.NotifyChanged(); @@ -121,15 +121,15 @@ public async Task CanTurnOnEndpointAtRuntimeFromExposureConfiguration() { MemoryFileProvider fileProvider = new(); - fileProvider.IncludeFile(MemoryFileProvider.DefaultAppSettingsFileName, """ - { - "Management:Endpoints:Actuator:Exposure:Include:0": "env", - "Management:Endpoints:Actuator:Exposure:Exclude:0": "*" - } - """); + fileProvider.IncludeAppSettingsJsonFile(""" + { + "Management:Endpoints:Actuator:Exposure:Include:0": "env", + "Management:Endpoints:Actuator:Exposure:Exclude:0": "*" + } + """); WebApplicationBuilder builder = TestWebApplicationBuilderFactory.Create(); - builder.Configuration.AddJsonFile(fileProvider, MemoryFileProvider.DefaultAppSettingsFileName, false, true); + builder.Configuration.AddInMemoryAppSettingsJsonFile(fileProvider); builder.Services.AddAllActuators(); await using WebApplication app = builder.Build(); @@ -139,11 +139,11 @@ public async Task CanTurnOnEndpointAtRuntimeFromExposureConfiguration() HttpResponseMessage response1 = await httpClient.GetAsync(new Uri("/actuator/env", UriKind.Relative), TestContext.Current.CancellationToken); response1.StatusCode.Should().Be(HttpStatusCode.NotFound); - fileProvider.ReplaceFile(MemoryFileProvider.DefaultAppSettingsFileName, """ - { - "Management:Endpoints:Actuator:Exposure:Include:0": "env" - } - """); + fileProvider.ReplaceAppSettingsJsonFile(""" + { + "Management:Endpoints:Actuator:Exposure:Include:0": "env" + } + """); fileProvider.NotifyChanged(); @@ -156,15 +156,15 @@ public async Task CanTurnOffEndpointAtRuntimeFromEndpointConfiguration() { MemoryFileProvider fileProvider = new(); - fileProvider.IncludeFile(MemoryFileProvider.DefaultAppSettingsFileName, """ - { - "Management:Endpoints:Actuator:Exposure:Include:0": "env", - "Management:Endpoints:Env:Enabled": "true" - } - """); + fileProvider.IncludeAppSettingsJsonFile(""" + { + "Management:Endpoints:Actuator:Exposure:Include:0": "env", + "Management:Endpoints:Env:Enabled": "true" + } + """); WebApplicationBuilder builder = TestWebApplicationBuilderFactory.Create(); - builder.Configuration.AddJsonFile(fileProvider, MemoryFileProvider.DefaultAppSettingsFileName, false, true); + builder.Configuration.AddInMemoryAppSettingsJsonFile(fileProvider); builder.Services.AddAllActuators(); await using WebApplication app = builder.Build(); @@ -174,12 +174,12 @@ public async Task CanTurnOffEndpointAtRuntimeFromEndpointConfiguration() HttpResponseMessage response1 = await httpClient.GetAsync(new Uri("/actuator/env", UriKind.Relative), TestContext.Current.CancellationToken); response1.StatusCode.Should().Be(HttpStatusCode.OK); - fileProvider.ReplaceFile(MemoryFileProvider.DefaultAppSettingsFileName, """ - { - "Management:Endpoints:Actuator:Exposure:Include:0": "env", - "Management:Endpoints:Env:Enabled": "false" - } - """); + fileProvider.ReplaceAppSettingsJsonFile(""" + { + "Management:Endpoints:Actuator:Exposure:Include:0": "env", + "Management:Endpoints:Env:Enabled": "false" + } + """); fileProvider.NotifyChanged(); @@ -192,15 +192,15 @@ public async Task CanTurnOnEndpointAtRuntimeFromEndpointConfiguration() { MemoryFileProvider fileProvider = new(); - fileProvider.IncludeFile(MemoryFileProvider.DefaultAppSettingsFileName, """ - { - "Management:Endpoints:Actuator:Exposure:Include:0": "env", - "Management:Endpoints:Env:Enabled": "false" - } - """); + fileProvider.IncludeAppSettingsJsonFile(""" + { + "Management:Endpoints:Actuator:Exposure:Include:0": "env", + "Management:Endpoints:Env:Enabled": "false" + } + """); WebApplicationBuilder builder = TestWebApplicationBuilderFactory.Create(); - builder.Configuration.AddJsonFile(fileProvider, MemoryFileProvider.DefaultAppSettingsFileName, false, true); + builder.Configuration.AddInMemoryAppSettingsJsonFile(fileProvider); builder.Services.AddAllActuators(); await using WebApplication app = builder.Build(); @@ -210,12 +210,12 @@ public async Task CanTurnOnEndpointAtRuntimeFromEndpointConfiguration() HttpResponseMessage response1 = await httpClient.GetAsync(new Uri("/actuator/env", UriKind.Relative), TestContext.Current.CancellationToken); response1.StatusCode.Should().Be(HttpStatusCode.NotFound); - fileProvider.ReplaceFile(MemoryFileProvider.DefaultAppSettingsFileName, """ - { - "Management:Endpoints:Actuator:Exposure:Include:0": "env", - "Management:Endpoints:Env:Enabled": "true" - } - """); + fileProvider.ReplaceAppSettingsJsonFile(""" + { + "Management:Endpoints:Actuator:Exposure:Include:0": "env", + "Management:Endpoints:Env:Enabled": "true" + } + """); fileProvider.NotifyChanged(); diff --git a/src/Management/test/Endpoint.Test/SpringBootAdminClient/HostBuilderTest.cs b/src/Management/test/Endpoint.Test/SpringBootAdminClient/HostBuilderTest.cs index 7687a578c4..81dfc18dee 100644 --- a/src/Management/test/Endpoint.Test/SpringBootAdminClient/HostBuilderTest.cs +++ b/src/Management/test/Endpoint.Test/SpringBootAdminClient/HostBuilderTest.cs @@ -154,7 +154,7 @@ public async Task PeriodicRefreshCanBeTurnedOnAfterStart() { var fileProvider = new MemoryFileProvider(); - fileProvider.IncludeFile(MemoryFileProvider.DefaultAppSettingsFileName, $$""" + fileProvider.IncludeAppSettingsJsonFile($$""" { "Spring": { "Boot": { @@ -171,7 +171,7 @@ public async Task PeriodicRefreshCanBeTurnedOnAfterStart() """); WebApplicationBuilder builder = TestWebApplicationBuilderFactory.CreateDefault(false); - builder.Configuration.AddJsonFile(fileProvider, MemoryFileProvider.DefaultAppSettingsFileName, false, true); + builder.Configuration.AddInMemoryAppSettingsJsonFile(fileProvider); builder.Services.AddSingleton(); builder.Services.AddSpringBootAdminClient(); @@ -186,7 +186,7 @@ public async Task PeriodicRefreshCanBeTurnedOnAfterStart() handler.Mock.GetMatchCount(registerMock).Should().Be(1); - fileProvider.ReplaceFile(MemoryFileProvider.DefaultAppSettingsFileName, $$""" + fileProvider.ReplaceAppSettingsJsonFile($$""" { "Spring": { "Boot": { @@ -213,7 +213,7 @@ public async Task PeriodicRefreshCanBeTurnedOffAfterStart() { var fileProvider = new MemoryFileProvider(); - fileProvider.IncludeFile(MemoryFileProvider.DefaultAppSettingsFileName, $$""" + fileProvider.IncludeAppSettingsJsonFile($$""" { "Spring": { "Boot": { @@ -230,7 +230,7 @@ public async Task PeriodicRefreshCanBeTurnedOffAfterStart() """); WebApplicationBuilder builder = TestWebApplicationBuilderFactory.CreateDefault(false); - builder.Configuration.AddJsonFile(fileProvider, MemoryFileProvider.DefaultAppSettingsFileName, false, true); + builder.Configuration.AddInMemoryAppSettingsJsonFile(fileProvider); builder.Services.AddSingleton(); builder.Services.AddSpringBootAdminClient(); @@ -246,7 +246,7 @@ public async Task PeriodicRefreshCanBeTurnedOffAfterStart() handler.Mock.GetMatchCount(registerMock).Should().BeGreaterThan(1); - fileProvider.ReplaceFile(MemoryFileProvider.DefaultAppSettingsFileName, $$""" + fileProvider.ReplaceAppSettingsJsonFile($$""" { "Spring": { "Boot": { @@ -370,7 +370,7 @@ public async Task UnregistersFromPreviousServerOnConfigurationChange() { var fileProvider = new MemoryFileProvider(); - fileProvider.IncludeFile(MemoryFileProvider.DefaultAppSettingsFileName, $$""" + fileProvider.IncludeAppSettingsJsonFile($$""" { "Spring": { "Boot": { @@ -387,7 +387,7 @@ public async Task UnregistersFromPreviousServerOnConfigurationChange() """); WebApplicationBuilder builder = TestWebApplicationBuilderFactory.CreateDefault(false); - builder.Configuration.AddJsonFile(fileProvider, MemoryFileProvider.DefaultAppSettingsFileName, false, true); + builder.Configuration.AddInMemoryAppSettingsJsonFile(fileProvider); builder.Services.AddSingleton(); builder.Services.AddSpringBootAdminClient(); @@ -405,7 +405,7 @@ public async Task UnregistersFromPreviousServerOnConfigurationChange() handler.Mock.GetMatchCount(registerMock1).Should().BeGreaterThan(1); - fileProvider.ReplaceFile(MemoryFileProvider.DefaultAppSettingsFileName, $$""" + fileProvider.ReplaceAppSettingsJsonFile($$""" { "Spring": { "Boot": { @@ -434,7 +434,7 @@ public async Task UnregistersFromPreviousServerOnShutdownAfterConfigurationBecam { var fileProvider = new MemoryFileProvider(); - fileProvider.IncludeFile(MemoryFileProvider.DefaultAppSettingsFileName, $$""" + fileProvider.IncludeAppSettingsJsonFile($$""" { "Spring": { "Boot": { @@ -451,7 +451,7 @@ public async Task UnregistersFromPreviousServerOnShutdownAfterConfigurationBecam """); WebApplicationBuilder builder = TestWebApplicationBuilderFactory.CreateDefault(false); - builder.Configuration.AddJsonFile(fileProvider, MemoryFileProvider.DefaultAppSettingsFileName, false, true); + builder.Configuration.AddInMemoryAppSettingsJsonFile(fileProvider); builder.Services.AddSingleton(); builder.Services.AddSpringBootAdminClient(); @@ -470,7 +470,7 @@ public async Task UnregistersFromPreviousServerOnShutdownAfterConfigurationBecam handler.Mock.GetMatchCount(registerMock1).Should().BeGreaterThan(1); - fileProvider.ReplaceFile(MemoryFileProvider.DefaultAppSettingsFileName, $$""" + fileProvider.ReplaceAppSettingsJsonFile($$""" { "Spring": { "Boot": { From 3d5b1e8c10bfdfda76c7852bf5ef8b53c53d23c2 Mon Sep 17 00:00:00 2001 From: Bart Koelman <104792814+bart-vmware@users.noreply.github.com> Date: Mon, 23 Mar 2026 06:47:51 +0100 Subject: [PATCH 03/29] Update existing test to verify precedence between options and appsettings --- ...erverConfigurationBuilderExtensionsTest.cs | 37 +++++++++++++------ 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationBuilderExtensionsTest.cs b/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationBuilderExtensionsTest.cs index bc57a9e0da..72e81c2591 100644 --- a/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationBuilderExtensionsTest.cs +++ b/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationBuilderExtensionsTest.cs @@ -204,34 +204,47 @@ public void AddConfigServer_VCAP_SERVICES_Override_Defaults(string vcapServices) } [Fact] - public void AddConfigServer_PaysAttentionToSettings() + public void AddConfigServer_ConfigurationOverridesOptionsFromCode() { var options = new ConfigServerClientOptions { - Name = "testConfigName", - Label = "testConfigLabel", - Environment = "testEnv", - Username = "testUser", - Password = "testPassword", + Name = "nameInOptions", + Label = "labelInOptions", + Environment = "environmentInOptions", + Username = "usernameInOptions", + Password = "passwordInOptions", Timeout = 10, Retry = { - Enabled = false + InitialInterval = 5, + MaxInterval = 15 } }; + var appSettings = new Dictionary + { + ["Spring:Cloud:Config:Name"] = "nameInAppSettings", + ["Spring:Cloud:Config:Label"] = "labelInAppSettings", + ["Spring:Cloud:Config:Timeout"] = "50", + ["Spring:Cloud:Config:Retry:MaxInterval"] = "100" + }; + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(appSettings); configurationBuilder.AddConfigServer(options, NullLoggerFactory.Instance); IConfigurationRoot configurationRoot = configurationBuilder.Build(); ConfigServerConfigurationProvider? provider = configurationRoot.EnumerateProviders().FirstOrDefault(); provider.Should().NotBeNull(); - provider.ClientOptions.Label.Should().Be("testConfigLabel"); - provider.ClientOptions.Name.Should().Be("testConfigName"); - provider.ClientOptions.Environment.Should().Be("testEnv"); - provider.ClientOptions.Username.Should().Be("testUser"); - provider.ClientOptions.Password.Should().Be("testPassword"); + provider.ClientOptions.Name.Should().Be("nameInAppSettings"); + provider.ClientOptions.Label.Should().Be("labelInAppSettings"); + provider.ClientOptions.Environment.Should().Be("environmentInOptions"); + provider.ClientOptions.Username.Should().Be("usernameInOptions"); + provider.ClientOptions.Password.Should().Be("passwordInOptions"); + provider.ClientOptions.Timeout.Should().Be(50); + provider.ClientOptions.Retry.InitialInterval.Should().Be(5); + provider.ClientOptions.Retry.MaxInterval.Should().Be(100); } [Fact] From 1dc382b43f74a47f6897a9ff44960e2102d35bb0 Mon Sep 17 00:00:00 2001 From: Bart Koelman <104792814+bart-vmware@users.noreply.github.com> Date: Fri, 27 Mar 2026 16:25:30 +0100 Subject: [PATCH 04/29] Make ConfigServerClientOptions reactive to configuration changes `ConfigServerClientOptions` are now re-evaluated from initial options + configuration on every settings change, instead of being mutated in-place. This prevents stale or torn reads when the provider runs concurrently (e.g. polling timer vs reload). Key changes: - The provider clones options on each configuration reload, starting from the initial options passed via code, then applying configuration on top. This ensures code-level defaults are restored when keys are removed from configuration. - Client settings (spring:cloud:config:*) are no longer written into the provider's data dictionary, eliminating a circular feedback loop where the provider's own output could influence its input. - Discovery lookup results are tracked separately and applied on top of the options snapshot at load time, rather than mutating shared state. - Client certificate configuration is moved from the source's `Build` method into `ConfigureConfigServerClientOptions`, so it participates in the options pipeline. - `ConfigServerClientOptions.Clone()` is introduced to produce isolated snapshots, preventing tearing when options are read during a concurrent reload. A bug was fixed that prevented using global certificates. - An internal `HttpClientHandler` parameter is threaded through the source and builder extensions, enabling direct handler injection for tests without reflection. - `IOptionsChangeTokenSource` is registered in DI so that `IOptionsMonitor` properly triggers on configuration changes. --- .../ConfigServer/ConfigServerClientOptions.cs | 62 ++- ...figServerConfigurationBuilderExtensions.cs | 10 +- .../ConfigServerConfigurationProvider.cs | 352 ++++++++------- .../ConfigServerConfigurationSource.cs | 32 +- .../ConfigServerDiscoveryService.cs | 2 +- ...ConfigServerServiceCollectionExtensions.cs | 3 + .../ConfigureConfigServerClientOptions.cs | 30 +- .../ConfigServerClientOptionsTest.cs | 412 ++++++++++++++++++ .../ConfigServerClientOptionsTest.cs | 311 +++++++++++++ ...erverConfigurationBuilderExtensionsTest.cs | 70 ++- ...ServerConfigurationProviderTest.Loading.cs | 187 +++++--- ...erverConfigurationProviderTest.Settings.cs | 26 +- .../ConfigServerConfigurationProviderTest.cs | 77 ++-- .../ConfigServerConfigurationSourceTest.cs | 6 +- .../ConfigServer.Test/TestServiceInstance.cs | 20 + 15 files changed, 1258 insertions(+), 342 deletions(-) create mode 100644 src/Configuration/test/ConfigServer.Discovery.Test/ConfigServerClientOptionsTest.cs create mode 100644 src/Configuration/test/ConfigServer.Test/TestServiceInstance.cs diff --git a/src/Configuration/src/ConfigServer/ConfigServerClientOptions.cs b/src/Configuration/src/ConfigServer/ConfigServerClientOptions.cs index d9ce59f505..b7c3c76259 100644 --- a/src/Configuration/src/ConfigServer/ConfigServerClientOptions.cs +++ b/src/Configuration/src/ConfigServer/ConfigServerClientOptions.cs @@ -17,10 +17,14 @@ public sealed class ConfigServerClientOptions : IValidateCertificatesOptions private const char CommaDelimiter = ','; internal const string ConfigurationPrefix = "spring:cloud:config"; - internal CertificateOptions ClientCertificate { get; } = new(); internal TimeSpan HttpTimeout => TimeSpan.FromMilliseconds(Timeout); internal bool IsMultiServerConfiguration => Uri != null && Uri.Contains(CommaDelimiter); + /// + /// Gets the client certificate used for mutual TLS authentication with the Config Server. + /// + internal CertificateOptions ClientCertificate { get; private set; } = new(); + /// /// Gets or sets a value indicating whether the Config Server provider is enabled. Default value: true. /// @@ -96,17 +100,17 @@ public bool ValidateCertificatesAlt /// /// Gets retry settings. /// - public ConfigServerRetryOptions Retry { get; } = new(); + public ConfigServerRetryOptions Retry { get; private set; } = new(); /// /// Gets service discovery settings. /// - public ConfigServerDiscoveryOptions Discovery { get; } = new(); + public ConfigServerDiscoveryOptions Discovery { get; private set; } = new(); /// /// Gets health check settings. /// - public ConfigServerHealthOptions Health { get; } = new(); + public ConfigServerHealthOptions Health { get; private set; } = new(); /// /// Gets or sets the address used by the provider to obtain a OAuth Access Token. @@ -141,7 +145,55 @@ public bool ValidateCertificatesAlt /// /// Gets headers that will be added to the Config Server request. /// - public IDictionary Headers { get; } = new Dictionary(); + public IDictionary Headers { get; private set; } = new Dictionary(); + + internal ConfigServerClientOptions Clone() + { + return new ConfigServerClientOptions + { + ClientCertificate = new CertificateOptions + { + Certificate = ClientCertificate.Certificate + }, + Enabled = Enabled, + FailFast = FailFast, + Environment = Environment, + Label = Label, + Name = Name, + Uri = Uri, + Username = Username, + Password = Password, + Token = Token, + Timeout = Timeout, + PollingInterval = PollingInterval, + ValidateCertificates = ValidateCertificates, + Retry = new ConfigServerRetryOptions + { + Enabled = Retry.Enabled, + InitialInterval = Retry.InitialInterval, + MaxInterval = Retry.MaxInterval, + Multiplier = Retry.Multiplier, + MaxAttempts = Retry.MaxAttempts + }, + Discovery = new ConfigServerDiscoveryOptions + { + Enabled = Discovery.Enabled, + ServiceId = Discovery.ServiceId + }, + Health = new ConfigServerHealthOptions + { + Enabled = Health.Enabled, + TimeToLive = Health.TimeToLive + }, + AccessTokenUri = AccessTokenUri, + ClientSecret = ClientSecret, + ClientId = ClientId, + TokenTtl = TokenTtl, + TokenRenewRate = TokenRenewRate, + DisableTokenRenewal = DisableTokenRenewal, + Headers = new Dictionary(Headers) + }; + } internal List GetUris() { diff --git a/src/Configuration/src/ConfigServer/ConfigServerConfigurationBuilderExtensions.cs b/src/Configuration/src/ConfigServer/ConfigServerConfigurationBuilderExtensions.cs index 187eff2ff8..d2fa42b6f3 100644 --- a/src/Configuration/src/ConfigServer/ConfigServerConfigurationBuilderExtensions.cs +++ b/src/Configuration/src/ConfigServer/ConfigServerConfigurationBuilderExtensions.cs @@ -64,6 +64,12 @@ public static IConfigurationBuilder AddConfigServer(this IConfigurationBuilder b /// The incoming so that additional calls can be chained. /// public static IConfigurationBuilder AddConfigServer(this IConfigurationBuilder builder, ConfigServerClientOptions options, ILoggerFactory loggerFactory) + { + return AddConfigServer(builder, options, null, loggerFactory); + } + + internal static IConfigurationBuilder AddConfigServer(this IConfigurationBuilder builder, ConfigServerClientOptions options, + HttpClientHandler? httpClientHandler, ILoggerFactory loggerFactory) { ArgumentNullException.ThrowIfNull(builder); ArgumentNullException.ThrowIfNull(options); @@ -75,8 +81,8 @@ public static IConfigurationBuilder AddConfigServer(this IConfigurationBuilder b builder.AddKubernetesServiceBindings(); ConfigServerConfigurationSource source = builder is IConfiguration configuration - ? new ConfigServerConfigurationSource(options, configuration, loggerFactory) - : new ConfigServerConfigurationSource(options, builder.Sources, builder.Properties, loggerFactory); + ? new ConfigServerConfigurationSource(options, configuration, httpClientHandler, loggerFactory) + : new ConfigServerConfigurationSource(options, builder.Sources, builder.Properties, httpClientHandler, loggerFactory); builder.Add(source); } diff --git a/src/Configuration/src/ConfigServer/ConfigServerConfigurationProvider.cs b/src/Configuration/src/ConfigServer/ConfigServerConfigurationProvider.cs index 36be91f368..828bee5f5d 100644 --- a/src/Configuration/src/ConfigServer/ConfigServerConfigurationProvider.cs +++ b/src/Configuration/src/ConfigServer/ConfigServerConfigurationProvider.cs @@ -30,9 +30,7 @@ internal sealed partial class ConfigServerConfigurationProvider : ConfigurationP private const string VaultRenewPath = "vault/v1/auth/token/renew-self"; private const string VaultTokenHeader = "X-Vault-Token"; private const char CommaDelimiter = ','; - internal const string TokenHeader = "X-Config-Token"; - private static readonly string[] EmptyLabels = [string.Empty]; private readonly ILoggerFactory _loggerFactory; @@ -41,11 +39,14 @@ internal sealed partial class ConfigServerConfigurationProvider : ConfigurationP private readonly bool _hasConfiguration; private readonly bool _ownsHttpClientHandler; private readonly ConfigureConfigServerClientOptions _configurer; - private HttpClientHandler? _httpClientHandler; + private readonly ConfigServerClientOptions _initialOptions; + private HttpClientHandler? _httpClientHandler; private ConfigServerDiscoveryService? _configServerDiscoveryService; private Timer? _refreshTimer; private SemaphoreSlim? _timerTickLock = new(1, 1); + private volatile DiscoveryLookupResult? _lastDiscoveryLookupResult; + private volatile ConfigServerClientOptions _clientOptions; internal static JsonSerializerOptions SerializerOptions { get; } = new() { @@ -54,12 +55,13 @@ internal sealed partial class ConfigServerConfigurationProvider : ConfigurationP PreferredObjectCreationHandling = JsonObjectCreationHandling.Populate }; - internal IDictionary Properties => Data; + internal IDictionary InnerData => Data; /// - /// Gets the configuration settings the provider uses when accessing the server. + /// Gets the settings used to access Config Server, excluding information found during service discovery (so that a provider (re)load properly observes + /// changes and triggers its change token). Returns a cloned snapshot to prevent tearing during reads/writes. /// - public ConfigServerClientOptions ClientOptions { get; } + internal ConfigServerClientOptions ClientOptions => _clientOptions.Clone(); /// /// Initializes a new instance of the class from a . @@ -71,7 +73,7 @@ internal sealed partial class ConfigServerConfigurationProvider : ConfigurationP /// Used for internal logging. Pass to disable logging. /// public ConfigServerConfigurationProvider(ConfigServerConfigurationSource source, ILoggerFactory loggerFactory) - : this(source.DefaultOptions, source.Configuration, null, loggerFactory) + : this(source.DefaultOptions, source.Configuration, source.HttpClientHandler, loggerFactory) { } @@ -97,7 +99,8 @@ internal ConfigServerConfigurationProvider(ConfigServerClientOptions clientOptio _configurer = new ConfigureConfigServerClientOptions(_configuration); - ClientOptions = clientOptions; + _initialOptions = clientOptions; + _clientOptions = clientOptions; if (httpClientHandler == null) { @@ -114,47 +117,46 @@ internal ConfigServerConfigurationProvider(ConfigServerClientOptions clientOptio private void OnSettingsChanged() { - TimeSpan existingPollingInterval = ClientOptions.PollingInterval; - - _configurer.Configure(ClientOptions); + ConfigServerClientOptions newOptions = _initialOptions.Clone(); + _configurer.Configure(newOptions); if (_hasConfiguration) { _configuration.GetReloadToken().RegisterChangeCallback(_ => OnSettingsChanged(), null); } - if (ClientOptions.PollingInterval == TimeSpan.Zero || !ClientOptions.Enabled) + TimeSpan previousPollingInterval = _clientOptions.PollingInterval; + _clientOptions = newOptions; + + if (newOptions.PollingInterval == TimeSpan.Zero || !newOptions.Enabled) { _refreshTimer?.Dispose(); _refreshTimer = null; } - else if (ClientOptions.Enabled) + else { if (_refreshTimer == null) { -#pragma warning disable S4462 // Calls to "async" methods should not be blocking - // Justification: Configuration sources and providers don't support async. - _refreshTimer = new Timer(_ => DoPolledLoadAsync().GetAwaiter().GetResult(), null, TimeSpan.Zero, ClientOptions.PollingInterval); -#pragma warning restore S4462 // Calls to "async" methods should not be blocking + _refreshTimer = new Timer(_ => RefreshTimerTick(), null, TimeSpan.Zero, newOptions.PollingInterval); } - else if (existingPollingInterval != ClientOptions.PollingInterval) + else if (previousPollingInterval != newOptions.PollingInterval) { - _refreshTimer.Change(TimeSpan.Zero, ClientOptions.PollingInterval); + _refreshTimer.Change(TimeSpan.Zero, newOptions.PollingInterval); } } } /// - /// DoPolledLoad is called by a Timer callback, so must catch all exceptions. + /// RefreshTimerTick is called by a Timer callback, so must catch all exceptions. /// - private async Task DoPolledLoadAsync() + private void RefreshTimerTick() { LogEnteringTimerCycle(); bool lockTaken = false; try { - lockTaken = _timerTickLock != null && await _timerTickLock.WaitAsync(0); + lockTaken = _timerTickLock != null && _timerTickLock.Wait(0); } catch (ObjectDisposedException) { @@ -166,7 +168,11 @@ private async Task DoPolledLoadAsync() if (lockTaken) { LogExclusiveLockObtained(); - await DoLoadAsync(true, CancellationToken.None); + +#pragma warning disable S4462 // Calls to "async" methods should not be blocking + // Justification: Configuration sources and providers don't support async. + DoLoadAsync(ClientOptions, true, CancellationToken.None).GetAwaiter().GetResult(); +#pragma warning restore S4462 // Calls to "async" methods should not be blocking } else { @@ -208,25 +214,28 @@ public override void Load() internal async Task LoadInternalAsync(bool updateDictionary, CancellationToken cancellationToken) { - if (!ClientOptions.Enabled) + ConfigServerClientOptions optionsSnapshot = ClientOptions; + + if (!optionsSnapshot.Enabled) { LogConfigServerClientDisabled(); return null; } - if (IsDiscoveryFirstEnabled()) + if (!optionsSnapshot.Discovery.Enabled) { - _configServerDiscoveryService ??= new ConfigServerDiscoveryService(_configuration, ClientOptions, _loggerFactory); - await DiscoverServerInstancesAsync(_configServerDiscoveryService, cancellationToken); + SetLastDiscoveryLookupResult([]); + } + else + { + _configServerDiscoveryService ??= new ConfigServerDiscoveryService(_configuration, optionsSnapshot, _loggerFactory); + await DiscoverServerInstancesAsync(_configServerDiscoveryService, optionsSnapshot.FailFast, cancellationToken); } - // Adds client settings (e.g. spring:cloud:config:uri, etc.) to the Data dictionary - AddConfigServerClientOptions(); - - if (ClientOptions is { Retry.Enabled: true, FailFast: true }) + if (optionsSnapshot is { Retry.Enabled: true, FailFast: true }) { int attempts = 0; - int backOff = ClientOptions.Retry.InitialInterval; + int backOff = optionsSnapshot.Retry.InitialInterval; do { @@ -234,18 +243,18 @@ public override void Load() try { - return await DoLoadAsync(updateDictionary, cancellationToken); + return await DoLoadAsync(optionsSnapshot, updateDictionary, cancellationToken); } catch (ConfigServerException exception) { LogFailedFetchingConfiguration(exception); attempts++; - if (attempts < ClientOptions.Retry.MaxAttempts) + if (attempts < optionsSnapshot.Retry.MaxAttempts) { Thread.CurrentThread.Join(backOff); - int nextBackOff = (int)(backOff * ClientOptions.Retry.Multiplier); - backOff = Math.Min(nextBackOff, ClientOptions.Retry.MaxInterval); + int nextBackOff = (int)(backOff * optionsSnapshot.Retry.Multiplier); + backOff = Math.Min(nextBackOff, optionsSnapshot.Retry.MaxInterval); } else { @@ -257,19 +266,21 @@ public override void Load() } LogFetchingConfiguration(); - return await DoLoadAsync(updateDictionary, cancellationToken); + return await DoLoadAsync(optionsSnapshot, updateDictionary, cancellationToken); } - internal async Task DoLoadAsync(bool updateDictionary, CancellationToken cancellationToken) + internal async Task DoLoadAsync(ConfigServerClientOptions optionsSnapshot, bool updateDictionary, CancellationToken cancellationToken) { + ApplyLastDiscoveryLookupResultToClientOptions(optionsSnapshot); + Exception? error = null; // Get list of Config Server uris to check - List uris = ClientOptions.GetUris(); + List uris = optionsSnapshot.GetUris(); try { - foreach (string label in GetLabels()) + foreach (string label in GetLabels(optionsSnapshot)) { LogProcessingLabel(label); @@ -279,7 +290,7 @@ public override void Load() } // Invoke Config Servers - ConfigEnvironment? env = await RemoteLoadAsync(uris, label, cancellationToken); + ConfigEnvironment? env = await RemoteLoadAsync(optionsSnapshot, uris, label, cancellationToken); // Update configuration Data dictionary with any results if (env != null) @@ -289,6 +300,7 @@ public override void Load() if (updateDictionary) { var data = new Dictionary(StringComparer.OrdinalIgnoreCase); + CopyLastDiscoveryLookupResultToData(data, optionsSnapshot.Discovery.Enabled); if (!string.IsNullOrEmpty(env.State)) { @@ -301,16 +313,12 @@ public override void Load() } IList sources = env.PropertySources; - int index = sources.Count - 1; - for (; index >= 0; index--) + for (int index = sources.Count - 1; index >= 0; index--) { AddPropertySource(sources[index], data); } - // Adds client settings (e.g. spring:cloud:config:uri, etc.) back to the (new) Data dictionary - AddConfigServerClientOptions(data); - if (!AreDictionariesEqual(Data, data)) { LogDataChanged(); @@ -334,7 +342,7 @@ public override void Load() LogCouldNotLocatePropertySource(error); - if (ClientOptions.FailFast) + if (optionsSnapshot.FailFast) { LogFailFastEnabled(error); throw new ConfigServerException("Could not locate PropertySource, fail fast property is set, failing", error); @@ -343,6 +351,42 @@ public override void Load() return null; } + internal void ApplyLastDiscoveryLookupResultToClientOptions(ConfigServerClientOptions optionsSnapshot) + { + if (_lastDiscoveryLookupResult != null && optionsSnapshot.Discovery.Enabled) + { + optionsSnapshot.Uri = _lastDiscoveryLookupResult.ConfigServerUri; + + if (_lastDiscoveryLookupResult.Username != null) + { + optionsSnapshot.Username = _lastDiscoveryLookupResult.Username; + } + + if (_lastDiscoveryLookupResult.Password != null) + { + optionsSnapshot.Password = _lastDiscoveryLookupResult.Password; + } + } + } + + private void CopyLastDiscoveryLookupResultToData(Dictionary data, bool isDiscoveryEnabled) + { + if (_lastDiscoveryLookupResult != null && isDiscoveryEnabled) + { + data["spring:cloud:config:uri"] = _lastDiscoveryLookupResult.ConfigServerUri; + + if (_lastDiscoveryLookupResult.Username != null) + { + data["spring:cloud:config:username"] = _lastDiscoveryLookupResult.Username; + } + + if (_lastDiscoveryLookupResult.Password != null) + { + data["spring:cloud:config:password"] = _lastDiscoveryLookupResult.Password; + } + } + } + private static bool AreDictionariesEqual(IDictionary first, Dictionary second) where TKey : notnull { @@ -350,23 +394,24 @@ private static bool AreDictionariesEqual(IDictionary second.ContainsKey(firstKey) && EqualityComparer.Default.Equals(first[firstKey], second[firstKey])); } - internal string[] GetLabels() + internal string[] GetLabels(ConfigServerClientOptions optionsSnapshot) { - if (string.IsNullOrWhiteSpace(ClientOptions.Label)) + if (string.IsNullOrWhiteSpace(optionsSnapshot.Label)) { return EmptyLabels; } - return ClientOptions.Label.Split(CommaDelimiter, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); + return optionsSnapshot.Label.Split(CommaDelimiter, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); } - private async Task DiscoverServerInstancesAsync(ConfigServerDiscoveryService configServerDiscoveryService, CancellationToken cancellationToken) + private async Task DiscoverServerInstancesAsync(ConfigServerDiscoveryService configServerDiscoveryService, bool failFast, + CancellationToken cancellationToken) { - IServiceInstance[] instances = (await configServerDiscoveryService.GetConfigServerInstancesAsync(cancellationToken)).ToArray(); + List instances = await configServerDiscoveryService.GetConfigServerInstancesAsync(cancellationToken); - if (instances.Length == 0) + if (instances.Count == 0) { - if (ClientOptions.FailFast) + if (failFast) { throw new ConfigServerException("Could not locate Config Server via discovery, are you missing a Discovery service assembly?"); } @@ -374,47 +419,48 @@ private async Task DiscoverServerInstancesAsync(ConfigServerDiscoveryService con return; } - UpdateSettingsFromDiscovery(instances, ClientOptions); + SetLastDiscoveryLookupResult(instances); } - internal void UpdateSettingsFromDiscovery(IEnumerable instances, ConfigServerClientOptions clientOptions) + internal void SetLastDiscoveryLookupResult(IEnumerable instances) { - var endpoints = new StringBuilder(); + var endpointBuilder = new StringBuilder(); + string? username = null; + string? password = null; foreach (IServiceInstance instance in instances) { + if (instance.Metadata.TryGetValue("password", out string? instancePassword)) + { + instance.Metadata.TryGetValue("user", out string? instanceUsername); + username = instanceUsername ?? "user"; + password = instancePassword; + } + string uri = instance.Uri.ToString(); - IReadOnlyDictionary metaData = instance.Metadata; - if (metaData.Count > 0) + if (instance.Metadata.TryGetValue("configPath", out string? path) && path != null) { - if (metaData.TryGetValue("password", out string? password)) + if (uri.EndsWith('/') && path.StartsWith('/')) { - metaData.TryGetValue("user", out string? username); - username ??= "user"; - clientOptions.Username = username; - clientOptions.Password = password; + uri = uri[..^1]; } - if (metaData.TryGetValue("configPath", out string? path) && path != null) - { - if (uri.EndsWith('/') && path.StartsWith('/')) - { - uri = uri[..^1]; - } - - uri += path; - } + uri += path; } - endpoints.Append(uri); - endpoints.Append(','); + endpointBuilder.Append(uri); + endpointBuilder.Append(','); } - if (endpoints.Length > 0) + if (endpointBuilder.Length > 0) { - string uris = endpoints.ToString(0, endpoints.Length - 1); - clientOptions.Uri = uris; + string uris = endpointBuilder.ToString(0, endpointBuilder.Length - 1); + _lastDiscoveryLookupResult = new DiscoveryLookupResult(uris, username, password); + } + else + { + _lastDiscoveryLookupResult = null; } } @@ -437,6 +483,9 @@ internal async Task ShutdownAsync(CancellationToken cancellationToken) /// /// Creates the that will be used in accessing the Spring Cloud Configuration server. /// + /// + /// A snapshot of the client options to use for this request. + /// /// /// The Uri used when accessing the server. /// @@ -446,7 +495,8 @@ internal async Task ShutdownAsync(CancellationToken cancellationToken) /// /// The HttpRequestMessage built from the path. /// - internal async Task GetRequestMessageAsync(Uri requestUri, CancellationToken cancellationToken) + internal async Task GetRequestMessageAsync(ConfigServerClientOptions optionsSnapshot, Uri requestUri, + CancellationToken cancellationToken) { var uriWithoutUserInfo = new Uri(requestUri.GetComponents(UriComponents.HttpRequestUrl, UriFormat.UriEscaped)); var requestMessage = new HttpRequestMessage(HttpMethod.Get, uriWithoutUserInfo); @@ -460,105 +510,52 @@ internal async Task GetRequestMessageAsync(Uri requestUri, C } else { - if (!string.IsNullOrEmpty(ClientOptions.AccessTokenUri)) + if (!string.IsNullOrEmpty(optionsSnapshot.AccessTokenUri)) { - using HttpClient httpClient = CreateHttpClient(ClientOptions); - var accessTokenUri = new Uri(ClientOptions.AccessTokenUri); + using HttpClient httpClient = CreateHttpClient(optionsSnapshot); + var accessTokenUri = new Uri(optionsSnapshot.AccessTokenUri); string accessToken = - await httpClient.GetAccessTokenAsync(accessTokenUri, ClientOptions.ClientId, ClientOptions.ClientSecret, cancellationToken); + await httpClient.GetAccessTokenAsync(accessTokenUri, optionsSnapshot.ClientId, optionsSnapshot.ClientSecret, cancellationToken); LogAccessTokenFetched(accessTokenUri.ToMaskedString()); requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); } } - if (!string.IsNullOrEmpty(ClientOptions.Token) && ClientOptions is { Uri: not null, IsMultiServerConfiguration: false }) + if (!string.IsNullOrEmpty(optionsSnapshot.Token) && optionsSnapshot is { Uri: not null, IsMultiServerConfiguration: false }) { - if (!ClientOptions.DisableTokenRenewal) + if (!optionsSnapshot.DisableTokenRenewal) { - RenewToken(); + RenewToken(optionsSnapshot); } - requestMessage.Headers.Add(TokenHeader, ClientOptions.Token); + requestMessage.Headers.Add(TokenHeader, optionsSnapshot.Token); } return requestMessage; } - /// - /// Adds the client settings for the Configuration Server to the data dictionary. - /// - internal void AddConfigServerClientOptions() - { - Dictionary data = Data.ToDictionary(entry => entry.Key, entry => entry.Value, StringComparer.OrdinalIgnoreCase); - - AddConfigServerClientOptions(data); - - Data = data; - } - - /// - /// Adds the client settings for the Configuration Server to the data dictionary. - /// - /// - /// The client settings to add. - /// - private void AddConfigServerClientOptions(Dictionary data) - { - data["spring:cloud:config:enabled"] = ClientOptions.Enabled.ToString(CultureInfo.InvariantCulture); - data["spring:cloud:config:failFast"] = ClientOptions.FailFast.ToString(CultureInfo.InvariantCulture); - data["spring:cloud:config:env"] = ClientOptions.Environment; - data["spring:cloud:config:label"] = ClientOptions.Label; - data["spring:cloud:config:name"] = ClientOptions.Name; - data["spring:cloud:config:uri"] = ClientOptions.Uri; - data["spring:cloud:config:username"] = ClientOptions.Username; - data["spring:cloud:config:password"] = ClientOptions.Password; - data["spring:cloud:config:token"] = ClientOptions.Token; - data["spring:cloud:config:timeout"] = ClientOptions.Timeout.ToString(CultureInfo.InvariantCulture); - data["spring:cloud:config:pollingInterval"] = ClientOptions.PollingInterval.ToString(null, CultureInfo.InvariantCulture); - data["spring:cloud:config:validateCertificates"] = ClientOptions.ValidateCertificates.ToString(CultureInfo.InvariantCulture); - data["spring:cloud:config:accessTokenUri"] = ClientOptions.AccessTokenUri; - data["spring:cloud:config:clientSecret"] = ClientOptions.ClientSecret; - data["spring:cloud:config:clientId"] = ClientOptions.ClientId; - data["spring:cloud:config:tokenTtl"] = ClientOptions.TokenTtl.ToString(CultureInfo.InvariantCulture); - data["spring:cloud:config:tokenRenewRate"] = ClientOptions.TokenRenewRate.ToString(CultureInfo.InvariantCulture); - data["spring:cloud:config:disableTokenRenewal"] = ClientOptions.DisableTokenRenewal.ToString(CultureInfo.InvariantCulture); - data["spring:cloud:config:retry:enabled"] = ClientOptions.Retry.Enabled.ToString(CultureInfo.InvariantCulture); - data["spring:cloud:config:retry:initialInterval"] = ClientOptions.Retry.InitialInterval.ToString(CultureInfo.InvariantCulture); - data["spring:cloud:config:retry:maxInterval"] = ClientOptions.Retry.MaxInterval.ToString(CultureInfo.InvariantCulture); - data["spring:cloud:config:retry:multiplier"] = ClientOptions.Retry.Multiplier.ToString(CultureInfo.InvariantCulture); - data["spring:cloud:config:retry:maxAttempts"] = ClientOptions.Retry.MaxAttempts.ToString(CultureInfo.InvariantCulture); - data["spring:cloud:config:discovery:enabled"] = ClientOptions.Discovery.Enabled.ToString(CultureInfo.InvariantCulture); - data["spring:cloud:config:discovery:serviceId"] = ClientOptions.Discovery.ServiceId; - data["spring:cloud:config:health:enabled"] = ClientOptions.Health.Enabled.ToString(CultureInfo.InvariantCulture); - data["spring:cloud:config:health:timeToLive"] = ClientOptions.Health.TimeToLive.ToString(CultureInfo.InvariantCulture); - - foreach ((string headerName, string headerValue) in ClientOptions.Headers) - { - data[$"spring:cloud:config:headers:{headerName}"] = headerValue; - } - } - - internal async Task RemoteLoadAsync(List requestUris, string? label, CancellationToken cancellationToken) + internal async Task RemoteLoadAsync(ConfigServerClientOptions optionsSnapshot, List requestUris, string? label, + CancellationToken cancellationToken) { LogRemoteLoadEntered(nameof(RemoteLoadAsync)); // Get client if not already set - using HttpClient httpClient = CreateHttpClient(ClientOptions); + using HttpClient httpClient = CreateHttpClient(optionsSnapshot); Exception? error = null; foreach (Uri requestUri in requestUris) { // Make Config Server URI from settings - Uri uri = BuildConfigServerUri(requestUri, label); + Uri uri = BuildConfigServerUri(optionsSnapshot, requestUri, label); LogTryingToConnect(uri.ToMaskedString()); // Get the request message LogBuildingHttpRequest(); - HttpRequestMessage request = await GetRequestMessageAsync(uri, cancellationToken); + HttpRequestMessage request = await GetRequestMessageAsync(optionsSnapshot, uri, cancellationToken); // Invoke Config Server try @@ -612,6 +609,9 @@ private void AddConfigServerClientOptions(Dictionary data) /// /// Creates the Uri that will be used in accessing the Configuration Server. /// + /// + /// A snapshot of the client options to use for URI construction. + /// /// /// Base server uri to use. /// @@ -621,23 +621,23 @@ private void AddConfigServerClientOptions(Dictionary data) /// /// The request URI for the Configuration Server. /// - internal Uri BuildConfigServerUri(Uri serverUri, string? label) + internal Uri BuildConfigServerUri(ConfigServerClientOptions optionsSnapshot, Uri serverUri, string? label) { ArgumentNullException.ThrowIfNull(serverUri); var uriBuilder = new UriBuilder(serverUri); - if (!string.IsNullOrEmpty(ClientOptions.Username)) + if (!string.IsNullOrEmpty(optionsSnapshot.Username)) { - uriBuilder.UserName = WebUtility.UrlEncode(ClientOptions.Username); + uriBuilder.UserName = WebUtility.UrlEncode(optionsSnapshot.Username); } - if (!string.IsNullOrEmpty(ClientOptions.Password)) + if (!string.IsNullOrEmpty(optionsSnapshot.Password)) { - uriBuilder.Password = WebUtility.UrlEncode(ClientOptions.Password); + uriBuilder.Password = WebUtility.UrlEncode(optionsSnapshot.Password); } - string pathSuffix = $"{WebUtility.UrlEncode(ClientOptions.Name)}/{WebUtility.UrlEncode(ClientOptions.Environment)}"; + string pathSuffix = $"{WebUtility.UrlEncode(optionsSnapshot.Name)}/{WebUtility.UrlEncode(optionsSnapshot.Environment)}"; if (!string.IsNullOrWhiteSpace(label)) { @@ -698,33 +698,33 @@ private void AddPropertySource(PropertySource? source, Dictionary RefreshVaultTokenAsync(CancellationToken.None).GetAwaiter().GetResult(), null, - TimeSpan.FromMilliseconds(ClientOptions.TokenRenewRate), TimeSpan.FromMilliseconds(ClientOptions.TokenRenewRate)); + _ = new Timer(_ => RefreshVaultTokenAsync(optionsSnapshot, CancellationToken.None).GetAwaiter().GetResult(), null, + TimeSpan.FromMilliseconds(optionsSnapshot.TokenRenewRate), TimeSpan.FromMilliseconds(optionsSnapshot.TokenRenewRate)); #pragma warning restore S4462 // Calls to "async" methods should not be blocking } // fire and forget - internal async Task RefreshVaultTokenAsync(CancellationToken cancellationToken) + internal async Task RefreshVaultTokenAsync(ConfigServerClientOptions optionsSnapshot, CancellationToken cancellationToken) { - if (string.IsNullOrEmpty(ClientOptions.Token)) + if (string.IsNullOrEmpty(optionsSnapshot.Token)) { return; } - string obscuredToken = $"{ClientOptions.Token[..4]}[*]{ClientOptions.Token[^4..]}"; + string obscuredToken = $"{optionsSnapshot.Token[..4]}[*]{optionsSnapshot.Token[^4..]}"; try { - using HttpClient httpClient = CreateHttpClient(ClientOptions); + using HttpClient httpClient = CreateHttpClient(optionsSnapshot); - Uri uri = GetVaultRenewUri(); - HttpRequestMessage message = await GetVaultRenewRequestMessageAsync(uri, cancellationToken); + Uri uri = GetVaultRenewUri(optionsSnapshot); + HttpRequestMessage message = await GetVaultRenewRequestMessageAsync(optionsSnapshot, uri, cancellationToken); - LogRenewingVaultToken(obscuredToken, ClientOptions.TokenTtl, uri.ToMaskedString()); + LogRenewingVaultToken(obscuredToken, optionsSnapshot.TokenTtl, uri.ToMaskedString()); using HttpResponseMessage response = await httpClient.SendAsync(message, cancellationToken); if (response.StatusCode != HttpStatusCode.OK) @@ -738,9 +738,9 @@ internal async Task RefreshVaultTokenAsync(CancellationToken cancellationToken) } } - private Uri GetVaultRenewUri() + private static Uri GetVaultRenewUri(ConfigServerClientOptions optionsSnapshot) { - string baseUri = ClientOptions.Uri!.Split(',')[0].Trim(); + string baseUri = optionsSnapshot.Uri!.Split(',')[0].Trim(); if (!baseUri.EndsWith('/')) { @@ -750,40 +750,36 @@ private Uri GetVaultRenewUri() return new Uri(baseUri + VaultRenewPath, UriKind.RelativeOrAbsolute); } - private async Task GetVaultRenewRequestMessageAsync(Uri requestUri, CancellationToken cancellationToken) + private async Task GetVaultRenewRequestMessageAsync(ConfigServerClientOptions optionsSnapshot, Uri requestUri, + CancellationToken cancellationToken) { var uriWithoutUserInfo = new Uri(requestUri.GetComponents(UriComponents.HttpRequestUrl, UriFormat.UriEscaped)); var requestMessage = new HttpRequestMessage(HttpMethod.Post, uriWithoutUserInfo); - if (!string.IsNullOrEmpty(ClientOptions.AccessTokenUri)) + if (!string.IsNullOrEmpty(optionsSnapshot.AccessTokenUri)) { - using HttpClient httpClient = CreateHttpClient(ClientOptions); - var accessTokenUri = new Uri(ClientOptions.AccessTokenUri); + using HttpClient httpClient = CreateHttpClient(optionsSnapshot); + var accessTokenUri = new Uri(optionsSnapshot.AccessTokenUri); - string accessToken = await httpClient.GetAccessTokenAsync(accessTokenUri, ClientOptions.ClientId, ClientOptions.ClientSecret, cancellationToken); + string accessToken = + await httpClient.GetAccessTokenAsync(accessTokenUri, optionsSnapshot.ClientId, optionsSnapshot.ClientSecret, cancellationToken); LogAccessTokenFetched(accessTokenUri.ToMaskedString()); requestMessage.Headers.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); } - if (!string.IsNullOrEmpty(ClientOptions.Token)) + if (!string.IsNullOrEmpty(optionsSnapshot.Token)) { - requestMessage.Headers.Add(VaultTokenHeader, ClientOptions.Token); + requestMessage.Headers.Add(VaultTokenHeader, optionsSnapshot.Token); } - int renewTtlInSeconds = ClientOptions.TokenTtl / 1000; + int renewTtlInSeconds = optionsSnapshot.TokenTtl / 1000; string json = $"{{\"increment\":{renewTtlInSeconds}}}"; requestMessage.Content = new StringContent(json, Encoding.UTF8, "application/json"); return requestMessage; } - internal bool IsDiscoveryFirstEnabled() - { - IConfigurationSection clientConfigSection = _configuration.GetSection(ConfigServerClientOptions.ConfigurationPrefix); - return clientConfigSection.GetValue("discovery:enabled", ClientOptions.Discovery.Enabled); - } - /// /// Creates an appropriately configured HttpClient that can be used in communicating with the Spring Cloud Configuration Server. /// @@ -922,4 +918,6 @@ public void Dispose() [LoggerMessage(Level = LogLevel.Error, Message = "Unable to renew Vault token {Token}. The token is likely invalid or has expired.")] private partial void LogUnableToRenewVaultToken(Exception exception, string token); + + private sealed record DiscoveryLookupResult(string ConfigServerUri, string? Username, string? Password); } diff --git a/src/Configuration/src/ConfigServer/ConfigServerConfigurationSource.cs b/src/Configuration/src/ConfigServer/ConfigServerConfigurationSource.cs index 02d2d15f0a..46dd25a327 100644 --- a/src/Configuration/src/ConfigServer/ConfigServerConfigurationSource.cs +++ b/src/Configuration/src/ConfigServer/ConfigServerConfigurationSource.cs @@ -5,7 +5,6 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; -using Steeltoe.Common.Certificates; namespace Steeltoe.Configuration.ConfigServer; @@ -21,6 +20,11 @@ internal sealed class ConfigServerConfigurationSource : IConfigurationSource /// internal ConfigServerClientOptions DefaultOptions { get; } + /// + /// Gets an optional handler to mock HTTP requests to Config Server. + /// + internal HttpClientHandler? HttpClientHandler { get; } + /// /// Gets the configuration the Config Server client uses to contact the Config Server. Values returned override the default values provided in /// . @@ -36,17 +40,22 @@ internal sealed class ConfigServerConfigurationSource : IConfigurationSource /// /// configuration used by the Config Server client. Values will override those found in default settings. /// + /// + /// An optional handler to mock HTTP requests to Config Server. + /// /// /// Used for internal logging. Pass to disable logging. /// - public ConfigServerConfigurationSource(ConfigServerClientOptions defaultOptions, IConfiguration configuration, ILoggerFactory loggerFactory) + public ConfigServerConfigurationSource(ConfigServerClientOptions defaultOptions, IConfiguration configuration, HttpClientHandler? httpClientHandler, + ILoggerFactory loggerFactory) { ArgumentNullException.ThrowIfNull(configuration); ArgumentNullException.ThrowIfNull(defaultOptions); ArgumentNullException.ThrowIfNull(loggerFactory); - Configuration = configuration; DefaultOptions = defaultOptions; + HttpClientHandler = httpClientHandler; + Configuration = configuration; _loggerFactory = loggerFactory; } @@ -63,11 +72,14 @@ public ConfigServerConfigurationSource(ConfigServerClientOptions defaultOptions, /// /// properties to be used when sources are built. /// + /// + /// An optional handler to mock HTTP requests to Config Server. + /// /// /// Used for internal logging. Pass to disable logging. /// public ConfigServerConfigurationSource(ConfigServerClientOptions defaultOptions, IList sources, - IDictionary? properties, ILoggerFactory loggerFactory) + IDictionary? properties, HttpClientHandler? httpClientHandler, ILoggerFactory loggerFactory) { ArgumentNullException.ThrowIfNull(defaultOptions); ArgumentNullException.ThrowIfNull(sources); @@ -81,6 +93,7 @@ public ConfigServerConfigurationSource(ConfigServerClientOptions defaultOptions, } DefaultOptions = defaultOptions; + HttpClientHandler = httpClientHandler; _loggerFactory = loggerFactory; } @@ -115,17 +128,6 @@ public IConfigurationProvider Build(IConfigurationBuilder builder) Configuration = configurationBuilder.Build(); } - string? clientCertificatePath = Configuration.GetValue($"{CertificateOptions.ConfigurationKeyPrefix}:ConfigServer:CertificateFilePath"); - - if (!string.IsNullOrEmpty(clientCertificatePath) && DefaultOptions.ClientCertificate.Certificate == null) - { - var certificateConfigurer = new ConfigureCertificateOptions(Configuration); - - var options = new CertificateOptions(); - certificateConfigurer.Configure("ConfigServer", options); - DefaultOptions.ClientCertificate.Certificate = options.Certificate; - } - return new ConfigServerConfigurationProvider(this, _loggerFactory); } } diff --git a/src/Configuration/src/ConfigServer/ConfigServerDiscoveryService.cs b/src/Configuration/src/ConfigServer/ConfigServerDiscoveryService.cs index a6b86ee428..57cc575711 100644 --- a/src/Configuration/src/ConfigServer/ConfigServerDiscoveryService.cs +++ b/src/Configuration/src/ConfigServer/ConfigServerDiscoveryService.cs @@ -105,7 +105,7 @@ private IDiscoveryClient[] GetDiscoveryClientsFromServiceCollection(ServiceColle return discoveryClients; } - internal async Task> GetConfigServerInstancesAsync(CancellationToken cancellationToken) + internal async Task> GetConfigServerInstancesAsync(CancellationToken cancellationToken) { int attempts = 0; int backOff = _options.Retry.InitialInterval; diff --git a/src/Configuration/src/ConfigServer/ConfigServerServiceCollectionExtensions.cs b/src/Configuration/src/ConfigServer/ConfigServerServiceCollectionExtensions.cs index 624aa54ee4..d40c2f686c 100644 --- a/src/Configuration/src/ConfigServer/ConfigServerServiceCollectionExtensions.cs +++ b/src/Configuration/src/ConfigServer/ConfigServerServiceCollectionExtensions.cs @@ -31,6 +31,9 @@ public static IServiceCollection ConfigureConfigServerClientOptions(this IServic services.AddOptions(); services.TryAddEnumerable(ServiceDescriptor.Singleton, ConfigureConfigServerClientOptions>()); + services.TryAddEnumerable(ServiceDescriptor + .Singleton, ConfigurationChangeTokenSource>()); + return services; } diff --git a/src/Configuration/src/ConfigServer/ConfigureConfigServerClientOptions.cs b/src/Configuration/src/ConfigServer/ConfigureConfigServerClientOptions.cs index 4a7ae64ffe..3d6cc8ddc9 100644 --- a/src/Configuration/src/ConfigServer/ConfigureConfigServerClientOptions.cs +++ b/src/Configuration/src/ConfigServer/ConfigureConfigServerClientOptions.cs @@ -5,14 +5,15 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Options; using Steeltoe.Common; +using Steeltoe.Common.Certificates; using Steeltoe.Configuration.CloudFoundry; namespace Steeltoe.Configuration.ConfigServer; internal sealed class ConfigureConfigServerClientOptions : IConfigureOptions { - private const string VcapServicesConfigServerCredentialsPrefix = "vcap:services:p-config-server:0:credentials"; - private const string VcapServicesConfigServer30CredentialsPrefix = "vcap:services:p.config-server:0:credentials"; + private const string VcapServicesConfigServerVersion2CredentialsPrefix = "vcap:services:p-config-server:0:credentials"; + private const string VcapServicesConfigServerVersion3CredentialsPrefix = "vcap:services:p.config-server:0:credentials"; private const string VcapServicesConfigServerCredentialsAltPrefix = "vcap:services:config-server:0:credentials"; private readonly IConfiguration _configuration; @@ -30,6 +31,7 @@ public void Configure(ConfigServerClientOptions options) _configuration.GetSection(ConfigServerClientOptions.ConfigurationPrefix).Bind(options); OverrideFromVcapServicesCredentials(options); + ConfigureClientCertificate(options); options.Name ??= GetApplicationName(); } @@ -38,8 +40,8 @@ private void OverrideFromVcapServicesCredentials(ConfigServerClientOptions optio { VcapServicesConfigServerCredentialsOptions credentialsOptions = new(); _configuration.GetSection(VcapServicesConfigServerCredentialsAltPrefix).Bind(credentialsOptions); - _configuration.GetSection(VcapServicesConfigServer30CredentialsPrefix).Bind(credentialsOptions); - _configuration.GetSection(VcapServicesConfigServerCredentialsPrefix).Bind(credentialsOptions); + _configuration.GetSection(VcapServicesConfigServerVersion2CredentialsPrefix).Bind(credentialsOptions); + _configuration.GetSection(VcapServicesConfigServerVersion3CredentialsPrefix).Bind(credentialsOptions); options.Uri = credentialsOptions.Uri ?? options.Uri; options.ClientId = credentialsOptions.ClientId ?? options.ClientId; @@ -47,6 +49,26 @@ private void OverrideFromVcapServicesCredentials(ConfigServerClientOptions optio options.AccessTokenUri = credentialsOptions.AccessTokenUri ?? options.AccessTokenUri; } + private void ConfigureClientCertificate(ConfigServerClientOptions options) + { + if (options.ClientCertificate.Certificate != null) + { + return; + } + + var certificateConfigurer = new ConfigureCertificateOptions(_configuration); + + var certificateOptions = new CertificateOptions(); + certificateConfigurer.Configure("ConfigServer", certificateOptions); + + if (certificateOptions.Certificate == null) + { + certificateConfigurer.Configure(certificateOptions); + } + + options.ClientCertificate.Certificate = certificateOptions.Certificate; + } + private string? GetApplicationName() { var vcapOptions = new CloudFoundryApplicationOptions(); diff --git a/src/Configuration/test/ConfigServer.Discovery.Test/ConfigServerClientOptionsTest.cs b/src/Configuration/test/ConfigServer.Discovery.Test/ConfigServerClientOptionsTest.cs new file mode 100644 index 0000000000..6ab1506d93 --- /dev/null +++ b/src/Configuration/test/ConfigServer.Discovery.Test/ConfigServerClientOptionsTest.cs @@ -0,0 +1,412 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information. + +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging.Abstractions; +using Microsoft.Extensions.Options; +using RichardSzalay.MockHttp; +using Steeltoe.Common.TestResources; + +namespace Steeltoe.Configuration.ConfigServer.Discovery.Test; + +public sealed class ConfigServerClientOptionsTest +{ + [Fact] + public void Config_Server_URI_is_resolved_from_discovery_and_survives_changes_in_IConfiguration() + { + const string configServerResponseJson = """ + { + "name": "example-app-name", + "profiles": [ + "example-profile" + ], + "label": "example-label", + "version": "1", + "propertySources": [ + { + "name": "example-source", + "source": { + "example-server-key": "example-server-value" + } + } + ] + } + """; + + var fileProvider = new MemoryFileProvider(); + + fileProvider.IncludeAppSettingsJsonFile(""" + { + "spring": { + "cloud": { + "config": { + "discovery": { + "enabled": true + }, + "uri": "http://overruled-by-discovery", + "name": "example-app-name", + "env": "example-profile", + "timeout": 30000, + "label": "example-label" + } + } + }, + "discovery": { + "services": [ + { + "serviceId": "configserver", + "host": "discovered-server.com", + "port": 9999, + "isSecure": true, + "metadata": { + "user": "example-user", + "password": "example-password", + "configPath": "internal" + } + } + ] + }, + "eureka": { + "client": { + "enabled": false + } + }, + "consul": { + "discovery": { + "enabled": false + } + } + } + """); + + using var handler = new DelegateToMockHttpClientHandler(); + + handler.Mock.Expect(HttpMethod.Get, "https://discovered-server.com:9999/internal/example-app-name/example-profile/example-label") + .Respond("application/json", configServerResponseJson); + + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryAppSettingsJsonFile(fileProvider); + configurationBuilder.AddConfigServer(new ConfigServerClientOptions(), handler, NullLoggerFactory.Instance); + IConfigurationRoot configuration = configurationBuilder.Build(); + + handler.Mock.VerifyNoOutstandingExpectation(); + + ConfigServerConfigurationProvider provider = configuration.Providers.OfType().Single(); + + IServiceCollection services = new ServiceCollection(); + services.AddSingleton(configuration); + services.ConfigureConfigServerClientOptions(); + + using ServiceProvider serviceProvider = services.BuildServiceProvider(true); + var optionsMonitor = serviceProvider.GetRequiredService>(); + + provider.ClientOptions.Uri.Should().Be("http://overruled-by-discovery"); + provider.ClientOptions.Username.Should().BeNull(); + provider.ClientOptions.Password.Should().BeNull(); + provider.ClientOptions.Name.Should().Be("example-app-name"); + provider.ClientOptions.Environment.Should().Be("example-profile"); + provider.ClientOptions.Timeout.Should().Be(30_000); + provider.ClientOptions.Label.Should().Be("example-label"); + + optionsMonitor.CurrentValue.Uri.Should().Be("https://discovered-server.com:9999/internal"); + optionsMonitor.CurrentValue.Username.Should().Be("example-user"); + optionsMonitor.CurrentValue.Password.Should().Be("example-password"); + optionsMonitor.CurrentValue.Name.Should().Be(provider.ClientOptions.Name); + optionsMonitor.CurrentValue.Environment.Should().Be(provider.ClientOptions.Environment); + optionsMonitor.CurrentValue.Timeout.Should().Be(provider.ClientOptions.Timeout); + optionsMonitor.CurrentValue.Label.Should().Be(provider.ClientOptions.Label); + + configuration["example-server-key"].Should().Be("example-server-value"); + + fileProvider.ReplaceAppSettingsJsonFile(""" + { + "spring": { + "cloud": { + "config": { + "discovery": { + "enabled": true + }, + "uri": "http://overruled-by-discovery", + "name": "alternate-name-1", + "env": "example-profile", + "timeout": 15000, + "label": "example-label" + } + } + }, + "discovery": { + "services": [ + { + "serviceId": "configserver", + "host": "ignored-other-discovered-server.com", + "port": 3333, + "isSecure": true, + "metadata": { + "user": "ignored-other-example-user", + "password": "ignored-other-example-password", + "configPath": "ignored-other-internal" + } + } + ] + }, + "eureka": { + "client": { + "enabled": false + } + }, + "consul": { + "discovery": { + "enabled": false + } + } + } + """); + + fileProvider.NotifyChanged(); + + provider.ClientOptions.Uri.Should().Be("http://overruled-by-discovery"); + provider.ClientOptions.Username.Should().BeNull(); + provider.ClientOptions.Password.Should().BeNull(); + provider.ClientOptions.Name.Should().Be("alternate-name-1"); + provider.ClientOptions.Environment.Should().Be("example-profile"); + provider.ClientOptions.Timeout.Should().Be(15_000); + provider.ClientOptions.Label.Should().Be("example-label"); + + // Discovery changes don't propagate until the provider reloads. + optionsMonitor.CurrentValue.Uri.Should().Be("https://discovered-server.com:9999/internal"); + optionsMonitor.CurrentValue.Username.Should().Be("example-user"); + optionsMonitor.CurrentValue.Password.Should().Be("example-password"); + optionsMonitor.CurrentValue.Name.Should().Be(provider.ClientOptions.Name); + optionsMonitor.CurrentValue.Environment.Should().Be(provider.ClientOptions.Environment); + optionsMonitor.CurrentValue.Timeout.Should().Be(provider.ClientOptions.Timeout); + optionsMonitor.CurrentValue.Label.Should().Be(provider.ClientOptions.Label); + + configuration["example-server-key"].Should().Be("example-server-value"); + + fileProvider.ReplaceAppSettingsJsonFile(""" + { + "spring": { + "cloud": { + "config": { + "discovery": { + "enabled": false + }, + "uri": "https://explicit-server:7777", + "name": "alternate-name-2", + "env": "example-profile", + "timeout": 10000 + } + } + } + } + """); + + fileProvider.NotifyChanged(); + + provider.ClientOptions.Uri.Should().Be("https://explicit-server:7777"); + provider.ClientOptions.Name.Should().Be("alternate-name-2"); + provider.ClientOptions.Environment.Should().Be("example-profile"); + provider.ClientOptions.Timeout.Should().Be(10_000); + provider.ClientOptions.Label.Should().BeNull(); + + // Discovery changes don't propagate until the provider reloads. + optionsMonitor.CurrentValue.Uri.Should().Be("https://discovered-server.com:9999/internal"); + optionsMonitor.CurrentValue.Name.Should().Be(provider.ClientOptions.Name); + optionsMonitor.CurrentValue.Environment.Should().Be(provider.ClientOptions.Environment); + optionsMonitor.CurrentValue.Timeout.Should().Be(provider.ClientOptions.Timeout); + optionsMonitor.CurrentValue.Label.Should().Be(provider.ClientOptions.Label); + + configuration["example-server-key"].Should().Be("example-server-value"); + } + + [Fact] + public void Updates_discovered_Config_Server_URI_on_provider_reload() + { + const string configServerResponseJson = """ + { + "name": "example-app-name", + "profiles": [ + "example-profile" + ], + "label": "example-label", + "version": "1", + "propertySources": [ + { + "name": "example-source", + "source": { + "example-server-key": "example-server-value" + } + } + ] + } + """; + + var fileProvider = new MemoryFileProvider(); + + fileProvider.IncludeAppSettingsJsonFile(""" + { + "spring": { + "cloud": { + "config": { + "discovery": { + "enabled": true + }, + "uri": "http://overruled-by-discovery", + "name": "example-app-name", + "env": "example-profile", + "label": "example-label" + } + } + }, + "discovery": { + "services": [ + { + "serviceId": "configserver", + "host": "discovered-server.com", + "port": 9999, + "isSecure": true, + "metadata": { + "user": "example-user", + "password": "example-password", + "configPath": "internal" + } + } + ] + }, + "eureka": { + "client": { + "enabled": false + } + }, + "consul": { + "discovery": { + "enabled": false + } + } + } + """); + + using var handler = new DelegateToMockHttpClientHandler(); + + handler.Mock.Expect(HttpMethod.Get, "https://discovered-server.com:9999/internal/example-app-name/example-profile/example-label") + .Respond("application/json", configServerResponseJson); + + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryAppSettingsJsonFile(fileProvider); + configurationBuilder.AddConfigServer(new ConfigServerClientOptions(), handler, NullLoggerFactory.Instance); + IConfigurationRoot configuration = configurationBuilder.Build(); + + handler.Mock.VerifyNoOutstandingExpectation(); + handler.Mock.Clear(); + + ConfigServerConfigurationProvider provider = configuration.Providers.OfType().Single(); + + IServiceCollection services = new ServiceCollection(); + services.AddSingleton(configuration); + services.ConfigureConfigServerClientOptions(); + + using ServiceProvider serviceProvider = services.BuildServiceProvider(true); + var optionsMonitor = serviceProvider.GetRequiredService>(); + + provider.ClientOptions.Uri.Should().Be("http://overruled-by-discovery"); + provider.ClientOptions.Username.Should().BeNull(); + provider.ClientOptions.Password.Should().BeNull(); + provider.ClientOptions.Name.Should().Be("example-app-name"); + provider.ClientOptions.Environment.Should().Be("example-profile"); + provider.ClientOptions.Label.Should().Be("example-label"); + + optionsMonitor.CurrentValue.Uri.Should().Be("https://discovered-server.com:9999/internal"); + optionsMonitor.CurrentValue.Username.Should().Be("example-user"); + optionsMonitor.CurrentValue.Password.Should().Be("example-password"); + optionsMonitor.CurrentValue.Name.Should().Be(provider.ClientOptions.Name); + optionsMonitor.CurrentValue.Environment.Should().Be(provider.ClientOptions.Environment); + optionsMonitor.CurrentValue.Label.Should().Be(provider.ClientOptions.Label); + + configuration["example-server-key"].Should().Be("example-server-value"); + + fileProvider.ReplaceAppSettingsJsonFile(""" + { + "spring": { + "cloud": { + "config": { + "discovery": { + "enabled": true + }, + "uri": "http://overruled-again-by-discovery", + "name": "alternate-name", + "env": "alternate-profile", + "label": "alternate-label" + } + } + }, + "discovery": { + "services": [ + { + "serviceId": "configserver", + "host": "alternate-discovered-server.com", + "port": 7777, + "isSecure": true, + "metadata": { + "user": "alternate-user", + "password": "alternate-password", + "configPath": "internal" + } + } + ] + }, + "eureka": { + "client": { + "enabled": false + } + }, + "consul": { + "discovery": { + "enabled": false + } + } + } + """); + + fileProvider.NotifyChanged(); + + provider.ClientOptions.Uri.Should().Be("http://overruled-again-by-discovery"); + provider.ClientOptions.Username.Should().BeNull(); + provider.ClientOptions.Password.Should().BeNull(); + provider.ClientOptions.Name.Should().Be("alternate-name"); + provider.ClientOptions.Environment.Should().Be("alternate-profile"); + provider.ClientOptions.Label.Should().Be("alternate-label"); + + optionsMonitor.CurrentValue.Uri.Should().Be("https://discovered-server.com:9999/internal"); + optionsMonitor.CurrentValue.Username.Should().Be("example-user"); + optionsMonitor.CurrentValue.Password.Should().Be("example-password"); + optionsMonitor.CurrentValue.Name.Should().Be(provider.ClientOptions.Name); + optionsMonitor.CurrentValue.Environment.Should().Be(provider.ClientOptions.Environment); + optionsMonitor.CurrentValue.Label.Should().Be(provider.ClientOptions.Label); + + configuration["example-server-key"].Should().Be("example-server-value"); + + handler.Mock.Expect(HttpMethod.Get, "https://alternate-discovered-server.com:7777/internal/alternate-name/alternate-profile/alternate-label") + .Respond("application/json", configServerResponseJson); + + provider.Load(); + handler.Mock.VerifyNoOutstandingExpectation(); + + provider.ClientOptions.Uri.Should().Be("http://overruled-again-by-discovery"); + provider.ClientOptions.Username.Should().BeNull(); + provider.ClientOptions.Password.Should().BeNull(); + provider.ClientOptions.Name.Should().Be("alternate-name"); + provider.ClientOptions.Environment.Should().Be("alternate-profile"); + provider.ClientOptions.Label.Should().Be("alternate-label"); + + optionsMonitor.CurrentValue.Uri.Should().Be("https://alternate-discovered-server.com:7777/internal"); + optionsMonitor.CurrentValue.Username.Should().Be("alternate-user"); + optionsMonitor.CurrentValue.Password.Should().Be("alternate-password"); + optionsMonitor.CurrentValue.Name.Should().Be(provider.ClientOptions.Name); + optionsMonitor.CurrentValue.Environment.Should().Be(provider.ClientOptions.Environment); + optionsMonitor.CurrentValue.Label.Should().Be(provider.ClientOptions.Label); + + configuration["example-server-key"].Should().Be("example-server-value"); + } +} diff --git a/src/Configuration/test/ConfigServer.Test/ConfigServerClientOptionsTest.cs b/src/Configuration/test/ConfigServer.Test/ConfigServerClientOptionsTest.cs index fe76426583..42950b3537 100644 --- a/src/Configuration/test/ConfigServer.Test/ConfigServerClientOptionsTest.cs +++ b/src/Configuration/test/ConfigServer.Test/ConfigServerClientOptionsTest.cs @@ -3,9 +3,12 @@ // See the LICENSE file in the project root for more information. using System.Reflection; +using System.Security.Cryptography.X509Certificates; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; +using RichardSzalay.MockHttp; using Steeltoe.Common.TestResources; namespace Steeltoe.Configuration.ConfigServer.Test; @@ -111,4 +114,312 @@ public async Task ConfigureConfigServerClientOptions_WithValues() options.Headers.Should().ContainKey("bar").WhoseValue.Should().Be("foo"); options.Headers.Should().ContainKey("foo").WhoseValue.Should().Be("bar"); } + + [Fact] + public void Clone_preserves_all_properties_and_produces_independent_nested_objects() + { + using var certificate = X509Certificate2.CreateFromPemFile("instance.crt", "instance.key"); + + var original = new ConfigServerClientOptions + { + ClientCertificate = + { + Certificate = certificate + }, + Enabled = false, + FailFast = true, + Environment = "staging", + Label = "feature/x", + Name = "my-app", + Uri = "https://config.example.com:9999", + Username = "user", + Password = "pass", + Token = "vault-token-123", + Timeout = 42_000, + PollingInterval = TimeSpan.FromSeconds(15), + ValidateCertificates = false, + Retry = + { + Enabled = true, + InitialInterval = 500, + MaxInterval = 5000, + Multiplier = 2.0, + MaxAttempts = 10 + }, + Discovery = + { + Enabled = true, + ServiceId = "my-config-server" + }, + Health = + { + Enabled = false, + TimeToLive = 999 + }, + AccessTokenUri = "https://uaa.example.com/oauth/token", + ClientSecret = "secret", + ClientId = "client-id", + TokenTtl = 600_000, + TokenRenewRate = 120_000, + DisableTokenRenewal = true, + Headers = + { + ["X-Custom"] = "value" + } + }; + + ConfigServerClientOptions clone = original.Clone(); + + clone.ClientCertificate.Should().NotBeSameAs(original.ClientCertificate); + clone.ClientCertificate.Certificate.Should().BeSameAs(original.ClientCertificate.Certificate); + + clone.Enabled.Should().Be(original.Enabled); + clone.FailFast.Should().Be(original.FailFast); + clone.Environment.Should().Be(original.Environment); + clone.Label.Should().Be(original.Label); + clone.Name.Should().Be(original.Name); + clone.Uri.Should().Be(original.Uri); + clone.Username.Should().Be(original.Username); + clone.Password.Should().Be(original.Password); + clone.Token.Should().Be(original.Token); + clone.Timeout.Should().Be(original.Timeout); + clone.PollingInterval.Should().Be(original.PollingInterval); + clone.ValidateCertificates.Should().Be(original.ValidateCertificates); + + clone.Retry.Should().NotBeSameAs(original.Retry); + clone.Retry.Enabled.Should().Be(original.Retry.Enabled); + clone.Retry.InitialInterval.Should().Be(original.Retry.InitialInterval); + clone.Retry.MaxInterval.Should().Be(original.Retry.MaxInterval); + clone.Retry.Multiplier.Should().Be(original.Retry.Multiplier); + clone.Retry.MaxAttempts.Should().Be(original.Retry.MaxAttempts); + + clone.Discovery.Should().NotBeSameAs(original.Discovery); + clone.Discovery.Enabled.Should().Be(original.Discovery.Enabled); + clone.Discovery.ServiceId.Should().Be(original.Discovery.ServiceId); + + clone.Health.Should().NotBeSameAs(original.Health); + clone.Health.Enabled.Should().Be(original.Health.Enabled); + clone.Health.TimeToLive.Should().Be(original.Health.TimeToLive); + + clone.AccessTokenUri.Should().Be(original.AccessTokenUri); + clone.ClientSecret.Should().Be(original.ClientSecret); + clone.ClientId.Should().Be(original.ClientId); + clone.TokenTtl.Should().Be(original.TokenTtl); + clone.TokenRenewRate.Should().Be(original.TokenRenewRate); + clone.DisableTokenRenewal.Should().Be(original.DisableTokenRenewal); + + clone.Headers.Should().NotBeSameAs(original.Headers); + clone.Headers.Should().BeEquivalentTo(original.Headers); + } + + [Fact] + public void Certificate_configuration_survives_options_reload() + { + const string configServerResponseJson = """ + { + "name": "myName", + "profiles": [ "Production" ], + "label": "test-label", + "version": "test-version", + "propertySources": [] + } + """; + + var fileProvider = new MemoryFileProvider(); + + fileProvider.IncludeAppSettingsJsonFile(""" + { + "spring": { + "cloud": { + "config": { + "name": "myName", + "timeout": 30000 + } + } + }, + "Certificates": { + "ConfigServer": { + "CertificateFilePath": "instance.crt", + "PrivateKeyFilePath": "instance.key" + } + } + } + """); + + using var handler = new DelegateToMockHttpClientHandler(); + + handler.Mock.Expect(HttpMethod.Get, "http://localhost:8888/myName/Production").Respond("application/json", configServerResponseJson); + + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryAppSettingsJsonFile(fileProvider); + configurationBuilder.AddConfigServer(new ConfigServerClientOptions(), handler, NullLoggerFactory.Instance); + IConfigurationRoot configuration = configurationBuilder.Build(); + + handler.Mock.VerifyNoOutstandingExpectation(); + + ConfigServerConfigurationProvider provider = configuration.Providers.OfType().Single(); + + IServiceCollection services = new ServiceCollection(); + services.AddSingleton(configuration); + services.ConfigureConfigServerClientOptions(); + + using ServiceProvider serviceProvider = services.BuildServiceProvider(true); + var optionsMonitor = serviceProvider.GetRequiredService>(); + + provider.ClientOptions.ClientCertificate.Certificate.Should().NotBeNull(); + optionsMonitor.CurrentValue.ClientCertificate.Certificate.Should().NotBeNull(); + + fileProvider.ReplaceAppSettingsJsonFile(""" + { + "spring": { + "cloud": { + "config": { + "name": "myName", + "timeout": 15000 + } + } + }, + "Certificates": { + "ConfigServer": { + "CertificateFilePath": "instance.crt", + "PrivateKeyFilePath": "instance.key" + } + } + } + """); + + fileProvider.NotifyChanged(); + + provider.ClientOptions.Timeout.Should().Be(15_000); + provider.ClientOptions.ClientCertificate.Certificate.Should().NotBeNull(); + optionsMonitor.CurrentValue.Timeout.Should().Be(15_000); + optionsMonitor.CurrentValue.ClientCertificate.Certificate.Should().NotBeNull(); + } + + [Fact] + public void Changes_in_IConfiguration_update_provider_options_and_injected_options() + { + const string configServerResponseJson = """ + { + "name": "example-app-name", + "profiles": [ + "example-profile" + ], + "label": "example-label", + "version": "1", + "propertySources": [ + { + "name": "example-source", + "source": { + "example-server-key": "example-server-value" + } + } + ] + } + """; + + var fileProvider = new MemoryFileProvider(); + + fileProvider.IncludeAppSettingsJsonFile(""" + { + "spring": { + "cloud": { + "config": { + "uri": "https://config.server.com:9999", + "name": "example-app-name", + "env": "example-profile", + "timeout": 30000 + } + } + } + } + """); + + using var handler = new DelegateToMockHttpClientHandler(); + + handler.Mock.Expect(HttpMethod.Get, "https://config.server.com:9999/example-app-name/example-profile/example-label") + .Respond("application/json", configServerResponseJson); + + var initialOptions = new ConfigServerClientOptions + { + Name = "ignored-because-overruled-from-appsettings", + Label = "example-label" // used, but missing in IConfiguration and injected options + }; + + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryAppSettingsJsonFile(fileProvider); + configurationBuilder.AddConfigServer(initialOptions, handler, NullLoggerFactory.Instance); + IConfigurationRoot configuration = configurationBuilder.Build(); + + handler.Mock.VerifyNoOutstandingExpectation(); + handler.Mock.Clear(); + + ConfigServerConfigurationProvider provider = configuration.Providers.OfType().Single(); + + IServiceCollection services = new ServiceCollection(); + services.AddSingleton(configuration); + services.ConfigureConfigServerClientOptions(); + + using ServiceProvider serviceProvider = services.BuildServiceProvider(true); + var optionsMonitor = serviceProvider.GetRequiredService>(); + + provider.ClientOptions.Uri.Should().Be("https://config.server.com:9999"); + provider.ClientOptions.Name.Should().Be("example-app-name"); + provider.ClientOptions.Environment.Should().Be("example-profile"); + provider.ClientOptions.Timeout.Should().Be(30_000); + provider.ClientOptions.Label.Should().Be("example-label"); + + optionsMonitor.CurrentValue.Uri.Should().Be(provider.ClientOptions.Uri); + optionsMonitor.CurrentValue.Name.Should().Be(provider.ClientOptions.Name); + optionsMonitor.CurrentValue.Environment.Should().Be(provider.ClientOptions.Environment); + optionsMonitor.CurrentValue.Timeout.Should().Be(provider.ClientOptions.Timeout); + optionsMonitor.CurrentValue.Label.Should().BeNull(); + + configuration["example-server-key"].Should().Be("example-server-value"); + + fileProvider.ReplaceAppSettingsJsonFile(""" + { + "spring": { + "cloud": { + "config": { + "uri": "https://alternate-config.server.com:7777", + "name": "alternate-name", + "env": "example-profile", + "timeout": 15000, + "label": "alternate-label" + } + } + } + } + """); + + fileProvider.NotifyChanged(); + + AssertFinal(); + + handler.Mock.Expect(HttpMethod.Get, "https://alternate-config.server.com:7777/alternate-name/example-profile/alternate-label") + .Respond("application/json", configServerResponseJson); + + provider.Load(); + handler.Mock.VerifyNoOutstandingExpectation(); + + AssertFinal(); + + void AssertFinal() + { + provider.ClientOptions.Uri.Should().Be("https://alternate-config.server.com:7777"); + provider.ClientOptions.Name.Should().Be("alternate-name"); + provider.ClientOptions.Environment.Should().Be("example-profile"); + provider.ClientOptions.Timeout.Should().Be(15_000); + provider.ClientOptions.Label.Should().Be("alternate-label"); + + optionsMonitor.CurrentValue.Uri.Should().Be(provider.ClientOptions.Uri); + optionsMonitor.CurrentValue.Name.Should().Be(provider.ClientOptions.Name); + optionsMonitor.CurrentValue.Environment.Should().Be(provider.ClientOptions.Environment); + optionsMonitor.CurrentValue.Timeout.Should().Be(provider.ClientOptions.Timeout); + optionsMonitor.CurrentValue.Label.Should().Be(provider.ClientOptions.Label); + + configuration["example-server-key"].Should().Be("example-server-value"); + } + } } diff --git a/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationBuilderExtensionsTest.cs b/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationBuilderExtensionsTest.cs index 72e81c2591..6d2db09874 100644 --- a/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationBuilderExtensionsTest.cs +++ b/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationBuilderExtensionsTest.cs @@ -130,11 +130,10 @@ public void AddConfigServer_WithConfigServerCertificate_AddsConfigServerSourceWi var configurationBuilder = new ConfigurationBuilder(); configurationBuilder.AddInMemoryCollection(appSettings); configurationBuilder.AddConfigServer(options, NullLoggerFactory.Instance); - _ = configurationBuilder.Build(); + IConfigurationRoot configurationRoot = configurationBuilder.Build(); - ConfigServerConfigurationSource? source = configurationBuilder.EnumerateSources().SingleOrDefault(); - source.Should().NotBeNull(); - source.DefaultOptions.ClientCertificate.Should().NotBeNull(); + ConfigServerConfigurationProvider provider = configurationRoot.EnumerateProviders().Single(); + provider.ClientOptions.ClientCertificate.Certificate.Should().NotBeNull(); } [Fact] @@ -154,11 +153,10 @@ public void AddConfigServer_WithGlobalCertificate_AddsConfigServerSourceWithCert var configurationBuilder = new ConfigurationBuilder(); configurationBuilder.AddInMemoryCollection(appSettings); configurationBuilder.AddConfigServer(options, NullLoggerFactory.Instance); - _ = configurationBuilder.Build(); + IConfigurationRoot configurationRoot = configurationBuilder.Build(); - ConfigServerConfigurationSource? source = configurationBuilder.EnumerateSources().SingleOrDefault(); - source.Should().NotBeNull(); - source.DefaultOptions.ClientCertificate.Should().NotBeNull(); + ConfigServerConfigurationProvider provider = configurationRoot.EnumerateProviders().Single(); + provider.ClientOptions.ClientCertificate.Certificate.Should().NotBeNull(); } [Fact] @@ -201,10 +199,13 @@ public void AddConfigServer_VCAP_SERVICES_Override_Defaults(string vcapServices) provider.Should().BeOfType(); provider.ClientOptions.Uri.Should().NotBe("https://uri-from-settings"); provider.ClientOptions.Uri.Should().Be("https://uri-from-vcap-services"); + provider.ClientOptions.ClientId.Should().Be("some-client-id"); + provider.ClientOptions.ClientSecret.Should().Be("some-secret"); + provider.ClientOptions.AccessTokenUri.Should().Be("https://uaa-uri-from-vcap-services/oauth/token"); } [Fact] - public void AddConfigServer_ConfigurationOverridesOptionsFromCode() + public void AddConfigServer_ConfigurationOverridesInitialOptionsFromCode() { var options = new ConfigServerClientOptions { @@ -221,16 +222,27 @@ public void AddConfigServer_ConfigurationOverridesOptionsFromCode() } }; - var appSettings = new Dictionary - { - ["Spring:Cloud:Config:Name"] = "nameInAppSettings", - ["Spring:Cloud:Config:Label"] = "labelInAppSettings", - ["Spring:Cloud:Config:Timeout"] = "50", - ["Spring:Cloud:Config:Retry:MaxInterval"] = "100" - }; + var fileProvider = new MemoryFileProvider(); + + fileProvider.IncludeAppSettingsJsonFile(""" + { + "Spring": { + "Cloud": { + "Config": { + "Name": "nameInAppSettings", + "Label": "labelInAppSettings", + "Timeout": 50, + "Retry": { + "MaxInterval": 100 + } + } + } + } + } + """); var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.AddInMemoryCollection(appSettings); + configurationBuilder.AddInMemoryAppSettingsJsonFile(fileProvider); configurationBuilder.AddConfigServer(options, NullLoggerFactory.Instance); IConfigurationRoot configurationRoot = configurationBuilder.Build(); @@ -245,6 +257,30 @@ public void AddConfigServer_ConfigurationOverridesOptionsFromCode() provider.ClientOptions.Timeout.Should().Be(50); provider.ClientOptions.Retry.InitialInterval.Should().Be(5); provider.ClientOptions.Retry.MaxInterval.Should().Be(100); + + fileProvider.ReplaceAppSettingsJsonFile(""" + { + "Spring": { + "Cloud": { + "Config": { + "Name": "alternateNameInAppSettings", + "Username": "alternateUsernameInAppSettings" + } + } + } + } + """); + + fileProvider.NotifyChanged(); + + provider.ClientOptions.Name.Should().Be("alternateNameInAppSettings"); + provider.ClientOptions.Label.Should().Be("labelInOptions"); + provider.ClientOptions.Environment.Should().Be("environmentInOptions"); + provider.ClientOptions.Username.Should().Be("alternateUsernameInAppSettings"); + provider.ClientOptions.Password.Should().Be("passwordInOptions"); + provider.ClientOptions.Timeout.Should().Be(10); + provider.ClientOptions.Retry.InitialInterval.Should().Be(5); + provider.ClientOptions.Retry.MaxInterval.Should().Be(15); } [Fact] diff --git a/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationProviderTest.Loading.cs b/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationProviderTest.Loading.cs index e5f9f669ac..1830173334 100644 --- a/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationProviderTest.Loading.cs +++ b/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationProviderTest.Loading.cs @@ -2,11 +2,13 @@ // The .NET Foundation licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information. +using System.Reflection; using FluentAssertions.Extensions; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.TestHost; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging.Abstractions; +using RichardSzalay.MockHttp; using Steeltoe.Common.TestResources; namespace Steeltoe.Configuration.ConfigServer.Test; @@ -26,7 +28,7 @@ public async Task RemoteLoadAsync_HostTimesOut() List requestUris = [new("http://localhost:9999/app/profile")]; // ReSharper disable once AccessToDisposedClosure - Func action = async () => await provider.RemoteLoadAsync(requestUris, null, TestContext.Current.CancellationToken); + Func action = async () => await provider.RemoteLoadAsync(provider.ClientOptions, requestUris, null, TestContext.Current.CancellationToken); (await action.Should().ThrowExactlyAsync()).WithInnerExceptionExactly(); } @@ -49,7 +51,7 @@ public async Task RemoteLoadAsync_ConfigServerReturnsGreaterThanEqualBadRequest( using var provider = new ConfigServerConfigurationProvider(options, null, httpClientHandler, NullLoggerFactory.Instance); // ReSharper disable once AccessToDisposedClosure - Func action = async () => await provider.RemoteLoadAsync(options.GetUris(), null, TestContext.Current.CancellationToken); + Func action = async () => await provider.RemoteLoadAsync(provider.ClientOptions, options.GetUris(), null, TestContext.Current.CancellationToken); await action.Should().ThrowExactlyAsync(); @@ -74,7 +76,7 @@ public async Task RemoteLoadAsync_ConfigServerReturnsLessThanBadRequest() using var httpClientHandler = new ForwardingHttpClientHandler(server.CreateHandler()); using var provider = new ConfigServerConfigurationProvider(options, null, httpClientHandler, NullLoggerFactory.Instance); - ConfigEnvironment? result = await provider.RemoteLoadAsync(options.GetUris(), null, TestContext.Current.CancellationToken); + ConfigEnvironment? result = await provider.RemoteLoadAsync(provider.ClientOptions, options.GetUris(), null, TestContext.Current.CancellationToken); startup.LastRequest.Should().NotBeNull(); startup.LastRequest.Path.Value.Should().Be($"/{options.Name}/{options.Environment}"); @@ -230,6 +232,70 @@ public async Task Create_WithNonZeroPollingIntervalAndClientDisabled_PollingDisa startup.WaitForFirstRequest(2.Seconds()).Should().BeFalse(); } + [Theory] + [InlineData(false, "00:00:01")] + [InlineData(true, "00:00:00")] + public void OnSettingsChanged_stops_timer_when_polling_becomes_ineffective(bool enabled, string pollingInterval) + { + const string configServerResponseJson = """ + { + "name": "myName", + "profiles": [ "Production" ], + "label": "test-label", + "version": "test-version", + "propertySources": [] + } + """; + + var fileProvider = new MemoryFileProvider(); + + fileProvider.IncludeAppSettingsJsonFile(""" + { + "spring": { + "cloud": { + "config": { + "name": "myName", + "enabled": true, + "pollingInterval": "00:00:01" + } + } + } + } + """); + + using var handler = new DelegateToMockHttpClientHandler(); + + handler.Mock.When(HttpMethod.Get, "http://localhost:8888/myName/Production").Respond("application/json", configServerResponseJson); + + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryAppSettingsJsonFile(fileProvider); + configurationBuilder.AddConfigServer(new ConfigServerClientOptions(), handler, NullLoggerFactory.Instance); + IConfigurationRoot configuration = configurationBuilder.Build(); + + ConfigServerConfigurationProvider provider = configuration.Providers.OfType().Single(); + FieldInfo refreshTimerField = typeof(ConfigServerConfigurationProvider).GetField("_refreshTimer", BindingFlags.NonPublic | BindingFlags.Instance)!; + + refreshTimerField.GetValue(provider).Should().NotBeNull(); + + fileProvider.ReplaceAppSettingsJsonFile($$""" + { + "spring": { + "cloud": { + "config": { + "name": "myName", + "enabled": {{(enabled ? "true" : "false")}}, + "pollingInterval": "{{pollingInterval}}" + } + } + } + } + """); + + fileProvider.NotifyChanged(); + + refreshTimerField.GetValue(provider).Should().BeNull(); + } + [Fact] public async Task DoLoad_MultipleLabels_ChecksAllLabels() { @@ -277,7 +343,7 @@ public async Task DoLoad_MultipleLabels_ChecksAllLabels() using var httpClientHandler = new ForwardingHttpClientHandler(server.CreateHandler()); using var provider = new ConfigServerConfigurationProvider(options, null, httpClientHandler, NullLoggerFactory.Instance); - await provider.DoLoadAsync(true, TestContext.Current.CancellationToken); + await provider.DoLoadAsync(provider.ClientOptions, true, TestContext.Current.CancellationToken); startup.LastRequest.Should().NotBeNull(); startup.RequestCount.Should().Be(2); @@ -321,7 +387,7 @@ public async Task RemoteLoadAsync_ConfigServerReturnsGood() using var httpClientHandler = new ForwardingHttpClientHandler(server.CreateHandler()); using var provider = new ConfigServerConfigurationProvider(options, null, httpClientHandler, NullLoggerFactory.Instance); - ConfigEnvironment? env = await provider.RemoteLoadAsync(options.GetUris(), null, TestContext.Current.CancellationToken); + ConfigEnvironment? env = await provider.RemoteLoadAsync(provider.ClientOptions, options.GetUris(), null, TestContext.Current.CancellationToken); startup.LastRequest.Should().NotBeNull(); startup.LastRequest.Path.Value.Should().Be($"/{options.Name}/{options.Environment}"); @@ -429,7 +495,7 @@ public async Task Load_ConfigServerReturnsNotFoundStatus() startup.LastRequest.Should().NotBeNull(); startup.LastRequest.Path.Value.Should().Be($"/{options.Name}/{options.Environment}"); - provider.Properties.Should().HaveCount(27); + provider.InnerData.Should().BeEmpty(); } [Fact] @@ -754,35 +820,29 @@ public async Task ReLoad_DataDictionary_With_New_Configurations() } [Fact] - public void AddConfigServerClientSettings_ChangesDataDictionary() + public void DataDictionary_DoesNotContainRedundantClientSettings() { var options = new ConfigServerClientOptions { Enabled = false, FailFast = true, Environment = "environment", - Label = "label", + Label = "main", Name = "name", Uri = "https://foo.bar/", - Username = "username", - Password = "password", - Token = "vaultToken", + Username = "user", + Password = "pass", + Token = "vault-token", Timeout = 75_000, - PollingInterval = 35.5.Seconds(), + PollingInterval = TimeSpan.FromSeconds(30), ValidateCertificates = false, - AccessTokenUri = "https://token.server.com/", - ClientSecret = "client_secret", - ClientId = "client_id", - TokenTtl = 2, - TokenRenewRate = 1, - DisableTokenRenewal = true, Retry = { Enabled = true, - InitialInterval = 8, - MaxInterval = 16, - Multiplier = 1.1, - MaxAttempts = 7 + InitialInterval = 500, + MaxInterval = 5000, + Multiplier = 2.0, + MaxAttempts = 10 }, Discovery = { @@ -792,53 +852,56 @@ public void AddConfigServerClientSettings_ChangesDataDictionary() Health = { Enabled = false, - TimeToLive = 9 + TimeToLive = 999 }, + AccessTokenUri = "https://uaa.example.com/oauth/token", + ClientSecret = "secret", + ClientId = "client-id", + TokenTtl = 600_000, + TokenRenewRate = 120_000, + DisableTokenRenewal = true, Headers = { - ["headerName1"] = "headerValue1", - ["headerName2"] = "headerValue2" + ["X-Custom"] = "value" } }; using var provider = new ConfigServerConfigurationProvider(options, null, null, NullLoggerFactory.Instance); - provider.AddConfigServerClientOptions(); - - AssertDataValue("spring:cloud:config:enabled", "False"); - AssertDataValue("spring:cloud:config:failFast", "True"); - AssertDataValue("spring:cloud:config:env", "environment"); - AssertDataValue("spring:cloud:config:label", "label"); - AssertDataValue("spring:cloud:config:name", "name"); - AssertDataValue("spring:cloud:config:uri", "https://foo.bar/"); - AssertDataValue("spring:cloud:config:username", "username"); - AssertDataValue("spring:cloud:config:password", "password"); - AssertDataValue("spring:cloud:config:token", "vaultToken"); - AssertDataValue("spring:cloud:config:timeout", "75000"); - AssertDataValue("spring:cloud:config:pollingInterval", "00:00:35.5000000"); - AssertDataValue("spring:cloud:config:validateCertificates", "False"); - AssertDataValue("spring:cloud:config:accessTokenUri", "https://token.server.com/"); - AssertDataValue("spring:cloud:config:clientSecret", "client_secret"); - AssertDataValue("spring:cloud:config:clientId", "client_id"); - AssertDataValue("spring:cloud:config:tokenTtl", "2"); - AssertDataValue("spring:cloud:config:tokenRenewRate", "1"); - AssertDataValue("spring:cloud:config:disableTokenRenewal", "True"); - AssertDataValue("spring:cloud:config:retry:enabled", "True"); - AssertDataValue("spring:cloud:config:retry:initialInterval", "8"); - AssertDataValue("spring:cloud:config:retry:maxInterval", "16"); - AssertDataValue("spring:cloud:config:retry:multiplier", "1.1"); - AssertDataValue("spring:cloud:config:retry:maxAttempts", "7"); - AssertDataValue("spring:cloud:config:discovery:enabled", "True"); - AssertDataValue("spring:cloud:config:discovery:serviceId", "my-config-server"); - AssertDataValue("spring:cloud:config:health:enabled", "False"); - AssertDataValue("spring:cloud:config:health:timeToLive", "9"); - AssertDataValue("spring:cloud:config:headers:headerName1", "headerValue1"); - AssertDataValue("spring:cloud:config:headers:headerName2", "headerValue2"); - - void AssertDataValue(string key, string expected) - { - provider.TryGet(key, out string? value).Should().BeTrue(); - value.Should().Be(expected); - } + + provider.TryGet("spring:cloud:config:enabled", out _).Should().BeFalse(); + provider.TryGet("spring:cloud:config:failFast", out _).Should().BeFalse(); + provider.TryGet("spring:cloud:config:env", out _).Should().BeFalse(); + provider.TryGet("spring:cloud:config:label", out _).Should().BeFalse(); + provider.TryGet("spring:cloud:config:name", out _).Should().BeFalse(); + provider.TryGet("spring:cloud:config:uri", out _).Should().BeFalse(); + provider.TryGet("spring:cloud:config:username", out _).Should().BeFalse(); + provider.TryGet("spring:cloud:config:password", out _).Should().BeFalse(); + provider.TryGet("spring:cloud:config:token", out _).Should().BeFalse(); + provider.TryGet("spring:cloud:config:timeout", out _).Should().BeFalse(); + provider.TryGet("spring:cloud:config:pollingInterval", out _).Should().BeFalse(); + provider.TryGet("spring:cloud:config:validateCertificates", out _).Should().BeFalse(); + provider.TryGet("spring:cloud:config:validate_Certificates", out _).Should().BeFalse(); + + provider.TryGet("spring:cloud:config:retry:enabled", out _).Should().BeFalse(); + provider.TryGet("spring:cloud:config:retry:initialInterval", out _).Should().BeFalse(); + provider.TryGet("spring:cloud:config:retry:maxInterval", out _).Should().BeFalse(); + provider.TryGet("spring:cloud:config:retry:multiplier", out _).Should().BeFalse(); + provider.TryGet("spring:cloud:config:retry:maxAttempts", out _).Should().BeFalse(); + + provider.TryGet("spring:cloud:config:discovery:enabled", out _).Should().BeFalse(); + provider.TryGet("spring:cloud:config:discovery:serviceId", out _).Should().BeFalse(); + + provider.TryGet("spring:cloud:config:health:enabled", out _).Should().BeFalse(); + provider.TryGet("spring:cloud:config:health:timeToLive", out _).Should().BeFalse(); + + provider.TryGet("spring:cloud:config:accessTokenUri", out _).Should().BeFalse(); + provider.TryGet("spring:cloud:config:clientSecret", out _).Should().BeFalse(); + provider.TryGet("spring:cloud:config:clientId", out _).Should().BeFalse(); + provider.TryGet("spring:cloud:config:tokenTtl", out _).Should().BeFalse(); + provider.TryGet("spring:cloud:config:tokenRenewRate", out _).Should().BeFalse(); + provider.TryGet("spring:cloud:config:disableTokenRenewal", out _).Should().BeFalse(); + + provider.TryGet("spring:cloud:config:headers:X-Custom", out _).Should().BeFalse(); } [Fact] diff --git a/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationProviderTest.Settings.cs b/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationProviderTest.Settings.cs index 8380debc6b..f861d98d0b 100644 --- a/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationProviderTest.Settings.cs +++ b/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationProviderTest.Settings.cs @@ -26,7 +26,7 @@ public void SourceConstructor_WithDefaults_InitializesWithDefaultSettings() { IConfiguration configuration = new ConfigurationBuilder().Build(); var options = new ConfigServerClientOptions(); - var source = new ConfigServerConfigurationSource(options, configuration, NullLoggerFactory.Instance); + var source = new ConfigServerConfigurationSource(options, configuration, null, NullLoggerFactory.Instance); using var provider = new ConfigServerConfigurationProvider(source, NullLoggerFactory.Instance); string? expectedAppName = Assembly.GetEntryAssembly()!.GetName().Name; @@ -44,9 +44,9 @@ public void SourceConstructor_WithTimeoutConfigured_InitializesHttpClientWithCon IConfiguration configuration = new ConfigurationBuilder().AddInMemoryCollection(appSettings).Build(); var options = new ConfigServerClientOptions(); - var source = new ConfigServerConfigurationSource(options, configuration, NullLoggerFactory.Instance); + var source = new ConfigServerConfigurationSource(options, configuration, null, NullLoggerFactory.Instance); using var provider = new ConfigServerConfigurationProvider(source, NullLoggerFactory.Instance); - using HttpClient httpClient = provider.CreateHttpClient(options); + using HttpClient httpClient = provider.CreateHttpClient(provider.ClientOptions); httpClient.Should().NotBeNull(); httpClient.Timeout.Should().Be(30.Seconds()); @@ -64,7 +64,7 @@ public void GetConfigServerUri_NoBaseUri_Throws() using var provider = new ConfigServerConfigurationProvider(options, null, null, NullLoggerFactory.Instance); // ReSharper disable once AccessToDisposedClosure - Action action = () => provider.BuildConfigServerUri(null!, null); + Action action = () => provider.BuildConfigServerUri(provider.ClientOptions, null!, null); action.Should().ThrowExactly(); } @@ -79,7 +79,7 @@ public void GetConfigServerUri_NoLabel() }; using var provider = new ConfigServerConfigurationProvider(options, null, null, NullLoggerFactory.Instance); - string uri = provider.BuildConfigServerUri(new Uri(options.Uri!), null).ToString(); + string uri = provider.BuildConfigServerUri(provider.ClientOptions, new Uri(options.Uri!), null).ToString(); uri.Should().Be($"{options.Uri}/{options.Name}/{options.Environment}"); } @@ -95,7 +95,7 @@ public void GetConfigServerUri_WithLabel() }; using var provider = new ConfigServerConfigurationProvider(options, null, null, NullLoggerFactory.Instance); - string uri = provider.BuildConfigServerUri(new Uri(options.Uri!), options.Label).ToString(); + string uri = provider.BuildConfigServerUri(provider.ClientOptions, new Uri(options.Uri!), options.Label).ToString(); uri.Should().Be($"{options.Uri}/{options.Name}/{options.Environment}/{options.Label}"); } @@ -111,7 +111,7 @@ public void GetConfigServerUri_WithLabelContainingSlash() }; using var provider = new ConfigServerConfigurationProvider(options, null, null, NullLoggerFactory.Instance); - string uri = provider.BuildConfigServerUri(new Uri(options.Uri!), options.Label).ToString(); + string uri = provider.BuildConfigServerUri(provider.ClientOptions, new Uri(options.Uri!), options.Label).ToString(); uri.Should().Be($"{options.Uri}/{options.Name}/{options.Environment}/myLabel(_)version"); } @@ -127,7 +127,7 @@ public void GetConfigServerUri_WithExtraPathInfo() }; using var provider = new ConfigServerConfigurationProvider(options, null, null, NullLoggerFactory.Instance); - string uri = provider.BuildConfigServerUri(new Uri(options.Uri), null).ToString(); + string uri = provider.BuildConfigServerUri(provider.ClientOptions, new Uri(options.Uri), null).ToString(); uri.Should().Be($"http://localhost:9999/myPath/path/{options.Name}/{options.Environment}"); } @@ -143,7 +143,7 @@ public void GetConfigServerUri_WithExtraPathInfo_NoEndingSlash() }; using var provider = new ConfigServerConfigurationProvider(options, null, null, NullLoggerFactory.Instance); - string uri = provider.BuildConfigServerUri(new Uri(options.Uri), null).ToString(); + string uri = provider.BuildConfigServerUri(provider.ClientOptions, new Uri(options.Uri), null).ToString(); uri.Should().Be($"http://localhost:9999/myPath/path/{options.Name}/{options.Environment}"); } @@ -159,7 +159,7 @@ public void GetConfigServerUri_NoEndingSlash() }; using var provider = new ConfigServerConfigurationProvider(options, null, null, NullLoggerFactory.Instance); - string uri = provider.BuildConfigServerUri(new Uri(options.Uri), null).ToString(); + string uri = provider.BuildConfigServerUri(provider.ClientOptions, new Uri(options.Uri), null).ToString(); uri.Should().Be($"http://localhost:9999/{options.Name}/{options.Environment}"); } @@ -175,7 +175,7 @@ public void GetConfigServerUri_WithEndingSlash() }; using var provider = new ConfigServerConfigurationProvider(options, null, null, NullLoggerFactory.Instance); - string uri = provider.BuildConfigServerUri(new Uri(options.Uri), null).ToString(); + string uri = provider.BuildConfigServerUri(provider.ClientOptions, new Uri(options.Uri), null).ToString(); uri.Should().Be($"http://localhost:9999/{options.Name}/{options.Environment}"); } @@ -191,7 +191,7 @@ public void GetConfigServerUri_MultipleEnvironments_EncodesComma() }; using var provider = new ConfigServerConfigurationProvider(options, null, null, NullLoggerFactory.Instance); - string uri = provider.BuildConfigServerUri(new Uri(options.Uri!), options.Label).ToString(); + string uri = provider.BuildConfigServerUri(provider.ClientOptions, new Uri(options.Uri!), options.Label).ToString(); uri.Should().Be("http://localhost:8888/myName/one%2Ctwo/demo"); } @@ -209,7 +209,7 @@ public void GetConfigServerUri_EncodesSpecialCharacters() }; using var provider = new ConfigServerConfigurationProvider(options, null, null, NullLoggerFactory.Instance); - string uri = provider.BuildConfigServerUri(new Uri(options.Uri!), options.Label).ToString(); + string uri = provider.BuildConfigServerUri(provider.ClientOptions, new Uri(options.Uri!), options.Label).ToString(); uri.Should().Be( "http://a%22%3A%3F%27*%2Cb%2F%5C%26:%23%26%3A%24%3C%3E%27%2Fso%2C%22me@localhost:8888/my%24<>%3A%2C\"%27Name/%40%2F%26%3F%3A\"%27/^%26%24%40%23*<>%2B"); diff --git a/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationProviderTest.cs b/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationProviderTest.cs index 1f13886305..c827ede9fa 100644 --- a/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationProviderTest.cs +++ b/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationProviderTest.cs @@ -67,7 +67,7 @@ public void GetLabels_Null() var options = new ConfigServerClientOptions(); using var provider = new ConfigServerConfigurationProvider(options, null, null, NullLoggerFactory.Instance); - string[] result = provider.GetLabels(); + string[] result = provider.GetLabels(provider.ClientOptions); result.Should().ContainSingle().Which.Should().BeEmpty(); } @@ -81,7 +81,7 @@ public void GetLabels_Empty() using var provider = new ConfigServerConfigurationProvider(options, null, null, NullLoggerFactory.Instance); - string[] result = provider.GetLabels(); + string[] result = provider.GetLabels(provider.ClientOptions); result.Should().ContainSingle().Which.Should().BeEmpty(); } @@ -95,7 +95,7 @@ public void GetLabels_SingleString() using var provider = new ConfigServerConfigurationProvider(options, null, null, NullLoggerFactory.Instance); - string[] result = provider.GetLabels(); + string[] result = provider.GetLabels(provider.ClientOptions); result.Should().ContainSingle().Which.Should().Be("foobar"); } @@ -109,7 +109,7 @@ public void GetLabels_MultiString() using var provider = new ConfigServerConfigurationProvider(options, null, null, NullLoggerFactory.Instance); - string[] result = provider.GetLabels(); + string[] result = provider.GetLabels(provider.ClientOptions); result.Should().HaveCount(3); result.Should().HaveElementAt(0, "1"); result.Should().HaveElementAt(1, "2"); @@ -126,7 +126,7 @@ public void GetLabels_MultiStringHoles() using var provider = new ConfigServerConfigurationProvider(options, null, null, NullLoggerFactory.Instance); - string[] result = provider.GetLabels(); + string[] result = provider.GetLabels(provider.ClientOptions); result.Should().HaveCount(3); result.Should().HaveElementAt(0, "1"); result.Should().HaveElementAt(1, "2"); @@ -145,8 +145,8 @@ public async Task GetRequestMessage_AddsBasicAuthIfUserNameAndPasswordInURL() using var provider = new ConfigServerConfigurationProvider(options, null, null, NullLoggerFactory.Instance); - Uri requestUri = provider.BuildConfigServerUri(new Uri(options.Uri), null); - HttpRequestMessage request = await provider.GetRequestMessageAsync(requestUri, TestContext.Current.CancellationToken); + Uri requestUri = provider.BuildConfigServerUri(provider.ClientOptions, new Uri(options.Uri), null); + HttpRequestMessage request = await provider.GetRequestMessageAsync(provider.ClientOptions, requestUri, TestContext.Current.CancellationToken); request.Method.Should().Be(HttpMethod.Get); request.RequestUri.Should().Be(requestUri); @@ -169,8 +169,8 @@ public async Task GetRequestMessage_AddsBasicAuthIfUserNameAndPasswordInSettings using var provider = new ConfigServerConfigurationProvider(options, null, null, NullLoggerFactory.Instance); - Uri requestUri = provider.BuildConfigServerUri(new Uri(options.Uri), null); - HttpRequestMessage request = await provider.GetRequestMessageAsync(requestUri, TestContext.Current.CancellationToken); + Uri requestUri = provider.BuildConfigServerUri(provider.ClientOptions, new Uri(options.Uri), null); + HttpRequestMessage request = await provider.GetRequestMessageAsync(provider.ClientOptions, requestUri, TestContext.Current.CancellationToken); request.Method.Should().Be(HttpMethod.Get); request.RequestUri.Should().Be(requestUri); @@ -193,8 +193,8 @@ public async Task GetRequestMessage_BasicAuthInSettingsOverridesUserNameAndPassw using var provider = new ConfigServerConfigurationProvider(options, null, null, NullLoggerFactory.Instance); - Uri requestUri = provider.BuildConfigServerUri(new Uri(options.Uri), null); - HttpRequestMessage request = await provider.GetRequestMessageAsync(requestUri, TestContext.Current.CancellationToken); + Uri requestUri = provider.BuildConfigServerUri(provider.ClientOptions, new Uri(options.Uri), null); + HttpRequestMessage request = await provider.GetRequestMessageAsync(provider.ClientOptions, requestUri, TestContext.Current.CancellationToken); request.Method.Should().Be(HttpMethod.Get); request.RequestUri.Should().Be(requestUri); @@ -215,8 +215,8 @@ public async Task GetRequestMessage_AddsVaultToken_IfNeeded() using var provider = new ConfigServerConfigurationProvider(options, null, null, NullLoggerFactory.Instance); - Uri requestUri = provider.BuildConfigServerUri(new Uri(options.Uri!), null); - HttpRequestMessage request = await provider.GetRequestMessageAsync(requestUri, TestContext.Current.CancellationToken); + Uri requestUri = provider.BuildConfigServerUri(provider.ClientOptions, new Uri(options.Uri!), null); + HttpRequestMessage request = await provider.GetRequestMessageAsync(provider.ClientOptions, requestUri, TestContext.Current.CancellationToken); request.Method.Should().Be(HttpMethod.Get); request.RequestUri.Should().Be(requestUri); @@ -241,7 +241,7 @@ public async Task RefreshVaultToken_Succeeds() .WithContent("{\"increment\":300}").Respond(HttpStatusCode.NoContent); using var provider = new ConfigServerConfigurationProvider(options, null, handler, NullLoggerFactory.Instance); - await provider.RefreshVaultTokenAsync(TestContext.Current.CancellationToken); + await provider.RefreshVaultTokenAsync(provider.ClientOptions, TestContext.Current.CancellationToken); handler.Mock.VerifyNoOutstandingExpectation(); } @@ -268,7 +268,7 @@ public async Task RefreshVaultToken_With_AccessTokenUri_Succeeds() .WithHeaders("Authorization", "Bearer secret").WithContent("{\"increment\":300}").Respond(HttpStatusCode.NoContent); using var provider = new ConfigServerConfigurationProvider(options, null, handler, NullLoggerFactory.Instance); - await provider.RefreshVaultTokenAsync(TestContext.Current.CancellationToken); + await provider.RefreshVaultTokenAsync(provider.ClientOptions, TestContext.Current.CancellationToken); handler.Mock.VerifyNoOutstandingExpectation(); } @@ -288,7 +288,7 @@ public void GetHttpClient_AddsHeaders_IfConfigured() }; using var provider = new ConfigServerConfigurationProvider(options, null, null, NullLoggerFactory.Instance); - using HttpClient httpClient = provider.CreateHttpClient(options); + using HttpClient httpClient = provider.CreateHttpClient(provider.ClientOptions); httpClient.Should().NotBeNull(); httpClient.DefaultRequestHeaders.GetValues("foo").SingleOrDefault().Should().Be("bar"); @@ -310,7 +310,7 @@ public void IsDiscoveryFirstEnabled_ReturnsExpected() using (var provider = new ConfigServerConfigurationProvider(options, null, null, NullLoggerFactory.Instance)) { - provider.IsDiscoveryFirstEnabled().Should().BeTrue(); + provider.ClientOptions.Discovery.Enabled.Should().BeTrue(); } var values = new Dictionary @@ -326,11 +326,11 @@ public void IsDiscoveryFirstEnabled_ReturnsExpected() Environment = "development" }; - var source = new ConfigServerConfigurationSource(options, configuration, NullLoggerFactory.Instance); + var source = new ConfigServerConfigurationSource(options, configuration, null, NullLoggerFactory.Instance); using (var provider = new ConfigServerConfigurationProvider(source, NullLoggerFactory.Instance)) { - provider.IsDiscoveryFirstEnabled().Should().BeTrue(); + provider.ClientOptions.Discovery.Enabled.Should().BeTrue(); } } @@ -344,20 +344,22 @@ public void UpdateSettingsFromDiscovery_UpdatesSettingsCorrectly() IConfiguration configuration = new ConfigurationBuilder().AddInMemoryCollection(values).Build(); - var options = new ConfigServerClientOptions + var initialOptions = new ConfigServerClientOptions { Uri = "http://localhost:8888/", Name = "foo", Environment = "development" }; - var source = new ConfigServerConfigurationSource(options, configuration, NullLoggerFactory.Instance); + var source = new ConfigServerConfigurationSource(initialOptions, configuration, null, NullLoggerFactory.Instance); using var provider = new ConfigServerConfigurationProvider(source, NullLoggerFactory.Instance); - provider.UpdateSettingsFromDiscovery(new List(), options); - options.Username.Should().BeNull(); - options.Password.Should().BeNull(); - options.Uri.Should().Be("http://localhost:8888/"); + ConfigServerClientOptions optionsSnapshot = provider.ClientOptions; + provider.SetLastDiscoveryLookupResult(new List()); + provider.ApplyLastDiscoveryLookupResultToClientOptions(optionsSnapshot); + optionsSnapshot.Username.Should().BeNull(); + optionsSnapshot.Password.Should().BeNull(); + optionsSnapshot.Uri.Should().Be("http://localhost:8888/"); var metadata1 = new Dictionary { @@ -377,10 +379,12 @@ public void UpdateSettingsFromDiscovery_UpdatesSettingsCorrectly() new TestServiceInstance("s", "i2", new Uri("https://foo.bar.baz:9999/"), metadata2) ]; - provider.UpdateSettingsFromDiscovery(instances, options); - options.Username.Should().Be("secondUser"); - options.Password.Should().Be("secondPassword"); - options.Uri.Should().Be("https://foo.bar:8888/,https://foo.bar.baz:9999/configPath"); + optionsSnapshot = provider.ClientOptions; + provider.SetLastDiscoveryLookupResult(instances); + provider.ApplyLastDiscoveryLookupResultToClientOptions(optionsSnapshot); + optionsSnapshot.Username.Should().Be("secondUser"); + optionsSnapshot.Password.Should().Be("secondPassword"); + optionsSnapshot.Uri.Should().Be("https://foo.bar:8888/,https://foo.bar.baz:9999/configPath"); } [Fact] @@ -402,7 +406,7 @@ public async Task DiscoverServerInstances_FailsFast() Timeout = 10 }; - var source = new ConfigServerConfigurationSource(options, configuration, NullLoggerFactory.Instance); + var source = new ConfigServerConfigurationSource(options, configuration, null, NullLoggerFactory.Instance); using var provider = new ConfigServerConfigurationProvider(source, NullLoggerFactory.Instance); // ReSharper disable once AccessToDisposedClosure @@ -415,17 +419,4 @@ private static string GetEncodedUserPassword(string user, string password) { return Convert.ToBase64String(Encoding.ASCII.GetBytes($"{user}:{password}")); } - - private sealed class TestServiceInstance(string serviceId, string instanceId, Uri uri, IReadOnlyDictionary metadata) : IServiceInstance - { - public string ServiceId { get; } = serviceId; - public string InstanceId { get; } = instanceId; - public string Host { get; } = uri.Host; - public int Port { get; } = uri.Port; - public bool IsSecure { get; } = uri.Scheme == Uri.UriSchemeHttps; - public Uri Uri { get; } = uri; - public Uri? NonSecureUri => IsSecure ? null : Uri; - public Uri? SecureUri => IsSecure ? Uri : null; - public IReadOnlyDictionary Metadata { get; } = metadata; - } } diff --git a/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationSourceTest.cs b/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationSourceTest.cs index 1be77f49a0..44a52b53c2 100644 --- a/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationSourceTest.cs +++ b/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationSourceTest.cs @@ -21,7 +21,7 @@ public void Constructors_InitializesProperties() var source = new ConfigServerConfigurationSource(options, sources, new Dictionary { ["foo"] = "bar" - }, NullLoggerFactory.Instance); + }, null, NullLoggerFactory.Instance); source.DefaultOptions.Should().Be(options); source.Configuration.Should().BeNull(); @@ -31,7 +31,7 @@ public void Constructors_InitializesProperties() source.Properties.Should().ContainKey("foo").WhoseValue.Should().Be("bar"); IConfigurationRoot configurationRoot = new ConfigurationBuilder().AddInMemoryCollection().Build(); - source = new ConfigServerConfigurationSource(options, configurationRoot, NullLoggerFactory.Instance); + source = new ConfigServerConfigurationSource(options, configurationRoot, null, NullLoggerFactory.Instance); source.DefaultOptions.Should().Be(options); ConfigurationRoot? root = source.Configuration.Should().BeOfType().Subject; @@ -46,7 +46,7 @@ public void Build_ReturnsProvider() var memSource = new MemoryConfigurationSource(); List sources = [memSource]; - var source = new ConfigServerConfigurationSource(options, sources, null, NullLoggerFactory.Instance); + var source = new ConfigServerConfigurationSource(options, sources, null, null, NullLoggerFactory.Instance); IConfigurationProvider provider = source.Build(new ConfigurationBuilder()); provider.Should().BeOfType(); diff --git a/src/Configuration/test/ConfigServer.Test/TestServiceInstance.cs b/src/Configuration/test/ConfigServer.Test/TestServiceInstance.cs new file mode 100644 index 0000000000..91167ccba5 --- /dev/null +++ b/src/Configuration/test/ConfigServer.Test/TestServiceInstance.cs @@ -0,0 +1,20 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the Apache 2.0 License. +// See the LICENSE file in the project root for more information. + +using Steeltoe.Common.Discovery; + +namespace Steeltoe.Configuration.ConfigServer.Test; + +internal sealed class TestServiceInstance(string serviceId, string instanceId, Uri uri, IReadOnlyDictionary metadata) : IServiceInstance +{ + public string ServiceId { get; } = serviceId; + public string InstanceId { get; } = instanceId; + public string Host { get; } = uri.Host; + public int Port { get; } = uri.Port; + public bool IsSecure { get; } = uri.Scheme == Uri.UriSchemeHttps; + public Uri Uri { get; } = uri; + public Uri? NonSecureUri => IsSecure ? null : Uri; + public Uri? SecureUri => IsSecure ? Uri : null; + public IReadOnlyDictionary Metadata { get; } = metadata; +} From 661ec730be9fed5c5fb3e28cd72f328efedf3570 Mon Sep 17 00:00:00 2001 From: Bart Koelman <104792814+bart-vmware@users.noreply.github.com> Date: Fri, 27 Mar 2026 23:59:12 +0100 Subject: [PATCH 05/29] Improve test coverage --- ...ServerConfigurationProviderTest.Loading.cs | 155 ++++++++++++++++++ .../ConfigServerConfigurationProviderTest.cs | 34 ++++ 2 files changed, 189 insertions(+) diff --git a/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationProviderTest.Loading.cs b/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationProviderTest.Loading.cs index 1830173334..7f62a5f779 100644 --- a/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationProviderTest.Loading.cs +++ b/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationProviderTest.Loading.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information. +using System.Net.Sockets; using System.Reflection; using FluentAssertions.Extensions; using Microsoft.AspNetCore.Builder; @@ -296,6 +297,154 @@ public void OnSettingsChanged_stops_timer_when_polling_becomes_ineffective(bool refreshTimerField.GetValue(provider).Should().BeNull(); } + [Fact] + public void OnSettingsChanged_reschedules_timer_when_polling_interval_changes() + { + const string configServerResponseJson = """ + { + "name": "myName", + "profiles": [ "Production" ], + "label": "test-label", + "version": "test-version", + "propertySources": [] + } + """; + + var fileProvider = new MemoryFileProvider(); + + fileProvider.IncludeAppSettingsJsonFile(""" + { + "spring": { + "cloud": { + "config": { + "name": "myName", + "enabled": true, + "pollingInterval": "00:00:05" + } + } + } + } + """); + + using var handler = new DelegateToMockHttpClientHandler(); + + handler.Mock.When(HttpMethod.Get, "http://localhost:8888/myName/Production").Respond("application/json", configServerResponseJson); + + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryAppSettingsJsonFile(fileProvider); + configurationBuilder.AddConfigServer(new ConfigServerClientOptions(), handler, NullLoggerFactory.Instance); + IConfigurationRoot configuration = configurationBuilder.Build(); + + ConfigServerConfigurationProvider provider = configuration.Providers.OfType().Single(); + FieldInfo refreshTimerField = typeof(ConfigServerConfigurationProvider).GetField("_refreshTimer", BindingFlags.NonPublic | BindingFlags.Instance)!; + + refreshTimerField.GetValue(provider).Should().NotBeNull(); + + fileProvider.ReplaceAppSettingsJsonFile(""" + { + "spring": { + "cloud": { + "config": { + "name": "myName", + "enabled": true, + "pollingInterval": "00:00:10" + } + } + } + } + """); + + fileProvider.NotifyChanged(); + + refreshTimerField.GetValue(provider).Should().NotBeNull("timer should be rescheduled, not stopped"); + } + + [Fact] + public async Task Load_MultipleConfigServers_SocketError_FallsBackToNextServer() + { + const string environment = """ + { + "name": "test-name", + "profiles": [ "Production" ], + "label": "test-label", + "version": "test-version", + "propertySources": [ + { + "name": "source", + "source": { + "key1": "value1" + } + } + ] + } + """; + + using var handler = new DelegateToMockHttpClientHandler(); + + handler.Mock.When(HttpMethod.Get, "http://server1:8888/myName/Production") + .Throw(new HttpRequestException("Connection refused", new SocketException((int)SocketError.ConnectionRefused))); + + handler.Mock.When(HttpMethod.Get, "http://server2:8888/myName/Production").Respond("application/json", environment); + + var options = new ConfigServerClientOptions + { + Name = "myName", + Uri = "http://server1:8888, http://server2:8888" + }; + + using var provider = new ConfigServerConfigurationProvider(options, null, handler, NullLoggerFactory.Instance); + + await provider.LoadInternalAsync(true, TestContext.Current.CancellationToken); + + provider.TryGet("key1", out string? value).Should().BeTrue(); + value.Should().Be("value1"); + } + + [Fact] + public async Task DoLoad_IdenticalData_DoesNotTriggerReload() + { + const string environment = """ + { + "name": "test-name", + "profiles": [ "Production" ], + "label": "test-label", + "version": "test-version", + "propertySources": [ + { + "name": "source", + "source": { + "key1": "value1" + } + } + ] + } + """; + + using var handler = new DelegateToMockHttpClientHandler(); + + handler.Mock.When(HttpMethod.Get, "http://localhost:8888/myName/Production").Respond("application/json", environment); + + var options = new ConfigServerClientOptions + { + Name = "myName" + }; + + using var provider = new ConfigServerConfigurationProvider(options, null, handler, NullLoggerFactory.Instance); + + await provider.DoLoadAsync(provider.ClientOptions, true, TestContext.Current.CancellationToken); + provider.TryGet("key1", out string? value).Should().BeTrue(); + value.Should().Be("value1"); + + bool reloadFired = false; + provider.GetReloadToken().RegisterChangeCallback(_ => reloadFired = true, null); + + await provider.DoLoadAsync(provider.ClientOptions, true, TestContext.Current.CancellationToken); + + reloadFired.Should().BeFalse("identical data should not trigger OnReload"); + provider.TryGet("key1", out value).Should().BeTrue(); + value.Should().Be("value1"); + } + [Fact] public async Task DoLoad_MultipleLabels_ChecksAllLabels() { @@ -701,6 +850,7 @@ public async Task Load_ChangesDataDictionary() ], "label": "test-label", "version": "test-version", + "state": "test-state", "propertySources": [ { "name": "source", @@ -736,6 +886,11 @@ public async Task Load_ChangesDataDictionary() value.Should().Be("value1"); provider.TryGet("key2", out value).Should().BeTrue(); value.Should().Be("10"); + + provider.TryGet("spring:cloud:config:client:version", out value).Should().BeTrue(); + value.Should().Be("test-version"); + provider.TryGet("spring:cloud:config:client:state", out value).Should().BeTrue(); + value.Should().Be("test-state"); } [Fact] diff --git a/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationProviderTest.cs b/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationProviderTest.cs index c827ede9fa..e0bfb947b5 100644 --- a/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationProviderTest.cs +++ b/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationProviderTest.cs @@ -225,6 +225,40 @@ public async Task GetRequestMessage_AddsVaultToken_IfNeeded() headerValues.Should().Contain("MyVaultToken"); } + [Fact] + public async Task GetRequestMessage_AddsBearerToken_WhenAccessTokenUriIsSet() + { + var options = new ConfigServerClientOptions + { + Uri = "http://localhost:8888/", + Name = "foo", + Environment = "development", + AccessTokenUri = "https://auth.server.com", + ClientId = "test-client-id", + ClientSecret = "test-client-secret" + }; + + using var handler = new DelegateToMockHttpClientHandler(); + + handler.Mock.Expect(HttpMethod.Post, "https://auth.server.com/").WithHeaders("Authorization", "Basic dGVzdC1jbGllbnQtaWQ6dGVzdC1jbGllbnQtc2VjcmV0") + .WithFormData("grant_type=client_credentials").Respond("application/json", """ + { + "access_token": "my-bearer-token" + } + """); + + using var provider = new ConfigServerConfigurationProvider(options, null, handler, NullLoggerFactory.Instance); + + Uri requestUri = provider.BuildConfigServerUri(provider.ClientOptions, new Uri(options.Uri), null); + HttpRequestMessage request = await provider.GetRequestMessageAsync(provider.ClientOptions, requestUri, TestContext.Current.CancellationToken); + + handler.Mock.VerifyNoOutstandingExpectation(); + + request.Headers.Authorization.Should().NotBeNull(); + request.Headers.Authorization.Scheme.Should().Be("Bearer"); + request.Headers.Authorization.Parameter.Should().Be("my-bearer-token"); + } + [Fact] public async Task RefreshVaultToken_Succeeds() { From 9737b70c5e05c4b0ab875cf3d535140bcd9ed72f Mon Sep 17 00:00:00 2001 From: Bart Koelman <104792814+bart-vmware@users.noreply.github.com> Date: Sat, 28 Mar 2026 00:35:49 +0100 Subject: [PATCH 06/29] Fix references to Config Server in comments --- .../src/ConfigServer/ConfigServerConfigurationProvider.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Configuration/src/ConfigServer/ConfigServerConfigurationProvider.cs b/src/Configuration/src/ConfigServer/ConfigServerConfigurationProvider.cs index 828bee5f5d..7a8beddac1 100644 --- a/src/Configuration/src/ConfigServer/ConfigServerConfigurationProvider.cs +++ b/src/Configuration/src/ConfigServer/ConfigServerConfigurationProvider.cs @@ -202,7 +202,7 @@ private void RefreshTimerTick() } /// - /// Loads configuration data from the Spring Cloud Configuration Server as specified by the . + /// Loads configuration data from the Spring Cloud Config Server as specified by the . /// public override void Load() { @@ -481,7 +481,7 @@ internal async Task ShutdownAsync(CancellationToken cancellationToken) } /// - /// Creates the that will be used in accessing the Spring Cloud Configuration server. + /// Creates the that will be used in accessing the Spring Cloud Config server. /// /// /// A snapshot of the client options to use for this request. @@ -781,7 +781,7 @@ private async Task GetVaultRenewRequestMessageAsync(ConfigSe } /// - /// Creates an appropriately configured HttpClient that can be used in communicating with the Spring Cloud Configuration Server. + /// Creates an appropriately configured HttpClient that can be used in communicating with the Spring Cloud Config Server. /// /// /// The settings used to configure the HttpClient. From b0d32f8d5e63b87f3ef6c5ca3fb6177cea24b476 Mon Sep 17 00:00:00 2001 From: Bart Koelman <104792814+bart-vmware@users.noreply.github.com> Date: Sat, 28 Mar 2026 04:27:15 +0100 Subject: [PATCH 07/29] Fix threading bugs and resource leaks in Config Server provider The provider had several concurrency and lifecycle issues: - Vault token renewal timers were created unboundedly on every HTTP request that carried a token, leaking timers that were never disposed. Vault renewal is now managed as a single timer with the same lifecycle as the polling timer. - Timer management, handler configuration, and disposal could race with each other without synchronization. A lifecycle lock now guards these operations, while non-blocking try-enter locks prevent timer callbacks from queueing up. - The HTTP client handler's certificates were reconfigured on every HTTP request, racing with concurrent requests sharing the same handler. Certificate and validation configuration is now applied once per settings change under the lifecycle lock, with certificates cleared before re-adding to prevent unbounded accumulation across reloads. - Options cloning did not copy the certificate issuer chain, losing intermediate CA certificates across reloads. - Disposal is now coordinated via a volatile flag so that in-flight timer callbacks and Load() calls exit gracefully instead of throwing unobserved exceptions. --- .../src/Certificates/CertificateOptions.cs | 16 +- .../ConfigServer/ConfigServerClientOptions.cs | 9 +- .../ConfigServerConfigurationProvider.cs | 297 +++++++++++++----- .../ConfigureConfigServerClientOptions.cs | 2 +- .../ConfigServerClientOptionsTest.cs | 12 +- ...ServerConfigurationProviderTest.Loading.cs | 89 +++++- .../ConfigServerHostBuilderExtensionsTest.cs | 16 +- 7 files changed, 329 insertions(+), 112 deletions(-) diff --git a/src/Common/src/Certificates/CertificateOptions.cs b/src/Common/src/Certificates/CertificateOptions.cs index 3b062e0f38..9482b2d792 100644 --- a/src/Common/src/Certificates/CertificateOptions.cs +++ b/src/Common/src/Certificates/CertificateOptions.cs @@ -14,6 +14,20 @@ public sealed class CertificateOptions internal const string ConfigurationKeyPrefix = "Certificates"; public X509Certificate2? Certificate { get; set; } - public IList IssuerChain { get; } = []; + + internal CertificateOptions Clone() + { + var clone = new CertificateOptions + { + Certificate = Certificate + }; + + foreach (X509Certificate2 issuer in IssuerChain) + { + clone.IssuerChain.Add(issuer); + } + + return clone; + } } diff --git a/src/Configuration/src/ConfigServer/ConfigServerClientOptions.cs b/src/Configuration/src/ConfigServer/ConfigServerClientOptions.cs index b7c3c76259..7a7565d999 100644 --- a/src/Configuration/src/ConfigServer/ConfigServerClientOptions.cs +++ b/src/Configuration/src/ConfigServer/ConfigServerClientOptions.cs @@ -21,9 +21,9 @@ public sealed class ConfigServerClientOptions : IValidateCertificatesOptions internal bool IsMultiServerConfiguration => Uri != null && Uri.Contains(CommaDelimiter); /// - /// Gets the client certificate used for mutual TLS authentication with the Config Server. + /// Gets or sets the client certificate used for mutual TLS authentication with the Config Server. /// - internal CertificateOptions ClientCertificate { get; private set; } = new(); + internal CertificateOptions ClientCertificate { get; set; } = new(); /// /// Gets or sets a value indicating whether the Config Server provider is enabled. Default value: true. @@ -151,10 +151,7 @@ internal ConfigServerClientOptions Clone() { return new ConfigServerClientOptions { - ClientCertificate = new CertificateOptions - { - Certificate = ClientCertificate.Certificate - }, + ClientCertificate = ClientCertificate.Clone(), Enabled = Enabled, FailFast = FailFast, Environment = Environment, diff --git a/src/Configuration/src/ConfigServer/ConfigServerConfigurationProvider.cs b/src/Configuration/src/ConfigServer/ConfigServerConfigurationProvider.cs index 7a8beddac1..dba55d6893 100644 --- a/src/Configuration/src/ConfigServer/ConfigServerConfigurationProvider.cs +++ b/src/Configuration/src/ConfigServer/ConfigServerConfigurationProvider.cs @@ -19,6 +19,13 @@ using Steeltoe.Common.Extensions; using Steeltoe.Common.Http; using Steeltoe.Common.Http.HttpClientPooling; +using LockPrimitive = +#if NET10_0_OR_GREATER + System.Threading.Lock +#else + object +#endif + ; namespace Steeltoe.Configuration.ConfigServer; @@ -40,13 +47,17 @@ internal sealed partial class ConfigServerConfigurationProvider : ConfigurationP private readonly bool _ownsHttpClientHandler; private readonly ConfigureConfigServerClientOptions _configurer; private readonly ConfigServerClientOptions _initialOptions; + private readonly LockPrimitive _lifecycleLock = new(); + private readonly LockPrimitive _configurationReloadTickLock = new(); + private readonly LockPrimitive _vaultRenewTickLock = new(); private HttpClientHandler? _httpClientHandler; private ConfigServerDiscoveryService? _configServerDiscoveryService; - private Timer? _refreshTimer; - private SemaphoreSlim? _timerTickLock = new(1, 1); + private Timer? _configurationReloadTimer; + private Timer? _vaultRenewTimer; private volatile DiscoveryLookupResult? _lastDiscoveryLookupResult; private volatile ConfigServerClientOptions _clientOptions; + private volatile bool _isDisposed; internal static JsonSerializerOptions SerializerOptions { get; } = new() { @@ -99,8 +110,8 @@ internal ConfigServerConfigurationProvider(ConfigServerClientOptions clientOptio _configurer = new ConfigureConfigServerClientOptions(_configuration); - _initialOptions = clientOptions; - _clientOptions = clientOptions; + _initialOptions = clientOptions.Clone(); + _clientOptions = _initialOptions; if (httpClientHandler == null) { @@ -125,91 +136,185 @@ private void OnSettingsChanged() _configuration.GetReloadToken().RegisterChangeCallback(_ => OnSettingsChanged(), null); } - TimeSpan previousPollingInterval = _clientOptions.PollingInterval; - _clientOptions = newOptions; + lock (_lifecycleLock) + { + if (_isDisposed) + { + // Prevent creating new timers after Dispose has already torn them down. + return; + } + + TimeSpan previousPollingInterval = _clientOptions.PollingInterval; + int previousTokenRenewRate = _clientOptions.TokenRenewRate; + + ConfigureHttpClientHandler(newOptions); + UpdateConfigurationReloadTimer(newOptions, previousPollingInterval); + UpdateVaultRenewTimer(newOptions, previousTokenRenewRate); + + _clientOptions = newOptions; + } + } - if (newOptions.PollingInterval == TimeSpan.Zero || !newOptions.Enabled) + private void UpdateConfigurationReloadTimer(ConfigServerClientOptions optionsSnapshot, TimeSpan previousPollingInterval) + { + if (optionsSnapshot.PollingInterval == TimeSpan.Zero || !optionsSnapshot.Enabled) { - _refreshTimer?.Dispose(); - _refreshTimer = null; + _configurationReloadTimer?.Dispose(); + _configurationReloadTimer = null; } - else + else if (_configurationReloadTimer == null) { - if (_refreshTimer == null) - { - _refreshTimer = new Timer(_ => RefreshTimerTick(), null, TimeSpan.Zero, newOptions.PollingInterval); - } - else if (previousPollingInterval != newOptions.PollingInterval) - { - _refreshTimer.Change(TimeSpan.Zero, newOptions.PollingInterval); - } + _configurationReloadTimer = new Timer(_ => ConfigurationReloadTimerTick(), null, TimeSpan.Zero, optionsSnapshot.PollingInterval); + } + else if (previousPollingInterval != optionsSnapshot.PollingInterval) + { + _configurationReloadTimer.Change(TimeSpan.Zero, optionsSnapshot.PollingInterval); + } + } + + private void UpdateVaultRenewTimer(ConfigServerClientOptions optionsSnapshot, int previousTokenRenewRate) + { + if (string.IsNullOrEmpty(optionsSnapshot.Token) || optionsSnapshot.DisableTokenRenewal || + optionsSnapshot is not { Uri: not null, IsMultiServerConfiguration: false }) + { + _vaultRenewTimer?.Dispose(); + _vaultRenewTimer = null; + } + else if (_vaultRenewTimer == null) + { + _vaultRenewTimer = new Timer(_ => VaultRenewTimerTick(), null, TimeSpan.FromMilliseconds(optionsSnapshot.TokenRenewRate), + TimeSpan.FromMilliseconds(optionsSnapshot.TokenRenewRate)); + } + else if (previousTokenRenewRate != optionsSnapshot.TokenRenewRate) + { + _vaultRenewTimer.Change(TimeSpan.FromMilliseconds(optionsSnapshot.TokenRenewRate), TimeSpan.FromMilliseconds(optionsSnapshot.TokenRenewRate)); } } /// - /// RefreshTimerTick is called by a Timer callback, so must catch all exceptions. + /// ConfigurationReloadTimerTick is called by a Timer callback, so must catch all exceptions. /// - private void RefreshTimerTick() + private void ConfigurationReloadTimerTick() { - LogEnteringTimerCycle(); - bool lockTaken = false; + LogEnteringConfigurationReloadCycle(); + +#if NET10_0_OR_GREATER + bool lockTaken = _configurationReloadTickLock.TryEnter(); +#else + bool lockTaken = Monitor.TryEnter(_configurationReloadTickLock); +#endif try { - lockTaken = _timerTickLock != null && _timerTickLock.Wait(0); + if (!lockTaken || _isDisposed) + { + LogSkippingConfigurationReloadCycle(); + return; + } + + LogConfigurationReloadLockObtained(); + +#pragma warning disable S4462 // Calls to "async" methods should not be blocking + // Justification: Configuration sources and providers don't support async. + DoLoadAsync(ClientOptions, true, CancellationToken.None).GetAwaiter().GetResult(); +#pragma warning restore S4462 // Calls to "async" methods should not be blocking + + LogConfigurationReloadCycleCompleted(); + } + catch (Exception exception) + { + if (exception is ObjectDisposedException && _isDisposed) + { + // Disposal can race with the timer callback because _isDisposed is read without locking. + } + else + { + LogFailedToReloadConfiguration(exception); + } } - catch (ObjectDisposedException) + finally { - // Ignore exception originating from potential race condition. + if (lockTaken) + { +#if NET10_0_OR_GREATER + _configurationReloadTickLock.Exit(); +#else + Monitor.Exit(_configurationReloadTickLock); +#endif + } } + } + + /// + /// VaultRenewTimerTick is called by a Timer callback, so must catch all exceptions. + /// + private void VaultRenewTimerTick() + { + LogEnteringVaultRenewCycle(); + +#if NET10_0_OR_GREATER + bool lockTaken = _vaultRenewTickLock.TryEnter(); +#else + bool lockTaken = Monitor.TryEnter(_vaultRenewTickLock); +#endif try { - if (lockTaken) + if (!lockTaken || _isDisposed) { - LogExclusiveLockObtained(); + LogSkippingVaultRenewCycle(); + return; + } + + LogVaultRenewLockObtained(); #pragma warning disable S4462 // Calls to "async" methods should not be blocking - // Justification: Configuration sources and providers don't support async. - DoLoadAsync(ClientOptions, true, CancellationToken.None).GetAwaiter().GetResult(); + // Justification: Configuration sources and providers don't support async. + RefreshVaultTokenAsync(ClientOptions, CancellationToken.None).GetAwaiter().GetResult(); #pragma warning restore S4462 // Calls to "async" methods should not be blocking + + LogVaultRenewCycleCompleted(); + } + catch (Exception exception) + { + if (exception is ObjectDisposedException && _isDisposed) + { + // Disposal can race with the timer callback because _isDisposed is read without locking. } else { - LogSkippingCycle(); + LogFailedToRenewVaultToken(exception); } } - catch (Exception exception) - { - LogCouldNotReloadDuringPolling(exception); - } finally { if (lockTaken) { - LogTimerCycleCompleted(); - - try - { - _timerTickLock?.Release(); - } - catch (ObjectDisposedException) - { - // Ignore exception originating from potential race condition. - } +#if NET10_0_OR_GREATER + _vaultRenewTickLock.Exit(); +#else + Monitor.Exit(_vaultRenewTickLock); +#endif } } } /// - /// Loads configuration data from the Spring Cloud Config Server as specified by the . + /// Loads configuration data from the Spring Cloud Config Server as specified by the . /// public override void Load() { + try + { #pragma warning disable S4462 // Calls to "async" methods should not be blocking - // Justification: Configuration sources and providers don't support async. - LoadInternalAsync(true, CancellationToken.None).GetAwaiter().GetResult(); + // Justification: Configuration sources and providers don't support async. + LoadInternalAsync(true, CancellationToken.None).GetAwaiter().GetResult(); #pragma warning restore S4462 // Calls to "async" methods should not be blocking + } + catch (ObjectDisposedException) when (_isDisposed) + { + // Expected during disposal; silently ignore. + } } internal async Task LoadInternalAsync(bool updateDictionary, CancellationToken cancellationToken) @@ -525,11 +630,6 @@ internal async Task GetRequestMessageAsync(ConfigServerClien if (!string.IsNullOrEmpty(optionsSnapshot.Token) && optionsSnapshot is { Uri: not null, IsMultiServerConfiguration: false }) { - if (!optionsSnapshot.DisableTokenRenewal) - { - RenewToken(optionsSnapshot); - } - requestMessage.Headers.Add(TokenHeader, optionsSnapshot.Token); } @@ -698,16 +798,10 @@ private void AddPropertySource(PropertySource? source, Dictionary RefreshVaultTokenAsync(optionsSnapshot, CancellationToken.None).GetAwaiter().GetResult(), null, - TimeSpan.FromMilliseconds(optionsSnapshot.TokenRenewRate), TimeSpan.FromMilliseconds(optionsSnapshot.TokenRenewRate)); -#pragma warning restore S4462 // Calls to "async" methods should not be blocking - } - - // fire and forget + /// + /// Extends the lease of the current HashiCorp Vault token; it does not generate a new token. A new token is only picked up when the configuration + /// changes and reconfigures the timer. + /// internal async Task RefreshVaultTokenAsync(ConfigServerClientOptions optionsSnapshot, CancellationToken cancellationToken) { if (string.IsNullOrEmpty(optionsSnapshot.Token)) @@ -794,14 +888,6 @@ internal HttpClient CreateHttpClient(ConfigServerClientOptions clientOptions) ArgumentNullException.ThrowIfNull(clientOptions); ObjectDisposedException.ThrowIf(_httpClientHandler == null, this); - var clientCertificateConfigurer = new ClientCertificateHttpClientHandlerConfigurer(OptionsMonitorWrapper.Create(clientOptions.ClientCertificate)); - clientCertificateConfigurer.Configure("ConfigServer", _httpClientHandler); - - var validateCertificatesHandler = - new ValidateCertificatesHttpClientHandlerConfigurer(OptionsMonitorWrapper.Create(clientOptions)); - - validateCertificatesHandler.Configure(Options.DefaultName, _httpClientHandler); - var httpClient = new HttpClient(_httpClientHandler, false); httpClient.ConfigureForSteeltoe(clientOptions.HttpTimeout); @@ -813,6 +899,24 @@ internal HttpClient CreateHttpClient(ConfigServerClientOptions clientOptions) return httpClient; } + private void ConfigureHttpClientHandler(ConfigServerClientOptions optionsSnapshot) + { + if (_httpClientHandler == null) + { + return; + } + + _httpClientHandler.ClientCertificates.Clear(); + + var clientCertificateConfigurer = new ClientCertificateHttpClientHandlerConfigurer(OptionsMonitorWrapper.Create(optionsSnapshot.ClientCertificate)); + clientCertificateConfigurer.Configure("ConfigServer", _httpClientHandler); + + var validateCertificatesHandler = + new ValidateCertificatesHttpClientHandlerConfigurer(OptionsMonitorWrapper.Create(optionsSnapshot)); + + validateCertificatesHandler.Configure(Options.DefaultName, _httpClientHandler); + } + private static bool IsSocketError(Exception exception) { return exception is HttpRequestException && exception.InnerException is SocketException; @@ -820,11 +924,21 @@ private static bool IsSocketError(Exception exception) public void Dispose() { - _refreshTimer?.Dispose(); - _refreshTimer = null; + lock (_lifecycleLock) + { + if (_isDisposed) + { + return; + } + + _isDisposed = true; - _timerTickLock?.Dispose(); - _timerTickLock = null; + _configurationReloadTimer?.Dispose(); + _configurationReloadTimer = null; + + _vaultRenewTimer?.Dispose(); + _vaultRenewTimer = null; + } if (_ownsHttpClientHandler) { @@ -834,20 +948,35 @@ public void Dispose() _httpClientHandler = null; } - [LoggerMessage(Level = LogLevel.Trace, Message = "Entering timer cycle.")] - private partial void LogEnteringTimerCycle(); + [LoggerMessage(Level = LogLevel.Trace, Message = "Entering polling configuration reload cycle.")] + private partial void LogEnteringConfigurationReloadCycle(); + + [LoggerMessage(Level = LogLevel.Trace, Message = "Polling configuration reload lock obtained.")] + private partial void LogConfigurationReloadLockObtained(); + + [LoggerMessage(Level = LogLevel.Trace, Message = "Previous polling configuration reload cycle is still running, or already disposed; skipping this cycle.")] + private partial void LogSkippingConfigurationReloadCycle(); + + [LoggerMessage(Level = LogLevel.Warning, Message = "Failed to reload configuration during polling.")] + private partial void LogFailedToReloadConfiguration(Exception exception); + + [LoggerMessage(Level = LogLevel.Trace, Message = "Polling configuration reload cycle completed, releasing lock.")] + private partial void LogConfigurationReloadCycleCompleted(); + + [LoggerMessage(Level = LogLevel.Trace, Message = "Entering Vault token renewal cycle.")] + private partial void LogEnteringVaultRenewCycle(); - [LoggerMessage(Level = LogLevel.Trace, Message = "Exclusive lock obtained.")] - private partial void LogExclusiveLockObtained(); + [LoggerMessage(Level = LogLevel.Trace, Message = "Vault token renewal lock obtained.")] + private partial void LogVaultRenewLockObtained(); - [LoggerMessage(Level = LogLevel.Trace, Message = "Previous cycle is still running, or already disposed; skipping this cycle.")] - private partial void LogSkippingCycle(); + [LoggerMessage(Level = LogLevel.Trace, Message = "Previous Vault token renewal cycle is still running, or already disposed; skipping this cycle.")] + private partial void LogSkippingVaultRenewCycle(); - [LoggerMessage(Level = LogLevel.Warning, Message = "Could not reload configuration during polling.")] - private partial void LogCouldNotReloadDuringPolling(Exception exception); + [LoggerMessage(Level = LogLevel.Warning, Message = "Failed to renew Vault token.")] + private partial void LogFailedToRenewVaultToken(Exception exception); - [LoggerMessage(Level = LogLevel.Trace, Message = "Timer cycle completed, releasing exclusive lock.")] - private partial void LogTimerCycleCompleted(); + [LoggerMessage(Level = LogLevel.Trace, Message = "Vault token renewal cycle completed, releasing lock.")] + private partial void LogVaultRenewCycleCompleted(); [LoggerMessage(Level = LogLevel.Information, Message = "Config Server client disabled, not fetching configuration.")] private partial void LogConfigServerClientDisabled(); diff --git a/src/Configuration/src/ConfigServer/ConfigureConfigServerClientOptions.cs b/src/Configuration/src/ConfigServer/ConfigureConfigServerClientOptions.cs index 3d6cc8ddc9..c8fbe53507 100644 --- a/src/Configuration/src/ConfigServer/ConfigureConfigServerClientOptions.cs +++ b/src/Configuration/src/ConfigServer/ConfigureConfigServerClientOptions.cs @@ -66,7 +66,7 @@ private void ConfigureClientCertificate(ConfigServerClientOptions options) certificateConfigurer.Configure(certificateOptions); } - options.ClientCertificate.Certificate = certificateOptions.Certificate; + options.ClientCertificate = certificateOptions.Clone(); } private string? GetApplicationName() diff --git a/src/Configuration/test/ConfigServer.Test/ConfigServerClientOptionsTest.cs b/src/Configuration/test/ConfigServer.Test/ConfigServerClientOptionsTest.cs index 42950b3537..ba75030896 100644 --- a/src/Configuration/test/ConfigServer.Test/ConfigServerClientOptionsTest.cs +++ b/src/Configuration/test/ConfigServer.Test/ConfigServerClientOptionsTest.cs @@ -119,12 +119,17 @@ public async Task ConfigureConfigServerClientOptions_WithValues() public void Clone_preserves_all_properties_and_produces_independent_nested_objects() { using var certificate = X509Certificate2.CreateFromPemFile("instance.crt", "instance.key"); + using var issuerCertificate = X509Certificate2.CreateFromPemFile("instance.crt", "instance.key"); var original = new ConfigServerClientOptions { ClientCertificate = { - Certificate = certificate + Certificate = certificate, + IssuerChain = + { + issuerCertificate + } }, Enabled = false, FailFast = true, @@ -172,6 +177,11 @@ public void Clone_preserves_all_properties_and_produces_independent_nested_objec clone.ClientCertificate.Should().NotBeSameAs(original.ClientCertificate); clone.ClientCertificate.Certificate.Should().BeSameAs(original.ClientCertificate.Certificate); + clone.ClientCertificate.IssuerChain.Should().NotBeSameAs(original.ClientCertificate.IssuerChain); + clone.ClientCertificate.IssuerChain.Should().ContainSingle().Which.Should().BeSameAs(issuerCertificate); + + original.ClientCertificate.IssuerChain.Clear(); + clone.ClientCertificate.IssuerChain.Should().ContainSingle(); clone.Enabled.Should().Be(original.Enabled); clone.FailFast.Should().Be(original.FailFast); diff --git a/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationProviderTest.Loading.cs b/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationProviderTest.Loading.cs index 7f62a5f779..bd79ca81c8 100644 --- a/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationProviderTest.Loading.cs +++ b/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationProviderTest.Loading.cs @@ -85,7 +85,7 @@ public async Task RemoteLoadAsync_ConfigServerReturnsLessThanBadRequest() } [Fact] - public async Task Create_WithPollingTimer() + public async Task Create_WithConfigurationReloadTimer() { await TestFailureTracer.CaptureAsync(async tracer => { @@ -137,7 +137,7 @@ await TestFailureTracer.CaptureAsync(async tracer => } [Fact] - public async Task Create_FailFastEnabledAndExceptionThrownDuringPolling_DoesNotCrash() + public async Task Create_FailFastEnabledAndExceptionThrownDuringPolledConfigurationReload_DoesNotCrash() { await TestFailureTracer.CaptureAsync(async tracer => { @@ -192,7 +192,7 @@ await TestFailureTracer.CaptureAsync(async tracer => } [Fact] - public async Task Create_WithNonZeroPollingIntervalAndClientDisabled_PollingDisabled() + public async Task Create_WithNonZeroPollingIntervalAndClientDisabled_PollingConfigurationReloadDisabled() { const string environment = """ { @@ -236,7 +236,7 @@ public async Task Create_WithNonZeroPollingIntervalAndClientDisabled_PollingDisa [Theory] [InlineData(false, "00:00:01")] [InlineData(true, "00:00:00")] - public void OnSettingsChanged_stops_timer_when_polling_becomes_ineffective(bool enabled, string pollingInterval) + public void OnSettingsChanged_stops_reload_timer_when_polling_becomes_ineffective(bool enabled, string pollingInterval) { const string configServerResponseJson = """ { @@ -274,9 +274,11 @@ public void OnSettingsChanged_stops_timer_when_polling_becomes_ineffective(bool IConfigurationRoot configuration = configurationBuilder.Build(); ConfigServerConfigurationProvider provider = configuration.Providers.OfType().Single(); - FieldInfo refreshTimerField = typeof(ConfigServerConfigurationProvider).GetField("_refreshTimer", BindingFlags.NonPublic | BindingFlags.Instance)!; - refreshTimerField.GetValue(provider).Should().NotBeNull(); + FieldInfo reloadTimerField = + typeof(ConfigServerConfigurationProvider).GetField("_configurationReloadTimer", BindingFlags.NonPublic | BindingFlags.Instance)!; + + reloadTimerField.GetValue(provider).Should().NotBeNull(); fileProvider.ReplaceAppSettingsJsonFile($$""" { @@ -294,11 +296,11 @@ public void OnSettingsChanged_stops_timer_when_polling_becomes_ineffective(bool fileProvider.NotifyChanged(); - refreshTimerField.GetValue(provider).Should().BeNull(); + reloadTimerField.GetValue(provider).Should().BeNull(); } [Fact] - public void OnSettingsChanged_reschedules_timer_when_polling_interval_changes() + public void OnSettingsChanged_reschedules_reload_timer_when_polling_interval_changes() { const string configServerResponseJson = """ { @@ -336,9 +338,11 @@ public void OnSettingsChanged_reschedules_timer_when_polling_interval_changes() IConfigurationRoot configuration = configurationBuilder.Build(); ConfigServerConfigurationProvider provider = configuration.Providers.OfType().Single(); - FieldInfo refreshTimerField = typeof(ConfigServerConfigurationProvider).GetField("_refreshTimer", BindingFlags.NonPublic | BindingFlags.Instance)!; - refreshTimerField.GetValue(provider).Should().NotBeNull(); + FieldInfo reloadTimerField = + typeof(ConfigServerConfigurationProvider).GetField("_configurationReloadTimer", BindingFlags.NonPublic | BindingFlags.Instance)!; + + reloadTimerField.GetValue(provider).Should().NotBeNull(); fileProvider.ReplaceAppSettingsJsonFile(""" { @@ -356,7 +360,70 @@ public void OnSettingsChanged_reschedules_timer_when_polling_interval_changes() fileProvider.NotifyChanged(); - refreshTimerField.GetValue(provider).Should().NotBeNull("timer should be rescheduled, not stopped"); + reloadTimerField.GetValue(provider).Should().NotBeNull("timer should be rescheduled, not stopped"); + } + + [Fact] + public void OnSettingsChanged_stops_vault_renew_timer_when_renewal_becomes_disabled() + { + const string configServerResponseJson = """ + { + "name": "myName", + "profiles": [ "Production" ], + "label": "test-label", + "version": "test-version", + "propertySources": [] + } + """; + + var fileProvider = new MemoryFileProvider(); + + fileProvider.IncludeAppSettingsJsonFile(""" + { + "spring": { + "cloud": { + "config": { + "name": "myName", + "token": "MyVaultToken", + "pollingInterval": "00:00:00" + } + } + } + } + """); + + using var handler = new DelegateToMockHttpClientHandler(); + + handler.Mock.When(HttpMethod.Get, "http://localhost:8888/myName/Production").Respond("application/json", configServerResponseJson); + + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryAppSettingsJsonFile(fileProvider); + configurationBuilder.AddConfigServer(new ConfigServerClientOptions(), handler, NullLoggerFactory.Instance); + IConfigurationRoot configuration = configurationBuilder.Build(); + + ConfigServerConfigurationProvider provider = configuration.Providers.OfType().Single(); + FieldInfo vaultTimerField = typeof(ConfigServerConfigurationProvider).GetField("_vaultRenewTimer", BindingFlags.NonPublic | BindingFlags.Instance)!; + + vaultTimerField.GetValue(provider).Should().NotBeNull(); + + fileProvider.ReplaceAppSettingsJsonFile(""" + { + "spring": { + "cloud": { + "config": { + "name": "myName", + "token": "MyVaultToken", + "pollingInterval": "00:00:00", + "disableTokenRenewal": true + } + } + } + } + """); + + fileProvider.NotifyChanged(); + + vaultTimerField.GetValue(provider).Should().BeNull(); } [Fact] diff --git a/src/Configuration/test/ConfigServer.Test/ConfigServerHostBuilderExtensionsTest.cs b/src/Configuration/test/ConfigServer.Test/ConfigServerHostBuilderExtensionsTest.cs index 776dcc8631..737c8a4563 100644 --- a/src/Configuration/test/ConfigServer.Test/ConfigServerHostBuilderExtensionsTest.cs +++ b/src/Configuration/test/ConfigServer.Test/ConfigServerHostBuilderExtensionsTest.cs @@ -102,8 +102,8 @@ public void AddConfigServer_HostBuilder_DisposesTimer() provider = configurationRoot.EnumerateProviders().Single(); } - FieldInfo refreshTimerField = provider.GetType().GetField("_refreshTimer", BindingFlags.NonPublic | BindingFlags.Instance)!; - refreshTimerField.GetValue(provider).Should().BeNull(); + FieldInfo reloadTimerField = provider.GetType().GetField("_configurationReloadTimer", BindingFlags.NonPublic | BindingFlags.Instance)!; + reloadTimerField.GetValue(provider).Should().BeNull(); } [Fact] @@ -126,8 +126,8 @@ public void AddConfigServer_WebHostBuilder_DisposesTimer() provider = configurationRoot.EnumerateProviders().Single(); } - FieldInfo refreshTimerField = provider.GetType().GetField("_refreshTimer", BindingFlags.NonPublic | BindingFlags.Instance)!; - refreshTimerField.GetValue(provider).Should().BeNull(); + FieldInfo reloadTimerField = provider.GetType().GetField("_configurationReloadTimer", BindingFlags.NonPublic | BindingFlags.Instance)!; + reloadTimerField.GetValue(provider).Should().BeNull(); } [Fact] @@ -150,8 +150,8 @@ public async Task AddConfigServer_WebApplicationBuilder_DisposesTimer() _ = host.Services.GetRequiredService(); } - FieldInfo refreshTimerField = provider.GetType().GetField("_refreshTimer", BindingFlags.NonPublic | BindingFlags.Instance)!; - refreshTimerField.GetValue(provider).Should().BeNull(); + FieldInfo reloadTimerField = provider.GetType().GetField("_configurationReloadTimer", BindingFlags.NonPublic | BindingFlags.Instance)!; + reloadTimerField.GetValue(provider).Should().BeNull(); } [Fact] @@ -174,8 +174,8 @@ public void AddConfigServer_HostApplicationBuilder_DisposesTimer() _ = host.Services.GetRequiredService(); } - FieldInfo refreshTimerField = provider.GetType().GetField("_refreshTimer", BindingFlags.NonPublic | BindingFlags.Instance)!; - refreshTimerField.GetValue(provider).Should().BeNull(); + FieldInfo reloadTimerField = provider.GetType().GetField("_configurationReloadTimer", BindingFlags.NonPublic | BindingFlags.Instance)!; + reloadTimerField.GetValue(provider).Should().BeNull(); } [Fact] From e66e1d77dc0c3e9f0cc809dba45c24e2ce8bb86f Mon Sep 17 00:00:00 2001 From: Bart Koelman <104792814+bart-vmware@users.noreply.github.com> Date: Sat, 28 Mar 2026 12:07:00 +0100 Subject: [PATCH 08/29] Fixed: use latest options snapshot during discovery in Config Server --- .../ConfigServerConfigurationProvider.cs | 10 +++---- .../ConfigServerDiscoveryService.cs | 28 ++++++++++--------- .../ConfigServerDiscoveryServiceTest.cs | 17 ++++++----- .../ConfigServerDiscoveryServiceTest.cs | 3 +- 4 files changed, 29 insertions(+), 29 deletions(-) diff --git a/src/Configuration/src/ConfigServer/ConfigServerConfigurationProvider.cs b/src/Configuration/src/ConfigServer/ConfigServerConfigurationProvider.cs index dba55d6893..ed156e5d2d 100644 --- a/src/Configuration/src/ConfigServer/ConfigServerConfigurationProvider.cs +++ b/src/Configuration/src/ConfigServer/ConfigServerConfigurationProvider.cs @@ -333,8 +333,8 @@ public override void Load() } else { - _configServerDiscoveryService ??= new ConfigServerDiscoveryService(_configuration, optionsSnapshot, _loggerFactory); - await DiscoverServerInstancesAsync(_configServerDiscoveryService, optionsSnapshot.FailFast, cancellationToken); + _configServerDiscoveryService ??= new ConfigServerDiscoveryService(_configuration, _loggerFactory); + await DiscoverServerInstancesAsync(_configServerDiscoveryService, optionsSnapshot, cancellationToken); } if (optionsSnapshot is { Retry.Enabled: true, FailFast: true }) @@ -509,14 +509,14 @@ internal string[] GetLabels(ConfigServerClientOptions optionsSnapshot) return optionsSnapshot.Label.Split(CommaDelimiter, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); } - private async Task DiscoverServerInstancesAsync(ConfigServerDiscoveryService configServerDiscoveryService, bool failFast, + private async Task DiscoverServerInstancesAsync(ConfigServerDiscoveryService configServerDiscoveryService, ConfigServerClientOptions optionsSnapshot, CancellationToken cancellationToken) { - List instances = await configServerDiscoveryService.GetConfigServerInstancesAsync(cancellationToken); + List instances = await configServerDiscoveryService.GetConfigServerInstancesAsync(optionsSnapshot, cancellationToken); if (instances.Count == 0) { - if (failFast) + if (optionsSnapshot.FailFast) { throw new ConfigServerException("Could not locate Config Server via discovery, are you missing a Discovery service assembly?"); } diff --git a/src/Configuration/src/ConfigServer/ConfigServerDiscoveryService.cs b/src/Configuration/src/ConfigServer/ConfigServerDiscoveryService.cs index 57cc575711..bdae501f58 100644 --- a/src/Configuration/src/ConfigServer/ConfigServerDiscoveryService.cs +++ b/src/Configuration/src/ConfigServer/ConfigServerDiscoveryService.cs @@ -19,20 +19,17 @@ internal sealed partial class ConfigServerDiscoveryService { private static readonly AssemblyLoader AssemblyLoader = new(); private readonly IConfiguration _configuration; - private readonly ConfigServerClientOptions _options; private readonly ILogger _logger; private ServiceProvider? _temporaryServiceProviderForDiscoveryClients; internal ICollection DiscoveryClients { get; private set; } - public ConfigServerDiscoveryService(IConfiguration configuration, ConfigServerClientOptions options, ILoggerFactory loggerFactory) + public ConfigServerDiscoveryService(IConfiguration configuration, ILoggerFactory loggerFactory) { ArgumentNullException.ThrowIfNull(configuration); - ArgumentNullException.ThrowIfNull(options); ArgumentNullException.ThrowIfNull(loggerFactory); _configuration = configuration; - _options = options; _logger = loggerFactory.CreateLogger(); DiscoveryClients = SetupDiscoveryClients(loggerFactory); } @@ -105,23 +102,28 @@ private IDiscoveryClient[] GetDiscoveryClientsFromServiceCollection(ServiceColle return discoveryClients; } - internal async Task> GetConfigServerInstancesAsync(CancellationToken cancellationToken) + internal async Task> GetConfigServerInstancesAsync(ConfigServerClientOptions optionsSnapshot, + CancellationToken cancellationToken) { + ArgumentNullException.ThrowIfNull(optionsSnapshot); + int attempts = 0; - int backOff = _options.Retry.InitialInterval; + int backOff = optionsSnapshot.Retry.InitialInterval; List instances = []; do { - LogLocatingConfigServer(_options.Discovery.ServiceId); + LogLocatingConfigServer(optionsSnapshot.Discovery.ServiceId); - if (_options.Discovery.ServiceId != null) + if (optionsSnapshot.Discovery.ServiceId != null) { foreach (IDiscoveryClient discoveryClient in DiscoveryClients) { try { - IList serviceInstances = await discoveryClient.GetInstancesAsync(_options.Discovery.ServiceId, cancellationToken); + IList serviceInstances = + await discoveryClient.GetInstancesAsync(optionsSnapshot.Discovery.ServiceId, cancellationToken); + instances.AddRange(serviceInstances); } catch (Exception exception) when (!exception.IsCancellation()) @@ -131,18 +133,18 @@ internal async Task> GetConfigServerInstancesAsync(Cancel } } - if (!_options.Retry.Enabled || instances.Count > 0) + if (!optionsSnapshot.Retry.Enabled || instances.Count > 0) { break; } attempts++; - if (attempts <= _options.Retry.MaxAttempts) + if (attempts <= optionsSnapshot.Retry.MaxAttempts) { Thread.CurrentThread.Join(backOff); - int nextBackOff = (int)(backOff * _options.Retry.Multiplier); - backOff = Math.Min(nextBackOff, _options.Retry.MaxInterval); + int nextBackOff = (int)(backOff * optionsSnapshot.Retry.Multiplier); + backOff = Math.Min(nextBackOff, optionsSnapshot.Retry.MaxInterval); } else { diff --git a/src/Configuration/test/ConfigServer.Discovery.Test/ConfigServerDiscoveryServiceTest.cs b/src/Configuration/test/ConfigServer.Discovery.Test/ConfigServerDiscoveryServiceTest.cs index 73770ae0a3..c762b5c3e1 100644 --- a/src/Configuration/test/ConfigServer.Discovery.Test/ConfigServerDiscoveryServiceTest.cs +++ b/src/Configuration/test/ConfigServer.Discovery.Test/ConfigServerDiscoveryServiceTest.cs @@ -18,9 +18,8 @@ public sealed class ConfigServerDiscoveryServiceTest public void ConfigServerDiscoveryService_FindsDiscoveryClients() { IConfiguration configuration = new ConfigurationBuilder().Add(FastTestConfigurations.ConfigServer | FastTestConfigurations.Discovery).Build(); - var options = new ConfigServerClientOptions(); - var service = new ConfigServerDiscoveryService(configuration, options, NullLoggerFactory.Instance); + var service = new ConfigServerDiscoveryService(configuration, NullLoggerFactory.Instance); service.DiscoveryClients.Should().HaveCount(3); service.DiscoveryClients.OfType().Should().ContainSingle(); @@ -41,8 +40,8 @@ public async Task InvokeGetInstances_ReturnsExpected() IConfigurationRoot configurationRoot = builder.Build(); var options = new ConfigServerClientOptions(); - var service = new ConfigServerDiscoveryService(configurationRoot, options, NullLoggerFactory.Instance); - IEnumerable result = await service.GetConfigServerInstancesAsync(TestContext.Current.CancellationToken); + var service = new ConfigServerDiscoveryService(configurationRoot, NullLoggerFactory.Instance); + IEnumerable result = await service.GetConfigServerInstancesAsync(options, TestContext.Current.CancellationToken); result.Should().BeEmpty(); } @@ -66,8 +65,8 @@ public async Task InvokeGetInstances_RetryEnabled_ReturnsExpected() Timeout = 10 }; - var service = new ConfigServerDiscoveryService(configurationRoot, options, NullLoggerFactory.Instance); - IEnumerable result = await service.GetConfigServerInstancesAsync(TestContext.Current.CancellationToken); + var service = new ConfigServerDiscoveryService(configurationRoot, NullLoggerFactory.Instance); + IEnumerable result = await service.GetConfigServerInstancesAsync(options, TestContext.Current.CancellationToken); result.Should().BeEmpty(); } @@ -92,8 +91,8 @@ public async Task GetConfigServerInstances_ReturnsExpected() Timeout = 10 }; - var service = new ConfigServerDiscoveryService(configurationRoot, options, NullLoggerFactory.Instance); - IEnumerable result = await service.GetConfigServerInstancesAsync(TestContext.Current.CancellationToken); + var service = new ConfigServerDiscoveryService(configurationRoot, NullLoggerFactory.Instance); + IEnumerable result = await service.GetConfigServerInstancesAsync(options, TestContext.Current.CancellationToken); result.Should().BeEmpty(); } @@ -103,7 +102,7 @@ public async Task RuntimeReplacementsCanBeProvided() IConfigurationRoot configurationRoot = new ConfigurationBuilder().Add(FastTestConfigurations.ConfigServer | FastTestConfigurations.Discovery).Build(); var testDiscoveryClient = new TestDiscoveryClient(); - var service = new ConfigServerDiscoveryService(configurationRoot, new ConfigServerClientOptions(), NullLoggerFactory.Instance); + var service = new ConfigServerDiscoveryService(configurationRoot, NullLoggerFactory.Instance); await service.ProvideRuntimeReplacementsAsync([testDiscoveryClient], TestContext.Current.CancellationToken); diff --git a/src/Configuration/test/ConfigServer.Test/ConfigServerDiscoveryServiceTest.cs b/src/Configuration/test/ConfigServer.Test/ConfigServerDiscoveryServiceTest.cs index 55d238660e..bc47ba2a81 100644 --- a/src/Configuration/test/ConfigServer.Test/ConfigServerDiscoveryServiceTest.cs +++ b/src/Configuration/test/ConfigServer.Test/ConfigServerDiscoveryServiceTest.cs @@ -14,9 +14,8 @@ public sealed class ConfigServerDiscoveryServiceTest public void ConfigServerDiscoveryService_FindsNoDiscoveryClients() { IConfiguration configuration = new ConfigurationBuilder().Add(FastTestConfigurations.ConfigServer).Build(); - var options = new ConfigServerClientOptions(); - var service = new ConfigServerDiscoveryService(configuration, options, NullLoggerFactory.Instance); + var service = new ConfigServerDiscoveryService(configuration, NullLoggerFactory.Instance); service.DiscoveryClients.Should().BeEmpty(); } From bdd0111f6345c252c385f96414276b1f07dc7a0f Mon Sep 17 00:00:00 2001 From: Bart Koelman <104792814+bart-vmware@users.noreply.github.com> Date: Sat, 28 Mar 2026 12:13:31 +0100 Subject: [PATCH 09/29] Fixed: preserve original stack trace when load throws --- .../src/ConfigServer/ConfigServerConfigurationProvider.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Configuration/src/ConfigServer/ConfigServerConfigurationProvider.cs b/src/Configuration/src/ConfigServer/ConfigServerConfigurationProvider.cs index ed156e5d2d..2862f5efa1 100644 --- a/src/Configuration/src/ConfigServer/ConfigServerConfigurationProvider.cs +++ b/src/Configuration/src/ConfigServer/ConfigServerConfigurationProvider.cs @@ -7,6 +7,7 @@ using System.Net.Http.Headers; using System.Net.Http.Json; using System.Net.Sockets; +using System.Runtime.ExceptionServices; using System.Text; using System.Text.Json; using System.Text.Json.Serialization; @@ -700,7 +701,7 @@ internal async Task GetRequestMessageAsync(ConfigServerClien if (error != null) { - throw error; + ExceptionDispatchInfo.Capture(error).Throw(); } return null; From fcf31f78777c347dadd3fdac56954d285563ddd7 Mon Sep 17 00:00:00 2001 From: Bart Koelman <104792814+bart-vmware@users.noreply.github.com> Date: Sat, 28 Mar 2026 12:39:41 +0100 Subject: [PATCH 10/29] Fix Config Server health contributor to act on a consistent snapshot --- .../ConfigServerConfigurationProvider.cs | 7 +- .../ConfigServerHealthContributor.cs | 27 +++---- ...ServerConfigurationProviderTest.Loading.cs | 22 +++--- .../ConfigServerConfigurationProviderTest.cs | 2 +- .../ConfigServerHealthContributorTest.cs | 75 ++++++------------- 5 files changed, 48 insertions(+), 85 deletions(-) diff --git a/src/Configuration/src/ConfigServer/ConfigServerConfigurationProvider.cs b/src/Configuration/src/ConfigServer/ConfigServerConfigurationProvider.cs index 2862f5efa1..8a90076b06 100644 --- a/src/Configuration/src/ConfigServer/ConfigServerConfigurationProvider.cs +++ b/src/Configuration/src/ConfigServer/ConfigServerConfigurationProvider.cs @@ -309,7 +309,7 @@ public override void Load() { #pragma warning disable S4462 // Calls to "async" methods should not be blocking // Justification: Configuration sources and providers don't support async. - LoadInternalAsync(true, CancellationToken.None).GetAwaiter().GetResult(); + LoadInternalAsync(ClientOptions, true, CancellationToken.None).GetAwaiter().GetResult(); #pragma warning restore S4462 // Calls to "async" methods should not be blocking } catch (ObjectDisposedException) when (_isDisposed) @@ -318,10 +318,9 @@ public override void Load() } } - internal async Task LoadInternalAsync(bool updateDictionary, CancellationToken cancellationToken) + internal async Task LoadInternalAsync(ConfigServerClientOptions optionsSnapshot, bool updateDictionary, + CancellationToken cancellationToken) { - ConfigServerClientOptions optionsSnapshot = ClientOptions; - if (!optionsSnapshot.Enabled) { LogConfigServerClientDisabled(); diff --git a/src/Configuration/src/ConfigServer/ConfigServerHealthContributor.cs b/src/Configuration/src/ConfigServer/ConfigServerHealthContributor.cs index 34b6b2f0c2..b6b9c821ab 100644 --- a/src/Configuration/src/ConfigServer/ConfigServerHealthContributor.cs +++ b/src/Configuration/src/ConfigServer/ConfigServerHealthContributor.cs @@ -46,12 +46,14 @@ public ConfigServerHealthContributor(IConfiguration configuration, TimeProvider return health; } - if (!IsEnabled()) + ConfigServerClientOptions optionsSnapshot = Provider.ClientOptions; + + if (!optionsSnapshot.Health.Enabled) { return null; } - IList? sources = await GetPropertySourcesAsync(Provider, cancellationToken); + IList? sources = await GetPropertySourcesAsync(Provider, optionsSnapshot, cancellationToken); if (sources == null || sources.Count == 0) { @@ -81,38 +83,29 @@ internal void UpdateHealth(HealthCheckResult health, IList sourc health.Details.Add("propertySources", names); } - internal async Task?> GetPropertySourcesAsync(ConfigServerConfigurationProvider provider, CancellationToken cancellationToken) + internal async Task?> GetPropertySourcesAsync(ConfigServerConfigurationProvider provider, ConfigServerClientOptions optionsSnapshot, + CancellationToken cancellationToken) { long currentTime = _timeProvider.GetUtcNow().ToUnixTimeMilliseconds(); - if (IsCacheStale(currentTime)) + if (IsCacheStale(currentTime, optionsSnapshot)) { LastAccess = currentTime; LogCacheStale(); - Cached = await provider.LoadInternalAsync(false, cancellationToken); + Cached = await provider.LoadInternalAsync(optionsSnapshot, false, cancellationToken); } return Cached?.PropertySources; } - internal bool IsCacheStale(long accessTime) + internal bool IsCacheStale(long accessTime, ConfigServerClientOptions optionsSnapshot) { if (Cached == null) { return true; } - return accessTime - LastAccess >= GetTimeToLive(); - } - - internal bool IsEnabled() - { - return Provider is { ClientOptions.Health.Enabled: true }; - } - - internal long GetTimeToLive() - { - return Provider != null ? Provider.ClientOptions.Health.TimeToLive : long.MaxValue; + return accessTime - LastAccess >= optionsSnapshot.Health.TimeToLive; } [LoggerMessage(Level = LogLevel.Warning, Message = "No Config Server provider found, health check disabled.")] diff --git a/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationProviderTest.Loading.cs b/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationProviderTest.Loading.cs index bd79ca81c8..4451bcaa61 100644 --- a/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationProviderTest.Loading.cs +++ b/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationProviderTest.Loading.cs @@ -461,7 +461,7 @@ public async Task Load_MultipleConfigServers_SocketError_FallsBackToNextServer() using var provider = new ConfigServerConfigurationProvider(options, null, handler, NullLoggerFactory.Instance); - await provider.LoadInternalAsync(true, TestContext.Current.CancellationToken); + await provider.LoadInternalAsync(provider.ClientOptions, true, TestContext.Current.CancellationToken); provider.TryGet("key1", out string? value).Should().BeTrue(); value.Should().Be("value1"); @@ -644,7 +644,7 @@ public async Task Load_MultipleConfigServers_ReturnsGreaterThanEqualBadRequest_S using var httpClientHandler = new ForwardingHttpClientHandler(server.CreateHandler()); using var provider = new ConfigServerConfigurationProvider(options, null, httpClientHandler, NullLoggerFactory.Instance); - await provider.LoadInternalAsync(true, TestContext.Current.CancellationToken); + await provider.LoadInternalAsync(provider.ClientOptions, true, TestContext.Current.CancellationToken); startup.LastRequest.Should().NotBeNull(); startup.LastRequest.Path.Value.Should().Be($"/{options.Name}/{options.Environment}"); @@ -679,7 +679,7 @@ public async Task Load_MultipleConfigServers_ReturnsNotFoundStatus_DoesNotContin using var httpClientHandler = new ForwardingHttpClientHandler(server.CreateHandler()); using var provider = new ConfigServerConfigurationProvider(options, null, httpClientHandler, NullLoggerFactory.Instance); - await provider.LoadInternalAsync(true, TestContext.Current.CancellationToken); + await provider.LoadInternalAsync(provider.ClientOptions, true, TestContext.Current.CancellationToken); startup.LastRequest.Should().NotBeNull(); startup.LastRequest.Path.Value.Should().Be($"/{options.Name}/{options.Environment}"); @@ -707,7 +707,7 @@ public async Task Load_ConfigServerReturnsNotFoundStatus() using var httpClientHandler = new ForwardingHttpClientHandler(server.CreateHandler()); using var provider = new ConfigServerConfigurationProvider(options, null, httpClientHandler, NullLoggerFactory.Instance); - await provider.LoadInternalAsync(true, TestContext.Current.CancellationToken); + await provider.LoadInternalAsync(provider.ClientOptions, true, TestContext.Current.CancellationToken); startup.LastRequest.Should().NotBeNull(); startup.LastRequest.Path.Value.Should().Be($"/{options.Name}/{options.Environment}"); @@ -734,7 +734,7 @@ public async Task Load_ConfigServerReturnsNotFoundStatus_FailFastEnabled() using var provider = new ConfigServerConfigurationProvider(options, null, httpClientHandler, NullLoggerFactory.Instance); // ReSharper disable once AccessToDisposedClosure - Func action = async () => await provider.LoadInternalAsync(true, TestContext.Current.CancellationToken); + Func action = async () => await provider.LoadInternalAsync(provider.ClientOptions, true, TestContext.Current.CancellationToken); await action.Should().ThrowExactlyAsync(); } @@ -767,7 +767,7 @@ public async Task Load_MultipleConfigServers_ReturnsNotFoundStatus__DoesNotConti ]; // ReSharper disable once AccessToDisposedClosure - Func action = async () => await provider.LoadInternalAsync(true, TestContext.Current.CancellationToken); + Func action = async () => await provider.LoadInternalAsync(provider.ClientOptions, true, TestContext.Current.CancellationToken); await action.Should().ThrowExactlyAsync(); startup.RequestCount.Should().Be(1); @@ -798,7 +798,7 @@ public async Task Load_UriInvalid_FailFastEnabled() using var provider = new ConfigServerConfigurationProvider(options, null, httpClientHandler, NullLoggerFactory.Instance); // ReSharper disable once AccessToDisposedClosure - Func action = async () => await provider.LoadInternalAsync(true, TestContext.Current.CancellationToken); + Func action = async () => await provider.LoadInternalAsync(provider.ClientOptions, true, TestContext.Current.CancellationToken); await action.Should().ThrowExactlyAsync().WithMessage("One or more Config Server URIs in configuration are invalid."); } @@ -823,7 +823,7 @@ public async Task Load_ConfigServerReturnsBadStatus_FailFastEnabled() using var provider = new ConfigServerConfigurationProvider(options, null, httpClientHandler, NullLoggerFactory.Instance); // ReSharper disable once AccessToDisposedClosure - Func action = async () => await provider.LoadInternalAsync(true, TestContext.Current.CancellationToken); + Func action = async () => await provider.LoadInternalAsync(provider.ClientOptions, true, TestContext.Current.CancellationToken); await action.Should().ThrowExactlyAsync(); } @@ -855,7 +855,7 @@ public async Task Load_MultipleConfigServers_ReturnsBadStatus_StopsChecking_Fail using var provider = new ConfigServerConfigurationProvider(options, null, httpClientHandler, NullLoggerFactory.Instance); // ReSharper disable once AccessToDisposedClosure - Func action = async () => await provider.LoadInternalAsync(true, TestContext.Current.CancellationToken); + Func action = async () => await provider.LoadInternalAsync(provider.ClientOptions, true, TestContext.Current.CancellationToken); await action.Should().ThrowExactlyAsync(); startup.RequestCount.Should().Be(1); @@ -896,7 +896,7 @@ await TestFailureTracer.CaptureAsync(async tracer => using var provider = new ConfigServerConfigurationProvider(options, null, httpClientHandler, tracer.LoggerFactory); // ReSharper disable once AccessToDisposedClosure - Func action = async () => await provider.LoadInternalAsync(true, TestContext.Current.CancellationToken); + Func action = async () => await provider.LoadInternalAsync(provider.ClientOptions, true, TestContext.Current.CancellationToken); await action.Should().ThrowExactlyAsync(); @@ -944,7 +944,7 @@ public async Task Load_ChangesDataDictionary() using var httpClientHandler = new ForwardingHttpClientHandler(server.CreateHandler()); using var provider = new ConfigServerConfigurationProvider(options, null, httpClientHandler, NullLoggerFactory.Instance); - await provider.LoadInternalAsync(true, TestContext.Current.CancellationToken); + await provider.LoadInternalAsync(provider.ClientOptions, true, TestContext.Current.CancellationToken); startup.LastRequest.Should().NotBeNull(); startup.LastRequest.Path.Value.Should().Be($"/{options.Name}/{options.Environment}"); diff --git a/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationProviderTest.cs b/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationProviderTest.cs index e0bfb947b5..ae1f8e35a5 100644 --- a/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationProviderTest.cs +++ b/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationProviderTest.cs @@ -444,7 +444,7 @@ public async Task DiscoverServerInstances_FailsFast() using var provider = new ConfigServerConfigurationProvider(source, NullLoggerFactory.Instance); // ReSharper disable once AccessToDisposedClosure - Func action = async () => await provider.LoadInternalAsync(true, TestContext.Current.CancellationToken); + Func action = async () => await provider.LoadInternalAsync(provider.ClientOptions, true, TestContext.Current.CancellationToken); await action.Should().ThrowExactlyAsync().WithMessage("Could not locate Config Server via discovery*"); } diff --git a/src/Configuration/test/ConfigServer.Test/ConfigServerHealthContributorTest.cs b/src/Configuration/test/ConfigServer.Test/ConfigServerHealthContributorTest.cs index f0a408d663..b6a1829cc0 100644 --- a/src/Configuration/test/ConfigServer.Test/ConfigServerHealthContributorTest.cs +++ b/src/Configuration/test/ConfigServer.Test/ConfigServerHealthContributorTest.cs @@ -27,7 +27,6 @@ public void Constructor_FindsConfigServerProviderInsidePlaceholderProvider() builder.AddInMemoryCollection(values); builder.AddConfigServer(); builder.AddPlaceholderResolver(); - IConfigurationRoot configurationRoot = builder.Build(); var contributor = new ConfigServerHealthContributor(configurationRoot, TimeProvider.System, NullLogger.Instance); @@ -48,57 +47,12 @@ public void FindProvider_FindsProvider() var builder = new ConfigurationBuilder(); builder.AddInMemoryCollection(values); builder.AddConfigServer(); - IConfigurationRoot configurationRoot = builder.Build(); var contributor = new ConfigServerHealthContributor(configurationRoot, TimeProvider.System, NullLogger.Instance); contributor.Provider.Should().NotBeNull(); } - [Fact] - public void GetTimeToLive_ReturnsExpected() - { - var values = new Dictionary(TestSettingsFactory.Get(FastTestConfigurations.ConfigServer)) - { - ["spring:cloud:config:uri"] = "http://localhost:8888/", - ["spring:cloud:config:name"] = "myName", - ["spring:cloud:config:label"] = "myLabel", - ["spring:cloud:config:health:timeToLive"] = "100000", - ["spring:cloud:config:timeout"] = "10" - }; - - var builder = new ConfigurationBuilder(); - builder.AddInMemoryCollection(values); - builder.AddConfigServer(); - - IConfigurationRoot configurationRoot = builder.Build(); - - var contributor = new ConfigServerHealthContributor(configurationRoot, TimeProvider.System, NullLogger.Instance); - contributor.GetTimeToLive().Should().Be(100_000); - } - - [Fact] - public void IsEnabled_ReturnsExpected() - { - var values = new Dictionary(TestSettingsFactory.Get(FastTestConfigurations.ConfigServer)) - { - ["spring:cloud:config:uri"] = "http://localhost:8888/", - ["spring:cloud:config:name"] = "myName", - ["spring:cloud:config:label"] = "myLabel", - ["spring:cloud:config:health:enabled"] = "true", - ["spring:cloud:config:timeout"] = "10" - }; - - var builder = new ConfigurationBuilder(); - builder.AddInMemoryCollection(values); - builder.AddConfigServer(); - - IConfigurationRoot configurationRoot = builder.Build(); - - var contributor = new ConfigServerHealthContributor(configurationRoot, TimeProvider.System, NullLogger.Instance); - contributor.IsEnabled().Should().BeTrue(); - } - [Fact] public void IsCacheStale_ReturnsExpected() { @@ -115,15 +69,23 @@ public void IsCacheStale_ReturnsExpected() var builder = new ConfigurationBuilder(); builder.AddInMemoryCollection(values); builder.AddConfigServer(); - IConfigurationRoot configurationRoot = builder.Build(); var contributor = new ConfigServerHealthContributor(configurationRoot, TimeProvider.System, NullLogger.Instance); - contributor.IsCacheStale(0).Should().BeTrue(); // No cache established yet + + var optionsSnapshot = new ConfigServerClientOptions + { + Health = + { + TimeToLive = 1 + } + }; + + contributor.IsCacheStale(0, optionsSnapshot).Should().BeTrue(); // No cache established yet contributor.Cached = new ConfigEnvironment(); contributor.LastAccess = 9; - contributor.IsCacheStale(10).Should().BeTrue(); - contributor.IsCacheStale(8).Should().BeFalse(); + contributor.IsCacheStale(10, optionsSnapshot).Should().BeTrue(); + contributor.IsCacheStale(8, optionsSnapshot).Should().BeFalse(); } [Fact] @@ -143,7 +105,6 @@ public async Task GetPropertySources_ReturnsExpected() var builder = new ConfigurationBuilder(); builder.AddInMemoryCollection(values); builder.AddConfigServer(); - IConfigurationRoot configurationRoot = builder.Build(); var contributor = new ConfigServerHealthContributor(configurationRoot, TimeProvider.System, NullLogger.Instance) @@ -151,8 +112,18 @@ public async Task GetPropertySources_ReturnsExpected() Cached = new ConfigEnvironment() }; + var optionsSnapshot = new ConfigServerClientOptions + { + Health = + { + TimeToLive = 1 + } + }; + long lastAccess = contributor.LastAccess = DateTimeOffset.Now.ToUnixTimeMilliseconds() - 100; - IList? sources = await contributor.GetPropertySourcesAsync(contributor.Provider!, TestContext.Current.CancellationToken); + + IList? sources = + await contributor.GetPropertySourcesAsync(contributor.Provider!, optionsSnapshot, TestContext.Current.CancellationToken); contributor.LastAccess.Should().NotBe(lastAccess); sources.Should().BeNull(); From 2e26d63010ec0e5cfcc9e6bab5cb663c6d944916 Mon Sep 17 00:00:00 2001 From: Bart Koelman <104792814+bart-vmware@users.noreply.github.com> Date: Sat, 28 Mar 2026 13:05:08 +0100 Subject: [PATCH 11/29] Fix torn reads in _lastDiscoveryLookupResult --- .../ConfigServerConfigurationProvider.cs | 28 +++++++++++-------- 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/src/Configuration/src/ConfigServer/ConfigServerConfigurationProvider.cs b/src/Configuration/src/ConfigServer/ConfigServerConfigurationProvider.cs index 8a90076b06..ed1559b308 100644 --- a/src/Configuration/src/ConfigServer/ConfigServerConfigurationProvider.cs +++ b/src/Configuration/src/ConfigServer/ConfigServerConfigurationProvider.cs @@ -458,36 +458,40 @@ public override void Load() internal void ApplyLastDiscoveryLookupResultToClientOptions(ConfigServerClientOptions optionsSnapshot) { - if (_lastDiscoveryLookupResult != null && optionsSnapshot.Discovery.Enabled) + DiscoveryLookupResult? lastResult = _lastDiscoveryLookupResult; + + if (lastResult != null && optionsSnapshot.Discovery.Enabled) { - optionsSnapshot.Uri = _lastDiscoveryLookupResult.ConfigServerUri; + optionsSnapshot.Uri = lastResult.ConfigServerUri; - if (_lastDiscoveryLookupResult.Username != null) + if (lastResult.Username != null) { - optionsSnapshot.Username = _lastDiscoveryLookupResult.Username; + optionsSnapshot.Username = lastResult.Username; } - if (_lastDiscoveryLookupResult.Password != null) + if (lastResult.Password != null) { - optionsSnapshot.Password = _lastDiscoveryLookupResult.Password; + optionsSnapshot.Password = lastResult.Password; } } } private void CopyLastDiscoveryLookupResultToData(Dictionary data, bool isDiscoveryEnabled) { - if (_lastDiscoveryLookupResult != null && isDiscoveryEnabled) + DiscoveryLookupResult? lastResult = _lastDiscoveryLookupResult; + + if (lastResult != null && isDiscoveryEnabled) { - data["spring:cloud:config:uri"] = _lastDiscoveryLookupResult.ConfigServerUri; + data["spring:cloud:config:uri"] = lastResult.ConfigServerUri; - if (_lastDiscoveryLookupResult.Username != null) + if (lastResult.Username != null) { - data["spring:cloud:config:username"] = _lastDiscoveryLookupResult.Username; + data["spring:cloud:config:username"] = lastResult.Username; } - if (_lastDiscoveryLookupResult.Password != null) + if (lastResult.Password != null) { - data["spring:cloud:config:password"] = _lastDiscoveryLookupResult.Password; + data["spring:cloud:config:password"] = lastResult.Password; } } } From 5d79adf468dc8e0ebe69cd2788248c4c05381b00 Mon Sep 17 00:00:00 2001 From: Bart Koelman <104792814+bart-vmware@users.noreply.github.com> Date: Sat, 28 Mar 2026 13:10:24 +0100 Subject: [PATCH 12/29] Fixed: act on single snapshot of _httpClientHandler --- .../ConfigServerConfigurationProvider.cs | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/Configuration/src/ConfigServer/ConfigServerConfigurationProvider.cs b/src/Configuration/src/ConfigServer/ConfigServerConfigurationProvider.cs index ed1559b308..5bd6917284 100644 --- a/src/Configuration/src/ConfigServer/ConfigServerConfigurationProvider.cs +++ b/src/Configuration/src/ConfigServer/ConfigServerConfigurationProvider.cs @@ -52,8 +52,8 @@ internal sealed partial class ConfigServerConfigurationProvider : ConfigurationP private readonly LockPrimitive _configurationReloadTickLock = new(); private readonly LockPrimitive _vaultRenewTickLock = new(); - private HttpClientHandler? _httpClientHandler; - private ConfigServerDiscoveryService? _configServerDiscoveryService; + private volatile HttpClientHandler? _httpClientHandler; + private volatile ConfigServerDiscoveryService? _configServerDiscoveryService; private Timer? _configurationReloadTimer; private Timer? _vaultRenewTimer; private volatile DiscoveryLookupResult? _lastDiscoveryLookupResult; @@ -890,9 +890,10 @@ private async Task GetVaultRenewRequestMessageAsync(ConfigSe internal HttpClient CreateHttpClient(ConfigServerClientOptions clientOptions) { ArgumentNullException.ThrowIfNull(clientOptions); - ObjectDisposedException.ThrowIf(_httpClientHandler == null, this); + HttpClientHandler? handler = _httpClientHandler; + ObjectDisposedException.ThrowIf(handler == null, this); - var httpClient = new HttpClient(_httpClientHandler, false); + var httpClient = new HttpClient(handler, false); httpClient.ConfigureForSteeltoe(clientOptions.HttpTimeout); foreach ((string headerName, string headerValue) in clientOptions.Headers) @@ -905,20 +906,22 @@ internal HttpClient CreateHttpClient(ConfigServerClientOptions clientOptions) private void ConfigureHttpClientHandler(ConfigServerClientOptions optionsSnapshot) { - if (_httpClientHandler == null) + HttpClientHandler? httpClientHandler = _httpClientHandler; + + if (httpClientHandler == null) { return; } - _httpClientHandler.ClientCertificates.Clear(); + httpClientHandler.ClientCertificates.Clear(); var clientCertificateConfigurer = new ClientCertificateHttpClientHandlerConfigurer(OptionsMonitorWrapper.Create(optionsSnapshot.ClientCertificate)); - clientCertificateConfigurer.Configure("ConfigServer", _httpClientHandler); + clientCertificateConfigurer.Configure("ConfigServer", httpClientHandler); var validateCertificatesHandler = new ValidateCertificatesHttpClientHandlerConfigurer(OptionsMonitorWrapper.Create(optionsSnapshot)); - validateCertificatesHandler.Configure(Options.DefaultName, _httpClientHandler); + validateCertificatesHandler.Configure(Options.DefaultName, httpClientHandler); } private static bool IsSocketError(Exception exception) From 2f771f508d9595ada569f09ff658dfc8fe7935b6 Mon Sep 17 00:00:00 2001 From: Bart Koelman <104792814+bart-vmware@users.noreply.github.com> Date: Sat, 28 Mar 2026 14:43:18 +0100 Subject: [PATCH 13/29] Refresh discovery during polled reload and fix lazy initialization race Polled configuration reloads now refresh the discovery service lookup before fetching from Config Server, enabling detection of server address changes between polling intervals. Unlike initial load, polled reload does not apply FailFast or retry semantics. Fixed a race where concurrent calls to LoadInternalAsync could create multiple ConfigServerDiscoveryService instances, each constructing their own temporary DI container and discovery clients. --- .../ConfigServerConfigurationProvider.cs | 47 +++++++------------ .../ConfigServerDiscoveryService.cs | 40 ++++++++++++---- .../ConfigServerDiscoveryServiceTest.cs | 3 +- .../ConfigServerDiscoveryServiceTest.cs | 3 +- 4 files changed, 53 insertions(+), 40 deletions(-) diff --git a/src/Configuration/src/ConfigServer/ConfigServerConfigurationProvider.cs b/src/Configuration/src/ConfigServer/ConfigServerConfigurationProvider.cs index 5bd6917284..e760be32ae 100644 --- a/src/Configuration/src/ConfigServer/ConfigServerConfigurationProvider.cs +++ b/src/Configuration/src/ConfigServer/ConfigServerConfigurationProvider.cs @@ -51,9 +51,9 @@ internal sealed partial class ConfigServerConfigurationProvider : ConfigurationP private readonly LockPrimitive _lifecycleLock = new(); private readonly LockPrimitive _configurationReloadTickLock = new(); private readonly LockPrimitive _vaultRenewTickLock = new(); + private readonly ConfigServerDiscoveryService _configServerDiscoveryService; private volatile HttpClientHandler? _httpClientHandler; - private volatile ConfigServerDiscoveryService? _configServerDiscoveryService; private Timer? _configurationReloadTimer; private Timer? _vaultRenewTimer; private volatile DiscoveryLookupResult? _lastDiscoveryLookupResult; @@ -110,6 +110,7 @@ internal ConfigServerConfigurationProvider(ConfigServerClientOptions clientOptio } _configurer = new ConfigureConfigServerClientOptions(_configuration); + _configServerDiscoveryService = new ConfigServerDiscoveryService(_configuration, _loggerFactory); _initialOptions = clientOptions.Clone(); _clientOptions = _initialOptions; @@ -214,10 +215,12 @@ private void ConfigurationReloadTimerTick() } LogConfigurationReloadLockObtained(); + ConfigServerClientOptions optionsSnapshot = ClientOptions; #pragma warning disable S4462 // Calls to "async" methods should not be blocking // Justification: Configuration sources and providers don't support async. - DoLoadAsync(ClientOptions, true, CancellationToken.None).GetAwaiter().GetResult(); + UpdateDiscoveryAsync(optionsSnapshot, false, CancellationToken.None).GetAwaiter().GetResult(); + DoLoadAsync(optionsSnapshot, true, CancellationToken.None).GetAwaiter().GetResult(); #pragma warning restore S4462 // Calls to "async" methods should not be blocking LogConfigurationReloadCycleCompleted(); @@ -327,15 +330,7 @@ public override void Load() return null; } - if (!optionsSnapshot.Discovery.Enabled) - { - SetLastDiscoveryLookupResult([]); - } - else - { - _configServerDiscoveryService ??= new ConfigServerDiscoveryService(_configuration, _loggerFactory); - await DiscoverServerInstancesAsync(_configServerDiscoveryService, optionsSnapshot, cancellationToken); - } + await UpdateDiscoveryAsync(optionsSnapshot, optionsSnapshot.FailFast, cancellationToken); if (optionsSnapshot is { Retry.Enabled: true, FailFast: true }) { @@ -513,22 +508,22 @@ internal string[] GetLabels(ConfigServerClientOptions optionsSnapshot) return optionsSnapshot.Label.Split(CommaDelimiter, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries); } - private async Task DiscoverServerInstancesAsync(ConfigServerDiscoveryService configServerDiscoveryService, ConfigServerClientOptions optionsSnapshot, - CancellationToken cancellationToken) + private async Task UpdateDiscoveryAsync(ConfigServerClientOptions optionsSnapshot, bool failFast, CancellationToken cancellationToken) { - List instances = await configServerDiscoveryService.GetConfigServerInstancesAsync(optionsSnapshot, cancellationToken); - - if (instances.Count == 0) + if (optionsSnapshot.Discovery.Enabled) { - if (optionsSnapshot.FailFast) + List instances = await _configServerDiscoveryService.GetConfigServerInstancesAsync(optionsSnapshot, cancellationToken); + SetLastDiscoveryLookupResult(instances); + + if (instances.Count == 0 && failFast) { throw new ConfigServerException("Could not locate Config Server via discovery, are you missing a Discovery service assembly?"); } - - return; } - - SetLastDiscoveryLookupResult(instances); + else + { + SetLastDiscoveryLookupResult([]); + } } internal void SetLastDiscoveryLookupResult(IEnumerable instances) @@ -575,18 +570,12 @@ internal void SetLastDiscoveryLookupResult(IEnumerable instanc internal async Task ProvideRuntimeReplacementsAsync(ICollection discoveryClientsFromServiceProvider, CancellationToken cancellationToken) { - if (_configServerDiscoveryService is not null) - { - await _configServerDiscoveryService.ProvideRuntimeReplacementsAsync(discoveryClientsFromServiceProvider, cancellationToken); - } + await _configServerDiscoveryService.ProvideRuntimeReplacementsAsync(discoveryClientsFromServiceProvider, cancellationToken); } internal async Task ShutdownAsync(CancellationToken cancellationToken) { - if (_configServerDiscoveryService is not null) - { - await _configServerDiscoveryService.ShutdownAsync(cancellationToken); - } + await _configServerDiscoveryService.ShutdownAsync(cancellationToken); } /// diff --git a/src/Configuration/src/ConfigServer/ConfigServerDiscoveryService.cs b/src/Configuration/src/ConfigServer/ConfigServerDiscoveryService.cs index bdae501f58..f61065b44d 100644 --- a/src/Configuration/src/ConfigServer/ConfigServerDiscoveryService.cs +++ b/src/Configuration/src/ConfigServer/ConfigServerDiscoveryService.cs @@ -12,6 +12,13 @@ using Steeltoe.Discovery.Configuration; using Steeltoe.Discovery.Consul; using Steeltoe.Discovery.Eureka; +using LockPrimitive = +#if NET10_0_OR_GREATER + System.Threading.Lock +#else + object +#endif + ; namespace Steeltoe.Configuration.ConfigServer; @@ -19,10 +26,12 @@ internal sealed partial class ConfigServerDiscoveryService { private static readonly AssemblyLoader AssemblyLoader = new(); private readonly IConfiguration _configuration; + private readonly ILoggerFactory _loggerFactory; private readonly ILogger _logger; + private readonly LockPrimitive _initLock = new(); private ServiceProvider? _temporaryServiceProviderForDiscoveryClients; - internal ICollection DiscoveryClients { get; private set; } + internal ICollection? DiscoveryClients { get; private set; } public ConfigServerDiscoveryService(IConfiguration configuration, ILoggerFactory loggerFactory) { @@ -30,15 +39,27 @@ public ConfigServerDiscoveryService(IConfiguration configuration, ILoggerFactory ArgumentNullException.ThrowIfNull(loggerFactory); _configuration = configuration; + _loggerFactory = loggerFactory; _logger = loggerFactory.CreateLogger(); - DiscoveryClients = SetupDiscoveryClients(loggerFactory); } - // Create discovery clients to be used (hopefully only) during startup - private IDiscoveryClient[] SetupDiscoveryClients(ILoggerFactory loggerFactory) + private void EnsureInitialized() + { + if (DiscoveryClients != null) + { + return; + } + + lock (_initLock) + { + DiscoveryClients ??= SetupDiscoveryClients(); + } + } + + private IDiscoveryClient[] SetupDiscoveryClients() { var tempServices = new ServiceCollection(); - tempServices.AddSingleton(loggerFactory); + tempServices.AddSingleton(_loggerFactory); tempServices.AddSingleton(typeof(ILogger<>), typeof(Logger<>)); // force settings to make sure we don't register the app here @@ -102,11 +123,12 @@ private IDiscoveryClient[] GetDiscoveryClientsFromServiceCollection(ServiceColle return discoveryClients; } - internal async Task> GetConfigServerInstancesAsync(ConfigServerClientOptions optionsSnapshot, - CancellationToken cancellationToken) + internal async Task> GetConfigServerInstancesAsync(ConfigServerClientOptions optionsSnapshot, CancellationToken cancellationToken) { ArgumentNullException.ThrowIfNull(optionsSnapshot); + EnsureInitialized(); + int attempts = 0; int backOff = optionsSnapshot.Retry.InitialInterval; List instances = []; @@ -115,7 +137,7 @@ internal async Task> GetConfigServerInstancesAsync(Config { LogLocatingConfigServer(optionsSnapshot.Discovery.ServiceId); - if (optionsSnapshot.Discovery.ServiceId != null) + if (optionsSnapshot.Discovery.ServiceId != null && DiscoveryClients != null) { foreach (IDiscoveryClient discoveryClient in DiscoveryClients) { @@ -171,7 +193,7 @@ internal async Task ShutdownAsync(CancellationToken cancellationToken) { if (_temporaryServiceProviderForDiscoveryClients != null) { - foreach (IDiscoveryClient discoveryClient in DiscoveryClients) + foreach (IDiscoveryClient discoveryClient in DiscoveryClients ?? []) { await discoveryClient.ShutdownAsync(cancellationToken); } diff --git a/src/Configuration/test/ConfigServer.Discovery.Test/ConfigServerDiscoveryServiceTest.cs b/src/Configuration/test/ConfigServer.Discovery.Test/ConfigServerDiscoveryServiceTest.cs index c762b5c3e1..2b193cad38 100644 --- a/src/Configuration/test/ConfigServer.Discovery.Test/ConfigServerDiscoveryServiceTest.cs +++ b/src/Configuration/test/ConfigServer.Discovery.Test/ConfigServerDiscoveryServiceTest.cs @@ -15,11 +15,12 @@ namespace Steeltoe.Configuration.ConfigServer.Discovery.Test; public sealed class ConfigServerDiscoveryServiceTest { [Fact] - public void ConfigServerDiscoveryService_FindsDiscoveryClients() + public async Task ConfigServerDiscoveryService_FindsDiscoveryClients() { IConfiguration configuration = new ConfigurationBuilder().Add(FastTestConfigurations.ConfigServer | FastTestConfigurations.Discovery).Build(); var service = new ConfigServerDiscoveryService(configuration, NullLoggerFactory.Instance); + await service.GetConfigServerInstancesAsync(new ConfigServerClientOptions(), TestContext.Current.CancellationToken); service.DiscoveryClients.Should().HaveCount(3); service.DiscoveryClients.OfType().Should().ContainSingle(); diff --git a/src/Configuration/test/ConfigServer.Test/ConfigServerDiscoveryServiceTest.cs b/src/Configuration/test/ConfigServer.Test/ConfigServerDiscoveryServiceTest.cs index bc47ba2a81..647f657180 100644 --- a/src/Configuration/test/ConfigServer.Test/ConfigServerDiscoveryServiceTest.cs +++ b/src/Configuration/test/ConfigServer.Test/ConfigServerDiscoveryServiceTest.cs @@ -11,11 +11,12 @@ namespace Steeltoe.Configuration.ConfigServer.Test; public sealed class ConfigServerDiscoveryServiceTest { [Fact] - public void ConfigServerDiscoveryService_FindsNoDiscoveryClients() + public async Task ConfigServerDiscoveryService_FindsNoDiscoveryClients() { IConfiguration configuration = new ConfigurationBuilder().Add(FastTestConfigurations.ConfigServer).Build(); var service = new ConfigServerDiscoveryService(configuration, NullLoggerFactory.Instance); + await service.GetConfigServerInstancesAsync(new ConfigServerClientOptions(), TestContext.Current.CancellationToken); service.DiscoveryClients.Should().BeEmpty(); } From eedb3312efb4eb82b55cc2dc4de675ed19746b55 Mon Sep 17 00:00:00 2001 From: Bart Koelman <104792814+bart-vmware@users.noreply.github.com> Date: Sat, 28 Mar 2026 15:20:38 +0100 Subject: [PATCH 14/29] Fix change callback accumulation on repeated configuration reloads Each configuration reload re-registered a new change callback via RegisterChangeCallback, without disposing the previous one. Rapid reloads could accumulate stale callbacks, causing redundant OnSettingsChanged invocations on multiple threads. Replaced with a single ChangeToken.OnChange registration in the constructor, which automatically handles re-registration and is disposed on shutdown. --- .../ConfigServerConfigurationProvider.cs | 35 ++++++------------- 1 file changed, 10 insertions(+), 25 deletions(-) diff --git a/src/Configuration/src/ConfigServer/ConfigServerConfigurationProvider.cs b/src/Configuration/src/ConfigServer/ConfigServerConfigurationProvider.cs index e760be32ae..68da90ba45 100644 --- a/src/Configuration/src/ConfigServer/ConfigServerConfigurationProvider.cs +++ b/src/Configuration/src/ConfigServer/ConfigServerConfigurationProvider.cs @@ -15,6 +15,7 @@ using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; using Microsoft.Extensions.Options; +using Microsoft.Extensions.Primitives; using Steeltoe.Common.Configuration; using Steeltoe.Common.Discovery; using Steeltoe.Common.Extensions; @@ -41,10 +42,7 @@ internal sealed partial class ConfigServerConfigurationProvider : ConfigurationP internal const string TokenHeader = "X-Config-Token"; private static readonly string[] EmptyLabels = [string.Empty]; - private readonly ILoggerFactory _loggerFactory; - private readonly ILogger _logger; - private readonly IConfiguration _configuration; - private readonly bool _hasConfiguration; + private readonly ILogger _logger; private readonly bool _ownsHttpClientHandler; private readonly ConfigureConfigServerClientOptions _configurer; private readonly ConfigServerClientOptions _initialOptions; @@ -52,6 +50,7 @@ internal sealed partial class ConfigServerConfigurationProvider : ConfigurationP private readonly LockPrimitive _configurationReloadTickLock = new(); private readonly LockPrimitive _vaultRenewTickLock = new(); private readonly ConfigServerDiscoveryService _configServerDiscoveryService; + private readonly IDisposable _changeTokenRegistration; private volatile HttpClientHandler? _httpClientHandler; private Timer? _configurationReloadTimer; @@ -95,22 +94,10 @@ internal ConfigServerConfigurationProvider(ConfigServerClientOptions clientOptio ArgumentNullException.ThrowIfNull(clientOptions); ArgumentNullException.ThrowIfNull(loggerFactory); - _loggerFactory = loggerFactory; - _logger = _loggerFactory.CreateLogger(); - - if (configuration != null) - { - _configuration = configuration; - _hasConfiguration = true; - } - else - { - _configuration = new ConfigurationBuilder().Build(); - _hasConfiguration = false; - } - - _configurer = new ConfigureConfigServerClientOptions(_configuration); - _configServerDiscoveryService = new ConfigServerDiscoveryService(_configuration, _loggerFactory); + _logger = loggerFactory.CreateLogger(); + IConfiguration effectiveConfiguration = configuration ?? new ConfigurationBuilder().Build(); + _configurer = new ConfigureConfigServerClientOptions(effectiveConfiguration); + _configServerDiscoveryService = new ConfigServerDiscoveryService(effectiveConfiguration, loggerFactory); _initialOptions = clientOptions.Clone(); _clientOptions = _initialOptions; @@ -126,6 +113,7 @@ internal ConfigServerConfigurationProvider(ConfigServerClientOptions clientOptio } OnSettingsChanged(); + _changeTokenRegistration = ChangeToken.OnChange(effectiveConfiguration.GetReloadToken, OnSettingsChanged); } private void OnSettingsChanged() @@ -133,11 +121,6 @@ private void OnSettingsChanged() ConfigServerClientOptions newOptions = _initialOptions.Clone(); _configurer.Configure(newOptions); - if (_hasConfiguration) - { - _configuration.GetReloadToken().RegisterChangeCallback(_ => OnSettingsChanged(), null); - } - lock (_lifecycleLock) { if (_isDisposed) @@ -929,6 +912,8 @@ public void Dispose() _isDisposed = true; + _changeTokenRegistration.Dispose(); + _configurationReloadTimer?.Dispose(); _configurationReloadTimer = null; From 9c621add76d21ebb85e59d1a1e9587b1d4258935 Mon Sep 17 00:00:00 2001 From: Bart Koelman <104792814+bart-vmware@users.noreply.github.com> Date: Sat, 28 Mar 2026 17:04:35 +0100 Subject: [PATCH 15/29] Fix HttpClientHandler mutation race and timer disposal race Replace shared mutable HttpClientHandler with a per-request factory, eliminating the race where OnSettingsChanged reconfigured a handler concurrently in use by HTTP requests. Production code creates and disposes a fresh handler per request; tests inject a factory for mocking. Replace _isDisposed flag with CancellationToken-based shutdown, enabling in-flight HTTP requests in timer callbacks to terminate promptly on disposal instead of running to completion. --- ...figServerConfigurationBuilderExtensions.cs | 6 +- .../ConfigServerConfigurationProvider.cs | 101 +++++++++--------- .../ConfigServerConfigurationSource.cs | 25 +++-- .../ConfigServerClientOptionsTest.cs | 4 +- .../ConfigServerClientOptionsTest.cs | 4 +- ...ServerConfigurationProviderTest.Loading.cs | 50 ++++----- .../ConfigServerConfigurationProviderTest.cs | 6 +- 7 files changed, 99 insertions(+), 97 deletions(-) diff --git a/src/Configuration/src/ConfigServer/ConfigServerConfigurationBuilderExtensions.cs b/src/Configuration/src/ConfigServer/ConfigServerConfigurationBuilderExtensions.cs index d2fa42b6f3..cd4b788391 100644 --- a/src/Configuration/src/ConfigServer/ConfigServerConfigurationBuilderExtensions.cs +++ b/src/Configuration/src/ConfigServer/ConfigServerConfigurationBuilderExtensions.cs @@ -69,7 +69,7 @@ public static IConfigurationBuilder AddConfigServer(this IConfigurationBuilder b } internal static IConfigurationBuilder AddConfigServer(this IConfigurationBuilder builder, ConfigServerClientOptions options, - HttpClientHandler? httpClientHandler, ILoggerFactory loggerFactory) + Func? createHttpClientHandler, ILoggerFactory loggerFactory) { ArgumentNullException.ThrowIfNull(builder); ArgumentNullException.ThrowIfNull(options); @@ -81,8 +81,8 @@ internal static IConfigurationBuilder AddConfigServer(this IConfigurationBuilder builder.AddKubernetesServiceBindings(); ConfigServerConfigurationSource source = builder is IConfiguration configuration - ? new ConfigServerConfigurationSource(options, configuration, httpClientHandler, loggerFactory) - : new ConfigServerConfigurationSource(options, builder.Sources, builder.Properties, httpClientHandler, loggerFactory); + ? new ConfigServerConfigurationSource(options, configuration, createHttpClientHandler, loggerFactory) + : new ConfigServerConfigurationSource(options, builder.Sources, builder.Properties, createHttpClientHandler, loggerFactory); builder.Add(source); } diff --git a/src/Configuration/src/ConfigServer/ConfigServerConfigurationProvider.cs b/src/Configuration/src/ConfigServer/ConfigServerConfigurationProvider.cs index 68da90ba45..e181ed3507 100644 --- a/src/Configuration/src/ConfigServer/ConfigServerConfigurationProvider.cs +++ b/src/Configuration/src/ConfigServer/ConfigServerConfigurationProvider.cs @@ -43,7 +43,8 @@ internal sealed partial class ConfigServerConfigurationProvider : ConfigurationP private static readonly string[] EmptyLabels = [string.Empty]; private readonly ILogger _logger; - private readonly bool _ownsHttpClientHandler; + private readonly Func _createHttpClientHandler; + private readonly bool _disposeHttpClientHandler; private readonly ConfigureConfigServerClientOptions _configurer; private readonly ConfigServerClientOptions _initialOptions; private readonly LockPrimitive _lifecycleLock = new(); @@ -51,13 +52,13 @@ internal sealed partial class ConfigServerConfigurationProvider : ConfigurationP private readonly LockPrimitive _vaultRenewTickLock = new(); private readonly ConfigServerDiscoveryService _configServerDiscoveryService; private readonly IDisposable _changeTokenRegistration; + private readonly CancellationTokenSource _shutdownTokenSource = new(); + private readonly CancellationToken _shutdownToken; - private volatile HttpClientHandler? _httpClientHandler; private Timer? _configurationReloadTimer; private Timer? _vaultRenewTimer; private volatile DiscoveryLookupResult? _lastDiscoveryLookupResult; private volatile ConfigServerClientOptions _clientOptions; - private volatile bool _isDisposed; internal static JsonSerializerOptions SerializerOptions { get; } = new() { @@ -84,17 +85,18 @@ internal sealed partial class ConfigServerConfigurationProvider : ConfigurationP /// Used for internal logging. Pass to disable logging. /// public ConfigServerConfigurationProvider(ConfigServerConfigurationSource source, ILoggerFactory loggerFactory) - : this(source.DefaultOptions, source.Configuration, source.HttpClientHandler, loggerFactory) + : this(source.DefaultOptions, source.Configuration, source.CreateHttpClientHandler, loggerFactory) { } - internal ConfigServerConfigurationProvider(ConfigServerClientOptions clientOptions, IConfiguration? configuration, HttpClientHandler? httpClientHandler, - ILoggerFactory loggerFactory) + internal ConfigServerConfigurationProvider(ConfigServerClientOptions clientOptions, IConfiguration? configuration, + Func? createHttpClientHandler, ILoggerFactory loggerFactory) { ArgumentNullException.ThrowIfNull(clientOptions); ArgumentNullException.ThrowIfNull(loggerFactory); _logger = loggerFactory.CreateLogger(); + _shutdownToken = _shutdownTokenSource.Token; IConfiguration effectiveConfiguration = configuration ?? new ConfigurationBuilder().Build(); _configurer = new ConfigureConfigServerClientOptions(effectiveConfiguration); _configServerDiscoveryService = new ConfigServerDiscoveryService(effectiveConfiguration, loggerFactory); @@ -102,14 +104,15 @@ internal ConfigServerConfigurationProvider(ConfigServerClientOptions clientOptio _initialOptions = clientOptions.Clone(); _clientOptions = _initialOptions; - if (httpClientHandler == null) + if (createHttpClientHandler != null) { - _httpClientHandler = new HttpClientHandler(); - _ownsHttpClientHandler = true; + _createHttpClientHandler = createHttpClientHandler; + _disposeHttpClientHandler = false; } else { - _httpClientHandler = httpClientHandler; + _createHttpClientHandler = static () => new HttpClientHandler(); + _disposeHttpClientHandler = true; } OnSettingsChanged(); @@ -123,16 +126,14 @@ private void OnSettingsChanged() lock (_lifecycleLock) { - if (_isDisposed) + if (_shutdownToken.IsCancellationRequested) { - // Prevent creating new timers after Dispose has already torn them down. return; } TimeSpan previousPollingInterval = _clientOptions.PollingInterval; int previousTokenRenewRate = _clientOptions.TokenRenewRate; - ConfigureHttpClientHandler(newOptions); UpdateConfigurationReloadTimer(newOptions, previousPollingInterval); UpdateVaultRenewTimer(newOptions, previousTokenRenewRate); @@ -191,7 +192,7 @@ private void ConfigurationReloadTimerTick() try { - if (!lockTaken || _isDisposed) + if (!lockTaken || _shutdownToken.IsCancellationRequested) { LogSkippingConfigurationReloadCycle(); return; @@ -202,19 +203,15 @@ private void ConfigurationReloadTimerTick() #pragma warning disable S4462 // Calls to "async" methods should not be blocking // Justification: Configuration sources and providers don't support async. - UpdateDiscoveryAsync(optionsSnapshot, false, CancellationToken.None).GetAwaiter().GetResult(); - DoLoadAsync(optionsSnapshot, true, CancellationToken.None).GetAwaiter().GetResult(); + UpdateDiscoveryAsync(optionsSnapshot, false, _shutdownToken).GetAwaiter().GetResult(); + DoLoadAsync(optionsSnapshot, true, _shutdownToken).GetAwaiter().GetResult(); #pragma warning restore S4462 // Calls to "async" methods should not be blocking LogConfigurationReloadCycleCompleted(); } catch (Exception exception) { - if (exception is ObjectDisposedException && _isDisposed) - { - // Disposal can race with the timer callback because _isDisposed is read without locking. - } - else + if (!exception.IsCancellation()) { LogFailedToReloadConfiguration(exception); } @@ -247,7 +244,7 @@ private void VaultRenewTimerTick() try { - if (!lockTaken || _isDisposed) + if (!lockTaken || _shutdownToken.IsCancellationRequested) { LogSkippingVaultRenewCycle(); return; @@ -257,18 +254,14 @@ private void VaultRenewTimerTick() #pragma warning disable S4462 // Calls to "async" methods should not be blocking // Justification: Configuration sources and providers don't support async. - RefreshVaultTokenAsync(ClientOptions, CancellationToken.None).GetAwaiter().GetResult(); + RefreshVaultTokenAsync(ClientOptions, _shutdownToken).GetAwaiter().GetResult(); #pragma warning restore S4462 // Calls to "async" methods should not be blocking LogVaultRenewCycleCompleted(); } catch (Exception exception) { - if (exception is ObjectDisposedException && _isDisposed) - { - // Disposal can race with the timer callback because _isDisposed is read without locking. - } - else + if (!exception.IsCancellation()) { LogFailedToRenewVaultToken(exception); } @@ -295,10 +288,10 @@ public override void Load() { #pragma warning disable S4462 // Calls to "async" methods should not be blocking // Justification: Configuration sources and providers don't support async. - LoadInternalAsync(ClientOptions, true, CancellationToken.None).GetAwaiter().GetResult(); + LoadInternalAsync(ClientOptions, true, _shutdownToken).GetAwaiter().GetResult(); #pragma warning restore S4462 // Calls to "async" methods should not be blocking } - catch (ObjectDisposedException) when (_isDisposed) + catch (OperationCanceledException) when (_shutdownToken.IsCancellationRequested) { // Expected during disposal; silently ignore. } @@ -862,10 +855,11 @@ private async Task GetVaultRenewRequestMessageAsync(ConfigSe internal HttpClient CreateHttpClient(ConfigServerClientOptions clientOptions) { ArgumentNullException.ThrowIfNull(clientOptions); - HttpClientHandler? handler = _httpClientHandler; - ObjectDisposedException.ThrowIf(handler == null, this); - var httpClient = new HttpClient(handler, false); + HttpClientHandler handler = _createHttpClientHandler(); + ConfigureHttpClientHandler(handler, clientOptions); + + var httpClient = new HttpClient(handler, _disposeHttpClientHandler); httpClient.ConfigureForSteeltoe(clientOptions.HttpTimeout); foreach ((string headerName, string headerValue) in clientOptions.Headers) @@ -876,15 +870,8 @@ internal HttpClient CreateHttpClient(ConfigServerClientOptions clientOptions) return httpClient; } - private void ConfigureHttpClientHandler(ConfigServerClientOptions optionsSnapshot) + private static void ConfigureHttpClientHandler(HttpClientHandler httpClientHandler, ConfigServerClientOptions optionsSnapshot) { - HttpClientHandler? httpClientHandler = _httpClientHandler; - - if (httpClientHandler == null) - { - return; - } - httpClientHandler.ClientCertificates.Clear(); var clientCertificateConfigurer = new ClientCertificateHttpClientHandlerConfigurer(OptionsMonitorWrapper.Create(optionsSnapshot.ClientCertificate)); @@ -905,28 +892,40 @@ public void Dispose() { lock (_lifecycleLock) { - if (_isDisposed) + if (_shutdownToken.IsCancellationRequested) { return; } - _isDisposed = true; - + _shutdownTokenSource.Cancel(); _changeTokenRegistration.Dispose(); + ShutdownTimers(); + _shutdownTokenSource.Dispose(); + } + } - _configurationReloadTimer?.Dispose(); - _configurationReloadTimer = null; + private void ShutdownTimers() + { + using var reloadTimerStopped = new ManualResetEvent(false); + using var vaultTimerStopped = new ManualResetEvent(false); - _vaultRenewTimer?.Dispose(); - _vaultRenewTimer = null; + if (_configurationReloadTimer == null || !_configurationReloadTimer.Dispose(reloadTimerStopped)) + { + reloadTimerStopped.Set(); } - if (_ownsHttpClientHandler) + if (_vaultRenewTimer == null || !_vaultRenewTimer.Dispose(vaultTimerStopped)) { - _httpClientHandler?.Dispose(); + vaultTimerStopped.Set(); } - _httpClientHandler = null; + WaitHandle.WaitAll([ + reloadTimerStopped, + vaultTimerStopped + ]); + + _configurationReloadTimer = null; + _vaultRenewTimer = null; } [LoggerMessage(Level = LogLevel.Trace, Message = "Entering polling configuration reload cycle.")] diff --git a/src/Configuration/src/ConfigServer/ConfigServerConfigurationSource.cs b/src/Configuration/src/ConfigServer/ConfigServerConfigurationSource.cs index 46dd25a327..6eb405bc3c 100644 --- a/src/Configuration/src/ConfigServer/ConfigServerConfigurationSource.cs +++ b/src/Configuration/src/ConfigServer/ConfigServerConfigurationSource.cs @@ -21,9 +21,10 @@ internal sealed class ConfigServerConfigurationSource : IConfigurationSource internal ConfigServerClientOptions DefaultOptions { get; } /// - /// Gets an optional handler to mock HTTP requests to Config Server. + /// Gets an optional factory to create the HTTP client handler, used to mock HTTP requests to Config Server in tests. When provided, the caller is + /// responsible for handler disposal. /// - internal HttpClientHandler? HttpClientHandler { get; } + internal Func? CreateHttpClientHandler { get; } /// /// Gets the configuration the Config Server client uses to contact the Config Server. Values returned override the default values provided in @@ -40,21 +41,22 @@ internal sealed class ConfigServerConfigurationSource : IConfigurationSource /// /// configuration used by the Config Server client. Values will override those found in default settings. /// - /// - /// An optional handler to mock HTTP requests to Config Server. + /// + /// An optional factory to create the HTTP client handler, used to mock HTTP requests to Config Server in tests. When provided, the caller is responsible + /// for handler disposal. /// /// /// Used for internal logging. Pass to disable logging. /// - public ConfigServerConfigurationSource(ConfigServerClientOptions defaultOptions, IConfiguration configuration, HttpClientHandler? httpClientHandler, - ILoggerFactory loggerFactory) + public ConfigServerConfigurationSource(ConfigServerClientOptions defaultOptions, IConfiguration configuration, + Func? createHttpClientHandler, ILoggerFactory loggerFactory) { ArgumentNullException.ThrowIfNull(configuration); ArgumentNullException.ThrowIfNull(defaultOptions); ArgumentNullException.ThrowIfNull(loggerFactory); DefaultOptions = defaultOptions; - HttpClientHandler = httpClientHandler; + CreateHttpClientHandler = createHttpClientHandler; Configuration = configuration; _loggerFactory = loggerFactory; } @@ -72,14 +74,15 @@ public ConfigServerConfigurationSource(ConfigServerClientOptions defaultOptions, /// /// properties to be used when sources are built. /// - /// - /// An optional handler to mock HTTP requests to Config Server. + /// + /// An optional factory to create the HTTP client handler, used to mock HTTP requests to Config Server in tests. When provided, the caller is responsible + /// for handler disposal. /// /// /// Used for internal logging. Pass to disable logging. /// public ConfigServerConfigurationSource(ConfigServerClientOptions defaultOptions, IList sources, - IDictionary? properties, HttpClientHandler? httpClientHandler, ILoggerFactory loggerFactory) + IDictionary? properties, Func? createHttpClientHandler, ILoggerFactory loggerFactory) { ArgumentNullException.ThrowIfNull(defaultOptions); ArgumentNullException.ThrowIfNull(sources); @@ -93,7 +96,7 @@ public ConfigServerConfigurationSource(ConfigServerClientOptions defaultOptions, } DefaultOptions = defaultOptions; - HttpClientHandler = httpClientHandler; + CreateHttpClientHandler = createHttpClientHandler; _loggerFactory = loggerFactory; } diff --git a/src/Configuration/test/ConfigServer.Discovery.Test/ConfigServerClientOptionsTest.cs b/src/Configuration/test/ConfigServer.Discovery.Test/ConfigServerClientOptionsTest.cs index 6ab1506d93..e3527ceaec 100644 --- a/src/Configuration/test/ConfigServer.Discovery.Test/ConfigServerClientOptionsTest.cs +++ b/src/Configuration/test/ConfigServer.Discovery.Test/ConfigServerClientOptionsTest.cs @@ -88,7 +88,7 @@ public void Config_Server_URI_is_resolved_from_discovery_and_survives_changes_in var configurationBuilder = new ConfigurationBuilder(); configurationBuilder.AddInMemoryAppSettingsJsonFile(fileProvider); - configurationBuilder.AddConfigServer(new ConfigServerClientOptions(), handler, NullLoggerFactory.Instance); + configurationBuilder.AddConfigServer(new ConfigServerClientOptions(), () => handler, NullLoggerFactory.Instance); IConfigurationRoot configuration = configurationBuilder.Build(); handler.Mock.VerifyNoOutstandingExpectation(); @@ -295,7 +295,7 @@ public void Updates_discovered_Config_Server_URI_on_provider_reload() var configurationBuilder = new ConfigurationBuilder(); configurationBuilder.AddInMemoryAppSettingsJsonFile(fileProvider); - configurationBuilder.AddConfigServer(new ConfigServerClientOptions(), handler, NullLoggerFactory.Instance); + configurationBuilder.AddConfigServer(new ConfigServerClientOptions(), () => handler, NullLoggerFactory.Instance); IConfigurationRoot configuration = configurationBuilder.Build(); handler.Mock.VerifyNoOutstandingExpectation(); diff --git a/src/Configuration/test/ConfigServer.Test/ConfigServerClientOptionsTest.cs b/src/Configuration/test/ConfigServer.Test/ConfigServerClientOptionsTest.cs index ba75030896..52f5824b6c 100644 --- a/src/Configuration/test/ConfigServer.Test/ConfigServerClientOptionsTest.cs +++ b/src/Configuration/test/ConfigServer.Test/ConfigServerClientOptionsTest.cs @@ -262,7 +262,7 @@ public void Certificate_configuration_survives_options_reload() var configurationBuilder = new ConfigurationBuilder(); configurationBuilder.AddInMemoryAppSettingsJsonFile(fileProvider); - configurationBuilder.AddConfigServer(new ConfigServerClientOptions(), handler, NullLoggerFactory.Instance); + configurationBuilder.AddConfigServer(new ConfigServerClientOptions(), () => handler, NullLoggerFactory.Instance); IConfigurationRoot configuration = configurationBuilder.Build(); handler.Mock.VerifyNoOutstandingExpectation(); @@ -358,7 +358,7 @@ public void Changes_in_IConfiguration_update_provider_options_and_injected_optio var configurationBuilder = new ConfigurationBuilder(); configurationBuilder.AddInMemoryAppSettingsJsonFile(fileProvider); - configurationBuilder.AddConfigServer(initialOptions, handler, NullLoggerFactory.Instance); + configurationBuilder.AddConfigServer(initialOptions, () => handler, NullLoggerFactory.Instance); IConfigurationRoot configuration = configurationBuilder.Build(); handler.Mock.VerifyNoOutstandingExpectation(); diff --git a/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationProviderTest.Loading.cs b/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationProviderTest.Loading.cs index 4451bcaa61..427e9fa6ce 100644 --- a/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationProviderTest.Loading.cs +++ b/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationProviderTest.Loading.cs @@ -25,7 +25,7 @@ public async Task RemoteLoadAsync_HostTimesOut() }; var httpClientHandler = new SlowHttpClientHandler(1.Seconds(), new HttpResponseMessage()); - using var provider = new ConfigServerConfigurationProvider(options, null, httpClientHandler, NullLoggerFactory.Instance); + using var provider = new ConfigServerConfigurationProvider(options, null, () => httpClientHandler, NullLoggerFactory.Instance); List requestUris = [new("http://localhost:9999/app/profile")]; // ReSharper disable once AccessToDisposedClosure @@ -49,7 +49,7 @@ public async Task RemoteLoadAsync_ConfigServerReturnsGreaterThanEqualBadRequest( ConfigServerClientOptions options = GetCommonOptions(); using var httpClientHandler = new ForwardingHttpClientHandler(server.CreateHandler()); - using var provider = new ConfigServerConfigurationProvider(options, null, httpClientHandler, NullLoggerFactory.Instance); + using var provider = new ConfigServerConfigurationProvider(options, null, () => httpClientHandler, NullLoggerFactory.Instance); // ReSharper disable once AccessToDisposedClosure Func action = async () => await provider.RemoteLoadAsync(provider.ClientOptions, options.GetUris(), null, TestContext.Current.CancellationToken); @@ -75,7 +75,7 @@ public async Task RemoteLoadAsync_ConfigServerReturnsLessThanBadRequest() ConfigServerClientOptions options = GetCommonOptions(); using var httpClientHandler = new ForwardingHttpClientHandler(server.CreateHandler()); - using var provider = new ConfigServerConfigurationProvider(options, null, httpClientHandler, NullLoggerFactory.Instance); + using var provider = new ConfigServerConfigurationProvider(options, null, () => httpClientHandler, NullLoggerFactory.Instance); ConfigEnvironment? result = await provider.RemoteLoadAsync(provider.ClientOptions, options.GetUris(), null, TestContext.Current.CancellationToken); @@ -121,7 +121,7 @@ await TestFailureTracer.CaptureAsync(async tracer => }; using var httpClientHandler = new ForwardingHttpClientHandler(server.CreateHandler()); - using var provider = new ConfigServerConfigurationProvider(options, null, httpClientHandler, tracer.LoggerFactory); + using var provider = new ConfigServerConfigurationProvider(options, null, () => httpClientHandler, tracer.LoggerFactory); bool firstRequestCompleted = startup.WaitForFirstRequest(2.Seconds()); firstRequestCompleted.Should().BeTrue(); @@ -176,7 +176,7 @@ await TestFailureTracer.CaptureAsync(async tracer => }; using var httpClientHandler = new ForwardingHttpClientHandler(server.CreateHandler()); - using var provider = new ConfigServerConfigurationProvider(options, null, httpClientHandler, tracer.LoggerFactory); + using var provider = new ConfigServerConfigurationProvider(options, null, () => httpClientHandler, tracer.LoggerFactory); bool firstRequestCompleted = startup.WaitForFirstRequest(2.Seconds()); firstRequestCompleted.Should().BeTrue(); @@ -228,7 +228,7 @@ public async Task Create_WithNonZeroPollingIntervalAndClientDisabled_PollingConf using var httpClientHandler = new ForwardingHttpClientHandler(server.CreateHandler()); - using var provider = new ConfigServerConfigurationProvider(options, null, httpClientHandler, NullLoggerFactory.Instance); + using var provider = new ConfigServerConfigurationProvider(options, null, () => httpClientHandler, NullLoggerFactory.Instance); startup.WaitForFirstRequest(2.Seconds()).Should().BeFalse(); } @@ -270,7 +270,7 @@ public void OnSettingsChanged_stops_reload_timer_when_polling_becomes_ineffectiv var configurationBuilder = new ConfigurationBuilder(); configurationBuilder.AddInMemoryAppSettingsJsonFile(fileProvider); - configurationBuilder.AddConfigServer(new ConfigServerClientOptions(), handler, NullLoggerFactory.Instance); + configurationBuilder.AddConfigServer(new ConfigServerClientOptions(), () => handler, NullLoggerFactory.Instance); IConfigurationRoot configuration = configurationBuilder.Build(); ConfigServerConfigurationProvider provider = configuration.Providers.OfType().Single(); @@ -334,7 +334,7 @@ public void OnSettingsChanged_reschedules_reload_timer_when_polling_interval_cha var configurationBuilder = new ConfigurationBuilder(); configurationBuilder.AddInMemoryAppSettingsJsonFile(fileProvider); - configurationBuilder.AddConfigServer(new ConfigServerClientOptions(), handler, NullLoggerFactory.Instance); + configurationBuilder.AddConfigServer(new ConfigServerClientOptions(), () => handler, NullLoggerFactory.Instance); IConfigurationRoot configuration = configurationBuilder.Build(); ConfigServerConfigurationProvider provider = configuration.Providers.OfType().Single(); @@ -398,7 +398,7 @@ public void OnSettingsChanged_stops_vault_renew_timer_when_renewal_becomes_disab var configurationBuilder = new ConfigurationBuilder(); configurationBuilder.AddInMemoryAppSettingsJsonFile(fileProvider); - configurationBuilder.AddConfigServer(new ConfigServerClientOptions(), handler, NullLoggerFactory.Instance); + configurationBuilder.AddConfigServer(new ConfigServerClientOptions(), () => handler, NullLoggerFactory.Instance); IConfigurationRoot configuration = configurationBuilder.Build(); ConfigServerConfigurationProvider provider = configuration.Providers.OfType().Single(); @@ -459,7 +459,7 @@ public async Task Load_MultipleConfigServers_SocketError_FallsBackToNextServer() Uri = "http://server1:8888, http://server2:8888" }; - using var provider = new ConfigServerConfigurationProvider(options, null, handler, NullLoggerFactory.Instance); + using var provider = new ConfigServerConfigurationProvider(options, null, () => handler, NullLoggerFactory.Instance); await provider.LoadInternalAsync(provider.ClientOptions, true, TestContext.Current.CancellationToken); @@ -496,7 +496,7 @@ public async Task DoLoad_IdenticalData_DoesNotTriggerReload() Name = "myName" }; - using var provider = new ConfigServerConfigurationProvider(options, null, handler, NullLoggerFactory.Instance); + using var provider = new ConfigServerConfigurationProvider(options, null, () => handler, NullLoggerFactory.Instance); await provider.DoLoadAsync(provider.ClientOptions, true, TestContext.Current.CancellationToken); provider.TryGet("key1", out string? value).Should().BeTrue(); @@ -557,7 +557,7 @@ public async Task DoLoad_MultipleLabels_ChecksAllLabels() options.Label = "label,test-label"; using var httpClientHandler = new ForwardingHttpClientHandler(server.CreateHandler()); - using var provider = new ConfigServerConfigurationProvider(options, null, httpClientHandler, NullLoggerFactory.Instance); + using var provider = new ConfigServerConfigurationProvider(options, null, () => httpClientHandler, NullLoggerFactory.Instance); await provider.DoLoadAsync(provider.ClientOptions, true, TestContext.Current.CancellationToken); @@ -601,7 +601,7 @@ public async Task RemoteLoadAsync_ConfigServerReturnsGood() ConfigServerClientOptions options = GetCommonOptions(); using var httpClientHandler = new ForwardingHttpClientHandler(server.CreateHandler()); - using var provider = new ConfigServerConfigurationProvider(options, null, httpClientHandler, NullLoggerFactory.Instance); + using var provider = new ConfigServerConfigurationProvider(options, null, () => httpClientHandler, NullLoggerFactory.Instance); ConfigEnvironment? env = await provider.RemoteLoadAsync(provider.ClientOptions, options.GetUris(), null, TestContext.Current.CancellationToken); @@ -642,7 +642,7 @@ public async Task Load_MultipleConfigServers_ReturnsGreaterThanEqualBadRequest_S ConfigServerClientOptions options = GetCommonOptions(); options.Uri = "http://localhost:8888, http://localhost:8888"; using var httpClientHandler = new ForwardingHttpClientHandler(server.CreateHandler()); - using var provider = new ConfigServerConfigurationProvider(options, null, httpClientHandler, NullLoggerFactory.Instance); + using var provider = new ConfigServerConfigurationProvider(options, null, () => httpClientHandler, NullLoggerFactory.Instance); await provider.LoadInternalAsync(provider.ClientOptions, true, TestContext.Current.CancellationToken); @@ -677,7 +677,7 @@ public async Task Load_MultipleConfigServers_ReturnsNotFoundStatus_DoesNotContin options.Uri = "http://localhost:8888, http://localhost:8888"; using var httpClientHandler = new ForwardingHttpClientHandler(server.CreateHandler()); - using var provider = new ConfigServerConfigurationProvider(options, null, httpClientHandler, NullLoggerFactory.Instance); + using var provider = new ConfigServerConfigurationProvider(options, null, () => httpClientHandler, NullLoggerFactory.Instance); await provider.LoadInternalAsync(provider.ClientOptions, true, TestContext.Current.CancellationToken); @@ -705,7 +705,7 @@ public async Task Load_ConfigServerReturnsNotFoundStatus() ConfigServerClientOptions options = GetCommonOptions(); using var httpClientHandler = new ForwardingHttpClientHandler(server.CreateHandler()); - using var provider = new ConfigServerConfigurationProvider(options, null, httpClientHandler, NullLoggerFactory.Instance); + using var provider = new ConfigServerConfigurationProvider(options, null, () => httpClientHandler, NullLoggerFactory.Instance); await provider.LoadInternalAsync(provider.ClientOptions, true, TestContext.Current.CancellationToken); @@ -731,7 +731,7 @@ public async Task Load_ConfigServerReturnsNotFoundStatus_FailFastEnabled() options.FailFast = true; using var httpClientHandler = new ForwardingHttpClientHandler(server.CreateHandler()); - using var provider = new ConfigServerConfigurationProvider(options, null, httpClientHandler, NullLoggerFactory.Instance); + using var provider = new ConfigServerConfigurationProvider(options, null, () => httpClientHandler, NullLoggerFactory.Instance); // ReSharper disable once AccessToDisposedClosure Func action = async () => await provider.LoadInternalAsync(provider.ClientOptions, true, TestContext.Current.CancellationToken); @@ -756,7 +756,7 @@ public async Task Load_MultipleConfigServers_ReturnsNotFoundStatus__DoesNotConti options.Uri = "http://localhost:8888,http://localhost:8888"; using var httpClientHandler = new ForwardingHttpClientHandler(server.CreateHandler()); - using var provider = new ConfigServerConfigurationProvider(options, null, httpClientHandler, NullLoggerFactory.Instance); + using var provider = new ConfigServerConfigurationProvider(options, null, () => httpClientHandler, NullLoggerFactory.Instance); startup.Reset(); @@ -795,7 +795,7 @@ public async Task Load_UriInvalid_FailFastEnabled() options.FailFast = true; using var httpClientHandler = new ForwardingHttpClientHandler(server.CreateHandler()); - using var provider = new ConfigServerConfigurationProvider(options, null, httpClientHandler, NullLoggerFactory.Instance); + using var provider = new ConfigServerConfigurationProvider(options, null, () => httpClientHandler, NullLoggerFactory.Instance); // ReSharper disable once AccessToDisposedClosure Func action = async () => await provider.LoadInternalAsync(provider.ClientOptions, true, TestContext.Current.CancellationToken); @@ -820,7 +820,7 @@ public async Task Load_ConfigServerReturnsBadStatus_FailFastEnabled() options.FailFast = true; using var httpClientHandler = new ForwardingHttpClientHandler(server.CreateHandler()); - using var provider = new ConfigServerConfigurationProvider(options, null, httpClientHandler, NullLoggerFactory.Instance); + using var provider = new ConfigServerConfigurationProvider(options, null, () => httpClientHandler, NullLoggerFactory.Instance); // ReSharper disable once AccessToDisposedClosure Func action = async () => await provider.LoadInternalAsync(provider.ClientOptions, true, TestContext.Current.CancellationToken); @@ -852,7 +852,7 @@ public async Task Load_MultipleConfigServers_ReturnsBadStatus_StopsChecking_Fail options.Uri = "http://localhost:8888, http://localhost:8888, http://localhost:8888"; using var httpClientHandler = new ForwardingHttpClientHandler(server.CreateHandler()); - using var provider = new ConfigServerConfigurationProvider(options, null, httpClientHandler, NullLoggerFactory.Instance); + using var provider = new ConfigServerConfigurationProvider(options, null, () => httpClientHandler, NullLoggerFactory.Instance); // ReSharper disable once AccessToDisposedClosure Func action = async () => await provider.LoadInternalAsync(provider.ClientOptions, true, TestContext.Current.CancellationToken); @@ -893,7 +893,7 @@ await TestFailureTracer.CaptureAsync(async tracer => }; using var httpClientHandler = new ForwardingHttpClientHandler(server.CreateHandler()); - using var provider = new ConfigServerConfigurationProvider(options, null, httpClientHandler, tracer.LoggerFactory); + using var provider = new ConfigServerConfigurationProvider(options, null, () => httpClientHandler, tracer.LoggerFactory); // ReSharper disable once AccessToDisposedClosure Func action = async () => await provider.LoadInternalAsync(provider.ClientOptions, true, TestContext.Current.CancellationToken); @@ -942,7 +942,7 @@ public async Task Load_ChangesDataDictionary() ConfigServerClientOptions options = GetCommonOptions(); using var httpClientHandler = new ForwardingHttpClientHandler(server.CreateHandler()); - using var provider = new ConfigServerConfigurationProvider(options, null, httpClientHandler, NullLoggerFactory.Instance); + using var provider = new ConfigServerConfigurationProvider(options, null, () => httpClientHandler, NullLoggerFactory.Instance); await provider.LoadInternalAsync(provider.ClientOptions, true, TestContext.Current.CancellationToken); @@ -997,7 +997,7 @@ public async Task ReLoad_DataDictionary_With_New_Configurations() ConfigServerClientOptions options = GetCommonOptions(); using var httpClientHandler = new ForwardingHttpClientHandler(server.CreateHandler()); - using var provider = new ConfigServerConfigurationProvider(options, null, httpClientHandler, NullLoggerFactory.Instance); + using var provider = new ConfigServerConfigurationProvider(options, null, () => httpClientHandler, NullLoggerFactory.Instance); provider.Load(); @@ -1161,7 +1161,7 @@ public async Task Reload_And_Bind_Without_Throwing_Exception() server.BaseAddress = new Uri(clientOptions.Uri!); using var httpClientHandler = new ForwardingHttpClientHandler(server.CreateHandler()); - using var provider = new ConfigServerConfigurationProvider(clientOptions, null, httpClientHandler, NullLoggerFactory.Instance); + using var provider = new ConfigServerConfigurationProvider(clientOptions, null, () => httpClientHandler, NullLoggerFactory.Instance); var configurationBuilder = new ConfigurationBuilder(); configurationBuilder.Add(new TestConfigServerConfigurationSource(provider)); diff --git a/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationProviderTest.cs b/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationProviderTest.cs index ae1f8e35a5..74b30a7213 100644 --- a/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationProviderTest.cs +++ b/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationProviderTest.cs @@ -247,7 +247,7 @@ public async Task GetRequestMessage_AddsBearerToken_WhenAccessTokenUriIsSet() } """); - using var provider = new ConfigServerConfigurationProvider(options, null, handler, NullLoggerFactory.Instance); + using var provider = new ConfigServerConfigurationProvider(options, null, () => handler, NullLoggerFactory.Instance); Uri requestUri = provider.BuildConfigServerUri(provider.ClientOptions, new Uri(options.Uri), null); HttpRequestMessage request = await provider.GetRequestMessageAsync(provider.ClientOptions, requestUri, TestContext.Current.CancellationToken); @@ -274,7 +274,7 @@ public async Task RefreshVaultToken_Succeeds() handler.Mock.Expect(HttpMethod.Post, "http://localhost:8888/vault/v1/auth/token/renew-self").WithHeaders("X-Vault-Token", "MyVaultToken") .WithContent("{\"increment\":300}").Respond(HttpStatusCode.NoContent); - using var provider = new ConfigServerConfigurationProvider(options, null, handler, NullLoggerFactory.Instance); + using var provider = new ConfigServerConfigurationProvider(options, null, () => handler, NullLoggerFactory.Instance); await provider.RefreshVaultTokenAsync(provider.ClientOptions, TestContext.Current.CancellationToken); handler.Mock.VerifyNoOutstandingExpectation(); @@ -301,7 +301,7 @@ public async Task RefreshVaultToken_With_AccessTokenUri_Succeeds() handler.Mock.Expect(HttpMethod.Post, "http://localhost:8888/vault/v1/auth/token/renew-self").WithHeaders("X-Vault-Token", "MyVaultToken") .WithHeaders("Authorization", "Bearer secret").WithContent("{\"increment\":300}").Respond(HttpStatusCode.NoContent); - using var provider = new ConfigServerConfigurationProvider(options, null, handler, NullLoggerFactory.Instance); + using var provider = new ConfigServerConfigurationProvider(options, null, () => handler, NullLoggerFactory.Instance); await provider.RefreshVaultTokenAsync(provider.ClientOptions, TestContext.Current.CancellationToken); handler.Mock.VerifyNoOutstandingExpectation(); From 90dfa08a0720572b348dc749687a02911296818c Mon Sep 17 00:00:00 2001 From: Bart Koelman <104792814+bart-vmware@users.noreply.github.com> Date: Sat, 28 Mar 2026 17:27:41 +0100 Subject: [PATCH 16/29] Fix threadpool starvation during retries --- .../src/ConfigServer/ConfigServerConfigurationProvider.cs | 2 +- .../src/ConfigServer/ConfigServerDiscoveryService.cs | 2 +- src/Discovery/src/Consul/Registry/ConsulServiceRegistrar.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Configuration/src/ConfigServer/ConfigServerConfigurationProvider.cs b/src/Configuration/src/ConfigServer/ConfigServerConfigurationProvider.cs index e181ed3507..6943fcdf0b 100644 --- a/src/Configuration/src/ConfigServer/ConfigServerConfigurationProvider.cs +++ b/src/Configuration/src/ConfigServer/ConfigServerConfigurationProvider.cs @@ -328,7 +328,7 @@ public override void Load() if (attempts < optionsSnapshot.Retry.MaxAttempts) { - Thread.CurrentThread.Join(backOff); + await Task.Delay(backOff, cancellationToken); int nextBackOff = (int)(backOff * optionsSnapshot.Retry.Multiplier); backOff = Math.Min(nextBackOff, optionsSnapshot.Retry.MaxInterval); } diff --git a/src/Configuration/src/ConfigServer/ConfigServerDiscoveryService.cs b/src/Configuration/src/ConfigServer/ConfigServerDiscoveryService.cs index f61065b44d..135f7c50c6 100644 --- a/src/Configuration/src/ConfigServer/ConfigServerDiscoveryService.cs +++ b/src/Configuration/src/ConfigServer/ConfigServerDiscoveryService.cs @@ -164,7 +164,7 @@ internal async Task> GetConfigServerInstancesAsync(Config if (attempts <= optionsSnapshot.Retry.MaxAttempts) { - Thread.CurrentThread.Join(backOff); + await Task.Delay(backOff, cancellationToken); int nextBackOff = (int)(backOff * optionsSnapshot.Retry.Multiplier); backOff = Math.Min(nextBackOff, optionsSnapshot.Retry.MaxInterval); } diff --git a/src/Discovery/src/Consul/Registry/ConsulServiceRegistrar.cs b/src/Discovery/src/Consul/Registry/ConsulServiceRegistrar.cs index ef0d9d392e..17b8c93391 100644 --- a/src/Discovery/src/Consul/Registry/ConsulServiceRegistrar.cs +++ b/src/Discovery/src/Consul/Registry/ConsulServiceRegistrar.cs @@ -136,7 +136,7 @@ private async Task DoWithRetryAsync(Func retryable, Con if (attempts < options.MaxAttempts) { LogStartingRetry(exception, attempts); - Thread.CurrentThread.Join(backOff); + await Task.Delay(backOff, cancellationToken); int nextBackOff = (int)(backOff * options.Multiplier); backOff = Math.Min(nextBackOff, options.MaxInterval); } From 76f21e98749554742e9d208649143fdfddf22593 Mon Sep 17 00:00:00 2001 From: Bart Koelman <104792814+bart-vmware@users.noreply.github.com> Date: Sat, 28 Mar 2026 17:43:00 +0100 Subject: [PATCH 17/29] Fix timer callbacks potentially using stale options when settings change --- .../src/ConfigServer/ConfigServerConfigurationProvider.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Configuration/src/ConfigServer/ConfigServerConfigurationProvider.cs b/src/Configuration/src/ConfigServer/ConfigServerConfigurationProvider.cs index 6943fcdf0b..1d7aa056d2 100644 --- a/src/Configuration/src/ConfigServer/ConfigServerConfigurationProvider.cs +++ b/src/Configuration/src/ConfigServer/ConfigServerConfigurationProvider.cs @@ -134,10 +134,10 @@ private void OnSettingsChanged() TimeSpan previousPollingInterval = _clientOptions.PollingInterval; int previousTokenRenewRate = _clientOptions.TokenRenewRate; + _clientOptions = newOptions; + UpdateConfigurationReloadTimer(newOptions, previousPollingInterval); UpdateVaultRenewTimer(newOptions, previousTokenRenewRate); - - _clientOptions = newOptions; } } From 7edbee589ffcd57c8b46a455426df1af2704e7c9 Mon Sep 17 00:00:00 2001 From: Bart Koelman <104792814+bart-vmware@users.noreply.github.com> Date: Sat, 28 Mar 2026 18:39:31 +0100 Subject: [PATCH 18/29] Fix stale reads in ConfigServerDiscoveryService --- .../ConfigServerDiscoveryService.cs | 32 +++++++++++-------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/src/Configuration/src/ConfigServer/ConfigServerDiscoveryService.cs b/src/Configuration/src/ConfigServer/ConfigServerDiscoveryService.cs index 135f7c50c6..595fa993cb 100644 --- a/src/Configuration/src/ConfigServer/ConfigServerDiscoveryService.cs +++ b/src/Configuration/src/ConfigServer/ConfigServerDiscoveryService.cs @@ -30,8 +30,13 @@ internal sealed partial class ConfigServerDiscoveryService private readonly ILogger _logger; private readonly LockPrimitive _initLock = new(); private ServiceProvider? _temporaryServiceProviderForDiscoveryClients; + private volatile ICollection? _discoveryClients; - internal ICollection? DiscoveryClients { get; private set; } + internal ICollection? DiscoveryClients + { + get => _discoveryClients; + private set => _discoveryClients = value; + } public ConfigServerDiscoveryService(IConfiguration configuration, ILoggerFactory loggerFactory) { @@ -45,18 +50,19 @@ public ConfigServerDiscoveryService(IConfiguration configuration, ILoggerFactory private void EnsureInitialized() { - if (DiscoveryClients != null) - { - return; - } - - lock (_initLock) + if (_discoveryClients == null) { - DiscoveryClients ??= SetupDiscoveryClients(); + lock (_initLock) + { + if (_discoveryClients == null) + { + SetupDiscoveryClients(); + } + } } } - private IDiscoveryClient[] SetupDiscoveryClients() + private void SetupDiscoveryClients() { var tempServices = new ServiceCollection(); tempServices.AddSingleton(_loggerFactory); @@ -88,7 +94,7 @@ private IDiscoveryClient[] SetupDiscoveryClients() WireEurekaDiscoveryClient(tempServices); } - return GetDiscoveryClientsFromServiceCollection(tempServices); + _discoveryClients = GetDiscoveryClientsFromServiceCollection(tempServices); } [MethodImpl(MethodImplOptions.NoInlining)] @@ -137,9 +143,9 @@ internal async Task> GetConfigServerInstancesAsync(Config { LogLocatingConfigServer(optionsSnapshot.Discovery.ServiceId); - if (optionsSnapshot.Discovery.ServiceId != null && DiscoveryClients != null) + if (optionsSnapshot.Discovery.ServiceId != null) { - foreach (IDiscoveryClient discoveryClient in DiscoveryClients) + foreach (IDiscoveryClient discoveryClient in _discoveryClients ?? []) { try { @@ -193,7 +199,7 @@ internal async Task ShutdownAsync(CancellationToken cancellationToken) { if (_temporaryServiceProviderForDiscoveryClients != null) { - foreach (IDiscoveryClient discoveryClient in DiscoveryClients ?? []) + foreach (IDiscoveryClient discoveryClient in _discoveryClients ?? []) { await discoveryClient.ShutdownAsync(cancellationToken); } From 4f7de6f0a9dbd2fa8595a6ba030c3da5126d7c03 Mon Sep 17 00:00:00 2001 From: Bart Koelman <104792814+bart-vmware@users.noreply.github.com> Date: Tue, 31 Mar 2026 12:31:18 +0200 Subject: [PATCH 19/29] Add overloads to post-configure Config Server options --- .../src/AutoConfiguration/BootstrapScanner.cs | 2 +- .../ConfigServer/ConfigServerClientOptions.cs | 2 +- ...figServerConfigurationBuilderExtensions.cs | 69 ++++++++-- .../ConfigServerConfigurationProvider.cs | 6 +- .../ConfigServerConfigurationSource.cs | 48 ++++--- .../ConfigServerHostBuilderExtensions.cs | 127 +++++++++++++++++- ...ConfigServerServiceCollectionExtensions.cs | 48 ++++++- .../ConfigureConfigServerClientOptions.cs | 7 +- .../HostBuilderWrapperExtensions.cs | 33 ++--- .../src/ConfigServer/PublicAPI.Unshipped.txt | 11 ++ .../ConfigServerClientOptionsTest.cs | 26 +++- .../ConfigServerClientOptionsTest.cs | 22 ++- ...erverConfigurationBuilderExtensionsTest.cs | 18 ++- ...ServerConfigurationProviderTest.Loading.cs | 103 +++++++++----- ...erverConfigurationProviderTest.Settings.cs | 33 ++--- .../ConfigServerConfigurationProviderTest.cs | 40 +++--- .../ConfigServerConfigurationSourceTest.cs | 6 +- .../ConfigServerHostBuilderExtensionsTest.cs | 8 +- .../ConfigServerHostedServiceTest.cs | 2 +- .../test/ConfigServer.Test/TestHelper.cs | 4 +- 20 files changed, 464 insertions(+), 151 deletions(-) diff --git a/src/Bootstrap/src/AutoConfiguration/BootstrapScanner.cs b/src/Bootstrap/src/AutoConfiguration/BootstrapScanner.cs index a3c471e64d..5b58a286a8 100644 --- a/src/Bootstrap/src/AutoConfiguration/BootstrapScanner.cs +++ b/src/Bootstrap/src/AutoConfiguration/BootstrapScanner.cs @@ -91,7 +91,7 @@ public void ConfigureSteeltoe() private void WireConfigServer() { - _wrapper.AddConfigServer(_loggerFactory); + _wrapper.AddConfigServer(null, _loggerFactory); LogConfigServerConfigured(); } diff --git a/src/Configuration/src/ConfigServer/ConfigServerClientOptions.cs b/src/Configuration/src/ConfigServer/ConfigServerClientOptions.cs index 7a7565d999..57de14eefe 100644 --- a/src/Configuration/src/ConfigServer/ConfigServerClientOptions.cs +++ b/src/Configuration/src/ConfigServer/ConfigServerClientOptions.cs @@ -39,7 +39,7 @@ public sealed class ConfigServerClientOptions : IValidateCertificatesOptions /// Gets or sets a comma-separated list of environments used when accessing configuration data. Default value: "Production". /// [ConfigurationKeyName("Env")] - public string? Environment { get; set; } = "Production"; + public string? Environment { get; set; } /// /// Gets or sets a comma-separated list of labels to request from the server. diff --git a/src/Configuration/src/ConfigServer/ConfigServerConfigurationBuilderExtensions.cs b/src/Configuration/src/ConfigServer/ConfigServerConfigurationBuilderExtensions.cs index cd4b788391..1e39433696 100644 --- a/src/Configuration/src/ConfigServer/ConfigServerConfigurationBuilderExtensions.cs +++ b/src/Configuration/src/ConfigServer/ConfigServerConfigurationBuilderExtensions.cs @@ -26,7 +26,7 @@ public static class ConfigServerConfigurationBuilderExtensions /// public static IConfigurationBuilder AddConfigServer(this IConfigurationBuilder builder) { - return AddConfigServer(builder, NullLoggerFactory.Instance); + return AddConfigServer(builder, new ConfigServerClientOptions(), null, null, NullLoggerFactory.Instance); } /// @@ -43,9 +43,24 @@ public static IConfigurationBuilder AddConfigServer(this IConfigurationBuilder b /// public static IConfigurationBuilder AddConfigServer(this IConfigurationBuilder builder, ILoggerFactory loggerFactory) { - var options = new ConfigServerClientOptions(); + return AddConfigServer(builder, new ConfigServerClientOptions(), null, null, loggerFactory); + } - return AddConfigServer(builder, options, loggerFactory); + /// + /// Adds a configuration source for Config Server to the . + /// + /// + /// The to add configuration to. + /// + /// + /// The initial options, whose values are overruled from . + /// + /// + /// The incoming so that additional calls can be chained. + /// + public static IConfigurationBuilder AddConfigServer(this IConfigurationBuilder builder, ConfigServerClientOptions options) + { + return AddConfigServer(builder, options, null, null, NullLoggerFactory.Instance); } /// @@ -55,7 +70,7 @@ public static IConfigurationBuilder AddConfigServer(this IConfigurationBuilder b /// The to add configuration to. /// /// - /// Enables configuring Config Server from code. + /// The initial options, whose values are overruled from . /// /// /// Used for internal logging. Pass to disable logging. @@ -65,11 +80,49 @@ public static IConfigurationBuilder AddConfigServer(this IConfigurationBuilder b /// public static IConfigurationBuilder AddConfigServer(this IConfigurationBuilder builder, ConfigServerClientOptions options, ILoggerFactory loggerFactory) { - return AddConfigServer(builder, options, null, loggerFactory); + return AddConfigServer(builder, options, null, null, loggerFactory); + } + + /// + /// Adds a configuration source for Config Server to the . + /// + /// + /// The to add configuration to. + /// + /// + /// An optional delegate that further configures options from code, after settings from have been applied. + /// + /// + /// The incoming so that additional calls can be chained. + /// + public static IConfigurationBuilder AddConfigServer(this IConfigurationBuilder builder, Action? configure) + { + return AddConfigServer(builder, new ConfigServerClientOptions(), configure, null, NullLoggerFactory.Instance); + } + + /// + /// Adds a configuration source for Config Server to the . + /// + /// + /// The to add configuration to. + /// + /// + /// An optional delegate that further configures options from code, after settings from have been applied. + /// + /// + /// Used for internal logging. Pass to disable logging. + /// + /// + /// The incoming so that additional calls can be chained. + /// + public static IConfigurationBuilder AddConfigServer(this IConfigurationBuilder builder, Action? configure, + ILoggerFactory loggerFactory) + { + return AddConfigServer(builder, new ConfigServerClientOptions(), configure, null, loggerFactory); } internal static IConfigurationBuilder AddConfigServer(this IConfigurationBuilder builder, ConfigServerClientOptions options, - Func? createHttpClientHandler, ILoggerFactory loggerFactory) + Action? configure, Func? createHttpClientHandler, ILoggerFactory loggerFactory) { ArgumentNullException.ThrowIfNull(builder); ArgumentNullException.ThrowIfNull(options); @@ -81,8 +134,8 @@ internal static IConfigurationBuilder AddConfigServer(this IConfigurationBuilder builder.AddKubernetesServiceBindings(); ConfigServerConfigurationSource source = builder is IConfiguration configuration - ? new ConfigServerConfigurationSource(options, configuration, createHttpClientHandler, loggerFactory) - : new ConfigServerConfigurationSource(options, builder.Sources, builder.Properties, createHttpClientHandler, loggerFactory); + ? new ConfigServerConfigurationSource(options, configuration, configure, createHttpClientHandler, loggerFactory) + : new ConfigServerConfigurationSource(options, builder.Sources, builder.Properties, configure, createHttpClientHandler, loggerFactory); builder.Add(source); } diff --git a/src/Configuration/src/ConfigServer/ConfigServerConfigurationProvider.cs b/src/Configuration/src/ConfigServer/ConfigServerConfigurationProvider.cs index 1d7aa056d2..21747d5e44 100644 --- a/src/Configuration/src/ConfigServer/ConfigServerConfigurationProvider.cs +++ b/src/Configuration/src/ConfigServer/ConfigServerConfigurationProvider.cs @@ -85,12 +85,12 @@ internal sealed partial class ConfigServerConfigurationProvider : ConfigurationP /// Used for internal logging. Pass to disable logging. /// public ConfigServerConfigurationProvider(ConfigServerConfigurationSource source, ILoggerFactory loggerFactory) - : this(source.DefaultOptions, source.Configuration, source.CreateHttpClientHandler, loggerFactory) + : this(source.DefaultOptions, source.Configuration, source.Configure, source.CreateHttpClientHandler, loggerFactory) { } internal ConfigServerConfigurationProvider(ConfigServerClientOptions clientOptions, IConfiguration? configuration, - Func? createHttpClientHandler, ILoggerFactory loggerFactory) + Action? configure, Func? createHttpClientHandler, ILoggerFactory loggerFactory) { ArgumentNullException.ThrowIfNull(clientOptions); ArgumentNullException.ThrowIfNull(loggerFactory); @@ -98,7 +98,7 @@ internal ConfigServerConfigurationProvider(ConfigServerClientOptions clientOptio _logger = loggerFactory.CreateLogger(); _shutdownToken = _shutdownTokenSource.Token; IConfiguration effectiveConfiguration = configuration ?? new ConfigurationBuilder().Build(); - _configurer = new ConfigureConfigServerClientOptions(effectiveConfiguration); + _configurer = new ConfigureConfigServerClientOptions(effectiveConfiguration, configure); _configServerDiscoveryService = new ConfigServerDiscoveryService(effectiveConfiguration, loggerFactory); _initialOptions = clientOptions.Clone(); diff --git a/src/Configuration/src/ConfigServer/ConfigServerConfigurationSource.cs b/src/Configuration/src/ConfigServer/ConfigServerConfigurationSource.cs index 6eb405bc3c..e047337a6f 100644 --- a/src/Configuration/src/ConfigServer/ConfigServerConfigurationSource.cs +++ b/src/Configuration/src/ConfigServer/ConfigServerConfigurationSource.cs @@ -16,30 +16,37 @@ internal sealed class ConfigServerConfigurationSource : IConfigurationSource internal Dictionary Properties { get; } = []; /// - /// Gets the default settings the Config Server client uses to contact the Config Server. + /// Gets the initial options the client uses to contact Config Server. /// - internal ConfigServerClientOptions DefaultOptions { get; } + public ConfigServerClientOptions DefaultOptions { get; } /// - /// Gets an optional factory to create the HTTP client handler, used to mock HTTP requests to Config Server in tests. When provided, the caller is - /// responsible for handler disposal. + /// Gets the configuration the client uses to contact Config Server. Entries overrule . /// - internal Func? CreateHttpClientHandler { get; } + public IConfiguration? Configuration { get; private set; } /// - /// Gets the configuration the Config Server client uses to contact the Config Server. Values returned override the default values provided in - /// . + /// Gets an optional delegate that further configures options from code, after settings from have been applied. /// - internal IConfiguration? Configuration { get; private set; } + public Action? Configure { get; } + + /// + /// Gets an optional factory to create the HTTP client handler, used to mock HTTP requests to Config Server in tests. When provided, the caller is + /// responsible for handler disposal. + /// + public Func? CreateHttpClientHandler { get; } /// /// Initializes a new instance of the class. /// /// - /// the default settings used by the Config Server client. + /// The initial options the client uses to contact Config Server. /// /// - /// configuration used by the Config Server client. Values will override those found in default settings. + /// The configuration the client uses to contact Config Server. Entries overrule . + /// + /// + /// An optional delegate that further configures options from code, after settings from have been applied. /// /// /// An optional factory to create the HTTP client handler, used to mock HTTP requests to Config Server in tests. When provided, the caller is responsible @@ -48,14 +55,15 @@ internal sealed class ConfigServerConfigurationSource : IConfigurationSource /// /// Used for internal logging. Pass to disable logging. /// - public ConfigServerConfigurationSource(ConfigServerClientOptions defaultOptions, IConfiguration configuration, + public ConfigServerConfigurationSource(ConfigServerClientOptions defaultOptions, IConfiguration configuration, Action? configure, Func? createHttpClientHandler, ILoggerFactory loggerFactory) { - ArgumentNullException.ThrowIfNull(configuration); ArgumentNullException.ThrowIfNull(defaultOptions); + ArgumentNullException.ThrowIfNull(configuration); ArgumentNullException.ThrowIfNull(loggerFactory); DefaultOptions = defaultOptions; + Configure = configure; CreateHttpClientHandler = createHttpClientHandler; Configuration = configuration; _loggerFactory = loggerFactory; @@ -65,14 +73,18 @@ public ConfigServerConfigurationSource(ConfigServerClientOptions defaultOptions, /// Initializes a new instance of the class. /// /// - /// the default settings used by the Config Server client. + /// The initial options the client uses to contact Config Server. /// /// - /// configuration sources used by the Config Server client. The will be built from these sources and the values will - /// override those found in . + /// Configuration sources the client uses to contact Config Server. The will be built from these, whose entries overrule + /// . /// /// - /// properties to be used when sources are built. + /// Configuration properties the client uses to contact Config Server. The will be built from these, whose entries overrule + /// . + /// + /// + /// An optional delegate that further configures options from code, after settings from the built have been applied. /// /// /// An optional factory to create the HTTP client handler, used to mock HTTP requests to Config Server in tests. When provided, the caller is responsible @@ -82,7 +94,8 @@ public ConfigServerConfigurationSource(ConfigServerClientOptions defaultOptions, /// Used for internal logging. Pass to disable logging. /// public ConfigServerConfigurationSource(ConfigServerClientOptions defaultOptions, IList sources, - IDictionary? properties, Func? createHttpClientHandler, ILoggerFactory loggerFactory) + IDictionary? properties, Action? configure, Func? createHttpClientHandler, + ILoggerFactory loggerFactory) { ArgumentNullException.ThrowIfNull(defaultOptions); ArgumentNullException.ThrowIfNull(sources); @@ -96,6 +109,7 @@ public ConfigServerConfigurationSource(ConfigServerClientOptions defaultOptions, } DefaultOptions = defaultOptions; + Configure = configure; CreateHttpClientHandler = createHttpClientHandler; _loggerFactory = loggerFactory; } diff --git a/src/Configuration/src/ConfigServer/ConfigServerHostBuilderExtensions.cs b/src/Configuration/src/ConfigServer/ConfigServerHostBuilderExtensions.cs index bf3e0d0337..ab3dde3d9a 100644 --- a/src/Configuration/src/ConfigServer/ConfigServerHostBuilderExtensions.cs +++ b/src/Configuration/src/ConfigServer/ConfigServerHostBuilderExtensions.cs @@ -3,6 +3,7 @@ // See the LICENSE file in the project root for more information. using Microsoft.AspNetCore.Hosting; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Abstractions; @@ -23,7 +24,7 @@ public static class ConfigServerHostBuilderExtensions /// public static IWebHostBuilder AddConfigServer(this IWebHostBuilder builder) { - return AddConfigServer(builder, NullLoggerFactory.Instance); + return AddConfigServer(builder, null, NullLoggerFactory.Instance); } /// @@ -39,12 +40,49 @@ public static IWebHostBuilder AddConfigServer(this IWebHostBuilder builder) /// The incoming so that additional calls can be chained. /// public static IWebHostBuilder AddConfigServer(this IWebHostBuilder builder, ILoggerFactory loggerFactory) + { + return AddConfigServer(builder, null, loggerFactory); + } + + /// + /// Adds Config Server and Cloud Foundry as application configuration sources. Adds Config Server health check contributor to the service container. + /// + /// + /// The to configure. + /// + /// + /// An optional delegate that further configures options from code, after settings from have been applied. + /// + /// + /// The incoming so that additional calls can be chained. + /// + public static IWebHostBuilder AddConfigServer(this IWebHostBuilder builder, Action? configure) + { + return AddConfigServer(builder, configure, NullLoggerFactory.Instance); + } + + /// + /// Adds Config Server and Cloud Foundry as application configuration sources. Adds Config Server health check contributor to the service container. + /// + /// + /// The to configure. + /// + /// + /// An optional delegate that further configures options from code, after settings from have been applied. + /// + /// + /// Used for internal logging. Pass to disable logging. + /// + /// + /// The incoming so that additional calls can be chained. + /// + public static IWebHostBuilder AddConfigServer(this IWebHostBuilder builder, Action? configure, ILoggerFactory loggerFactory) { ArgumentNullException.ThrowIfNull(builder); ArgumentNullException.ThrowIfNull(loggerFactory); HostBuilderWrapper wrapper = HostBuilderWrapper.Wrap(builder); - wrapper.AddConfigServer(loggerFactory); + wrapper.AddConfigServer(configure, loggerFactory); return builder; } @@ -60,7 +98,7 @@ public static IWebHostBuilder AddConfigServer(this IWebHostBuilder builder, ILog /// public static IHostBuilder AddConfigServer(this IHostBuilder builder) { - return AddConfigServer(builder, NullLoggerFactory.Instance); + return AddConfigServer(builder, null, NullLoggerFactory.Instance); } /// @@ -76,12 +114,49 @@ public static IHostBuilder AddConfigServer(this IHostBuilder builder) /// The incoming so that additional calls can be chained. /// public static IHostBuilder AddConfigServer(this IHostBuilder builder, ILoggerFactory loggerFactory) + { + return AddConfigServer(builder, null, loggerFactory); + } + + /// + /// Adds Config Server and Cloud Foundry as application configuration sources. Adds Config Server health check contributor to the service container. + /// + /// + /// The to configure. + /// + /// + /// An optional delegate that further configures options from code, after settings from have been applied. + /// + /// + /// The incoming so that additional calls can be chained. + /// + public static IHostBuilder AddConfigServer(this IHostBuilder builder, Action? configure) + { + return AddConfigServer(builder, configure, NullLoggerFactory.Instance); + } + + /// + /// Adds Config Server and Cloud Foundry as application configuration sources. Adds Config Server health check contributor to the service container. + /// + /// + /// The to configure. + /// + /// + /// An optional delegate that further configures options from code, after settings from have been applied. + /// + /// + /// Used for internal logging. Pass to disable logging. + /// + /// + /// The incoming so that additional calls can be chained. + /// + public static IHostBuilder AddConfigServer(this IHostBuilder builder, Action? configure, ILoggerFactory loggerFactory) { ArgumentNullException.ThrowIfNull(builder); ArgumentNullException.ThrowIfNull(loggerFactory); HostBuilderWrapper wrapper = HostBuilderWrapper.Wrap(builder); - wrapper.AddConfigServer(loggerFactory); + wrapper.AddConfigServer(configure, loggerFactory); return builder; } @@ -98,7 +173,7 @@ public static IHostBuilder AddConfigServer(this IHostBuilder builder, ILoggerFac /// public static IHostApplicationBuilder AddConfigServer(this IHostApplicationBuilder builder) { - return AddConfigServer(builder, NullLoggerFactory.Instance); + return AddConfigServer(builder, null, NullLoggerFactory.Instance); } /// @@ -115,12 +190,52 @@ public static IHostApplicationBuilder AddConfigServer(this IHostApplicationBuild /// The incoming so that additional calls can be chained. /// public static IHostApplicationBuilder AddConfigServer(this IHostApplicationBuilder builder, ILoggerFactory loggerFactory) + { + return AddConfigServer(builder, null, loggerFactory); + } + + /// + /// Adds Config Server and Cloud Foundry as application configuration sources. Also adds Config Server health check contributor and related services to + /// the service container. + /// + /// + /// The to configure. + /// + /// + /// An optional delegate that further configures options from code, after settings from have been applied. + /// + /// + /// The incoming so that additional calls can be chained. + /// + public static IHostApplicationBuilder AddConfigServer(this IHostApplicationBuilder builder, Action? configure) + { + return AddConfigServer(builder, configure, NullLoggerFactory.Instance); + } + + /// + /// Adds Config Server and Cloud Foundry as application configuration sources. Also adds Config Server health check contributor and related services to + /// the service container. + /// + /// + /// The to configure. + /// + /// + /// An optional delegate that further configures options from code, after settings from have been applied. + /// + /// + /// Used for internal logging. Pass to disable logging. + /// + /// + /// The incoming so that additional calls can be chained. + /// + public static IHostApplicationBuilder AddConfigServer(this IHostApplicationBuilder builder, Action? configure, + ILoggerFactory loggerFactory) { ArgumentNullException.ThrowIfNull(builder); ArgumentNullException.ThrowIfNull(loggerFactory); HostBuilderWrapper wrapper = HostBuilderWrapper.Wrap(builder); - wrapper.AddConfigServer(loggerFactory); + wrapper.AddConfigServer(configure, loggerFactory); return builder; } diff --git a/src/Configuration/src/ConfigServer/ConfigServerServiceCollectionExtensions.cs b/src/Configuration/src/ConfigServer/ConfigServerServiceCollectionExtensions.cs index d40c2f686c..8c891be1cd 100644 --- a/src/Configuration/src/ConfigServer/ConfigServerServiceCollectionExtensions.cs +++ b/src/Configuration/src/ConfigServer/ConfigServerServiceCollectionExtensions.cs @@ -25,11 +25,37 @@ public static class ConfigServerServiceCollectionExtensions /// The incoming so that additional calls can be chained. /// public static IServiceCollection ConfigureConfigServerClientOptions(this IServiceCollection services) + { + return ConfigureConfigServerClientOptions(services, null); + } + + /// + /// Adds for use with the options pattern. + /// + /// + /// The to add services to. + /// + /// + /// An optional delegate that further configures options from code, after settings from have been applied. + /// + /// + /// The incoming so that additional calls can be chained. + /// + public static IServiceCollection ConfigureConfigServerClientOptions(this IServiceCollection services, Action? configure) { ArgumentNullException.ThrowIfNull(services); services.AddOptions(); - services.TryAddEnumerable(ServiceDescriptor.Singleton, ConfigureConfigServerClientOptions>()); + + services.AddSingleton(serviceProvider => + { + var configuration = serviceProvider.GetRequiredService(); + return new ConfigureConfigServerClientOptions(configuration, configure); + }); + + services.TryAddEnumerable( + ServiceDescriptor.Singleton, ConfigureConfigServerClientOptions>(serviceProvider => + serviceProvider.GetRequiredService())); services.TryAddEnumerable(ServiceDescriptor .Singleton, ConfigurationChangeTokenSource>()); @@ -67,10 +93,28 @@ public static IServiceCollection AddConfigServerHealthContributor(this IServiceC /// The incoming so that additional calls can be chained. /// public static IServiceCollection AddConfigServerServices(this IServiceCollection services) + { + return AddConfigServerServices(services, null); + } + + /// + /// Configures , hosted service and health contributor, and ensures is + /// available. + /// + /// + /// The to add services to. + /// + /// + /// An optional delegate that further configures options from code, after settings from have been applied. + /// + /// + /// The incoming so that additional calls can be chained. + /// + public static IServiceCollection AddConfigServerServices(this IServiceCollection services, Action? configure) { ArgumentNullException.ThrowIfNull(services); - services.ConfigureConfigServerClientOptions(); + services.ConfigureConfigServerClientOptions(configure); services.TryAddSingleton(serviceProvider => (IConfigurationRoot)serviceProvider.GetRequiredService()); services.AddHostedService(); services.AddConfigServerHealthContributor(); diff --git a/src/Configuration/src/ConfigServer/ConfigureConfigServerClientOptions.cs b/src/Configuration/src/ConfigServer/ConfigureConfigServerClientOptions.cs index c8fbe53507..d1855d5d15 100644 --- a/src/Configuration/src/ConfigServer/ConfigureConfigServerClientOptions.cs +++ b/src/Configuration/src/ConfigServer/ConfigureConfigServerClientOptions.cs @@ -17,12 +17,14 @@ internal sealed class ConfigureConfigServerClientOptions : IConfigureOptions? _configure; - public ConfigureConfigServerClientOptions(IConfiguration configuration) + public ConfigureConfigServerClientOptions(IConfiguration configuration, Action? configure) { ArgumentNullException.ThrowIfNull(configuration); _configuration = configuration; + _configure = configure; } public void Configure(ConfigServerClientOptions options) @@ -30,10 +32,13 @@ public void Configure(ConfigServerClientOptions options) ArgumentNullException.ThrowIfNull(options); _configuration.GetSection(ConfigServerClientOptions.ConfigurationPrefix).Bind(options); + _configure?.Invoke(options); + OverrideFromVcapServicesCredentials(options); ConfigureClientCertificate(options); options.Name ??= GetApplicationName(); + options.Environment ??= "Production"; } private void OverrideFromVcapServicesCredentials(ConfigServerClientOptions options) diff --git a/src/Configuration/src/ConfigServer/HostBuilderWrapperExtensions.cs b/src/Configuration/src/ConfigServer/HostBuilderWrapperExtensions.cs index 4709af0345..7c1c699ae2 100644 --- a/src/Configuration/src/ConfigServer/HostBuilderWrapperExtensions.cs +++ b/src/Configuration/src/ConfigServer/HostBuilderWrapperExtensions.cs @@ -10,36 +10,37 @@ namespace Steeltoe.Configuration.ConfigServer; internal static class HostBuilderWrapperExtensions { - public static HostBuilderWrapper AddConfigServer(this HostBuilderWrapper wrapper, ILoggerFactory loggerFactory) + public static HostBuilderWrapper AddConfigServer(this HostBuilderWrapper wrapper, Action? configure, + ILoggerFactory loggerFactory) { ArgumentNullException.ThrowIfNull(wrapper); ArgumentNullException.ThrowIfNull(loggerFactory); wrapper.ConfigureAppConfiguration((context, builder) => { - ConfigServerClientOptions options = CreateOptions(context.HostEnvironment); - builder.AddConfigServer(options, loggerFactory); + Action configureOptions = CreateOptionsConfigurer(configure, context.HostEnvironment); + builder.AddConfigServer(configureOptions, loggerFactory); }); - wrapper.ConfigureServices(services => services.AddConfigServerServices()); + wrapper.ConfigureServices((context, services) => + { + Action configureOptions = CreateOptionsConfigurer(configure, context.HostEnvironment); + services.AddConfigServerServices(configureOptions); + }); return wrapper; } - private static ConfigServerClientOptions CreateOptions(IHostEnvironment hostEnvironment) + private static Action CreateOptionsConfigurer(Action? configure, IHostEnvironment hostEnvironment) { - var options = new ConfigServerClientOptions(); - - if (!string.IsNullOrEmpty(hostEnvironment.EnvironmentName) && hostEnvironment.EnvironmentName != "Production") + return options => { - // Only take IHostEnvironment.EnvironmentName when it was explicitly set (it defaults to "Production"). - // In the default case, we want the various other ways of setting the environment name to kick in. - options.Environment = hostEnvironment.EnvironmentName; - } - - // Intentionally NOT taking hostEnvironment.ApplicationName here, because that would disable the various other ways of setting the application name. - // Ultimately, its value ends up in configuration key "applicationName", whose value is used if nothing else is configured. + configure?.Invoke(options); - return options; + if (!string.IsNullOrEmpty(hostEnvironment.EnvironmentName)) + { + options.Environment ??= hostEnvironment.EnvironmentName; + } + }; } } diff --git a/src/Configuration/src/ConfigServer/PublicAPI.Unshipped.txt b/src/Configuration/src/ConfigServer/PublicAPI.Unshipped.txt index 7dc5c58110..ee4a75b906 100644 --- a/src/Configuration/src/ConfigServer/PublicAPI.Unshipped.txt +++ b/src/Configuration/src/ConfigServer/PublicAPI.Unshipped.txt @@ -1 +1,12 @@ #nullable enable +static Steeltoe.Configuration.ConfigServer.ConfigServerConfigurationBuilderExtensions.AddConfigServer(this Microsoft.Extensions.Configuration.IConfigurationBuilder! builder, Steeltoe.Configuration.ConfigServer.ConfigServerClientOptions! options) -> Microsoft.Extensions.Configuration.IConfigurationBuilder! +static Steeltoe.Configuration.ConfigServer.ConfigServerConfigurationBuilderExtensions.AddConfigServer(this Microsoft.Extensions.Configuration.IConfigurationBuilder! builder, System.Action? configure) -> Microsoft.Extensions.Configuration.IConfigurationBuilder! +static Steeltoe.Configuration.ConfigServer.ConfigServerConfigurationBuilderExtensions.AddConfigServer(this Microsoft.Extensions.Configuration.IConfigurationBuilder! builder, System.Action? configure, Microsoft.Extensions.Logging.ILoggerFactory! loggerFactory) -> Microsoft.Extensions.Configuration.IConfigurationBuilder! +static Steeltoe.Configuration.ConfigServer.ConfigServerHostBuilderExtensions.AddConfigServer(this Microsoft.AspNetCore.Hosting.IWebHostBuilder! builder, System.Action? configure) -> Microsoft.AspNetCore.Hosting.IWebHostBuilder! +static Steeltoe.Configuration.ConfigServer.ConfigServerHostBuilderExtensions.AddConfigServer(this Microsoft.AspNetCore.Hosting.IWebHostBuilder! builder, System.Action? configure, Microsoft.Extensions.Logging.ILoggerFactory! loggerFactory) -> Microsoft.AspNetCore.Hosting.IWebHostBuilder! +static Steeltoe.Configuration.ConfigServer.ConfigServerHostBuilderExtensions.AddConfigServer(this Microsoft.Extensions.Hosting.IHostApplicationBuilder! builder, System.Action? configure) -> Microsoft.Extensions.Hosting.IHostApplicationBuilder! +static Steeltoe.Configuration.ConfigServer.ConfigServerHostBuilderExtensions.AddConfigServer(this Microsoft.Extensions.Hosting.IHostApplicationBuilder! builder, System.Action? configure, Microsoft.Extensions.Logging.ILoggerFactory! loggerFactory) -> Microsoft.Extensions.Hosting.IHostApplicationBuilder! +static Steeltoe.Configuration.ConfigServer.ConfigServerHostBuilderExtensions.AddConfigServer(this Microsoft.Extensions.Hosting.IHostBuilder! builder, System.Action? configure) -> Microsoft.Extensions.Hosting.IHostBuilder! +static Steeltoe.Configuration.ConfigServer.ConfigServerHostBuilderExtensions.AddConfigServer(this Microsoft.Extensions.Hosting.IHostBuilder! builder, System.Action? configure, Microsoft.Extensions.Logging.ILoggerFactory! loggerFactory) -> Microsoft.Extensions.Hosting.IHostBuilder! +static Steeltoe.Configuration.ConfigServer.ConfigServerServiceCollectionExtensions.AddConfigServerServices(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action? configure) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! +static Steeltoe.Configuration.ConfigServer.ConfigServerServiceCollectionExtensions.ConfigureConfigServerClientOptions(this Microsoft.Extensions.DependencyInjection.IServiceCollection! services, System.Action? configure) -> Microsoft.Extensions.DependencyInjection.IServiceCollection! diff --git a/src/Configuration/test/ConfigServer.Discovery.Test/ConfigServerClientOptionsTest.cs b/src/Configuration/test/ConfigServer.Discovery.Test/ConfigServerClientOptionsTest.cs index e3527ceaec..a63d19f06f 100644 --- a/src/Configuration/test/ConfigServer.Discovery.Test/ConfigServerClientOptionsTest.cs +++ b/src/Configuration/test/ConfigServer.Discovery.Test/ConfigServerClientOptionsTest.cs @@ -86,9 +86,12 @@ public void Config_Server_URI_is_resolved_from_discovery_and_survives_changes_in handler.Mock.Expect(HttpMethod.Get, "https://discovered-server.com:9999/internal/example-app-name/example-profile/example-label") .Respond("application/json", configServerResponseJson); + Action configureOptions = options => options.ValidateCertificates = false; + var configurationBuilder = new ConfigurationBuilder(); configurationBuilder.AddInMemoryAppSettingsJsonFile(fileProvider); - configurationBuilder.AddConfigServer(new ConfigServerClientOptions(), () => handler, NullLoggerFactory.Instance); + // ReSharper disable once AccessToDisposedClosure + configurationBuilder.AddConfigServer(new ConfigServerClientOptions(), configureOptions, () => handler, NullLoggerFactory.Instance); IConfigurationRoot configuration = configurationBuilder.Build(); handler.Mock.VerifyNoOutstandingExpectation(); @@ -97,7 +100,7 @@ public void Config_Server_URI_is_resolved_from_discovery_and_survives_changes_in IServiceCollection services = new ServiceCollection(); services.AddSingleton(configuration); - services.ConfigureConfigServerClientOptions(); + services.ConfigureConfigServerClientOptions(configureOptions); using ServiceProvider serviceProvider = services.BuildServiceProvider(true); var optionsMonitor = serviceProvider.GetRequiredService>(); @@ -109,6 +112,7 @@ public void Config_Server_URI_is_resolved_from_discovery_and_survives_changes_in provider.ClientOptions.Environment.Should().Be("example-profile"); provider.ClientOptions.Timeout.Should().Be(30_000); provider.ClientOptions.Label.Should().Be("example-label"); + provider.ClientOptions.ValidateCertificates.Should().BeFalse(); optionsMonitor.CurrentValue.Uri.Should().Be("https://discovered-server.com:9999/internal"); optionsMonitor.CurrentValue.Username.Should().Be("example-user"); @@ -117,6 +121,7 @@ public void Config_Server_URI_is_resolved_from_discovery_and_survives_changes_in optionsMonitor.CurrentValue.Environment.Should().Be(provider.ClientOptions.Environment); optionsMonitor.CurrentValue.Timeout.Should().Be(provider.ClientOptions.Timeout); optionsMonitor.CurrentValue.Label.Should().Be(provider.ClientOptions.Label); + optionsMonitor.CurrentValue.ValidateCertificates.Should().BeFalse(); configuration["example-server-key"].Should().Be("example-server-value"); @@ -173,6 +178,7 @@ public void Config_Server_URI_is_resolved_from_discovery_and_survives_changes_in provider.ClientOptions.Environment.Should().Be("example-profile"); provider.ClientOptions.Timeout.Should().Be(15_000); provider.ClientOptions.Label.Should().Be("example-label"); + provider.ClientOptions.ValidateCertificates.Should().BeFalse(); // Discovery changes don't propagate until the provider reloads. optionsMonitor.CurrentValue.Uri.Should().Be("https://discovered-server.com:9999/internal"); @@ -182,6 +188,7 @@ public void Config_Server_URI_is_resolved_from_discovery_and_survives_changes_in optionsMonitor.CurrentValue.Environment.Should().Be(provider.ClientOptions.Environment); optionsMonitor.CurrentValue.Timeout.Should().Be(provider.ClientOptions.Timeout); optionsMonitor.CurrentValue.Label.Should().Be(provider.ClientOptions.Label); + optionsMonitor.CurrentValue.ValidateCertificates.Should().BeFalse(); configuration["example-server-key"].Should().Be("example-server-value"); @@ -210,6 +217,7 @@ public void Config_Server_URI_is_resolved_from_discovery_and_survives_changes_in provider.ClientOptions.Environment.Should().Be("example-profile"); provider.ClientOptions.Timeout.Should().Be(10_000); provider.ClientOptions.Label.Should().BeNull(); + provider.ClientOptions.ValidateCertificates.Should().BeFalse(); // Discovery changes don't propagate until the provider reloads. optionsMonitor.CurrentValue.Uri.Should().Be("https://discovered-server.com:9999/internal"); @@ -217,6 +225,7 @@ public void Config_Server_URI_is_resolved_from_discovery_and_survives_changes_in optionsMonitor.CurrentValue.Environment.Should().Be(provider.ClientOptions.Environment); optionsMonitor.CurrentValue.Timeout.Should().Be(provider.ClientOptions.Timeout); optionsMonitor.CurrentValue.Label.Should().Be(provider.ClientOptions.Label); + optionsMonitor.CurrentValue.ValidateCertificates.Should().BeFalse(); configuration["example-server-key"].Should().Be("example-server-value"); } @@ -293,9 +302,12 @@ public void Updates_discovered_Config_Server_URI_on_provider_reload() handler.Mock.Expect(HttpMethod.Get, "https://discovered-server.com:9999/internal/example-app-name/example-profile/example-label") .Respond("application/json", configServerResponseJson); + Action configureOptions = options => options.ValidateCertificates = false; + var configurationBuilder = new ConfigurationBuilder(); configurationBuilder.AddInMemoryAppSettingsJsonFile(fileProvider); - configurationBuilder.AddConfigServer(new ConfigServerClientOptions(), () => handler, NullLoggerFactory.Instance); + // ReSharper disable once AccessToDisposedClosure + configurationBuilder.AddConfigServer(new ConfigServerClientOptions(), configureOptions, () => handler, NullLoggerFactory.Instance); IConfigurationRoot configuration = configurationBuilder.Build(); handler.Mock.VerifyNoOutstandingExpectation(); @@ -305,7 +317,7 @@ public void Updates_discovered_Config_Server_URI_on_provider_reload() IServiceCollection services = new ServiceCollection(); services.AddSingleton(configuration); - services.ConfigureConfigServerClientOptions(); + services.ConfigureConfigServerClientOptions(configureOptions); using ServiceProvider serviceProvider = services.BuildServiceProvider(true); var optionsMonitor = serviceProvider.GetRequiredService>(); @@ -316,6 +328,7 @@ public void Updates_discovered_Config_Server_URI_on_provider_reload() provider.ClientOptions.Name.Should().Be("example-app-name"); provider.ClientOptions.Environment.Should().Be("example-profile"); provider.ClientOptions.Label.Should().Be("example-label"); + provider.ClientOptions.ValidateCertificates.Should().BeFalse(); optionsMonitor.CurrentValue.Uri.Should().Be("https://discovered-server.com:9999/internal"); optionsMonitor.CurrentValue.Username.Should().Be("example-user"); @@ -323,6 +336,7 @@ public void Updates_discovered_Config_Server_URI_on_provider_reload() optionsMonitor.CurrentValue.Name.Should().Be(provider.ClientOptions.Name); optionsMonitor.CurrentValue.Environment.Should().Be(provider.ClientOptions.Environment); optionsMonitor.CurrentValue.Label.Should().Be(provider.ClientOptions.Label); + optionsMonitor.CurrentValue.ValidateCertificates.Should().BeFalse(); configuration["example-server-key"].Should().Be("example-server-value"); @@ -377,6 +391,7 @@ public void Updates_discovered_Config_Server_URI_on_provider_reload() provider.ClientOptions.Name.Should().Be("alternate-name"); provider.ClientOptions.Environment.Should().Be("alternate-profile"); provider.ClientOptions.Label.Should().Be("alternate-label"); + provider.ClientOptions.ValidateCertificates.Should().BeFalse(); optionsMonitor.CurrentValue.Uri.Should().Be("https://discovered-server.com:9999/internal"); optionsMonitor.CurrentValue.Username.Should().Be("example-user"); @@ -384,6 +399,7 @@ public void Updates_discovered_Config_Server_URI_on_provider_reload() optionsMonitor.CurrentValue.Name.Should().Be(provider.ClientOptions.Name); optionsMonitor.CurrentValue.Environment.Should().Be(provider.ClientOptions.Environment); optionsMonitor.CurrentValue.Label.Should().Be(provider.ClientOptions.Label); + optionsMonitor.CurrentValue.ValidateCertificates.Should().BeFalse(); configuration["example-server-key"].Should().Be("example-server-value"); @@ -399,6 +415,7 @@ public void Updates_discovered_Config_Server_URI_on_provider_reload() provider.ClientOptions.Name.Should().Be("alternate-name"); provider.ClientOptions.Environment.Should().Be("alternate-profile"); provider.ClientOptions.Label.Should().Be("alternate-label"); + provider.ClientOptions.ValidateCertificates.Should().BeFalse(); optionsMonitor.CurrentValue.Uri.Should().Be("https://alternate-discovered-server.com:7777/internal"); optionsMonitor.CurrentValue.Username.Should().Be("alternate-user"); @@ -406,6 +423,7 @@ public void Updates_discovered_Config_Server_URI_on_provider_reload() optionsMonitor.CurrentValue.Name.Should().Be(provider.ClientOptions.Name); optionsMonitor.CurrentValue.Environment.Should().Be(provider.ClientOptions.Environment); optionsMonitor.CurrentValue.Label.Should().Be(provider.ClientOptions.Label); + optionsMonitor.CurrentValue.ValidateCertificates.Should().BeFalse(); configuration["example-server-key"].Should().Be("example-server-value"); } diff --git a/src/Configuration/test/ConfigServer.Test/ConfigServerClientOptionsTest.cs b/src/Configuration/test/ConfigServer.Test/ConfigServerClientOptionsTest.cs index 52f5824b6c..005c6a3e5a 100644 --- a/src/Configuration/test/ConfigServer.Test/ConfigServerClientOptionsTest.cs +++ b/src/Configuration/test/ConfigServer.Test/ConfigServerClientOptionsTest.cs @@ -20,7 +20,7 @@ public void DefaultConstructor_InitializedWithDefaults() { var options = new ConfigServerClientOptions(); - TestHelper.VerifyDefaults(options, null); + TestHelper.VerifyDefaults(options, null, null); } [Fact] @@ -38,7 +38,7 @@ public async Task ConfigureConfigServerClientOptions_WithDefaults() var optionsMonitor = serviceProvider.GetRequiredService>(); string? expectedAppName = Assembly.GetEntryAssembly()!.GetName().Name; - TestHelper.VerifyDefaults(optionsMonitor.CurrentValue, expectedAppName); + TestHelper.VerifyDefaults(optionsMonitor.CurrentValue, expectedAppName, "Production"); } [Fact] @@ -79,7 +79,7 @@ public async Task ConfigureConfigServerClientOptions_WithValues() IConfiguration configuration = builder.Build(); services.AddSingleton(configuration); - services.ConfigureConfigServerClientOptions(); + services.ConfigureConfigServerClientOptions(options => options.Environment = "staging"); await using ServiceProvider serviceProvider = services.BuildServiceProvider(true); var service = serviceProvider.GetRequiredService>(); @@ -88,7 +88,7 @@ public async Task ConfigureConfigServerClientOptions_WithValues() options.Enabled.Should().BeTrue(); options.FailFast.Should().BeTrue(); options.Uri.Should().Be("http://localhost:8888"); - options.Environment.Should().Be("development"); + options.Environment.Should().Be("staging"); options.AccessTokenUri.Should().BeNull(); options.ClientId.Should().BeNull(); options.ClientSecret.Should().BeNull(); @@ -262,7 +262,8 @@ public void Certificate_configuration_survives_options_reload() var configurationBuilder = new ConfigurationBuilder(); configurationBuilder.AddInMemoryAppSettingsJsonFile(fileProvider); - configurationBuilder.AddConfigServer(new ConfigServerClientOptions(), () => handler, NullLoggerFactory.Instance); + // ReSharper disable once AccessToDisposedClosure + configurationBuilder.AddConfigServer(new ConfigServerClientOptions(), null, () => handler, NullLoggerFactory.Instance); IConfigurationRoot configuration = configurationBuilder.Build(); handler.Mock.VerifyNoOutstandingExpectation(); @@ -356,9 +357,12 @@ public void Changes_in_IConfiguration_update_provider_options_and_injected_optio Label = "example-label" // used, but missing in IConfiguration and injected options }; + Action configureOptions = options => options.FailFast = true; + var configurationBuilder = new ConfigurationBuilder(); configurationBuilder.AddInMemoryAppSettingsJsonFile(fileProvider); - configurationBuilder.AddConfigServer(initialOptions, () => handler, NullLoggerFactory.Instance); + // ReSharper disable once AccessToDisposedClosure + configurationBuilder.AddConfigServer(initialOptions, configureOptions, () => handler, NullLoggerFactory.Instance); IConfigurationRoot configuration = configurationBuilder.Build(); handler.Mock.VerifyNoOutstandingExpectation(); @@ -368,7 +372,7 @@ public void Changes_in_IConfiguration_update_provider_options_and_injected_optio IServiceCollection services = new ServiceCollection(); services.AddSingleton(configuration); - services.ConfigureConfigServerClientOptions(); + services.ConfigureConfigServerClientOptions(configureOptions); using ServiceProvider serviceProvider = services.BuildServiceProvider(true); var optionsMonitor = serviceProvider.GetRequiredService>(); @@ -378,12 +382,14 @@ public void Changes_in_IConfiguration_update_provider_options_and_injected_optio provider.ClientOptions.Environment.Should().Be("example-profile"); provider.ClientOptions.Timeout.Should().Be(30_000); provider.ClientOptions.Label.Should().Be("example-label"); + provider.ClientOptions.FailFast.Should().BeTrue(); optionsMonitor.CurrentValue.Uri.Should().Be(provider.ClientOptions.Uri); optionsMonitor.CurrentValue.Name.Should().Be(provider.ClientOptions.Name); optionsMonitor.CurrentValue.Environment.Should().Be(provider.ClientOptions.Environment); optionsMonitor.CurrentValue.Timeout.Should().Be(provider.ClientOptions.Timeout); optionsMonitor.CurrentValue.Label.Should().BeNull(); + optionsMonitor.CurrentValue.FailFast.Should().BeTrue(); configuration["example-server-key"].Should().Be("example-server-value"); @@ -422,12 +428,14 @@ void AssertFinal() provider.ClientOptions.Environment.Should().Be("example-profile"); provider.ClientOptions.Timeout.Should().Be(15_000); provider.ClientOptions.Label.Should().Be("alternate-label"); + provider.ClientOptions.FailFast.Should().BeTrue(); optionsMonitor.CurrentValue.Uri.Should().Be(provider.ClientOptions.Uri); optionsMonitor.CurrentValue.Name.Should().Be(provider.ClientOptions.Name); optionsMonitor.CurrentValue.Environment.Should().Be(provider.ClientOptions.Environment); optionsMonitor.CurrentValue.Timeout.Should().Be(provider.ClientOptions.Timeout); optionsMonitor.CurrentValue.Label.Should().Be(provider.ClientOptions.Label); + optionsMonitor.CurrentValue.FailFast.Should().BeTrue(); configuration["example-server-key"].Should().Be("example-server-value"); } diff --git a/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationBuilderExtensionsTest.cs b/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationBuilderExtensionsTest.cs index 6d2db09874..02fe3a9048 100644 --- a/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationBuilderExtensionsTest.cs +++ b/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationBuilderExtensionsTest.cs @@ -129,7 +129,7 @@ public void AddConfigServer_WithConfigServerCertificate_AddsConfigServerSourceWi var configurationBuilder = new ConfigurationBuilder(); configurationBuilder.AddInMemoryCollection(appSettings); - configurationBuilder.AddConfigServer(options, NullLoggerFactory.Instance); + configurationBuilder.AddConfigServer(options); IConfigurationRoot configurationRoot = configurationBuilder.Build(); ConfigServerConfigurationProvider provider = configurationRoot.EnumerateProviders().Single(); @@ -152,7 +152,7 @@ public void AddConfigServer_WithGlobalCertificate_AddsConfigServerSourceWithCert var configurationBuilder = new ConfigurationBuilder(); configurationBuilder.AddInMemoryCollection(appSettings); - configurationBuilder.AddConfigServer(options, NullLoggerFactory.Instance); + configurationBuilder.AddConfigServer(options); IConfigurationRoot configurationRoot = configurationBuilder.Build(); ConfigServerConfigurationProvider provider = configurationRoot.EnumerateProviders().Single(); @@ -205,7 +205,7 @@ public void AddConfigServer_VCAP_SERVICES_Override_Defaults(string vcapServices) } [Fact] - public void AddConfigServer_ConfigurationOverridesInitialOptionsFromCode() + public void AddConfigServer_CallbackOverridesConfigurationOverridesInitialOptions() { var options = new ConfigServerClientOptions { @@ -218,7 +218,8 @@ public void AddConfigServer_ConfigurationOverridesInitialOptionsFromCode() Retry = { InitialInterval = 5, - MaxInterval = 15 + MaxInterval = 15, + MaxAttempts = 12 } }; @@ -233,7 +234,8 @@ public void AddConfigServer_ConfigurationOverridesInitialOptionsFromCode() "Label": "labelInAppSettings", "Timeout": 50, "Retry": { - "MaxInterval": 100 + "MaxInterval": 100, + "MaxAttempts": 9 } } } @@ -241,9 +243,11 @@ public void AddConfigServer_ConfigurationOverridesInitialOptionsFromCode() } """); + Action configureOptions = clientOptions => clientOptions.Retry.MaxAttempts = 2; + var configurationBuilder = new ConfigurationBuilder(); configurationBuilder.AddInMemoryAppSettingsJsonFile(fileProvider); - configurationBuilder.AddConfigServer(options, NullLoggerFactory.Instance); + configurationBuilder.AddConfigServer(options, configureOptions, null, NullLoggerFactory.Instance); IConfigurationRoot configurationRoot = configurationBuilder.Build(); ConfigServerConfigurationProvider? provider = configurationRoot.EnumerateProviders().FirstOrDefault(); @@ -257,6 +261,7 @@ public void AddConfigServer_ConfigurationOverridesInitialOptionsFromCode() provider.ClientOptions.Timeout.Should().Be(50); provider.ClientOptions.Retry.InitialInterval.Should().Be(5); provider.ClientOptions.Retry.MaxInterval.Should().Be(100); + provider.ClientOptions.Retry.MaxAttempts.Should().Be(2); fileProvider.ReplaceAppSettingsJsonFile(""" { @@ -281,6 +286,7 @@ public void AddConfigServer_ConfigurationOverridesInitialOptionsFromCode() provider.ClientOptions.Timeout.Should().Be(10); provider.ClientOptions.Retry.InitialInterval.Should().Be(5); provider.ClientOptions.Retry.MaxInterval.Should().Be(15); + provider.ClientOptions.Retry.MaxAttempts.Should().Be(2); } [Fact] diff --git a/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationProviderTest.Loading.cs b/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationProviderTest.Loading.cs index 427e9fa6ce..26ee12d28a 100644 --- a/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationProviderTest.Loading.cs +++ b/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationProviderTest.Loading.cs @@ -25,11 +25,12 @@ public async Task RemoteLoadAsync_HostTimesOut() }; var httpClientHandler = new SlowHttpClientHandler(1.Seconds(), new HttpResponseMessage()); - using var provider = new ConfigServerConfigurationProvider(options, null, () => httpClientHandler, NullLoggerFactory.Instance); + using var provider = new ConfigServerConfigurationProvider(options, null, null, () => httpClientHandler, NullLoggerFactory.Instance); List requestUris = [new("http://localhost:9999/app/profile")]; - // ReSharper disable once AccessToDisposedClosure + // ReSharper disable AccessToDisposedClosure Func action = async () => await provider.RemoteLoadAsync(provider.ClientOptions, requestUris, null, TestContext.Current.CancellationToken); + // ReSharper restore AccessToDisposedClosure (await action.Should().ThrowExactlyAsync()).WithInnerExceptionExactly(); } @@ -49,10 +50,12 @@ public async Task RemoteLoadAsync_ConfigServerReturnsGreaterThanEqualBadRequest( ConfigServerClientOptions options = GetCommonOptions(); using var httpClientHandler = new ForwardingHttpClientHandler(server.CreateHandler()); - using var provider = new ConfigServerConfigurationProvider(options, null, () => httpClientHandler, NullLoggerFactory.Instance); - // ReSharper disable once AccessToDisposedClosure + using var provider = new ConfigServerConfigurationProvider(options, null, null, () => httpClientHandler, NullLoggerFactory.Instance); + + // ReSharper disable AccessToDisposedClosure Func action = async () => await provider.RemoteLoadAsync(provider.ClientOptions, options.GetUris(), null, TestContext.Current.CancellationToken); + // ReSharper restore AccessToDisposedClosure await action.Should().ThrowExactlyAsync(); @@ -75,7 +78,8 @@ public async Task RemoteLoadAsync_ConfigServerReturnsLessThanBadRequest() ConfigServerClientOptions options = GetCommonOptions(); using var httpClientHandler = new ForwardingHttpClientHandler(server.CreateHandler()); - using var provider = new ConfigServerConfigurationProvider(options, null, () => httpClientHandler, NullLoggerFactory.Instance); + // ReSharper disable once AccessToDisposedClosure + using var provider = new ConfigServerConfigurationProvider(options, null, null, () => httpClientHandler, NullLoggerFactory.Instance); ConfigEnvironment? result = await provider.RemoteLoadAsync(provider.ClientOptions, options.GetUris(), null, TestContext.Current.CancellationToken); @@ -121,7 +125,8 @@ await TestFailureTracer.CaptureAsync(async tracer => }; using var httpClientHandler = new ForwardingHttpClientHandler(server.CreateHandler()); - using var provider = new ConfigServerConfigurationProvider(options, null, () => httpClientHandler, tracer.LoggerFactory); + // ReSharper disable once AccessToDisposedClosure + using var provider = new ConfigServerConfigurationProvider(options, null, null, () => httpClientHandler, tracer.LoggerFactory); bool firstRequestCompleted = startup.WaitForFirstRequest(2.Seconds()); firstRequestCompleted.Should().BeTrue(); @@ -176,7 +181,8 @@ await TestFailureTracer.CaptureAsync(async tracer => }; using var httpClientHandler = new ForwardingHttpClientHandler(server.CreateHandler()); - using var provider = new ConfigServerConfigurationProvider(options, null, () => httpClientHandler, tracer.LoggerFactory); + // ReSharper disable once AccessToDisposedClosure + using var provider = new ConfigServerConfigurationProvider(options, null, null, () => httpClientHandler, tracer.LoggerFactory); bool firstRequestCompleted = startup.WaitForFirstRequest(2.Seconds()); firstRequestCompleted.Should().BeTrue(); @@ -228,7 +234,8 @@ public async Task Create_WithNonZeroPollingIntervalAndClientDisabled_PollingConf using var httpClientHandler = new ForwardingHttpClientHandler(server.CreateHandler()); - using var provider = new ConfigServerConfigurationProvider(options, null, () => httpClientHandler, NullLoggerFactory.Instance); + // ReSharper disable once AccessToDisposedClosure + using var provider = new ConfigServerConfigurationProvider(options, null, null, () => httpClientHandler, NullLoggerFactory.Instance); startup.WaitForFirstRequest(2.Seconds()).Should().BeFalse(); } @@ -270,7 +277,8 @@ public void OnSettingsChanged_stops_reload_timer_when_polling_becomes_ineffectiv var configurationBuilder = new ConfigurationBuilder(); configurationBuilder.AddInMemoryAppSettingsJsonFile(fileProvider); - configurationBuilder.AddConfigServer(new ConfigServerClientOptions(), () => handler, NullLoggerFactory.Instance); + // ReSharper disable once AccessToDisposedClosure + configurationBuilder.AddConfigServer(new ConfigServerClientOptions(), null, () => handler, NullLoggerFactory.Instance); IConfigurationRoot configuration = configurationBuilder.Build(); ConfigServerConfigurationProvider provider = configuration.Providers.OfType().Single(); @@ -334,7 +342,8 @@ public void OnSettingsChanged_reschedules_reload_timer_when_polling_interval_cha var configurationBuilder = new ConfigurationBuilder(); configurationBuilder.AddInMemoryAppSettingsJsonFile(fileProvider); - configurationBuilder.AddConfigServer(new ConfigServerClientOptions(), () => handler, NullLoggerFactory.Instance); + // ReSharper disable once AccessToDisposedClosure + configurationBuilder.AddConfigServer(new ConfigServerClientOptions(), null, () => handler, NullLoggerFactory.Instance); IConfigurationRoot configuration = configurationBuilder.Build(); ConfigServerConfigurationProvider provider = configuration.Providers.OfType().Single(); @@ -398,7 +407,8 @@ public void OnSettingsChanged_stops_vault_renew_timer_when_renewal_becomes_disab var configurationBuilder = new ConfigurationBuilder(); configurationBuilder.AddInMemoryAppSettingsJsonFile(fileProvider); - configurationBuilder.AddConfigServer(new ConfigServerClientOptions(), () => handler, NullLoggerFactory.Instance); + // ReSharper disable once AccessToDisposedClosure + configurationBuilder.AddConfigServer(new ConfigServerClientOptions(), null, () => handler, NullLoggerFactory.Instance); IConfigurationRoot configuration = configurationBuilder.Build(); ConfigServerConfigurationProvider provider = configuration.Providers.OfType().Single(); @@ -459,7 +469,8 @@ public async Task Load_MultipleConfigServers_SocketError_FallsBackToNextServer() Uri = "http://server1:8888, http://server2:8888" }; - using var provider = new ConfigServerConfigurationProvider(options, null, () => handler, NullLoggerFactory.Instance); + // ReSharper disable once AccessToDisposedClosure + using var provider = new ConfigServerConfigurationProvider(options, null, null, () => handler, NullLoggerFactory.Instance); await provider.LoadInternalAsync(provider.ClientOptions, true, TestContext.Current.CancellationToken); @@ -496,7 +507,8 @@ public async Task DoLoad_IdenticalData_DoesNotTriggerReload() Name = "myName" }; - using var provider = new ConfigServerConfigurationProvider(options, null, () => handler, NullLoggerFactory.Instance); + // ReSharper disable once AccessToDisposedClosure + using var provider = new ConfigServerConfigurationProvider(options, null, null, () => handler, NullLoggerFactory.Instance); await provider.DoLoadAsync(provider.ClientOptions, true, TestContext.Current.CancellationToken); provider.TryGet("key1", out string? value).Should().BeTrue(); @@ -557,7 +569,8 @@ public async Task DoLoad_MultipleLabels_ChecksAllLabels() options.Label = "label,test-label"; using var httpClientHandler = new ForwardingHttpClientHandler(server.CreateHandler()); - using var provider = new ConfigServerConfigurationProvider(options, null, () => httpClientHandler, NullLoggerFactory.Instance); + // ReSharper disable once AccessToDisposedClosure + using var provider = new ConfigServerConfigurationProvider(options, null, null, () => httpClientHandler, NullLoggerFactory.Instance); await provider.DoLoadAsync(provider.ClientOptions, true, TestContext.Current.CancellationToken); @@ -601,7 +614,8 @@ public async Task RemoteLoadAsync_ConfigServerReturnsGood() ConfigServerClientOptions options = GetCommonOptions(); using var httpClientHandler = new ForwardingHttpClientHandler(server.CreateHandler()); - using var provider = new ConfigServerConfigurationProvider(options, null, () => httpClientHandler, NullLoggerFactory.Instance); + // ReSharper disable once AccessToDisposedClosure + using var provider = new ConfigServerConfigurationProvider(options, null, null, () => httpClientHandler, NullLoggerFactory.Instance); ConfigEnvironment? env = await provider.RemoteLoadAsync(provider.ClientOptions, options.GetUris(), null, TestContext.Current.CancellationToken); @@ -642,7 +656,8 @@ public async Task Load_MultipleConfigServers_ReturnsGreaterThanEqualBadRequest_S ConfigServerClientOptions options = GetCommonOptions(); options.Uri = "http://localhost:8888, http://localhost:8888"; using var httpClientHandler = new ForwardingHttpClientHandler(server.CreateHandler()); - using var provider = new ConfigServerConfigurationProvider(options, null, () => httpClientHandler, NullLoggerFactory.Instance); + // ReSharper disable once AccessToDisposedClosure + using var provider = new ConfigServerConfigurationProvider(options, null, null, () => httpClientHandler, NullLoggerFactory.Instance); await provider.LoadInternalAsync(provider.ClientOptions, true, TestContext.Current.CancellationToken); @@ -677,7 +692,8 @@ public async Task Load_MultipleConfigServers_ReturnsNotFoundStatus_DoesNotContin options.Uri = "http://localhost:8888, http://localhost:8888"; using var httpClientHandler = new ForwardingHttpClientHandler(server.CreateHandler()); - using var provider = new ConfigServerConfigurationProvider(options, null, () => httpClientHandler, NullLoggerFactory.Instance); + // ReSharper disable once AccessToDisposedClosure + using var provider = new ConfigServerConfigurationProvider(options, null, null, () => httpClientHandler, NullLoggerFactory.Instance); await provider.LoadInternalAsync(provider.ClientOptions, true, TestContext.Current.CancellationToken); @@ -705,7 +721,8 @@ public async Task Load_ConfigServerReturnsNotFoundStatus() ConfigServerClientOptions options = GetCommonOptions(); using var httpClientHandler = new ForwardingHttpClientHandler(server.CreateHandler()); - using var provider = new ConfigServerConfigurationProvider(options, null, () => httpClientHandler, NullLoggerFactory.Instance); + // ReSharper disable once AccessToDisposedClosure + using var provider = new ConfigServerConfigurationProvider(options, null, null, () => httpClientHandler, NullLoggerFactory.Instance); await provider.LoadInternalAsync(provider.ClientOptions, true, TestContext.Current.CancellationToken); @@ -731,10 +748,12 @@ public async Task Load_ConfigServerReturnsNotFoundStatus_FailFastEnabled() options.FailFast = true; using var httpClientHandler = new ForwardingHttpClientHandler(server.CreateHandler()); - using var provider = new ConfigServerConfigurationProvider(options, null, () => httpClientHandler, NullLoggerFactory.Instance); - // ReSharper disable once AccessToDisposedClosure + using var provider = new ConfigServerConfigurationProvider(options, null, null, () => httpClientHandler, NullLoggerFactory.Instance); + + // ReSharper disable AccessToDisposedClosure Func action = async () => await provider.LoadInternalAsync(provider.ClientOptions, true, TestContext.Current.CancellationToken); + // ReSharper restore AccessToDisposedClosure await action.Should().ThrowExactlyAsync(); } @@ -756,7 +775,8 @@ public async Task Load_MultipleConfigServers_ReturnsNotFoundStatus__DoesNotConti options.Uri = "http://localhost:8888,http://localhost:8888"; using var httpClientHandler = new ForwardingHttpClientHandler(server.CreateHandler()); - using var provider = new ConfigServerConfigurationProvider(options, null, () => httpClientHandler, NullLoggerFactory.Instance); + // ReSharper disable once AccessToDisposedClosure + using var provider = new ConfigServerConfigurationProvider(options, null, null, () => httpClientHandler, NullLoggerFactory.Instance); startup.Reset(); @@ -766,8 +786,9 @@ public async Task Load_MultipleConfigServers_ReturnsNotFoundStatus__DoesNotConti 200 ]; - // ReSharper disable once AccessToDisposedClosure + // ReSharper disable AccessToDisposedClosure Func action = async () => await provider.LoadInternalAsync(provider.ClientOptions, true, TestContext.Current.CancellationToken); + // ReSharper restore AccessToDisposedClosure await action.Should().ThrowExactlyAsync(); startup.RequestCount.Should().Be(1); @@ -795,10 +816,12 @@ public async Task Load_UriInvalid_FailFastEnabled() options.FailFast = true; using var httpClientHandler = new ForwardingHttpClientHandler(server.CreateHandler()); - using var provider = new ConfigServerConfigurationProvider(options, null, () => httpClientHandler, NullLoggerFactory.Instance); - // ReSharper disable once AccessToDisposedClosure + using var provider = new ConfigServerConfigurationProvider(options, null, null, () => httpClientHandler, NullLoggerFactory.Instance); + + // ReSharper disable AccessToDisposedClosure Func action = async () => await provider.LoadInternalAsync(provider.ClientOptions, true, TestContext.Current.CancellationToken); + // ReSharper restore AccessToDisposedClosure await action.Should().ThrowExactlyAsync().WithMessage("One or more Config Server URIs in configuration are invalid."); } @@ -820,10 +843,12 @@ public async Task Load_ConfigServerReturnsBadStatus_FailFastEnabled() options.FailFast = true; using var httpClientHandler = new ForwardingHttpClientHandler(server.CreateHandler()); - using var provider = new ConfigServerConfigurationProvider(options, null, () => httpClientHandler, NullLoggerFactory.Instance); - // ReSharper disable once AccessToDisposedClosure + using var provider = new ConfigServerConfigurationProvider(options, null, null, () => httpClientHandler, NullLoggerFactory.Instance); + + // ReSharper disable AccessToDisposedClosure Func action = async () => await provider.LoadInternalAsync(provider.ClientOptions, true, TestContext.Current.CancellationToken); + // ReSharper restore AccessToDisposedClosure await action.Should().ThrowExactlyAsync(); } @@ -852,10 +877,12 @@ public async Task Load_MultipleConfigServers_ReturnsBadStatus_StopsChecking_Fail options.Uri = "http://localhost:8888, http://localhost:8888, http://localhost:8888"; using var httpClientHandler = new ForwardingHttpClientHandler(server.CreateHandler()); - using var provider = new ConfigServerConfigurationProvider(options, null, () => httpClientHandler, NullLoggerFactory.Instance); - // ReSharper disable once AccessToDisposedClosure + using var provider = new ConfigServerConfigurationProvider(options, null, null, () => httpClientHandler, NullLoggerFactory.Instance); + + // ReSharper disable AccessToDisposedClosure Func action = async () => await provider.LoadInternalAsync(provider.ClientOptions, true, TestContext.Current.CancellationToken); + // ReSharper restore AccessToDisposedClosure await action.Should().ThrowExactlyAsync(); startup.RequestCount.Should().Be(1); @@ -893,10 +920,12 @@ await TestFailureTracer.CaptureAsync(async tracer => }; using var httpClientHandler = new ForwardingHttpClientHandler(server.CreateHandler()); - using var provider = new ConfigServerConfigurationProvider(options, null, () => httpClientHandler, tracer.LoggerFactory); - // ReSharper disable once AccessToDisposedClosure + using var provider = new ConfigServerConfigurationProvider(options, null, null, () => httpClientHandler, tracer.LoggerFactory); + + // ReSharper disable AccessToDisposedClosure Func action = async () => await provider.LoadInternalAsync(provider.ClientOptions, true, TestContext.Current.CancellationToken); + // ReSharper restore AccessToDisposedClosure await action.Should().ThrowExactlyAsync(); @@ -942,7 +971,8 @@ public async Task Load_ChangesDataDictionary() ConfigServerClientOptions options = GetCommonOptions(); using var httpClientHandler = new ForwardingHttpClientHandler(server.CreateHandler()); - using var provider = new ConfigServerConfigurationProvider(options, null, () => httpClientHandler, NullLoggerFactory.Instance); + // ReSharper disable once AccessToDisposedClosure + using var provider = new ConfigServerConfigurationProvider(options, null, null, () => httpClientHandler, NullLoggerFactory.Instance); await provider.LoadInternalAsync(provider.ClientOptions, true, TestContext.Current.CancellationToken); @@ -997,7 +1027,8 @@ public async Task ReLoad_DataDictionary_With_New_Configurations() ConfigServerClientOptions options = GetCommonOptions(); using var httpClientHandler = new ForwardingHttpClientHandler(server.CreateHandler()); - using var provider = new ConfigServerConfigurationProvider(options, null, () => httpClientHandler, NullLoggerFactory.Instance); + // ReSharper disable once AccessToDisposedClosure + using var provider = new ConfigServerConfigurationProvider(options, null, null, () => httpClientHandler, NullLoggerFactory.Instance); provider.Load(); @@ -1088,7 +1119,7 @@ public void DataDictionary_DoesNotContainRedundantClientSettings() } }; - using var provider = new ConfigServerConfigurationProvider(options, null, null, NullLoggerFactory.Instance); + using var provider = new ConfigServerConfigurationProvider(options, null, null, null, NullLoggerFactory.Instance); provider.TryGet("spring:cloud:config:enabled", out _).Should().BeFalse(); provider.TryGet("spring:cloud:config:failFast", out _).Should().BeFalse(); @@ -1161,7 +1192,8 @@ public async Task Reload_And_Bind_Without_Throwing_Exception() server.BaseAddress = new Uri(clientOptions.Uri!); using var httpClientHandler = new ForwardingHttpClientHandler(server.CreateHandler()); - using var provider = new ConfigServerConfigurationProvider(clientOptions, null, () => httpClientHandler, NullLoggerFactory.Instance); + // ReSharper disable once AccessToDisposedClosure + using var provider = new ConfigServerConfigurationProvider(clientOptions, null, null, () => httpClientHandler, NullLoggerFactory.Instance); var configurationBuilder = new ConfigurationBuilder(); configurationBuilder.Add(new TestConfigServerConfigurationSource(provider)); @@ -1193,7 +1225,8 @@ private static ConfigServerClientOptions GetCommonOptions() { return new ConfigServerClientOptions { - Name = "myName" + Name = "myName", + Environment = "Staging" }; } diff --git a/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationProviderTest.Settings.cs b/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationProviderTest.Settings.cs index f861d98d0b..d6536e0510 100644 --- a/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationProviderTest.Settings.cs +++ b/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationProviderTest.Settings.cs @@ -15,10 +15,10 @@ public sealed partial class ConfigServerConfigurationProviderTest public void DefaultConstructor_InitializedWithDefaultSettings() { var options = new ConfigServerClientOptions(); - using var provider = new ConfigServerConfigurationProvider(options, null, null, NullLoggerFactory.Instance); + using var provider = new ConfigServerConfigurationProvider(options, null, null, null, NullLoggerFactory.Instance); string? expectedAppName = Assembly.GetEntryAssembly()!.GetName().Name; - TestHelper.VerifyDefaults(provider.ClientOptions, expectedAppName); + TestHelper.VerifyDefaults(provider.ClientOptions, expectedAppName, "Production"); } [Fact] @@ -26,11 +26,11 @@ public void SourceConstructor_WithDefaults_InitializesWithDefaultSettings() { IConfiguration configuration = new ConfigurationBuilder().Build(); var options = new ConfigServerClientOptions(); - var source = new ConfigServerConfigurationSource(options, configuration, null, NullLoggerFactory.Instance); + var source = new ConfigServerConfigurationSource(options, configuration, null, null, NullLoggerFactory.Instance); using var provider = new ConfigServerConfigurationProvider(source, NullLoggerFactory.Instance); string? expectedAppName = Assembly.GetEntryAssembly()!.GetName().Name; - TestHelper.VerifyDefaults(provider.ClientOptions, expectedAppName); + TestHelper.VerifyDefaults(provider.ClientOptions, expectedAppName, "Production"); } [Fact] @@ -44,7 +44,7 @@ public void SourceConstructor_WithTimeoutConfigured_InitializesHttpClientWithCon IConfiguration configuration = new ConfigurationBuilder().AddInMemoryCollection(appSettings).Build(); var options = new ConfigServerClientOptions(); - var source = new ConfigServerConfigurationSource(options, configuration, null, NullLoggerFactory.Instance); + var source = new ConfigServerConfigurationSource(options, configuration, null, null, NullLoggerFactory.Instance); using var provider = new ConfigServerConfigurationProvider(source, NullLoggerFactory.Instance); using HttpClient httpClient = provider.CreateHttpClient(provider.ClientOptions); @@ -61,10 +61,11 @@ public void GetConfigServerUri_NoBaseUri_Throws() Environment = "Production" }; - using var provider = new ConfigServerConfigurationProvider(options, null, null, NullLoggerFactory.Instance); + using var provider = new ConfigServerConfigurationProvider(options, null, null, null, NullLoggerFactory.Instance); - // ReSharper disable once AccessToDisposedClosure + // ReSharper disable AccessToDisposedClosure Action action = () => provider.BuildConfigServerUri(provider.ClientOptions, null!, null); + // ReSharper restore AccessToDisposedClosure action.Should().ThrowExactly(); } @@ -78,7 +79,7 @@ public void GetConfigServerUri_NoLabel() Environment = "Production" }; - using var provider = new ConfigServerConfigurationProvider(options, null, null, NullLoggerFactory.Instance); + using var provider = new ConfigServerConfigurationProvider(options, null, null, null, NullLoggerFactory.Instance); string uri = provider.BuildConfigServerUri(provider.ClientOptions, new Uri(options.Uri!), null).ToString(); uri.Should().Be($"{options.Uri}/{options.Name}/{options.Environment}"); @@ -94,7 +95,7 @@ public void GetConfigServerUri_WithLabel() Label = "myLabel" }; - using var provider = new ConfigServerConfigurationProvider(options, null, null, NullLoggerFactory.Instance); + using var provider = new ConfigServerConfigurationProvider(options, null, null, null, NullLoggerFactory.Instance); string uri = provider.BuildConfigServerUri(provider.ClientOptions, new Uri(options.Uri!), options.Label).ToString(); uri.Should().Be($"{options.Uri}/{options.Name}/{options.Environment}/{options.Label}"); @@ -110,7 +111,7 @@ public void GetConfigServerUri_WithLabelContainingSlash() Label = "myLabel/version" }; - using var provider = new ConfigServerConfigurationProvider(options, null, null, NullLoggerFactory.Instance); + using var provider = new ConfigServerConfigurationProvider(options, null, null, null, NullLoggerFactory.Instance); string uri = provider.BuildConfigServerUri(provider.ClientOptions, new Uri(options.Uri!), options.Label).ToString(); uri.Should().Be($"{options.Uri}/{options.Name}/{options.Environment}/myLabel(_)version"); @@ -126,7 +127,7 @@ public void GetConfigServerUri_WithExtraPathInfo() Environment = "Production" }; - using var provider = new ConfigServerConfigurationProvider(options, null, null, NullLoggerFactory.Instance); + using var provider = new ConfigServerConfigurationProvider(options, null, null, null, NullLoggerFactory.Instance); string uri = provider.BuildConfigServerUri(provider.ClientOptions, new Uri(options.Uri), null).ToString(); uri.Should().Be($"http://localhost:9999/myPath/path/{options.Name}/{options.Environment}"); @@ -142,7 +143,7 @@ public void GetConfigServerUri_WithExtraPathInfo_NoEndingSlash() Environment = "Production" }; - using var provider = new ConfigServerConfigurationProvider(options, null, null, NullLoggerFactory.Instance); + using var provider = new ConfigServerConfigurationProvider(options, null, null, null, NullLoggerFactory.Instance); string uri = provider.BuildConfigServerUri(provider.ClientOptions, new Uri(options.Uri), null).ToString(); uri.Should().Be($"http://localhost:9999/myPath/path/{options.Name}/{options.Environment}"); @@ -158,7 +159,7 @@ public void GetConfigServerUri_NoEndingSlash() Environment = "Production" }; - using var provider = new ConfigServerConfigurationProvider(options, null, null, NullLoggerFactory.Instance); + using var provider = new ConfigServerConfigurationProvider(options, null, null, null, NullLoggerFactory.Instance); string uri = provider.BuildConfigServerUri(provider.ClientOptions, new Uri(options.Uri), null).ToString(); uri.Should().Be($"http://localhost:9999/{options.Name}/{options.Environment}"); @@ -174,7 +175,7 @@ public void GetConfigServerUri_WithEndingSlash() Environment = "Production" }; - using var provider = new ConfigServerConfigurationProvider(options, null, null, NullLoggerFactory.Instance); + using var provider = new ConfigServerConfigurationProvider(options, null, null, null, NullLoggerFactory.Instance); string uri = provider.BuildConfigServerUri(provider.ClientOptions, new Uri(options.Uri), null).ToString(); uri.Should().Be($"http://localhost:9999/{options.Name}/{options.Environment}"); @@ -190,7 +191,7 @@ public void GetConfigServerUri_MultipleEnvironments_EncodesComma() Label = "demo" }; - using var provider = new ConfigServerConfigurationProvider(options, null, null, NullLoggerFactory.Instance); + using var provider = new ConfigServerConfigurationProvider(options, null, null, null, NullLoggerFactory.Instance); string uri = provider.BuildConfigServerUri(provider.ClientOptions, new Uri(options.Uri!), options.Label).ToString(); uri.Should().Be("http://localhost:8888/myName/one%2Ctwo/demo"); @@ -208,7 +209,7 @@ public void GetConfigServerUri_EncodesSpecialCharacters() Password = "#&:$<>'/so,\"me" }; - using var provider = new ConfigServerConfigurationProvider(options, null, null, NullLoggerFactory.Instance); + using var provider = new ConfigServerConfigurationProvider(options, null, null, null, NullLoggerFactory.Instance); string uri = provider.BuildConfigServerUri(provider.ClientOptions, new Uri(options.Uri!), options.Label).ToString(); uri.Should().Be( diff --git a/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationProviderTest.cs b/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationProviderTest.cs index 74b30a7213..312b894c87 100644 --- a/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationProviderTest.cs +++ b/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationProviderTest.cs @@ -65,7 +65,7 @@ public async Task Deserialize_GoodJson() public void GetLabels_Null() { var options = new ConfigServerClientOptions(); - using var provider = new ConfigServerConfigurationProvider(options, null, null, NullLoggerFactory.Instance); + using var provider = new ConfigServerConfigurationProvider(options, null, null, null, NullLoggerFactory.Instance); string[] result = provider.GetLabels(provider.ClientOptions); result.Should().ContainSingle().Which.Should().BeEmpty(); @@ -79,7 +79,7 @@ public void GetLabels_Empty() Label = string.Empty }; - using var provider = new ConfigServerConfigurationProvider(options, null, null, NullLoggerFactory.Instance); + using var provider = new ConfigServerConfigurationProvider(options, null, null, null, NullLoggerFactory.Instance); string[] result = provider.GetLabels(provider.ClientOptions); result.Should().ContainSingle().Which.Should().BeEmpty(); @@ -93,7 +93,7 @@ public void GetLabels_SingleString() Label = "foobar" }; - using var provider = new ConfigServerConfigurationProvider(options, null, null, NullLoggerFactory.Instance); + using var provider = new ConfigServerConfigurationProvider(options, null, null, null, NullLoggerFactory.Instance); string[] result = provider.GetLabels(provider.ClientOptions); result.Should().ContainSingle().Which.Should().Be("foobar"); @@ -107,7 +107,7 @@ public void GetLabels_MultiString() Label = "1,2,3," }; - using var provider = new ConfigServerConfigurationProvider(options, null, null, NullLoggerFactory.Instance); + using var provider = new ConfigServerConfigurationProvider(options, null, null, null, NullLoggerFactory.Instance); string[] result = provider.GetLabels(provider.ClientOptions); result.Should().HaveCount(3); @@ -124,7 +124,7 @@ public void GetLabels_MultiStringHoles() Label = "1,,2,3," }; - using var provider = new ConfigServerConfigurationProvider(options, null, null, NullLoggerFactory.Instance); + using var provider = new ConfigServerConfigurationProvider(options, null, null, null, NullLoggerFactory.Instance); string[] result = provider.GetLabels(provider.ClientOptions); result.Should().HaveCount(3); @@ -143,7 +143,7 @@ public async Task GetRequestMessage_AddsBasicAuthIfUserNameAndPasswordInURL() Environment = "development" }; - using var provider = new ConfigServerConfigurationProvider(options, null, null, NullLoggerFactory.Instance); + using var provider = new ConfigServerConfigurationProvider(options, null, null, null, NullLoggerFactory.Instance); Uri requestUri = provider.BuildConfigServerUri(provider.ClientOptions, new Uri(options.Uri), null); HttpRequestMessage request = await provider.GetRequestMessageAsync(provider.ClientOptions, requestUri, TestContext.Current.CancellationToken); @@ -167,7 +167,7 @@ public async Task GetRequestMessage_AddsBasicAuthIfUserNameAndPasswordInSettings Password = "password" }; - using var provider = new ConfigServerConfigurationProvider(options, null, null, NullLoggerFactory.Instance); + using var provider = new ConfigServerConfigurationProvider(options, null, null, null, NullLoggerFactory.Instance); Uri requestUri = provider.BuildConfigServerUri(provider.ClientOptions, new Uri(options.Uri), null); HttpRequestMessage request = await provider.GetRequestMessageAsync(provider.ClientOptions, requestUri, TestContext.Current.CancellationToken); @@ -191,7 +191,7 @@ public async Task GetRequestMessage_BasicAuthInSettingsOverridesUserNameAndPassw Password = "password" }; - using var provider = new ConfigServerConfigurationProvider(options, null, null, NullLoggerFactory.Instance); + using var provider = new ConfigServerConfigurationProvider(options, null, null, null, NullLoggerFactory.Instance); Uri requestUri = provider.BuildConfigServerUri(provider.ClientOptions, new Uri(options.Uri), null); HttpRequestMessage request = await provider.GetRequestMessageAsync(provider.ClientOptions, requestUri, TestContext.Current.CancellationToken); @@ -213,7 +213,7 @@ public async Task GetRequestMessage_AddsVaultToken_IfNeeded() Token = "MyVaultToken" }; - using var provider = new ConfigServerConfigurationProvider(options, null, null, NullLoggerFactory.Instance); + using var provider = new ConfigServerConfigurationProvider(options, null, null, null, NullLoggerFactory.Instance); Uri requestUri = provider.BuildConfigServerUri(provider.ClientOptions, new Uri(options.Uri!), null); HttpRequestMessage request = await provider.GetRequestMessageAsync(provider.ClientOptions, requestUri, TestContext.Current.CancellationToken); @@ -247,7 +247,8 @@ public async Task GetRequestMessage_AddsBearerToken_WhenAccessTokenUriIsSet() } """); - using var provider = new ConfigServerConfigurationProvider(options, null, () => handler, NullLoggerFactory.Instance); + // ReSharper disable once AccessToDisposedClosure + using var provider = new ConfigServerConfigurationProvider(options, null, null, () => handler, NullLoggerFactory.Instance); Uri requestUri = provider.BuildConfigServerUri(provider.ClientOptions, new Uri(options.Uri), null); HttpRequestMessage request = await provider.GetRequestMessageAsync(provider.ClientOptions, requestUri, TestContext.Current.CancellationToken); @@ -274,7 +275,8 @@ public async Task RefreshVaultToken_Succeeds() handler.Mock.Expect(HttpMethod.Post, "http://localhost:8888/vault/v1/auth/token/renew-self").WithHeaders("X-Vault-Token", "MyVaultToken") .WithContent("{\"increment\":300}").Respond(HttpStatusCode.NoContent); - using var provider = new ConfigServerConfigurationProvider(options, null, () => handler, NullLoggerFactory.Instance); + // ReSharper disable once AccessToDisposedClosure + using var provider = new ConfigServerConfigurationProvider(options, null, null, () => handler, NullLoggerFactory.Instance); await provider.RefreshVaultTokenAsync(provider.ClientOptions, TestContext.Current.CancellationToken); handler.Mock.VerifyNoOutstandingExpectation(); @@ -301,7 +303,8 @@ public async Task RefreshVaultToken_With_AccessTokenUri_Succeeds() handler.Mock.Expect(HttpMethod.Post, "http://localhost:8888/vault/v1/auth/token/renew-self").WithHeaders("X-Vault-Token", "MyVaultToken") .WithHeaders("Authorization", "Bearer secret").WithContent("{\"increment\":300}").Respond(HttpStatusCode.NoContent); - using var provider = new ConfigServerConfigurationProvider(options, null, () => handler, NullLoggerFactory.Instance); + // ReSharper disable once AccessToDisposedClosure + using var provider = new ConfigServerConfigurationProvider(options, null, null, () => handler, NullLoggerFactory.Instance); await provider.RefreshVaultTokenAsync(provider.ClientOptions, TestContext.Current.CancellationToken); handler.Mock.VerifyNoOutstandingExpectation(); @@ -321,7 +324,7 @@ public void GetHttpClient_AddsHeaders_IfConfigured() } }; - using var provider = new ConfigServerConfigurationProvider(options, null, null, NullLoggerFactory.Instance); + using var provider = new ConfigServerConfigurationProvider(options, null, null, null, NullLoggerFactory.Instance); using HttpClient httpClient = provider.CreateHttpClient(provider.ClientOptions); httpClient.Should().NotBeNull(); @@ -342,7 +345,7 @@ public void IsDiscoveryFirstEnabled_ReturnsExpected() } }; - using (var provider = new ConfigServerConfigurationProvider(options, null, null, NullLoggerFactory.Instance)) + using (var provider = new ConfigServerConfigurationProvider(options, null, null, null, NullLoggerFactory.Instance)) { provider.ClientOptions.Discovery.Enabled.Should().BeTrue(); } @@ -360,7 +363,7 @@ public void IsDiscoveryFirstEnabled_ReturnsExpected() Environment = "development" }; - var source = new ConfigServerConfigurationSource(options, configuration, null, NullLoggerFactory.Instance); + var source = new ConfigServerConfigurationSource(options, configuration, null, null, NullLoggerFactory.Instance); using (var provider = new ConfigServerConfigurationProvider(source, NullLoggerFactory.Instance)) { @@ -385,7 +388,7 @@ public void UpdateSettingsFromDiscovery_UpdatesSettingsCorrectly() Environment = "development" }; - var source = new ConfigServerConfigurationSource(initialOptions, configuration, null, NullLoggerFactory.Instance); + var source = new ConfigServerConfigurationSource(initialOptions, configuration, null, null, NullLoggerFactory.Instance); using var provider = new ConfigServerConfigurationProvider(source, NullLoggerFactory.Instance); ConfigServerClientOptions optionsSnapshot = provider.ClientOptions; @@ -440,11 +443,12 @@ public async Task DiscoverServerInstances_FailsFast() Timeout = 10 }; - var source = new ConfigServerConfigurationSource(options, configuration, null, NullLoggerFactory.Instance); + var source = new ConfigServerConfigurationSource(options, configuration, null, null, NullLoggerFactory.Instance); using var provider = new ConfigServerConfigurationProvider(source, NullLoggerFactory.Instance); - // ReSharper disable once AccessToDisposedClosure + // ReSharper disable AccessToDisposedClosure Func action = async () => await provider.LoadInternalAsync(provider.ClientOptions, true, TestContext.Current.CancellationToken); + // ReSharper restore AccessToDisposedClosure await action.Should().ThrowExactlyAsync().WithMessage("Could not locate Config Server via discovery*"); } diff --git a/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationSourceTest.cs b/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationSourceTest.cs index 44a52b53c2..d07020258a 100644 --- a/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationSourceTest.cs +++ b/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationSourceTest.cs @@ -21,7 +21,7 @@ public void Constructors_InitializesProperties() var source = new ConfigServerConfigurationSource(options, sources, new Dictionary { ["foo"] = "bar" - }, null, NullLoggerFactory.Instance); + }, null, null, NullLoggerFactory.Instance); source.DefaultOptions.Should().Be(options); source.Configuration.Should().BeNull(); @@ -31,7 +31,7 @@ public void Constructors_InitializesProperties() source.Properties.Should().ContainKey("foo").WhoseValue.Should().Be("bar"); IConfigurationRoot configurationRoot = new ConfigurationBuilder().AddInMemoryCollection().Build(); - source = new ConfigServerConfigurationSource(options, configurationRoot, null, NullLoggerFactory.Instance); + source = new ConfigServerConfigurationSource(options, configurationRoot, null, null, NullLoggerFactory.Instance); source.DefaultOptions.Should().Be(options); ConfigurationRoot? root = source.Configuration.Should().BeOfType().Subject; @@ -46,7 +46,7 @@ public void Build_ReturnsProvider() var memSource = new MemoryConfigurationSource(); List sources = [memSource]; - var source = new ConfigServerConfigurationSource(options, sources, null, null, NullLoggerFactory.Instance); + var source = new ConfigServerConfigurationSource(options, sources, null, null, null, NullLoggerFactory.Instance); IConfigurationProvider provider = source.Build(new ConfigurationBuilder()); provider.Should().BeOfType(); diff --git a/src/Configuration/test/ConfigServer.Test/ConfigServerHostBuilderExtensionsTest.cs b/src/Configuration/test/ConfigServer.Test/ConfigServerHostBuilderExtensionsTest.cs index 737c8a4563..ad06440291 100644 --- a/src/Configuration/test/ConfigServer.Test/ConfigServerHostBuilderExtensionsTest.cs +++ b/src/Configuration/test/ConfigServer.Test/ConfigServerHostBuilderExtensionsTest.cs @@ -114,13 +114,13 @@ public void AddConfigServer_WebHostBuilder_DisposesTimer() ["spring:cloud:config:pollingInterval"] = 1.Seconds().ToString() }; - WebHostBuilder builder = TestWebHostBuilderFactory.Create(); - builder.ConfigureAppConfiguration(configurationBuilder => configurationBuilder.AddInMemoryCollection(appSettings)); - builder.AddConfigServer(); + WebHostBuilder hostBuilder = TestWebHostBuilderFactory.Create(); + hostBuilder.ConfigureAppConfiguration(configurationBuilder => configurationBuilder.AddInMemoryCollection(appSettings)); + hostBuilder.AddConfigServer(); ConfigServerConfigurationProvider provider; - using (IWebHost webHost = builder.Build()) + using (IWebHost webHost = hostBuilder.Build()) { var configurationRoot = (IConfigurationRoot)webHost.Services.GetRequiredService(); provider = configurationRoot.EnumerateProviders().Single(); diff --git a/src/Configuration/test/ConfigServer.Test/ConfigServerHostedServiceTest.cs b/src/Configuration/test/ConfigServer.Test/ConfigServerHostedServiceTest.cs index ac140148b3..4dea770d8c 100644 --- a/src/Configuration/test/ConfigServer.Test/ConfigServerHostedServiceTest.cs +++ b/src/Configuration/test/ConfigServer.Test/ConfigServerHostedServiceTest.cs @@ -18,7 +18,7 @@ public async Task ServiceConstructsAndOperatesWithConfigurationRoot() var provider = new ConfigServerConfigurationProvider(new ConfigServerClientOptions { Enabled = false - }, null, null, NullLoggerFactory.Instance); + }, null, null, null, NullLoggerFactory.Instance); var configurationRoot = new ConfigurationRoot([provider]); var service = new ConfigServerHostedService(configurationRoot, []); diff --git a/src/Configuration/test/ConfigServer.Test/TestHelper.cs b/src/Configuration/test/ConfigServer.Test/TestHelper.cs index 57db7c8e6f..6fd0ff93ac 100644 --- a/src/Configuration/test/ConfigServer.Test/TestHelper.cs +++ b/src/Configuration/test/ConfigServer.Test/TestHelper.cs @@ -6,12 +6,12 @@ namespace Steeltoe.Configuration.ConfigServer.Test; internal static class TestHelper { - public static void VerifyDefaults(ConfigServerClientOptions options, string? expectedAppName) + public static void VerifyDefaults(ConfigServerClientOptions options, string? expectedAppName, string? expectedEnvironment) { options.Enabled.Should().BeTrue(); options.FailFast.Should().BeFalse(); options.Uri.Should().Be("http://localhost:8888"); - options.Environment.Should().Be("Production"); + options.Environment.Should().Be(expectedEnvironment); options.AccessTokenUri.Should().BeNull(); options.ClientId.Should().BeNull(); options.ClientSecret.Should().BeNull(); From a3dc4528db7691e28da56726e350ad4bf22873ab Mon Sep 17 00:00:00 2001 From: Bart Koelman <104792814+bart-vmware@users.noreply.github.com> Date: Thu, 2 Apr 2026 14:55:15 +0200 Subject: [PATCH 20/29] Cleanup existing tests --- ...ServerConfigurationProviderTest.Loading.cs | 575 ++++++------------ .../ForwardingHttpClientHandler.cs | 22 - .../TestConfigServerStartup.cs | 129 ---- 3 files changed, 187 insertions(+), 539 deletions(-) delete mode 100644 src/Configuration/test/ConfigServer.Test/ForwardingHttpClientHandler.cs delete mode 100644 src/Configuration/test/ConfigServer.Test/TestConfigServerStartup.cs diff --git a/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationProviderTest.Loading.cs b/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationProviderTest.Loading.cs index 26ee12d28a..1bf4c7e831 100644 --- a/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationProviderTest.Loading.cs +++ b/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationProviderTest.Loading.cs @@ -2,16 +2,18 @@ // The .NET Foundation licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information. +using System.Net; using System.Net.Sockets; using System.Reflection; +using System.Text; using FluentAssertions.Extensions; -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.TestHost; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging.Abstractions; using RichardSzalay.MockHttp; using Steeltoe.Common.TestResources; +// ReSharper disable AccessToDisposedClosure + namespace Steeltoe.Configuration.ConfigServer.Test; public sealed partial class ConfigServerConfigurationProviderTest @@ -28,9 +30,7 @@ public async Task RemoteLoadAsync_HostTimesOut() using var provider = new ConfigServerConfigurationProvider(options, null, null, () => httpClientHandler, NullLoggerFactory.Instance); List requestUris = [new("http://localhost:9999/app/profile")]; - // ReSharper disable AccessToDisposedClosure Func action = async () => await provider.RemoteLoadAsync(provider.ClientOptions, requestUris, null, TestContext.Current.CancellationToken); - // ReSharper restore AccessToDisposedClosure (await action.Should().ThrowExactlyAsync()).WithInnerExceptionExactly(); } @@ -38,53 +38,33 @@ public async Task RemoteLoadAsync_HostTimesOut() [Fact] public async Task RemoteLoadAsync_ConfigServerReturnsGreaterThanEqualBadRequest() { - using var startup = new TestConfigServerStartup(); - startup.ReturnStatus = [500]; - - await using WebApplication app = TestWebApplicationBuilderFactory.Create().Build(); - startup.Configure(app); - await app.StartAsync(TestContext.Current.CancellationToken); + ConfigServerClientOptions options = GetCommonOptions(); - using TestServer server = app.GetTestServer(); - server.BaseAddress = new Uri("http://localhost:8888"); + using var handler = new DelegateToMockHttpClientHandler(); + handler.Mock.Expect(HttpMethod.Get, $"http://localhost:8888/{options.Name}/{options.Environment}").Respond(HttpStatusCode.InternalServerError); - ConfigServerClientOptions options = GetCommonOptions(); - using var httpClientHandler = new ForwardingHttpClientHandler(server.CreateHandler()); - // ReSharper disable once AccessToDisposedClosure - using var provider = new ConfigServerConfigurationProvider(options, null, null, () => httpClientHandler, NullLoggerFactory.Instance); + using var provider = new ConfigServerConfigurationProvider(options, null, null, () => handler, NullLoggerFactory.Instance); - // ReSharper disable AccessToDisposedClosure Func action = async () => await provider.RemoteLoadAsync(provider.ClientOptions, options.GetUris(), null, TestContext.Current.CancellationToken); - // ReSharper restore AccessToDisposedClosure await action.Should().ThrowExactlyAsync(); - startup.LastRequest.Should().NotBeNull(); - startup.LastRequest.Path.Value.Should().Be($"/{options.Name}/{options.Environment}"); + handler.Mock.VerifyNoOutstandingExpectation(); } [Fact] public async Task RemoteLoadAsync_ConfigServerReturnsLessThanBadRequest() { - using var startup = new TestConfigServerStartup(); - startup.ReturnStatus = [204]; - - await using WebApplication app = TestWebApplicationBuilderFactory.Create().Build(); - startup.Configure(app); - await app.StartAsync(TestContext.Current.CancellationToken); + ConfigServerClientOptions options = GetCommonOptions(); - using TestServer server = app.GetTestServer(); - server.BaseAddress = new Uri("http://localhost:8888"); + using var handler = new DelegateToMockHttpClientHandler(); + handler.Mock.Expect(HttpMethod.Get, $"http://localhost:8888/{options.Name}/{options.Environment}").Respond(HttpStatusCode.NoContent); - ConfigServerClientOptions options = GetCommonOptions(); - using var httpClientHandler = new ForwardingHttpClientHandler(server.CreateHandler()); - // ReSharper disable once AccessToDisposedClosure - using var provider = new ConfigServerConfigurationProvider(options, null, null, () => httpClientHandler, NullLoggerFactory.Instance); + using var provider = new ConfigServerConfigurationProvider(options, null, null, () => handler, NullLoggerFactory.Instance); ConfigEnvironment? result = await provider.RemoteLoadAsync(provider.ClientOptions, options.GetUris(), null, TestContext.Current.CancellationToken); - startup.LastRequest.Should().NotBeNull(); - startup.LastRequest.Path.Value.Should().Be($"/{options.Name}/{options.Environment}"); + handler.Mock.VerifyNoOutstandingExpectation(); result.Should().BeNull(); } @@ -93,7 +73,7 @@ public async Task Create_WithConfigurationReloadTimer() { await TestFailureTracer.CaptureAsync(async tracer => { - const string environment = """ + const string responseJson = """ { "name": "test-name", "profiles": [ @@ -105,18 +85,6 @@ await TestFailureTracer.CaptureAsync(async tracer => } """; - using var startup = new TestConfigServerStartup(); - startup.Response = environment; - startup.ReturnStatus = [.. Enumerable.Repeat(200, 100)]; - startup.Label = "test-label"; - - await using WebApplication app = TestWebApplicationBuilderFactory.Create().Build(); - startup.Configure(app); - await app.StartAsync(TestContext.Current.CancellationToken); - - using TestServer server = app.GetTestServer(); - server.BaseAddress = new Uri("http://localhost:8888"); - var options = new ConfigServerClientOptions { Name = "myName", @@ -124,19 +92,34 @@ await TestFailureTracer.CaptureAsync(async tracer => Label = "label,test-label" }; - using var httpClientHandler = new ForwardingHttpClientHandler(server.CreateHandler()); - // ReSharper disable once AccessToDisposedClosure - using var provider = new ConfigServerConfigurationProvider(options, null, null, () => httpClientHandler, tracer.LoggerFactory); + using var handler = new DelegateToMockHttpClientHandler(); + handler.Mock.When(HttpMethod.Get, "http://localhost:8888/myName/Production/label").Respond(HttpStatusCode.NotFound); + + using var firstRequestCountdownEvent = new CountdownEvent(1); - bool firstRequestCompleted = startup.WaitForFirstRequest(2.Seconds()); + MockedRequest testLabelRequest = handler.Mock.When(HttpMethod.Get, "http://localhost:8888/myName/Production/test-label").Respond(_ => + { + if (!firstRequestCountdownEvent.IsSet) + { + firstRequestCountdownEvent.Signal(); + } + + return new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(responseJson, Encoding.UTF8, "application/json") + }; + }); + + using var provider = new ConfigServerConfigurationProvider(options, null, null, () => handler, tracer.LoggerFactory); + + bool firstRequestCompleted = firstRequestCountdownEvent.Wait(2.Seconds(), TestContext.Current.CancellationToken); firstRequestCompleted.Should().BeTrue(); - startup.RequestCount.Should().BeGreaterThanOrEqualTo(1); - startup.LastRequest.Should().NotBeNull(); + handler.Mock.GetMatchCount(testLabelRequest).Should().BeGreaterThanOrEqualTo(1); await Task.Delay(2.Seconds(), TestContext.Current.CancellationToken); - startup.RequestCount.Should().BeGreaterThanOrEqualTo(2); + handler.Mock.GetMatchCount(testLabelRequest).Should().BeGreaterThanOrEqualTo(2); provider.GetReloadToken().HasChanged.Should().BeFalse(); }); } @@ -146,7 +129,7 @@ public async Task Create_FailFastEnabledAndExceptionThrownDuringPolledConfigurat { await TestFailureTracer.CaptureAsync(async tracer => { - const string environment = """ + const string responseJson = """ { "name": "test-name", "profiles": [ @@ -158,20 +141,6 @@ await TestFailureTracer.CaptureAsync(async tracer => } """; - using var startup = new TestConfigServerStartup(); - startup.Response = environment; - - // Initial requests succeed, but later requests return 400 status code so that an exception is thrown during polling - startup.ReturnStatus = [.. Enumerable.Repeat(200, 2).Concat(Enumerable.Repeat(400, 100))]; - startup.Label = "test-label"; - - await using WebApplication app = TestWebApplicationBuilderFactory.Create().Build(); - startup.Configure(app); - await app.StartAsync(TestContext.Current.CancellationToken); - - using TestServer server = app.GetTestServer(); - server.BaseAddress = new Uri("http://localhost:8888"); - var options = new ConfigServerClientOptions { Name = "myName", @@ -180,19 +149,40 @@ await TestFailureTracer.CaptureAsync(async tracer => Label = "test-label" }; - using var httpClientHandler = new ForwardingHttpClientHandler(server.CreateHandler()); - // ReSharper disable once AccessToDisposedClosure - using var provider = new ConfigServerConfigurationProvider(options, null, null, () => httpClientHandler, tracer.LoggerFactory); + using var handler = new DelegateToMockHttpClientHandler(); + using var firstRequestCountdownEvent = new CountdownEvent(1); + int requestCount = 0; + + MockedRequest testLabelRequest = handler.Mock.When(HttpMethod.Get, "http://localhost:8888/myName/Production/test-label").Respond(_ => + { + int currentCount = Interlocked.Increment(ref requestCount); + + if (!firstRequestCountdownEvent.IsSet) + { + firstRequestCountdownEvent.Signal(); + } - bool firstRequestCompleted = startup.WaitForFirstRequest(2.Seconds()); + if (currentCount <= 2) + { + return new HttpResponseMessage(HttpStatusCode.OK) + { + Content = new StringContent(responseJson, Encoding.UTF8, "application/json") + }; + } + + return new HttpResponseMessage(HttpStatusCode.BadRequest); + }); + + using var provider = new ConfigServerConfigurationProvider(options, null, null, () => handler, tracer.LoggerFactory); + + bool firstRequestCompleted = firstRequestCountdownEvent.Wait(2.Seconds(), TestContext.Current.CancellationToken); firstRequestCompleted.Should().BeTrue(); - startup.RequestCount.Should().BeGreaterThanOrEqualTo(1); - startup.LastRequest.Should().NotBeNull(); + handler.Mock.GetMatchCount(testLabelRequest).Should().BeGreaterThanOrEqualTo(1); await Task.Delay(2.Seconds(), TestContext.Current.CancellationToken); - startup.RequestCount.Should().BeGreaterThanOrEqualTo(2); + handler.Mock.GetMatchCount(testLabelRequest).Should().BeGreaterThanOrEqualTo(2); provider.GetReloadToken().HasChanged.Should().BeFalse(); }); } @@ -200,30 +190,6 @@ await TestFailureTracer.CaptureAsync(async tracer => [Fact] public async Task Create_WithNonZeroPollingIntervalAndClientDisabled_PollingConfigurationReloadDisabled() { - const string environment = """ - { - "name": "test-name", - "profiles": [ - "Production" - ], - "label": "test-label", - "version": "test-version", - "propertySources": [] - } - """; - - using var startup = new TestConfigServerStartup(); - startup.Response = environment; - startup.ReturnStatus = [.. Enumerable.Repeat(200, 100)]; - startup.Label = "test-label"; - - await using WebApplication app = TestWebApplicationBuilderFactory.Create().Build(); - startup.Configure(app); - await app.StartAsync(TestContext.Current.CancellationToken); - - using TestServer server = app.GetTestServer(); - server.BaseAddress = new Uri("http://localhost:8888"); - var options = new ConfigServerClientOptions { Name = "myName", @@ -232,12 +198,13 @@ public async Task Create_WithNonZeroPollingIntervalAndClientDisabled_PollingConf Label = "label,test-label" }; - using var httpClientHandler = new ForwardingHttpClientHandler(server.CreateHandler()); + using var handler = new DelegateToMockHttpClientHandler(); + MockedRequest request = handler.Mock.When(HttpMethod.Get, "http://localhost:8888/myName/Production/label").Respond(HttpStatusCode.OK); - // ReSharper disable once AccessToDisposedClosure - using var provider = new ConfigServerConfigurationProvider(options, null, null, () => httpClientHandler, NullLoggerFactory.Instance); + using var provider = new ConfigServerConfigurationProvider(options, null, null, () => handler, NullLoggerFactory.Instance); - startup.WaitForFirstRequest(2.Seconds()).Should().BeFalse(); + await Task.Delay(2.Seconds(), TestContext.Current.CancellationToken); + handler.Mock.GetMatchCount(request).Should().Be(0); } [Theory] @@ -245,7 +212,7 @@ public async Task Create_WithNonZeroPollingIntervalAndClientDisabled_PollingConf [InlineData(true, "00:00:00")] public void OnSettingsChanged_stops_reload_timer_when_polling_becomes_ineffective(bool enabled, string pollingInterval) { - const string configServerResponseJson = """ + const string responseJson = """ { "name": "myName", "profiles": [ "Production" ], @@ -272,12 +239,10 @@ public void OnSettingsChanged_stops_reload_timer_when_polling_becomes_ineffectiv """); using var handler = new DelegateToMockHttpClientHandler(); - - handler.Mock.When(HttpMethod.Get, "http://localhost:8888/myName/Production").Respond("application/json", configServerResponseJson); + handler.Mock.When(HttpMethod.Get, "http://localhost:8888/myName/Production").Respond("application/json", responseJson); var configurationBuilder = new ConfigurationBuilder(); configurationBuilder.AddInMemoryAppSettingsJsonFile(fileProvider); - // ReSharper disable once AccessToDisposedClosure configurationBuilder.AddConfigServer(new ConfigServerClientOptions(), null, () => handler, NullLoggerFactory.Instance); IConfigurationRoot configuration = configurationBuilder.Build(); @@ -310,7 +275,7 @@ public void OnSettingsChanged_stops_reload_timer_when_polling_becomes_ineffectiv [Fact] public void OnSettingsChanged_reschedules_reload_timer_when_polling_interval_changes() { - const string configServerResponseJson = """ + const string responseJson = """ { "name": "myName", "profiles": [ "Production" ], @@ -337,12 +302,10 @@ public void OnSettingsChanged_reschedules_reload_timer_when_polling_interval_cha """); using var handler = new DelegateToMockHttpClientHandler(); - - handler.Mock.When(HttpMethod.Get, "http://localhost:8888/myName/Production").Respond("application/json", configServerResponseJson); + handler.Mock.When(HttpMethod.Get, "http://localhost:8888/myName/Production").Respond("application/json", responseJson); var configurationBuilder = new ConfigurationBuilder(); configurationBuilder.AddInMemoryAppSettingsJsonFile(fileProvider); - // ReSharper disable once AccessToDisposedClosure configurationBuilder.AddConfigServer(new ConfigServerClientOptions(), null, () => handler, NullLoggerFactory.Instance); IConfigurationRoot configuration = configurationBuilder.Build(); @@ -375,7 +338,7 @@ public void OnSettingsChanged_reschedules_reload_timer_when_polling_interval_cha [Fact] public void OnSettingsChanged_stops_vault_renew_timer_when_renewal_becomes_disabled() { - const string configServerResponseJson = """ + const string responseJson = """ { "name": "myName", "profiles": [ "Production" ], @@ -402,12 +365,10 @@ public void OnSettingsChanged_stops_vault_renew_timer_when_renewal_becomes_disab """); using var handler = new DelegateToMockHttpClientHandler(); - - handler.Mock.When(HttpMethod.Get, "http://localhost:8888/myName/Production").Respond("application/json", configServerResponseJson); + handler.Mock.When(HttpMethod.Get, "http://localhost:8888/myName/Production").Respond("application/json", responseJson); var configurationBuilder = new ConfigurationBuilder(); configurationBuilder.AddInMemoryAppSettingsJsonFile(fileProvider); - // ReSharper disable once AccessToDisposedClosure configurationBuilder.AddConfigServer(new ConfigServerClientOptions(), null, () => handler, NullLoggerFactory.Instance); IConfigurationRoot configuration = configurationBuilder.Build(); @@ -439,7 +400,7 @@ public void OnSettingsChanged_stops_vault_renew_timer_when_renewal_becomes_disab [Fact] public async Task Load_MultipleConfigServers_SocketError_FallsBackToNextServer() { - const string environment = """ + const string responseJson = """ { "name": "test-name", "profiles": [ "Production" ], @@ -461,7 +422,7 @@ public async Task Load_MultipleConfigServers_SocketError_FallsBackToNextServer() handler.Mock.When(HttpMethod.Get, "http://server1:8888/myName/Production") .Throw(new HttpRequestException("Connection refused", new SocketException((int)SocketError.ConnectionRefused))); - handler.Mock.When(HttpMethod.Get, "http://server2:8888/myName/Production").Respond("application/json", environment); + handler.Mock.When(HttpMethod.Get, "http://server2:8888/myName/Production").Respond("application/json", responseJson); var options = new ConfigServerClientOptions { @@ -469,7 +430,6 @@ public async Task Load_MultipleConfigServers_SocketError_FallsBackToNextServer() Uri = "http://server1:8888, http://server2:8888" }; - // ReSharper disable once AccessToDisposedClosure using var provider = new ConfigServerConfigurationProvider(options, null, null, () => handler, NullLoggerFactory.Instance); await provider.LoadInternalAsync(provider.ClientOptions, true, TestContext.Current.CancellationToken); @@ -481,7 +441,7 @@ public async Task Load_MultipleConfigServers_SocketError_FallsBackToNextServer() [Fact] public async Task DoLoad_IdenticalData_DoesNotTriggerReload() { - const string environment = """ + const string responseJson = """ { "name": "test-name", "profiles": [ "Production" ], @@ -499,15 +459,13 @@ public async Task DoLoad_IdenticalData_DoesNotTriggerReload() """; using var handler = new DelegateToMockHttpClientHandler(); - - handler.Mock.When(HttpMethod.Get, "http://localhost:8888/myName/Production").Respond("application/json", environment); + handler.Mock.When(HttpMethod.Get, "http://localhost:8888/myName/Production").Respond("application/json", responseJson); var options = new ConfigServerClientOptions { Name = "myName" }; - // ReSharper disable once AccessToDisposedClosure using var provider = new ConfigServerConfigurationProvider(options, null, null, () => handler, NullLoggerFactory.Instance); await provider.DoLoadAsync(provider.ClientOptions, true, TestContext.Current.CancellationToken); @@ -527,7 +485,7 @@ public async Task DoLoad_IdenticalData_DoesNotTriggerReload() [Fact] public async Task DoLoad_MultipleLabels_ChecksAllLabels() { - const string environment = """ + const string responseJson = """ { "name": "test-name", "profiles": [ @@ -547,42 +505,24 @@ public async Task DoLoad_MultipleLabels_ChecksAllLabels() } """; - using var startup = new TestConfigServerStartup(); - startup.Response = environment; - - startup.ReturnStatus = - [ - 404, - 200 - ]; - - startup.Label = "test-label"; - - await using WebApplication app = TestWebApplicationBuilderFactory.Create().Build(); - startup.Configure(app); - await app.StartAsync(TestContext.Current.CancellationToken); - - using TestServer server = app.GetTestServer(); - server.BaseAddress = new Uri("http://localhost:8888"); - ConfigServerClientOptions options = GetCommonOptions(); options.Label = "label,test-label"; - using var httpClientHandler = new ForwardingHttpClientHandler(server.CreateHandler()); - // ReSharper disable once AccessToDisposedClosure - using var provider = new ConfigServerConfigurationProvider(options, null, null, () => httpClientHandler, NullLoggerFactory.Instance); + using var handler = new DelegateToMockHttpClientHandler(); + handler.Mock.Expect(HttpMethod.Get, $"http://localhost:8888/{options.Name}/{options.Environment}/label").Respond(HttpStatusCode.NotFound); + handler.Mock.Expect(HttpMethod.Get, $"http://localhost:8888/{options.Name}/{options.Environment}/test-label").Respond("application/json", responseJson); + + using var provider = new ConfigServerConfigurationProvider(options, null, null, () => handler, NullLoggerFactory.Instance); await provider.DoLoadAsync(provider.ClientOptions, true, TestContext.Current.CancellationToken); - startup.LastRequest.Should().NotBeNull(); - startup.RequestCount.Should().Be(2); - startup.LastRequest.Path.Value.Should().Be($"/{options.Name}/{options.Environment}/test-label"); + handler.Mock.VerifyNoOutstandingExpectation(); } [Fact] public async Task RemoteLoadAsync_ConfigServerReturnsGood() { - const string environment = """ + const string responseJson = """ { "name": "test-name", "profiles": [ @@ -602,25 +542,16 @@ public async Task RemoteLoadAsync_ConfigServerReturnsGood() } """; - using var startup = new TestConfigServerStartup(); - startup.Response = environment; - - await using WebApplication app = TestWebApplicationBuilderFactory.Create().Build(); - startup.Configure(app); - await app.StartAsync(TestContext.Current.CancellationToken); + ConfigServerClientOptions options = GetCommonOptions(); - using TestServer server = app.GetTestServer(); - server.BaseAddress = new Uri("http://localhost:8888"); + using var handler = new DelegateToMockHttpClientHandler(); + handler.Mock.Expect(HttpMethod.Get, $"http://localhost:8888/{options.Name}/{options.Environment}").Respond("application/json", responseJson); - ConfigServerClientOptions options = GetCommonOptions(); - using var httpClientHandler = new ForwardingHttpClientHandler(server.CreateHandler()); - // ReSharper disable once AccessToDisposedClosure - using var provider = new ConfigServerConfigurationProvider(options, null, null, () => httpClientHandler, NullLoggerFactory.Instance); + using var provider = new ConfigServerConfigurationProvider(options, null, null, () => handler, NullLoggerFactory.Instance); ConfigEnvironment? env = await provider.RemoteLoadAsync(provider.ClientOptions, options.GetUris(), null, TestContext.Current.CancellationToken); - startup.LastRequest.Should().NotBeNull(); - startup.LastRequest.Path.Value.Should().Be($"/{options.Name}/{options.Environment}"); + handler.Mock.VerifyNoOutstandingExpectation(); env.Should().NotBeNull(); env.Name.Should().Be("test-name"); @@ -638,258 +569,159 @@ public async Task RemoteLoadAsync_ConfigServerReturnsGood() [Fact] public async Task Load_MultipleConfigServers_ReturnsGreaterThanEqualBadRequest_StopsChecking() { - using var startup = new TestConfigServerStartup(); - - startup.ReturnStatus = - [ - 500, - 200 - ]; + ConfigServerClientOptions options = GetCommonOptions(); + options.Uri = "http://localhost:8888, http://localhost:8888"; - await using WebApplication app = TestWebApplicationBuilderFactory.Create().Build(); - startup.Configure(app); - await app.StartAsync(TestContext.Current.CancellationToken); + using var handler = new DelegateToMockHttpClientHandler(); - using TestServer server = app.GetTestServer(); - server.BaseAddress = new Uri("http://localhost:8888"); + MockedRequest request = handler.Mock.When(HttpMethod.Get, $"http://localhost:8888/{options.Name}/{options.Environment}") + .Respond(HttpStatusCode.InternalServerError); - ConfigServerClientOptions options = GetCommonOptions(); - options.Uri = "http://localhost:8888, http://localhost:8888"; - using var httpClientHandler = new ForwardingHttpClientHandler(server.CreateHandler()); - // ReSharper disable once AccessToDisposedClosure - using var provider = new ConfigServerConfigurationProvider(options, null, null, () => httpClientHandler, NullLoggerFactory.Instance); + using var provider = new ConfigServerConfigurationProvider(options, null, null, () => handler, NullLoggerFactory.Instance); await provider.LoadInternalAsync(provider.ClientOptions, true, TestContext.Current.CancellationToken); - startup.LastRequest.Should().NotBeNull(); - startup.LastRequest.Path.Value.Should().Be($"/{options.Name}/{options.Environment}"); - startup.RequestCount.Should().Be(1); + handler.Mock.GetMatchCount(request).Should().Be(1); await Task.Delay(2.Seconds(), TestContext.Current.CancellationToken); - startup.RequestCount.Should().Be(1); + handler.Mock.GetMatchCount(request).Should().Be(1); } [Fact] public async Task Load_MultipleConfigServers_ReturnsNotFoundStatus_DoesNotContinueChecking() { - using var startup = new TestConfigServerStartup(); - - startup.ReturnStatus = - [ - 404, - 200 - ]; - - await using WebApplication app = TestWebApplicationBuilderFactory.Create().Build(); - startup.Configure(app); - await app.StartAsync(TestContext.Current.CancellationToken); - - using TestServer server = app.GetTestServer(); - server.BaseAddress = new Uri("http://localhost:8888"); - ConfigServerClientOptions options = GetCommonOptions(); options.Uri = "http://localhost:8888, http://localhost:8888"; - using var httpClientHandler = new ForwardingHttpClientHandler(server.CreateHandler()); - // ReSharper disable once AccessToDisposedClosure - using var provider = new ConfigServerConfigurationProvider(options, null, null, () => httpClientHandler, NullLoggerFactory.Instance); + using var handler = new DelegateToMockHttpClientHandler(); + + MockedRequest request = handler.Mock.When(HttpMethod.Get, $"http://localhost:8888/{options.Name}/{options.Environment}") + .Respond(HttpStatusCode.NotFound); + + using var provider = new ConfigServerConfigurationProvider(options, null, null, () => handler, NullLoggerFactory.Instance); await provider.LoadInternalAsync(provider.ClientOptions, true, TestContext.Current.CancellationToken); - startup.LastRequest.Should().NotBeNull(); - startup.LastRequest.Path.Value.Should().Be($"/{options.Name}/{options.Environment}"); - startup.RequestCount.Should().Be(1); + handler.Mock.GetMatchCount(request).Should().Be(1); await Task.Delay(2.Seconds(), TestContext.Current.CancellationToken); - startup.RequestCount.Should().Be(1); + handler.Mock.GetMatchCount(request).Should().Be(1); } [Fact] public async Task Load_ConfigServerReturnsNotFoundStatus() { - using var startup = new TestConfigServerStartup(); - startup.ReturnStatus = [404]; - - await using WebApplication app = TestWebApplicationBuilderFactory.Create().Build(); - startup.Configure(app); - await app.StartAsync(TestContext.Current.CancellationToken); + ConfigServerClientOptions options = GetCommonOptions(); - using TestServer server = app.GetTestServer(); - server.BaseAddress = new Uri("http://localhost:8888"); + using var handler = new DelegateToMockHttpClientHandler(); + handler.Mock.Expect(HttpMethod.Get, $"http://localhost:8888/{options.Name}/{options.Environment}").Respond(HttpStatusCode.NotFound); - ConfigServerClientOptions options = GetCommonOptions(); - using var httpClientHandler = new ForwardingHttpClientHandler(server.CreateHandler()); - // ReSharper disable once AccessToDisposedClosure - using var provider = new ConfigServerConfigurationProvider(options, null, null, () => httpClientHandler, NullLoggerFactory.Instance); + using var provider = new ConfigServerConfigurationProvider(options, null, null, () => handler, NullLoggerFactory.Instance); await provider.LoadInternalAsync(provider.ClientOptions, true, TestContext.Current.CancellationToken); - startup.LastRequest.Should().NotBeNull(); - startup.LastRequest.Path.Value.Should().Be($"/{options.Name}/{options.Environment}"); + handler.Mock.VerifyNoOutstandingExpectation(); provider.InnerData.Should().BeEmpty(); } [Fact] public async Task Load_ConfigServerReturnsNotFoundStatus_FailFastEnabled() { - using var startup = new TestConfigServerStartup(); - startup.ReturnStatus = [404]; - - await using WebApplication app = TestWebApplicationBuilderFactory.Create().Build(); - startup.Configure(app); - await app.StartAsync(TestContext.Current.CancellationToken); - - using TestServer server = app.GetTestServer(); - server.BaseAddress = new Uri("http://localhost:8888"); - ConfigServerClientOptions options = GetCommonOptions(); options.FailFast = true; - using var httpClientHandler = new ForwardingHttpClientHandler(server.CreateHandler()); - // ReSharper disable once AccessToDisposedClosure - using var provider = new ConfigServerConfigurationProvider(options, null, null, () => httpClientHandler, NullLoggerFactory.Instance); + using var handler = new DelegateToMockHttpClientHandler(); + handler.Mock.Expect(HttpMethod.Get, $"http://localhost:8888/{options.Name}/{options.Environment}").Respond(HttpStatusCode.NotFound); + + using var provider = new ConfigServerConfigurationProvider(options, null, null, () => handler, NullLoggerFactory.Instance); - // ReSharper disable AccessToDisposedClosure Func action = async () => await provider.LoadInternalAsync(provider.ClientOptions, true, TestContext.Current.CancellationToken); - // ReSharper restore AccessToDisposedClosure await action.Should().ThrowExactlyAsync(); + handler.Mock.VerifyNoOutstandingExpectation(); } [Fact] public async Task Load_MultipleConfigServers_ReturnsNotFoundStatus__DoesNotContinueChecking_FailFastEnabled() { - using var startup = new TestConfigServerStartup(); - - await using WebApplication app = TestWebApplicationBuilderFactory.Create().Build(); - startup.Configure(app); - await app.StartAsync(TestContext.Current.CancellationToken); - - using TestServer server = app.GetTestServer(); - server.BaseAddress = new Uri("http://localhost:8888"); - ConfigServerClientOptions options = GetCommonOptions(); options.FailFast = true; options.Uri = "http://localhost:8888,http://localhost:8888"; - using var httpClientHandler = new ForwardingHttpClientHandler(server.CreateHandler()); - // ReSharper disable once AccessToDisposedClosure - using var provider = new ConfigServerConfigurationProvider(options, null, null, () => httpClientHandler, NullLoggerFactory.Instance); + using var handler = new DelegateToMockHttpClientHandler(); - startup.Reset(); + MockedRequest request = handler.Mock.When(HttpMethod.Get, $"http://localhost:8888/{options.Name}/{options.Environment}") + .Respond(HttpStatusCode.NotFound); - startup.ReturnStatus = - [ - 404, - 200 - ]; + using var provider = new ConfigServerConfigurationProvider(options, null, null, () => handler, NullLoggerFactory.Instance); - // ReSharper disable AccessToDisposedClosure Func action = async () => await provider.LoadInternalAsync(provider.ClientOptions, true, TestContext.Current.CancellationToken); - // ReSharper restore AccessToDisposedClosure await action.Should().ThrowExactlyAsync(); - startup.RequestCount.Should().Be(1); + handler.Mock.GetMatchCount(request).Should().Be(1); await Task.Delay(2.Seconds(), TestContext.Current.CancellationToken); - startup.RequestCount.Should().Be(1); + handler.Mock.GetMatchCount(request).Should().Be(1); } [Fact] public async Task Load_UriInvalid_FailFastEnabled() { - using var startup = new TestConfigServerStartup(); - startup.ReturnStatus = [500]; - - await using WebApplication app = TestWebApplicationBuilderFactory.Create().Build(); - startup.Configure(app); - await app.StartAsync(TestContext.Current.CancellationToken); - - using TestServer server = app.GetTestServer(); - server.BaseAddress = new Uri("http://localhost:8888"); - ConfigServerClientOptions options = GetCommonOptions(); options.Uri = "http://username:p@ssword@localhost:8888"; options.FailFast = true; - using var httpClientHandler = new ForwardingHttpClientHandler(server.CreateHandler()); - // ReSharper disable once AccessToDisposedClosure - using var provider = new ConfigServerConfigurationProvider(options, null, null, () => httpClientHandler, NullLoggerFactory.Instance); + using var handler = new DelegateToMockHttpClientHandler(); + using var provider = new ConfigServerConfigurationProvider(options, null, null, () => handler, NullLoggerFactory.Instance); - // ReSharper disable AccessToDisposedClosure Func action = async () => await provider.LoadInternalAsync(provider.ClientOptions, true, TestContext.Current.CancellationToken); - // ReSharper restore AccessToDisposedClosure await action.Should().ThrowExactlyAsync().WithMessage("One or more Config Server URIs in configuration are invalid."); + handler.Mock.VerifyNoOutstandingExpectation(); } [Fact] public async Task Load_ConfigServerReturnsBadStatus_FailFastEnabled() { - using var startup = new TestConfigServerStartup(); - startup.ReturnStatus = [500]; - - await using WebApplication app = TestWebApplicationBuilderFactory.Create().Build(); - startup.Configure(app); - await app.StartAsync(TestContext.Current.CancellationToken); - - using TestServer server = app.GetTestServer(); - server.BaseAddress = new Uri("http://localhost:8888"); - ConfigServerClientOptions options = GetCommonOptions(); options.FailFast = true; - using var httpClientHandler = new ForwardingHttpClientHandler(server.CreateHandler()); - // ReSharper disable once AccessToDisposedClosure - using var provider = new ConfigServerConfigurationProvider(options, null, null, () => httpClientHandler, NullLoggerFactory.Instance); + using var handler = new DelegateToMockHttpClientHandler(); + handler.Mock.Expect(HttpMethod.Get, $"http://localhost:8888/{options.Name}/{options.Environment}").Respond(HttpStatusCode.InternalServerError); + + using var provider = new ConfigServerConfigurationProvider(options, null, null, () => handler, NullLoggerFactory.Instance); - // ReSharper disable AccessToDisposedClosure Func action = async () => await provider.LoadInternalAsync(provider.ClientOptions, true, TestContext.Current.CancellationToken); - // ReSharper restore AccessToDisposedClosure await action.Should().ThrowExactlyAsync(); + handler.Mock.VerifyNoOutstandingExpectation(); } [Fact] public async Task Load_MultipleConfigServers_ReturnsBadStatus_StopsChecking_FailFastEnabled() { - using var startup = new TestConfigServerStartup(); - - startup.ReturnStatus = - [ - 500, - 500, - 500 - ]; - - await using WebApplication app = TestWebApplicationBuilderFactory.Create().Build(); - startup.Configure(app); - await app.StartAsync(TestContext.Current.CancellationToken); - - using TestServer server = app.GetTestServer(); - server.BaseAddress = new Uri("http://localhost:8888"); - ConfigServerClientOptions options = GetCommonOptions(); options.FailFast = true; options.Uri = "http://localhost:8888, http://localhost:8888, http://localhost:8888"; - using var httpClientHandler = new ForwardingHttpClientHandler(server.CreateHandler()); - // ReSharper disable once AccessToDisposedClosure - using var provider = new ConfigServerConfigurationProvider(options, null, null, () => httpClientHandler, NullLoggerFactory.Instance); + using var handler = new DelegateToMockHttpClientHandler(); + + MockedRequest request = handler.Mock.When(HttpMethod.Get, $"http://localhost:8888/{options.Name}/{options.Environment}") + .Respond(HttpStatusCode.InternalServerError); + + using var provider = new ConfigServerConfigurationProvider(options, null, null, () => handler, NullLoggerFactory.Instance); - // ReSharper disable AccessToDisposedClosure Func action = async () => await provider.LoadInternalAsync(provider.ClientOptions, true, TestContext.Current.CancellationToken); - // ReSharper restore AccessToDisposedClosure await action.Should().ThrowExactlyAsync(); - startup.RequestCount.Should().Be(1); + handler.Mock.GetMatchCount(request).Should().Be(1); await Task.Delay(2.Seconds(), TestContext.Current.CancellationToken); - startup.RequestCount.Should().Be(1); + handler.Mock.GetMatchCount(request).Should().Be(1); } [Fact] @@ -897,16 +729,6 @@ public async Task Load_ConfigServerReturnsBadStatus_FailFastEnabled_RetryEnabled { await TestFailureTracer.CaptureAsync(async tracer => { - using var startup = new TestConfigServerStartup(); - startup.ReturnStatus = [.. Enumerable.Repeat(500, 100)]; - - await using WebApplication app = TestWebApplicationBuilderFactory.Create().Build(); - startup.Configure(app); - await app.StartAsync(TestContext.Current.CancellationToken); - - using TestServer server = app.GetTestServer(); - server.BaseAddress = new Uri("http://localhost:8888"); - var options = new ConfigServerClientOptions { Name = "myName", @@ -919,26 +741,25 @@ await TestFailureTracer.CaptureAsync(async tracer => Timeout = 1000 }; - using var httpClientHandler = new ForwardingHttpClientHandler(server.CreateHandler()); - // ReSharper disable once AccessToDisposedClosure - using var provider = new ConfigServerConfigurationProvider(options, null, null, () => httpClientHandler, tracer.LoggerFactory); + using var handler = new DelegateToMockHttpClientHandler(); + MockedRequest request = handler.Mock.When(HttpMethod.Get, "http://localhost:8888/myName/Production").Respond(HttpStatusCode.InternalServerError); + + using var provider = new ConfigServerConfigurationProvider(options, null, null, () => handler, tracer.LoggerFactory); - // ReSharper disable AccessToDisposedClosure Func action = async () => await provider.LoadInternalAsync(provider.ClientOptions, true, TestContext.Current.CancellationToken); - // ReSharper restore AccessToDisposedClosure await action.Should().ThrowExactlyAsync(); await Task.Delay(2.Seconds(), TestContext.Current.CancellationToken); - startup.RequestCount.Should().BeGreaterThan(3); + handler.Mock.GetMatchCount(request).Should().BeGreaterThan(3); }); } [Fact] public async Task Load_ChangesDataDictionary() { - const string environment = """ + const string responseJson = """ { "name": "test-name", "profiles": [ @@ -959,25 +780,16 @@ public async Task Load_ChangesDataDictionary() } """; - using var startup = new TestConfigServerStartup(); - startup.Response = environment; - - await using WebApplication app = TestWebApplicationBuilderFactory.Create().Build(); - startup.Configure(app); - await app.StartAsync(TestContext.Current.CancellationToken); + ConfigServerClientOptions options = GetCommonOptions(); - using TestServer server = app.GetTestServer(); - server.BaseAddress = new Uri("http://localhost:8888"); + using var handler = new DelegateToMockHttpClientHandler(); + handler.Mock.Expect(HttpMethod.Get, $"http://localhost:8888/{options.Name}/{options.Environment}").Respond("application/json", responseJson); - ConfigServerClientOptions options = GetCommonOptions(); - using var httpClientHandler = new ForwardingHttpClientHandler(server.CreateHandler()); - // ReSharper disable once AccessToDisposedClosure - using var provider = new ConfigServerConfigurationProvider(options, null, null, () => httpClientHandler, NullLoggerFactory.Instance); + using var provider = new ConfigServerConfigurationProvider(options, null, null, () => handler, NullLoggerFactory.Instance); await provider.LoadInternalAsync(provider.ClientOptions, true, TestContext.Current.CancellationToken); - startup.LastRequest.Should().NotBeNull(); - startup.LastRequest.Path.Value.Should().Be($"/{options.Name}/{options.Environment}"); + handler.Mock.VerifyNoOutstandingExpectation(); provider.TryGet("key1", out string? value).Should().BeTrue(); value.Should().Be("value1"); @@ -991,9 +803,9 @@ public async Task Load_ChangesDataDictionary() } [Fact] - public async Task ReLoad_DataDictionary_With_New_Configurations() + public void ReLoad_DataDictionary_With_New_Configurations() { - const string environment = """ + const string responseJson = """ { "name": "test-name", "profiles": [ @@ -1015,24 +827,16 @@ public async Task ReLoad_DataDictionary_With_New_Configurations() } """; - using var startup = new TestConfigServerStartup(); - startup.Response = environment; - - await using WebApplication app = TestWebApplicationBuilderFactory.Create().Build(); - startup.Configure(app); - await app.StartAsync(TestContext.Current.CancellationToken); + ConfigServerClientOptions options = GetCommonOptions(); - using TestServer server = app.GetTestServer(); - server.BaseAddress = new Uri("http://localhost:8888"); + using var handler = new DelegateToMockHttpClientHandler(); + handler.Mock.Expect(HttpMethod.Get, $"http://localhost:8888/{options.Name}/{options.Environment}").Respond("application/json", responseJson); - ConfigServerClientOptions options = GetCommonOptions(); - using var httpClientHandler = new ForwardingHttpClientHandler(server.CreateHandler()); - // ReSharper disable once AccessToDisposedClosure - using var provider = new ConfigServerConfigurationProvider(options, null, null, () => httpClientHandler, NullLoggerFactory.Instance); + using var provider = new ConfigServerConfigurationProvider(options, null, null, () => handler, NullLoggerFactory.Instance); provider.Load(); - startup.LastRequest.Should().NotBeNull(); + handler.Mock.VerifyNoOutstandingExpectation(); provider.TryGet("featureToggles:ShowModule:0", out string? value).Should().BeTrue(); value.Should().Be("FT1"); provider.TryGet("featureToggles:ShowModule:1", out value).Should().BeTrue(); @@ -1042,29 +846,33 @@ public async Task ReLoad_DataDictionary_With_New_Configurations() provider.TryGet("enableSettings", out value).Should().BeTrue(); value.Should().Be("true"); - startup.Reset(); + handler.Mock.Clear(); - startup.Response = """ - { - "name": "test-name", - "profiles": [ - "Production" - ], - "label": "test-label", - "version": "test-version", - "propertySources": [ + const string newResponseJson = """ { - "name": "source", - "source": { - "featureToggles.ShowModule[0]": "none" - } + "name": "test-name", + "profiles": [ + "Production" + ], + "label": "test-label", + "version": "test-version", + "propertySources": [ + { + "name": "source", + "source": { + "featureToggles.ShowModule[0]": "none" + } + } + ] } - ] - } - """; + """; + + handler.Mock.Expect(HttpMethod.Get, $"http://localhost:8888/{options.Name}/{options.Environment}").Respond("application/json", newResponseJson); provider.Load(); + handler.Mock.VerifyNoOutstandingExpectation(); + provider.TryGet("featureToggles:ShowModule:0", out value).Should().BeTrue(); value.Should().Be("none"); provider.TryGet("featureToggles:ShowModule:1", out _).Should().BeFalse(); @@ -1158,9 +966,9 @@ public void DataDictionary_DoesNotContainRedundantClientSettings() } [Fact] - public async Task Reload_And_Bind_Without_Throwing_Exception() + public void Reload_And_Bind_Without_Throwing_Exception() { - const string environment = """ + const string responseJson = """ { "name": "test-name", "profiles": [ @@ -1180,20 +988,12 @@ public async Task Reload_And_Bind_Without_Throwing_Exception() } """; - using var startup = new TestConfigServerStartup(); - startup.Response = environment; - - await using WebApplication app = TestWebApplicationBuilderFactory.Create().Build(); - startup.Configure(app); - await app.StartAsync(TestContext.Current.CancellationToken); - ConfigServerClientOptions clientOptions = GetCommonOptions(); - using TestServer server = app.GetTestServer(); - server.BaseAddress = new Uri(clientOptions.Uri!); - using var httpClientHandler = new ForwardingHttpClientHandler(server.CreateHandler()); - // ReSharper disable once AccessToDisposedClosure - using var provider = new ConfigServerConfigurationProvider(clientOptions, null, null, () => httpClientHandler, NullLoggerFactory.Instance); + using var handler = new DelegateToMockHttpClientHandler(); + handler.Mock.When(HttpMethod.Get, $"http://localhost:8888/{clientOptions.Name}/{clientOptions.Environment}").Respond("application/json", responseJson); + + using var provider = new ConfigServerConfigurationProvider(clientOptions, null, null, () => handler, NullLoggerFactory.Instance); var configurationBuilder = new ConfigurationBuilder(); configurationBuilder.Add(new TestConfigServerConfigurationSource(provider)); @@ -1204,7 +1004,6 @@ public async Task Reload_And_Bind_Without_Throwing_Exception() _ = Task.Run(() => { - // ReSharper disable once AccessToDisposedClosure while (!tokenSource.IsCancellationRequested) { configurationRoot.Reload(); diff --git a/src/Configuration/test/ConfigServer.Test/ForwardingHttpClientHandler.cs b/src/Configuration/test/ConfigServer.Test/ForwardingHttpClientHandler.cs deleted file mode 100644 index 27100c24a7..0000000000 --- a/src/Configuration/test/ConfigServer.Test/ForwardingHttpClientHandler.cs +++ /dev/null @@ -1,22 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the Apache 2.0 License. -// See the LICENSE file in the project root for more information. - -namespace Steeltoe.Configuration.ConfigServer.Test; - -internal sealed class ForwardingHttpClientHandler : HttpClientHandler -{ - private readonly HttpMessageInvoker _invoker; - - public ForwardingHttpClientHandler(HttpMessageHandler innerHandler) - { - ArgumentNullException.ThrowIfNull(innerHandler); - - _invoker = new HttpMessageInvoker(innerHandler, false); - } - - protected override Task SendAsync(HttpRequestMessage request, CancellationToken cancellationToken) - { - return _invoker.SendAsync(request, cancellationToken); - } -} diff --git a/src/Configuration/test/ConfigServer.Test/TestConfigServerStartup.cs b/src/Configuration/test/ConfigServer.Test/TestConfigServerStartup.cs deleted file mode 100644 index e3947d75d1..0000000000 --- a/src/Configuration/test/ConfigServer.Test/TestConfigServerStartup.cs +++ /dev/null @@ -1,129 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the Apache 2.0 License. -// See the LICENSE file in the project root for more information. - -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http; - -namespace Steeltoe.Configuration.ConfigServer.Test; - -internal sealed class TestConfigServerStartup : IDisposable -{ - private volatile CountdownEvent _firstRequestCountdownEvent = new(1); - private volatile string? _response; - private volatile int[] _returnStatus = [200]; - private volatile HttpRequestInfo? _lastRequest; - private volatile int _requestCount; - private volatile string _label = string.Empty; - private volatile string _appName = string.Empty; - private volatile string _env = string.Empty; - - public string? Response - { - get => _response; - set => _response = value; - } - - public int[] ReturnStatus - { - get => _returnStatus; - set => _returnStatus = value; - } - - public HttpRequestInfo? LastRequest - { - get => _lastRequest; - set => _lastRequest = value; - } - - public int RequestCount - { - get => _requestCount; - set => _requestCount = value; - } - - public string Label - { - get => _label; - set => _label = value; - } - - public string AppName - { - get => _appName; - set => _appName = value; - } - - public string Env - { - get => _env; - set => _env = value; - } - - public void Reset() - { - Response = null; - ReturnStatus = [200]; - LastRequest = null; - RequestCount = 0; - Label = AppName = Env = string.Empty; - - _firstRequestCountdownEvent.Dispose(); - _firstRequestCountdownEvent = new CountdownEvent(1); - } - - public void Configure(IApplicationBuilder app) - { - app.Run(async context => - { - LastRequest = await HttpRequestInfo.CopyFromAsync(context); - context.Response.StatusCode = GetStatusCode(context.Request.Path); - RequestCount++; - - if (context.Response.StatusCode == 200) - { - context.Response.Headers.Append("content-type", "application/json"); - - if (Response != null) - { - await context.Response.WriteAsync(Response, context.RequestAborted); - } - } - - if (RequestCount == 1) - { - _firstRequestCountdownEvent.Signal(); - } - }); - } - - private int GetStatusCode(string path) - { - if (!string.IsNullOrEmpty(Label) && !path.Contains(Label, StringComparison.Ordinal)) - { - return 404; - } - - if (!string.IsNullOrEmpty(Env) && !path.Contains(Env, StringComparison.Ordinal)) - { - return 404; - } - - if (!string.IsNullOrEmpty(AppName) && !path.Contains(AppName, StringComparison.Ordinal)) - { - return 404; - } - - return ReturnStatus[RequestCount]; - } - - public bool WaitForFirstRequest(TimeSpan timeout) - { - return _firstRequestCountdownEvent.Wait(timeout, TestContext.Current.CancellationToken); - } - - public void Dispose() - { - _firstRequestCountdownEvent.Dispose(); - } -} From cdccbd23650573613f903766058ffd233d8b1f62 Mon Sep 17 00:00:00 2001 From: Bart Koelman <104792814+bart-vmware@users.noreply.github.com> Date: Thu, 2 Apr 2026 16:29:21 +0200 Subject: [PATCH 21/29] Various fixes - Improved log messages (and don't log multiple times), fix exception stack traces - Don't remote-fetch twice at startup (load + timer that immediately fired) - Moved options bind and remote-fetch from constructor to Load, fix combination with placeholder --- .../CompositeConfigurationProvider.cs | 5 +- .../ConfigServer/ConfigServerClientOptions.cs | 2 +- ...figServerConfigurationBuilderExtensions.cs | 5 +- .../ConfigServerConfigurationProvider.cs | 148 +++++++++++------- .../ConfigServerConfigurationSource.cs | 68 ++------ .../ConfigServerHealthContributor.cs | 21 ++- .../src/ConfigServer/ConfigurationSchema.json | 2 +- .../ConfigServerClientOptionsTest.cs | 14 +- ...rConfigurationBuilderExtensionsCoreTest.cs | 3 +- ...ServerConfigurationProviderTest.Loading.cs | 80 +++++----- ...erverConfigurationProviderTest.Settings.cs | 32 +++- .../ConfigServerConfigurationProviderTest.cs | 57 ++++--- .../ConfigServerConfigurationSourceTest.cs | 8 - .../ConfigServerHostedServiceTest.cs | 2 + .../TestConfigServerConfigurationSource.cs | 17 -- 15 files changed, 238 insertions(+), 226 deletions(-) delete mode 100644 src/Configuration/test/ConfigServer.Test/TestConfigServerConfigurationSource.cs diff --git a/src/Configuration/src/Abstractions/CompositeConfigurationProvider.cs b/src/Configuration/src/Abstractions/CompositeConfigurationProvider.cs index 0664a92946..dae60f1e3f 100644 --- a/src/Configuration/src/Abstractions/CompositeConfigurationProvider.cs +++ b/src/Configuration/src/Abstractions/CompositeConfigurationProvider.cs @@ -53,10 +53,9 @@ private void Load(bool isReload) public IEnumerable GetChildKeys(IEnumerable earlierKeys, string? parentPath) { + ArgumentNullException.ThrowIfNull(earlierKeys); + string[] earlierKeysArray = earlierKeys as string[] ?? earlierKeys.ToArray(); -#pragma warning disable S3236 // Caller information arguments should not be provided explicitly - ArgumentNullException.ThrowIfNull(earlierKeysArray, nameof(earlierKeys)); -#pragma warning restore S3236 // Caller information arguments should not be provided explicitly if (_logger.IsEnabled(LogLevel.Trace)) { diff --git a/src/Configuration/src/ConfigServer/ConfigServerClientOptions.cs b/src/Configuration/src/ConfigServer/ConfigServerClientOptions.cs index 57de14eefe..f8be0b3b41 100644 --- a/src/Configuration/src/ConfigServer/ConfigServerClientOptions.cs +++ b/src/Configuration/src/ConfigServer/ConfigServerClientOptions.cs @@ -133,7 +133,7 @@ public bool ValidateCertificatesAlt public int TokenTtl { get; set; } = 300_000; /// - /// Gets or sets the vault token renew rate (in milliseconds). Default value: 60_000 (1 minute). + /// Gets or sets the Vault token renew rate (in milliseconds). Default value: 60_000 (1 minute). /// public int TokenRenewRate { get; set; } = 60_000; diff --git a/src/Configuration/src/ConfigServer/ConfigServerConfigurationBuilderExtensions.cs b/src/Configuration/src/ConfigServer/ConfigServerConfigurationBuilderExtensions.cs index 1e39433696..9cb9504617 100644 --- a/src/Configuration/src/ConfigServer/ConfigServerConfigurationBuilderExtensions.cs +++ b/src/Configuration/src/ConfigServer/ConfigServerConfigurationBuilderExtensions.cs @@ -133,10 +133,7 @@ internal static IConfigurationBuilder AddConfigServer(this IConfigurationBuilder builder.AddCloudFoundry(); builder.AddKubernetesServiceBindings(); - ConfigServerConfigurationSource source = builder is IConfiguration configuration - ? new ConfigServerConfigurationSource(options, configuration, configure, createHttpClientHandler, loggerFactory) - : new ConfigServerConfigurationSource(options, builder.Sources, builder.Properties, configure, createHttpClientHandler, loggerFactory); - + var source = new ConfigServerConfigurationSource(options, builder.Sources, builder.Properties, configure, createHttpClientHandler, loggerFactory); builder.Add(source); } diff --git a/src/Configuration/src/ConfigServer/ConfigServerConfigurationProvider.cs b/src/Configuration/src/ConfigServer/ConfigServerConfigurationProvider.cs index 21747d5e44..f2a602d838 100644 --- a/src/Configuration/src/ConfigServer/ConfigServerConfigurationProvider.cs +++ b/src/Configuration/src/ConfigServer/ConfigServerConfigurationProvider.cs @@ -59,6 +59,7 @@ internal sealed partial class ConfigServerConfigurationProvider : ConfigurationP private Timer? _vaultRenewTimer; private volatile DiscoveryLookupResult? _lastDiscoveryLookupResult; private volatile ConfigServerClientOptions _clientOptions; + private long _isReload; internal static JsonSerializerOptions SerializerOptions { get; } = new() { @@ -96,7 +97,7 @@ internal ConfigServerConfigurationProvider(ConfigServerClientOptions clientOptio ArgumentNullException.ThrowIfNull(loggerFactory); _logger = loggerFactory.CreateLogger(); - _shutdownToken = _shutdownTokenSource.Token; + _shutdownToken = _shutdownTokenSource.Token; // Don't inline: the token survives disposal, while the source does not. IConfiguration effectiveConfiguration = configuration ?? new ConfigurationBuilder().Build(); _configurer = new ConfigureConfigServerClientOptions(effectiveConfiguration, configure); _configServerDiscoveryService = new ConfigServerDiscoveryService(effectiveConfiguration, loggerFactory); @@ -115,14 +116,24 @@ internal ConfigServerConfigurationProvider(ConfigServerClientOptions clientOptio _disposeHttpClientHandler = true; } - OnSettingsChanged(); - _changeTokenRegistration = ChangeToken.OnChange(effectiveConfiguration.GetReloadToken, OnSettingsChanged); + _changeTokenRegistration = ChangeToken.OnChange(effectiveConfiguration.GetReloadToken, () => OnSettingsChanged(true)); } - private void OnSettingsChanged() + private void OnSettingsChanged(bool skipTimerDueTime) { + LogEnteringOnSettingsChanged(); + ConfigServerClientOptions newOptions = _initialOptions.Clone(); - _configurer.Configure(newOptions); + + try + { + _configurer.Configure(newOptions); + } + catch (Exception exception) + { + LogBindSettingsFailed(exception); + throw; + } lock (_lifecycleLock) { @@ -136,12 +147,12 @@ private void OnSettingsChanged() _clientOptions = newOptions; - UpdateConfigurationReloadTimer(newOptions, previousPollingInterval); - UpdateVaultRenewTimer(newOptions, previousTokenRenewRate); + UpdateConfigurationReloadTimer(newOptions, previousPollingInterval, skipTimerDueTime); + UpdateVaultRenewTimer(newOptions, previousTokenRenewRate, skipTimerDueTime); } } - private void UpdateConfigurationReloadTimer(ConfigServerClientOptions optionsSnapshot, TimeSpan previousPollingInterval) + private void UpdateConfigurationReloadTimer(ConfigServerClientOptions optionsSnapshot, TimeSpan previousPollingInterval, bool skipDueTime) { if (optionsSnapshot.PollingInterval == TimeSpan.Zero || !optionsSnapshot.Enabled) { @@ -150,15 +161,16 @@ private void UpdateConfigurationReloadTimer(ConfigServerClientOptions optionsSna } else if (_configurationReloadTimer == null) { - _configurationReloadTimer = new Timer(_ => ConfigurationReloadTimerTick(), null, TimeSpan.Zero, optionsSnapshot.PollingInterval); + _configurationReloadTimer = new Timer(_ => ConfigurationReloadTimerTick(), null, skipDueTime ? TimeSpan.Zero : optionsSnapshot.PollingInterval, + optionsSnapshot.PollingInterval); } else if (previousPollingInterval != optionsSnapshot.PollingInterval) { - _configurationReloadTimer.Change(TimeSpan.Zero, optionsSnapshot.PollingInterval); + _configurationReloadTimer.Change(skipDueTime ? TimeSpan.Zero : optionsSnapshot.PollingInterval, optionsSnapshot.PollingInterval); } } - private void UpdateVaultRenewTimer(ConfigServerClientOptions optionsSnapshot, int previousTokenRenewRate) + private void UpdateVaultRenewTimer(ConfigServerClientOptions optionsSnapshot, int previousTokenRenewRate, bool skipDueTime) { if (string.IsNullOrEmpty(optionsSnapshot.Token) || optionsSnapshot.DisableTokenRenewal || optionsSnapshot is not { Uri: not null, IsMultiServerConfiguration: false }) @@ -168,12 +180,13 @@ private void UpdateVaultRenewTimer(ConfigServerClientOptions optionsSnapshot, in } else if (_vaultRenewTimer == null) { - _vaultRenewTimer = new Timer(_ => VaultRenewTimerTick(), null, TimeSpan.FromMilliseconds(optionsSnapshot.TokenRenewRate), - TimeSpan.FromMilliseconds(optionsSnapshot.TokenRenewRate)); + TimeSpan refreshInterval = TimeSpan.FromMilliseconds(optionsSnapshot.TokenRenewRate); + _vaultRenewTimer = new Timer(_ => VaultRenewTimerTick(), null, skipDueTime ? TimeSpan.Zero : refreshInterval, refreshInterval); } else if (previousTokenRenewRate != optionsSnapshot.TokenRenewRate) { - _vaultRenewTimer.Change(TimeSpan.FromMilliseconds(optionsSnapshot.TokenRenewRate), TimeSpan.FromMilliseconds(optionsSnapshot.TokenRenewRate)); + TimeSpan refreshInterval = TimeSpan.FromMilliseconds(optionsSnapshot.TokenRenewRate); + _vaultRenewTimer.Change(skipDueTime ? TimeSpan.Zero : refreshInterval, refreshInterval); } } @@ -198,7 +211,7 @@ private void ConfigurationReloadTimerTick() return; } - LogConfigurationReloadLockObtained(); + LogConfigurationReloadCycleLockObtained(); ConfigServerClientOptions optionsSnapshot = ClientOptions; #pragma warning disable S4462 // Calls to "async" methods should not be blocking @@ -211,9 +224,9 @@ private void ConfigurationReloadTimerTick() } catch (Exception exception) { - if (!exception.IsCancellation()) + if (!_shutdownToken.IsCancellationRequested) { - LogFailedToReloadConfiguration(exception); + LogConfigurationReloadCycleFailed(exception); } } finally @@ -250,7 +263,7 @@ private void VaultRenewTimerTick() return; } - LogVaultRenewLockObtained(); + LogVaultRenewCycleLockObtained(); #pragma warning disable S4462 // Calls to "async" methods should not be blocking // Justification: Configuration sources and providers don't support async. @@ -261,9 +274,9 @@ private void VaultRenewTimerTick() } catch (Exception exception) { - if (!exception.IsCancellation()) + if (!_shutdownToken.IsCancellationRequested) { - LogFailedToRenewVaultToken(exception); + LogVaultRenewCycleFailed(exception); } } finally @@ -284,17 +297,35 @@ private void VaultRenewTimerTick() /// public override void Load() { + long previousIsReload = Interlocked.CompareExchange(ref _isReload, 1, 0); + + if (previousIsReload == 0) + { + OnSettingsChanged(false); + } + + ConfigServerClientOptions optionsSnapshot = ClientOptions; + try { #pragma warning disable S4462 // Calls to "async" methods should not be blocking // Justification: Configuration sources and providers don't support async. - LoadInternalAsync(ClientOptions, true, _shutdownToken).GetAwaiter().GetResult(); + LoadInternalAsync(optionsSnapshot, true, _shutdownToken).GetAwaiter().GetResult(); #pragma warning restore S4462 // Calls to "async" methods should not be blocking } catch (OperationCanceledException) when (_shutdownToken.IsCancellationRequested) { // Expected during disposal; silently ignore. } + catch (ConfigServerException exception) + { + if (optionsSnapshot.FailFast) + { + throw; + } + + LogFetchingRemoteConfigurationFailed(exception); + } } internal async Task LoadInternalAsync(ConfigServerClientOptions optionsSnapshot, bool updateDictionary, @@ -312,18 +343,17 @@ public override void Load() { int attempts = 0; int backOff = optionsSnapshot.Retry.InitialInterval; + List errors = []; do { - LogFetchingConfiguration(); - try { return await DoLoadAsync(optionsSnapshot, updateDictionary, cancellationToken); } catch (ConfigServerException exception) { - LogFailedFetchingConfiguration(exception); + errors.Add(exception); attempts++; if (attempts < optionsSnapshot.Retry.MaxAttempts) @@ -334,19 +364,21 @@ public override void Load() } else { - throw; + throw new ConfigServerException($"Failed fetching remote configuration from server(s) after {attempts} attempts.", + new AggregateException(null, errors)); } } } while (true); } - LogFetchingConfiguration(); return await DoLoadAsync(optionsSnapshot, updateDictionary, cancellationToken); } internal async Task DoLoadAsync(ConfigServerClientOptions optionsSnapshot, bool updateDictionary, CancellationToken cancellationToken) { + LogFetchingRemoteConfiguration(); + ApplyLastDiscoveryLookupResultToClientOptions(optionsSnapshot); Exception? error = null; @@ -416,15 +448,7 @@ public override void Load() error = exception; } - LogCouldNotLocatePropertySource(error); - - if (optionsSnapshot.FailFast) - { - LogFailFastEnabled(error); - throw new ConfigServerException("Could not locate PropertySource, fail fast property is set, failing", error); - } - - return null; + throw new ConfigServerException("Failed fetching remote configuration from server(s).", error); } internal void ApplyLastDiscoveryLookupResultToClientOptions(ConfigServerClientOptions optionsSnapshot) @@ -897,6 +921,7 @@ public void Dispose() return; } + LogDisposing(); _shutdownTokenSource.Cancel(); _changeTokenRegistration.Dispose(); ShutdownTimers(); @@ -906,6 +931,8 @@ public void Dispose() private void ShutdownTimers() { + // This is fast because in-flight timer callbacks terminate quickly: outstanding HTTP requests are canceled via shutdown token. + using var reloadTimerStopped = new ManualResetEvent(false); using var vaultTimerStopped = new ManualResetEvent(false); @@ -928,66 +955,66 @@ private void ShutdownTimers() _vaultRenewTimer = null; } - [LoggerMessage(Level = LogLevel.Trace, Message = "Entering polling configuration reload cycle.")] + [LoggerMessage(Level = LogLevel.Trace, Message = "Rebinding options after outer configuration change.")] + private partial void LogEnteringOnSettingsChanged(); + + [LoggerMessage(Level = LogLevel.Warning, Message = "Failed to bind Config Server options from configuration.")] + private partial void LogBindSettingsFailed(Exception exception); + + [LoggerMessage(Level = LogLevel.Debug, Message = "Entering remote configuration reload polling cycle.")] private partial void LogEnteringConfigurationReloadCycle(); - [LoggerMessage(Level = LogLevel.Trace, Message = "Polling configuration reload lock obtained.")] - private partial void LogConfigurationReloadLockObtained(); + [LoggerMessage(Level = LogLevel.Trace, Message = "Remote configuration reload polling lock obtained.")] + private partial void LogConfigurationReloadCycleLockObtained(); - [LoggerMessage(Level = LogLevel.Trace, Message = "Previous polling configuration reload cycle is still running, or already disposed; skipping this cycle.")] + [LoggerMessage(Level = LogLevel.Trace, Message = "Previous remote configuration reload cycle is still running, or already disposed; skipping this cycle.")] private partial void LogSkippingConfigurationReloadCycle(); - [LoggerMessage(Level = LogLevel.Warning, Message = "Failed to reload configuration during polling.")] - private partial void LogFailedToReloadConfiguration(Exception exception); + [LoggerMessage(Level = LogLevel.Warning, Message = "Failed to reload remote configuration during polling.")] + private partial void LogConfigurationReloadCycleFailed(Exception exception); - [LoggerMessage(Level = LogLevel.Trace, Message = "Polling configuration reload cycle completed, releasing lock.")] + [LoggerMessage(Level = LogLevel.Trace, Message = "Remote configuration reload polling cycle completed, releasing lock.")] private partial void LogConfigurationReloadCycleCompleted(); - [LoggerMessage(Level = LogLevel.Trace, Message = "Entering Vault token renewal cycle.")] + [LoggerMessage(Level = LogLevel.Debug, Message = "Entering Vault token renewal cycle.")] private partial void LogEnteringVaultRenewCycle(); [LoggerMessage(Level = LogLevel.Trace, Message = "Vault token renewal lock obtained.")] - private partial void LogVaultRenewLockObtained(); + private partial void LogVaultRenewCycleLockObtained(); [LoggerMessage(Level = LogLevel.Trace, Message = "Previous Vault token renewal cycle is still running, or already disposed; skipping this cycle.")] private partial void LogSkippingVaultRenewCycle(); [LoggerMessage(Level = LogLevel.Warning, Message = "Failed to renew Vault token.")] - private partial void LogFailedToRenewVaultToken(Exception exception); + private partial void LogVaultRenewCycleFailed(Exception exception); [LoggerMessage(Level = LogLevel.Trace, Message = "Vault token renewal cycle completed, releasing lock.")] private partial void LogVaultRenewCycleCompleted(); - [LoggerMessage(Level = LogLevel.Information, Message = "Config Server client disabled, not fetching configuration.")] + [LoggerMessage(Level = LogLevel.Information, Message = "Config Server client disabled, not fetching remote configuration.")] private partial void LogConfigServerClientDisabled(); - [LoggerMessage(Level = LogLevel.Debug, Message = "Fetching configuration from server(s).")] - private partial void LogFetchingConfiguration(); - - [LoggerMessage(Level = LogLevel.Warning, Message = "Failed fetching configuration from server(s).")] - private partial void LogFailedFetchingConfiguration(Exception exception); + [LoggerMessage(Level = LogLevel.Debug, Message = "Fetching remote configuration from server(s).")] + private partial void LogFetchingRemoteConfiguration(); [LoggerMessage(Level = LogLevel.Trace, Message = "Processing label '{Label}'.")] private partial void LogProcessingLabel(string? label); - [LoggerMessage(Level = LogLevel.Debug, Message = "Multiple Config Server Uris listed.")] + [LoggerMessage(Level = LogLevel.Debug, Message = "Multiple Config Server uris listed.")] private partial void LogMultipleConfigServerUris(); [LoggerMessage(Level = LogLevel.Debug, Message = "Located environment with name {Name}, profiles {Profiles}, label {Label}, version {Version} and state {State}.")] private partial void LogEnvironmentLocated(string? name, string profiles, string? label, string? version, string? state); - [LoggerMessage(Level = LogLevel.Trace, Message = "Data has changed, raising configuration reload.")] + [LoggerMessage(Level = LogLevel.Trace, Message = "Remote data has changed, raising configuration reload.")] private partial void LogDataChanged(); - [LoggerMessage(Level = LogLevel.Trace, Message = "Data has not changed.")] + [LoggerMessage(Level = LogLevel.Trace, Message = "Remote data has not changed.")] private partial void LogDataNotChanged(); - [LoggerMessage(Level = LogLevel.Warning, Message = "Could not locate property source.")] - private partial void LogCouldNotLocatePropertySource(Exception? error); - - [LoggerMessage(Level = LogLevel.Trace, Message = "Failure with FailFast enabled, throwing ConfigServerException.")] - private partial void LogFailFastEnabled(Exception? error); + [LoggerMessage(Level = LogLevel.Warning, Message = "Failed fetching remote configuration from server(s).")] + private partial void LogFetchingRemoteConfigurationFailed(Exception? error); [LoggerMessage(Level = LogLevel.Debug, Message = "Adding credentials from '{RequestUri}' to Authorization header.")] private partial void LogAddingCredentials(string requestUri); @@ -1028,5 +1055,8 @@ private void ShutdownTimers() [LoggerMessage(Level = LogLevel.Error, Message = "Unable to renew Vault token {Token}. The token is likely invalid or has expired.")] private partial void LogUnableToRenewVaultToken(Exception exception, string token); + [LoggerMessage(Level = LogLevel.Trace, Message = "Disposing Config Server configuration provider.")] + private partial void LogDisposing(); + private sealed record DiscoveryLookupResult(string ConfigServerUri, string? Username, string? Password); } diff --git a/src/Configuration/src/ConfigServer/ConfigServerConfigurationSource.cs b/src/Configuration/src/ConfigServer/ConfigServerConfigurationSource.cs index e047337a6f..7c8dc950dd 100644 --- a/src/Configuration/src/ConfigServer/ConfigServerConfigurationSource.cs +++ b/src/Configuration/src/ConfigServer/ConfigServerConfigurationSource.cs @@ -12,8 +12,8 @@ internal sealed class ConfigServerConfigurationSource : IConfigurationSource { private readonly ILoggerFactory _loggerFactory; - internal List Sources { get; } = []; - internal Dictionary Properties { get; } = []; + internal List Sources { get; } + internal Dictionary Properties { get; } /// /// Gets the initial options the client uses to contact Config Server. @@ -36,39 +36,6 @@ internal sealed class ConfigServerConfigurationSource : IConfigurationSource /// public Func? CreateHttpClientHandler { get; } - /// - /// Initializes a new instance of the class. - /// - /// - /// The initial options the client uses to contact Config Server. - /// - /// - /// The configuration the client uses to contact Config Server. Entries overrule . - /// - /// - /// An optional delegate that further configures options from code, after settings from have been applied. - /// - /// - /// An optional factory to create the HTTP client handler, used to mock HTTP requests to Config Server in tests. When provided, the caller is responsible - /// for handler disposal. - /// - /// - /// Used for internal logging. Pass to disable logging. - /// - public ConfigServerConfigurationSource(ConfigServerClientOptions defaultOptions, IConfiguration configuration, Action? configure, - Func? createHttpClientHandler, ILoggerFactory loggerFactory) - { - ArgumentNullException.ThrowIfNull(defaultOptions); - ArgumentNullException.ThrowIfNull(configuration); - ArgumentNullException.ThrowIfNull(loggerFactory); - - DefaultOptions = defaultOptions; - Configure = configure; - CreateHttpClientHandler = createHttpClientHandler; - Configuration = configuration; - _loggerFactory = loggerFactory; - } - /// /// Initializes a new instance of the class. /// @@ -102,11 +69,7 @@ public ConfigServerConfigurationSource(ConfigServerClientOptions defaultOptions, ArgumentNullException.ThrowIfNull(loggerFactory); Sources = sources.ToList(); - - if (properties != null) - { - Properties = new Dictionary(properties); - } + Properties = properties != null ? new Dictionary(properties) : []; DefaultOptions = defaultOptions; Configure = configure; @@ -125,26 +88,19 @@ public ConfigServerConfigurationSource(ConfigServerClientOptions defaultOptions, /// public IConfigurationProvider Build(IConfigurationBuilder builder) { - if (Configuration == null) - { - // Create our own builder to build sources - var configurationBuilder = new ConfigurationBuilder(); + var configurationBuilder = new ConfigurationBuilder(); - foreach (IConfigurationSource source in Sources) - { - configurationBuilder.Add(source); - } - - // Use properties provided - foreach (KeyValuePair pair in Properties) - { - configurationBuilder.Properties.Add(pair); - } + foreach (IConfigurationSource source in Sources) + { + configurationBuilder.Add(source); + } - // Create configuration - Configuration = configurationBuilder.Build(); + foreach (KeyValuePair pair in Properties) + { + configurationBuilder.Properties.Add(pair); } + Configuration = configurationBuilder.Build(); return new ConfigServerConfigurationProvider(this, _loggerFactory); } } diff --git a/src/Configuration/src/ConfigServer/ConfigServerHealthContributor.cs b/src/Configuration/src/ConfigServer/ConfigServerHealthContributor.cs index b6b9c821ab..5517cb3b11 100644 --- a/src/Configuration/src/ConfigServer/ConfigServerHealthContributor.cs +++ b/src/Configuration/src/ConfigServer/ConfigServerHealthContributor.cs @@ -76,10 +76,10 @@ internal void UpdateHealth(HealthCheckResult health, IList sourc foreach (PropertySource source in sources) { - LogReturningPropertySource(source.Name); names.Add(source.Name); } + LogReturningPropertySources(string.Join(", ", names)); health.Details.Add("propertySources", names); } @@ -92,7 +92,17 @@ internal void UpdateHealth(HealthCheckResult health, IList sourc { LastAccess = currentTime; LogCacheStale(); - Cached = await provider.LoadInternalAsync(optionsSnapshot, false, cancellationToken); + + try + { + Cached = await provider.LoadInternalAsync(optionsSnapshot, false, cancellationToken); + } + catch (ConfigServerException exception) + { + LogFetchFailed(exception); + Cached = null; + return null; + } } return Cached?.PropertySources; @@ -114,14 +124,17 @@ internal bool IsCacheStale(long accessTime, ConfigServerClientOptions optionsSna [LoggerMessage(Level = LogLevel.Debug, Message = "No Config Server provider found.")] private partial void LogNoProviderFound(); + [LoggerMessage(Level = LogLevel.Debug, Message = "Failed fetching remote configuration from server(s).")] + private partial void LogFetchFailed(Exception exception); + [LoggerMessage(Level = LogLevel.Debug, Message = "No property sources found.")] private partial void LogNoPropertySourcesFound(); [LoggerMessage(Level = LogLevel.Debug, Message = "Config Server health check returning UP.")] private partial void LogHealthCheckReturningUp(); - [LoggerMessage(Level = LogLevel.Debug, Message = "Returning property source {PropertySource}.")] - private partial void LogReturningPropertySource(string? propertySource); + [LoggerMessage(Level = LogLevel.Debug, Message = "Returning property sources: {PropertySources}.")] + private partial void LogReturningPropertySources(string propertySources); [LoggerMessage(Level = LogLevel.Debug, Message = "Cache stale, fetching config server health.")] private partial void LogCacheStale(); diff --git a/src/Configuration/src/ConfigServer/ConfigurationSchema.json b/src/Configuration/src/ConfigServer/ConfigurationSchema.json index 8f9e677fc0..0bfa06a7a1 100644 --- a/src/Configuration/src/ConfigServer/ConfigurationSchema.json +++ b/src/Configuration/src/ConfigServer/ConfigurationSchema.json @@ -173,7 +173,7 @@ }, "TokenRenewRate": { "type": "integer", - "description": "Gets or sets the vault token renew rate (in milliseconds). Default value: 60_000 (1 minute)." + "description": "Gets or sets the Vault token renew rate (in milliseconds). Default value: 60_000 (1 minute)." }, "TokenTtl": { "type": "integer", diff --git a/src/Configuration/test/ConfigServer.Test/ConfigServerClientOptionsTest.cs b/src/Configuration/test/ConfigServer.Test/ConfigServerClientOptionsTest.cs index 005c6a3e5a..365d77fea4 100644 --- a/src/Configuration/test/ConfigServer.Test/ConfigServerClientOptionsTest.cs +++ b/src/Configuration/test/ConfigServer.Test/ConfigServerClientOptionsTest.cs @@ -10,6 +10,7 @@ using Microsoft.Extensions.Options; using RichardSzalay.MockHttp; using Steeltoe.Common.TestResources; +using Steeltoe.Configuration.Placeholder; namespace Steeltoe.Configuration.ConfigServer.Test; @@ -333,12 +334,15 @@ public void Changes_in_IConfiguration_update_provider_options_and_injected_optio fileProvider.IncludeAppSettingsJsonFile(""" { + "custom": { + "profileName": "example-profile" + }, "spring": { "cloud": { "config": { "uri": "https://config.server.com:9999", "name": "example-app-name", - "env": "example-profile", + "env": "${custom:profileName}", "timeout": 30000 } } @@ -361,6 +365,7 @@ public void Changes_in_IConfiguration_update_provider_options_and_injected_optio var configurationBuilder = new ConfigurationBuilder(); configurationBuilder.AddInMemoryAppSettingsJsonFile(fileProvider); + configurationBuilder.AddPlaceholderResolver(); // ReSharper disable once AccessToDisposedClosure configurationBuilder.AddConfigServer(initialOptions, configureOptions, () => handler, NullLoggerFactory.Instance); IConfigurationRoot configuration = configurationBuilder.Build(); @@ -395,12 +400,15 @@ public void Changes_in_IConfiguration_update_provider_options_and_injected_optio fileProvider.ReplaceAppSettingsJsonFile(""" { - "spring": { + "custom": { + "profileName": "example-profile" + }, + "spring": { "cloud": { "config": { "uri": "https://alternate-config.server.com:7777", "name": "alternate-name", - "env": "example-profile", + "env": "${custom:profileName}", "timeout": 15000, "label": "alternate-label" } diff --git a/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationBuilderExtensionsCoreTest.cs b/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationBuilderExtensionsCoreTest.cs index 6706391ae3..f299b94b3a 100644 --- a/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationBuilderExtensionsCoreTest.cs +++ b/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationBuilderExtensionsCoreTest.cs @@ -42,7 +42,8 @@ public void AddConfigServer_WithLoggerFactorySucceeds() IList logMessages = loggerProvider.GetAll(); - logMessages.Should().Contain("DBUG Steeltoe.Configuration.ConfigServer.ConfigServerConfigurationProvider: Fetching configuration from server(s)."); + logMessages.Should().Contain( + "DBUG Steeltoe.Configuration.ConfigServer.ConfigServerConfigurationProvider: Fetching remote configuration from server(s)."); } [Fact] diff --git a/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationProviderTest.Loading.cs b/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationProviderTest.Loading.cs index 1bf4c7e831..cbe9ff0c23 100644 --- a/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationProviderTest.Loading.cs +++ b/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationProviderTest.Loading.cs @@ -28,6 +28,8 @@ public async Task RemoteLoadAsync_HostTimesOut() var httpClientHandler = new SlowHttpClientHandler(1.Seconds(), new HttpResponseMessage()); using var provider = new ConfigServerConfigurationProvider(options, null, null, () => httpClientHandler, NullLoggerFactory.Instance); + provider.Load(); + List requestUris = [new("http://localhost:9999/app/profile")]; Func action = async () => await provider.RemoteLoadAsync(provider.ClientOptions, requestUris, null, TestContext.Current.CancellationToken); @@ -111,6 +113,7 @@ await TestFailureTracer.CaptureAsync(async tracer => }); using var provider = new ConfigServerConfigurationProvider(options, null, null, () => handler, tracer.LoggerFactory); + provider.Load(); bool firstRequestCompleted = firstRequestCountdownEvent.Wait(2.Seconds(), TestContext.Current.CancellationToken); firstRequestCompleted.Should().BeTrue(); @@ -174,6 +177,7 @@ await TestFailureTracer.CaptureAsync(async tracer => }); using var provider = new ConfigServerConfigurationProvider(options, null, null, () => handler, tracer.LoggerFactory); + provider.Load(); bool firstRequestCompleted = firstRequestCountdownEvent.Wait(2.Seconds(), TestContext.Current.CancellationToken); firstRequestCompleted.Should().BeTrue(); @@ -202,6 +206,7 @@ public async Task Create_WithNonZeroPollingIntervalAndClientDisabled_PollingConf MockedRequest request = handler.Mock.When(HttpMethod.Get, "http://localhost:8888/myName/Production/label").Respond(HttpStatusCode.OK); using var provider = new ConfigServerConfigurationProvider(options, null, null, () => handler, NullLoggerFactory.Instance); + provider.Load(); await Task.Delay(2.Seconds(), TestContext.Current.CancellationToken); handler.Mock.GetMatchCount(request).Should().Be(0); @@ -398,7 +403,7 @@ public void OnSettingsChanged_stops_vault_renew_timer_when_renewal_becomes_disab } [Fact] - public async Task Load_MultipleConfigServers_SocketError_FallsBackToNextServer() + public void Load_MultipleConfigServers_SocketError_FallsBackToNextServer() { const string responseJson = """ { @@ -431,15 +436,14 @@ public async Task Load_MultipleConfigServers_SocketError_FallsBackToNextServer() }; using var provider = new ConfigServerConfigurationProvider(options, null, null, () => handler, NullLoggerFactory.Instance); - - await provider.LoadInternalAsync(provider.ClientOptions, true, TestContext.Current.CancellationToken); + provider.Load(); provider.TryGet("key1", out string? value).Should().BeTrue(); value.Should().Be("value1"); } [Fact] - public async Task DoLoad_IdenticalData_DoesNotTriggerReload() + public void Load_IdenticalData_DoesNotTriggerReload() { const string responseJson = """ { @@ -467,15 +471,15 @@ public async Task DoLoad_IdenticalData_DoesNotTriggerReload() }; using var provider = new ConfigServerConfigurationProvider(options, null, null, () => handler, NullLoggerFactory.Instance); + provider.Load(); - await provider.DoLoadAsync(provider.ClientOptions, true, TestContext.Current.CancellationToken); provider.TryGet("key1", out string? value).Should().BeTrue(); value.Should().Be("value1"); bool reloadFired = false; provider.GetReloadToken().RegisterChangeCallback(_ => reloadFired = true, null); - await provider.DoLoadAsync(provider.ClientOptions, true, TestContext.Current.CancellationToken); + provider.Load(); reloadFired.Should().BeFalse("identical data should not trigger OnReload"); provider.TryGet("key1", out value).Should().BeTrue(); @@ -483,7 +487,7 @@ public async Task DoLoad_IdenticalData_DoesNotTriggerReload() } [Fact] - public async Task DoLoad_MultipleLabels_ChecksAllLabels() + public void Load_MultipleLabels_ChecksAllLabels() { const string responseJson = """ { @@ -513,8 +517,7 @@ public async Task DoLoad_MultipleLabels_ChecksAllLabels() handler.Mock.Expect(HttpMethod.Get, $"http://localhost:8888/{options.Name}/{options.Environment}/test-label").Respond("application/json", responseJson); using var provider = new ConfigServerConfigurationProvider(options, null, null, () => handler, NullLoggerFactory.Instance); - - await provider.DoLoadAsync(provider.ClientOptions, true, TestContext.Current.CancellationToken); + provider.Load(); handler.Mock.VerifyNoOutstandingExpectation(); } @@ -578,8 +581,7 @@ public async Task Load_MultipleConfigServers_ReturnsGreaterThanEqualBadRequest_S .Respond(HttpStatusCode.InternalServerError); using var provider = new ConfigServerConfigurationProvider(options, null, null, () => handler, NullLoggerFactory.Instance); - - await provider.LoadInternalAsync(provider.ClientOptions, true, TestContext.Current.CancellationToken); + provider.Load(); handler.Mock.GetMatchCount(request).Should().Be(1); @@ -600,8 +602,7 @@ public async Task Load_MultipleConfigServers_ReturnsNotFoundStatus_DoesNotContin .Respond(HttpStatusCode.NotFound); using var provider = new ConfigServerConfigurationProvider(options, null, null, () => handler, NullLoggerFactory.Instance); - - await provider.LoadInternalAsync(provider.ClientOptions, true, TestContext.Current.CancellationToken); + provider.Load(); handler.Mock.GetMatchCount(request).Should().Be(1); @@ -611,7 +612,7 @@ public async Task Load_MultipleConfigServers_ReturnsNotFoundStatus_DoesNotContin } [Fact] - public async Task Load_ConfigServerReturnsNotFoundStatus() + public void Load_ConfigServerReturnsNotFoundStatus() { ConfigServerClientOptions options = GetCommonOptions(); @@ -619,15 +620,14 @@ public async Task Load_ConfigServerReturnsNotFoundStatus() handler.Mock.Expect(HttpMethod.Get, $"http://localhost:8888/{options.Name}/{options.Environment}").Respond(HttpStatusCode.NotFound); using var provider = new ConfigServerConfigurationProvider(options, null, null, () => handler, NullLoggerFactory.Instance); - - await provider.LoadInternalAsync(provider.ClientOptions, true, TestContext.Current.CancellationToken); + provider.Load(); handler.Mock.VerifyNoOutstandingExpectation(); provider.InnerData.Should().BeEmpty(); } [Fact] - public async Task Load_ConfigServerReturnsNotFoundStatus_FailFastEnabled() + public void Load_ConfigServerReturnsNotFoundStatus_FailFastEnabled() { ConfigServerClientOptions options = GetCommonOptions(); options.FailFast = true; @@ -637,9 +637,9 @@ public async Task Load_ConfigServerReturnsNotFoundStatus_FailFastEnabled() using var provider = new ConfigServerConfigurationProvider(options, null, null, () => handler, NullLoggerFactory.Instance); - Func action = async () => await provider.LoadInternalAsync(provider.ClientOptions, true, TestContext.Current.CancellationToken); + Action action = provider.Load; - await action.Should().ThrowExactlyAsync(); + action.Should().ThrowExactly(); handler.Mock.VerifyNoOutstandingExpectation(); } @@ -657,9 +657,9 @@ public async Task Load_MultipleConfigServers_ReturnsNotFoundStatus__DoesNotConti using var provider = new ConfigServerConfigurationProvider(options, null, null, () => handler, NullLoggerFactory.Instance); - Func action = async () => await provider.LoadInternalAsync(provider.ClientOptions, true, TestContext.Current.CancellationToken); + Action action = provider.Load; - await action.Should().ThrowExactlyAsync(); + action.Should().ThrowExactly(); handler.Mock.GetMatchCount(request).Should().Be(1); await Task.Delay(2.Seconds(), TestContext.Current.CancellationToken); @@ -668,7 +668,7 @@ public async Task Load_MultipleConfigServers_ReturnsNotFoundStatus__DoesNotConti } [Fact] - public async Task Load_UriInvalid_FailFastEnabled() + public void Load_UriInvalid_FailFastEnabled() { ConfigServerClientOptions options = GetCommonOptions(); options.Uri = "http://username:p@ssword@localhost:8888"; @@ -677,14 +677,14 @@ public async Task Load_UriInvalid_FailFastEnabled() using var handler = new DelegateToMockHttpClientHandler(); using var provider = new ConfigServerConfigurationProvider(options, null, null, () => handler, NullLoggerFactory.Instance); - Func action = async () => await provider.LoadInternalAsync(provider.ClientOptions, true, TestContext.Current.CancellationToken); + Action action = provider.Load; - await action.Should().ThrowExactlyAsync().WithMessage("One or more Config Server URIs in configuration are invalid."); + action.Should().ThrowExactly().WithMessage("One or more Config Server URIs in configuration are invalid."); handler.Mock.VerifyNoOutstandingExpectation(); } [Fact] - public async Task Load_ConfigServerReturnsBadStatus_FailFastEnabled() + public void Load_ConfigServerReturnsBadStatus_FailFastEnabled() { ConfigServerClientOptions options = GetCommonOptions(); options.FailFast = true; @@ -694,9 +694,9 @@ public async Task Load_ConfigServerReturnsBadStatus_FailFastEnabled() using var provider = new ConfigServerConfigurationProvider(options, null, null, () => handler, NullLoggerFactory.Instance); - Func action = async () => await provider.LoadInternalAsync(provider.ClientOptions, true, TestContext.Current.CancellationToken); + Action action = provider.Load; - await action.Should().ThrowExactlyAsync(); + action.Should().ThrowExactly(); handler.Mock.VerifyNoOutstandingExpectation(); } @@ -714,9 +714,9 @@ public async Task Load_MultipleConfigServers_ReturnsBadStatus_StopsChecking_Fail using var provider = new ConfigServerConfigurationProvider(options, null, null, () => handler, NullLoggerFactory.Instance); - Func action = async () => await provider.LoadInternalAsync(provider.ClientOptions, true, TestContext.Current.CancellationToken); + Action action = provider.Load; - await action.Should().ThrowExactlyAsync(); + action.Should().ThrowExactly(); handler.Mock.GetMatchCount(request).Should().Be(1); await Task.Delay(2.Seconds(), TestContext.Current.CancellationToken); @@ -746,9 +746,9 @@ await TestFailureTracer.CaptureAsync(async tracer => using var provider = new ConfigServerConfigurationProvider(options, null, null, () => handler, tracer.LoggerFactory); - Func action = async () => await provider.LoadInternalAsync(provider.ClientOptions, true, TestContext.Current.CancellationToken); + Action action = () => provider.Load(); - await action.Should().ThrowExactlyAsync(); + action.Should().ThrowExactly(); await Task.Delay(2.Seconds(), TestContext.Current.CancellationToken); @@ -757,7 +757,7 @@ await TestFailureTracer.CaptureAsync(async tracer => } [Fact] - public async Task Load_ChangesDataDictionary() + public void Load_ChangesDataDictionary() { const string responseJson = """ { @@ -786,9 +786,7 @@ public async Task Load_ChangesDataDictionary() handler.Mock.Expect(HttpMethod.Get, $"http://localhost:8888/{options.Name}/{options.Environment}").Respond("application/json", responseJson); using var provider = new ConfigServerConfigurationProvider(options, null, null, () => handler, NullLoggerFactory.Instance); - - await provider.LoadInternalAsync(provider.ClientOptions, true, TestContext.Current.CancellationToken); - + provider.Load(); handler.Mock.VerifyNoOutstandingExpectation(); provider.TryGet("key1", out string? value).Should().BeTrue(); @@ -833,10 +831,9 @@ public void ReLoad_DataDictionary_With_New_Configurations() handler.Mock.Expect(HttpMethod.Get, $"http://localhost:8888/{options.Name}/{options.Environment}").Respond("application/json", responseJson); using var provider = new ConfigServerConfigurationProvider(options, null, null, () => handler, NullLoggerFactory.Instance); - provider.Load(); - handler.Mock.VerifyNoOutstandingExpectation(); + provider.TryGet("featureToggles:ShowModule:0", out string? value).Should().BeTrue(); value.Should().Be("FT1"); provider.TryGet("featureToggles:ShowModule:1", out value).Should().BeTrue(); @@ -868,9 +865,7 @@ public void ReLoad_DataDictionary_With_New_Configurations() """; handler.Mock.Expect(HttpMethod.Get, $"http://localhost:8888/{options.Name}/{options.Environment}").Respond("application/json", newResponseJson); - provider.Load(); - handler.Mock.VerifyNoOutstandingExpectation(); provider.TryGet("featureToggles:ShowModule:0", out value).Should().BeTrue(); @@ -928,6 +923,7 @@ public void DataDictionary_DoesNotContainRedundantClientSettings() }; using var provider = new ConfigServerConfigurationProvider(options, null, null, null, NullLoggerFactory.Instance); + provider.Load(); provider.TryGet("spring:cloud:config:enabled", out _).Should().BeFalse(); provider.TryGet("spring:cloud:config:failFast", out _).Should().BeFalse(); @@ -993,12 +989,12 @@ public void Reload_And_Bind_Without_Throwing_Exception() using var handler = new DelegateToMockHttpClientHandler(); handler.Mock.When(HttpMethod.Get, $"http://localhost:8888/{clientOptions.Name}/{clientOptions.Environment}").Respond("application/json", responseJson); - using var provider = new ConfigServerConfigurationProvider(clientOptions, null, null, () => handler, NullLoggerFactory.Instance); - var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.Add(new TestConfigServerConfigurationSource(provider)); + configurationBuilder.AddConfigServer(clientOptions, null, () => handler, NullLoggerFactory.Instance); IConfigurationRoot configurationRoot = configurationBuilder.Build(); + using ConfigServerConfigurationProvider provider = configurationRoot.EnumerateProviders().Single(); + TestOptions? testOptions = null; using var tokenSource = new CancellationTokenSource(250.Milliseconds()); diff --git a/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationProviderTest.Settings.cs b/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationProviderTest.Settings.cs index d6536e0510..1cd6f664f6 100644 --- a/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationProviderTest.Settings.cs +++ b/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationProviderTest.Settings.cs @@ -16,6 +16,7 @@ public void DefaultConstructor_InitializedWithDefaultSettings() { var options = new ConfigServerClientOptions(); using var provider = new ConfigServerConfigurationProvider(options, null, null, null, NullLoggerFactory.Instance); + provider.Load(); string? expectedAppName = Assembly.GetEntryAssembly()!.GetName().Name; TestHelper.VerifyDefaults(provider.ClientOptions, expectedAppName, "Production"); @@ -24,10 +25,10 @@ public void DefaultConstructor_InitializedWithDefaultSettings() [Fact] public void SourceConstructor_WithDefaults_InitializesWithDefaultSettings() { - IConfiguration configuration = new ConfigurationBuilder().Build(); var options = new ConfigServerClientOptions(); - var source = new ConfigServerConfigurationSource(options, configuration, null, null, NullLoggerFactory.Instance); + var source = new ConfigServerConfigurationSource(options, [], null, null, null, NullLoggerFactory.Instance); using var provider = new ConfigServerConfigurationProvider(source, NullLoggerFactory.Instance); + provider.Load(); string? expectedAppName = Assembly.GetEntryAssembly()!.GetName().Name; TestHelper.VerifyDefaults(provider.ClientOptions, expectedAppName, "Production"); @@ -41,11 +42,12 @@ public void SourceConstructor_WithTimeoutConfigured_InitializesHttpClientWithCon ["spring:cloud:config:timeout"] = "30000" }; - IConfiguration configuration = new ConfigurationBuilder().AddInMemoryCollection(appSettings).Build(); + var builder = new ConfigurationBuilder(); + builder.AddInMemoryCollection(appSettings); + builder.AddConfigServer(); + IConfigurationRoot configuration = builder.Build(); - var options = new ConfigServerClientOptions(); - var source = new ConfigServerConfigurationSource(options, configuration, null, null, NullLoggerFactory.Instance); - using var provider = new ConfigServerConfigurationProvider(source, NullLoggerFactory.Instance); + ConfigServerConfigurationProvider provider = configuration.EnumerateProviders().Single(); using HttpClient httpClient = provider.CreateHttpClient(provider.ClientOptions); httpClient.Should().NotBeNull(); @@ -80,6 +82,8 @@ public void GetConfigServerUri_NoLabel() }; using var provider = new ConfigServerConfigurationProvider(options, null, null, null, NullLoggerFactory.Instance); + provider.Load(); + string uri = provider.BuildConfigServerUri(provider.ClientOptions, new Uri(options.Uri!), null).ToString(); uri.Should().Be($"{options.Uri}/{options.Name}/{options.Environment}"); @@ -96,6 +100,8 @@ public void GetConfigServerUri_WithLabel() }; using var provider = new ConfigServerConfigurationProvider(options, null, null, null, NullLoggerFactory.Instance); + provider.Load(); + string uri = provider.BuildConfigServerUri(provider.ClientOptions, new Uri(options.Uri!), options.Label).ToString(); uri.Should().Be($"{options.Uri}/{options.Name}/{options.Environment}/{options.Label}"); @@ -112,6 +118,8 @@ public void GetConfigServerUri_WithLabelContainingSlash() }; using var provider = new ConfigServerConfigurationProvider(options, null, null, null, NullLoggerFactory.Instance); + provider.Load(); + string uri = provider.BuildConfigServerUri(provider.ClientOptions, new Uri(options.Uri!), options.Label).ToString(); uri.Should().Be($"{options.Uri}/{options.Name}/{options.Environment}/myLabel(_)version"); @@ -128,6 +136,8 @@ public void GetConfigServerUri_WithExtraPathInfo() }; using var provider = new ConfigServerConfigurationProvider(options, null, null, null, NullLoggerFactory.Instance); + provider.Load(); + string uri = provider.BuildConfigServerUri(provider.ClientOptions, new Uri(options.Uri), null).ToString(); uri.Should().Be($"http://localhost:9999/myPath/path/{options.Name}/{options.Environment}"); @@ -144,6 +154,8 @@ public void GetConfigServerUri_WithExtraPathInfo_NoEndingSlash() }; using var provider = new ConfigServerConfigurationProvider(options, null, null, null, NullLoggerFactory.Instance); + provider.Load(); + string uri = provider.BuildConfigServerUri(provider.ClientOptions, new Uri(options.Uri), null).ToString(); uri.Should().Be($"http://localhost:9999/myPath/path/{options.Name}/{options.Environment}"); @@ -160,6 +172,8 @@ public void GetConfigServerUri_NoEndingSlash() }; using var provider = new ConfigServerConfigurationProvider(options, null, null, null, NullLoggerFactory.Instance); + provider.Load(); + string uri = provider.BuildConfigServerUri(provider.ClientOptions, new Uri(options.Uri), null).ToString(); uri.Should().Be($"http://localhost:9999/{options.Name}/{options.Environment}"); @@ -176,6 +190,8 @@ public void GetConfigServerUri_WithEndingSlash() }; using var provider = new ConfigServerConfigurationProvider(options, null, null, null, NullLoggerFactory.Instance); + provider.Load(); + string uri = provider.BuildConfigServerUri(provider.ClientOptions, new Uri(options.Uri), null).ToString(); uri.Should().Be($"http://localhost:9999/{options.Name}/{options.Environment}"); @@ -192,6 +208,8 @@ public void GetConfigServerUri_MultipleEnvironments_EncodesComma() }; using var provider = new ConfigServerConfigurationProvider(options, null, null, null, NullLoggerFactory.Instance); + provider.Load(); + string uri = provider.BuildConfigServerUri(provider.ClientOptions, new Uri(options.Uri!), options.Label).ToString(); uri.Should().Be("http://localhost:8888/myName/one%2Ctwo/demo"); @@ -210,6 +228,8 @@ public void GetConfigServerUri_EncodesSpecialCharacters() }; using var provider = new ConfigServerConfigurationProvider(options, null, null, null, NullLoggerFactory.Instance); + provider.Load(); + string uri = provider.BuildConfigServerUri(provider.ClientOptions, new Uri(options.Uri!), options.Label).ToString(); uri.Should().Be( diff --git a/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationProviderTest.cs b/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationProviderTest.cs index 312b894c87..5cccbdb791 100644 --- a/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationProviderTest.cs +++ b/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationProviderTest.cs @@ -66,6 +66,7 @@ public void GetLabels_Null() { var options = new ConfigServerClientOptions(); using var provider = new ConfigServerConfigurationProvider(options, null, null, null, NullLoggerFactory.Instance); + provider.Load(); string[] result = provider.GetLabels(provider.ClientOptions); result.Should().ContainSingle().Which.Should().BeEmpty(); @@ -80,6 +81,7 @@ public void GetLabels_Empty() }; using var provider = new ConfigServerConfigurationProvider(options, null, null, null, NullLoggerFactory.Instance); + provider.Load(); string[] result = provider.GetLabels(provider.ClientOptions); result.Should().ContainSingle().Which.Should().BeEmpty(); @@ -94,6 +96,7 @@ public void GetLabels_SingleString() }; using var provider = new ConfigServerConfigurationProvider(options, null, null, null, NullLoggerFactory.Instance); + provider.Load(); string[] result = provider.GetLabels(provider.ClientOptions); result.Should().ContainSingle().Which.Should().Be("foobar"); @@ -108,6 +111,7 @@ public void GetLabels_MultiString() }; using var provider = new ConfigServerConfigurationProvider(options, null, null, null, NullLoggerFactory.Instance); + provider.Load(); string[] result = provider.GetLabels(provider.ClientOptions); result.Should().HaveCount(3); @@ -125,6 +129,7 @@ public void GetLabels_MultiStringHoles() }; using var provider = new ConfigServerConfigurationProvider(options, null, null, null, NullLoggerFactory.Instance); + provider.Load(); string[] result = provider.GetLabels(provider.ClientOptions); result.Should().HaveCount(3); @@ -144,6 +149,7 @@ public async Task GetRequestMessage_AddsBasicAuthIfUserNameAndPasswordInURL() }; using var provider = new ConfigServerConfigurationProvider(options, null, null, null, NullLoggerFactory.Instance); + provider.Load(); Uri requestUri = provider.BuildConfigServerUri(provider.ClientOptions, new Uri(options.Uri), null); HttpRequestMessage request = await provider.GetRequestMessageAsync(provider.ClientOptions, requestUri, TestContext.Current.CancellationToken); @@ -168,6 +174,7 @@ public async Task GetRequestMessage_AddsBasicAuthIfUserNameAndPasswordInSettings }; using var provider = new ConfigServerConfigurationProvider(options, null, null, null, NullLoggerFactory.Instance); + provider.Load(); Uri requestUri = provider.BuildConfigServerUri(provider.ClientOptions, new Uri(options.Uri), null); HttpRequestMessage request = await provider.GetRequestMessageAsync(provider.ClientOptions, requestUri, TestContext.Current.CancellationToken); @@ -192,6 +199,7 @@ public async Task GetRequestMessage_BasicAuthInSettingsOverridesUserNameAndPassw }; using var provider = new ConfigServerConfigurationProvider(options, null, null, null, NullLoggerFactory.Instance); + provider.Load(); Uri requestUri = provider.BuildConfigServerUri(provider.ClientOptions, new Uri(options.Uri), null); HttpRequestMessage request = await provider.GetRequestMessageAsync(provider.ClientOptions, requestUri, TestContext.Current.CancellationToken); @@ -214,6 +222,7 @@ public async Task GetRequestMessage_AddsVaultToken_IfNeeded() }; using var provider = new ConfigServerConfigurationProvider(options, null, null, null, NullLoggerFactory.Instance); + provider.Load(); Uri requestUri = provider.BuildConfigServerUri(provider.ClientOptions, new Uri(options.Uri!), null); HttpRequestMessage request = await provider.GetRequestMessageAsync(provider.ClientOptions, requestUri, TestContext.Current.CancellationToken); @@ -277,6 +286,8 @@ public async Task RefreshVaultToken_Succeeds() // ReSharper disable once AccessToDisposedClosure using var provider = new ConfigServerConfigurationProvider(options, null, null, () => handler, NullLoggerFactory.Instance); + provider.Load(); + await provider.RefreshVaultTokenAsync(provider.ClientOptions, TestContext.Current.CancellationToken); handler.Mock.VerifyNoOutstandingExpectation(); @@ -305,6 +316,7 @@ public async Task RefreshVaultToken_With_AccessTokenUri_Succeeds() // ReSharper disable once AccessToDisposedClosure using var provider = new ConfigServerConfigurationProvider(options, null, null, () => handler, NullLoggerFactory.Instance); + await provider.RefreshVaultTokenAsync(provider.ClientOptions, TestContext.Current.CancellationToken); handler.Mock.VerifyNoOutstandingExpectation(); @@ -325,6 +337,8 @@ public void GetHttpClient_AddsHeaders_IfConfigured() }; using var provider = new ConfigServerConfigurationProvider(options, null, null, null, NullLoggerFactory.Instance); + provider.Load(); + using HttpClient httpClient = provider.CreateHttpClient(provider.ClientOptions); httpClient.Should().NotBeNull(); @@ -347,25 +361,27 @@ public void IsDiscoveryFirstEnabled_ReturnsExpected() using (var provider = new ConfigServerConfigurationProvider(options, null, null, null, NullLoggerFactory.Instance)) { + provider.Load(); provider.ClientOptions.Discovery.Enabled.Should().BeTrue(); } - var values = new Dictionary + var appSettings = new Dictionary { ["spring:cloud:config:discovery:enabled"] = "True" }; - IConfiguration configuration = new ConfigurationBuilder().AddInMemoryCollection(values).Build(); - options = new ConfigServerClientOptions { Name = "foo", Environment = "development" }; - var source = new ConfigServerConfigurationSource(options, configuration, null, null, NullLoggerFactory.Instance); + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(appSettings); + configurationBuilder.AddConfigServer(options); + IConfigurationRoot configuration = configurationBuilder.Build(); - using (var provider = new ConfigServerConfigurationProvider(source, NullLoggerFactory.Instance)) + using (ConfigServerConfigurationProvider provider = configuration.EnumerateProviders().Single()) { provider.ClientOptions.Discovery.Enabled.Should().BeTrue(); } @@ -374,22 +390,24 @@ public void IsDiscoveryFirstEnabled_ReturnsExpected() [Fact] public void UpdateSettingsFromDiscovery_UpdatesSettingsCorrectly() { - var values = new Dictionary + var appSettings = new Dictionary { ["spring:cloud:config:discovery:enabled"] = "True" }; - IConfiguration configuration = new ConfigurationBuilder().AddInMemoryCollection(values).Build(); - - var initialOptions = new ConfigServerClientOptions + var options = new ConfigServerClientOptions { Uri = "http://localhost:8888/", Name = "foo", Environment = "development" }; - var source = new ConfigServerConfigurationSource(initialOptions, configuration, null, null, NullLoggerFactory.Instance); - using var provider = new ConfigServerConfigurationProvider(source, NullLoggerFactory.Instance); + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(appSettings); + configurationBuilder.AddConfigServer(options); + IConfigurationRoot configuration = configurationBuilder.Build(); + + using ConfigServerConfigurationProvider provider = configuration.EnumerateProviders().Single(); ConfigServerClientOptions optionsSnapshot = provider.ClientOptions; provider.SetLastDiscoveryLookupResult(new List()); @@ -425,17 +443,15 @@ public void UpdateSettingsFromDiscovery_UpdatesSettingsCorrectly() } [Fact] - public async Task DiscoverServerInstances_FailsFast() + public void DiscoverServerInstances_FailsFast() { - var values = new Dictionary + var appSettings = new Dictionary { ["spring:cloud:config:discovery:enabled"] = "True", ["spring:cloud:config:failFast"] = "True", ["eureka:client:eurekaServer:retryCount"] = "0" }; - IConfiguration configuration = new ConfigurationBuilder().AddInMemoryCollection(values).Build(); - var options = new ConfigServerClientOptions { Name = "foo", @@ -443,14 +459,13 @@ public async Task DiscoverServerInstances_FailsFast() Timeout = 10 }; - var source = new ConfigServerConfigurationSource(options, configuration, null, null, NullLoggerFactory.Instance); - using var provider = new ConfigServerConfigurationProvider(source, NullLoggerFactory.Instance); + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(appSettings); + configurationBuilder.AddConfigServer(options); - // ReSharper disable AccessToDisposedClosure - Func action = async () => await provider.LoadInternalAsync(provider.ClientOptions, true, TestContext.Current.CancellationToken); - // ReSharper restore AccessToDisposedClosure + Action action = () => _ = configurationBuilder.Build(); - await action.Should().ThrowExactlyAsync().WithMessage("Could not locate Config Server via discovery*"); + action.Should().ThrowExactly().WithMessage("Could not locate Config Server via discovery*"); } private static string GetEncodedUserPassword(string user, string password) diff --git a/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationSourceTest.cs b/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationSourceTest.cs index d07020258a..a66b415c5c 100644 --- a/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationSourceTest.cs +++ b/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationSourceTest.cs @@ -29,14 +29,6 @@ public void Constructors_InitializesProperties() source.Sources.Should().ContainSingle().Which.Should().Be(memSource); source.Properties.Should().ContainSingle(); source.Properties.Should().ContainKey("foo").WhoseValue.Should().Be("bar"); - - IConfigurationRoot configurationRoot = new ConfigurationBuilder().AddInMemoryCollection().Build(); - source = new ConfigServerConfigurationSource(options, configurationRoot, null, null, NullLoggerFactory.Instance); - source.DefaultOptions.Should().Be(options); - - ConfigurationRoot? root = source.Configuration.Should().BeOfType().Subject; - - root.Should().BeSameAs(configurationRoot); } [Fact] diff --git a/src/Configuration/test/ConfigServer.Test/ConfigServerHostedServiceTest.cs b/src/Configuration/test/ConfigServer.Test/ConfigServerHostedServiceTest.cs index 4dea770d8c..11de4911de 100644 --- a/src/Configuration/test/ConfigServer.Test/ConfigServerHostedServiceTest.cs +++ b/src/Configuration/test/ConfigServer.Test/ConfigServerHostedServiceTest.cs @@ -20,6 +20,8 @@ public async Task ServiceConstructsAndOperatesWithConfigurationRoot() Enabled = false }, null, null, null, NullLoggerFactory.Instance); + provider.Load(); + var configurationRoot = new ConfigurationRoot([provider]); var service = new ConfigServerHostedService(configurationRoot, []); diff --git a/src/Configuration/test/ConfigServer.Test/TestConfigServerConfigurationSource.cs b/src/Configuration/test/ConfigServer.Test/TestConfigServerConfigurationSource.cs deleted file mode 100644 index 23c99470bf..0000000000 --- a/src/Configuration/test/ConfigServer.Test/TestConfigServerConfigurationSource.cs +++ /dev/null @@ -1,17 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the Apache 2.0 License. -// See the LICENSE file in the project root for more information. - -using Microsoft.Extensions.Configuration; - -namespace Steeltoe.Configuration.ConfigServer.Test; - -internal sealed class TestConfigServerConfigurationSource(IConfigurationProvider provider) : IConfigurationSource -{ - private readonly IConfigurationProvider _provider = provider; - - public IConfigurationProvider Build(IConfigurationBuilder builder) - { - return _provider; - } -} From 0d65fa8aed1645d181883534f038bd92b4bdb4b8 Mon Sep 17 00:00:00 2001 From: Bart Koelman <104792814+bart-vmware@users.noreply.github.com> Date: Thu, 2 Apr 2026 16:55:03 +0200 Subject: [PATCH 22/29] Increase timeout to reduce failures in CI --- .../test/Certificates.Test/ConfigureCertificateOptionsTest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Common/test/Certificates.Test/ConfigureCertificateOptionsTest.cs b/src/Common/test/Certificates.Test/ConfigureCertificateOptionsTest.cs index 24e9b70cf8..f14de9eed7 100644 --- a/src/Common/test/Certificates.Test/ConfigureCertificateOptionsTest.cs +++ b/src/Common/test/Certificates.Test/ConfigureCertificateOptionsTest.cs @@ -167,7 +167,7 @@ public async Task CertificateOptions_update_on_changed_contents(string certifica await File.WriteAllTextAsync(privateKeyFilePath, secondPrivateKeyContent, TestContext.Current.CancellationToken); using Task pollTask = WaitUntilCertificateChangedToAsync(secondX509, optionsMonitor, certificateName, TestContext.Current.CancellationToken); - await pollTask.WaitAsync(TimeSpan.FromSeconds(1), TestContext.Current.CancellationToken); + await pollTask.WaitAsync(TimeSpan.FromSeconds(5), TestContext.Current.CancellationToken); optionsMonitor.Get(certificateName).Certificate.Should().Be(secondX509); } From 1a543eedfbf2f75b80574d0216a148e5459c31c8 Mon Sep 17 00:00:00 2001 From: Bart Koelman <104792814+bart-vmware@users.noreply.github.com> Date: Tue, 7 Apr 2026 12:43:46 +0200 Subject: [PATCH 23/29] Update src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationProviderTest.Loading.cs Co-authored-by: Tim Hess --- .../ConfigServerConfigurationProviderTest.Loading.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationProviderTest.Loading.cs b/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationProviderTest.Loading.cs index cbe9ff0c23..b82d693616 100644 --- a/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationProviderTest.Loading.cs +++ b/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationProviderTest.Loading.cs @@ -215,7 +215,7 @@ public async Task Create_WithNonZeroPollingIntervalAndClientDisabled_PollingConf [Theory] [InlineData(false, "00:00:01")] [InlineData(true, "00:00:00")] - public void OnSettingsChanged_stops_reload_timer_when_polling_becomes_ineffective(bool enabled, string pollingInterval) + public void OnSettingsChanged_stops_reload_timer_when_polling_no_longer_enabled(bool enabled, string pollingInterval) { const string responseJson = """ { From 160c3746f111944570748b6a5098c68de2b00583 Mon Sep 17 00:00:00 2001 From: Bart Koelman <104792814+bart-vmware@users.noreply.github.com> Date: Tue, 7 Apr 2026 12:32:19 +0200 Subject: [PATCH 24/29] Review feedback: replace overrule with override --- ...nfigServerConfigurationBuilderExtensions.cs | 4 ++-- .../ConfigServerConfigurationSource.cs | 6 +++--- .../ConfigServerClientOptionsTest.cs | 18 +++++++++--------- .../ConfigServerClientOptionsTest.cs | 2 +- .../src/Eureka/EurekaApplicationInfoManager.cs | 4 ++-- .../Configuration/ManagementOptions.cs | 2 +- .../src/Endpoint/ConfigurationSchema.json | 2 +- .../test/RazorPagesTestWebApp/Program.cs | 2 +- 8 files changed, 20 insertions(+), 20 deletions(-) diff --git a/src/Configuration/src/ConfigServer/ConfigServerConfigurationBuilderExtensions.cs b/src/Configuration/src/ConfigServer/ConfigServerConfigurationBuilderExtensions.cs index 9cb9504617..2450d239f4 100644 --- a/src/Configuration/src/ConfigServer/ConfigServerConfigurationBuilderExtensions.cs +++ b/src/Configuration/src/ConfigServer/ConfigServerConfigurationBuilderExtensions.cs @@ -53,7 +53,7 @@ public static IConfigurationBuilder AddConfigServer(this IConfigurationBuilder b /// The to add configuration to. /// /// - /// The initial options, whose values are overruled from . + /// The initial options, whose values are overridden from . /// /// /// The incoming so that additional calls can be chained. @@ -70,7 +70,7 @@ public static IConfigurationBuilder AddConfigServer(this IConfigurationBuilder b /// The to add configuration to. /// /// - /// The initial options, whose values are overruled from . + /// The initial options, whose values are overridden from . /// /// /// Used for internal logging. Pass to disable logging. diff --git a/src/Configuration/src/ConfigServer/ConfigServerConfigurationSource.cs b/src/Configuration/src/ConfigServer/ConfigServerConfigurationSource.cs index 7c8dc950dd..49ac3422ed 100644 --- a/src/Configuration/src/ConfigServer/ConfigServerConfigurationSource.cs +++ b/src/Configuration/src/ConfigServer/ConfigServerConfigurationSource.cs @@ -21,7 +21,7 @@ internal sealed class ConfigServerConfigurationSource : IConfigurationSource public ConfigServerClientOptions DefaultOptions { get; } /// - /// Gets the configuration the client uses to contact Config Server. Entries overrule . + /// Gets the configuration the client uses to contact Config Server. Entries override . /// public IConfiguration? Configuration { get; private set; } @@ -43,11 +43,11 @@ internal sealed class ConfigServerConfigurationSource : IConfigurationSource /// The initial options the client uses to contact Config Server. /// /// - /// Configuration sources the client uses to contact Config Server. The will be built from these, whose entries overrule + /// Configuration sources the client uses to contact Config Server. The will be built from these, whose entries override /// . /// /// - /// Configuration properties the client uses to contact Config Server. The will be built from these, whose entries overrule + /// Configuration properties the client uses to contact Config Server. The will be built from these, whose entries override /// . /// /// diff --git a/src/Configuration/test/ConfigServer.Discovery.Test/ConfigServerClientOptionsTest.cs b/src/Configuration/test/ConfigServer.Discovery.Test/ConfigServerClientOptionsTest.cs index a63d19f06f..5ca2eab3c0 100644 --- a/src/Configuration/test/ConfigServer.Discovery.Test/ConfigServerClientOptionsTest.cs +++ b/src/Configuration/test/ConfigServer.Discovery.Test/ConfigServerClientOptionsTest.cs @@ -45,7 +45,7 @@ public void Config_Server_URI_is_resolved_from_discovery_and_survives_changes_in "discovery": { "enabled": true }, - "uri": "http://overruled-by-discovery", + "uri": "http://overridden-by-discovery", "name": "example-app-name", "env": "example-profile", "timeout": 30000, @@ -105,7 +105,7 @@ public void Config_Server_URI_is_resolved_from_discovery_and_survives_changes_in using ServiceProvider serviceProvider = services.BuildServiceProvider(true); var optionsMonitor = serviceProvider.GetRequiredService>(); - provider.ClientOptions.Uri.Should().Be("http://overruled-by-discovery"); + provider.ClientOptions.Uri.Should().Be("http://overridden-by-discovery"); provider.ClientOptions.Username.Should().BeNull(); provider.ClientOptions.Password.Should().BeNull(); provider.ClientOptions.Name.Should().Be("example-app-name"); @@ -133,7 +133,7 @@ public void Config_Server_URI_is_resolved_from_discovery_and_survives_changes_in "discovery": { "enabled": true }, - "uri": "http://overruled-by-discovery", + "uri": "http://overridden-by-discovery", "name": "alternate-name-1", "env": "example-profile", "timeout": 15000, @@ -171,7 +171,7 @@ public void Config_Server_URI_is_resolved_from_discovery_and_survives_changes_in fileProvider.NotifyChanged(); - provider.ClientOptions.Uri.Should().Be("http://overruled-by-discovery"); + provider.ClientOptions.Uri.Should().Be("http://overridden-by-discovery"); provider.ClientOptions.Username.Should().BeNull(); provider.ClientOptions.Password.Should().BeNull(); provider.ClientOptions.Name.Should().Be("alternate-name-1"); @@ -262,7 +262,7 @@ public void Updates_discovered_Config_Server_URI_on_provider_reload() "discovery": { "enabled": true }, - "uri": "http://overruled-by-discovery", + "uri": "http://overridden-by-discovery", "name": "example-app-name", "env": "example-profile", "label": "example-label" @@ -322,7 +322,7 @@ public void Updates_discovered_Config_Server_URI_on_provider_reload() using ServiceProvider serviceProvider = services.BuildServiceProvider(true); var optionsMonitor = serviceProvider.GetRequiredService>(); - provider.ClientOptions.Uri.Should().Be("http://overruled-by-discovery"); + provider.ClientOptions.Uri.Should().Be("http://overridden-by-discovery"); provider.ClientOptions.Username.Should().BeNull(); provider.ClientOptions.Password.Should().BeNull(); provider.ClientOptions.Name.Should().Be("example-app-name"); @@ -348,7 +348,7 @@ public void Updates_discovered_Config_Server_URI_on_provider_reload() "discovery": { "enabled": true }, - "uri": "http://overruled-again-by-discovery", + "uri": "http://overridden-again-by-discovery", "name": "alternate-name", "env": "alternate-profile", "label": "alternate-label" @@ -385,7 +385,7 @@ public void Updates_discovered_Config_Server_URI_on_provider_reload() fileProvider.NotifyChanged(); - provider.ClientOptions.Uri.Should().Be("http://overruled-again-by-discovery"); + provider.ClientOptions.Uri.Should().Be("http://overridden-again-by-discovery"); provider.ClientOptions.Username.Should().BeNull(); provider.ClientOptions.Password.Should().BeNull(); provider.ClientOptions.Name.Should().Be("alternate-name"); @@ -409,7 +409,7 @@ public void Updates_discovered_Config_Server_URI_on_provider_reload() provider.Load(); handler.Mock.VerifyNoOutstandingExpectation(); - provider.ClientOptions.Uri.Should().Be("http://overruled-again-by-discovery"); + provider.ClientOptions.Uri.Should().Be("http://overridden-again-by-discovery"); provider.ClientOptions.Username.Should().BeNull(); provider.ClientOptions.Password.Should().BeNull(); provider.ClientOptions.Name.Should().Be("alternate-name"); diff --git a/src/Configuration/test/ConfigServer.Test/ConfigServerClientOptionsTest.cs b/src/Configuration/test/ConfigServer.Test/ConfigServerClientOptionsTest.cs index 365d77fea4..359726fc02 100644 --- a/src/Configuration/test/ConfigServer.Test/ConfigServerClientOptionsTest.cs +++ b/src/Configuration/test/ConfigServer.Test/ConfigServerClientOptionsTest.cs @@ -357,7 +357,7 @@ public void Changes_in_IConfiguration_update_provider_options_and_injected_optio var initialOptions = new ConfigServerClientOptions { - Name = "ignored-because-overruled-from-appsettings", + Name = "ignored-because-overridden-from-appsettings", Label = "example-label" // used, but missing in IConfiguration and injected options }; diff --git a/src/Discovery/src/Eureka/EurekaApplicationInfoManager.cs b/src/Discovery/src/Eureka/EurekaApplicationInfoManager.cs index 3bf7b05b3c..dfb1702476 100644 --- a/src/Discovery/src/Eureka/EurekaApplicationInfoManager.cs +++ b/src/Discovery/src/Eureka/EurekaApplicationInfoManager.cs @@ -31,7 +31,7 @@ public sealed partial class EurekaApplicationInfoManager : IDisposable // Readers must never be blocked, as it may delay the periodic heartbeat. // Updates from user code must be synchronized with configuration changes. // After update, the readonly snapshot is replaced. Volatile prevents reading stale data. - // Once metadata has been set from user code, it overrules what's in configuration. + // Once metadata has been set from user code, it overrides what's in configuration. private volatile InstanceInfo _instance; private IReadOnlyDictionary? _explicitMetadata; @@ -125,7 +125,7 @@ private void InnerUpdateInstance(EurekaInstanceOptions newInstanceOptions, bool newInstance = previousInstance; } - // Status in configuration is the initial startup status. New or previous instance status always overrules it. + // Status in configuration is the initial startup status. New or previous instance status always overrides it. newInstance.ReplaceStatus(newStatus ?? previousInstance.Status); if (newOverriddenStatus != null) diff --git a/src/Management/src/Endpoint/Configuration/ManagementOptions.cs b/src/Management/src/Endpoint/Configuration/ManagementOptions.cs index cb9bcc5f86..0314b64669 100644 --- a/src/Management/src/Endpoint/Configuration/ManagementOptions.cs +++ b/src/Management/src/Endpoint/Configuration/ManagementOptions.cs @@ -55,7 +55,7 @@ public sealed class ManagementOptions public bool SslEnabled { get; set; } /// - /// Gets or sets a value indicating whether the HTTP response status code is based on the health status. This setting can be overruled by sending an + /// Gets or sets a value indicating whether the HTTP response status code is based on the health status. This setting can be overridden by sending an /// X-Use-Status-Code-From-Response HTTP header. Default value: true. /// public bool UseStatusCodeFromResponse { get; set; } = true; diff --git a/src/Management/src/Endpoint/ConfigurationSchema.json b/src/Management/src/Endpoint/ConfigurationSchema.json index 883f3049cd..6d5e8eacf8 100644 --- a/src/Management/src/Endpoint/ConfigurationSchema.json +++ b/src/Management/src/Endpoint/ConfigurationSchema.json @@ -810,7 +810,7 @@ }, "UseStatusCodeFromResponse": { "type": "boolean", - "description": "Gets or sets a value indicating whether the HTTP response status code is based on the health status. This setting can be overruled by sending an X-Use-Status-Code-From-Response HTTP header. Default value: true." + "description": "Gets or sets a value indicating whether the HTTP response status code is based on the health status. This setting can be overridden by sending an X-Use-Status-Code-From-Response HTTP header. Default value: true." }, "Web": { "type": "object", diff --git a/src/Management/test/RazorPagesTestWebApp/Program.cs b/src/Management/test/RazorPagesTestWebApp/Program.cs index 508e5edcfa..b4295ca480 100644 --- a/src/Management/test/RazorPagesTestWebApp/Program.cs +++ b/src/Management/test/RazorPagesTestWebApp/Program.cs @@ -12,7 +12,7 @@ { // This project intentionally does NOT include appsettings*.json files, because they get copied to test projects // that reference this project, and that affects test outcomes. For example, setting the minimum log level - // to Trace on WebApplicationBuilder wouldn't work, because these files overrule log levels. + // to Trace on WebApplicationBuilder wouldn't work, because these files override log levels. ["DetailedErrors"] = builder.Environment.IsDevelopment() ? "true" : "false", ["Logging:LogLevel:Default"] = "Information", From 603250ba221646e10a02a615cd6f1095aca4c294 Mon Sep 17 00:00:00 2001 From: Bart Koelman <104792814+bart-vmware@users.noreply.github.com> Date: Tue, 7 Apr 2026 12:42:51 +0200 Subject: [PATCH 25/29] Review feedback: Change initial to default options --- .../ConfigServerConfigurationBuilderExtensions.cs | 4 ++-- .../src/ConfigServer/ConfigServerConfigurationProvider.cs | 8 ++++---- .../src/ConfigServer/ConfigServerConfigurationSource.cs | 4 ++-- .../ConfigServer.Test/ConfigServerClientOptionsTest.cs | 4 ++-- .../ConfigServerConfigurationBuilderExtensionsTest.cs | 2 +- 5 files changed, 11 insertions(+), 11 deletions(-) diff --git a/src/Configuration/src/ConfigServer/ConfigServerConfigurationBuilderExtensions.cs b/src/Configuration/src/ConfigServer/ConfigServerConfigurationBuilderExtensions.cs index 2450d239f4..19b9ade24e 100644 --- a/src/Configuration/src/ConfigServer/ConfigServerConfigurationBuilderExtensions.cs +++ b/src/Configuration/src/ConfigServer/ConfigServerConfigurationBuilderExtensions.cs @@ -53,7 +53,7 @@ public static IConfigurationBuilder AddConfigServer(this IConfigurationBuilder b /// The to add configuration to. /// /// - /// The initial options, whose values are overridden from . + /// The default options, whose values are overridden from . /// /// /// The incoming so that additional calls can be chained. @@ -70,7 +70,7 @@ public static IConfigurationBuilder AddConfigServer(this IConfigurationBuilder b /// The to add configuration to. /// /// - /// The initial options, whose values are overridden from . + /// The default options, whose values are overridden from . /// /// /// Used for internal logging. Pass to disable logging. diff --git a/src/Configuration/src/ConfigServer/ConfigServerConfigurationProvider.cs b/src/Configuration/src/ConfigServer/ConfigServerConfigurationProvider.cs index f2a602d838..2246ff3e45 100644 --- a/src/Configuration/src/ConfigServer/ConfigServerConfigurationProvider.cs +++ b/src/Configuration/src/ConfigServer/ConfigServerConfigurationProvider.cs @@ -46,7 +46,7 @@ internal sealed partial class ConfigServerConfigurationProvider : ConfigurationP private readonly Func _createHttpClientHandler; private readonly bool _disposeHttpClientHandler; private readonly ConfigureConfigServerClientOptions _configurer; - private readonly ConfigServerClientOptions _initialOptions; + private readonly ConfigServerClientOptions _defaultOptions; private readonly LockPrimitive _lifecycleLock = new(); private readonly LockPrimitive _configurationReloadTickLock = new(); private readonly LockPrimitive _vaultRenewTickLock = new(); @@ -102,8 +102,8 @@ internal ConfigServerConfigurationProvider(ConfigServerClientOptions clientOptio _configurer = new ConfigureConfigServerClientOptions(effectiveConfiguration, configure); _configServerDiscoveryService = new ConfigServerDiscoveryService(effectiveConfiguration, loggerFactory); - _initialOptions = clientOptions.Clone(); - _clientOptions = _initialOptions; + _defaultOptions = clientOptions.Clone(); + _clientOptions = _defaultOptions; if (createHttpClientHandler != null) { @@ -123,7 +123,7 @@ private void OnSettingsChanged(bool skipTimerDueTime) { LogEnteringOnSettingsChanged(); - ConfigServerClientOptions newOptions = _initialOptions.Clone(); + ConfigServerClientOptions newOptions = _defaultOptions.Clone(); try { diff --git a/src/Configuration/src/ConfigServer/ConfigServerConfigurationSource.cs b/src/Configuration/src/ConfigServer/ConfigServerConfigurationSource.cs index 49ac3422ed..711a9c0796 100644 --- a/src/Configuration/src/ConfigServer/ConfigServerConfigurationSource.cs +++ b/src/Configuration/src/ConfigServer/ConfigServerConfigurationSource.cs @@ -16,7 +16,7 @@ internal sealed class ConfigServerConfigurationSource : IConfigurationSource internal Dictionary Properties { get; } /// - /// Gets the initial options the client uses to contact Config Server. + /// Gets the default options the client uses to contact Config Server. /// public ConfigServerClientOptions DefaultOptions { get; } @@ -40,7 +40,7 @@ internal sealed class ConfigServerConfigurationSource : IConfigurationSource /// Initializes a new instance of the class. /// /// - /// The initial options the client uses to contact Config Server. + /// The default options the client uses to contact Config Server. /// /// /// Configuration sources the client uses to contact Config Server. The will be built from these, whose entries override diff --git a/src/Configuration/test/ConfigServer.Test/ConfigServerClientOptionsTest.cs b/src/Configuration/test/ConfigServer.Test/ConfigServerClientOptionsTest.cs index 359726fc02..1da40b2981 100644 --- a/src/Configuration/test/ConfigServer.Test/ConfigServerClientOptionsTest.cs +++ b/src/Configuration/test/ConfigServer.Test/ConfigServerClientOptionsTest.cs @@ -355,7 +355,7 @@ public void Changes_in_IConfiguration_update_provider_options_and_injected_optio handler.Mock.Expect(HttpMethod.Get, "https://config.server.com:9999/example-app-name/example-profile/example-label") .Respond("application/json", configServerResponseJson); - var initialOptions = new ConfigServerClientOptions + var defaultOptions = new ConfigServerClientOptions { Name = "ignored-because-overridden-from-appsettings", Label = "example-label" // used, but missing in IConfiguration and injected options @@ -367,7 +367,7 @@ public void Changes_in_IConfiguration_update_provider_options_and_injected_optio configurationBuilder.AddInMemoryAppSettingsJsonFile(fileProvider); configurationBuilder.AddPlaceholderResolver(); // ReSharper disable once AccessToDisposedClosure - configurationBuilder.AddConfigServer(initialOptions, configureOptions, () => handler, NullLoggerFactory.Instance); + configurationBuilder.AddConfigServer(defaultOptions, configureOptions, () => handler, NullLoggerFactory.Instance); IConfigurationRoot configuration = configurationBuilder.Build(); handler.Mock.VerifyNoOutstandingExpectation(); diff --git a/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationBuilderExtensionsTest.cs b/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationBuilderExtensionsTest.cs index 02fe3a9048..9ba11961bb 100644 --- a/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationBuilderExtensionsTest.cs +++ b/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationBuilderExtensionsTest.cs @@ -205,7 +205,7 @@ public void AddConfigServer_VCAP_SERVICES_Override_Defaults(string vcapServices) } [Fact] - public void AddConfigServer_CallbackOverridesConfigurationOverridesInitialOptions() + public void AddConfigServer_CallbackOverridesConfigurationOverridesDefaultOptions() { var options = new ConfigServerClientOptions { From a9239879c989643185df165ed57e4a0c1c3a31db Mon Sep 17 00:00:00 2001 From: Bart Koelman <104792814+bart-vmware@users.noreply.github.com> Date: Tue, 7 Apr 2026 12:57:58 +0200 Subject: [PATCH 26/29] Review feedback: remove redundant settings in test --- .../ConfigServerConfigurationProviderTest.Loading.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationProviderTest.Loading.cs b/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationProviderTest.Loading.cs index b82d693616..d199aaf07b 100644 --- a/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationProviderTest.Loading.cs +++ b/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationProviderTest.Loading.cs @@ -361,8 +361,7 @@ public void OnSettingsChanged_stops_vault_renew_timer_when_renewal_becomes_disab "cloud": { "config": { "name": "myName", - "token": "MyVaultToken", - "pollingInterval": "00:00:00" + "token": "MyVaultToken" } } } @@ -389,7 +388,6 @@ public void OnSettingsChanged_stops_vault_renew_timer_when_renewal_becomes_disab "config": { "name": "myName", "token": "MyVaultToken", - "pollingInterval": "00:00:00", "disableTokenRenewal": true } } From 94743ac354f22e0a4bbb942a317160f4a7355b77 Mon Sep 17 00:00:00 2001 From: Bart Koelman <104792814+bart-vmware@users.noreply.github.com> Date: Tue, 7 Apr 2026 13:02:52 +0200 Subject: [PATCH 27/29] Review feedback: adjust test name --- .../ConfigServerConfigurationBuilderExtensionsTest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationBuilderExtensionsTest.cs b/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationBuilderExtensionsTest.cs index 9ba11961bb..cdfeb6bbb4 100644 --- a/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationBuilderExtensionsTest.cs +++ b/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationBuilderExtensionsTest.cs @@ -205,7 +205,7 @@ public void AddConfigServer_VCAP_SERVICES_Override_Defaults(string vcapServices) } [Fact] - public void AddConfigServer_CallbackOverridesConfigurationOverridesDefaultOptions() + public void AddConfigServer_CallbackOverridesConfigurationAndDefaultOptions() { var options = new ConfigServerClientOptions { From 885a2ff20c9efef209c9480bbd43415c61f4428e Mon Sep 17 00:00:00 2001 From: Bart Koelman <104792814+bart-vmware@users.noreply.github.com> Date: Tue, 7 Apr 2026 13:07:28 +0200 Subject: [PATCH 28/29] Review feedback: remove duplicate configuration --- .../ConfigServerHealthContributorTest.cs | 19 ++----------------- 1 file changed, 2 insertions(+), 17 deletions(-) diff --git a/src/Configuration/test/ConfigServer.Test/ConfigServerHealthContributorTest.cs b/src/Configuration/test/ConfigServer.Test/ConfigServerHealthContributorTest.cs index b6a1829cc0..8d6f046141 100644 --- a/src/Configuration/test/ConfigServer.Test/ConfigServerHealthContributorTest.cs +++ b/src/Configuration/test/ConfigServer.Test/ConfigServerHealthContributorTest.cs @@ -72,14 +72,7 @@ public void IsCacheStale_ReturnsExpected() IConfigurationRoot configurationRoot = builder.Build(); var contributor = new ConfigServerHealthContributor(configurationRoot, TimeProvider.System, NullLogger.Instance); - - var optionsSnapshot = new ConfigServerClientOptions - { - Health = - { - TimeToLive = 1 - } - }; + ConfigServerClientOptions optionsSnapshot = contributor.Provider!.ClientOptions; contributor.IsCacheStale(0, optionsSnapshot).Should().BeTrue(); // No cache established yet contributor.Cached = new ConfigEnvironment(); @@ -112,18 +105,10 @@ public async Task GetPropertySources_ReturnsExpected() Cached = new ConfigEnvironment() }; - var optionsSnapshot = new ConfigServerClientOptions - { - Health = - { - TimeToLive = 1 - } - }; - long lastAccess = contributor.LastAccess = DateTimeOffset.Now.ToUnixTimeMilliseconds() - 100; IList? sources = - await contributor.GetPropertySourcesAsync(contributor.Provider!, optionsSnapshot, TestContext.Current.CancellationToken); + await contributor.GetPropertySourcesAsync(contributor.Provider!, contributor.Provider!.ClientOptions, TestContext.Current.CancellationToken); contributor.LastAccess.Should().NotBe(lastAccess); sources.Should().BeNull(); From 747f5f1a6dec6a244928c4d6933ecbfa0356d805 Mon Sep 17 00:00:00 2001 From: Bart Koelman <104792814+bart-vmware@users.noreply.github.com> Date: Tue, 7 Apr 2026 13:15:54 +0200 Subject: [PATCH 29/29] Review feedback: reformat JSON snippets in tests --- .../ConfigServerClientOptionsTest.cs | 10 ++++++---- ...ServerConfigurationProviderTest.Loading.cs | 20 ++++++++++++++----- 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/src/Configuration/test/ConfigServer.Test/ConfigServerClientOptionsTest.cs b/src/Configuration/test/ConfigServer.Test/ConfigServerClientOptionsTest.cs index 1da40b2981..2c648dc0b3 100644 --- a/src/Configuration/test/ConfigServer.Test/ConfigServerClientOptionsTest.cs +++ b/src/Configuration/test/ConfigServer.Test/ConfigServerClientOptionsTest.cs @@ -229,7 +229,9 @@ public void Certificate_configuration_survives_options_reload() const string configServerResponseJson = """ { "name": "myName", - "profiles": [ "Production" ], + "profiles": [ + "Production" + ], "label": "test-label", "version": "test-version", "propertySources": [] @@ -401,9 +403,9 @@ public void Changes_in_IConfiguration_update_provider_options_and_injected_optio fileProvider.ReplaceAppSettingsJsonFile(""" { "custom": { - "profileName": "example-profile" - }, - "spring": { + "profileName": "example-profile" + }, + "spring": { "cloud": { "config": { "uri": "https://alternate-config.server.com:7777", diff --git a/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationProviderTest.Loading.cs b/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationProviderTest.Loading.cs index d199aaf07b..257fdb3821 100644 --- a/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationProviderTest.Loading.cs +++ b/src/Configuration/test/ConfigServer.Test/ConfigServerConfigurationProviderTest.Loading.cs @@ -220,7 +220,9 @@ public void OnSettingsChanged_stops_reload_timer_when_polling_no_longer_enabled( const string responseJson = """ { "name": "myName", - "profiles": [ "Production" ], + "profiles": [ + "Production" + ], "label": "test-label", "version": "test-version", "propertySources": [] @@ -283,7 +285,9 @@ public void OnSettingsChanged_reschedules_reload_timer_when_polling_interval_cha const string responseJson = """ { "name": "myName", - "profiles": [ "Production" ], + "profiles": [ + "Production" + ], "label": "test-label", "version": "test-version", "propertySources": [] @@ -346,7 +350,9 @@ public void OnSettingsChanged_stops_vault_renew_timer_when_renewal_becomes_disab const string responseJson = """ { "name": "myName", - "profiles": [ "Production" ], + "profiles": [ + "Production" + ], "label": "test-label", "version": "test-version", "propertySources": [] @@ -406,7 +412,9 @@ public void Load_MultipleConfigServers_SocketError_FallsBackToNextServer() const string responseJson = """ { "name": "test-name", - "profiles": [ "Production" ], + "profiles": [ + "Production" + ], "label": "test-label", "version": "test-version", "propertySources": [ @@ -446,7 +454,9 @@ public void Load_IdenticalData_DoesNotTriggerReload() const string responseJson = """ { "name": "test-name", - "profiles": [ "Production" ], + "profiles": [ + "Production" + ], "label": "test-label", "version": "test-version", "propertySources": [