diff --git a/src/DefaultBuilder/DefaultBuilder.slnf b/src/DefaultBuilder/DefaultBuilder.slnf index f0ea3f2eddf3..1f2d25e3a749 100644 --- a/src/DefaultBuilder/DefaultBuilder.slnf +++ b/src/DefaultBuilder/DefaultBuilder.slnf @@ -1,13 +1,24 @@ -{ +{ "solution": { "path": "..\\..\\AspNetCore.sln", - "projects" : [ + "projects": [ "src\\DefaultBuilder\\samples\\SampleApp\\DefaultBuilder.SampleApp.csproj", - "src\\DefaultBuilder\\test\\Microsoft.AspNetCore.Tests\\Microsoft.AspNetCore.Tests.csproj", - "src\\DefaultBuilder\\test\\Microsoft.AspNetCore.FunctionalTests\\Microsoft.AspNetCore.FunctionalTests.csproj", "src\\DefaultBuilder\\src\\Microsoft.AspNetCore.csproj", + "src\\DefaultBuilder\\test\\Microsoft.AspNetCore.FunctionalTests\\Microsoft.AspNetCore.FunctionalTests.csproj", + "src\\DefaultBuilder\\test\\Microsoft.AspNetCore.Tests\\Microsoft.AspNetCore.Tests.csproj", + "src\\Hosting\\Abstractions\\src\\Microsoft.AspNetCore.Hosting.Abstractions.csproj", + "src\\Hosting\\Hosting\\src\\Microsoft.AspNetCore.Hosting.csproj", + "src\\Hosting\\Server.Abstractions\\src\\Microsoft.AspNetCore.Hosting.Server.Abstractions.csproj", "src\\Hosting\\Server.IntegrationTesting\\src\\Microsoft.AspNetCore.Server.IntegrationTesting.csproj", - "src\\Middleware\\StaticFiles\\src\\Microsoft.AspNetCore.StaticFiles.csproj" + "src\\Http\\Features\\src\\Microsoft.Extensions.Features.csproj", + "src\\Http\\Headers\\src\\Microsoft.Net.Http.Headers.csproj", + "src\\Http\\Http.Abstractions\\src\\Microsoft.AspNetCore.Http.Abstractions.csproj", + "src\\Http\\Http.Extensions\\src\\Microsoft.AspNetCore.Http.Extensions.csproj", + "src\\Http\\Http.Features\\src\\Microsoft.AspNetCore.Http.Features.csproj", + "src\\Http\\Http\\src\\Microsoft.AspNetCore.Http.csproj", + "src\\Http\\WebUtilities\\src\\Microsoft.AspNetCore.WebUtilities.csproj", + "src\\Middleware\\StaticFiles\\src\\Microsoft.AspNetCore.StaticFiles.csproj", + "src\\ObjectPool\\src\\Microsoft.Extensions.ObjectPool.csproj" ] } -} +} \ No newline at end of file diff --git a/src/DefaultBuilder/src/BootstrapHostBuilder.cs b/src/DefaultBuilder/src/BootstrapHostBuilder.cs index 563dba7907a4..8fdd740a83f2 100644 --- a/src/DefaultBuilder/src/BootstrapHostBuilder.cs +++ b/src/DefaultBuilder/src/BootstrapHostBuilder.cs @@ -13,22 +13,28 @@ 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 HostBuilderContext _hostContext; + + private readonly List> _configureHostActions = new(); + private readonly List> _configureAppActions = new(); + public BootstrapHostBuilder(Configuration configuration, WebHostEnvironment webHostEnvironment) { _configuration = configuration; _environment = webHostEnvironment; - _context = new HostBuilderContext(Properties) + + _hostContext = new HostBuilderContext(Properties) { Configuration = configuration, HostingEnvironment = webHostEnvironment }; } + public IDictionary Properties { get; } = new Dictionary(); + public IHost Build() { // HostingHostBuilderExtensions.ConfigureDefaults should never call this. @@ -37,9 +43,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,12 +56,15 @@ public IHostBuilder ConfigureContainer(Action configureDelegate) { - configureDelegate(_configuration); - _environment.ApplyConfigurationSettings(_configuration); - _configuration.ChangeBasePath(_environment.ContentRootPath); + _configureHostActions.Add(configureDelegate ?? throw new ArgumentNullException(nameof(configureDelegate))); return this; } + public string? GetSetting(string key) + { + return _configuration[key]; + } + public IHostBuilder ConfigureServices(Action configureDelegate) { // HostingHostBuilderExtensions.ConfigureDefaults calls this via ConfigureLogging @@ -67,7 +74,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 +85,26 @@ 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 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. + /// Creates an empty mutable configuration object that is both an and an . /// - /// 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) + public Configuration() { - return _configuration.GetSection(key); + _sources = new ConfigurationSources(this); + + // Make sure there's some default storage since there are no default providers. + this.AddInMemoryCollection(); + + Update(); } /// - /// Gets the immediate descendant configuration sub-sections. + /// Automatically update the on changes. + /// If , will manually update the . /// - /// The configuration sub-sections. - public IEnumerable GetChildren() => _configuration.GetChildren(); + internal bool AutoUpdate { get; set; } = true; + + /// + public string this[string key] { get => _configurationRoot[key]; set => _configurationRoot[key] = value; } - IDictionary IConfigurationBuilder.Properties => _builder.Properties; + /// + public IConfigurationSection GetSection(string key) => new ConfigurationSection(this, key); - // TODO: Handle modifications to Sources and keep the configuration root in sync - IList IConfigurationBuilder.Sources => Sources; + /// + public IEnumerable GetChildren() => GetChildrenImplementation(null); - internal IList Sources { get; } + IDictionary IConfigurationBuilder.Properties { get; } = new Dictionary(); - IEnumerable IConfigurationRoot.Providers => _configuration.Providers; + IList IConfigurationBuilder.Sources => _sources; + + IEnumerable IConfigurationRoot.Providers => _configurationRoot.Providers; /// - /// Creates a new . + /// Manually update the to reflect changes. + /// It is not necessary to call this if is . /// - public Configuration() + [MemberNotNull(nameof(_configurationRoot))] + internal void Update() { - _configuration = _builder.Build(); + var newConfiguration = BuildConfigurationRoot(); + var prevConfiguration = _configurationRoot; + + _configurationRoot = newConfiguration; - var sources = new ConfigurationSources(_builder.Sources, UpdateConfigurationRoot); + _changeTokenRegistration?.Dispose(); + (prevConfiguration as IDisposable)?.Dispose(); - Sources = sources; + _changeTokenRegistration = ChangeToken.OnChange(() => newConfiguration.GetReloadToken(), RaiseChanged); + RaiseChanged(); } - internal void ChangeBasePath(string path) + /// + void IDisposable.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() + IConfigurationRoot IConfigurationBuilder.Build() => BuildConfigurationRoot(); + + IChangeToken IConfiguration.GetReloadToken() => _changeToken; + + void IConfigurationRoot.Reload() => _configurationRoot.Reload(); + + private void NotifySourcesChanged() { - var current = _configuration; - if (current is IDisposable disposable) + if (AutoUpdate) { - disposable.Dispose(); + Update(); } - _configuration = _builder.Build(); - } - - IConfigurationBuilder IConfigurationBuilder.Add(IConfigurationSource source) - { - Sources.Add(source); - return this; } - 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 List _sources = new(); + private readonly Configuration _config; - public ConfigurationSources(IList sources, Action sourcesModified) + public ConfigurationSources(Configuration config) { - _sources = sources; - _sourcesModified = sourcesModified; + _config = config; } public IConfigurationSource this[int index] @@ -132,24 +152,24 @@ public IConfigurationSource this[int index] set { _sources[index] = value; - _sourcesModified(); + _config.NotifySourcesChanged(); } } public int Count => _sources.Count; - public bool IsReadOnly => _sources.IsReadOnly; + public bool IsReadOnly => false; public void Add(IConfigurationSource item) { _sources.Add(item); - _sourcesModified(); + _config.NotifySourcesChanged(); } public void Clear() { _sources.Clear(); - _sourcesModified(); + _config.NotifySourcesChanged(); } public bool Contains(IConfigurationSource item) @@ -175,20 +195,20 @@ public int IndexOf(IConfigurationSource item) public void Insert(int index, IConfigurationSource item) { _sources.Insert(index, item); - _sourcesModified(); + _config.NotifySourcesChanged(); } public bool Remove(IConfigurationSource item) { var removed = _sources.Remove(item); - _sourcesModified(); + _config.NotifySourcesChanged(); return removed; } public void RemoveAt(int index) { _sources.RemoveAt(index); - _sourcesModified(); + _config.NotifySourcesChanged(); } IEnumerator IEnumerable.GetEnumerator() diff --git a/src/DefaultBuilder/src/ConfigureHostBuilder.cs b/src/DefaultBuilder/src/ConfigureHostBuilder.cs index 73c9b7e03598..4e36de917b47 100644 --- a/src/DefaultBuilder/src/ConfigureHostBuilder.cs +++ b/src/DefaultBuilder/src/ConfigureHostBuilder.cs @@ -15,26 +15,32 @@ namespace Microsoft.AspNetCore.Builder /// public sealed class ConfigureHostBuilder : IHostBuilder { - private Action? _operations; + private readonly List> _operations = new(); /// 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,27 +49,38 @@ 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; } /// public IHostBuilder ConfigureContainer(Action configureDelegate) { - _operations += b => b.ConfigureContainer(configureDelegate); + if (configureDelegate is null) + { + throw new ArgumentNullException(nameof(configureDelegate)); + } + + _operations.Add(b => b.ConfigureContainer(configureDelegate)); return this; } /// 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 +88,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; } @@ -84,20 +96,33 @@ public IHostBuilder ConfigureServices(Action public IHostBuilder UseServiceProviderFactory(IServiceProviderFactory factory) where TContainerBuilder : notnull { - _operations += b => b.UseServiceProviderFactory(factory); + if (factory is null) + { + throw new ArgumentNullException(nameof(factory)); + } + + _operations.Add(b => b.UseServiceProviderFactory(factory)); return this; } /// public IHostBuilder UseServiceProviderFactory(Func> factory) where TContainerBuilder : notnull { - _operations += b => b.UseServiceProviderFactory(factory); + if (factory is null) + { + throw new ArgumentNullException(nameof(factory)); + } + + _operations.Add(b => b.UseServiceProviderFactory(factory)); return this; } - internal void ExecuteActions(IHostBuilder hostBuilder) + internal void RunDeferredCallbacks(IHostBuilder hostBuilder) { - _operations?.Invoke(hostBuilder); + foreach (var operation in _operations) + { + operation(hostBuilder); + } } } } diff --git a/src/DefaultBuilder/src/ConfigureWebHostBuilder.cs b/src/DefaultBuilder/src/ConfigureWebHostBuilder.cs index 5d95319e7738..86da16c87bce 100644 --- a/src/DefaultBuilder/src/ConfigureWebHostBuilder.cs +++ b/src/DefaultBuilder/src/ConfigureWebHostBuilder.cs @@ -15,18 +15,24 @@ namespace Microsoft.AspNetCore.Builder /// public sealed class ConfigureWebHostBuilder : IWebHostBuilder { - private Action? _operations; - private readonly WebHostEnvironment _environment; private readonly Configuration _configuration; private readonly Dictionary _settings = new(StringComparer.OrdinalIgnoreCase); private readonly IServiceCollection _services; + private readonly WebHostBuilderContext _context; + internal ConfigureWebHostBuilder(Configuration configuration, WebHostEnvironment environment, IServiceCollection services) { _configuration = configuration; _environment = environment; _services = services; + + _context = new WebHostBuilderContext + { + Configuration = _configuration, + HostingEnvironment = _environment + }; } IWebHost IWebHostBuilder.Build() @@ -37,19 +43,17 @@ IWebHost IWebHostBuilder.Build() /// public IWebHostBuilder ConfigureAppConfiguration(Action configureDelegate) { - _operations += b => b.ConfigureAppConfiguration(configureDelegate); + // Run these immediately so that they are observable by the imperative code + configureDelegate(_context, _configuration); + _environment.ApplyConfigurationSettings(_configuration); return this; } /// public IWebHostBuilder ConfigureServices(Action configureServices) { - configureServices(new WebHostBuilderContext - { - Configuration = _configuration, - HostingEnvironment = _environment - }, - _services); + // Run these immediately so that they are observable by the imperative code + configureServices(_context, _services); return this; } @@ -70,7 +74,6 @@ public IWebHostBuilder ConfigureServices(Action configureSer public IWebHostBuilder UseSetting(string key, string? value) { _settings[key] = value; - _operations += b => b.UseSetting(key, value); // All properties on IWebHostEnvironment are non-nullable. if (value is null) @@ -86,8 +89,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)) { @@ -102,9 +103,12 @@ public IWebHostBuilder UseSetting(string key, string? value) return this; } - internal void ExecuteActions(IWebHostBuilder webHostBuilder) + internal void ApplySettings(IWebHostBuilder webHostBuilder) { - _operations?.Invoke(webHostBuilder); + foreach (var (key, value) in _settings) + { + webHostBuilder.UseSetting(key, value); + } } } } diff --git a/src/DefaultBuilder/src/WebApplicationBuilder.cs b/src/DefaultBuilder/src/WebApplicationBuilder.cs index 3be41cf94061..0a3ac35b9279 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,33 @@ internal WebApplicationBuilder(Assembly? callingAssembly, string[]? args = null) var bootstrapBuilder = new BootstrapHostBuilder(Configuration, _environment); bootstrapBuilder.ConfigureDefaults(args); bootstrapBuilder.ConfigureWebHostDefaults(configure: _ => { }); + bootstrapBuilder.RunConfigurationCallbacks(); - 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); + _deferredHostBuilder.ConfigureWebHostDefaults(configure: _ => { }); + + // This is important because GenericWebHostBuilder does the following and we want to preserve the WebHostBuilderContext: + // context.Properties[typeof(WebHostBuilderContext)] = webHostBuilderContext; + // context.Properties[typeof(WebHostOptions)] = options; + foreach (var (key, value) in _deferredHostBuilder.Properties) + { + _hostBuilder.Properties[key] = value; + } + + // 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; + // Now that consuming code can start modifying Configuration, we need to automatically rebuild on modification. + // To this point, we've been manually calling Configuration.UpdateConfiguration() only when needed to reduce I/O. + Configuration.AutoUpdate = true; } /// @@ -58,16 +80,16 @@ internal WebApplicationBuilder(Assembly? callingAssembly, string[]? args = null) /// /// A collection of configuration providers for the application to compose. This is useful for adding new configuration sources and providers. /// - public Configuration Configuration { get; } = new(); + public Configuration Configuration { get; } = new() { AutoUpdate = false }; /// - /// 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; } @@ -83,9 +105,11 @@ internal WebApplicationBuilder(Assembly? callingAssembly, string[]? args = null) /// A configured . public WebApplication Build() { + // We call ConfigureWebHostDefaults AGAIN because config might be added like "ForwardedHeaders_Enabled" + // which can add even more services. If not for that, we probably call _hostBuilder.ConfigureWebHost(ConfigureWebHost) + // instead in order to avoid duplicate service registration. _hostBuilder.ConfigureWebHostDefaults(ConfigureWebHost); - _builtApplication = new WebApplication(_hostBuilder.Build()); - return _builtApplication; + return _builtApplication = new WebApplication(_hostBuilder.Build()); } private void ConfigureApplication(WebHostBuilderContext context, IApplicationBuilder app) @@ -150,31 +174,52 @@ private void ConfigureApplication(WebHostBuilderContext context, IApplicationBui { app.Properties[item.Key] = item.Value; } - } private void ConfigureWebHost(IWebHostBuilder genericWebHostBuilder) { - genericWebHostBuilder.Configure(ConfigureApplication); - - _hostBuilder.ConfigureServices((context, services) => + _hostBuilder.ConfigureHostConfiguration(builder => { - foreach (var s in Services) + // TODO: Use a ChainedConfigurationSource instead. + // See EnvironmentSpecificLoggingConfigurationSectionPassedToLoggerByDefault in WebApplicationFuncationalTests. + + // All the sources in builder.Sources should be in Configuration.Sources + // already thanks to the BootstrapHostBuilder. + builder.Sources.Clear(); + + foreach (var (key, value) in ((IConfigurationBuilder)Configuration).Properties) { - services.Add(s); + builder.Properties[key] = value; + } + + foreach (var s in ((IConfigurationBuilder)Configuration).Sources) + { + builder.Sources.Add(s); } }); - _hostBuilder.ConfigureAppConfiguration((hostContext, builder) => + genericWebHostBuilder.ConfigureServices((context, services) => { - foreach (var s in Configuration.Sources) + // We've only added services configured by the GenericWebHostBuilder and WebHost.ConfigureWebDefaults + // at this point. HostBuilder news up a new ServiceCollection in HostBuilder.Build() we haven't seen + // until now, so we cannot clear these services even though some are redundant because + // we called ConfigureWebHostDefaults on both the _deferredHostBuilder and _hostBuilder. + + // Ideally, we'd only call _hostBuilder.ConfigureWebHost(ConfigureWebHost) instead of + // _hostBuilder.ConfigureWebHostDefaults(ConfigureWebHost) to avoid some duplicate service descriptors, + // but we want to add services in the WebApplicationBuilder constructor so code can inspect + // WebApplicationBuilder.Services. At the same time, we want to be able which services are loaded + // to react to config changes (e.g. ForwardedHeadersStartupFilter). + foreach (var s in Services) { - builder.Sources.Add(s); + services.Add(s); } }); - _deferredHostBuilder.ExecuteActions(_hostBuilder); - _deferredWebHostBuilder.ExecuteActions(genericWebHostBuilder); + genericWebHostBuilder.Configure(ConfigureApplication); + + _deferredHostBuilder.RunDeferredCallbacks(_hostBuilder); + _deferredWebHostBuilder.ApplySettings(genericWebHostBuilder); _environment.ApplyEnvironmentSettings(genericWebHostBuilder); } diff --git a/src/DefaultBuilder/src/WebHostEnvironment.cs b/src/DefaultBuilder/src/WebHostEnvironment.cs index 052764c2ea63..89d92794ff72 100644 --- a/src/DefaultBuilder/src/WebHostEnvironment.cs +++ b/src/DefaultBuilder/src/WebHostEnvironment.cs @@ -69,6 +69,23 @@ public void ApplyEnvironmentSettings(IWebHostBuilder genericWebHostBuilder) genericWebHostBuilder.UseSetting(WebHostDefaults.EnvironmentKey, EnvironmentName); genericWebHostBuilder.UseSetting(WebHostDefaults.ContentRootKey, ContentRootPath); genericWebHostBuilder.UseSetting(WebHostDefaults.WebRootKey, WebRootPath); + + genericWebHostBuilder.ConfigureAppConfiguration((context, builder) => + { + CopyProperitesTo(context.HostingEnvironment); + }); + } + + internal void CopyProperitesTo(IWebHostEnvironment destination) + { + destination.ApplicationName = ApplicationName; + destination.EnvironmentName = EnvironmentName; + + destination.ContentRootPath = ContentRootPath; + destination.ContentRootFileProvider = ContentRootFileProvider; + + destination.WebRootPath = WebRootPath; + destination.WebRootFileProvider = WebRootFileProvider; } public void ResolveFileProviders(IConfiguration configuration) diff --git a/src/DefaultBuilder/test/Microsoft.AspNetCore.FunctionalTests/WebApplicationFunctionalTests.cs b/src/DefaultBuilder/test/Microsoft.AspNetCore.FunctionalTests/WebApplicationFunctionalTests.cs index 53273e7c77da..abeb0f16661c 100644 --- a/src/DefaultBuilder/test/Microsoft.AspNetCore.FunctionalTests/WebApplicationFunctionalTests.cs +++ b/src/DefaultBuilder/test/Microsoft.AspNetCore.FunctionalTests/WebApplicationFunctionalTests.cs @@ -51,5 +51,52 @@ await File.WriteAllTextAsync("appsettings.json", @" File.Delete("appsettings.json"); } } + + [Fact] + public async Task EnvironmentSpecificLoggingConfigurationSectionPassedToLoggerByDefault() + { + try + { + await File.WriteAllTextAsync("appsettings.Development.json", @" +{ + ""Logging"": { + ""LogLevel"": { + ""Default"": ""Warning"" + } + } +}"); + + var app = WebApplication.Create(new[] { "--environment", "Development" }); + + // TODO: Make this work! I think it should be possible if we register our Configuration + // as a ChainedConfigurationSource instead of copying over the bootstrapped IConfigurationSources. + //var builder = WebApplication.CreateBuilder(); + //builder.Environment.EnvironmentName = "Development"; + //await using var app = builder.Build(); + + var factory = (ILoggerFactory)app.Services.GetService(typeof(ILoggerFactory)); + var logger = factory.CreateLogger("Test"); + + logger.Log(LogLevel.Information, 0, "Message", null, (s, e) => + { + Assert.True(false); + return string.Empty; + }); + + var logWritten = false; + logger.Log(LogLevel.Warning, 0, "Message", null, (s, e) => + { + logWritten = true; + return string.Empty; + }); + + Assert.True(logWritten); + } + finally + { + File.Delete("appsettings.json"); + } + } + } } diff --git a/src/DefaultBuilder/test/Microsoft.AspNetCore.Tests/ConfigurationTests.cs b/src/DefaultBuilder/test/Microsoft.AspNetCore.Tests/ConfigurationTests.cs new file mode 100644 index 000000000000..75668b4c14bb --- /dev/null +++ b/src/DefaultBuilder/test/Microsoft.AspNetCore.Tests/ConfigurationTests.cs @@ -0,0 +1,142 @@ +// 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.Collections.Generic; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.Configuration; +using Xunit; + +namespace Microsoft.AspNetCore.Tests +{ + public class ConfigurationTests + { + [Fact] + public void AutoUpdatesByDefault() + { + var config = new Configuration(); + + Assert.True(config.AutoUpdate); + + config.AddInMemoryCollection(new Dictionary + { + { "TestKey", "TestValue" }, + }); + + Assert.Equal("TestValue", config["TestKey"]); + } + + [Fact] + public void AutoUpdateTriggersReloadTokenOnSourceModification() + { + var config = new Configuration(); + + var reloadToken = ((IConfiguration)config).GetReloadToken(); + + Assert.False(reloadToken.HasChanged); + + config.AddInMemoryCollection(new Dictionary + { + { "TestKey", "TestValue" }, + }); + + Assert.True(reloadToken.HasChanged); + } + + [Fact] + public void DoesNotAutoUpdateWhenAutoUpdateDisabled() + { + var config = new Configuration + { + AutoUpdate = false, + }; + + config.AddInMemoryCollection(new Dictionary + { + { "TestKey", "TestValue" }, + }); + + Assert.Null(config["TestKey"]); + + config.Update(); + + Assert.Equal("TestValue", config["TestKey"]); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void ManualUpdateTriggersReloadTokenWithOrWithoutAutoUpdate(bool autoUpdate) + { + var config = new Configuration + { + AutoUpdate = autoUpdate, + }; + + var manualReloadToken = ((IConfiguration)config).GetReloadToken(); + + Assert.False(manualReloadToken.HasChanged); + + config.Update(); + + Assert.True(manualReloadToken.HasChanged); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void SettingValuesWorksWithOrWithoutAutoUpdate(bool autoUpdate) + { + var config = new Configuration + { + AutoUpdate = autoUpdate, + ["TestKey"] = "TestValue", + }; + + Assert.Equal("TestValue", config["TestKey"]); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void SettingValuesDoesNotTriggerReloadTokenWithOrWithoutAutoUpdate(bool autoUpdate) + { + var config = new Configuration + { + AutoUpdate = autoUpdate, + }; + + var reloadToken = ((IConfiguration)config).GetReloadToken(); + + config["TestKey"] = "TestValue"; + + Assert.Equal("TestValue", config["TestKey"]); + + // ConfigurationRoot doesn't fire the token today when the setter is called. Maybe we should change that. + // At least you can manually call Configuration.Update() to fire a reload though this reloads all sources unnecessarily. + Assert.False(reloadToken.HasChanged); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + public void SettingIConfigurationBuilderPropertiesWorksWithoutAutoUpdate(bool autoUpdate) + { + var config = new Configuration + { + AutoUpdate = autoUpdate, + }; + + var configBuilder = (IConfigurationBuilder)config; + + var reloadToken = ((IConfiguration)config).GetReloadToken(); + + configBuilder.Properties["TestKey"] = "TestValue"; + + Assert.Equal("TestValue", configBuilder.Properties["TestKey"]); + + // Changing properties should not change config keys or fire reload token. + Assert.Null(config["TestKey"]); + Assert.False(reloadToken.HasChanged); + } + } +} diff --git a/src/DefaultBuilder/test/Microsoft.AspNetCore.Tests/WebApplicationTests.cs b/src/DefaultBuilder/test/Microsoft.AspNetCore.Tests/WebApplicationTests.cs index dcd8e0b865c9..edd70f7568b8 100644 --- a/src/DefaultBuilder/test/Microsoft.AspNetCore.Tests/WebApplicationTests.cs +++ b/src/DefaultBuilder/test/Microsoft.AspNetCore.Tests/WebApplicationTests.cs @@ -179,7 +179,6 @@ public void WebApplicationBuilderWebHostUseSettings_IsCaseInsensitive() var envName = $"{nameof(WebApplicationTests)}_ENV"; builder.WebHost.UseSetting("applicationname", nameof(WebApplicationTests)); - builder.WebHost.UseSetting("ENVIRONMENT", envName); builder.WebHost.UseSetting("CONTENTROOT", contentRoot); builder.WebHost.UseSetting("WEBROOT", webRoot); diff --git a/src/DefaultBuilder/test/Microsoft.AspNetCore.Tests/WebHostEnvironmentTests.cs b/src/DefaultBuilder/test/Microsoft.AspNetCore.Tests/WebHostEnvironmentTests.cs index 369a11a61837..f86ae488fa33 100644 --- a/src/DefaultBuilder/test/Microsoft.AspNetCore.Tests/WebHostEnvironmentTests.cs +++ b/src/DefaultBuilder/test/Microsoft.AspNetCore.Tests/WebHostEnvironmentTests.cs @@ -7,6 +7,8 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.FileProviders; +using Moq; using Xunit; namespace Microsoft.AspNetCore.Tests @@ -37,35 +39,50 @@ public void ApplyConfigurationSettingsUsesTheCorrectKeys() } [Fact] - public void ApplyEnvironmentSettingsUsesTheCorrectKeys() + public void ApplyEnvironmentSettingsUsesTheCorrectKeysAndProperties() { - var env = new WebHostEnvironment + var originalEnvironment = new WebHostEnvironment { ApplicationName = WebHostDefaults.ApplicationKey, EnvironmentName = WebHostDefaults.EnvironmentKey, ContentRootPath = WebHostDefaults.ContentRootKey, WebRootPath = WebHostDefaults.WebRootKey, + ContentRootFileProvider = Mock.Of(), + WebRootFileProvider = Mock.Of(), }; var settings = new Dictionary(); + var webHostBuilderEnvironment = new WebHostEnvironment(); - env.ApplyEnvironmentSettings(new TestWebHostBuilder(settings)); + originalEnvironment.ApplyEnvironmentSettings(new TestWebHostBuilder(settings, webHostBuilderEnvironment)); Assert.Equal(WebHostDefaults.ApplicationKey, settings[WebHostDefaults.ApplicationKey]); Assert.Equal(WebHostDefaults.EnvironmentKey, settings[WebHostDefaults.EnvironmentKey]); Assert.Equal(WebHostDefaults.ContentRootKey, settings[WebHostDefaults.ContentRootKey]); Assert.Equal(WebHostDefaults.WebRootKey, settings[WebHostDefaults.WebRootKey]); + + Assert.Equal(WebHostDefaults.ApplicationKey, webHostBuilderEnvironment.ApplicationName); + Assert.Equal(WebHostDefaults.EnvironmentKey, webHostBuilderEnvironment.EnvironmentName); + Assert.Equal(WebHostDefaults.ContentRootKey, webHostBuilderEnvironment.ContentRootPath); + Assert.Equal(WebHostDefaults.WebRootKey, webHostBuilderEnvironment.WebRootPath); + + Assert.Same(originalEnvironment.ContentRootFileProvider, webHostBuilderEnvironment.ContentRootFileProvider); + Assert.Same(originalEnvironment.WebRootFileProvider, webHostBuilderEnvironment.WebRootFileProvider); } private class TestWebHostBuilder : IWebHostBuilder { private readonly Dictionary _settings; + private readonly IWebHostEnvironment _environment; - public TestWebHostBuilder(Dictionary settingsDictionary) + public TestWebHostBuilder(Dictionary settingsDictionary, IWebHostEnvironment environment) { _settings = settingsDictionary; + _environment = environment; } + public IWebHostEnvironment Environment { get; } + public IWebHost Build() { throw new NotImplementedException(); @@ -73,7 +90,14 @@ public IWebHost Build() public IWebHostBuilder ConfigureAppConfiguration(Action configureDelegate) { - throw new NotImplementedException(); + var context = new WebHostBuilderContext + { + HostingEnvironment = _environment, + }; + + configureDelegate(context, null!); + + return this; } public IWebHostBuilder ConfigureServices(Action configureServices)