Add ReloadOnChange to KeyPerFile configuration provider (dotnet/extensions#2808)

\n\nCommit migrated from cca1c7ca95
This commit is contained in:
Kahbazi 2020-02-11 00:39:13 +03:30 committed by GitHub
parent 44c226ccac
commit 4aab03bf9a
6 changed files with 210 additions and 18 deletions

View File

@ -6,14 +6,17 @@ namespace Microsoft.Extensions.Configuration
public static partial class KeyPerFileConfigurationBuilderExtensions public static partial class KeyPerFileConfigurationBuilderExtensions
{ {
public static Microsoft.Extensions.Configuration.IConfigurationBuilder AddKeyPerFile(this Microsoft.Extensions.Configuration.IConfigurationBuilder builder, System.Action<Microsoft.Extensions.Configuration.KeyPerFile.KeyPerFileConfigurationSource> configureSource) { throw null; } public static Microsoft.Extensions.Configuration.IConfigurationBuilder AddKeyPerFile(this Microsoft.Extensions.Configuration.IConfigurationBuilder builder, System.Action<Microsoft.Extensions.Configuration.KeyPerFile.KeyPerFileConfigurationSource> 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) { 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 namespace Microsoft.Extensions.Configuration.KeyPerFile
{ {
public partial class KeyPerFileConfigurationProvider : Microsoft.Extensions.Configuration.ConfigurationProvider public partial class KeyPerFileConfigurationProvider : Microsoft.Extensions.Configuration.ConfigurationProvider, System.IDisposable
{ {
public KeyPerFileConfigurationProvider(Microsoft.Extensions.Configuration.KeyPerFile.KeyPerFileConfigurationSource source) { } public KeyPerFileConfigurationProvider(Microsoft.Extensions.Configuration.KeyPerFile.KeyPerFileConfigurationSource source) { }
public void Dispose() { }
public override void Load() { } public override void Load() { }
public override string ToString() { throw null; } public override string ToString() { throw null; }
} }
@ -24,6 +27,8 @@ namespace Microsoft.Extensions.Configuration.KeyPerFile
public System.Func<string, bool> IgnoreCondition { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } } public System.Func<string, bool> 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 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 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; } public Microsoft.Extensions.Configuration.IConfigurationProvider Build(Microsoft.Extensions.Configuration.IConfigurationBuilder builder) { throw null; }
} }
} }

View File

@ -6,14 +6,17 @@ namespace Microsoft.Extensions.Configuration
public static partial class KeyPerFileConfigurationBuilderExtensions public static partial class KeyPerFileConfigurationBuilderExtensions
{ {
public static Microsoft.Extensions.Configuration.IConfigurationBuilder AddKeyPerFile(this Microsoft.Extensions.Configuration.IConfigurationBuilder builder, System.Action<Microsoft.Extensions.Configuration.KeyPerFile.KeyPerFileConfigurationSource> configureSource) { throw null; } public static Microsoft.Extensions.Configuration.IConfigurationBuilder AddKeyPerFile(this Microsoft.Extensions.Configuration.IConfigurationBuilder builder, System.Action<Microsoft.Extensions.Configuration.KeyPerFile.KeyPerFileConfigurationSource> 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) { 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 namespace Microsoft.Extensions.Configuration.KeyPerFile
{ {
public partial class KeyPerFileConfigurationProvider : Microsoft.Extensions.Configuration.ConfigurationProvider public partial class KeyPerFileConfigurationProvider : Microsoft.Extensions.Configuration.ConfigurationProvider, System.IDisposable
{ {
public KeyPerFileConfigurationProvider(Microsoft.Extensions.Configuration.KeyPerFile.KeyPerFileConfigurationSource source) { } public KeyPerFileConfigurationProvider(Microsoft.Extensions.Configuration.KeyPerFile.KeyPerFileConfigurationSource source) { }
public void Dispose() { }
public override void Load() { } public override void Load() { }
public override string ToString() { throw null; } public override string ToString() { throw null; }
} }
@ -24,6 +27,8 @@ namespace Microsoft.Extensions.Configuration.KeyPerFile
public System.Func<string, bool> IgnoreCondition { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } } public System.Func<string, bool> 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 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 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; } public Microsoft.Extensions.Configuration.IConfigurationProvider Build(Microsoft.Extensions.Configuration.IConfigurationBuilder builder) { throw null; }
} }
} }

View File

