diff --git a/src/Configuration/Config.Abstractions/src/ConfigurationRootExtensions.cs b/src/Configuration/Config.Abstractions/src/ConfigurationRootExtensions.cs new file mode 100644 index 00000000000..b6a9024c03a --- /dev/null +++ b/src/Configuration/Config.Abstractions/src/ConfigurationRootExtensions.cs @@ -0,0 +1,75 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Collections.Generic; +using System.Linq; +using System.Text; + +namespace Microsoft.Extensions.Configuration +{ + /// + /// Extension methods for . + /// + public static class ConfigurationRootExtensions + { + /// + /// Generates a human-readable view of the configuration showing where each value came from. + /// + /// The debug view. + public static string GetDebugView(this IConfigurationRoot root) + { + void RecurseChildren( + StringBuilder stringBuilder, + IEnumerable children, + string indent) + { + foreach (var child in children) + { + var valueAndProvider = GetValueAndProvider(root, child.Path); + + if (valueAndProvider.Provider != null) + { + stringBuilder + .Append(indent) + .Append(child.Key) + .Append("=") + .Append(valueAndProvider.Value) + .Append(" (") + .Append(valueAndProvider.Provider) + .AppendLine(")"); + } + else + { + stringBuilder + .Append(indent) + .Append(child.Key) + .AppendLine(":"); + } + + RecurseChildren(stringBuilder, child.GetChildren(), indent + " "); + } + } + + var builder = new StringBuilder(); + + RecurseChildren(builder, root.GetChildren(), ""); + + return builder.ToString(); + } + + private static (string Value, IConfigurationProvider Provider) GetValueAndProvider( + IConfigurationRoot root, + string key) + { + foreach (var provider in root.Providers.Reverse()) + { + if (provider.TryGet(key, out var value)) + { + return (value, provider); + } + } + + return (null, null); + } + } +} diff --git a/src/Configuration/Config.Abstractions/src/IConfigurationProvider.cs b/src/Configuration/Config.Abstractions/src/IConfigurationProvider.cs index fa951582835..949826fd565 100644 --- a/src/Configuration/Config.Abstractions/src/IConfigurationProvider.cs +++ b/src/Configuration/Config.Abstractions/src/IConfigurationProvider.cs @@ -47,4 +47,4 @@ public interface IConfigurationProvider /// The child keys. IEnumerable GetChildKeys(IEnumerable earlierKeys, string parentPath); } -} \ No newline at end of file +} diff --git a/src/Configuration/Config.Abstractions/src/IConfigurationRoot.cs b/src/Configuration/Config.Abstractions/src/IConfigurationRoot.cs index 4587294a293..2d0750da058 100644 --- a/src/Configuration/Config.Abstractions/src/IConfigurationRoot.cs +++ b/src/Configuration/Config.Abstractions/src/IConfigurationRoot.cs @@ -1,7 +1,6 @@ // Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. -using System; using System.Collections.Generic; namespace Microsoft.Extensions.Configuration diff --git a/src/Configuration/Config.AzureKeyVault/test/ConfigurationProviderAzureKeyVaultTest.cs b/src/Configuration/Config.AzureKeyVault/test/ConfigurationProviderAzureKeyVaultTest.cs index fad8cb90b17..9eb15353cb1 100644 --- a/src/Configuration/Config.AzureKeyVault/test/ConfigurationProviderAzureKeyVaultTest.cs +++ b/src/Configuration/Config.AzureKeyVault/test/ConfigurationProviderAzureKeyVaultTest.cs @@ -14,6 +14,11 @@ namespace Microsoft.Extensions.Configuration.AzureKeyVault.Test { public class ConfigurationProviderKeyVaultTest : ConfigurationProviderTestBase { + public override void Null_values_are_included_in_the_config() + { + AssertConfig(BuildConfigRoot(LoadThroughProvider(TestSection.NullsTestConfig)), expectNulls: true); + } + protected override (IConfigurationProvider Provider, System.Action Initializer) LoadThroughProvider( TestSection testConfig) { diff --git a/src/Configuration/Config.EnvironmentVariables/test/ConfigurationProviderEnvironmentVariablesTest.cs b/src/Configuration/Config.EnvironmentVariables/test/ConfigurationProviderEnvironmentVariablesTest.cs index 34853944ed6..65736ff2ae6 100644 --- a/src/Configuration/Config.EnvironmentVariables/test/ConfigurationProviderEnvironmentVariablesTest.cs +++ b/src/Configuration/Config.EnvironmentVariables/test/ConfigurationProviderEnvironmentVariablesTest.cs @@ -26,5 +26,10 @@ public override void Load_from_single_provider_with_differing_case_duplicates_th { AssertConfig(BuildConfigRoot(LoadThroughProvider(TestSection.DuplicatesDifferentCaseTestConfig))); } + + public override void Null_values_are_included_in_the_config() + { + AssertConfig(BuildConfigRoot(LoadThroughProvider(TestSection.NullsTestConfig)), expectNulls: true); + } } } diff --git a/src/Configuration/Config.FileExtensions/src/FileConfigurationProvider.cs b/src/Configuration/Config.FileExtensions/src/FileConfigurationProvider.cs index 92c29faafcb..47fff096d61 100644 --- a/src/Configuration/Config.FileExtensions/src/FileConfigurationProvider.cs +++ b/src/Configuration/Config.FileExtensions/src/FileConfigurationProvider.cs @@ -42,6 +42,13 @@ public FileConfigurationProvider(FileConfigurationSource source) /// The source settings for this provider. /// public FileConfigurationSource Source { get; } + + /// + /// Generates a string representing this provider name and relevant details. + /// + /// The configuration name. + public override string ToString() + => $"{GetType().Name} for '{Source.Path}' ({(Source.Optional ? "Optional" : "Required")})"; private void Load(bool reload) { diff --git a/src/Configuration/Config.Json/test/ConfigurationProviderJsonTest.cs b/src/Configuration/Config.Json/test/ConfigurationProviderJsonTest.cs index 9413785580b..b58263a082b 100644 --- a/src/Configuration/Config.Json/test/ConfigurationProviderJsonTest.cs +++ b/src/Configuration/Config.Json/test/ConfigurationProviderJsonTest.cs @@ -35,13 +35,15 @@ protected override (IConfigurationProvider Provider, Action Initializer) LoadThr private void SectionToJson(StringBuilder jsonBuilder, TestSection section) { + string ValueToJson(object value) => value == null ? "null" : $"'{value}'"; + jsonBuilder.AppendLine("{"); foreach (var tuple in section.Values) { jsonBuilder.AppendLine(tuple.Value.AsArray != null - ? $"'{tuple.Key}': [{string.Join(", ", tuple.Value.AsArray.Select(v => $"'{v}'"))}]," - : $"'{tuple.Key}': '{tuple.Value.AsString}',"); + ? $"'{tuple.Key}': [{string.Join(", ", tuple.Value.AsArray.Select(ValueToJson))}]," + : $"'{tuple.Key}': {ValueToJson(tuple.Value.AsString)},"); } foreach (var tuple in section.Sections) diff --git a/src/Configuration/Config.KeyPerFile/src/KeyPerFileConfigurationProvider.cs b/src/Configuration/Config.KeyPerFile/src/KeyPerFileConfigurationProvider.cs index 47488957441..13541110e63 100644 --- a/src/Configuration/Config.KeyPerFile/src/KeyPerFileConfigurationProvider.cs +++ b/src/Configuration/Config.KeyPerFile/src/KeyPerFileConfigurationProvider.cs @@ -39,10 +39,8 @@ public override void Load() { return; } - else - { - throw new DirectoryNotFoundException("A non-null file provider for the directory is required when this source is not optional."); - } + + throw new DirectoryNotFoundException("A non-null file provider for the directory is required when this source is not optional."); } var directory = Source.FileProvider.GetDirectoryContents("/"); @@ -68,5 +66,15 @@ public override void Load() } } } + + private string GetDirectoryName() + => Source.FileProvider?.GetFileInfo("/")?.PhysicalPath ?? ""; + + /// + /// Generates a string representing this provider name and relevant details. + /// + /// The configuration name. + public override string ToString() + => $"{GetType().Name} for files in '{GetDirectoryName()}' ({(Source.Optional ? "Optional" : "Required")})"; } } diff --git a/src/Configuration/Config.KeyPerFile/test/KeyPerFileTests.cs b/src/Configuration/Config.KeyPerFile/test/KeyPerFileTests.cs index fac0e839e8b..4de528c0116 100644 --- a/src/Configuration/Config.KeyPerFile/test/KeyPerFileTests.cs +++ b/src/Configuration/Config.KeyPerFile/test/KeyPerFileTests.cs @@ -190,20 +190,11 @@ public TestFileProvider(params IFileInfo[] files) _contents = new TestDirectoryContents(files); } - public IDirectoryContents GetDirectoryContents(string subpath) - { - return _contents; - } + public IDirectoryContents GetDirectoryContents(string subpath) => _contents; - public IFileInfo GetFileInfo(string subpath) - { - throw new NotImplementedException(); - } + public IFileInfo GetFileInfo(string subpath) => new TestFile("TestDirectory"); - public IChangeToken Watch(string filter) - { - throw new NotImplementedException(); - } + public IChangeToken Watch(string filter) => throw new NotImplementedException(); } class TestDirectoryContents : IDirectoryContents @@ -215,75 +206,33 @@ public TestDirectoryContents(params IFileInfo[] files) _list = new List(files); } - public bool Exists - { - get - { - return true; - } - } + public bool Exists => true; - public IEnumerator GetEnumerator() - { - return _list.GetEnumerator(); - } + public IEnumerator GetEnumerator() => _list.GetEnumerator(); - IEnumerator IEnumerable.GetEnumerator() - { - return GetEnumerator(); - } + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); } //TODO: Probably need a directory and file type. class TestFile : IFileInfo { - private string _name; - private string _contents; + private readonly string _name; + private readonly string _contents; - public bool Exists - { - get - { - return true; - } - } + public bool Exists => true; public bool IsDirectory { get; } - public DateTimeOffset LastModified - { - get - { - throw new NotImplementedException(); - } - } + public DateTimeOffset LastModified => throw new NotImplementedException(); - public long Length - { - get - { - throw new NotImplementedException(); - } - } + public long Length => throw new NotImplementedException(); - public string Name - { - get - { - return _name; - } - } + public string Name => _name; - public string PhysicalPath - { - get - { - throw new NotImplementedException(); - } - } + public string PhysicalPath => "Root/" + Name; public TestFile(string name) { @@ -304,7 +253,9 @@ public Stream CreateReadStream() throw new InvalidOperationException("Cannot create stream from directory"); } - return new MemoryStream(Encoding.UTF8.GetBytes(_contents)); + return _contents == null + ? new MemoryStream() + : new MemoryStream(Encoding.UTF8.GetBytes(_contents)); } } } diff --git a/src/Configuration/Config.Xml/test/ConfigurationProviderXmlTest.cs b/src/Configuration/Config.Xml/test/ConfigurationProviderXmlTest.cs index 9d42d8c207a..6e235149867 100644 --- a/src/Configuration/Config.Xml/test/ConfigurationProviderXmlTest.cs +++ b/src/Configuration/Config.Xml/test/ConfigurationProviderXmlTest.cs @@ -10,11 +10,42 @@ namespace Microsoft.Extensions.Configuration.Xml.Test { public class ConfigurationProviderXmlTest : ConfigurationProviderTestBase { + public override void Combine_before_other_provider() + { + // Disabled test due to XML handling of empty section. + } + public override void Combine_after_other_provider() { // Disabled test due to XML handling of empty section. } + public override void Has_debug_view() + { + var configRoot = BuildConfigRoot(LoadThroughProvider(TestSection.TestConfig)); + var providerTag = configRoot.Providers.Single().ToString(); + + var expected = + $@"Key1=Value1 ({providerTag}) +Section1: + Key2=Value12 ({providerTag}) + Section2: + Key3=Value123 ({providerTag}) + Key3a: + 0=ArrayValue0 ({providerTag}) + Name=0 ({providerTag}) + 1=ArrayValue1 ({providerTag}) + Name=1 ({providerTag}) + 2=ArrayValue2 ({providerTag}) + Name=2 ({providerTag}) +Section3: + Section4: + Key4=Value344 ({providerTag}) +"; + + AssertDebugView(configRoot, expected); + } + protected override (IConfigurationProvider Provider, Action Initializer) LoadThroughProvider(TestSection testConfig) { var xmlBuilder = new StringBuilder(); @@ -36,7 +67,7 @@ private void SectionToXml(StringBuilder xmlBuilder, string sectionName, TestSect foreach (var tuple in section.Values) { - if (tuple.Value.AsString != null) + if (tuple.Value.AsArray == null) { xmlBuilder.AppendLine($"<{tuple.Key}>{tuple.Value.AsString}"); } diff --git a/src/Configuration/Config/src/ConfigurationProvider.cs b/src/Configuration/Config/src/ConfigurationProvider.cs index 8011a6b218e..4e0d438f707 100644 --- a/src/Configuration/Config/src/ConfigurationProvider.cs +++ b/src/Configuration/Config/src/ConfigurationProvider.cs @@ -94,5 +94,11 @@ protected void OnReload() var previousToken = Interlocked.Exchange(ref _reloadToken, new ConfigurationReloadToken()); previousToken.OnReload(); } + + /// + /// Generates a string representing this provider name and relevant details. + /// + /// The configuration name. + public override string ToString() => $"{GetType().Name}"; } } diff --git a/src/Configuration/Config/src/ConfigurationRoot.cs b/src/Configuration/Config/src/ConfigurationRoot.cs index 275efa88126..f104e8178ce 100644 --- a/src/Configuration/Config/src/ConfigurationRoot.cs +++ b/src/Configuration/Config/src/ConfigurationRoot.cs @@ -14,7 +14,7 @@ namespace Microsoft.Extensions.Configuration /// public class ConfigurationRoot : IConfigurationRoot { - private IList _providers; + private readonly IList _providers; private ConfigurationReloadToken _changeToken = new ConfigurationReloadToken(); /// @@ -52,9 +52,7 @@ public string this[string key] { foreach (var provider in _providers.Reverse()) { - string value; - - if (provider.TryGet(key, out value)) + if (provider.TryGet(key, out var value)) { return value; } @@ -62,7 +60,6 @@ public string this[string key] return null; } - set { if (!_providers.Any()) diff --git a/src/Configuration/Config/src/ConfigurationRootExtension.cs b/src/Configuration/Config/src/InternalConfigurationRootExtensions.cs similarity index 94% rename from src/Configuration/Config/src/ConfigurationRootExtension.cs rename to src/Configuration/Config/src/InternalConfigurationRootExtensions.cs index 08366048a29..e03aaa31b9d 100644 --- a/src/Configuration/Config/src/ConfigurationRootExtension.cs +++ b/src/Configuration/Config/src/InternalConfigurationRootExtensions.cs @@ -7,7 +7,7 @@ namespace Microsoft.Extensions.Configuration /// /// Extensions method for /// - internal static class ConfigurationRootExtension + internal static class InternalConfigurationRootExtensions { /// /// Gets the immediate children sub-sections of configuration root based on key. diff --git a/src/Configuration/Config/test/ConfigurationProviderMemoryTest.cs b/src/Configuration/Config/test/ConfigurationProviderMemoryTest.cs index 8d15ed83d1b..3d81b57c4eb 100644 --- a/src/Configuration/Config/test/ConfigurationProviderMemoryTest.cs +++ b/src/Configuration/Config/test/ConfigurationProviderMemoryTest.cs @@ -7,6 +7,11 @@ namespace Microsoft.Extensions.Configuration.Test { public class ConfigurationProviderMemoryTest : ConfigurationProviderTestBase { + public override void Null_values_are_included_in_the_config() + { + AssertConfig(BuildConfigRoot(LoadThroughProvider(TestSection.NullsTestConfig)), expectNulls: true); + } + protected override (IConfigurationProvider Provider, Action Initializer) LoadThroughProvider( TestSection testConfig) => LoadUsingMemoryProvider(testConfig); diff --git a/src/Configuration/Config/test/ConfigurationProviderTestBase.cs b/src/Configuration/Config/test/ConfigurationProviderTestBase.cs index c18bea8d285..4609ee2560f 100644 --- a/src/Configuration/Config/test/ConfigurationProviderTestBase.cs +++ b/src/Configuration/Config/test/ConfigurationProviderTestBase.cs @@ -14,7 +14,39 @@ public abstract class ConfigurationProviderTestBase [Fact] public virtual void Load_from_single_provider() { - AssertConfig(BuildConfigRoot(LoadThroughProvider(TestSection.TestConfig))); + var configRoot = BuildConfigRoot(LoadThroughProvider(TestSection.TestConfig)); + + AssertConfig(configRoot); + } + + [Fact] + public virtual void Has_debug_view() + { + var configRoot = BuildConfigRoot(LoadThroughProvider(TestSection.TestConfig)); + var providerTag = configRoot.Providers.Single().ToString(); + + var expected = + $@"Key1=Value1 ({providerTag}) +Section1: + Key2=Value12 ({providerTag}) + Section2: + Key3=Value123 ({providerTag}) + Key3a: + 0=ArrayValue0 ({providerTag}) + 1=ArrayValue1 ({providerTag}) + 2=ArrayValue2 ({providerTag}) +Section3: + Section4: + Key4=Value344 ({providerTag}) +"; + + AssertDebugView(configRoot, expected); + } + + [Fact] + public virtual void Null_values_are_included_in_the_config() + { + AssertConfig(BuildConfigRoot(LoadThroughProvider(TestSection.NullsTestConfig)), expectNulls: true, nullValue: ""); } [Fact] @@ -22,8 +54,13 @@ public virtual void Combine_after_other_provider() { AssertConfig( BuildConfigRoot( - LoadUsingMemoryProvider(TestSection.MissingSection2Config), + LoadUsingMemoryProvider(TestSection.MissingSection2ValuesConfig), LoadThroughProvider(TestSection.MissingSection4Config))); + + AssertConfig( + BuildConfigRoot( + LoadUsingMemoryProvider(TestSection.MissingSection4Config), + LoadThroughProvider(TestSection.MissingSection2ValuesConfig))); } [Fact] @@ -31,8 +68,13 @@ public virtual void Combine_before_other_provider() { AssertConfig( BuildConfigRoot( - LoadThroughProvider(TestSection.MissingSection2Config), + LoadThroughProvider(TestSection.MissingSection2ValuesConfig), LoadUsingMemoryProvider(TestSection.MissingSection4Config))); + + AssertConfig( + BuildConfigRoot( + LoadThroughProvider(TestSection.MissingSection4Config), + LoadUsingMemoryProvider(TestSection.MissingSection2ValuesConfig))); } [Fact] @@ -128,48 +170,83 @@ public class Section4AsOptions public string Key4 { get; set; } } - protected virtual void AssertConfig(IConfigurationRoot config) + protected virtual void AssertDebugView( + IConfigurationRoot config, + string expected) + { + string RemoveLineEnds(string source) => source.Replace("\n", "").Replace("\r", ""); + + var actual = config.GetDebugView(); + + Assert.Equal( + RemoveLineEnds(expected), + RemoveLineEnds(actual)); + } + + protected virtual void AssertConfig( + IConfigurationRoot config, + bool expectNulls = false, + string nullValue = null) { - Assert.Equal("Value1", config["Key1"], StringComparer.InvariantCultureIgnoreCase); - Assert.Equal("Value12", config["Section1:Key2"], StringComparer.InvariantCultureIgnoreCase); - Assert.Equal("Value123", config["Section1:Section2:Key3"], StringComparer.InvariantCultureIgnoreCase); - Assert.Equal("ArrayValue0", config["Section1:Section2:Key3a:0"], StringComparer.InvariantCultureIgnoreCase); - Assert.Equal("ArrayValue1", config["Section1:Section2:Key3a:1"], StringComparer.InvariantCultureIgnoreCase); - Assert.Equal("ArrayValue2", config["Section1:Section2:Key3a:2"], StringComparer.InvariantCultureIgnoreCase); - Assert.Equal("Value344", config["Section3:Section4:Key4"], StringComparer.InvariantCultureIgnoreCase); + var value1 = expectNulls ? nullValue : "Value1"; + var value12 = expectNulls ? nullValue : "Value12"; + var value123 = expectNulls ? nullValue : "Value123"; + var arrayvalue0 = expectNulls ? nullValue : "ArrayValue0"; + var arrayvalue1 = expectNulls ? nullValue : "ArrayValue1"; + var arrayvalue2 = expectNulls ? nullValue : "ArrayValue2"; + var value344 = expectNulls ? nullValue : "Value344"; + + Assert.Equal(value1, config["Key1"], StringComparer.InvariantCultureIgnoreCase); + Assert.Equal(value12, config["Section1:Key2"], StringComparer.InvariantCultureIgnoreCase); + Assert.Equal(value123, config["Section1:Section2:Key3"], StringComparer.InvariantCultureIgnoreCase); + Assert.Equal(arrayvalue0, config["Section1:Section2:Key3a:0"], StringComparer.InvariantCultureIgnoreCase); + Assert.Equal(arrayvalue1, config["Section1:Section2:Key3a:1"], StringComparer.InvariantCultureIgnoreCase); + Assert.Equal(arrayvalue2, config["Section1:Section2:Key3a:2"], StringComparer.InvariantCultureIgnoreCase); + Assert.Equal(value344, config["Section3:Section4:Key4"], StringComparer.InvariantCultureIgnoreCase); var section1 = config.GetSection("Section1"); - Assert.Equal("Value12", section1["Key2"], StringComparer.InvariantCultureIgnoreCase); - Assert.Equal("Value123", section1["Section2:Key3"], StringComparer.InvariantCultureIgnoreCase); - Assert.Equal("ArrayValue0", section1["Section2:Key3a:0"], StringComparer.InvariantCultureIgnoreCase); - Assert.Equal("ArrayValue1", section1["Section2:Key3a:1"], StringComparer.InvariantCultureIgnoreCase); - Assert.Equal("ArrayValue2", section1["Section2:Key3a:2"], StringComparer.InvariantCultureIgnoreCase); + Assert.Equal(value12, section1["Key2"], StringComparer.InvariantCultureIgnoreCase); + Assert.Equal(value123, section1["Section2:Key3"], StringComparer.InvariantCultureIgnoreCase); + Assert.Equal(arrayvalue0, section1["Section2:Key3a:0"], StringComparer.InvariantCultureIgnoreCase); + Assert.Equal(arrayvalue1, section1["Section2:Key3a:1"], StringComparer.InvariantCultureIgnoreCase); + Assert.Equal(arrayvalue2, section1["Section2:Key3a:2"], StringComparer.InvariantCultureIgnoreCase); Assert.Equal("Section1", section1.Path, StringComparer.InvariantCultureIgnoreCase); Assert.Null(section1.Value); var section2 = config.GetSection("Section1:Section2"); - Assert.Equal("Value123", section2["Key3"], StringComparer.InvariantCultureIgnoreCase); - Assert.Equal("ArrayValue0", section2["Key3a:0"], StringComparer.InvariantCultureIgnoreCase); - Assert.Equal("ArrayValue1", section2["Key3a:1"], StringComparer.InvariantCultureIgnoreCase); - Assert.Equal("ArrayValue2", section2["Key3a:2"], StringComparer.InvariantCultureIgnoreCase); + Assert.Equal(value123, section2["Key3"], StringComparer.InvariantCultureIgnoreCase); + Assert.Equal(arrayvalue0, section2["Key3a:0"], StringComparer.InvariantCultureIgnoreCase); + Assert.Equal(arrayvalue1, section2["Key3a:1"], StringComparer.InvariantCultureIgnoreCase); + Assert.Equal(arrayvalue2, section2["Key3a:2"], StringComparer.InvariantCultureIgnoreCase); Assert.Equal("Section1:Section2", section2.Path, StringComparer.InvariantCultureIgnoreCase); Assert.Null(section2.Value); section2 = section1.GetSection("Section2"); - Assert.Equal("Value123", section2["Key3"], StringComparer.InvariantCultureIgnoreCase); - Assert.Equal("ArrayValue0", section2["Key3a:0"], StringComparer.InvariantCultureIgnoreCase); - Assert.Equal("ArrayValue1", section2["Key3a:1"], StringComparer.InvariantCultureIgnoreCase); - Assert.Equal("ArrayValue2", section2["Key3a:2"], StringComparer.InvariantCultureIgnoreCase); + Assert.Equal(value123, section2["Key3"], StringComparer.InvariantCultureIgnoreCase); + Assert.Equal(arrayvalue0, section2["Key3a:0"], StringComparer.InvariantCultureIgnoreCase); + Assert.Equal(arrayvalue1, section2["Key3a:1"], StringComparer.InvariantCultureIgnoreCase); + Assert.Equal(arrayvalue2, section2["Key3a:2"], StringComparer.InvariantCultureIgnoreCase); Assert.Equal("Section1:Section2", section2.Path, StringComparer.InvariantCultureIgnoreCase); Assert.Null(section2.Value); + var section3a = section2.GetSection("Key3a"); + Assert.Equal(arrayvalue0, section3a["0"], StringComparer.InvariantCultureIgnoreCase); + Assert.Equal(arrayvalue1, section3a["1"], StringComparer.InvariantCultureIgnoreCase); + Assert.Equal(arrayvalue2, section3a["2"], StringComparer.InvariantCultureIgnoreCase); + Assert.Equal("Section1:Section2:Key3a", section3a.Path, StringComparer.InvariantCultureIgnoreCase); + Assert.Null(section3a.Value); + + var section3 = config.GetSection("Section3"); + Assert.Equal("Section3", section3.Path, StringComparer.InvariantCultureIgnoreCase); + Assert.Null(section3.Value); + var section4 = config.GetSection("Section3:Section4"); - Assert.Equal("Value344", section4["Key4"], StringComparer.InvariantCultureIgnoreCase); + Assert.Equal(value344, section4["Key4"], StringComparer.InvariantCultureIgnoreCase); Assert.Equal("Section3:Section4", section4.Path, StringComparer.InvariantCultureIgnoreCase); Assert.Null(section4.Value); section4 = config.GetSection("Section3").GetSection("Section4"); - Assert.Equal("Value344", section4["Key4"], StringComparer.InvariantCultureIgnoreCase); + Assert.Equal(value344, section4["Key4"], StringComparer.InvariantCultureIgnoreCase); Assert.Equal("Section3:Section4", section4.Path, StringComparer.InvariantCultureIgnoreCase); Assert.Null(section4.Value); @@ -179,7 +256,7 @@ protected virtual void AssertConfig(IConfigurationRoot config) Assert.Equal("Key1", sections[0].Key, StringComparer.InvariantCultureIgnoreCase); Assert.Equal("Key1", sections[0].Path, StringComparer.InvariantCultureIgnoreCase); - Assert.Equal("Value1", sections[0].Value, StringComparer.InvariantCultureIgnoreCase); + Assert.Equal(value1, sections[0].Value, StringComparer.InvariantCultureIgnoreCase); Assert.Equal("Section1", sections[1].Key, StringComparer.InvariantCultureIgnoreCase); Assert.Equal("Section1", sections[1].Path, StringComparer.InvariantCultureIgnoreCase); @@ -195,11 +272,55 @@ protected virtual void AssertConfig(IConfigurationRoot config) Assert.Equal("Key2", sections[0].Key, StringComparer.InvariantCultureIgnoreCase); Assert.Equal("Section1:Key2", sections[0].Path, StringComparer.InvariantCultureIgnoreCase); - Assert.Equal("Value12", sections[0].Value, StringComparer.InvariantCultureIgnoreCase); + Assert.Equal(value12, sections[0].Value, StringComparer.InvariantCultureIgnoreCase); Assert.Equal("Section2", sections[1].Key, StringComparer.InvariantCultureIgnoreCase); Assert.Equal("Section1:Section2", sections[1].Path, StringComparer.InvariantCultureIgnoreCase); Assert.Null(sections[1].Value); + + sections = section2.GetChildren().ToList(); + + Assert.Equal(2, sections.Count); + + Assert.Equal("Key3", sections[0].Key, StringComparer.InvariantCultureIgnoreCase); + Assert.Equal("Section1:Section2:Key3", sections[0].Path, StringComparer.InvariantCultureIgnoreCase); + Assert.Equal(value123, sections[0].Value, StringComparer.InvariantCultureIgnoreCase); + + Assert.Equal("Key3a", sections[1].Key, StringComparer.InvariantCultureIgnoreCase); + Assert.Equal("Section1:Section2:Key3a", sections[1].Path, StringComparer.InvariantCultureIgnoreCase); + Assert.Null(sections[1].Value); + + sections = section3a.GetChildren().ToList(); + + Assert.Equal(3, sections.Count); + + Assert.Equal("0", sections[0].Key, StringComparer.InvariantCultureIgnoreCase); + Assert.Equal("Section1:Section2:Key3a:0", sections[0].Path, StringComparer.InvariantCultureIgnoreCase); + Assert.Equal(arrayvalue0, sections[0].Value, StringComparer.InvariantCultureIgnoreCase); + + Assert.Equal("1", sections[1].Key, StringComparer.InvariantCultureIgnoreCase); + Assert.Equal("Section1:Section2:Key3a:1", sections[1].Path, StringComparer.InvariantCultureIgnoreCase); + Assert.Equal(arrayvalue1, sections[1].Value, StringComparer.InvariantCultureIgnoreCase); + + Assert.Equal("2", sections[2].Key, StringComparer.InvariantCultureIgnoreCase); + Assert.Equal("Section1:Section2:Key3a:2", sections[2].Path, StringComparer.InvariantCultureIgnoreCase); + Assert.Equal(arrayvalue2, sections[2].Value, StringComparer.InvariantCultureIgnoreCase); + + sections = section3.GetChildren().ToList(); + + Assert.Single(sections); + + Assert.Equal("Section4", sections[0].Key, StringComparer.InvariantCultureIgnoreCase); + Assert.Equal("Section3:Section4", sections[0].Path, StringComparer.InvariantCultureIgnoreCase); + Assert.Null(sections[0].Value); + + sections = section4.GetChildren().ToList(); + + Assert.Single(sections); + + Assert.Equal("Key4", sections[0].Key, StringComparer.InvariantCultureIgnoreCase); + Assert.Equal("Section3:Section4:Key4", sections[0].Path, StringComparer.InvariantCultureIgnoreCase); + Assert.Equal(value344, sections[0].Value, StringComparer.InvariantCultureIgnoreCase); } protected abstract (IConfigurationProvider Provider, Action Initializer) LoadThroughProvider(TestSection testConfig); @@ -272,7 +393,7 @@ public TestKeyValue(string[] values) public IEnumerable<(string Key, string Value)> Expand(string key) { - if (AsString != null) + if (AsArray == null) { yield return (key, AsString); } @@ -310,7 +431,7 @@ protected class TestSection Values = new[] { ("Key3", (TestKeyValue)"Value123"), - ("Key3a", (TestKeyValue)new[] {"ArrayValue0", "ArrayValue1", "ArrayValue2"}), + ("Key3a", (TestKeyValue)new[] {"ArrayValue0", "ArrayValue1", "ArrayValue2"}) }, }) } @@ -321,11 +442,10 @@ protected class TestSection { ("Section4", new TestSection { - Values = new[] {("Key4", (TestKeyValue)"Value344")}, + Values = new[] {("Key4", (TestKeyValue)"Value344")} }) } - }), - + }) } }; @@ -345,7 +465,7 @@ protected class TestSection Values = new[] { ("Key3", (TestKeyValue)"-----"), - ("Key3a", (TestKeyValue)new[] {"-----------", "-----------", "-----------"}), + ("Key3a", (TestKeyValue)new[] {"-----------", "-----------", "-----------"}) }, }) } @@ -356,15 +476,14 @@ protected class TestSection { ("Section4", new TestSection { - Values = new[] {("Key4", (TestKeyValue)"--------")}, + Values = new[] {("Key4", (TestKeyValue)"--------")} }) } - }), - + }) } }; - public static TestSection MissingSection2Config { get; } + public static TestSection MissingSection2ValuesConfig { get; } = new TestSection { Values = new[] { ("Key1", (TestKeyValue)"Value1") }, @@ -373,6 +492,16 @@ protected class TestSection ("Section1", new TestSection { Values = new[] {("Key2", (TestKeyValue)"Value12")}, + Sections = new[] + { + ("Section2", new TestSection + { + Values = new[] + { + ("Key3a", (TestKeyValue)new[] {"ArrayValue0"}) + }, + }) + } }), ("Section3", new TestSection { @@ -380,11 +509,10 @@ protected class TestSection { ("Section4", new TestSection { - Values = new[] {("Key4", (TestKeyValue)"Value344")}, + Values = new[] {("Key4", (TestKeyValue)"Value344")} }) } - }), - + }) } }; @@ -405,15 +533,12 @@ protected class TestSection Values = new[] { ("Key3", (TestKeyValue)"Value123"), - ("Key3a", (TestKeyValue)new[] {"ArrayValue0", "ArrayValue1", "ArrayValue2"}), + ("Key3a", (TestKeyValue)new[] {"ArrayValue0", "ArrayValue1", "ArrayValue2"}) }, }) } }), - ("Section3", new TestSection - { - }), - + ("Section3", new TestSection()) } }; @@ -433,7 +558,7 @@ protected class TestSection Values = new[] { ("KeY3", (TestKeyValue)"Value123"), - ("KeY3a", (TestKeyValue)new[] {"ArrayValue0", "ArrayValue1", "ArrayValue2"}), + ("KeY3a", (TestKeyValue)new[] {"ArrayValue0", "ArrayValue1", "ArrayValue2"}) }, }) } @@ -444,11 +569,10 @@ protected class TestSection { ("SectioN4", new TestSection { - Values = new[] {("KeY4", (TestKeyValue)"Value344")}, + Values = new[] {("KeY4", (TestKeyValue)"Value344")} }) } - }), - + }) } }; @@ -472,7 +596,7 @@ protected class TestSection Values = new[] { ("Key3", (TestKeyValue)"Value123"), - ("Key3a", (TestKeyValue)new[] {"ArrayValue0", "ArrayValue1", "ArrayValue2"}), + ("Key3a", (TestKeyValue)new[] {"ArrayValue0", "ArrayValue1", "ArrayValue2"}) }, }), ("Section2", new TestSection @@ -480,7 +604,7 @@ protected class TestSection Values = new[] { ("Key3", (TestKeyValue)"Value123"), - ("Key3a", (TestKeyValue)new[] {"ArrayValue0", "ArrayValue1", "ArrayValue2"}), + ("Key3a", (TestKeyValue)new[] {"ArrayValue0", "ArrayValue1", "ArrayValue2"}) }, }) @@ -492,11 +616,10 @@ protected class TestSection { ("Section4", new TestSection { - Values = new[] {("Key4", (TestKeyValue)"Value344")}, + Values = new[] {("Key4", (TestKeyValue)"Value344")} }) } - }), - + }) } }; @@ -520,7 +643,7 @@ protected class TestSection Values = new[] { ("Key3", (TestKeyValue)"Value123"), - ("Key3a", (TestKeyValue)new[] {"ArrayValue0", "ArrayValue1", "ArrayValue2"}), + ("Key3a", (TestKeyValue)new[] {"ArrayValue0", "ArrayValue1", "ArrayValue2"}) }, }), ("SectioN2", new TestSection @@ -528,7 +651,7 @@ protected class TestSection Values = new[] { ("KeY3", (TestKeyValue)"Value123"), - ("KeY3a", (TestKeyValue)new[] {"ArrayValue0", "ArrayValue1", "ArrayValue2"}), + ("KeY3a", (TestKeyValue)new[] {"ArrayValue0", "ArrayValue1", "ArrayValue2"}) }, }) @@ -540,11 +663,97 @@ protected class TestSection { ("Section4", new TestSection { - Values = new[] {("Key4", (TestKeyValue)"Value344")}, + Values = new[] {("Key4", (TestKeyValue)"Value344")} + }) + } + }) + } + }; + + public static TestSection NullsTestConfig { get; } + = new TestSection + { + Values = new[] { ("Key1", new TestKeyValue((string)null)) }, + Sections = new[] + { + ("Section1", new TestSection + { + Values = new[] {("Key2", new TestKeyValue((string)null))}, + Sections = new[] + { + ("Section2", new TestSection + { + Values = new[] + { + ("Key3", new TestKeyValue((string)null)), + ("Key3a", (TestKeyValue)new string[] {null, null, null}) + }, }) } }), + ("Section3", new TestSection + { + Sections = new[] + { + ("Section4", new TestSection + { + Values = new[] {("Key4", new TestKeyValue((string)null))} + }) + } + }) + } + }; + public static TestSection ExtraValuesTestConfig { get; } + = new TestSection + { + Values = new[] + { + ("Key1", (TestKeyValue)"Value1"), + ("Key1r", (TestKeyValue)"Value1r") + }, + Sections = new[] + { + ("Section1", new TestSection + { + Values = new[] + { + ("Key2", (TestKeyValue)"Value12"), + ("Key2r", (TestKeyValue)"Value12r") + }, + Sections = new[] + { + ("Section2", new TestSection + { + Values = new[] + { + ("Key3", (TestKeyValue)"Value123"), + ("Key3a", (TestKeyValue)new[] {"ArrayValue0", "ArrayValue1", "ArrayValue2", "ArrayValue2r"}), + ("Key3ar", (TestKeyValue)new[] {"ArrayValue0r"}) + }, + }) + } + }), + ("Section3", new TestSection + { + Sections = new[] + { + ("Section4", new TestSection + { + Values = new[] {("Key4", (TestKeyValue)"Value344")} + }) + } + }), + ("Section5r", new TestSection + { + Sections = new[] + { + ("Section6r", new TestSection + { + Values = new[] {("Key5r", (TestKeyValue)"Value565r")} + }) + } + }) } }; }