Atomically swap config data (dotnet/extensions#1202)

Swap the configuration providers' data atomically, rather than directly changing the property, so that any enumeration of the dictionary running during the reload operation does not throw an InvalidOperationException due to the collection being modified.
Relates to dotnet/extensions#1189.\n\nCommit migrated from 192abfdf3e
This commit is contained in:
Martin Costello 2019-03-04 18:14:16 +00:00 committed by Chris Ross
parent aff09d16a1
commit 09b9a49da6
2 changed files with 48 additions and 2 deletions

View File

@ -31,12 +31,13 @@ namespace Microsoft.Extensions.Configuration.KeyPerFile
/// </summary>
public override void Load()
{
Data = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
var data = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
if (Source.FileProvider == null)
{
if (Source.Optional)
{
Data = data;
return;
}
@ -61,10 +62,12 @@ namespace Microsoft.Extensions.Configuration.KeyPerFile
{
if (Source.IgnoreCondition == null || !Source.IgnoreCondition(file.Name))
{
Data.Add(NormalizeKey(file.Name), TrimNewLine(streamReader.ReadToEnd()));
data.Add(NormalizeKey(file.Name), TrimNewLine(streamReader.ReadToEnd()));
}
}
}
Data = data;
}
private string GetDirectoryName()

View File

@ -6,6 +6,8 @@ using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.Primitives;
using Xunit;
@ -179,6 +181,47 @@ namespace Microsoft.Extensions.Configuration.KeyPerFile.Test
Assert.Equal("SecretValue1", config["ignore.Secret1"]);
Assert.Equal("SecretValue2", config["Secret2"]);
}
[Fact]
public void BindingDoesNotThrowIfReloadedDuringBinding()
{
var testFileProvider = new TestFileProvider(
new TestFile("Number", "-2"),
new TestFile("Text", "Foo"));
var config = new ConfigurationBuilder()
.AddKeyPerFile(o => o.FileProvider = testFileProvider)
.Build();
MyOptions options = null;
using (var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(250)))
{
void ReloadLoop()
{
while (!cts.IsCancellationRequested)
{
config.Reload();
}
}
_ = Task.Run(ReloadLoop);
while (!cts.IsCancellationRequested)
{
options = config.Get<MyOptions>();
}
}
Assert.Equal(-2, options.Number);
Assert.Equal("Foo", options.Text);
}
private sealed class MyOptions
{
public int Number { get; set; }
public string Text { get; set; }
}
}
class TestFileProvider : IFileProvider