@ -10,6 +10,16 @@ namespace Microsoft.Extensions.Configuration
/// </summary> /// </summary>
public static class KeyPerFileConfigurationBuilderExtensions 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>
/// <returns>The <see cref="IConfigurationBuilder"/>.</returns>
public static IConfigurationBuilder AddKeyPerFile(this IConfigurationBuilder builder, string directoryPath)
=> builder.AddKeyPerFile(directoryPath, optional: false, reloadOnChange: false);
/// <summary> /// <summary>
/// Adds configuration using files from a directory. File names are used as the key, /// Adds configuration using files from a directory. File names are used as the key,
/// file contents are used as the value. /// file contents are used as the value.
@ -19,6 +29,18 @@ namespace Microsoft.Extensions.Configuration
/// <param name="optional">Whether the directory is optional.</param> /// <param name="optional">Whether the directory is optional.</param>
/// <returns>The <see cref="IConfigurationBuilder"/>.</returns> /// <returns>The <see cref="IConfigurationBuilder"/>.</returns>
public static IConfigurationBuilder AddKeyPerFile(this IConfigurationBuilder builder, string directoryPath, bool optional) public static IConfigurationBuilder AddKeyPerFile(this IConfigurationBuilder builder, string directoryPath, bool optional)
=> builder.AddKeyPerFile(directoryPath, optional, reloadOnChange: false);
/// <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>
/// <param name="reloadOnChange">Whether the configuration should be reloaded if the files are changed, added or removed.</param>
/// <returns>The <see cref="IConfigurationBuilder"/>.</returns>
public static IConfigurationBuilder AddKeyPerFile(this IConfigurationBuilder builder, string directoryPath, bool optional, bool reloadOnChange)
=> builder.AddKeyPerFile(source => => builder.AddKeyPerFile(source =>
{ {
// Only try to set the file provider if its not optional or the directory exists // Only try to set the file provider if its not optional or the directory exists
@ -27,6 +49,7 @@ namespace Microsoft.Extensions.Configuration
source.FileProvider = new PhysicalFileProvider(directoryPath); source.FileProvider = new PhysicalFileProvider(directoryPath);
} }
source.Optional = optional; source.Optional = optional;
source.ReloadOnChange = reloadOnChange;
}); });
/// <summary> /// <summary>

View File

@ -1,14 +1,18 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Threading;
using Microsoft.Extensions.Primitives;
namespace Microsoft.Extensions.Configuration.KeyPerFile namespace Microsoft.Extensions.Configuration.KeyPerFile
{ {
/// <summary> /// <summary>
/// A <see cref="ConfigurationProvider"/> that uses a directory's files as configuration key/values. /// A <see cref="ConfigurationProvider"/> that uses a directory's files as configuration key/values.
/// </summary> /// </summary>
public class KeyPerFileConfigurationProvider : ConfigurationProvider public class KeyPerFileConfigurationProvider : ConfigurationProvider, IDisposable
{ {
private readonly IDisposable _changeTokenRegistration;
KeyPerFileConfigurationSource Source { get; set; } KeyPerFileConfigurationSource Source { get; set; }
/// <summary> /// <summary>
@ -16,7 +20,21 @@ namespace Microsoft.Extensions.Configuration.KeyPerFile
/// </summary> /// </summary>
/// <param name="source">The settings.</param> /// <param name="source">The settings.</param>
public KeyPerFileConfigurationProvider(KeyPerFileConfigurationSource source) public KeyPerFileConfigurationProvider(KeyPerFileConfigurationSource source)
=> Source = source ?? throw new ArgumentNullException(nameof(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) private static string NormalizeKey(string key)
=> key.Replace("__", ConfigurationPath.KeyDelimiter); => key.Replace("__", ConfigurationPath.KeyDelimiter);
@ -27,15 +45,20 @@ namespace Microsoft.Extensions.Configuration.KeyPerFile
: value; : value;
/// <summary> /// <summary>
/// Loads the docker secrets. /// Loads the configuration values.
/// </summary> /// </summary>
public override void Load() public override void Load()
{
Load(reload: false);
}
private void Load(bool reload)
{ {
var data = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase); var data = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
if (Source.FileProvider == null) if (Source.FileProvider == null)
{ {
if (Source.Optional) if (Source.Optional || reload) // Always optional on reload
{ {
Data = data; Data = data;
return; return;
@ -45,25 +68,32 @@ namespace Microsoft.Extensions.Configuration.KeyPerFile
} }
var directory = Source.FileProvider.GetDirectoryContents("/"); var directory = Source.FileProvider.GetDirectoryContents("/");
if (!directory.Exists && !Source.Optional) 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."); 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) foreach (var file in directory)
{ {
continue; if (file.IsDirectory)
} {
continue;
}
using var stream = file.CreateReadStream();
using var streamReader = new StreamReader(stream);
using (var stream = file.CreateReadStream())
using (var streamReader = new StreamReader(stream))
{
if (Source.IgnoreCondition == null || !Source.IgnoreCondition(file.Name)) if (Source.IgnoreCondition == null || !Source.IgnoreCondition(file.Name))
{ {
data.Add(NormalizeKey(file.Name), TrimNewLine(streamReader.ReadToEnd())); data.Add(NormalizeKey(file.Name), TrimNewLine(streamReader.ReadToEnd()));
} }
} }
} }
@ -79,5 +109,11 @@ namespace Microsoft.Extensions.Configuration.KeyPerFile
/// <returns> The configuration name. </returns> /// <returns> The configuration name. </returns>
public override string ToString() public override string ToString()
=> $"{GetType().Name} for files in '{GetDirectoryName()}' ({(Source.Optional ? "Optional" : "Required")})"; => $"{GetType().Name} for files in '{GetDirectoryName()}' ({(Source.Optional ? "Optional" : "Required")})";
/// <inheritdoc />
public void Dispose()
{
_changeTokenRegistration?.Dispose();
}
} }
} }

View File

@ -37,6 +37,17 @@ namespace Microsoft.Extensions.Configuration.KeyPerFile
/// </summary> /// </summary>
public bool Optional { get; set; } public bool Optional { get; set; }
/// <summary>
/// Determines whether the source will be loaded if the underlying file changes.
/// </summary>
public bool ReloadOnChange { get; set; }
/// <summary>
/// Number of milliseconds that reload will wait before calling Load. This helps
/// avoid triggering reload before a file is completely written. Default is 250.
/// </summary>
public int ReloadDelay { get; set; } = 250;
/// <summary> /// <summary>
/// Builds the <see cref="KeyPerFileConfigurationProvider"/> for this source. /// Builds the <see cref="KeyPerFileConfigurationProvider"/> for this source.
/// </summary> /// </summary>

View File

@ -217,6 +217,79 @@ namespace Microsoft.Extensions.Configuration.KeyPerFile.Test
Assert.Equal("Foo", options.Text); 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 private sealed class MyOptions
{ {
public int Number { get; set; } public int Number { get; set; }
@ -227,17 +300,56 @@ namespace Microsoft.Extensions.Configuration.KeyPerFile.Test
class TestFileProvider : IFileProvider class TestFileProvider : IFileProvider
{ {
IDirectoryContents _contents; IDirectoryContents _contents;
MockChangeToken _changeToken;
public TestFileProvider(params IFileInfo[] files) public TestFileProvider(params IFileInfo[] files)
{ {
_contents = new TestDirectoryContents(files); _contents = new TestDirectoryContents(files);
_changeToken = new MockChangeToken();
} }
public IDirectoryContents GetDirectoryContents(string subpath) => _contents; public IDirectoryContents GetDirectoryContents(string subpath) => _contents;
public IFileInfo GetFileInfo(string subpath) => new TestFile("TestDirectory"); public IFileInfo GetFileInfo(string subpath) => new TestFile("TestDirectory");
public IChangeToken Watch(string filter) => throw new NotImplementedException(); 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<object> callback, object state)
{
var disposable = new MockDisposable();
_callback = () => callback(state);
return disposable;
}
internal void RaiseCallback()
{
_callback?.Invoke();
}
}
class MockDisposable : IDisposable
{
public bool Disposed { get; set; }
public void Dispose()
{
Disposed = true;
}
} }
class TestDirectoryContents : IDirectoryContents class TestDirectoryContents : IDirectoryContents
@ -291,7 +403,7 @@ namespace Microsoft.Extensions.Configuration.KeyPerFile.Test
public Stream CreateReadStream() public Stream CreateReadStream()
{ {
if(IsDirectory) if (IsDirectory)
{ {
throw new InvalidOperationException("Cannot create stream from directory"); throw new InvalidOperationException("Cannot create stream from directory");
} }