Add ReloadOnChange to KeyPerFile configuration provider (dotnet/extensions#2808)
\n\nCommit migrated from cca1c7ca95
This commit is contained in:
parent
44c226ccac
commit
4aab03bf9a
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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; }
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue