Skip to content

Commit 18a9268

Browse files
authored
Only add default services once in WebApplicationBuilder (#34782)
- Only add default services once - Lazily read ForwardedHeaders_Enabled config at starup - Add test verifying service scope validation is enabled in dev
1 parent dd476b1 commit 18a9268

File tree

9 files changed

+204
-117
lines changed

9 files changed

+204
-117
lines changed

src/DefaultBuilder/src/BootstrapHostBuilder.cs

Lines changed: 36 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +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;
5-
using System.Collections.Generic;
64
using Microsoft.AspNetCore.Builder;
75
using Microsoft.Extensions.Configuration;
86
using Microsoft.Extensions.DependencyInjection;
@@ -15,16 +13,20 @@ internal class BootstrapHostBuilder : IHostBuilder
1513
{
1614
private readonly ConfigurationManager _configuration;
1715
private readonly WebHostEnvironment _environment;
18-
16+
private readonly IServiceCollection _serviceCollection;
1917
private readonly HostBuilderContext _hostContext;
2018

2119
private readonly List<Action<IConfigurationBuilder>> _configureHostActions = new();
2220
private readonly List<Action<HostBuilderContext, IConfigurationBuilder>> _configureAppActions = new();
21+
private readonly List<Action<HostBuilderContext, IServiceCollection>> _configureServicesActions = new();
22+
23+
private readonly List<Action<IHostBuilder>> _remainingOperations = new();
2324

24-
public BootstrapHostBuilder(ConfigurationManager configuration, WebHostEnvironment webHostEnvironment)
25+
public BootstrapHostBuilder(ConfigurationManager configuration, WebHostEnvironment webHostEnvironment, IServiceCollection serviceCollection)
2526
{
2627
_configuration = configuration;
2728
_environment = webHostEnvironment;
29+
_serviceCollection = serviceCollection;
2830

2931
_hostContext = new HostBuilderContext(Properties)
3032
{
@@ -51,6 +53,12 @@ public IHostBuilder ConfigureContainer<TContainerBuilder>(Action<HostBuilderCont
5153
{
5254
// This is not called by HostingHostBuilderExtensions.ConfigureDefaults currently, but that could change in the future.
5355
// If this does get called in the future, it should be called again at a later stage on the ConfigureHostBuilder.
56+
if (configureDelegate is null)
57+
{
58+
throw new ArgumentNullException(nameof(configureDelegate));
59+
}
60+
61+
_remainingOperations.Add(hostBuilder => hostBuilder.ConfigureContainer<TContainerBuilder>(configureDelegate));
5462
return this;
5563
}
5664

@@ -60,33 +68,40 @@ public IHostBuilder ConfigureHostConfiguration(Action<IConfigurationBuilder> con
6068
return this;
6169
}
6270

63-
public string? GetSetting(string key)
64-
{
65-
return _configuration[key];
66-
}
67-
6871
public IHostBuilder ConfigureServices(Action<HostBuilderContext, IServiceCollection> configureDelegate)
6972
{
7073
// HostingHostBuilderExtensions.ConfigureDefaults calls this via ConfigureLogging
71-
// during the initial config stage. It should be called again later on the ConfigureHostBuilder.
74+
_configureServicesActions.Add(configureDelegate ?? throw new ArgumentNullException(nameof(configureDelegate)));
7275
return this;
7376
}
7477

7578
public IHostBuilder UseServiceProviderFactory<TContainerBuilder>(IServiceProviderFactory<TContainerBuilder> factory) where TContainerBuilder : notnull
7679
{
7780
// This is not called by HostingHostBuilderExtensions.ConfigureDefaults currently, but that could change in the future.
7881
// If this does get called in the future, it should be called again at a later stage on the ConfigureHostBuilder.
82+
if (factory is null)
83+
{
84+
throw new ArgumentNullException(nameof(factory));
85+
}
86+
87+
_remainingOperations.Add(hostBuilder => hostBuilder.UseServiceProviderFactory<TContainerBuilder>(factory));
7988
return this;
8089
}
8190

8291
public IHostBuilder UseServiceProviderFactory<TContainerBuilder>(Func<HostBuilderContext, IServiceProviderFactory<TContainerBuilder>> factory) where TContainerBuilder : notnull
8392
{
8493
// HostingHostBuilderExtensions.ConfigureDefaults calls this via UseDefaultServiceProvider
8594
// during the initial config stage. It should be called again later on the ConfigureHostBuilder.
95+
if (factory is null)
96+
{
97+
throw new ArgumentNullException(nameof(factory));
98+
}
99+
100+
_remainingOperations.Add(hostBuilder => hostBuilder.UseServiceProviderFactory<TContainerBuilder>(factory));
86101
return this;
87102
}
88103

89-
internal void RunConfigurationCallbacks()
104+
public void RunDefaultCallbacks(HostBuilder innerBuilder)
90105
{
91106
foreach (var configureHostAction in _configureHostActions)
92107
{
@@ -103,6 +118,16 @@ internal void RunConfigurationCallbacks()
103118
}
104119

105120
_environment.ApplyConfigurationSettings(_configuration);
121+
122+
foreach (var configureServicesAction in _configureServicesActions)
123+
{
124+
configureServicesAction(_hostContext, _serviceCollection);
125+
}
126+
127+
foreach (var callback in _remainingOperations)
128+
{
129+
callback(innerBuilder);
130+
}
106131
}
107132
}
108133
}

src/DefaultBuilder/src/ConfigureHostBuilder.cs

Lines changed: 6 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +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;
5-
using System.Collections.Generic;
64
using Microsoft.Extensions.Configuration;
75
using Microsoft.Extensions.DependencyInjection;
86
using Microsoft.Extensions.Hosting;
@@ -39,8 +37,6 @@ internal ConfigureHostBuilder(ConfigurationManager configuration, WebHostEnviron
3937
};
4038
}
4139

42-
internal bool ConfigurationEnabled { get; set; }
43-
4440
IHost IHostBuilder.Build()
4541
{
4642
throw new NotSupportedException($"Call {nameof(WebApplicationBuilder)}.{nameof(WebApplicationBuilder.Build)}() instead.");
@@ -49,12 +45,9 @@ IHost IHostBuilder.Build()
4945
/// <inheritdoc />
5046
public IHostBuilder ConfigureAppConfiguration(Action<HostBuilderContext, IConfigurationBuilder> configureDelegate)
5147
{
52-
if (ConfigurationEnabled)
53-
{
54-
// Run these immediately so that they are observable by the imperative code
55-
configureDelegate(_context, _configuration);
56-
_environment.ApplyConfigurationSettings(_configuration);
57-
}
48+
// Run these immediately so that they are observable by the imperative code
49+
configureDelegate(_context, _configuration);
50+
_environment.ApplyConfigurationSettings(_configuration);
5851

5952
return this;
6053
}
@@ -74,12 +67,9 @@ public IHostBuilder ConfigureContainer<TContainerBuilder>(Action<HostBuilderCont
7467
/// <inheritdoc />
7568
public IHostBuilder ConfigureHostConfiguration(Action<IConfigurationBuilder> configureDelegate)
7669
{
77-
if (ConfigurationEnabled)
78-
{
79-
// Run these immediately so that they are observable by the imperative code
80-
configureDelegate(_configuration);
81-
_environment.ApplyConfigurationSettings(_configuration);
82-
}
70+
// Run these immediately so that they are observable by the imperative code
71+
configureDelegate(_configuration);
72+
_environment.ApplyConfigurationSettings(_configuration);
8373

8474
return this;
8575
}
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
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.AspNetCore.Builder;
5+
using Microsoft.AspNetCore.HttpOverrides;
6+
using Microsoft.Extensions.Configuration;
7+
using Microsoft.Extensions.Options;
8+
9+
namespace Microsoft.AspNetCore
10+
{
11+
internal class ForwardedHeadersOptionsSetup : IConfigureOptions<ForwardedHeadersOptions>
12+
{
13+
private readonly IConfiguration _configuration;
14+
15+
public ForwardedHeadersOptionsSetup(IConfiguration configuration)
16+
{
17+
_configuration = configuration;
18+
}
19+
20+
public void Configure(ForwardedHeadersOptions options)
21+
{
22+
if (!string.Equals("true", _configuration["ForwardedHeaders_Enabled"], StringComparison.OrdinalIgnoreCase))
23+
{
24+
return;
25+
}
26+
27+
options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto;
28+
// Only loopback proxies are allowed by default. Clear that restriction because forwarders are
29+
// being enabled by explicit configuration.
30+
options.KnownNetworks.Clear();
31+
options.KnownProxies.Clear();
32+
}
33+
}
34+
}

src/DefaultBuilder/src/ForwardedHeadersStartupFilter.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,26 @@
44
using System;
55
using Microsoft.AspNetCore.Builder;
66
using Microsoft.AspNetCore.Hosting;
7+
using Microsoft.Extensions.Configuration;
78

89
namespace Microsoft.AspNetCore
910
{
1011
internal class ForwardedHeadersStartupFilter : IStartupFilter
1112
{
13+
private readonly IConfiguration _configuration;
14+
15+
public ForwardedHeadersStartupFilter(IConfiguration configuration)
16+
{
17+
_configuration = configuration;
18+
}
19+
1220
public Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next)
1321
{
22+
if (!string.Equals("true", _configuration["ForwardedHeaders_Enabled"], StringComparison.OrdinalIgnoreCase))
23+
{
24+
return next;
25+
}
26+
1427
return app =>
1528
{
1629
app.UseForwardedHeaders();

src/DefaultBuilder/src/WebApplicationBuilder.cs

Lines changed: 55 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,13 @@ public sealed class WebApplicationBuilder
2020
private const string EndpointRouteBuilderKey = "__EndpointRouteBuilder";
2121

2222
private readonly HostBuilder _hostBuilder = new();
23-
private readonly ConfigureHostBuilder _deferredHostBuilder;
24-
private readonly ConfigureWebHostBuilder _deferredWebHostBuilder;
23+
private readonly BootstrapHostBuilder _bootstrapHostBuilder;
2524
private readonly WebHostEnvironment _environment;
2625
private readonly WebApplicationServiceCollection _services = new();
26+
27+
// This is effectively readonly because it is always set inline by the ConfigureWebHostDefaults callback in the ctor.
28+
private IWebHostBuilder _capturedWebHostBuilder = default!;
29+
2730
private WebApplication? _builtApplication;
2831

2932
internal WebApplicationBuilder(Assembly? callingAssembly, string[]? args = null)
@@ -34,31 +37,34 @@ internal WebApplicationBuilder(Assembly? callingAssembly, string[]? args = null)
3437
Environment = _environment = new WebHostEnvironment(callingAssembly);
3538

3639
Configuration.SetBasePath(_environment.ContentRootPath);
37-
Services.AddSingleton(Environment);
3840

3941
// Run methods to configure both generic and web host defaults early to populate config from appsettings.json
4042
// environment variables (both DOTNET_ and ASPNETCORE_ prefixed) and other possible default sources to prepopulate
4143
// the correct defaults.
42-
var bootstrapBuilder = new BootstrapHostBuilder(Configuration, _environment);
43-
bootstrapBuilder.ConfigureDefaults(args);
44-
bootstrapBuilder.ConfigureWebHostDefaults(configure: _ => { });
45-
bootstrapBuilder.RunConfigurationCallbacks();
44+
_bootstrapHostBuilder = new BootstrapHostBuilder(Configuration, _environment, Services);
45+
_bootstrapHostBuilder.ConfigureDefaults(args);
46+
_bootstrapHostBuilder.ConfigureWebHostDefaults(webHostBuilder =>
47+
{
48+
// Runs inline.
49+
webHostBuilder.Configure(ConfigureApplication);
4650

47-
Logging = new LoggingBuilder(Services);
48-
WebHost = _deferredWebHostBuilder = new ConfigureWebHostBuilder(Configuration, _environment, Services);
49-
Host = _deferredHostBuilder = new ConfigureHostBuilder(Configuration, _environment, Services);
51+
// We need to override the application name since the call to Configure will set it to
52+
// be the calling assembly's name.
53+
webHostBuilder.UseSetting(WebHostDefaults.ApplicationKey, _environment.ApplicationName);
5054

51-
// Add default services
52-
_deferredHostBuilder.ConfigureDefaults(args);
55+
// Store this so we can apply settings from _environment during build.
56+
_capturedWebHostBuilder = webHostBuilder;
57+
});
58+
_bootstrapHostBuilder.RunDefaultCallbacks(_hostBuilder);
5359

54-
// Enable changes here because we need to pick up configuration sources added by the generic web host
55-
_deferredHostBuilder.ConfigurationEnabled = true;
56-
_deferredHostBuilder.ConfigureWebHostDefaults(configure: _ => { });
60+
Logging = new LoggingBuilder(Services);
61+
WebHost = new ConfigureWebHostBuilder(Configuration, _environment, Services);
62+
Host = new ConfigureHostBuilder(Configuration, _environment, Services);
5763

5864
// This is important because GenericWebHostBuilder does the following and we want to preserve the WebHostBuilderContext:
5965
// context.Properties[typeof(WebHostBuilderContext)] = webHostBuilderContext;
6066
// context.Properties[typeof(WebHostOptions)] = options;
61-
foreach (var (key, value) in _deferredHostBuilder.Properties)
67+
foreach (var (key, value) in _bootstrapHostBuilder.Properties)
6268
{
6369
_hostBuilder.Properties[key] = value;
6470
}
@@ -102,6 +108,9 @@ internal WebApplicationBuilder(Assembly? callingAssembly, string[]? args = null)
102108
/// <returns>A configured <see cref="WebApplication"/>.</returns>
103109
public WebApplication Build()
104110
{
111+
// Apply changes made to WebApplicationBuilder.Environment directly before building.
112+
_environment.ApplyEnvironmentSettings(_capturedWebHostBuilder, _hostBuilder);
113+
105114
// Copy the configuration sources into the final IConfigurationBuilder
106115
_hostBuilder.ConfigureHostConfiguration(builder =>
107116
{
@@ -116,10 +125,36 @@ public WebApplication Build()
116125
}
117126
});
118127

119-
// We call ConfigureWebHostDefaults AGAIN because config might be added like "ForwardedHeaders_Enabled"
120-
// which can add even more services. If not for that, we probably call _hostBuilder.ConfigureWebHost(ConfigureWebHost)
121-
// instead in order to avoid duplicate service registration.
122-
_hostBuilder.ConfigureWebHostDefaults(ConfigureWebHost);
128+
// This needs to go here to avoid adding the IHostedService that boots the server twice (the GenericWebHostService).
129+
// Copy the services that were added via WebApplicationBuilder.Services into the final IServiceCollection
130+
_hostBuilder.ConfigureServices((context, services) =>
131+
{
132+
// We've only added services configured by the GenericWebHostBuilder and WebHost.ConfigureWebDefaults
133+
// at this point. HostBuilder news up a new ServiceCollection in HostBuilder.Build() we haven't seen
134+
// until now, so we cannot clear these services even though some are redundant because
135+
// we called ConfigureWebHostDefaults on both the _deferredHostBuilder and _hostBuilder.
136+
137+
// Ideally, we'd only call _hostBuilder.ConfigureWebHost(ConfigureWebHost) instead of
138+
// _hostBuilder.ConfigureWebHostDefaults(ConfigureWebHost) to avoid some duplicate service descriptors,
139+
// but we want to add services in the WebApplicationBuilder constructor so code can inspect
140+
// WebApplicationBuilder.Services. At the same time, we want to be able which services are loaded
141+
// to react to config changes (e.g. ForwardedHeadersStartupFilter).
142+
foreach (var s in _services)
143+
{
144+
services.Add(s);
145+
}
146+
147+
// Add any services to the user visible service collection so that they are observable
148+
// just in case users capture the Services property. Orchard does this to get a "blueprint"
149+
// of the service collection
150+
151+
// Drop the reference to the existing collection and set the inner collection
152+
// to the new one. This allows code that has references to the service collection to still function.
153+
_services.InnerCollection = services;
154+
});
155+
156+
// Run the other callbacks on the final host builder
157+
Host.RunDeferredCallbacks(_hostBuilder);
123158

124159
_builtApplication = new WebApplication(_hostBuilder.Build());
125160

@@ -202,44 +237,6 @@ private void ConfigureApplication(WebHostBuilderContext context, IApplicationBui
202237
}
203238
}
204239

205-
private void ConfigureWebHost(IWebHostBuilder genericWebHostBuilder)
206-
{
207-
genericWebHostBuilder.Configure(ConfigureApplication);
208-
209-
// This needs to go here to avoid adding the IHostedService that boots the server twice (the GenericWebHostService).
210-
// Copy the services that were added via WebApplicationBuilder.Services into the final IServiceCollection
211-
genericWebHostBuilder.ConfigureServices((context, services) =>
212-
{
213-
// We've only added services configured by the GenericWebHostBuilder and WebHost.ConfigureWebDefaults
214-
// at this point. HostBuilder news up a new ServiceCollection in HostBuilder.Build() we haven't seen
215-
// until now, so we cannot clear these services even though some are redundant because
216-
// we called ConfigureWebHostDefaults on both the _deferredHostBuilder and _hostBuilder.
217-
218-
// Ideally, we'd only call _hostBuilder.ConfigureWebHost(ConfigureWebHost) instead of
219-
// _hostBuilder.ConfigureWebHostDefaults(ConfigureWebHost) to avoid some duplicate service descriptors,
220-
// but we want to add services in the WebApplicationBuilder constructor so code can inspect
221-
// WebApplicationBuilder.Services. At the same time, we want to be able which services are loaded
222-
// to react to config changes (e.g. ForwardedHeadersStartupFilter).
223-
foreach (var s in _services)
224-
{
225-
services.Add(s);
226-
}
227-
228-
// Add any services to the user visible service collection so that they are observable
229-
// just in case users capture the Services property. Orchard does this to get a "blueprint"
230-
// of the service collection
231-
232-
// Drop the reference to the existing collection and set the inner collection
233-
// to the new one. This allows code that has references to the service collection to still function.
234-
_services.InnerCollection = services;
235-
});
236-
237-
// Run the other callbacks on the final host builder
238-
_deferredHostBuilder.RunDeferredCallbacks(_hostBuilder);
239-
240-
_environment.ApplyEnvironmentSettings(genericWebHostBuilder);
241-
}
242-
243240
private static IEndpointRouteBuilder? GetEndpointRouteBuilder(IApplicationBuilder app)
244241
{
245242
app.Properties.TryGetValue(EndpointRouteBuilderKey, out var value);

0 commit comments

Comments
 (0)