Skip to content

Commit 4aab03b

Browse files
authored
Add ReloadOnChange to KeyPerFile configuration provider (dotnet/extensions#2808)
\n\nCommit migrated from dotnet/extensions@cca1c7c
1 parent 44c226c commit 4aab03b

6 files changed

+210
-18
lines changed

src/Configuration.KeyPerFile/ref/Microsoft.Extensions.Configuration.KeyPerFile.netcoreapp.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,17 @@ namespace Microsoft.Extensions.Configuration
66
public static partial class KeyPerFileConfigurationBuilderExtensions
77
{
88
public static Microsoft.Extensions.Configuration.IConfigurationBuilder AddKeyPerFile(this Microsoft.Extensions.Configuration.IConfigurationBuilder builder, System.Action<Microsoft.Extensions.Configuration.KeyPerFile.KeyPerFileConfigurationSource> configureSource) { throw null; }
9+
public static Microsoft.Extensions.Configuration.IConfigurationBuilder AddKeyPerFile(this Microsoft.Extensions.Configuration.IConfigurationBuilder builder, string directoryPath) { throw null; }
910
public static Microsoft.Extensions.Configuration.IConfigurationBuilder AddKeyPerFile(this Microsoft.Extensions.Configuration.IConfigurationBuilder builder, string directoryPath, bool optional) { throw null; }
11+
public static Microsoft.Extensions.Configuration.IConfigurationBuilder AddKeyPerFile(this Microsoft.Extensions.Configuration.IConfigurationBuilder builder, string directoryPath, bool optional, bool reloadOnChange) { throw null; }
1012
}
1113
}
1214
namespace Microsoft.Extensions.Configuration.KeyPerFile
1315
{
14-
public partial class KeyPerFileConfigurationProvider : Microsoft.Extensions.Configuration.ConfigurationProvider
16+
public partial class KeyPerFileConfigurationProvider : Microsoft.Extensions.Configuration.ConfigurationProvider, System.IDisposable
1517
{
1618
public KeyPerFileConfigurationProvider(Microsoft.Extensions.Configuration.KeyPerFile.KeyPerFileConfigurationSource source) { }
19+
public void Dispose() { }
1720
public override void Load() { }
1821
public override string ToString() { throw null; }
1922
}
@@ -24,6 +27,8 @@ public KeyPerFileConfigurationSource() { }
2427
public System.Func<string, bool> IgnoreCondition { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
2528
public string IgnorePrefix { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
2629
public bool Optional { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
30+
public int ReloadDelay { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
31+
public bool ReloadOnChange { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
2732
public Microsoft.Extensions.Configuration.IConfigurationProvider Build(Microsoft.Extensions.Configuration.IConfigurationBuilder builder) { throw null; }
2833
}
2934
}

src/Configuration.KeyPerFile/ref/Microsoft.Extensions.Configuration.KeyPerFile.netstandard2.0.cs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,17 @@ namespace Microsoft.Extensions.Configuration
66
public static partial class KeyPerFileConfigurationBuilderExtensions
77
{
88
public static Microsoft.Extensions.Configuration.IConfigurationBuilder AddKeyPerFile(this Microsoft.Extensions.Configuration.IConfigurationBuilder builder, System.Action<Microsoft.Extensions.Configuration.KeyPerFile.KeyPerFileConfigurationSource> configureSource) { throw null; }
9+
public static Microsoft.Extensions.Configuration.IConfigurationBuilder AddKeyPerFile(this Microsoft.Extensions.Configuration.IConfigurationBuilder builder, string directoryPath) { throw null; }
910
public static Microsoft.Extensions.Configuration.IConfigurationBuilder AddKeyPerFile(this Microsoft.Extensions.Configuration.IConfigurationBuilder builder, string directoryPath, bool optional) { throw null; }
11+
public static Microsoft.Extensions.Configuration.IConfigurationBuilder AddKeyPerFile(this Microsoft.Extensions.Configuration.IConfigurationBuilder builder, string directoryPath, bool optional, bool reloadOnChange) { throw null; }
1012
}
1113
}
1214
namespace Microsoft.Extensions.Configuration.KeyPerFile
1315
{
14-
public partial class KeyPerFileConfigurationProvider : Microsoft.Extensions.Configuration.ConfigurationProvider
16+
public partial class KeyPerFileConfigurationProvider : Microsoft.Extensions.Configuration.ConfigurationProvider, System.IDisposable
1517
{
1618
public KeyPerFileConfigurationProvider(Microsoft.Extensions.Configuration.KeyPerFile.KeyPerFileConfigurationSource source) { }
19+
public void Dispose() { }
1720
public override void Load() { }
1821
public override string ToString() { throw null; }
1922
}
@@ -24,6 +27,8 @@ public KeyPerFileConfigurationSource() { }
2427
public System.Func<string, bool> IgnoreCondition { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
2528
public string IgnorePrefix { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
2629
public bool Optional { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
30+
public int ReloadDelay { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
31+
public bool ReloadOnChange { [System.Runtime.CompilerServices.CompilerGeneratedAttribute] get { throw null; } [System.Runtime.CompilerServices.CompilerGeneratedAttribute] set { } }
2732
public Microsoft.Extensions.Configuration.IConfigurationProvider Build(Microsoft.Extensions.Configuration.IConfigurationBuilder builder) { throw null; }
2833
}
2934
}

src/Configuration.KeyPerFile/src/KeyPerFileConfigurationBuilderExtensions.cs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,16 @@ namespace Microsoft.Extensions.Configuration
1010
/// </summary>
1111
public static class KeyPerFileConfigurationBuilderExtensions
1212
{
13+
/// <summary>
14+
/// Adds configuration using files from a directory. File names are used as the key,
15+
/// file contents are used as the value.
16+
/// </summary>
17+
/// <param name="builder">The <see cref="IConfigurationBuilder"/> to add to.</param>
18+
/// <param name="directoryPath">The path to the directory.</param>
19+
/// <returns>The <see cref="IConfigurationBuilder"/>.</returns>
20+
public static IConfigurationBuilder AddKeyPerFile(this IConfigurationBuilder builder, string directoryPath)
21+
=> builder.AddKeyPerFile(directoryPath, optional: false, reloadOnChange: false);
22+
1323
/// <summary>
1424
/// Adds configuration using files from a directory. File names are used as the key,
1525
/// file contents are used as the value.
@@ -19,6 +29,18 @@ public static class KeyPerFileConfigurationBuilderExtensions
1929
/// <param name="optional">Whether the directory is optional.</param>
2030
/// <returns>The <see cref="IConfigurationBuilder"/>.</returns>
2131
public static IConfigurationBuilder AddKeyPerFile(this IConfigurationBuilder builder, string directoryPath, bool optional)
32+
=> builder.AddKeyPerFile(directoryPath, optional, reloadOnChange: false);
33+
34+
/// <summary>
35+
/// Adds configuration using files from a directory. File names are used as the key,
36+
/// file contents are used as the value.
37+
/// </summary>
38+
/// <param name="builder">The <see cref="IConfigurationBuilder"/> to add to.</param>
39+
/// <param name="directoryPath">The path to the directory.</param>
40+
/// <param name="optional">Whether the directory is optional.</param>
41+
/// <param name="reloadOnChange">Whether the configuration should be reloaded if the files are changed, added or removed.</param>
42+
/// <returns>The <see cref="IConfigurationBuilder"/>.</returns>
43+
public static IConfigurationBuilder AddKeyPerFile(this IConfigurationBuilder builder, string directoryPath, bool optional, bool reloadOnChange)
2244
=> builder.AddKeyPerFile(source =>
2345
{
2446
// Only try to set the file provider if its not optional or the directory exists
@@ -27,6 +49,7 @@ public static IConfigurationBuilder AddKeyPerFile(this IConfigurationBuilder bui
2749
source.FileProvider = new PhysicalFileProvider(directoryPath);
2850
}
2951
source.Optional = optional;
52+
source.ReloadOnChange = reloadOnChange;
3053
});
3154

3255
/// <summary>
Lines changed: 49 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,40 @@
11
using System;
22
using System.Collections.Generic;
33
using System.IO;
4+
using System.Threading;
5+
using Microsoft.Extensions.Primitives;
46

57
namespace Microsoft.Extensions.Configuration.KeyPerFile
68
{
79
/// <summary>
810
/// A <see cref="ConfigurationProvider"/> that uses a directory's files as configuration key/values.
911
/// </summary>
10-
public class KeyPerFileConfigurationProvider : ConfigurationProvider
12+
public class KeyPerFileConfigurationProvider : ConfigurationProvider, IDisposable
1113
{
14+
private readonly IDisposable _changeTokenRegistration;
15+
1216
KeyPerFileConfigurationSource Source { get; set; }
1317

1418
/// <summary>
1519
/// Initializes a new instance.
1620
/// </summary>
1721
/// <param name="source">The settings.</param>
1822
public KeyPerFileConfigurationProvider(KeyPerFileConfigurationSource source)
19-
=> Source = source ?? throw new ArgumentNullException(nameof(source));
23+
{
24+
Source = source ?? throw new ArgumentNullException(nameof(source));
25+
26+
if (Source.ReloadOnChange && Source.FileProvider != null)
27+
{
28+
_changeTokenRegistration = ChangeToken.OnChange(
29+
() => Source.FileProvider.Watch("*"),
30+
() =>
31+
{
32+
Thread.Sleep(Source.ReloadDelay);
33+
Load(reload: true);
34+
});
35+
}
36+
37+
}
2038

2139
private static string NormalizeKey(string key)
2240
=> key.Replace("__", ConfigurationPath.KeyDelimiter);
@@ -27,15 +45,20 @@ private static string TrimNewLine(string value)
2745
: value;
2846

2947
/// <summary>
30-
/// Loads the docker secrets.
48+
/// Loads the configuration values.
3149
/// </summary>
3250
public override void Load()
51+
{
52+
Load(reload: false);
53+
}
54+
55+
private void Load(bool reload)
3356
{
3457
var data = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
3558

3659
if (Source.FileProvider == null)
3760
{
38-
if (Source.Optional)
61+
if (Source.Optional || reload) // Always optional on reload
3962
{
4063
Data = data;
4164
return;
@@ -45,25 +68,32 @@ public override void Load()
4568
}
4669

4770
var directory = Source.FileProvider.GetDirectoryContents("/");
48-
if (!directory.Exists && !Source.Optional)
71+
if (!directory.Exists)
4972
{
73+
if (Source.Optional || reload) // Always optional on reload
74+
{
75+
Data = data;
76+
return;
77+
}
5078
throw new DirectoryNotFoundException("The root directory for the FileProvider doesn't exist and is not optional.");
5179
}
52-
53-
foreach (var file in directory)
80+
else
5481
{
55-
if (file.IsDirectory)
82+
foreach (var file in directory)
5683
{
57-
continue;
58-
}
84+
if (file.IsDirectory)
85+
{
86+
continue;
87+
}
88+
89+
using var stream = file.CreateReadStream();
90+
using var streamReader = new StreamReader(stream);
5991

60-
using (var stream = file.CreateReadStream())
61-
using (var streamReader = new StreamReader(stream))
62-
{
6392
if (Source.IgnoreCondition == null || !Source.IgnoreCondition(file.Name))
6493
{
6594
data.Add(NormalizeKey(file.Name), TrimNewLine(streamReader.ReadToEnd()));
6695
}
96+
6797
}
6898
}
6999

@@ -79,5 +109,11 @@ private string GetDirectoryName()
79109
/// <returns> The configuration name. </returns>
80110
public override string ToString()
81111
=> $"{GetType().Name} for files in '{GetDirectoryName()}' ({(Source.Optional ? "Optional" : "Required")})";
112+
113+
/// <inheritdoc />
114+
public void Dispose()
115+
{
116+
_changeTokenRegistration?.Dispose();
117+
}
82118
}
83119
}

src/Configuration.KeyPerFile/src/KeyPerFileConfigurationSource.cs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,17 @@ public KeyPerFileConfigurationSource()
3737
/// </summary>
3838
public bool Optional { get; set; }
3939

40+
/// <summary>
41+
/// Determines whether the source will be loaded if the underlying file changes.
42+
/// </summary>
43+
public bool ReloadOnChange { get; set; }
44+
45+
/// <summary>
46+
/// Number of milliseconds that reload will wait before calling Load. This helps
47+
/// avoid triggering reload before a file is completely written. Default is 250.
48+
/// </summary>
49+
public int ReloadDelay { get; set; } = 250;
50+
4051
/// <summary>
4152
/// Builds the <see cref="KeyPerFileConfigurationProvider"/> for this source.
4253
/// </summary>

src/Configuration.KeyPerFile/test/KeyPerFileTests.cs

Lines changed: 115 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,79 @@ void ReloadLoop()
217217
Assert.Equal("Foo", options.Text);
218218
}
219219

220+
[Fact]
221+
public void ReloadConfigWhenReloadOnChangeIsTrue()
222+
{
223+
var testFileProvider = new TestFileProvider(
224+
new TestFile("Secret1", "SecretValue1"),
225+
new TestFile("Secret2", "SecretValue2"));
226+
227+
var config = new ConfigurationBuilder()
228+
.AddKeyPerFile(o =>
229+
{
230+
o.FileProvider = testFileProvider;
231+
o.ReloadOnChange = true;
232+
}).Build();
233+
234+
Assert.Equal("SecretValue1", config["Secret1"]);
235+
Assert.Equal("SecretValue2", config["Secret2"]);
236+
237+
testFileProvider.ChangeFiles(
238+
new TestFile("Secret1", "NewSecretValue1"),
239+
new TestFile("Secret3", "NewSecretValue3"));
240+
241+
Assert.Equal("NewSecretValue1", config["Secret1"]);
242+
Assert.Null(config["NewSecret2"]);
243+
Assert.Equal("NewSecretValue3", config["Secret3"]);
244+
}
245+
246+
[Fact]
247+
public void SameConfigWhenReloadOnChangeIsFalse()
248+
{
249+
var testFileProvider = new TestFileProvider(
250+
new TestFile("Secret1", "SecretValue1"),
251+
new TestFile("Secret2", "SecretValue2"));
252+
253+
var config = new ConfigurationBuilder()
254+
.AddKeyPerFile(o =>
255+
{
256+
o.FileProvider = testFileProvider;
257+
o.ReloadOnChange = false;
258+
}).Build();
259+
260+
Assert.Equal("SecretValue1", config["Secret1"]);
261+
Assert.Equal("SecretValue2", config["Secret2"]);
262+
263+
testFileProvider.ChangeFiles(
264+
new TestFile("Secret1", "NewSecretValue1"),
265+
new TestFile("Secret3", "NewSecretValue3"));
266+
267+
Assert.Equal("SecretValue1", config["Secret1"]);
268+
Assert.Equal("SecretValue2", config["Secret2"]);
269+
}
270+
271+
[Fact]
272+
public void NoFilesReloadWhenAddedFiles()
273+
{
274+
var testFileProvider = new TestFileProvider();
275+
276+
var config = new ConfigurationBuilder()
277+
.AddKeyPerFile(o =>
278+
{
279+
o.FileProvider = testFileProvider;
280+
o.ReloadOnChange = true;
281+
}).Build();
282+
283+
Assert.Empty(config.AsEnumerable());
284+
285+
testFileProvider.ChangeFiles(
286+
new TestFile("Secret1", "SecretValue1"),
287+
new TestFile("Secret2", "SecretValue2"));
288+
289+
Assert.Equal("SecretValue1", config["Secret1"]);
290+
Assert.Equal("SecretValue2", config["Secret2"]);
291+
}
292+
220293
private sealed class MyOptions
221294
{
222295
public int Number { get; set; }
@@ -227,17 +300,56 @@ private sealed class MyOptions
227300
class TestFileProvider : IFileProvider
228301
{
229302
IDirectoryContents _contents;
230-
303+
MockChangeToken _changeToken;
304+
231305
public TestFileProvider(params IFileInfo[] files)
232306
{
233307
_contents = new TestDirectoryContents(files);
308+
_changeToken = new MockChangeToken();
234309
}
235310

236311
public IDirectoryContents GetDirectoryContents(string subpath) => _contents;
237312

238313
public IFileInfo GetFileInfo(string subpath) => new TestFile("TestDirectory");
239314

240-
public IChangeToken Watch(string filter) => throw new NotImplementedException();
315+
public IChangeToken Watch(string filter) => _changeToken;
316+
317+
internal void ChangeFiles(params IFileInfo[] files)
318+
{
319+
_contents = new TestDirectoryContents(files);
320+
_changeToken.RaiseCallback();
321+
}
322+
}
323+
324+
class MockChangeToken : IChangeToken
325+
{
326+
private Action _callback;
327+
328+
public bool ActiveChangeCallbacks => true;
329+
330+
public bool HasChanged => true;
331+
332+
public IDisposable RegisterChangeCallback(Action<object> callback, object state)
333+
{
334+
var disposable = new MockDisposable();
335+
_callback = () => callback(state);
336+
return disposable;
337+
}
338+
339+
internal void RaiseCallback()
340+
{
341+
_callback?.Invoke();
342+
}
343+
}
344+
345+
class MockDisposable : IDisposable
346+
{
347+
public bool Disposed { get; set; }
348+
349+
public void Dispose()
350+
{
351+
Disposed = true;
352+
}
241353
}
242354

243355
class TestDirectoryContents : IDirectoryContents
@@ -291,7 +403,7 @@ public TestFile(string name, string contents)
291403

292404
public Stream CreateReadStream()
293405
{
294-
if(IsDirectory)
406+
if (IsDirectory)
295407
{
296408
throw new InvalidOperationException("Cannot create stream from directory");
297409
}

0 commit comments

Comments
 (0)