diff --git a/src/Configuration.KeyPerFile/Directory.Build.props b/src/Configuration.KeyPerFile/Directory.Build.props
new file mode 100644
index 0000000000..2082380096
--- /dev/null
+++ b/src/Configuration.KeyPerFile/Directory.Build.props
@@ -0,0 +1,8 @@
+
+
+
+
+ true
+ configuration
+
+
diff --git a/src/Configuration.KeyPerFile/src/KeyPerFileConfigurationBuilderExtensions.cs b/src/Configuration.KeyPerFile/src/KeyPerFileConfigurationBuilderExtensions.cs
new file mode 100644
index 0000000000..435ef9e155
--- /dev/null
+++ b/src/Configuration.KeyPerFile/src/KeyPerFileConfigurationBuilderExtensions.cs
@@ -0,0 +1,42 @@
+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.
+ /// Whether the directory is optional.
+ /// The .
+ public static IConfigurationBuilder AddKeyPerFile(this IConfigurationBuilder builder, string directoryPath, bool optional)
+ => 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;
+ });
+
+ ///
+ /// 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 0000000000..4748895744
--- /dev/null
+++ b/src/Configuration.KeyPerFile/src/KeyPerFileConfigurationProvider.cs
@@ -0,0 +1,72 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+
+namespace Microsoft.Extensions.Configuration.KeyPerFile
+{
+ ///
+ /// A that uses a directory's files as configuration key/values.
+ ///
+ public class KeyPerFileConfigurationProvider : ConfigurationProvider
+ {
+ KeyPerFileConfigurationSource Source { get; set; }
+
+ ///
+ /// Initializes a new instance.
+ ///
+ /// The settings.
+ public KeyPerFileConfigurationProvider(KeyPerFileConfigurationSource source)
+ => Source = source ?? throw new ArgumentNullException(nameof(source));
+
+ 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 docker secrets.
+ ///
+ public override void Load()
+ {
+ Data = new Dictionary(StringComparer.OrdinalIgnoreCase);
+
+ if (Source.FileProvider == null)
+ {
+ if (Source.Optional)
+ {
+ return;
+ }
+ else
+ {
+ 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 && !Source.Optional)
+ {
+ throw new DirectoryNotFoundException("The root directory for the FileProvider doesn't exist and is not optional.");
+ }
+
+ 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()));
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/src/Configuration.KeyPerFile/src/KeyPerFileConfigurationSource.cs b/src/Configuration.KeyPerFile/src/KeyPerFileConfigurationSource.cs
new file mode 100644
index 0000000000..c32e948e82
--- /dev/null
+++ b/src/Configuration.KeyPerFile/src/KeyPerFileConfigurationSource.cs
@@ -0,0 +1,48 @@
+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; }
+
+ ///
+ /// 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 0000000000..4eb19f3293
--- /dev/null
+++ b/src/Configuration.KeyPerFile/src/Microsoft.Extensions.Configuration.KeyPerFile.csproj
@@ -0,0 +1,14 @@
+
+
+
+ Configuration provider that uses files in a directory for Microsoft.Extensions.Configuration.
+ netstandard2.0
+ false
+
+
+
+
+
+
+
+
diff --git a/src/Configuration.KeyPerFile/src/README.md b/src/Configuration.KeyPerFile/src/README.md
new file mode 100644
index 0000000000..29952e9139
--- /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/KeyPerFileTests.cs b/src/Configuration.KeyPerFile/test/KeyPerFileTests.cs
new file mode 100644
index 0000000000..d55387a404
--- /dev/null
+++ b/src/Configuration.KeyPerFile/test/KeyPerFileTests.cs
@@ -0,0 +1,308 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+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 directory name", 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"]);
+ }
+ }
+
+ class TestFileProvider : IFileProvider
+ {
+ IDirectoryContents _contents;
+
+ public TestFileProvider(params IFileInfo[] files)
+ {
+ _contents = new TestDirectoryContents(files);
+ }
+
+ public IDirectoryContents GetDirectoryContents(string subpath)
+ {
+ return _contents;
+ }
+
+ public IFileInfo GetFileInfo(string subpath)
+ {
+ throw new NotImplementedException();
+ }
+
+ public IChangeToken Watch(string filter)
+ {
+ throw new NotImplementedException();
+ }
+ }
+
+ class TestDirectoryContents : IDirectoryContents
+ {
+ List _list;
+
+ public TestDirectoryContents(params IFileInfo[] files)
+ {
+ _list = new List(files);
+ }
+
+ public bool Exists
+ {
+ get
+ {
+ return true;
+ }
+ }
+
+ public IEnumerator GetEnumerator()
+ {
+ return _list.GetEnumerator();
+ }
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return GetEnumerator();
+ }
+ }
+
+ //TODO: Probably need a directory and file type.
+ class TestFile : IFileInfo
+ {
+ private string _name;
+ private string _contents;
+
+ public bool Exists
+ {
+ get
+ {
+ return true;
+ }
+ }
+
+ public bool IsDirectory
+ {
+ get;
+ }
+
+ public DateTimeOffset LastModified
+ {
+ get
+ {
+ throw new NotImplementedException();
+ }
+ }
+
+ public long Length
+ {
+ get
+ {
+ throw new NotImplementedException();
+ }
+ }
+
+ public string Name
+ {
+ get
+ {
+ return _name;
+ }
+ }
+
+ public string PhysicalPath
+ {
+ get
+ {
+ throw new NotImplementedException();
+ }
+ }
+
+ public TestFile(string name)
+ {
+ _name = name;
+ IsDirectory = true;
+ }
+
+ public TestFile(string name, string contents)
+ {
+ _name = name;
+ _contents = contents;
+ }
+
+ public Stream CreateReadStream()
+ {
+ if(IsDirectory)
+ {
+ throw new InvalidOperationException("Cannot create stream from directory");
+ }
+
+ return new MemoryStream(Encoding.UTF8.GetBytes(_contents));
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Configuration.KeyPerFile/test/Microsoft.Extensions.Configuration.KeyPerFile.Tests.csproj b/src/Configuration.KeyPerFile/test/Microsoft.Extensions.Configuration.KeyPerFile.Tests.csproj
new file mode 100644
index 0000000000..634056a345
--- /dev/null
+++ b/src/Configuration.KeyPerFile/test/Microsoft.Extensions.Configuration.KeyPerFile.Tests.csproj
@@ -0,0 +1,11 @@
+
+
+
+ $(StandardTestTfms)
+
+
+
+
+
+
+