Skip to content

Commit 1a4db56

Browse files
authored
Load IConfigurationProviders once in WebApplicationBuilder (#37039)
1 parent a547aa8 commit 1a4db56

File tree

7 files changed

+470
-68
lines changed

7 files changed

+470
-68
lines changed
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System.Collections;
5+
using Microsoft.Extensions.Configuration;
6+
using Microsoft.Extensions.Primitives;
7+
8+
namespace Microsoft.AspNetCore.Builder
9+
{
10+
internal sealed class ConfigurationProviderSource : IConfigurationSource
11+
{
12+
private readonly IConfigurationProvider _configurationProvider;
13+
14+
public ConfigurationProviderSource(IConfigurationProvider configurationProvider)
15+
{
16+
_configurationProvider = configurationProvider;
17+
}
18+
19+
public IConfigurationProvider Build(IConfigurationBuilder builder)
20+
{
21+
return new IgnoreFirstLoadConfigurationProvider(_configurationProvider);
22+
}
23+
24+
// These providers have already been loaded, so no need to reload initially.
25+
// Otherwise, providers that cannot be reloaded like StreamConfigurationProviders will fail.
26+
private sealed class IgnoreFirstLoadConfigurationProvider : IConfigurationProvider, IEnumerable<IConfigurationProvider>, IDisposable
27+
{
28+
private readonly IConfigurationProvider _provider;
29+
30+
private bool _hasIgnoredFirstLoad;
31+
32+
public IgnoreFirstLoadConfigurationProvider(IConfigurationProvider provider)
33+
{
34+
_provider = provider;
35+
}
36+
37+
public IEnumerable<string> GetChildKeys(IEnumerable<string> earlierKeys, string parentPath)
38+
{
39+
return _provider.GetChildKeys(earlierKeys, parentPath);
40+
}
41+
42+
public IChangeToken GetReloadToken()
43+
{
44+
return _provider.GetReloadToken();
45+
}
46+
47+
public void Load()
48+
{
49+
if (!_hasIgnoredFirstLoad)
50+
{
51+
_hasIgnoredFirstLoad = true;
52+
return;
53+
}
54+
55+
_provider.Load();
56+
}
57+
58+
public void Set(string key, string value)
59+
{
60+
_provider.Set(key, value);
61+
}
62+
63+
public bool TryGet(string key, out string value)
64+
{
65+
return _provider.TryGet(key, out value);
66+
}
67+
68+
// Provide access to the original IConfigurationProvider via a single-element IEnumerable to code that goes out of its way to look for it.
69+
public IEnumerator<IConfigurationProvider> GetEnumerator() => GetUnwrappedEnumerable().GetEnumerator();
70+
71+
IEnumerator IEnumerable.GetEnumerator() => GetUnwrappedEnumerable().GetEnumerator();
72+
73+
public override bool Equals(object? obj)
74+
{
75+
return _provider.Equals(obj);
76+
}
77+
78+
public override int GetHashCode()
79+
{
80+
return _provider.GetHashCode();
81+
}
82+
83+
public override string? ToString()
84+
{
85+
return _provider.ToString();
86+
}
87+
88+
public void Dispose()
89+
{
90+
(_provider as IDisposable)?.Dispose();
91+
}
92+
93+
private IEnumerable<IConfigurationProvider> GetUnwrappedEnumerable()
94+
{
95+
yield return _provider;
96+
}
97+
}
98+
}
99+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using Microsoft.Extensions.Configuration;
5+
6+
namespace Microsoft.AspNetCore.Builder
7+
{
8+
internal sealed class TrackingChainedConfigurationSource : IConfigurationSource
9+
{
10+
private readonly ChainedConfigurationSource _chainedConfigurationSource = new();
11+
12+
public TrackingChainedConfigurationSource(ConfigurationManager configManager)
13+
{
14+
_chainedConfigurationSource.Configuration = configManager;
15+
}
16+
17+
public IConfigurationProvider? BuiltProvider { get; set; }
18+
19+
public IConfigurationProvider Build(IConfigurationBuilder builder)
20+
{
21+
BuiltProvider = _chainedConfigurationSource.Build(builder);
22+
return BuiltProvider;
23+
}
24+
}
25+
}

src/DefaultBuilder/src/WebApplication.cs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4-
using System.Reflection;
54
using Microsoft.AspNetCore.Hosting;
65
using Microsoft.AspNetCore.Hosting.Server;
76
using Microsoft.AspNetCore.Hosting.Server.Features;
@@ -20,11 +19,11 @@ namespace Microsoft.AspNetCore.Builder
2019
/// </summary>
2120
public sealed class WebApplication : IHost, IApplicationBuilder, IEndpointRouteBuilder, IAsyncDisposable
2221
{
22+
internal const string GlobalEndpointRouteBuilderKey = "__GlobalEndpointRouteBuilder";
23+
2324
private readonly IHost _host;
2425
private readonly List<EndpointDataSource> _dataSources = new();
2526

26-
internal static string GlobalEndpointRouteBuilderKey = "__GlobalEndpointRouteBuilder";
27-
2827
internal WebApplication(IHost host)
2928
{
3029
_host = host;

src/DefaultBuilder/src/WebApplicationBuilder.cs

Lines changed: 34 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using System.Diagnostics;
5+
using System.Linq;
56
using Microsoft.AspNetCore.Hosting;
67
using Microsoft.Extensions.Configuration;
78
using Microsoft.Extensions.DependencyInjection;
@@ -15,11 +16,12 @@ namespace Microsoft.AspNetCore.Builder
1516
/// </summary>
1617
public sealed class WebApplicationBuilder
1718
{
19+
private const string EndpointRouteBuilderKey = "__EndpointRouteBuilder";
20+
1821
private readonly HostBuilder _hostBuilder = new();
1922
private readonly BootstrapHostBuilder _bootstrapHostBuilder;
2023
private readonly WebApplicationServiceCollection _services = new();
2124
private readonly List<KeyValuePair<string, string>> _hostConfigurationValues;
22-
private const string EndpointRouteBuilderKey = "__EndpointRouteBuilder";
2325

2426
private WebApplication? _builtApplication;
2527

@@ -62,7 +64,6 @@ internal WebApplicationBuilder(WebApplicationOptions options, Action<IHostBuilde
6264
});
6365

6466
// Apply the args to host configuration last since ConfigureWebHostDefaults overrides a host specific setting (the application name).
65-
6667
_bootstrapHostBuilder.ConfigureHostConfiguration(config =>
6768
{
6869
if (args is { Length: > 0 })
@@ -74,7 +75,6 @@ internal WebApplicationBuilder(WebApplicationOptions options, Action<IHostBuilde
7475
options.ApplyHostConfiguration(config);
7576
});
7677

77-
7878
Configuration = new();
7979

8080
// Collect the hosted services separately since we want those to run after the user's hosted services
@@ -100,7 +100,7 @@ internal WebApplicationBuilder(WebApplicationOptions options, Action<IHostBuilde
100100
Host = new ConfigureHostBuilder(hostContext, Configuration, Services);
101101
WebHost = new ConfigureWebHostBuilder(webHostContext, Configuration, Services);
102102

103-
Services.AddSingleton<IConfiguration>(Configuration);
103+
Services.AddSingleton<IConfiguration>(_ => Configuration);
104104
}
105105

106106
/// <summary>
@@ -148,14 +148,13 @@ public WebApplication Build()
148148
builder.AddInMemoryCollection(_hostConfigurationValues);
149149
});
150150

151-
// Wire up the application configuration by copying the already built configuration providers over to final configuration builder.
152-
// We wrap the existing provider in a configuration source to avoid re-bulding the already added configuration sources.
151+
// Wire up the _hostBuilder's application configuration with a ChainedConfigurationSource to the ConfigurationManager.
152+
// We use a "tracking" source to avoid creating a circular reference when copying providers in ConfigureServices.
153+
var chainedConfigSource = new TrackingChainedConfigurationSource(Configuration);
154+
153155
_hostBuilder.ConfigureAppConfiguration(builder =>
154156
{
155-
foreach (var provider in ((IConfigurationRoot)Configuration).Providers)
156-
{
157-
builder.Sources.Add(new ConfigurationProviderSource(provider));
158-
}
157+
builder.Add(chainedConfigSource);
159158

160159
foreach (var (key, value) in ((IConfigurationBuilder)Configuration).Properties)
161160
{
@@ -173,17 +172,6 @@ public WebApplication Build()
173172
// we called ConfigureWebHostDefaults on both the _deferredHostBuilder and _hostBuilder.
174173
foreach (var s in _services)
175174
{
176-
// Skip the configuration manager instance we added earlier
177-
// we're already going to wire it up to this new configuration source
178-
// after we've built the application. There's a chance the user manually added
179-
// this as well but we still need to remove it from the final configuration
180-
// to avoid cycles in the configuration graph
181-
if (s.ServiceType == typeof(IConfiguration) &&
182-
s.ImplementationInstance == Configuration)
183-
{
184-
continue;
185-
}
186-
187175
services.Add(s);
188176
}
189177

@@ -205,21 +193,41 @@ public WebApplication Build()
205193
// Drop the reference to the existing collection and set the inner collection
206194
// to the new one. This allows code that has references to the service collection to still function.
207195
_services.InnerCollection = services;
196+
197+
var hostBuilderProviders = ((IConfigurationRoot)context.Configuration).Providers;
198+
199+
if (!hostBuilderProviders.Contains(chainedConfigSource.BuiltProvider))
200+
{
201+
// Something removed the _hostBuilder's TrackingChainedConfigurationSource pointing back to the ConfigurationManager.
202+
// This is likely a test using WebApplicationFactory. Replicate the effect by clearing the ConfingurationManager sources.
203+
((IConfigurationBuilder)Configuration).Sources.Clear();
204+
}
205+
206+
// Make the ConfigurationManager match the final _hostBuilder's configuration. To do that, we add the additional providers
207+
// to the inner _hostBuilders's configuration to the ConfigurationManager. We wrap the existing provider in a
208+
// configuration source to avoid rebulding or reloading the already added configuration sources.
209+
foreach (var provider in hostBuilderProviders)
210+
{
211+
// Avoid creating a circular reference to the ConfigurationManager via the chained configuration source.
212+
if (!ReferenceEquals(provider, chainedConfigSource.BuiltProvider))
213+
{
214+
((IConfigurationBuilder)Configuration).Add(new ConfigurationProviderSource(provider));
215+
}
216+
}
208217
});
209218

210219
// Run the other callbacks on the final host builder
211220
Host.RunDeferredCallbacks(_hostBuilder);
212221

213222
_builtApplication = new WebApplication(_hostBuilder.Build());
214223

215-
// Make builder.Configuration match the final configuration. To do that
216-
// we clear the sources and add the built configuration as a source
217-
((IConfigurationBuilder)Configuration).Sources.Clear();
218-
Configuration.AddConfiguration(_builtApplication.Configuration);
219-
220224
// Mark the service collection as read-only to prevent future modifications
221225
_services.IsReadOnly = true;
222226

227+
// Resolve both the _hostBuilder's Configuration and builder.Configuration to mark both as resolved within the
228+
// service provider ensuring both will be properly disposed with the provider.
229+
_ = _builtApplication.Services.GetService<IEnumerable<IConfiguration>>();
230+
223231
return _builtApplication;
224232
}
225233

@@ -300,20 +308,5 @@ public LoggingBuilder(IServiceCollection services)
300308

301309
public IServiceCollection Services { get; }
302310
}
303-
304-
private sealed class ConfigurationProviderSource : IConfigurationSource
305-
{
306-
private readonly IConfigurationProvider _configurationProvider;
307-
308-
public ConfigurationProviderSource(IConfigurationProvider configurationProvider)
309-
{
310-
_configurationProvider = configurationProvider;
311-
}
312-
313-
public IConfigurationProvider Build(IConfigurationBuilder builder)
314-
{
315-
return _configurationProvider;
316-
}
317-
}
318311
}
319312
}

0 commit comments

Comments
 (0)