diff --git a/src/DefaultBuilder/src/BootstrapHostBuilder.cs b/src/DefaultBuilder/src/BootstrapHostBuilder.cs index 563dba7907a4..6a7f41eea64f 100644 --- a/src/DefaultBuilder/src/BootstrapHostBuilder.cs +++ b/src/DefaultBuilder/src/BootstrapHostBuilder.cs @@ -13,11 +13,13 @@ namespace Microsoft.AspNetCore.Hosting // This exists solely to bootstrap the configuration internal class BootstrapHostBuilder : IHostBuilder { - public IDictionary Properties { get; } = new Dictionary(); private readonly HostBuilderContext _context; private readonly Configuration _configuration; private readonly WebHostEnvironment _environment; + private readonly List> _configureHostActions = new List>(); + private readonly List> _configureAppActions = new List>(); + public BootstrapHostBuilder(Configuration configuration, WebHostEnvironment webHostEnvironment) { _configuration = configuration; @@ -29,6 +31,8 @@ public BootstrapHostBuilder(Configuration configuration, WebHostEnvironment webH }; } + public IDictionary Properties { get; } = new Dictionary(); + public IHost Build() { // HostingHostBuilderExtensions.ConfigureDefaults should never call this. @@ -37,9 +41,7 @@ public IHost Build() public IHostBuilder ConfigureAppConfiguration(Action configureDelegate) { - configureDelegate(_context, _configuration); - _environment.ApplyConfigurationSettings(_configuration); - _configuration.ChangeBasePath(_environment.ContentRootPath); + _configureAppActions.Add(configureDelegate ?? throw new ArgumentNullException(nameof(configureDelegate))); return this; } @@ -52,9 +54,7 @@ public IHostBuilder ConfigureContainer(Action configureDelegate) { - configureDelegate(_configuration); - _environment.ApplyConfigurationSettings(_configuration); - _configuration.ChangeBasePath(_environment.ContentRootPath); + _configureHostActions.Add(configureDelegate ?? throw new ArgumentNullException(nameof(configureDelegate))); return this; } @@ -67,7 +67,7 @@ public IHostBuilder ConfigureServices(Action(IServiceProviderFactory factory) where TContainerBuilder : notnull { - // This is not called by HostingHostBuilderExtensions.ConfigureDefaults currently, but that chould change in the future. + // This is not called by HostingHostBuilderExtensions.ConfigureDefaults currently, but that could change in the future. // If this does get called in the future, it should be called again at a later stage on the ConfigureHostBuilder. return this; } @@ -78,5 +78,22 @@ public IHostBuilder UseServiceProviderFactory(Func - /// Configuration is mutable configuration object. It is both a configuration builder and an IConfigurationRoot. + /// Configuration is mutable configuration object. It is both an and an . /// As sources are added, it updates its current view of configuration. Once Build is called, configuration is frozen. /// - public sealed class Configuration : IConfigurationRoot, IConfigurationBuilder + public sealed class Configuration : IConfigurationRoot, IConfigurationBuilder, IDisposable { - private readonly ConfigurationBuilder _builder = new(); - private IConfigurationRoot _configuration; + private readonly ConfigurationSources _sources; + private readonly IDictionary _properties; + private ConfigurationRoot _configurationRoot; - /// - /// Gets or sets a configuration value. - /// - /// The configuration key. - /// The configuration value. - public string this[string key] { get => _configuration[key]; set => _configuration[key] = value; } + private ConfigurationReloadToken _changeToken = new(); + private IDisposable? _changeTokenRegistration; - /// - /// 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) - { - return _configuration.GetSection(key); - } + /// + public string this[string key] { get => _configurationRoot[key]; set => _configurationRoot[key] = value; } - /// - /// Gets the immediate descendant configuration sub-sections. - /// - /// The configuration sub-sections. - public IEnumerable GetChildren() => _configuration.GetChildren(); + /// + public IConfigurationSection GetSection(string key) => new ConfigurationSection(this, key); - IDictionary IConfigurationBuilder.Properties => _builder.Properties; + /// + public IEnumerable GetChildren() => GetChildrenImplementation(null); - // TODO: Handle modifications to Sources and keep the configuration root in sync - IList IConfigurationBuilder.Sources => Sources; + IDictionary IConfigurationBuilder.Properties => _properties; - internal IList Sources { get; } + IList IConfigurationBuilder.Sources => _sources; - IEnumerable IConfigurationRoot.Providers => _configuration.Providers; + IEnumerable IConfigurationRoot.Providers => _configurationRoot.Providers; /// - /// Creates a new . + /// Creates an empty mutable configuration object that is both an and an . /// public Configuration() { - _configuration = _builder.Build(); - - var sources = new ConfigurationSources(_builder.Sources, UpdateConfigurationRoot); + _properties = new ConfigurationProperties(this); + _sources = new ConfigurationSources(this); - Sources = sources; + // _configurationRoot is set by UpdateConfiguration() + _configurationRoot = default!; + UpdateConfiguration(); } - internal void ChangeBasePath(string path) + /// + public void Dispose() { - this.SetBasePath(path); - UpdateConfigurationRoot(); + _changeTokenRegistration?.Dispose(); + _configurationRoot?.Dispose(); } - internal void ChangeFileProvider(IFileProvider fileProvider) + IConfigurationBuilder IConfigurationBuilder.Add(IConfigurationSource source) { - this.SetFileProvider(fileProvider); - UpdateConfigurationRoot(); + _sources.Add(source ?? throw new ArgumentNullException(nameof(source))); + return this; } - private void UpdateConfigurationRoot() - { - var current = _configuration; - if (current is IDisposable disposable) - { - disposable.Dispose(); - } - _configuration = _builder.Build(); - } + IConfigurationRoot IConfigurationBuilder.Build() => BuildConfigurationRoot(); - IConfigurationBuilder IConfigurationBuilder.Add(IConfigurationSource source) + IChangeToken IConfiguration.GetReloadToken() => _changeToken; + + void IConfigurationRoot.Reload() => _configurationRoot.Reload(); + + private void UpdateConfiguration() { - Sources.Add(source); - return this; + var newConfiguration = BuildConfigurationRoot(); + var prevConfiguration = _configurationRoot; + + _configurationRoot = newConfiguration; + + _changeTokenRegistration?.Dispose(); + (prevConfiguration as IDisposable)?.Dispose(); + + _changeTokenRegistration = ChangeToken.OnChange(() => newConfiguration.GetReloadToken(), RaiseChanged); + RaiseChanged(); } - IConfigurationRoot IConfigurationBuilder.Build() + private ConfigurationRoot BuildConfigurationRoot() { - // No more modification is expected after this final build - UpdateConfigurationRoot(); - return this; + var providers = new List(); + foreach (var source in _sources) + { + var provider = source.Build(this); + providers.Add(provider); + } + return new ConfigurationRoot(providers); } - IChangeToken IConfiguration.GetReloadToken() + private void RaiseChanged() { - // REVIEW: Is this correct? - return _configuration.GetReloadToken(); + var previousToken = Interlocked.Exchange(ref _changeToken, new ConfigurationReloadToken()); + previousToken.OnReload(); } - void IConfigurationRoot.Reload() + /// + /// Gets the immediate children sub-sections of configuration root based on key. + /// + /// Key of a section of which children to retrieve. + /// Immediate children sub-sections of section specified by key. + private IEnumerable GetChildrenImplementation(string? path) { - _configuration.Reload(); + // From https://github.com/dotnet/runtime/blob/01b7e73cd378145264a7cb7a09365b41ed42b240/src/libraries/Microsoft.Extensions.Configuration/src/InternalConfigurationRootExtensions.cs + return _configurationRoot.Providers + .Aggregate(Enumerable.Empty(), + (seed, source) => source.GetChildKeys(seed, path)) + .Distinct(StringComparer.OrdinalIgnoreCase) + .Select(key => _configurationRoot.GetSection(path == null ? key : ConfigurationPath.Combine(path, key))); } - // On source modifications, we rebuild configuration private class ConfigurationSources : IList { private readonly IList _sources; - private readonly Action _sourcesModified; + private readonly Configuration _config; - public ConfigurationSources(IList sources, Action sourcesModified) + public ConfigurationSources(Configuration config) { - _sources = sources; - _sourcesModified = sourcesModified; + _sources = new List(); + _config = config; } public IConfigurationSource this[int index] @@ -132,7 +135,7 @@ public IConfigurationSource this[int index] set { _sources[index] = value; - _sourcesModified(); + _config.UpdateConfiguration(); } } @@ -143,13 +146,13 @@ public IConfigurationSource this[int index] public void Add(IConfigurationSource item) { _sources.Add(item); - _sourcesModified(); + _config.UpdateConfiguration(); } public void Clear() { _sources.Clear(); - _sourcesModified(); + _config.UpdateConfiguration(); } public bool Contains(IConfigurationSource item) @@ -175,20 +178,20 @@ public int IndexOf(IConfigurationSource item) public void Insert(int index, IConfigurationSource item) { _sources.Insert(index, item); - _sourcesModified(); + _config.UpdateConfiguration(); } public bool Remove(IConfigurationSource item) { var removed = _sources.Remove(item); - _sourcesModified(); + _config.UpdateConfiguration(); return removed; } public void RemoveAt(int index) { _sources.RemoveAt(index); - _sourcesModified(); + _config.UpdateConfiguration(); } IEnumerator IEnumerable.GetEnumerator() @@ -196,5 +199,77 @@ IEnumerator IEnumerable.GetEnumerator() return GetEnumerator(); } } + + private class ConfigurationProperties : IDictionary + { + private readonly IDictionary _properties = new Dictionary(); + private readonly Configuration _config; + + public ConfigurationProperties(Configuration config) + { + _config = config; + } + + public object this[string key] + { + get => _properties[key]; + set + { + _properties[key] = value; + _config.UpdateConfiguration(); + } + } + + public ICollection Keys => _properties.Keys; + + public ICollection Values => _properties.Values; + + public int Count => _properties.Count; + + public bool IsReadOnly => false; + + public void Add(string key, object value) + { + _properties.Add(key, value); + _config.UpdateConfiguration(); + } + + public void Add(KeyValuePair item) + { + _properties.Add(item); + _config.UpdateConfiguration(); + } + + public void Clear() + { + _properties.Clear(); + _config.UpdateConfiguration(); + } + + public bool Contains(KeyValuePair item) => _properties.Contains(item); + + public bool ContainsKey(string key) => _properties.ContainsKey(key); + + public void CopyTo(KeyValuePair[] array, int arrayIndex) => _properties.CopyTo(array, arrayIndex); + + public IEnumerator> GetEnumerator() => _properties.GetEnumerator(); + + public bool Remove(string key) + { + var removed = _properties.Remove(key); + _config.UpdateConfiguration(); + return removed; + } + + public bool Remove(KeyValuePair item) + { + var removed = _properties.Remove(item); + _config.UpdateConfiguration(); + return removed; + } + + public bool TryGetValue(string key, [MaybeNullWhen(false)] out object value) => _properties.TryGetValue(key, out value); + IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable)_properties).GetEnumerator(); + } } } diff --git a/src/DefaultBuilder/src/ConfigureHostBuilder.cs b/src/DefaultBuilder/src/ConfigureHostBuilder.cs index 73c9b7e03598..151d3b87061b 100644 --- a/src/DefaultBuilder/src/ConfigureHostBuilder.cs +++ b/src/DefaultBuilder/src/ConfigureHostBuilder.cs @@ -20,21 +20,27 @@ public sealed class ConfigureHostBuilder : IHostBuilder /// public IDictionary Properties { get; } = new Dictionary(); - internal Configuration Configuration => _configuration; - - private readonly IConfigurationBuilder _hostConfiguration = new ConfigurationBuilder(); - private readonly WebHostEnvironment _environment; private readonly Configuration _configuration; private readonly IServiceCollection _services; + private readonly HostBuilderContext _context; + internal ConfigureHostBuilder(Configuration configuration, WebHostEnvironment environment, IServiceCollection services) { _configuration = configuration; _environment = environment; _services = services; + + _context = new HostBuilderContext(Properties) + { + Configuration = _configuration, + HostingEnvironment = _environment + }; } + internal bool ConfigurationEnabled { get; set; } + IHost IHostBuilder.Build() { throw new NotSupportedException($"Call {nameof(WebApplicationBuilder)}.{nameof(WebApplicationBuilder.Build)}() instead."); @@ -43,7 +49,13 @@ IHost IHostBuilder.Build() /// public IHostBuilder ConfigureAppConfiguration(Action configureDelegate) { - _operations += b => b.ConfigureAppConfiguration(configureDelegate); + if (ConfigurationEnabled) + { + // Run these immediately so that they are observable by the imperative code + configureDelegate(_context, _configuration); + _environment.ApplyConfigurationSettings(_configuration); + } + return this; } @@ -57,13 +69,13 @@ public IHostBuilder ConfigureContainer(Action public IHostBuilder ConfigureHostConfiguration(Action configureDelegate) { - // HACK: We need to evaluate the host configuration as they are changes so that we have an accurate view of the world - configureDelegate(_hostConfiguration); - - _environment.ApplyConfigurationSettings(_hostConfiguration.Build()); - Configuration.ChangeFileProvider(_environment.ContentRootFileProvider); + if (ConfigurationEnabled) + { + // Run these immediately so that they are observable by the imperative code + configureDelegate(_configuration); + _environment.ApplyConfigurationSettings(_configuration); + } - _operations += b => b.ConfigureHostConfiguration(configureDelegate); return this; } @@ -71,12 +83,7 @@ public IHostBuilder ConfigureHostConfiguration(Action con public IHostBuilder ConfigureServices(Action configureDelegate) { // Run these immediately so that they are observable by the imperative code - configureDelegate(new HostBuilderContext(Properties) - { - Configuration = Configuration, - HostingEnvironment = _environment - }, - _services); + configureDelegate(_context, _services); return this; } diff --git a/src/DefaultBuilder/src/ConfigureWebHostBuilder.cs b/src/DefaultBuilder/src/ConfigureWebHostBuilder.cs index 5d95319e7738..043b590777c5 100644 --- a/src/DefaultBuilder/src/ConfigureWebHostBuilder.cs +++ b/src/DefaultBuilder/src/ConfigureWebHostBuilder.cs @@ -86,8 +86,6 @@ public IWebHostBuilder UseSetting(string key, string? value) { _environment.ContentRootPath = value; _environment.ResolveFileProviders(_configuration); - - _configuration.ChangeBasePath(value); } else if (string.Equals(key, WebHostDefaults.EnvironmentKey, StringComparison.OrdinalIgnoreCase)) { diff --git a/src/DefaultBuilder/src/PublicAPI.Unshipped.txt b/src/DefaultBuilder/src/PublicAPI.Unshipped.txt index 3c537d7e886b..47d57f8126c4 100644 --- a/src/DefaultBuilder/src/PublicAPI.Unshipped.txt +++ b/src/DefaultBuilder/src/PublicAPI.Unshipped.txt @@ -1,6 +1,7 @@ #nullable enable Microsoft.AspNetCore.Builder.Configuration Microsoft.AspNetCore.Builder.Configuration.Configuration() -> void +Microsoft.AspNetCore.Builder.Configuration.Dispose() -> void Microsoft.AspNetCore.Builder.Configuration.GetChildren() -> System.Collections.Generic.IEnumerable! Microsoft.AspNetCore.Builder.Configuration.GetSection(string! key) -> Microsoft.Extensions.Configuration.IConfigurationSection! Microsoft.AspNetCore.Builder.Configuration.this[string! key].get -> string! diff --git a/src/DefaultBuilder/src/WebApplicationBuilder.cs b/src/DefaultBuilder/src/WebApplicationBuilder.cs index 3be41cf94061..d4f021064ab1 100644 --- a/src/DefaultBuilder/src/WebApplicationBuilder.cs +++ b/src/DefaultBuilder/src/WebApplicationBuilder.cs @@ -28,6 +28,8 @@ internal WebApplicationBuilder(Assembly? callingAssembly, string[]? args = null) // HACK: MVC and Identity do this horrible thing to get the hosting environment as an instance // from the service collection before it is built. That needs to be fixed... Environment = _environment = new WebHostEnvironment(callingAssembly); + + Configuration.SetBasePath(_environment.ContentRootPath); Services.AddSingleton(Environment); // Run methods to configure both generic and web host defaults early to populate config from appsettings.json @@ -36,13 +38,20 @@ internal WebApplicationBuilder(Assembly? callingAssembly, string[]? args = null) var bootstrapBuilder = new BootstrapHostBuilder(Configuration, _environment); bootstrapBuilder.ConfigureDefaults(args); bootstrapBuilder.ConfigureWebHostDefaults(configure: _ => { }); + bootstrapBuilder.ExecuteActions(); - Configuration.SetBasePath(_environment.ContentRootPath); Logging = new LoggingBuilder(Services); WebHost = _deferredWebHostBuilder = new ConfigureWebHostBuilder(Configuration, _environment, Services); Host = _deferredHostBuilder = new ConfigureHostBuilder(Configuration, _environment, Services); + // Register Configuration as IConfiguration so updates can be observed even after the WebApplication is built. + Services.AddSingleton(Configuration); + + // Add default services _deferredHostBuilder.ConfigureDefaults(args); + // Configuration changes made by ConfigureDefaults(args) were already picked up by the BootstrapHostBuilder, + // so we ignore changes to config until ConfigureDefaults completes. + _deferredHostBuilder.ConfigurationEnabled = true; } /// @@ -61,13 +70,13 @@ internal WebApplicationBuilder(Assembly? callingAssembly, string[]? args = null) public Configuration Configuration { get; } = new(); /// - /// A collection of logging providers for the applicaiton to compose. This is useful for adding new logging providers. + /// A collection of logging providers for the application to compose. This is useful for adding new logging providers. /// public ILoggingBuilder Logging { get; } /// /// An for configuring server specific properties, but not building. - /// To build after configuruation, call . + /// To build after configuration, call . /// public ConfigureWebHostBuilder WebHost { get; } @@ -155,24 +164,28 @@ private void ConfigureApplication(WebHostBuilderContext context, IApplicationBui private void ConfigureWebHost(IWebHostBuilder genericWebHostBuilder) { - genericWebHostBuilder.Configure(ConfigureApplication); - - _hostBuilder.ConfigureServices((context, services) => + _hostBuilder.ConfigureHostConfiguration(builder => { - foreach (var s in Services) + // All the sources in builder.Sources should be in Configuration.Sources + // already thanks to the BootstrapHostBuilder. + builder.Sources.Clear(); + + foreach (var s in ((IConfigurationBuilder)Configuration).Sources) { - services.Add(s); + builder.Sources.Add(s); } }); - _hostBuilder.ConfigureAppConfiguration((hostContext, builder) => + _hostBuilder.ConfigureServices((context, services) => { - foreach (var s in Configuration.Sources) + foreach (var s in Services) { - builder.Sources.Add(s); + services.Add(s); } }); + genericWebHostBuilder.Configure(ConfigureApplication); + _deferredHostBuilder.ExecuteActions(_hostBuilder); _deferredWebHostBuilder.ExecuteActions(genericWebHostBuilder);