Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
}
Expand All @@ -24,6 +27,8 @@ public KeyPerFileConfigurationSource() { }
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; }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will need API review (even if its the same as what other providers do).

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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; }
}
Expand All @@ -24,6 +27,8 @@ public KeyPerFileConfigurationSource() { }
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; }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -19,6 +29,18 @@ public static class KeyPerFileConfigurationBuilderExtensions
/// <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);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

question - do our other config providers do reloading by default? Looks like JSON does not: https://github.com/aspnet/Extensions/blob/master/src/Configuration/Config.Json/src/JsonConfigurationExtensions.cs#L38

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should be consistent so it should be off by default.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah this is expensive and has some unexpected side effects so generally reload should be off by default unless people explicitly make it enabled


/// <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
Expand All @@ -27,6 +49,7 @@ public static IConfigurationBuilder AddKeyPerFile(this IConfigurationBuilder bui
source.FileProvider = new PhysicalFileProvider(directoryPath);
}
source.Optional = optional;
source.ReloadOnChange = reloadOnChange;
});

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,22 +1,40 @@
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>
/// Initializes a new instance.
/// </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);
Expand All @@ -27,15 +45,20 @@ private static string TrimNewLine(string value)
: 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;
Expand All @@ -45,25 +68,32 @@ public override void Load()
}

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

}
}

Expand All @@ -79,5 +109,11 @@ private string GetDirectoryName()
/// <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();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,17 @@ public KeyPerFileConfigurationSource()
/// </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>
Expand Down
118 changes: 115 additions & 3 deletions src/Configuration/Config.KeyPerFile/test/KeyPerFileTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,79 @@ void ReloadLoop()
Assert.Equal("Foo", options.Text);
}

[Fact]
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you add a test where nothing exists to start, and the directory/files get created later so its the nothing found to something case as opposed to just modification scenarios?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@HaoK I added the test.

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; }
Expand All @@ -227,17 +300,56 @@ private sealed class MyOptions
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
Expand Down Expand Up @@ -291,7 +403,7 @@ public TestFile(string name, string contents)

public Stream CreateReadStream()
{
if(IsDirectory)
if (IsDirectory)
{
throw new InvalidOperationException("Cannot create stream from directory");
}
Expand Down