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 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, bool reloadOnChange) { throw null; }
}
}
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 void Dispose() { }
public override void Load() { }
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 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; }
}
}

View File

@ -6,14 +6,17 @@ namespace Microsoft.Extensions.Configuration
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, 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
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; }
}
@ -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 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; }
}
}

View File

@ -10,6 +10,16 @@ namespace Microsoft.Extensions.Configuration
/// </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>
/// <returns>The <see cref="IConfigurationBuilder"/>.</returns>
public static IConfigurationBuilder AddKeyPerFile(this IConfigurationBuilder builder, string directoryPath)
=> builder.AddKeyPerFile(directoryPath, optional: false, reloadOnChange: false);
/// <summary>
/// Adds configuration using files from a directory. File names are used as the key,
/// file contents are used as the value.
@ -19,6 +29,18 @@ namespace Microsoft.Extensions.Configuration
/// <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(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 =>
{
// 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.Optional = optional;
source.ReloadOnChange = reloadOnChange;
});
/// <summary>

View File

@ -1,14 +1,18 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Threading;
using Microsoft.Extensions.Primitives;
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
public class KeyPerFileConfigurationProvider : ConfigurationProvider, IDisposable
{
private readonly IDisposable _changeTokenRegistration;
KeyPerFileConfigurationSource Source { get; set; }
/// <summary>
@ -16,7 +20,21 @@ namespace Microsoft.Extensions.Configuration.KeyPerFile
/// </summary>
/// <param name="source">The settings.</param>
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)
=> key.Replace("__", ConfigurationPath.KeyDelimiter);
@ -27,15 +45,20 @@ namespace Microsoft.Extensions.Configuration.KeyPerFile
: value;
/// <summary>
/// Loads the docker secrets.
/// Loads the configuration values.
/// </summary>
public override void Load()
{
Load(reload: false);
}
private void Load(bool reload)
{
var data = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
if (Source.FileProvider == null)
{
if (Source.Optional)
if (Source.Optional || reload) // Always optional on reload
{
Data = data;
return;
@ -45,25 +68,32 @@ namespace Microsoft.Extensions.Configuration.KeyPerFile
}
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.");
}
foreach (var file in directory)
else
{
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))
{
data.Add(NormalizeKey(file.Name), TrimNewLine(streamReader.ReadToEnd()));
}
}
}
@ -79,5 +109,11 @@ namespace Microsoft.Extensions.Configuration.KeyPerFile
/// <returns> The configuration name. </returns>
public override string ToString()
=> $"{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>
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>
/// Builds the <see cref="KeyPerFileConfigurationProvider"/> for this source.
/// </summary>

View File

@ -217,6 +217,79 @@ namespace Microsoft.Extensions.Configuration.KeyPerFile.Test
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; }
@ -227,17 +300,56 @@ namespace Microsoft.Extensions.Configuration.KeyPerFile.Test
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) => 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
@ -291,7 +403,7 @@ namespace Microsoft.Extensions.Configuration.KeyPerFile.Test
public Stream CreateReadStream()
{
if(IsDirectory)
if (IsDirectory)
{
throw new InvalidOperationException("Cannot create stream from directory");
}