diff --git a/src/Configuration/Config.EnvironmentVariables/src/EnvironmentVariablesConfigurationProvider.cs b/src/Configuration/Config.EnvironmentVariables/src/EnvironmentVariablesConfigurationProvider.cs index 3089093ff16..108c639486a 100644 --- a/src/Configuration/Config.EnvironmentVariables/src/EnvironmentVariablesConfigurationProvider.cs +++ b/src/Configuration/Config.EnvironmentVariables/src/EnvironmentVariablesConfigurationProvider.cs @@ -48,7 +48,7 @@ public override void Load() internal void Load(IDictionary envVariables) { - Data = new Dictionary(StringComparer.OrdinalIgnoreCase); + var data = new Dictionary(StringComparer.OrdinalIgnoreCase); var filteredEnvVariables = envVariables .Cast() @@ -58,8 +58,10 @@ internal void Load(IDictionary envVariables) foreach (var envVariable in filteredEnvVariables) { var key = ((string)envVariable.Key).Substring(_prefix.Length); - Data[key] = (string)envVariable.Value; + data[key] = (string)envVariable.Value; } + + Data = data; } private static string NormalizeKey(string key) diff --git a/src/Configuration/Config.EnvironmentVariables/test/EnvironmentVariablesTest.cs b/src/Configuration/Config.EnvironmentVariables/test/EnvironmentVariablesTest.cs index ca846f3d0e6..72504a552ad 100644 --- a/src/Configuration/Config.EnvironmentVariables/test/EnvironmentVariablesTest.cs +++ b/src/Configuration/Config.EnvironmentVariables/test/EnvironmentVariablesTest.cs @@ -3,6 +3,9 @@ using System; using System.Collections; +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; using Microsoft.Extensions.Configuration.Test; using Xunit; @@ -149,5 +152,48 @@ public void ReplaceDoubleUnderscoreInEnvironmentVariables() Assert.Equal("connection", envConfigSrc.Get("data:ConnectionString")); Assert.Equal("System.Data.SqlClient", envConfigSrc.Get("ConnectionStrings:_db1_ProviderName")); } + + [Fact] + public void BindingDoesNotThrowIfReloadedDuringBinding() + { + var dic = new Dictionary + { + {"Number", "-2"}, + {"Text", "Foo"} + }; + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.AddInMemoryCollection(dic); + configurationBuilder.AddEnvironmentVariables(); + var config = configurationBuilder.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(); + } + } + + Assert.Equal(-2, options.Number); + Assert.Equal("Foo", options.Text); + } + + private sealed class MyOptions + { + public int Number { get; set; } + public string Text { get; set; } + } } } diff --git a/src/Configuration/Config.KeyPerFile/src/KeyPerFileConfigurationProvider.cs b/src/Configuration/Config.KeyPerFile/src/KeyPerFileConfigurationProvider.cs index 47488957441..6e4234ecf3c 100644 --- a/src/Configuration/Config.KeyPerFile/src/KeyPerFileConfigurationProvider.cs +++ b/src/Configuration/Config.KeyPerFile/src/KeyPerFileConfigurationProvider.cs @@ -31,12 +31,13 @@ private static string TrimNewLine(string value) /// public override void Load() { - Data = new Dictionary(StringComparer.OrdinalIgnoreCase); + var data = new Dictionary(StringComparer.OrdinalIgnoreCase); if (Source.FileProvider == null) { if (Source.Optional) { + Data = data; return; } else @@ -63,10 +64,12 @@ public override void Load() { 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; } } } diff --git a/src/Configuration/Config.KeyPerFile/test/KeyPerFileTests.cs b/src/Configuration/Config.KeyPerFile/test/KeyPerFileTests.cs index d409c0eab08..6f6075f74ab 100644 --- a/src/Configuration/Config.KeyPerFile/test/KeyPerFileTests.cs +++ b/src/Configuration/Config.KeyPerFile/test/KeyPerFileTests.cs @@ -4,6 +4,8 @@ using System.IO; using System.Linq; using System.Text; +using System.Threading; +using System.Threading.Tasks; using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.Primitives; using Xunit; @@ -177,6 +179,47 @@ public void CanUnIgnoreDefaultFiles() 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(); + } + } + + 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 diff --git a/src/Configuration/test/Config.FunctionalTests/ConfigurationTests.cs b/src/Configuration/test/Config.FunctionalTests/ConfigurationTests.cs index f7bdb78c0c0..1b7d8ce4372 100644 --- a/src/Configuration/test/Config.FunctionalTests/ConfigurationTests.cs +++ b/src/Configuration/test/Config.FunctionalTests/ConfigurationTests.cs @@ -5,9 +5,12 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Threading; using System.Threading.Tasks; +using Microsoft.AspNetCore.Testing.xunit; using Microsoft.Extensions.Configuration.Ini; using Microsoft.Extensions.Configuration.Json; +using Microsoft.Extensions.Configuration.UserSecrets; using Microsoft.Extensions.Configuration.Xml; using Microsoft.Extensions.FileProviders; using Microsoft.Extensions.Primitives; @@ -866,6 +869,79 @@ public void CanEnumerateProviders() Assert.NotNull(providers.Single(p => p is IniConfigurationProvider)); } + [ConditionalFact] + [OSSkipCondition(OperatingSystems.Linux, SkipReason = "File watching is flaky on non windows.")] + [OSSkipCondition(OperatingSystems.MacOSX, SkipReason = "File watching is flaky on non windows.")] + public async Task TouchingFileWillReloadForUserSecrets() + { + string userSecretsId = "Test"; + var userSecretsPath = PathHelper.GetSecretsPathFromSecretsId(userSecretsId); + var userSecretsFolder = Path.GetDirectoryName(userSecretsPath); + + _fileSystem.CreateFolder(userSecretsFolder); + _fileSystem.WriteFile(userSecretsPath, @"{""UserSecretKey1"": ""UserSecretValue1""}"); + + var config = CreateBuilder() + .AddUserSecrets(userSecretsId, reloadOnChange: true) + .Build(); + + Assert.Equal("UserSecretValue1", config["UserSecretKey1"]); + + var token = config.GetReloadToken(); + + // Update file + _fileSystem.WriteFile(userSecretsPath, @"{""UserSecretKey1"": ""UserSecretValue2""}"); + + await WaitForChange( + () => config["UserSecretKey1"] == "UserSecretValue2", + "Reload failed after create-delete-create."); + + Assert.Equal("UserSecretValue2", config["UserSecretKey1"]); + Assert.True(token.HasChanged); + } + + [Fact] + public void BindingDoesNotThrowIfReloadedDuringBinding() + { + WriteTestFiles(); + + var configurationBuilder = CreateBuilder(); + configurationBuilder.Add(new TestIniSourceProvider(_iniFile)); + configurationBuilder.Add(new TestJsonSourceProvider(_jsonFile)); + configurationBuilder.Add(new TestXmlSourceProvider(_xmlFile)); + configurationBuilder.AddEnvironmentVariables(); + configurationBuilder.AddCommandLine(new[] { "--CmdKey1=CmdValue1" }); + configurationBuilder.AddInMemoryCollection(_memConfigContent); + + var config = configurationBuilder.Build(); + + using (var cts = new CancellationTokenSource(TimeSpan.FromMilliseconds(250))) + { + void ReloadLoop() + { + while (!cts.IsCancellationRequested) + { + config.Reload(); + } + } + + _ = Task.Run(ReloadLoop); + + MyOptions options = null; + + while (!cts.IsCancellationRequested) + { + options = config.Get(); + } + + Assert.Equal("CmdValue1", options.CmdKey1); + Assert.Equal("IniValue1", options.IniKey1); + Assert.Equal("JsonValue1", options.JsonKey1); + Assert.Equal("MemValue1", options.MemKey1); + Assert.Equal("XmlValue1", options.XmlKey1); + } + } + public void Dispose() { _fileProvider.Dispose(); @@ -888,5 +964,18 @@ public void Dispose() await Task.Delay(_msDelay); } } + + private sealed class MyOptions + { + public string CmdKey1 { get; set; } + + public string IniKey1 { get; set; } + + public string JsonKey1 { get; set; } + + public string MemKey1 { get; set; } + + public string XmlKey1 { get; set; } + } } } diff --git a/src/Configuration/test/Config.FunctionalTests/Microsoft.Extensions.Configuration.FunctionalTests.csproj b/src/Configuration/test/Config.FunctionalTests/Microsoft.Extensions.Configuration.FunctionalTests.csproj index 3dcfb321a73..b6a4c963e80 100644 --- a/src/Configuration/test/Config.FunctionalTests/Microsoft.Extensions.Configuration.FunctionalTests.csproj +++ b/src/Configuration/test/Config.FunctionalTests/Microsoft.Extensions.Configuration.FunctionalTests.csproj @@ -5,6 +5,13 @@ + + + + + + +