Merge source code from aspnet/Configuration into this repo
\n\nCommit migrated from 8a9b64a3e5
This commit is contained in:
commit
8d17a5b9f3
|
|
@ -0,0 +1,8 @@
|
||||||
|
<Project>
|
||||||
|
<Import Project="$([MSBuild]::GetDirectoryNameOfFileAbove($(MSBuildThisFileDirectory)..\, Directory.Build.props))\Directory.Build.props" />
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<GenerateDocumentationFile Condition=" '$(IsTestProject)' != 'true' AND '$(IsSampleProject)' != 'true' ">true</GenerateDocumentationFile>
|
||||||
|
<PackageTags>configuration</PackageTags>
|
||||||
|
</PropertyGroup>
|
||||||
|
</Project>
|
||||||
|
|
@ -0,0 +1,42 @@
|
||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using Microsoft.Extensions.Configuration.KeyPerFile;
|
||||||
|
using Microsoft.Extensions.FileProviders;
|
||||||
|
|
||||||
|
namespace Microsoft.Extensions.Configuration
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Extension methods for registering <see cref="KeyPerFileConfigurationProvider"/> with <see cref="IConfigurationBuilder"/>.
|
||||||
|
/// </summary>
|
||||||
|
public static class KeyPerFileConfigurationBuilderExtensions
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Adds configuration using files from a directory. File names are used as the key,
|
||||||
|
/// file contents are used as the value.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="builder">The <see cref="IConfigurationBuilder"/> to add to.</param>
|
||||||
|
/// <param name="directoryPath">The path to the directory.</param>
|
||||||
|
/// <param name="optional">Whether the directory is optional.</param>
|
||||||
|
/// <returns>The <see cref="IConfigurationBuilder"/>.</returns>
|
||||||
|
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;
|
||||||
|
});
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Adds configuration using files from a directory. File names are used as the key,
|
||||||
|
/// file contents are used as the value.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="builder">The <see cref="IConfigurationBuilder"/> to add to.</param>
|
||||||
|
/// <param name="configureSource">Configures the source.</param>
|
||||||
|
/// <returns>The <see cref="IConfigurationBuilder"/>.</returns>
|
||||||
|
public static IConfigurationBuilder AddKeyPerFile(this IConfigurationBuilder builder, Action<KeyPerFileConfigurationSource> configureSource)
|
||||||
|
=> builder.Add(configureSource);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,72 @@
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
|
||||||
|
namespace Microsoft.Extensions.Configuration.KeyPerFile
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// A <see cref="ConfigurationProvider"/> that uses a directory's files as configuration key/values.
|
||||||
|
/// </summary>
|
||||||
|
public class KeyPerFileConfigurationProvider : ConfigurationProvider
|
||||||
|
{
|
||||||
|
KeyPerFileConfigurationSource Source { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="source">The settings.</param>
|
||||||
|
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;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Loads the docker secrets.
|
||||||
|
/// </summary>
|
||||||
|
public override void Load()
|
||||||
|
{
|
||||||
|
Data = new Dictionary<string, string>(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()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,48 @@
|
||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using Microsoft.Extensions.FileProviders;
|
||||||
|
|
||||||
|
namespace Microsoft.Extensions.Configuration.KeyPerFile
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// An <see cref="IConfigurationSource"/> used to configure <see cref="KeyPerFileConfigurationProvider"/>.
|
||||||
|
/// </summary>
|
||||||
|
public class KeyPerFileConfigurationSource : IConfigurationSource
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Constructor;
|
||||||
|
/// </summary>
|
||||||
|
public KeyPerFileConfigurationSource()
|
||||||
|
=> IgnoreCondition = s => IgnorePrefix != null && s.StartsWith(IgnorePrefix);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The FileProvider whos root "/" directory files will be used as configuration data.
|
||||||
|
/// </summary>
|
||||||
|
public IFileProvider FileProvider { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Files that start with this prefix will be excluded.
|
||||||
|
/// Defaults to "ignore.".
|
||||||
|
/// </summary>
|
||||||
|
public string IgnorePrefix { get; set; } = "ignore.";
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Used to determine if a file should be ignored using its name.
|
||||||
|
/// Defaults to using the IgnorePrefix.
|
||||||
|
/// </summary>
|
||||||
|
public Func<string, bool> IgnoreCondition { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// If false, will throw if the directory doesn't exist.
|
||||||
|
/// </summary>
|
||||||
|
public bool Optional { get; set; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Builds the <see cref="KeyPerFileConfigurationProvider"/> for this source.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="builder">The <see cref="IConfigurationBuilder"/>.</param>
|
||||||
|
/// <returns>A <see cref="KeyPerFileConfigurationProvider"/></returns>
|
||||||
|
public IConfigurationProvider Build(IConfigurationBuilder builder)
|
||||||
|
=> new KeyPerFileConfigurationProvider(this);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<Description>Configuration provider that uses files in a directory for Microsoft.Extensions.Configuration.</Description>
|
||||||
|
<TargetFramework>netstandard2.0</TargetFramework>
|
||||||
|
<EnableApiCheck>false</EnableApiCheck>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Reference Include="Microsoft.Extensions.Configuration" />
|
||||||
|
<Reference Include="Microsoft.Extensions.FileProviders.Physical" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
|
|
@ -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.
|
||||||
|
|
@ -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<ArgumentException>(() => 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<IFileInfo> _list;
|
||||||
|
|
||||||
|
public TestDirectoryContents(params IFileInfo[] files)
|
||||||
|
{
|
||||||
|
_list = new List<IFileInfo>(files);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Exists
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public IEnumerator<IFileInfo> 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<TargetFrameworks>$(StandardTestTfms)</TargetFrameworks>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Reference Include="Microsoft.Extensions.Configuration.KeyPerFile" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
Loading…
Reference in New Issue