diff --git a/src/Configuration.KeyPerFile/Directory.Build.props b/src/Configuration.KeyPerFile/Directory.Build.props
new file mode 100644
index 000000000000..63d0c8b102d7
--- /dev/null
+++ b/src/Configuration.KeyPerFile/Directory.Build.props
@@ -0,0 +1,9 @@
+
+
+
+
+ true
+ configuration
+ $(NoWarn);PKG0001
+
+
diff --git a/src/Configuration.KeyPerFile/ref/Microsoft.Extensions.Configuration.KeyPerFile.csproj b/src/Configuration.KeyPerFile/ref/Microsoft.Extensions.Configuration.KeyPerFile.csproj
new file mode 100644
index 000000000000..8dbe6d3ba11c
--- /dev/null
+++ b/src/Configuration.KeyPerFile/ref/Microsoft.Extensions.Configuration.KeyPerFile.csproj
@@ -0,0 +1,16 @@
+
+
+
+ netstandard2.0;$(DefaultNetCoreTargetFramework)
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Configuration.KeyPerFile/ref/Microsoft.Extensions.Configuration.KeyPerFile.netcoreapp.cs b/src/Configuration.KeyPerFile/ref/Microsoft.Extensions.Configuration.KeyPerFile.netcoreapp.cs
new file mode 100644
index 000000000000..d6e800587ad9
--- /dev/null
+++ b/src/Configuration.KeyPerFile/ref/Microsoft.Extensions.Configuration.KeyPerFile.netcoreapp.cs
@@ -0,0 +1,34 @@
+// 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.
+
+namespace Microsoft.Extensions.Configuration
+{
+ public static partial class KeyPerFileConfigurationBuilderExtensions
+ {
+ public static Microsoft.Extensions.Configuration.IConfigurationBuilder AddKeyPerFile(this Microsoft.Extensions.Configuration.IConfigurationBuilder builder, System.Action configureSource) { throw null; }
+ public static Microsoft.Extensions.Configuration.IConfigurationBuilder AddKeyPerFile(this Microsoft.Extensions.Configuration.IConfigurationBuilder builder, string directoryPath) { throw null; }
+ public static Microsoft.Extensions.Configuration.IConfigurationBuilder AddKeyPerFile(this Microsoft.Extensions.Configuration.IConfigurationBuilder builder, string directoryPath, bool optional) { throw null; }
+ public static Microsoft.Extensions.Configuration.IConfigurationBuilder AddKeyPerFile(this Microsoft.Extensions.Configuration.IConfigurationBuilder builder, string directoryPath, bool optional, bool reloadOnChange) { throw null; }
+ }
+}
+namespace Microsoft.Extensions.Configuration.KeyPerFile
+{
+ public partial class KeyPerFileConfigurationProvider : Microsoft.Extensions.Configuration.ConfigurationProvider, System.IDisposable
+ {
+ public KeyPerFileConfigurationProvider(Microsoft.Extensions.Configuration.KeyPerFile.KeyPerFileConfigurationSource source) { }
+ public void Dispose() { }
+ public override void Load() { }
+ public override string ToString() { throw null; }
+ }
+ public partial class KeyPerFileConfigurationSource : Microsoft.Extensions.Configuration.IConfigurationSource
+ {
+ public KeyPerFileConfigurationSource() { }
+ public Microsoft.Extensions.FileProviders.IFileProvider FileProvider { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
+ public System.Func IgnoreCondition { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
+ public string IgnorePrefix { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
+ public bool Optional { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
+ public int ReloadDelay { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
+ public bool ReloadOnChange { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
+ public Microsoft.Extensions.Configuration.IConfigurationProvider Build(Microsoft.Extensions.Configuration.IConfigurationBuilder builder) { throw null; }
+ }
+}
diff --git a/src/Configuration.KeyPerFile/ref/Microsoft.Extensions.Configuration.KeyPerFile.netstandard2.0.cs b/src/Configuration.KeyPerFile/ref/Microsoft.Extensions.Configuration.KeyPerFile.netstandard2.0.cs
new file mode 100644
index 000000000000..d6e800587ad9
--- /dev/null
+++ b/src/Configuration.KeyPerFile/ref/Microsoft.Extensions.Configuration.KeyPerFile.netstandard2.0.cs
@@ -0,0 +1,34 @@
+// 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.
+
+namespace Microsoft.Extensions.Configuration
+{
+ public static partial class KeyPerFileConfigurationBuilderExtensions
+ {
+ public static Microsoft.Extensions.Configuration.IConfigurationBuilder AddKeyPerFile(this Microsoft.Extensions.Configuration.IConfigurationBuilder builder, System.Action configureSource) { throw null; }
+ public static Microsoft.Extensions.Configuration.IConfigurationBuilder AddKeyPerFile(this Microsoft.Extensions.Configuration.IConfigurationBuilder builder, string directoryPath) { throw null; }
+ public static Microsoft.Extensions.Configuration.IConfigurationBuilder AddKeyPerFile(this Microsoft.Extensions.Configuration.IConfigurationBuilder builder, string directoryPath, bool optional) { throw null; }
+ public static Microsoft.Extensions.Configuration.IConfigurationBuilder AddKeyPerFile(this Microsoft.Extensions.Configuration.IConfigurationBuilder builder, string directoryPath, bool optional, bool reloadOnChange) { throw null; }
+ }
+}
+namespace Microsoft.Extensions.Configuration.KeyPerFile
+{
+ public partial class KeyPerFileConfigurationProvider : Microsoft.Extensions.Configuration.ConfigurationProvider, System.IDisposable
+ {
+ public KeyPerFileConfigurationProvider(Microsoft.Extensions.Configuration.KeyPerFile.KeyPerFileConfigurationSource source) { }
+ public void Dispose() { }
+ public override void Load() { }
+ public override string ToString() { throw null; }
+ }
+ public partial class KeyPerFileConfigurationSource : Microsoft.Extensions.Configuration.IConfigurationSource
+ {
+ public KeyPerFileConfigurationSource() { }
+ public Microsoft.Extensions.FileProviders.IFileProvider FileProvider { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
+ public System.Func IgnoreCondition { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
+ public string IgnorePrefix { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
+ public bool Optional { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
+ public int ReloadDelay { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
+ public bool ReloadOnChange { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
+ public Microsoft.Extensions.Configuration.IConfigurationProvider Build(Microsoft.Extensions.Configuration.IConfigurationBuilder builder) { throw null; }
+ }
+}
diff --git a/src/Configuration.KeyPerFile/src/KeyPerFileConfigurationBuilderExtensions.cs b/src/Configuration.KeyPerFile/src/KeyPerFileConfigurationBuilderExtensions.cs
new file mode 100644
index 000000000000..e4c8dd58eea5
--- /dev/null
+++ b/src/Configuration.KeyPerFile/src/KeyPerFileConfigurationBuilderExtensions.cs
@@ -0,0 +1,65 @@
+using System;
+using System.IO;
+using Microsoft.Extensions.Configuration.KeyPerFile;
+using Microsoft.Extensions.FileProviders;
+
+namespace Microsoft.Extensions.Configuration
+{
+ ///
+ /// Extension methods for registering with .
+ ///
+ public static class KeyPerFileConfigurationBuilderExtensions
+ {
+ ///
+ /// Adds configuration using files from a directory. File names are used as the key,
+ /// file contents are used as the value.
+ ///
+ /// The to add to.
+ /// The path to the directory.
+ /// The .
+ public static IConfigurationBuilder AddKeyPerFile(this IConfigurationBuilder builder, string directoryPath)
+ => builder.AddKeyPerFile(directoryPath, optional: false, reloadOnChange: false);
+
+ ///
+ /// Adds configuration using files from a directory. File names are used as the key,
+ /// file contents are used as the value.
+ ///
+ /// The to add to.
+ /// The path to the directory.
+ /// Whether the directory is optional.
+ /// The .
+ public static IConfigurationBuilder AddKeyPerFile(this IConfigurationBuilder builder, string directoryPath, bool optional)
+ => builder.AddKeyPerFile(directoryPath, optional, reloadOnChange: false);
+
+ ///
+ /// Adds configuration using files from a directory. File names are used as the key,
+ /// file contents are used as the value.
+ ///
+ /// The to add to.
+ /// The path to the directory.
+ /// Whether the directory is optional.
+ /// Whether the configuration should be reloaded if the files are changed, added or removed.
+ /// The .
+ public static IConfigurationBuilder AddKeyPerFile(this IConfigurationBuilder builder, string directoryPath, bool optional, bool reloadOnChange)
+ => builder.AddKeyPerFile(source =>
+ {
+ // Only try to set the file provider if its not optional or the directory exists
+ if (!optional || Directory.Exists(directoryPath))
+ {
+ source.FileProvider = new PhysicalFileProvider(directoryPath);
+ }
+ source.Optional = optional;
+ source.ReloadOnChange = reloadOnChange;
+ });
+
+ ///
+ /// Adds configuration using files from a directory. File names are used as the key,
+ /// file contents are used as the value.
+ ///
+ /// The to add to.
+ /// Configures the source.
+ /// The .
+ public static IConfigurationBuilder AddKeyPerFile(this IConfigurationBuilder builder, Action configureSource)
+ => builder.Add(configureSource);
+ }
+}
diff --git a/src/Configuration.KeyPerFile/src/KeyPerFileConfigurationProvider.cs b/src/Configuration.KeyPerFile/src/KeyPerFileConfigurationProvider.cs
new file mode 100644
index 000000000000..f586896fc275
--- /dev/null
+++ b/src/Configuration.KeyPerFile/src/KeyPerFileConfigurationProvider.cs
@@ -0,0 +1,119 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Threading;
+using Microsoft.Extensions.Primitives;
+
+namespace Microsoft.Extensions.Configuration.KeyPerFile
+{
+ ///
+ /// A that uses a directory's files as configuration key/values.
+ ///
+ public class KeyPerFileConfigurationProvider : ConfigurationProvider, IDisposable
+ {
+ private readonly IDisposable _changeTokenRegistration;
+
+ KeyPerFileConfigurationSource Source { get; set; }
+
+ ///
+ /// Initializes a new instance.
+ ///
+ /// The settings.
+ public KeyPerFileConfigurationProvider(KeyPerFileConfigurationSource source)
+ {
+ Source = source ?? throw new ArgumentNullException(nameof(source));
+
+ if (Source.ReloadOnChange && Source.FileProvider != null)
+ {
+ _changeTokenRegistration = ChangeToken.OnChange(
+ () => Source.FileProvider.Watch("*"),
+ () =>
+ {
+ Thread.Sleep(Source.ReloadDelay);
+ Load(reload: true);
+ });
+ }
+
+ }
+
+ private static string NormalizeKey(string key)
+ => key.Replace("__", ConfigurationPath.KeyDelimiter);
+
+ private static string TrimNewLine(string value)
+ => value.EndsWith(Environment.NewLine)
+ ? value.Substring(0, value.Length - Environment.NewLine.Length)
+ : value;
+
+ ///
+ /// Loads the configuration values.
+ ///
+ public override void Load()
+ {
+ Load(reload: false);
+ }
+
+ private void Load(bool reload)
+ {
+ var data = new Dictionary(StringComparer.OrdinalIgnoreCase);
+
+ if (Source.FileProvider == null)
+ {
+ if (Source.Optional || reload) // Always optional on reload
+ {
+ Data = data;
+ return;
+ }
+
+ throw new DirectoryNotFoundException("A non-null file provider for the directory is required when this source is not optional.");
+ }
+
+ var directory = Source.FileProvider.GetDirectoryContents("/");
+ if (!directory.Exists)
+ {
+ if (Source.Optional || reload) // Always optional on reload
+ {
+ Data = data;
+ return;
+ }
+ throw new DirectoryNotFoundException("The root directory for the FileProvider doesn't exist and is not optional.");
+ }
+ else
+ {
+ foreach (var file in directory)
+ {
+ if (file.IsDirectory)
+ {
+ continue;
+ }
+
+ using var stream = file.CreateReadStream();
+ using var streamReader = new StreamReader(stream);
+
+ if (Source.IgnoreCondition == null || !Source.IgnoreCondition(file.Name))
+ {
+ data.Add(NormalizeKey(file.Name), TrimNewLine(streamReader.ReadToEnd()));
+ }
+
+ }
+ }
+
+ Data = data;
+ }
+
+ 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")})";
+
+ ///
+ public void Dispose()
+ {
+ _changeTokenRegistration?.Dispose();
+ }
+ }
+}
diff --git a/src/Configuration.KeyPerFile/src/KeyPerFileConfigurationSource.cs b/src/Configuration.KeyPerFile/src/KeyPerFileConfigurationSource.cs
new file mode 100644
index 000000000000..2b64d5a8dda7
--- /dev/null
+++ b/src/Configuration.KeyPerFile/src/KeyPerFileConfigurationSource.cs
@@ -0,0 +1,59 @@
+using System;
+using System.IO;
+using Microsoft.Extensions.FileProviders;
+
+namespace Microsoft.Extensions.Configuration.KeyPerFile
+{
+ ///
+ /// An used to configure .
+ ///
+ public class KeyPerFileConfigurationSource : IConfigurationSource
+ {
+ ///
+ /// Constructor;
+ ///
+ public KeyPerFileConfigurationSource()
+ => IgnoreCondition = s => IgnorePrefix != null && s.StartsWith(IgnorePrefix);
+
+ ///
+ /// The FileProvider whos root "/" directory files will be used as configuration data.
+ ///
+ public IFileProvider FileProvider { get; set; }
+
+ ///
+ /// Files that start with this prefix will be excluded.
+ /// Defaults to "ignore.".
+ ///
+ public string IgnorePrefix { get; set; } = "ignore.";
+
+ ///
+ /// Used to determine if a file should be ignored using its name.
+ /// Defaults to using the IgnorePrefix.
+ ///
+ public Func IgnoreCondition { get; set; }
+
+ ///
+ /// If false, will throw if the directory doesn't exist.
+ ///
+ public bool Optional { get; set; }
+
+ ///
+ /// Determines whether the source will be loaded if the underlying file changes.
+ ///
+ public bool ReloadOnChange { get; set; }
+
+ ///
+ /// Number of milliseconds that reload will wait before calling Load. This helps
+ /// avoid triggering reload before a file is completely written. Default is 250.
+ ///
+ public int ReloadDelay { get; set; } = 250;
+
+ ///
+ /// Builds the for this source.
+ ///
+ /// The .
+ /// A
+ public IConfigurationProvider Build(IConfigurationBuilder builder)
+ => new KeyPerFileConfigurationProvider(this);
+ }
+}
diff --git a/src/Configuration.KeyPerFile/src/Microsoft.Extensions.Configuration.KeyPerFile.csproj b/src/Configuration.KeyPerFile/src/Microsoft.Extensions.Configuration.KeyPerFile.csproj
new file mode 100644
index 000000000000..7f9c5e7eb193
--- /dev/null
+++ b/src/Configuration.KeyPerFile/src/Microsoft.Extensions.Configuration.KeyPerFile.csproj
@@ -0,0 +1,16 @@
+
+
+
+ Configuration provider that uses files in a directory for Microsoft.Extensions.Configuration.
+ netstandard2.0;$(DefaultNetCoreTargetFramework)
+ $(DefaultNetCoreTargetFramework)
+ true
+ true
+
+
+
+
+
+
+
+
diff --git a/src/Configuration.KeyPerFile/src/README.md b/src/Configuration.KeyPerFile/src/README.md
new file mode 100644
index 000000000000..29952e9139dc
--- /dev/null
+++ b/src/Configuration.KeyPerFile/src/README.md
@@ -0,0 +1,2 @@
+
+This is a configuration provider that uses a directory's files as data. A file's name is the key and the contents are the value.
diff --git a/src/Configuration.KeyPerFile/test/ConfigurationProviderCommandLineTest.cs b/src/Configuration.KeyPerFile/test/ConfigurationProviderCommandLineTest.cs
new file mode 100644
index 000000000000..066aecf33756
--- /dev/null
+++ b/src/Configuration.KeyPerFile/test/ConfigurationProviderCommandLineTest.cs
@@ -0,0 +1,43 @@
+// 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;
+using System.Linq;
+using Microsoft.Extensions.Configuration.Test;
+using Microsoft.Extensions.FileProviders;
+
+namespace Microsoft.Extensions.Configuration.KeyPerFile.Test
+{
+ public class ConfigurationProviderCommandLineTest : ConfigurationProviderTestBase
+ {
+ protected override (IConfigurationProvider Provider, Action Initializer) LoadThroughProvider(
+ TestSection testConfig)
+ {
+ var testFiles = new List();
+ SectionToTestFiles(testFiles, "", testConfig);
+
+ var provider = new KeyPerFileConfigurationProvider(
+ new KeyPerFileConfigurationSource
+ {
+ Optional = true,
+ FileProvider = new TestFileProvider(testFiles.ToArray())
+ });
+
+ return (provider, () => { });
+ }
+
+ private void SectionToTestFiles(List testFiles, string sectionName, TestSection section)
+ {
+ foreach (var tuple in section.Values.SelectMany(e => e.Value.Expand(e.Key)))
+ {
+ testFiles.Add(new TestFile(sectionName + tuple.Key, tuple.Value));
+ }
+
+ foreach (var tuple in section.Sections)
+ {
+ SectionToTestFiles(testFiles, sectionName + tuple.Key + "__", tuple.Section);
+ }
+ }
+ }
+}
diff --git a/src/Configuration.KeyPerFile/test/ConfigurationProviderTestBase.cs b/src/Configuration.KeyPerFile/test/ConfigurationProviderTestBase.cs
new file mode 100644
index 000000000000..4609ee2560ff
--- /dev/null
+++ b/src/Configuration.KeyPerFile/test/ConfigurationProviderTestBase.cs
@@ -0,0 +1,761 @@
+// 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;
+using System.Linq;
+using Microsoft.Extensions.Configuration.Memory;
+using Xunit;
+
+namespace Microsoft.Extensions.Configuration.Test
+{
+ public abstract class ConfigurationProviderTestBase
+ {
+ [Fact]
+ public virtual void Load_from_single_provider()
+ {
+ 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]
+ public virtual void Combine_after_other_provider()
+ {
+ AssertConfig(
+ BuildConfigRoot(
+ LoadUsingMemoryProvider(TestSection.MissingSection2ValuesConfig),
+ LoadThroughProvider(TestSection.MissingSection4Config)));
+
+ AssertConfig(
+ BuildConfigRoot(
+ LoadUsingMemoryProvider(TestSection.MissingSection4Config),
+ LoadThroughProvider(TestSection.MissingSection2ValuesConfig)));
+ }
+
+ [Fact]
+ public virtual void Combine_before_other_provider()
+ {
+ AssertConfig(
+ BuildConfigRoot(
+ LoadThroughProvider(TestSection.MissingSection2ValuesConfig),
+ LoadUsingMemoryProvider(TestSection.MissingSection4Config)));
+
+ AssertConfig(
+ BuildConfigRoot(
+ LoadThroughProvider(TestSection.MissingSection4Config),
+ LoadUsingMemoryProvider(TestSection.MissingSection2ValuesConfig)));
+ }
+
+ [Fact]
+ public virtual void Second_provider_overrides_values_from_first()
+ {
+ AssertConfig(
+ BuildConfigRoot(
+ LoadUsingMemoryProvider(TestSection.NoValuesTestConfig),
+ LoadThroughProvider(TestSection.TestConfig)));
+ }
+
+ [Fact]
+ public virtual void Combining_from_multiple_providers_is_case_insensitive()
+ {
+ AssertConfig(
+ BuildConfigRoot(
+ LoadUsingMemoryProvider(TestSection.DifferentCasedTestConfig),
+ LoadThroughProvider(TestSection.TestConfig)));
+ }
+
+ [Fact]
+ public virtual void Load_from_single_provider_with_duplicates_throws()
+ {
+ AssertFormatOrArgumentException(
+ () => BuildConfigRoot(LoadThroughProvider(TestSection.DuplicatesTestConfig)));
+ }
+
+ [Fact]
+ public virtual void Load_from_single_provider_with_differing_case_duplicates_throws()
+ {
+ AssertFormatOrArgumentException(
+ () => BuildConfigRoot(LoadThroughProvider(TestSection.DuplicatesDifferentCaseTestConfig)));
+ }
+
+ private void AssertFormatOrArgumentException(Action test)
+ {
+ Exception caught = null;
+ try
+ {
+ test();
+ }
+ catch (Exception e)
+ {
+ caught = e;
+ }
+
+ Assert.True(caught is ArgumentException
+ || caught is FormatException);
+ }
+
+ [Fact]
+ public virtual void Bind_to_object()
+ {
+ var configuration = BuildConfigRoot(LoadThroughProvider(TestSection.TestConfig));
+
+ var options = configuration.Get();
+
+ Assert.Equal("Value1", options.Key1);
+ Assert.Equal("Value12", options.Section1.Key2);
+ Assert.Equal("Value123", options.Section1.Section2.Key3);
+ Assert.Equal("Value344", options.Section3.Section4.Key4);
+ Assert.Equal(new[] { "ArrayValue0", "ArrayValue1", "ArrayValue2" }, options.Section1.Section2.Key3a);
+ }
+
+ public class AsOptions
+ {
+ public string Key1 { get; set; }
+
+ public Section1AsOptions Section1 { get; set; }
+ public Section3AsOptions Section3 { get; set; }
+ }
+
+ public class Section1AsOptions
+ {
+ public string Key2 { get; set; }
+
+ public Section2AsOptions Section2 { get; set; }
+ }
+
+ public class Section2AsOptions
+ {
+ public string Key3 { get; set; }
+ public string[] Key3a { get; set; }
+ }
+
+ public class Section3AsOptions
+ {
+ public Section4AsOptions Section4 { get; set; }
+ }
+
+ public class Section4AsOptions
+ {
+ public string Key4 { get; set; }
+ }
+
+ 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)
+ {
+ 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("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("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("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("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("Section3:Section4", section4.Path, StringComparer.InvariantCultureIgnoreCase);
+ Assert.Null(section4.Value);
+
+ var sections = config.GetChildren().ToList();
+
+ Assert.Equal(3, sections.Count);
+
+ 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("Section1", sections[1].Key, StringComparer.InvariantCultureIgnoreCase);
+ Assert.Equal("Section1", sections[1].Path, StringComparer.InvariantCultureIgnoreCase);
+ Assert.Null(sections[1].Value);
+
+ Assert.Equal("Section3", sections[2].Key, StringComparer.InvariantCultureIgnoreCase);
+ Assert.Equal("Section3", sections[2].Path, StringComparer.InvariantCultureIgnoreCase);
+ Assert.Null(sections[2].Value);
+
+ sections = section1.GetChildren().ToList();
+
+ Assert.Equal(2, sections.Count);
+
+ 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("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);
+
+ protected virtual IConfigurationRoot BuildConfigRoot(
+ params (IConfigurationProvider Provider, Action Initializer)[] providers)
+ {
+ var root = new ConfigurationRoot(providers.Select(e => e.Provider).ToList());
+
+ foreach (var initializer in providers.Select(e => e.Initializer))
+ {
+ initializer();
+ }
+
+ return root;
+ }
+
+ protected static (IConfigurationProvider Provider, Action Initializer) LoadUsingMemoryProvider(TestSection testConfig)
+ {
+ var values = new List>();
+ SectionToValues(testConfig, "", values);
+
+ return (new MemoryConfigurationProvider(
+ new MemoryConfigurationSource
+ {
+ InitialData = values
+ }),
+ () => { });
+ }
+
+ protected static void SectionToValues(
+ TestSection section,
+ string sectionName,
+ IList> values)
+ {
+ foreach (var tuple in section.Values.SelectMany(e => e.Value.Expand(e.Key)))
+ {
+ values.Add(new KeyValuePair(sectionName + tuple.Key, tuple.Value));
+ }
+
+ foreach (var tuple in section.Sections)
+ {
+ SectionToValues(
+ tuple.Section,
+ sectionName + tuple.Key + ":",
+ values);
+ }
+ }
+
+ protected class TestKeyValue
+ {
+ public object Value { get; }
+
+ public TestKeyValue(string value)
+ {
+ Value = value;
+ }
+
+ public TestKeyValue(string[] values)
+ {
+ Value = values;
+ }
+
+ public static implicit operator TestKeyValue(string value) => new TestKeyValue(value);
+ public static implicit operator TestKeyValue(string[] values) => new TestKeyValue(values);
+
+ public string[] AsArray => Value as string[];
+
+ public string AsString => Value as string;
+
+ public IEnumerable<(string Key, string Value)> Expand(string key)
+ {
+ if (AsArray == null)
+ {
+ yield return (key, AsString);
+ }
+ else
+ {
+ for (var i = 0; i < AsArray.Length; i++)
+ {
+ yield return ($"{key}:{i}", AsArray[i]);
+ }
+ }
+ }
+ }
+
+ protected class TestSection
+ {
+ public IEnumerable<(string Key, TestKeyValue Value)> Values { get; set; }
+ = Enumerable.Empty<(string, TestKeyValue)>();
+
+ public IEnumerable<(string Key, TestSection Section)> Sections { get; set; }
+ = Enumerable.Empty<(string, TestSection)>();
+
+ public static TestSection TestConfig { get; }
+ = new TestSection
+ {
+ Values = new[] { ("Key1", (TestKeyValue)"Value1") },
+ Sections = new[]
+ {
+ ("Section1", new TestSection
+ {
+ Values = new[] {("Key2", (TestKeyValue)"Value12")},
+ Sections = new[]
+ {
+ ("Section2", new TestSection
+ {
+ Values = new[]
+ {
+ ("Key3", (TestKeyValue)"Value123"),
+ ("Key3a", (TestKeyValue)new[] {"ArrayValue0", "ArrayValue1", "ArrayValue2"})
+ },
+ })
+ }
+ }),
+ ("Section3", new TestSection
+ {
+ Sections = new[]
+ {
+ ("Section4", new TestSection
+ {
+ Values = new[] {("Key4", (TestKeyValue)"Value344")}
+ })
+ }
+ })
+ }
+ };
+
+ public static TestSection NoValuesTestConfig { get; }
+ = new TestSection
+ {
+ Values = new[] { ("Key1", (TestKeyValue)"------") },
+ Sections = new[]
+ {
+ ("Section1", new TestSection
+ {
+ Values = new[] {("Key2", (TestKeyValue)"-------")},
+ Sections = new[]
+ {
+ ("Section2", new TestSection
+ {
+ Values = new[]
+ {
+ ("Key3", (TestKeyValue)"-----"),
+ ("Key3a", (TestKeyValue)new[] {"-----------", "-----------", "-----------"})
+ },
+ })
+ }
+ }),
+ ("Section3", new TestSection
+ {
+ Sections = new[]
+ {
+ ("Section4", new TestSection
+ {
+ Values = new[] {("Key4", (TestKeyValue)"--------")}
+ })
+ }
+ })
+ }
+ };
+
+ public static TestSection MissingSection2ValuesConfig { get; }
+ = new TestSection
+ {
+ Values = new[] { ("Key1", (TestKeyValue)"Value1") },
+ Sections = new[]
+ {
+ ("Section1", new TestSection
+ {
+ Values = new[] {("Key2", (TestKeyValue)"Value12")},
+ Sections = new[]
+ {
+ ("Section2", new TestSection
+ {
+ Values = new[]
+ {
+ ("Key3a", (TestKeyValue)new[] {"ArrayValue0"})
+ },
+ })
+ }
+ }),
+ ("Section3", new TestSection
+ {
+ Sections = new[]
+ {
+ ("Section4", new TestSection
+ {
+ Values = new[] {("Key4", (TestKeyValue)"Value344")}
+ })
+ }
+ })
+ }
+ };
+
+
+ public static TestSection MissingSection4Config { get; }
+ = new TestSection
+ {
+ Values = new[] { ("Key1", (TestKeyValue)"Value1") },
+ Sections = new[]
+ {
+ ("Section1", new TestSection
+ {
+ Values = new[] {("Key2", (TestKeyValue)"Value12")},
+ Sections = new[]
+ {
+ ("Section2", new TestSection
+ {
+ Values = new[]
+ {
+ ("Key3", (TestKeyValue)"Value123"),
+ ("Key3a", (TestKeyValue)new[] {"ArrayValue0", "ArrayValue1", "ArrayValue2"})
+ },
+ })
+ }
+ }),
+ ("Section3", new TestSection())
+ }
+ };
+
+ public static TestSection DifferentCasedTestConfig { get; }
+ = new TestSection
+ {
+ Values = new[] { ("KeY1", (TestKeyValue)"Value1") },
+ Sections = new[]
+ {
+ ("SectioN1", new TestSection
+ {
+ Values = new[] {("KeY2", (TestKeyValue)"Value12")},
+ Sections = new[]
+ {
+ ("SectioN2", new TestSection
+ {
+ Values = new[]
+ {
+ ("KeY3", (TestKeyValue)"Value123"),
+ ("KeY3a", (TestKeyValue)new[] {"ArrayValue0", "ArrayValue1", "ArrayValue2"})
+ },
+ })
+ }
+ }),
+ ("SectioN3", new TestSection
+ {
+ Sections = new[]
+ {
+ ("SectioN4", new TestSection
+ {
+ Values = new[] {("KeY4", (TestKeyValue)"Value344")}
+ })
+ }
+ })
+ }
+ };
+
+ public static TestSection DuplicatesTestConfig { get; }
+ = new TestSection
+ {
+ Values = new[]
+ {
+ ("Key1", (TestKeyValue)"Value1"),
+ ("Key1", (TestKeyValue)"Value1")
+ },
+ Sections = new[]
+ {
+ ("Section1", new TestSection
+ {
+ Values = new[] {("Key2", (TestKeyValue)"Value12")},
+ Sections = new[]
+ {
+ ("Section2", new TestSection
+ {
+ Values = new[]
+ {
+ ("Key3", (TestKeyValue)"Value123"),
+ ("Key3a", (TestKeyValue)new[] {"ArrayValue0", "ArrayValue1", "ArrayValue2"})
+ },
+ }),
+ ("Section2", new TestSection
+ {
+ Values = new[]
+ {
+ ("Key3", (TestKeyValue)"Value123"),
+ ("Key3a", (TestKeyValue)new[] {"ArrayValue0", "ArrayValue1", "ArrayValue2"})
+ },
+ })
+
+ }
+ }),
+ ("Section3", new TestSection
+ {
+ Sections = new[]
+ {
+ ("Section4", new TestSection
+ {
+ Values = new[] {("Key4", (TestKeyValue)"Value344")}
+ })
+ }
+ })
+ }
+ };
+
+ public static TestSection DuplicatesDifferentCaseTestConfig { get; }
+ = new TestSection
+ {
+ Values = new[]
+ {
+ ("Key1", (TestKeyValue)"Value1"),
+ ("KeY1", (TestKeyValue)"Value1")
+ },
+ Sections = new[]
+ {
+ ("Section1", new TestSection
+ {
+ Values = new[] {("Key2", (TestKeyValue)"Value12")},
+ Sections = new[]
+ {
+ ("Section2", new TestSection
+ {
+ Values = new[]
+ {
+ ("Key3", (TestKeyValue)"Value123"),
+ ("Key3a", (TestKeyValue)new[] {"ArrayValue0", "ArrayValue1", "ArrayValue2"})
+ },
+ }),
+ ("SectioN2", new TestSection
+ {
+ Values = new[]
+ {
+ ("KeY3", (TestKeyValue)"Value123"),
+ ("KeY3a", (TestKeyValue)new[] {"ArrayValue0", "ArrayValue1", "ArrayValue2"})
+ },
+ })
+
+ }
+ }),
+ ("Section3", new TestSection
+ {
+ Sections = new[]
+ {
+ ("Section4", new TestSection
+ {
+ 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")}
+ })
+ }
+ })
+ }
+ };
+ }
+ }
+}
diff --git a/src/Configuration.KeyPerFile/test/KeyPerFileTests.cs b/src/Configuration.KeyPerFile/test/KeyPerFileTests.cs
new file mode 100644
index 000000000000..794f7abf23fc
--- /dev/null
+++ b/src/Configuration.KeyPerFile/test/KeyPerFileTests.cs
@@ -0,0 +1,416 @@
+// 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;
+using System.Collections.Generic;
+using System.IO;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Extensions.FileProviders;
+using Microsoft.Extensions.Primitives;
+using Xunit;
+
+namespace Microsoft.Extensions.Configuration.KeyPerFile.Test
+{
+ public class KeyPerFileTests
+ {
+ [Fact]
+ public void DoesNotThrowWhenOptionalAndNoSecrets()
+ {
+ new ConfigurationBuilder().AddKeyPerFile(o => o.Optional = true).Build();
+ }
+
+ [Fact]
+ public void DoesNotThrowWhenOptionalAndDirectoryDoesntExist()
+ {
+ new ConfigurationBuilder().AddKeyPerFile("nonexistent", true).Build();
+ }
+
+ [Fact]
+ public void ThrowsWhenNotOptionalAndDirectoryDoesntExist()
+ {
+ var e = Assert.Throws(() => new ConfigurationBuilder().AddKeyPerFile("nonexistent", false).Build());
+ Assert.Contains("The path must be absolute.", e.Message);
+ }
+
+ [Fact]
+ public void CanLoadMultipleSecrets()
+ {
+ var testFileProvider = new TestFileProvider(
+ new TestFile("Secret1", "SecretValue1"),
+ new TestFile("Secret2", "SecretValue2"));
+
+ var config = new ConfigurationBuilder()
+ .AddKeyPerFile(o => o.FileProvider = testFileProvider)
+ .Build();
+
+ Assert.Equal("SecretValue1", config["Secret1"]);
+ Assert.Equal("SecretValue2", config["Secret2"]);
+ }
+
+ [Fact]
+ public void CanLoadMultipleSecretsWithDirectory()
+ {
+ var testFileProvider = new TestFileProvider(
+ new TestFile("Secret1", "SecretValue1"),
+ new TestFile("Secret2", "SecretValue2"),
+ new TestFile("directory"));
+
+ var config = new ConfigurationBuilder()
+ .AddKeyPerFile(o => o.FileProvider = testFileProvider)
+ .Build();
+
+ Assert.Equal("SecretValue1", config["Secret1"]);
+ Assert.Equal("SecretValue2", config["Secret2"]);
+ }
+
+ [Fact]
+ public void CanLoadNestedKeys()
+ {
+ var testFileProvider = new TestFileProvider(
+ new TestFile("Secret0__Secret1__Secret2__Key", "SecretValue2"),
+ new TestFile("Secret0__Secret1__Key", "SecretValue1"),
+ new TestFile("Secret0__Key", "SecretValue0"));
+
+ var config = new ConfigurationBuilder()
+ .AddKeyPerFile(o => o.FileProvider = testFileProvider)
+ .Build();
+
+ Assert.Equal("SecretValue0", config["Secret0:Key"]);
+ Assert.Equal("SecretValue1", config["Secret0:Secret1:Key"]);
+ Assert.Equal("SecretValue2", config["Secret0:Secret1:Secret2:Key"]);
+ }
+
+ [Fact]
+ public void CanIgnoreFilesWithDefault()
+ {
+ var testFileProvider = new TestFileProvider(
+ new TestFile("ignore.Secret0", "SecretValue0"),
+ new TestFile("ignore.Secret1", "SecretValue1"),
+ new TestFile("Secret2", "SecretValue2"));
+
+ var config = new ConfigurationBuilder()
+ .AddKeyPerFile(o => o.FileProvider = testFileProvider)
+ .Build();
+
+ Assert.Null(config["ignore.Secret0"]);
+ Assert.Null(config["ignore.Secret1"]);
+ Assert.Equal("SecretValue2", config["Secret2"]);
+ }
+
+ [Fact]
+ public void CanTurnOffDefaultIgnorePrefixWithCondition()
+ {
+ var testFileProvider = new TestFileProvider(
+ new TestFile("ignore.Secret0", "SecretValue0"),
+ new TestFile("ignore.Secret1", "SecretValue1"),
+ new TestFile("Secret2", "SecretValue2"));
+
+ var config = new ConfigurationBuilder()
+ .AddKeyPerFile(o =>
+ {
+ o.FileProvider = testFileProvider;
+ o.IgnoreCondition = null;
+ })
+ .Build();
+
+ Assert.Equal("SecretValue0", config["ignore.Secret0"]);
+ Assert.Equal("SecretValue1", config["ignore.Secret1"]);
+ Assert.Equal("SecretValue2", config["Secret2"]);
+ }
+
+ [Fact]
+ public void CanIgnoreAllWithCondition()
+ {
+ var testFileProvider = new TestFileProvider(
+ new TestFile("Secret0", "SecretValue0"),
+ new TestFile("Secret1", "SecretValue1"),
+ new TestFile("Secret2", "SecretValue2"));
+
+ var config = new ConfigurationBuilder()
+ .AddKeyPerFile(o =>
+ {
+ o.FileProvider = testFileProvider;
+ o.IgnoreCondition = s => true;
+ })
+ .Build();
+
+ Assert.Empty(config.AsEnumerable());
+ }
+
+ [Fact]
+ public void CanIgnoreFilesWithCustomIgnore()
+ {
+ var testFileProvider = new TestFileProvider(
+ new TestFile("meSecret0", "SecretValue0"),
+ new TestFile("meSecret1", "SecretValue1"),
+ new TestFile("Secret2", "SecretValue2"));
+
+ var config = new ConfigurationBuilder()
+ .AddKeyPerFile(o =>
+ {
+ o.FileProvider = testFileProvider;
+ o.IgnorePrefix = "me";
+ })
+ .Build();
+
+ Assert.Null(config["meSecret0"]);
+ Assert.Null(config["meSecret1"]);
+ Assert.Equal("SecretValue2", config["Secret2"]);
+ }
+
+ [Fact]
+ public void CanUnIgnoreDefaultFiles()
+ {
+ var testFileProvider = new TestFileProvider(
+ new TestFile("ignore.Secret0", "SecretValue0"),
+ new TestFile("ignore.Secret1", "SecretValue1"),
+ new TestFile("Secret2", "SecretValue2"));
+
+ var config = new ConfigurationBuilder()
+ .AddKeyPerFile(o =>
+ {
+ o.FileProvider = testFileProvider;
+ o.IgnorePrefix = null;
+ })
+ .Build();
+
+ Assert.Equal("SecretValue0", config["ignore.Secret0"]);
+ Assert.Equal("SecretValue1", config["ignore.Secret1"]);
+ Assert.Equal("SecretValue2", config["Secret2"]);
+ }
+
+ [Fact]
+ public void BindingDoesNotThrowIfReloadedDuringBinding()
+ {
+ var testFileProvider = new TestFileProvider(
+ new TestFile("Number", "-2"),
+ new TestFile("Text", "Foo"));
+
+ var config = new ConfigurationBuilder()
+ .AddKeyPerFile(o => o.FileProvider = testFileProvider)
+ .Build();
+
+ MyOptions options = null;
+
+ using (var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(250)))
+ {
+ void ReloadLoop()
+ {
+ while (!cts.IsCancellationRequested)
+ {
+ config.Reload();
+ }
+ }
+
+ _ = Task.Run(ReloadLoop);
+
+ while (!cts.IsCancellationRequested)
+ {
+ options = config.Get();
+ }
+ }
+
+ Assert.Equal(-2, options.Number);
+ Assert.Equal("Foo", options.Text);
+ }
+
+ [Fact]
+ public void ReloadConfigWhenReloadOnChangeIsTrue()
+ {
+ var testFileProvider = new TestFileProvider(
+ new TestFile("Secret1", "SecretValue1"),
+ new TestFile("Secret2", "SecretValue2"));
+
+ var config = new ConfigurationBuilder()
+ .AddKeyPerFile(o =>
+ {
+ o.FileProvider = testFileProvider;
+ o.ReloadOnChange = true;
+ }).Build();
+
+ Assert.Equal("SecretValue1", config["Secret1"]);
+ Assert.Equal("SecretValue2", config["Secret2"]);
+
+ testFileProvider.ChangeFiles(
+ new TestFile("Secret1", "NewSecretValue1"),
+ new TestFile("Secret3", "NewSecretValue3"));
+
+ Assert.Equal("NewSecretValue1", config["Secret1"]);
+ Assert.Null(config["NewSecret2"]);
+ Assert.Equal("NewSecretValue3", config["Secret3"]);
+ }
+
+ [Fact]
+ public void SameConfigWhenReloadOnChangeIsFalse()
+ {
+ var testFileProvider = new TestFileProvider(
+ new TestFile("Secret1", "SecretValue1"),
+ new TestFile("Secret2", "SecretValue2"));
+
+ var config = new ConfigurationBuilder()
+ .AddKeyPerFile(o =>
+ {
+ o.FileProvider = testFileProvider;
+ o.ReloadOnChange = false;
+ }).Build();
+
+ Assert.Equal("SecretValue1", config["Secret1"]);
+ Assert.Equal("SecretValue2", config["Secret2"]);
+
+ testFileProvider.ChangeFiles(
+ new TestFile("Secret1", "NewSecretValue1"),
+ new TestFile("Secret3", "NewSecretValue3"));
+
+ Assert.Equal("SecretValue1", config["Secret1"]);
+ Assert.Equal("SecretValue2", config["Secret2"]);
+ }
+
+ [Fact]
+ public void NoFilesReloadWhenAddedFiles()
+ {
+ var testFileProvider = new TestFileProvider();
+
+ var config = new ConfigurationBuilder()
+ .AddKeyPerFile(o =>
+ {
+ o.FileProvider = testFileProvider;
+ o.ReloadOnChange = true;
+ }).Build();
+
+ Assert.Empty(config.AsEnumerable());
+
+ testFileProvider.ChangeFiles(
+ new TestFile("Secret1", "SecretValue1"),
+ new TestFile("Secret2", "SecretValue2"));
+
+ Assert.Equal("SecretValue1", config["Secret1"]);
+ Assert.Equal("SecretValue2", config["Secret2"]);
+ }
+
+ private sealed class MyOptions
+ {
+ public int Number { get; set; }
+ public string Text { get; set; }
+ }
+ }
+
+ class TestFileProvider : IFileProvider
+ {
+ IDirectoryContents _contents;
+ MockChangeToken _changeToken;
+
+ public TestFileProvider(params IFileInfo[] files)
+ {
+ _contents = new TestDirectoryContents(files);
+ _changeToken = new MockChangeToken();
+ }
+
+ public IDirectoryContents GetDirectoryContents(string subpath) => _contents;
+
+ public IFileInfo GetFileInfo(string subpath) => new TestFile("TestDirectory");
+
+ public IChangeToken Watch(string filter) => _changeToken;
+
+ internal void ChangeFiles(params IFileInfo[] files)
+ {
+ _contents = new TestDirectoryContents(files);
+ _changeToken.RaiseCallback();
+ }
+ }
+
+ class MockChangeToken : IChangeToken
+ {
+ private Action _callback;
+
+ public bool ActiveChangeCallbacks => true;
+
+ public bool HasChanged => true;
+
+ public IDisposable RegisterChangeCallback(Action