diff options
author | Stephen Halter <halter73@gmail.com> | 2021-09-29 01:30:35 +0300 |
---|---|---|
committer | Stephen Halter <halter73@gmail.com> | 2021-09-29 01:48:26 +0300 |
commit | a5c042afbd07e3a1b24f691b8c623a776e7b42f4 (patch) | |
tree | 4dfda70a3158799ac48d266518eeb6064d7cecfb | |
parent | f2a8e7a893a444ce4307859862ea7ba3e501b448 (diff) |
Reverse configuration chaininghalter73/reverse-chain
-rw-r--r-- | src/DefaultBuilder/src/WebApplication.cs | 5 | ||||
-rw-r--r-- | src/DefaultBuilder/src/WebApplicationBuilder.cs | 58 | ||||
-rw-r--r-- | src/DefaultBuilder/test/Microsoft.AspNetCore.Tests/WebApplicationTests.cs | 100 |
3 files changed, 131 insertions, 32 deletions
diff --git a/src/DefaultBuilder/src/WebApplication.cs b/src/DefaultBuilder/src/WebApplication.cs index d6c8ca5c60..9d0a837c83 100644 --- a/src/DefaultBuilder/src/WebApplication.cs +++ b/src/DefaultBuilder/src/WebApplication.cs @@ -1,7 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Reflection; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Hosting.Server; using Microsoft.AspNetCore.Hosting.Server.Features; @@ -20,11 +19,11 @@ namespace Microsoft.AspNetCore.Builder /// </summary> public sealed class WebApplication : IHost, IApplicationBuilder, IEndpointRouteBuilder, IAsyncDisposable { + internal const string GlobalEndpointRouteBuilderKey = "__GlobalEndpointRouteBuilder"; + private readonly IHost _host; private readonly List<EndpointDataSource> _dataSources = new(); - internal static string GlobalEndpointRouteBuilderKey = "__GlobalEndpointRouteBuilder"; - internal WebApplication(IHost host) { _host = host; diff --git a/src/DefaultBuilder/src/WebApplicationBuilder.cs b/src/DefaultBuilder/src/WebApplicationBuilder.cs index e7e6269d02..dfb153eb9b 100644 --- a/src/DefaultBuilder/src/WebApplicationBuilder.cs +++ b/src/DefaultBuilder/src/WebApplicationBuilder.cs @@ -15,11 +15,12 @@ namespace Microsoft.AspNetCore.Builder /// </summary> public sealed class WebApplicationBuilder { + private const string EndpointRouteBuilderKey = "__EndpointRouteBuilder"; + private readonly HostBuilder _hostBuilder = new(); private readonly BootstrapHostBuilder _bootstrapHostBuilder; private readonly WebApplicationServiceCollection _services = new(); private readonly List<KeyValuePair<string, string>> _hostConfigurationValues; - private const string EndpointRouteBuilderKey = "__EndpointRouteBuilder"; private WebApplication? _builtApplication; @@ -62,7 +63,6 @@ namespace Microsoft.AspNetCore.Builder }); // Apply the args to host configuration last since ConfigureWebHostDefaults overrides a host specific setting (the application name). - _bootstrapHostBuilder.ConfigureHostConfiguration(config => { if (args is { Length: > 0 }) @@ -74,7 +74,6 @@ namespace Microsoft.AspNetCore.Builder options.ApplyHostConfiguration(config); }); - Configuration = new(); // Collect the hosted services separately since we want those to run after the user's hosted services @@ -100,7 +99,7 @@ namespace Microsoft.AspNetCore.Builder Host = new ConfigureHostBuilder(hostContext, Configuration, Services); WebHost = new ConfigureWebHostBuilder(webHostContext, Configuration, Services); - Services.AddSingleton<IConfiguration>(Configuration); + Services.AddSingleton<IConfiguration>(_ => Configuration); } /// <summary> @@ -148,14 +147,13 @@ namespace Microsoft.AspNetCore.Builder builder.AddInMemoryCollection(_hostConfigurationValues); }); + var chainedConfigSource = new TrackingChainedConfigurationSource(Configuration); + // Wire up the application configuration by copying the already built configuration providers over to final configuration builder. // We wrap the existing provider in a configuration source to avoid re-bulding the already added configuration sources. _hostBuilder.ConfigureAppConfiguration(builder => { - foreach (var provider in ((IConfigurationRoot)Configuration).Providers) - { - builder.Sources.Add(new ConfigurationProviderSource(provider)); - } + builder.Add(chainedConfigSource); foreach (var (key, value) in ((IConfigurationBuilder)Configuration).Properties) { @@ -173,17 +171,6 @@ namespace Microsoft.AspNetCore.Builder // we called ConfigureWebHostDefaults on both the _deferredHostBuilder and _hostBuilder. foreach (var s in _services) { - // Skip the configuration manager instance we added earlier - // we're already going to wire it up to this new configuration source - // after we've built the application. There's a chance the user manually added - // this as well but we still need to remove it from the final configuration - // to avoid cycles in the configuration graph - if (s.ServiceType == typeof(IConfiguration) && - s.ImplementationInstance == Configuration) - { - continue; - } - services.Add(s); } @@ -205,6 +192,16 @@ namespace Microsoft.AspNetCore.Builder // Drop the reference to the existing collection and set the inner collection // to the new one. This allows code that has references to the service collection to still function. _services.InnerCollection = services; + + // Make builder.Configuration match the final configuration. To do that, we add the additional + // providers in the inner _hostBuilders's Configuration to the ConfigurationManager. + foreach (var provider in ((IConfigurationRoot)context.Configuration).Providers) + { + if (!ReferenceEquals(provider, chainedConfigSource.BuiltProvider)) + { + ((IConfigurationBuilder)Configuration).Add(new ConfigurationProviderSource(provider)); + } + } }); // Run the other callbacks on the final host builder @@ -212,11 +209,6 @@ namespace Microsoft.AspNetCore.Builder _builtApplication = new WebApplication(_hostBuilder.Build()); - // Make builder.Configuration match the final configuration. To do that - // we clear the sources and add the built configuration as a source - ((IConfigurationBuilder)Configuration).Sources.Clear(); - Configuration.AddConfiguration(_builtApplication.Configuration); - // Mark the service collection as read-only to prevent future modifications _services.IsReadOnly = true; @@ -301,6 +293,24 @@ namespace Microsoft.AspNetCore.Builder public IServiceCollection Services { get; } } + private sealed class TrackingChainedConfigurationSource : IConfigurationSource + { + private readonly ChainedConfigurationSource _chainedConfigurationSource = new(); + + public TrackingChainedConfigurationSource(ConfigurationManager configManager) + { + _chainedConfigurationSource.Configuration = configManager; + } + + public IConfigurationProvider? BuiltProvider { get; set; } + + public IConfigurationProvider Build(IConfigurationBuilder builder) + { + BuiltProvider = _chainedConfigurationSource.Build(builder); + return BuiltProvider; + } + } + private sealed class ConfigurationProviderSource : IConfigurationSource { private readonly IConfigurationProvider _configurationProvider; diff --git a/src/DefaultBuilder/test/Microsoft.AspNetCore.Tests/WebApplicationTests.cs b/src/DefaultBuilder/test/Microsoft.AspNetCore.Tests/WebApplicationTests.cs index 595f5db980..d6b2326b6a 100644 --- a/src/DefaultBuilder/test/Microsoft.AspNetCore.Tests/WebApplicationTests.cs +++ b/src/DefaultBuilder/test/Microsoft.AspNetCore.Tests/WebApplicationTests.cs @@ -23,7 +23,6 @@ using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using Xunit; [assembly: HostingStartup(typeof(WebApplicationTests.TestHostingStartup))] @@ -706,7 +705,7 @@ namespace Microsoft.AspNetCore.Tests var app = builder.Build(); // These are different - Assert.NotSame(app.Configuration, builder.Configuration); + Assert.Same(app.Configuration, builder.Configuration); } [Fact] @@ -723,7 +722,28 @@ namespace Microsoft.AspNetCore.Tests var app = builder.Build(); // These are different - Assert.NotSame(app.Configuration, builder.Configuration); + Assert.Same(app.Configuration, builder.Configuration); + } + + [Fact] + public void AddingMemoryStreamBackedConfigurationWorks() + { + var builder = WebApplication.CreateBuilder(); + + var jsonConfig = @"{ ""foo"": ""bar"" }"; + using var ms = new MemoryStream(); + using var sw = new StreamWriter(ms); + sw.WriteLine(jsonConfig); + sw.Flush(); + + ms.Position = 0; + builder.Configuration.AddJsonStream(ms); + + Assert.Equal("bar", builder.Configuration["foo"]); + + var app = builder.Build(); + + Assert.Equal("bar", app.Configuration["foo"]); } [Fact] @@ -1496,6 +1516,22 @@ namespace Microsoft.AspNetCore.Tests } [Fact] + public void ConfigurationGetDebugViewWorks() + { + var builder = WebApplication.CreateBuilder(); + + builder.Configuration.AddInMemoryCollection(new Dictionary<string, string> + { + ["foo"] = "bar", + }); + + var app = builder.Build(); + + // Make sure we don't lose "MemoryConfigurationProvider" from GetDebugView() when wrapping the provider. + Assert.Contains("foo=bar (MemoryConfigurationProvider)", ((IConfigurationRoot)app.Configuration).GetDebugView()); + } + + [Fact] public void ConfigurationCanBeReloaded() { var builder = WebApplication.CreateBuilder(); @@ -1524,23 +1560,77 @@ namespace Microsoft.AspNetCore.Tests Assert.Equal(1, configSource.ProvidersBuilt); } + [Fact] + public void ConfigurationProvidersAreLoadedOnceAfterBuild() + { + var builder = WebApplication.CreateBuilder(); + + var configSource = new RandomConfigurationSource(); + ((IConfigurationBuilder)builder.Configuration).Sources.Add(configSource); + + using var app = builder.Build(); + + Assert.Equal(1, configSource.ProvidersLoaded); + } + + [Fact] + public void ConfigurationProvidersAreDisposedWithWebApplication() + { + var builder = WebApplication.CreateBuilder(); + + var configSource = new RandomConfigurationSource(); + ((IConfigurationBuilder)builder.Configuration).Sources.Add(configSource); + + { + using var app = builder.Build(); + + Assert.Equal(0, configSource.ProvidersDisposed); + } + + Assert.Equal(1, configSource.ProvidersDisposed); + } + + [Fact] + public void ConfigurationProviderTypesArePreserved() + { + var builder = WebApplication.CreateBuilder(); + + ((IConfigurationBuilder)builder.Configuration).Sources.Add(new RandomConfigurationSource()); + + var app = builder.Build(); + + Assert.Single(((IConfigurationRoot)app.Configuration).Providers.OfType<RandomConfigurationProvider>()); + } + public class RandomConfigurationSource : IConfigurationSource { public int ProvidersBuilt { get; set; } + public int ProvidersLoaded { get; set; } + public int ProvidersDisposed { get; set; } public IConfigurationProvider Build(IConfigurationBuilder builder) { ProvidersBuilt++; - return new RandomConfigurationProvider(); + return new RandomConfigurationProvider(this); } } - public class RandomConfigurationProvider : ConfigurationProvider + public class RandomConfigurationProvider : ConfigurationProvider, IDisposable { + private readonly RandomConfigurationSource _source; + + public RandomConfigurationProvider(RandomConfigurationSource source) + { + _source = source; + } + public override void Load() { + _source.ProvidersLoaded++; Data["Random"] = Guid.NewGuid().ToString(); } + + public void Dispose() => _source.ProvidersDisposed++; } class ThrowingStartupFilter : IStartupFilter |