Skip to content

Commit e0f2d09

Browse files
authored
Update CreateSlimBuilder to use Host.CreateEmptyApplicationBuilder. (#46579)
* Update CreateSlimBuilder to use Host.CreateEmptyApplicationBuilder. Also fix 2 existing bugs: 1. ContentRootPath is set to the app's BaseDirectory, and not CurrentWorkingDirectory. Default to CWD by using the same logic as HostApplicationBuilder. 2. Add un-prefixed environment variables to the configuration, and ensure the configuration order is the same as WebApplication.CreateBuilder. Fix #46293
1 parent 9bb6ab7 commit e0f2d09

File tree

2 files changed

+134
-14
lines changed

2 files changed

+134
-14
lines changed

src/DefaultBuilder/src/WebApplicationBuilder.cs

Lines changed: 45 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -94,25 +94,26 @@ internal WebApplicationBuilder(WebApplicationOptions options, bool slim, Action<
9494

9595
configuration.AddEnvironmentVariables(prefix: "ASPNETCORE_");
9696

97-
// add the default host configuration sources, so they are picked up by the HostApplicationBuilder constructor.
98-
// These won't be added by HostApplicationBuilder since DisableDefaults = true.
97+
// SetDefaultContentRoot needs to be added between 'ASPNETCORE_' and 'DOTNET_' in order to match behavior of the non-slim WebApplicationBuilder.
98+
SetDefaultContentRoot(options, configuration);
99+
100+
// Add the default host environment variable configuration source.
101+
// This won't be added by CreateEmptyApplicationBuilder.
99102
configuration.AddEnvironmentVariables(prefix: "DOTNET_");
100-
if (options.Args is { Length: > 0 } args)
101-
{
102-
configuration.AddCommandLine(args);
103-
}
104103

105-
_hostApplicationBuilder = new HostApplicationBuilder(new HostApplicationBuilderSettings
104+
_hostApplicationBuilder = Microsoft.Extensions.Hosting.Host.CreateEmptyApplicationBuilder(new HostApplicationBuilderSettings
106105
{
107-
DisableDefaults = true,
108106
Args = options.Args,
109107
ApplicationName = options.ApplicationName,
110108
EnvironmentName = options.EnvironmentName,
111109
ContentRootPath = options.ContentRootPath,
112110
Configuration = configuration,
113111
});
114112

115-
// configure the ServiceProviderOptions here since DisableDefaults = true means HostApplicationBuilder won't.
113+
// Ensure the same behavior of the non-slim WebApplicationBuilder by adding the default "app" Configuration sources
114+
ApplyDefaultAppConfiguration(options, configuration);
115+
116+
// configure the ServiceProviderOptions here since CreateEmptyApplicationBuilder won't.
116117
var serviceProviderFactory = GetServiceProviderFactory(_hostApplicationBuilder);
117118
_hostApplicationBuilder.ConfigureContainer(serviceProviderFactory);
118119

@@ -177,6 +178,41 @@ private static DefaultServiceProviderFactory GetServiceProviderFactory(HostAppli
177178
return new DefaultServiceProviderFactory();
178179
}
179180

181+
private static void SetDefaultContentRoot(WebApplicationOptions options, ConfigurationManager configuration)
182+
{
183+
if (options.ContentRootPath is null && configuration[HostDefaults.ContentRootKey] is null)
184+
{
185+
// Logic taken from https://github.com/dotnet/runtime/blob/78ed4438a42acab80541e9bde1910abaa8841db2/src/libraries/Microsoft.Extensions.Hosting/src/HostingHostBuilderExtensions.cs#L209-L227
186+
187+
// If we're running anywhere other than C:\Windows\system32, we default to using the CWD for the ContentRoot.
188+
// However, since many things like Windows services and MSIX installers have C:\Windows\system32 as there CWD which is not likely
189+
// to really be the home for things like appsettings.json, we skip changing the ContentRoot in that case. The non-"default" initial
190+
// value for ContentRoot is AppContext.BaseDirectory (e.g. the executable path) which probably makes more sense than the system32.
191+
192+
// In my testing, both Environment.CurrentDirectory and Environment.GetFolderPath(Environment.SpecialFolder.System) return the path without
193+
// any trailing directory separator characters. I'm not even sure the casing can ever be different from these APIs, but I think it makes sense to
194+
// ignore case for Windows path comparisons given the file system is usually (always?) going to be case insensitive for the system path.
195+
string cwd = System.Environment.CurrentDirectory;
196+
if (!OperatingSystem.IsWindows() || !string.Equals(cwd, System.Environment.GetFolderPath(System.Environment.SpecialFolder.System), StringComparison.OrdinalIgnoreCase))
197+
{
198+
configuration.AddInMemoryCollection(new[]
199+
{
200+
new KeyValuePair<string, string?>(HostDefaults.ContentRootKey, cwd),
201+
});
202+
}
203+
}
204+
}
205+
206+
private static void ApplyDefaultAppConfiguration(WebApplicationOptions options, ConfigurationManager configuration)
207+
{
208+
configuration.AddEnvironmentVariables();
209+
210+
if (options.Args is { Length: > 0 } args)
211+
{
212+
configuration.AddCommandLine(args);
213+
}
214+
}
215+
180216
/// <summary>
181217
/// Provides information about the web hosting environment an application is running.
182218
/// </summary>

src/DefaultBuilder/test/Microsoft.AspNetCore.Tests/WebApplicationTests.cs

Lines changed: 89 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,11 @@
55
using System.Diagnostics;
66
using System.Diagnostics.Tracing;
77
using System.Net;
8-
using System.Net.Http;
98
using System.Reflection;
109
using System.Security.Claims;
1110
using System.Text;
1211
using System.Text.Encodings.Web;
1312
using Microsoft.AspNetCore.Authentication;
14-
using Microsoft.AspNetCore.Authorization;
1513
using Microsoft.AspNetCore.Builder;
1614
using Microsoft.AspNetCore.HostFiltering;
1715
using Microsoft.AspNetCore.Hosting;
@@ -23,6 +21,7 @@
2321
using Microsoft.AspNetCore.TestHost;
2422
using Microsoft.AspNetCore.Testing;
2523
using Microsoft.AspNetCore.Tests;
24+
using Microsoft.DotNet.RemoteExecutor;
2625
using Microsoft.Extensions.Configuration;
2726
using Microsoft.Extensions.DependencyInjection;
2827
using Microsoft.Extensions.DependencyInjection.Extensions;
@@ -556,9 +555,57 @@ public void SettingContentRootToRelativePathUsesAppContextBaseDirectoryAsPathBas
556555
builder.WebHost.UseContentRoot("");
557556

558557
Assert.Equal(NormalizePath(AppContext.BaseDirectory), NormalizePath(builder.Environment.ContentRootPath));
558+
}
559+
560+
private static string NormalizePath(string unnormalizedPath) =>
561+
Path.TrimEndingDirectorySeparator(Path.GetFullPath(unnormalizedPath));
562+
563+
[ConditionalFact]
564+
[RemoteExecutionSupported]
565+
public void ContentRootIsDefaultedToCurrentDirectory()
566+
{
567+
var tmpDir = Directory.CreateTempSubdirectory();
568+
569+
try
570+
{
571+
var options = new RemoteInvokeOptions();
572+
options.StartInfo.WorkingDirectory = tmpDir.FullName;
573+
574+
using var remoteHandle = RemoteExecutor.Invoke(static () =>
575+
{
576+
foreach (object[] data in CreateBuilderFuncs)
577+
{
578+
var createBuilder = (CreateBuilderFunc)data[0];
579+
var builder = createBuilder();
559580

560-
static string NormalizePath(string unnormalizedPath) =>
561-
Path.TrimEndingDirectorySeparator(Path.GetFullPath(unnormalizedPath));
581+
Assert.Equal(NormalizePath(Environment.CurrentDirectory), NormalizePath(builder.Environment.ContentRootPath));
582+
}
583+
}, options);
584+
}
585+
finally
586+
{
587+
tmpDir.Delete(recursive: true);
588+
}
589+
}
590+
591+
[ConditionalFact]
592+
[OSSkipCondition(OperatingSystems.Linux | OperatingSystems.MacOSX)]
593+
[RemoteExecutionSupported]
594+
public void ContentRootIsBaseDirectoryWhenCurrentIsSpecialFolderSystem()
595+
{
596+
var options = new RemoteInvokeOptions();
597+
options.StartInfo.WorkingDirectory = Environment.GetFolderPath(Environment.SpecialFolder.System);
598+
599+
using var remoteHandle = RemoteExecutor.Invoke(static () =>
600+
{
601+
foreach (object[] data in CreateBuilderFuncs)
602+
{
603+
var createBuilder = (CreateBuilderFunc)data[0];
604+
var builder = createBuilder();
605+
606+
Assert.Equal(NormalizePath(AppContext.BaseDirectory), NormalizePath(builder.Environment.ContentRootPath));
607+
}
608+
}, options);
562609
}
563610

564611
[Theory]
@@ -795,6 +842,43 @@ public void WebApplicationBuilderApplicationNameCanBeOverridden(WebApplicationBu
795842
Assert.Equal(assemblyName, webHostEnv.ApplicationName);
796843
}
797844

845+
[ConditionalFact]
846+
[RemoteExecutionSupported]
847+
public void WebApplicationBuilderConfigurationSourcesOrderedCorrectly()
848+
{
849+
// all WebApplicationBuilders have the following configuration sources ordered highest to lowest priority:
850+
// 1. Command-line arguments
851+
// 2. Non-prefixed environment variables
852+
// 3. DOTNET_-prefixed environment variables
853+
// 4. ASPNETCORE_-prefixed environment variables
854+
855+
var options = new RemoteInvokeOptions();
856+
options.StartInfo.EnvironmentVariables.Add("one", "unprefixed_one");
857+
options.StartInfo.EnvironmentVariables.Add("two", "unprefixed_two");
858+
options.StartInfo.EnvironmentVariables.Add("DOTNET_one", "DOTNET_one");
859+
options.StartInfo.EnvironmentVariables.Add("DOTNET_two", "DOTNET_two");
860+
options.StartInfo.EnvironmentVariables.Add("DOTNET_three", "DOTNET_three");
861+
options.StartInfo.EnvironmentVariables.Add("ASPNETCORE_one", "ASPNETCORE_one");
862+
options.StartInfo.EnvironmentVariables.Add("ASPNETCORE_two", "ASPNETCORE_two");
863+
options.StartInfo.EnvironmentVariables.Add("ASPNETCORE_three", "ASPNETCORE_three");
864+
options.StartInfo.EnvironmentVariables.Add("ASPNETCORE_four", "ASPNETCORE_four");
865+
866+
using var remoteHandle = RemoteExecutor.Invoke(static () =>
867+
{
868+
var args = new[] { "--one=command_line_one" };
869+
foreach (object[] data in CreateBuilderArgsFuncs)
870+
{
871+
var createBuilder = (CreateBuilderArgsFunc)data[0];
872+
var builder = createBuilder(args);
873+
874+
Assert.Equal("command_line_one", builder.Configuration["one"]);
875+
Assert.Equal("unprefixed_two", builder.Configuration["two"]);
876+
Assert.Equal("DOTNET_three", builder.Configuration["three"]);
877+
Assert.Equal("ASPNETCORE_four", builder.Configuration["four"]);
878+
}
879+
}, options);
880+
}
881+
798882
[Theory]
799883
[MemberData(nameof(CreateBuilderArgsFuncs))]
800884
public void WebApplicationBuilderCanFlowCommandLineConfigurationToApplication(CreateBuilderArgsFunc createBuilder)
@@ -976,7 +1060,7 @@ public async Task WebApplicationCanObserveSourcesClearedInBuild(CreateBuilderFun
9761060

9771061
[Theory]
9781062
[MemberData(nameof(CreateBuilderOptionsFuncs))]
979-
public async Task WebApplicationCanObserveSourcesClearedInConfiguratHostConfiguration(CreateBuilderOptionsFunc createBuilder)
1063+
public async Task WebApplicationCanObserveSourcesClearedInHostConfiguration(CreateBuilderOptionsFunc createBuilder)
9801064
{
9811065
// This mimics what WebApplicationFactory<T> does and runs configure
9821066
// services callbacks

0 commit comments

Comments
 (0)