Merge source code from aspnet/Configuration into this repo

\n\nCommit migrated from 8a9b64a3e5
This commit is contained in:
Nate McMaster 2018-11-05 16:22:57 -08:00
commit 8d17a5b9f3
8 changed files with 505 additions and 0 deletions

View File

@ -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>

View File

@ -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);
}
}

View File

@ -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()));
}
}
}
}
}
}

View File

@ -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);
}
}

View File

@ -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>

View File

@ -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.

View File

@ -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));
}
}
}

View File

@ -0,0 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>$(StandardTestTfms)</TargetFrameworks>
</PropertyGroup>
<ItemGroup>
<Reference Include="Microsoft.Extensions.Configuration.KeyPerFile" />
</ItemGroup>
</Project>