diff --git a/src/Configuration/src/Abstractions/CompositeConfigurationProvider.cs b/src/Configuration/src/Abstractions/CompositeConfigurationProvider.cs index f7296f00d4..0664a92946 100644 --- a/src/Configuration/src/Abstractions/CompositeConfigurationProvider.cs +++ b/src/Configuration/src/Abstractions/CompositeConfigurationProvider.cs @@ -84,8 +84,13 @@ public virtual bool TryGet(string key, out string? value) LogTryGet(GetType().Name, key); - value = ConfigurationRoot?.GetValue(key); - bool found = value != null; + if (ConfigurationRoot == null) + { + value = null; + return false; + } + + bool found = InnerTryGet(ConfigurationRoot, key, out value); if (found) { @@ -95,6 +100,31 @@ public virtual bool TryGet(string key, out string? value) return found; } + private static bool InnerTryGet(IConfigurationRoot root, string key, out string? value) + { + IList providers = root.Providers as IList ?? root.Providers.ToList(); + + for (int index = providers.Count - 1; index >= 0; index--) + { + IConfigurationProvider provider = providers[index]; + + try + { + if (provider.TryGet(key, out value)) + { + return true; + } + } + catch (ObjectDisposedException) + { + // Skip disposed providers to avoid exceptions during access. + } + } + + value = null; + return false; + } + public void Set(string key, string? value) { ArgumentNullException.ThrowIfNull(key); diff --git a/src/Configuration/src/Encryption/DecryptionConfigurationProvider.cs b/src/Configuration/src/Encryption/DecryptionConfigurationProvider.cs index 0b8be46740..b5f536f2d5 100644 --- a/src/Configuration/src/Encryption/DecryptionConfigurationProvider.cs +++ b/src/Configuration/src/Encryption/DecryptionConfigurationProvider.cs @@ -46,7 +46,7 @@ public override bool TryGet(string key, out string? value) } } - return value != null; + return found; } private ITextDecryptor EnsureDecryptor(IConfigurationRoot configurationRoot) diff --git a/src/Configuration/src/Placeholder/PlaceholderConfigurationProvider.cs b/src/Configuration/src/Placeholder/PlaceholderConfigurationProvider.cs index 1c4c341578..3f956affad 100644 --- a/src/Configuration/src/Placeholder/PlaceholderConfigurationProvider.cs +++ b/src/Configuration/src/Placeholder/PlaceholderConfigurationProvider.cs @@ -36,7 +36,7 @@ public override bool TryGet(string key, out string? value) } } - return value != null; + return found; } [LoggerMessage(Level = LogLevel.Trace, Message = "Replaced value '{OriginalValue}' at key '{Key}' with '{ReplacementValue}'.")] diff --git a/src/Configuration/test/Placeholder.Test/PlaceholderConfigurationTest.cs b/src/Configuration/test/Placeholder.Test/PlaceholderConfigurationTest.cs index c8f73a2c1f..7869c46492 100644 --- a/src/Configuration/test/Placeholder.Test/PlaceholderConfigurationTest.cs +++ b/src/Configuration/test/Placeholder.Test/PlaceholderConfigurationTest.cs @@ -205,7 +205,8 @@ public void Substitutes_placeholders_in_key_lookups() ["key2"] = "${key1?not-found}", ["key3"] = "${no-key?not-found}", ["key4"] = "${no-key}", - ["key5"] = string.Empty + ["key5"] = string.Empty, + ["key6"] = null }; var builder = new ConfigurationBuilder(); @@ -219,6 +220,8 @@ public void Substitutes_placeholders_in_key_lookups() configuration["key3"].Should().Be("not-found"); configuration["key4"].Should().Be("${no-key}"); configuration["key5"].Should().BeEmpty(); + configuration["key6"].Should().BeNull(); + configuration["key7"].Should().BeNull(); configuration["no-key"] = "new-key-value"; @@ -408,6 +411,46 @@ static void AssertTypesInSourceTree(IList sources, int ind } } + [Fact] + public void Binding_property_against_null_overwrites_default_value() + { + var appSettings = new Dictionary + { + ["Root:TestOptions:Value"] = null + }; + + var builder = new ConfigurationBuilder(); + builder.AddInMemoryCollection(appSettings); + builder.AddPlaceholderResolver(); + IConfigurationRoot configuration = builder.Build(); + + IConfigurationSection section = configuration.GetSection("Root:TestOptions"); + var options = section.Get(); + + options.Should().NotBeNull(); + options.Value.Should().BeNull(); + } + + [Fact] + public void Binding_property_against_empty_string_overwrites_default_value() + { + var appSettings = new Dictionary + { + ["Root:TestOptions:Value"] = string.Empty + }; + + var builder = new ConfigurationBuilder(); + builder.AddInMemoryCollection(appSettings); + builder.AddPlaceholderResolver(); + IConfigurationRoot configuration = builder.Build(); + + IConfigurationSection section = configuration.GetSection("Root:TestOptions"); + var options = section.Get(); + + options.Should().NotBeNull(); + options.Value.Should().BeEmpty(); + } + public void Dispose() { _loggerFactory.Dispose(); diff --git a/src/Configuration/test/Placeholder.Test/TestOptions.cs b/src/Configuration/test/Placeholder.Test/TestOptions.cs index bbbeca4572..7600520855 100644 --- a/src/Configuration/test/Placeholder.Test/TestOptions.cs +++ b/src/Configuration/test/Placeholder.Test/TestOptions.cs @@ -6,5 +6,5 @@ namespace Steeltoe.Configuration.Placeholder.Test; internal sealed class TestOptions { - public string? Value { get; set; } + public string? Value { get; set; } = "DefaultValue"; }