Enable reading and editing from same Configuration object (#20647)

This commit is contained in:
Safia Abdalla 2020-04-10 09:59:05 -07:00 committed by GitHub
parent 43bd9dbf3e
commit c4703acfa7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 469 additions and 12 deletions

View File

@ -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
}
/// <summary>
/// Gets an <see cref="IConfigurationBuilder"/> that can be used to customize the application's
/// configuration sources.
/// Gets an <see cref="WebAssemblyHostConfiguration"/> that can be used to customize the application's
/// configuration sources and read configuration attributes.
/// </summary>
public IConfigurationBuilder Configuration { get; }
public WebAssemblyHostConfiguration Configuration { get; }
/// <summary>
/// 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<IConfiguration>(configuration);
Services.AddSingleton<IConfiguration>(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<IServiceScopeFactory>().CreateScope();
return new WebAssemblyHost(services, scope, configuration, RootComponents.ToArray());
return new WebAssemblyHost(services, scope, Configuration, RootComponents.ToArray());
}
internal void InitializeDefaultServices()

View File

@ -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
{
/// <summary>
/// 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.
/// </summary>
public class WebAssemblyHostConfiguration : IConfiguration, IConfigurationRoot, IConfigurationBuilder
{
private readonly List<IConfigurationProvider> _providers = new List<IConfigurationProvider>();
private readonly List<IConfigurationSource> _sources = new List<IConfigurationSource>();
private readonly List<IDisposable> _changeTokenRegistrations = new List<IDisposable>();
private ConfigurationReloadToken _changeToken = new ConfigurationReloadToken();
/// <summary>
/// Gets the sources used to obtain configuration values.
/// </summary>
IList<IConfigurationSource> IConfigurationBuilder.Sources => new ReadOnlyCollection<IConfigurationSource>(_sources.ToList());
/// <summary>
/// Gets the providers used to obtain configuration values.
/// </summary>
IEnumerable<IConfigurationProvider> IConfigurationRoot.Providers => new ReadOnlyCollection<IConfigurationProvider>(_providers.ToList());
/// <summary>
/// Gets a key/value collection that can be used to share data between the <see cref="IConfigurationBuilder"/>
/// and the registered <see cref="IConfigurationProvider"/> instances.
/// </summary>
// 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<string, object> IConfigurationBuilder.Properties { get; } = new Dictionary<string, object>();
/// <inheritdoc />
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);
}
}
}
/// <summary>
/// Gets a configuration sub-section with the specified key.
/// </summary>
/// <param name="key">The key of the configuration section.</param>
/// <returns>The <see cref="IConfigurationSection"/>.</returns>
/// <remarks>
/// This method will never return <c>null</c>. If no matching sub-section is found with the specified key,
/// an empty <see cref="IConfigurationSection"/> will be returned.
/// </remarks>
public IConfigurationSection GetSection(string key) => new ConfigurationSection(this, key);
/// <summary>
/// Gets the immediate descendant configuration sub-sections.
/// </summary>
/// <returns>The configuration sub-sections.</returns>
IEnumerable<IConfigurationSection> IConfiguration.GetChildren()
{
return _providers
.SelectMany(s => s.GetChildKeys(Enumerable.Empty<string>(), null))
.Distinct(StringComparer.OrdinalIgnoreCase)
.Select(key => this.GetSection(key))
.ToList();
}
/// <summary>
/// Returns a <see cref="IChangeToken"/> that can be used to observe when this configuration is reloaded.
/// </summary>
/// <returns>The <see cref="IChangeToken"/>.</returns>
public IChangeToken GetReloadToken() => _changeToken;
/// <summary>
/// Force the configuration values to be reloaded from the underlying sources.
/// </summary>
public void Reload()
{
foreach (var provider in _providers)
{
provider.Load();
}
RaiseChanged();
}
private void RaiseChanged()
{
var previousToken = Interlocked.Exchange(ref _changeToken, new ConfigurationReloadToken());
previousToken.OnReload();
}
/// <summary>
/// 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.
/// </summary>
/// <param name="source">The configuration source to add.</param>
/// <returns>The same <see cref="IConfigurationBuilder"/>.</returns>
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;
}
/// <summary>
/// Builds an <see cref="IConfiguration"/> with keys and values from the set of providers registered in
/// <see cref="Providers"/>.
/// </summary>
/// <returns>An <see cref="IConfigurationRoot"/> with keys and values from the registered providers.</returns>
public IConfigurationRoot Build()
{
return this;
}
/// <inheritdoc />
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();
}
}
}
}

View File

@ -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<string, string>() {
{ "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<string, string>() { { "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<string, string>() { { "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<string, string>() {
{ "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<string, string>() { { "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<string, string>()
{
{"Mem1", "Value1"},
{"Mem1:", "NoKeyValue1"},
{"Mem1:KeyInMem1", "ValueInMem1"},
{"Mem1:KeyInMem1:Deep1", "ValueDeep1"}
};
var dic2 = new Dictionary<string, string>()
{
{"Mem2", "Value2"},
{"Mem2:", "NoKeyValue2"},
{"Mem2:KeyInMem2", "ValueInMem2"},
{"Mem2:KeyInMem2:Deep2", "ValueDeep2"}
};
var dic3 = new Dictionary<string, string>()
{
{"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<string, string>()
{
{"Key1:Key2", "ValueInMem1"}
};
var dic2 = new Dictionary<string, string>()
{
{"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");
}
}
}
}

View File

@ -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()
{

View File

@ -18,6 +18,7 @@
<Reference Include="Microsoft.AspNetCore.Components.WebAssembly" />
<Reference Include="Microsoft.AspNetCore.Components.Authorization" />
<Reference Include="Microsoft.AspNetCore.Components.DataAnnotations.Validation" />
<Reference Include="Microsoft.Extensions.Logging.Configuration" />
</ItemGroup>
<ItemGroup>

View File

@ -8,3 +8,18 @@
</ul>
<div id="environment">@HostEnvironment.Environment</div>
<p>
<input id="key1-input" @bind-value=newKey1 @bind-value:event="oninput" />
<button id="trigger-change" @onclick="@(() => TriggerChange())">Change key1</button>
</p>
@code {
string newKey1 { get; set; }
void TriggerChange()
{
Config["key1"] = newKey1;
}
}

View File

@ -16,6 +16,7 @@ using Microsoft.AspNetCore.Components.WebAssembly.Http;
using Microsoft.AspNetCore.Components.WebAssembly.Services;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Configuration;
using Microsoft.JSInterop;
namespace BasicTestApp
@ -45,8 +46,9 @@ namespace BasicTestApp
policy.RequireAssertion(ctx => ctx.User.Identity.Name?.StartsWith("B") ?? false));
});
builder.Logging.Services.AddSingleton<ILoggerProvider, PrependMessageLoggerProvider>(s => new PrependMessageLoggerProvider("Custom logger", s.GetService<IJSRuntime>()));
builder.Logging.SetMinimumLevel(LogLevel.Information);
builder.Logging.Services.AddSingleton<ILoggerProvider, PrependMessageLoggerProvider>(s =>
new PrependMessageLoggerProvider(builder.Configuration["Logging:PrependMessage:Message"], s.GetService<IJSRuntime>()));
builder.Logging.AddConfiguration(builder.Configuration);
var host = builder.Build();
ConfigureCulture(host);

View File

@ -1,4 +1,12 @@
{
"key1": "Default key1-value",
"key2": "Default key2-value"
"key2": "Default key2-value",
"Logging": {
"PrependMessage": {
"Message": "Custom logger",
"LogLevel": {
"Default": "Information"
}
}
}
}