diff --git a/src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHostBuilder.cs b/src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHostBuilder.cs
index 0a60896f3e..f81607bc90 100644
--- a/src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHostBuilder.cs
+++ b/src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHostBuilder.cs
@@ -52,13 +52,11 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Hosting
// Private right now because we don't have much reason to expose it. This can be exposed
// in the future if we want to give people a choice between CreateDefault and something
// less opinionated.
- Configuration = new ConfigurationBuilder();
+ Configuration = new WebAssemblyHostConfiguration();
RootComponents = new RootComponentMappingCollection();
Services = new ServiceCollection();
Logging = new LoggingBuilder(Services);
- Logging.SetMinimumLevel(LogLevel.Warning);
-
// Retrieve required attributes from JSRuntimeInvoker
InitializeNavigationManager(jsRuntimeInvoker);
InitializeDefaultServices();
@@ -111,10 +109,10 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Hosting
}
///
- /// Gets an that can be used to customize the application's
- /// configuration sources.
+ /// Gets an that can be used to customize the application's
+ /// configuration sources and read configuration attributes.
///
- public IConfigurationBuilder Configuration { get; }
+ public WebAssemblyHostConfiguration Configuration { get; }
///
/// Gets the collection of root component mappings configured for the application.
@@ -177,8 +175,7 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Hosting
public WebAssemblyHost Build()
{
// Intentionally overwrite configuration with the one we're creating.
- var configuration = Configuration.Build();
- Services.AddSingleton(configuration);
+ Services.AddSingleton(Configuration);
// A Blazor application always runs in a scope. Since we want to make it possible for the user
// to configure services inside *that scope* inside their startup code, we create *both* the
@@ -186,7 +183,7 @@ namespace Microsoft.AspNetCore.Components.WebAssembly.Hosting
var services = _createServiceProvider();
var scope = services.GetRequiredService().CreateScope();
- return new WebAssemblyHost(services, scope, configuration, RootComponents.ToArray());
+ return new WebAssemblyHost(services, scope, Configuration, RootComponents.ToArray());
}
internal void InitializeDefaultServices()
diff --git a/src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHostConfiguration.cs b/src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHostConfiguration.cs
new file mode 100644
index 0000000000..07c23e6a6d
--- /dev/null
+++ b/src/Components/WebAssembly/WebAssembly/src/Hosting/WebAssemblyHostConfiguration.cs
@@ -0,0 +1,188 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Linq;
+using System.Threading;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Primitives;
+
+namespace Microsoft.AspNetCore.Components.WebAssembly.Hosting
+{
+ ///
+ /// WebAssemblyHostConfiguration is a class that implements the interface of an IConfiguration,
+ /// IConfigurationRoot, and IConfigurationBuilder. It can be used to simulatneously build
+ /// and read from a configuration object.
+ ///
+ public class WebAssemblyHostConfiguration : IConfiguration, IConfigurationRoot, IConfigurationBuilder
+ {
+ private readonly List _providers = new List();
+ private readonly List _sources = new List();
+
+ private readonly List _changeTokenRegistrations = new List();
+ private ConfigurationReloadToken _changeToken = new ConfigurationReloadToken();
+
+ ///
+ /// Gets the sources used to obtain configuration values.
+ ///
+ IList IConfigurationBuilder.Sources => new ReadOnlyCollection(_sources.ToList());
+
+ ///
+ /// Gets the providers used to obtain configuration values.
+ ///
+ IEnumerable IConfigurationRoot.Providers => new ReadOnlyCollection(_providers.ToList());
+
+ ///
+ /// Gets a key/value collection that can be used to share data between the
+ /// and the registered instances.
+ ///
+ // In this implementation, this largely exists as a way to satisfy the
+ // requirements of the IConfigurationBuilder and is not populated by
+ // the WebAssemblyHostConfiguration with any meaningful info.
+ IDictionary IConfigurationBuilder.Properties { get; } = new Dictionary();
+
+ ///
+ public string this[string key]
+ {
+ get
+ {
+ // Iterate through the providers in reverse to extract
+ // the value from the most recently inserted provider.
+ for (var i = _providers.Count - 1; i >= 0; i--)
+ {
+ var provider = _providers[i];
+
+ if (provider.TryGet(key, out var value))
+ {
+ return value;
+ }
+ }
+
+ return null;
+ }
+ set
+ {
+ if (_providers.Count == 0)
+ {
+ throw new InvalidOperationException("Can only set property if at least one provider has been inserted.");
+ }
+
+ foreach (var provider in _providers)
+ {
+ provider.Set(key, value);
+ }
+
+ }
+ }
+
+ ///
+ /// Gets a configuration sub-section with the specified key.
+ ///
+ /// The key of the configuration section.
+ /// The .
+ ///
+ /// This method will never return null. If no matching sub-section is found with the specified key,
+ /// an empty will be returned.
+ ///
+ public IConfigurationSection GetSection(string key) => new ConfigurationSection(this, key);
+
+ ///
+ /// Gets the immediate descendant configuration sub-sections.
+ ///
+ /// The configuration sub-sections.
+ IEnumerable IConfiguration.GetChildren()
+ {
+ return _providers
+ .SelectMany(s => s.GetChildKeys(Enumerable.Empty(), null))
+ .Distinct(StringComparer.OrdinalIgnoreCase)
+ .Select(key => this.GetSection(key))
+ .ToList();
+ }
+
+ ///
+ /// Returns a that can be used to observe when this configuration is reloaded.
+ ///
+ /// The .
+ public IChangeToken GetReloadToken() => _changeToken;
+
+ ///
+ /// Force the configuration values to be reloaded from the underlying sources.
+ ///
+ public void Reload()
+ {
+ foreach (var provider in _providers)
+ {
+ provider.Load();
+ }
+ RaiseChanged();
+ }
+
+ private void RaiseChanged()
+ {
+ var previousToken = Interlocked.Exchange(ref _changeToken, new ConfigurationReloadToken());
+ previousToken.OnReload();
+ }
+
+ ///
+ /// Adds a new configuration source, retrieves the provider for the source, and
+ /// adds a change listener that triggers a reload of the provider whenever a change
+ /// is detected.
+ ///
+ /// The configuration source to add.
+ /// The same .
+ public IConfigurationBuilder Add(IConfigurationSource source)
+ {
+ if (source == null)
+ {
+ throw new ArgumentNullException(nameof(source));
+ }
+
+ // Ads this source and its associated provider to the source
+ // and provider references in this class. We make sure to load
+ // the data from the provider so that values are properly initialized.
+ _sources.Add(source);
+ var provider = source.Build(this);
+ provider.Load();
+
+ // Add a handler that will detect when the the configuration
+ // provider has reloaded data. This will invoke the RaiseChanged
+ // method which maps changes in individual providers to the change
+ // token on the WebAssemblyHostConfiguration object.
+ _changeTokenRegistrations.Add(ChangeToken.OnChange(() => provider.GetReloadToken(), () => RaiseChanged()));
+
+ // We keep a list of providers in this class so that we can map
+ // set and get methods on this class to the set and get methods
+ // on the individual configuration providers.
+ _providers.Add(provider);
+ return this;
+ }
+
+ ///
+ /// Builds an with keys and values from the set of providers registered in
+ /// .
+ ///
+ /// An with keys and values from the registered providers.
+ public IConfigurationRoot Build()
+ {
+ return this;
+ }
+
+ ///
+ public void Dispose()
+ {
+ // dispose change token registrations
+ foreach (var registration in _changeTokenRegistrations)
+ {
+ registration.Dispose();
+ }
+
+ // dispose providers
+ foreach (var provider in _providers)
+ {
+ (provider as IDisposable)?.Dispose();
+ }
+ }
+ }
+}
diff --git a/src/Components/WebAssembly/WebAssembly/test/Hosting/WebAssemblyHostConfigurationTest.cs b/src/Components/WebAssembly/WebAssembly/test/Hosting/WebAssemblyHostConfigurationTest.cs
new file mode 100644
index 0000000000..922293fa0a
--- /dev/null
+++ b/src/Components/WebAssembly/WebAssembly/test/Hosting/WebAssemblyHostConfigurationTest.cs
@@ -0,0 +1,230 @@
+// Copyright (c) .NET Foundation. All rights reserved.
+// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using Xunit;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Configuration.Memory;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Microsoft.AspNetCore.Components.WebAssembly.Hosting
+{
+ public class WebAssemblyHostConfigurationTest
+ {
+ [Fact]
+ public void CanSetAndGetConfigurationValue()
+ {
+ // Arrange
+ var initialData = new Dictionary() {
+ { "color", "blue" },
+ { "type", "car" },
+ { "wheels:year", "2008" },
+ { "wheels:count", "4" },
+ { "wheels:brand", "michelin" },
+ { "wheels:brand:type", "rally" },
+ };
+ var memoryConfig = new MemoryConfigurationSource { InitialData = initialData };
+ var configuration = new WebAssemblyHostConfiguration();
+
+ // Act
+ configuration.Add(memoryConfig);
+ configuration["type"] = "car";
+ configuration["wheels:count"] = "6";
+
+ // Assert
+ Assert.Equal("car", configuration["type"]);
+ Assert.Equal("blue", configuration["color"]);
+ Assert.Equal("6", configuration["wheels:count"]);
+ }
+
+ [Fact]
+ public void SettingValueUpdatesAllProviders()
+ {
+ // Arrange
+ var initialData = new Dictionary() { { "color", "blue" } };
+ var source1 = new MemoryConfigurationSource { InitialData = initialData };
+ var source2 = new CustomizedTestConfigurationSource();
+ var configuration = new WebAssemblyHostConfiguration();
+
+ // Act
+ configuration.Add(source1);
+ configuration.Add(source2);
+ configuration["type"] = "car";
+
+ // Assert
+ Assert.Equal("car", configuration["type"]);
+ IConfigurationRoot root = configuration;
+ Assert.All(root.Providers, provider =>
+ {
+ provider.TryGet("type", out var value);
+ Assert.Equal("car", value);
+ });
+ }
+
+ [Fact]
+ public void CanGetChildren()
+ {
+ // Arrange
+ var initialData = new Dictionary() { { "color", "blue" } };
+ var memoryConfig = new MemoryConfigurationSource { InitialData = initialData };
+ var configuration = new WebAssemblyHostConfiguration();
+
+ // Act
+ configuration.Add(memoryConfig);
+ IConfiguration readableConfig = configuration;
+ var children = readableConfig.GetChildren();
+
+ // Assert
+ Assert.NotNull(children);
+ Assert.NotEmpty(children);
+ }
+
+ [Fact]
+ public void CanGetSection()
+ {
+ // Arrange
+ var initialData = new Dictionary() {
+ { "color", "blue" },
+ { "type", "car" },
+ { "wheels:year", "2008" },
+ { "wheels:count", "4" },
+ { "wheels:brand", "michelin" },
+ { "wheels:brand:type", "rally" },
+ };
+ var memoryConfig = new MemoryConfigurationSource { InitialData = initialData };
+ var configuration = new WebAssemblyHostConfiguration();
+
+ // Act
+ configuration.Add(memoryConfig);
+ var section = configuration.GetSection("wheels").AsEnumerable(makePathsRelative: true).ToDictionary(k => k.Key, v => v.Value);
+
+ // Assert
+ Assert.Equal(4, section.Count);
+ Assert.Equal("2008", section["year"]);
+ Assert.Equal("4", section["count"]);
+ Assert.Equal("michelin", section["brand"]);
+ Assert.Equal("rally", section["brand:type"]);
+ }
+
+ [Fact]
+ public void CanDisposeProviders()
+ {
+ // Arrange
+ var initialData = new Dictionary() { { "color", "blue" } };
+ var memoryConfig = new MemoryConfigurationSource { InitialData = initialData };
+ var configuration = new WebAssemblyHostConfiguration();
+
+ // Act
+ configuration.Add(memoryConfig);
+ Assert.Equal("blue", configuration["color"]);
+ var exception = Record.Exception(() => configuration.Dispose());
+
+ // Assert
+ Assert.Null(exception);
+ }
+
+ [Fact]
+ public void CanSupportDeeplyNestedConfigs()
+ {
+ // Arrange
+ var dic1 = new Dictionary()
+ {
+ {"Mem1", "Value1"},
+ {"Mem1:", "NoKeyValue1"},
+ {"Mem1:KeyInMem1", "ValueInMem1"},
+ {"Mem1:KeyInMem1:Deep1", "ValueDeep1"}
+ };
+ var dic2 = new Dictionary()
+ {
+ {"Mem2", "Value2"},
+ {"Mem2:", "NoKeyValue2"},
+ {"Mem2:KeyInMem2", "ValueInMem2"},
+ {"Mem2:KeyInMem2:Deep2", "ValueDeep2"}
+ };
+ var dic3 = new Dictionary()
+ {
+ {"Mem3", "Value3"},
+ {"Mem3:", "NoKeyValue3"},
+ {"Mem3:KeyInMem3", "ValueInMem3"},
+ {"Mem3:KeyInMem4", "ValueInMem4"},
+ {"Mem3:KeyInMem3:Deep3", "ValueDeep3"},
+ {"Mem3:KeyInMem3:Deep4", "ValueDeep4"}
+ };
+ var memConfigSrc1 = new MemoryConfigurationSource { InitialData = dic1 };
+ var memConfigSrc2 = new MemoryConfigurationSource { InitialData = dic2 };
+ var memConfigSrc3 = new MemoryConfigurationSource { InitialData = dic3 };
+ var configuration = new WebAssemblyHostConfiguration();
+
+ // Act
+ configuration.Add(memConfigSrc1);
+ configuration.Add(memConfigSrc2);
+ configuration.Add(memConfigSrc3);
+
+ // Assert
+ var dict = configuration.GetSection("Mem1").AsEnumerable(makePathsRelative: true).ToDictionary(k => k.Key, v => v.Value);
+ Assert.Equal(3, dict.Count);
+ Assert.Equal("NoKeyValue1", dict[""]);
+ Assert.Equal("ValueInMem1", dict["KeyInMem1"]);
+ Assert.Equal("ValueDeep1", dict["KeyInMem1:Deep1"]);
+
+ var dict2 = configuration.GetSection("Mem2").AsEnumerable(makePathsRelative: true).ToDictionary(k => k.Key, v => v.Value);
+ Assert.Equal(3, dict2.Count);
+ Assert.Equal("NoKeyValue2", dict2[""]);
+ Assert.Equal("ValueInMem2", dict2["KeyInMem2"]);
+ Assert.Equal("ValueDeep2", dict2["KeyInMem2:Deep2"]);
+
+ var dict3 = configuration.GetSection("Mem3").AsEnumerable(makePathsRelative: true).ToDictionary(k => k.Key, v => v.Value);
+ Assert.Equal(5, dict3.Count);
+ Assert.Equal("NoKeyValue3", dict3[""]);
+ Assert.Equal("ValueInMem3", dict3["KeyInMem3"]);
+ Assert.Equal("ValueInMem4", dict3["KeyInMem4"]);
+ Assert.Equal("ValueDeep3", dict3["KeyInMem3:Deep3"]);
+ Assert.Equal("ValueDeep4", dict3["KeyInMem3:Deep4"]);
+ }
+
+ [Fact]
+ public void NewConfigurationProviderOverridesOldOneWhenKeyIsDuplicated()
+ {
+ // Arrange
+ var dic1 = new Dictionary()
+ {
+ {"Key1:Key2", "ValueInMem1"}
+ };
+ var dic2 = new Dictionary()
+ {
+ {"Key1:Key2", "ValueInMem2"}
+ };
+ var memConfigSrc1 = new MemoryConfigurationSource { InitialData = dic1 };
+ var memConfigSrc2 = new MemoryConfigurationSource { InitialData = dic2 };
+
+ var configuration = new WebAssemblyHostConfiguration();
+
+ // Act
+ configuration.Add(memConfigSrc1);
+ configuration.Add(memConfigSrc2);
+
+ // Assert
+ Assert.Equal("ValueInMem2", configuration["Key1:Key2"]);
+ }
+
+ private class CustomizedTestConfigurationProvider : ConfigurationProvider
+ {
+ public CustomizedTestConfigurationProvider(string key, string value)
+ => Data.Add(key, value.ToUpper());
+
+ public override void Set(string key, string value)
+ {
+ Data[key] = value;
+ }
+ }
+
+ private class CustomizedTestConfigurationSource : IConfigurationSource
+ {
+ public IConfigurationProvider Build(IConfigurationBuilder builder)
+ {
+ return new CustomizedTestConfigurationProvider("initialKey", "initialValue");
+ }
+ }
+ }
+}
diff --git a/src/Components/test/E2ETest/Tests/WebAssemblyConfigurationTest.cs b/src/Components/test/E2ETest/Tests/WebAssemblyConfigurationTest.cs
index 23fb14ed10..3dc2a0a11c 100644
--- a/src/Components/test/E2ETest/Tests/WebAssemblyConfigurationTest.cs
+++ b/src/Components/test/E2ETest/Tests/WebAssemblyConfigurationTest.cs
@@ -47,6 +47,22 @@ namespace Microsoft.AspNetCore.Components.E2ETest.Tests
Assert.Equal("Development key3-value", _appElement.FindElement(By.Id("key3")).Text);
}
+ [Fact]
+ public void WebAssemblyConfiguration_ReloadingWorks()
+ {
+ // Verify values from the default 'appsettings.json' are read.
+ Browser.Equal("Default key1-value", () => _appElement.FindElement(By.Id("key1")).Text);
+
+ // Change the value of key1 using the form in the UI
+ var input = _appElement.FindElement(By.Id("key1-input"));
+ input.SendKeys("newValue");
+ var submit = _appElement.FindElement(By.Id("trigger-change"));
+ submit.Click();
+
+ // Asser that the value of the key has been updated
+ Browser.Equal("newValue", () => _appElement.FindElement(By.Id("key1")).Text);
+ }
+
[Fact]
public void WebAssemblyHostingEnvironment_Works()
{
diff --git a/src/Components/test/testassets/BasicTestApp/BasicTestApp.csproj b/src/Components/test/testassets/BasicTestApp/BasicTestApp.csproj
index f6c755d21c..56b7245ae7 100644
--- a/src/Components/test/testassets/BasicTestApp/BasicTestApp.csproj
+++ b/src/Components/test/testassets/BasicTestApp/BasicTestApp.csproj
@@ -18,6 +18,7 @@
+
diff --git a/src/Components/test/testassets/BasicTestApp/ConfigurationComponent.razor b/src/Components/test/testassets/BasicTestApp/ConfigurationComponent.razor
index 5f40b677ca..13b95fb037 100644
--- a/src/Components/test/testassets/BasicTestApp/ConfigurationComponent.razor
+++ b/src/Components/test/testassets/BasicTestApp/ConfigurationComponent.razor
@@ -8,3 +8,18 @@