diff --git a/src/Aspire.Cli/Projects/AppHostEnvironmentDefaults.cs b/src/Aspire.Cli/Projects/AppHostEnvironmentDefaults.cs
new file mode 100644
index 00000000000..b8cf92b46a8
--- /dev/null
+++ b/src/Aspire.Cli/Projects/AppHostEnvironmentDefaults.cs
@@ -0,0 +1,168 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace Aspire.Cli.Projects;
+
+///
+/// Resolves the effective AppHost environment for CLI-launched processes.
+///
+internal static class AppHostEnvironmentDefaults
+{
+ private const string EnvironmentArgumentName = "--environment";
+ private const string EnvironmentArgumentAlias = "-e";
+ private const string AspNetCoreEnvironmentVariableName = "ASPNETCORE_ENVIRONMENT";
+
+ internal const string AspireEnvironmentVariableName = "ASPIRE_ENVIRONMENT";
+ internal const string DotNetEnvironmentVariableName = "DOTNET_ENVIRONMENT";
+ internal const string DevelopmentEnvironmentName = "Development";
+ internal const string ProductionEnvironmentName = "Production";
+
+ ///
+ /// Determines whether the variable name should be treated as an environment-selection variable
+ /// when filtering launch profile values.
+ ///
+ internal static bool IsEnvironmentVariableName(string variableName) =>
+ variableName is DotNetEnvironmentVariableName or AspNetCoreEnvironmentVariableName or AspireEnvironmentVariableName;
+
+ ///
+ /// Applies the effective environment to the launch environment variables.
+ ///
+ /// The environment variables passed to the launched process.
+ /// The fallback environment used when no explicit environment is provided.
+ /// Optional inherited environment variables used by tests.
+ /// Optional command-line arguments that may contain --environment.
+ internal static void ApplyEffectiveEnvironment(
+ IDictionary environmentVariables,
+ string? defaultEnvironment = null,
+ IReadOnlyDictionary? inheritedEnvironmentVariables = null,
+ string[]? args = null)
+ {
+ if (TryResolveEnvironment(environmentVariables, inheritedEnvironmentVariables, args, out var environment))
+ {
+ environmentVariables[DotNetEnvironmentVariableName] = environment;
+ }
+ else if (defaultEnvironment is not null)
+ {
+ environmentVariables[DotNetEnvironmentVariableName] = defaultEnvironment;
+ }
+ }
+
+ private static bool TryResolveEnvironment(
+ IDictionary environmentVariables,
+ IReadOnlyDictionary? inheritedEnvironmentVariables,
+ string[]? args,
+ out string environment)
+ {
+ // Match DistributedApplicationBuilder precedence:
+ // explicit --environment, then DOTNET_ENVIRONMENT, then ASPIRE_ENVIRONMENT.
+ if (TryGetRequestedEnvironment(args, out environment) ||
+ TryGetEnvironmentValue(environmentVariables, DotNetEnvironmentVariableName, out environment) ||
+ TryGetInheritedEnvironmentValue(inheritedEnvironmentVariables, DotNetEnvironmentVariableName, out environment) ||
+ TryGetEnvironmentValue(environmentVariables, AspireEnvironmentVariableName, out environment) ||
+ TryGetInheritedEnvironmentValue(inheritedEnvironmentVariables, AspireEnvironmentVariableName, out environment))
+ {
+ return true;
+ }
+
+ environment = null!;
+ return false;
+ }
+
+ private static bool TryGetRequestedEnvironment(string[]? args, out string environment)
+ {
+ if (args is not null)
+ {
+ // Walk from the end so the last --environment flag wins.
+ for (var i = args.Length - 1; i >= 0; i--)
+ {
+ if (TryGetRequestedEnvironment(args, i, out environment))
+ {
+ return true;
+ }
+ }
+ }
+
+ environment = null!;
+ return false;
+ }
+
+ private static bool TryGetRequestedEnvironment(string[] args, int index, out string environment)
+ {
+ var argument = args[index];
+
+ if (argument.StartsWith(EnvironmentArgumentName + "=", StringComparison.Ordinal))
+ {
+ return TryGetEnvironmentArgumentValue(argument[(EnvironmentArgumentName.Length + 1)..], out environment);
+ }
+
+ if (argument.StartsWith(EnvironmentArgumentAlias + "=", StringComparison.Ordinal))
+ {
+ return TryGetEnvironmentArgumentValue(argument[(EnvironmentArgumentAlias.Length + 1)..], out environment);
+ }
+
+ if (argument is EnvironmentArgumentName or EnvironmentArgumentAlias)
+ {
+ if (index + 1 < args.Length)
+ {
+ return TryGetEnvironmentArgumentValue(args[index + 1], out environment);
+ }
+ }
+
+ environment = null!;
+ return false;
+ }
+
+ private static bool TryGetEnvironmentArgumentValue(string value, out string environment)
+ {
+ if (!string.IsNullOrWhiteSpace(value))
+ {
+ environment = value;
+ return true;
+ }
+
+ environment = null!;
+ return false;
+ }
+
+ private static bool TryGetEnvironmentValue(
+ IDictionary environmentVariables,
+ string variableName,
+ out string environment)
+ {
+ if (environmentVariables.TryGetValue(variableName, out var value) && !string.IsNullOrWhiteSpace(value))
+ {
+ environment = value;
+ return true;
+ }
+
+ environment = null!;
+ return false;
+ }
+
+ private static bool TryGetInheritedEnvironmentValue(
+ IReadOnlyDictionary? inheritedEnvironmentVariables,
+ string variableName,
+ out string environment)
+ {
+ if (inheritedEnvironmentVariables is not null)
+ {
+ if (inheritedEnvironmentVariables.TryGetValue(variableName, out var value) && !string.IsNullOrWhiteSpace(value))
+ {
+ environment = value;
+ return true;
+ }
+ }
+ else
+ {
+ var value = Environment.GetEnvironmentVariable(variableName);
+ if (!string.IsNullOrWhiteSpace(value))
+ {
+ environment = value;
+ return true;
+ }
+ }
+
+ environment = null!;
+ return false;
+ }
+}
diff --git a/src/Aspire.Cli/Projects/DotNetAppHostProject.cs b/src/Aspire.Cli/Projects/DotNetAppHostProject.cs
index 481f6420736..5e2d2487eb0 100644
--- a/src/Aspire.Cli/Projects/DotNetAppHostProject.cs
+++ b/src/Aspire.Cli/Projects/DotNetAppHostProject.cs
@@ -332,7 +332,7 @@ public async Task RunAsync(AppHostProjectContext context, CancellationToken
if (isSingleFileAppHost)
{
- ConfigureSingleFileEnvironment(effectiveAppHostFile, env);
+ ConfigureSingleFileRunEnvironment(effectiveAppHostFile, env, args: context.UnmatchedTokens);
}
// Start the apphost - the runner will signal the backchannel when ready
@@ -362,17 +362,89 @@ public async Task RunAsync(AppHostProjectContext context, CancellationToken
}
}
- private static void ConfigureSingleFileEnvironment(FileInfo appHostFile, Dictionary env)
+ internal static void ConfigureSingleFileRunEnvironment(
+ FileInfo appHostFile,
+ Dictionary env,
+ IReadOnlyDictionary? inheritedEnvironmentVariables = null,
+ string[]? args = null)
{
var runJsonFilePath = appHostFile.FullName[..^2] + "run.json";
if (!File.Exists(runJsonFilePath))
{
- env["ASPNETCORE_ENVIRONMENT"] = "Development";
- env["DOTNET_ENVIRONMENT"] = "Development";
- env["ASPNETCORE_URLS"] = "https://localhost:17193;http://localhost:15069";
- env["ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL"] = "https://localhost:21293";
- env["ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL"] = "https://localhost:22086";
+ AppHostEnvironmentDefaults.ApplyEffectiveEnvironment(
+ env,
+ AppHostEnvironmentDefaults.DevelopmentEnvironmentName,
+ inheritedEnvironmentVariables,
+ args);
+ ApplyDefaultSingleFileEndpoints(env);
+ }
+ }
+
+ internal static void ConfigureSingleFilePublishEnvironment(
+ FileInfo appHostFile,
+ Dictionary env,
+ IReadOnlyDictionary? inheritedEnvironmentVariables = null,
+ string[]? args = null)
+ {
+ if (!TryApplySingleFileLaunchProfileEnvironmentVariables(appHostFile, env))
+ {
+ ApplyDefaultSingleFileEndpoints(env);
+ }
+
+ AppHostEnvironmentDefaults.ApplyEffectiveEnvironment(
+ env,
+ AppHostEnvironmentDefaults.ProductionEnvironmentName,
+ inheritedEnvironmentVariables,
+ args);
+ }
+
+ private static bool TryApplySingleFileLaunchProfileEnvironmentVariables(
+ FileInfo appHostFile,
+ Dictionary env)
+ {
+ var profiles = AspireConfigFile.ReadApphostRunProfiles(appHostFile.FullName[..^2] + "run.json");
+ AspireConfigProfile? profile;
+
+ if (profiles?.TryGetValue("https", out var httpsProfile) == true)
+ {
+ profile = httpsProfile;
+ }
+ else
+ {
+ profile = profiles?.Values.FirstOrDefault();
+ }
+
+ if (profile is null)
+ {
+ return false;
+ }
+
+ if (!string.IsNullOrEmpty(profile.ApplicationUrl))
+ {
+ env["ASPNETCORE_URLS"] = profile.ApplicationUrl;
+ }
+
+ if (profile.EnvironmentVariables is not null)
+ {
+ foreach (var (key, value) in profile.EnvironmentVariables)
+ {
+ if (AppHostEnvironmentDefaults.IsEnvironmentVariableName(key))
+ {
+ continue;
+ }
+
+ env[key] = value;
+ }
}
+
+ return true;
+ }
+
+ private static void ApplyDefaultSingleFileEndpoints(IDictionary env)
+ {
+ env["ASPNETCORE_URLS"] = "https://localhost:17193;http://localhost:15069";
+ env["ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL"] = "https://localhost:21293";
+ env["ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL"] = "https://localhost:22086";
}
///
@@ -461,7 +533,7 @@ public async Task PublishAsync(PublishContext context, CancellationToken ca
if (isSingleFileAppHost)
{
- ConfigureSingleFileEnvironment(effectiveAppHostFile, env);
+ ConfigureSingleFilePublishEnvironment(effectiveAppHostFile, env, args: context.Arguments);
}
return await _runner.RunAsync(
diff --git a/src/Aspire.Cli/Projects/GuestAppHostProject.cs b/src/Aspire.Cli/Projects/GuestAppHostProject.cs
index f9754b59ac9..45e603bd1ce 100644
--- a/src/Aspire.Cli/Projects/GuestAppHostProject.cs
+++ b/src/Aspire.Cli/Projects/GuestAppHostProject.cs
@@ -384,7 +384,10 @@ public async Task RunAsync(AppHostProjectContext context, CancellationToken
// Read launch settings once and reuse them for both the temporary server and guest AppHost.
var launchProfileEnvironmentVariables = ReadLaunchSettingsEnvironmentVariables(directory);
- var launchSettingsEnvVars = GetServerEnvironmentVariables(launchProfileEnvironmentVariables);
+ var launchSettingsEnvVars = GetServerEnvironmentVariables(
+ launchProfileEnvironmentVariables,
+ defaultEnvironment: AppHostEnvironmentDefaults.DevelopmentEnvironmentName,
+ args: context.UnmatchedTokens);
// Apply certificate environment variables (e.g., SSL_CERT_DIR on Linux)
foreach (var kvp in certEnvVars)
@@ -475,7 +478,12 @@ await GenerateCodeViaRpcAsync(
// Pass the launch profile and certificate environment variables through to the guest AppHost
// so it sees the same dashboard and resource service endpoints as the temporary .NET server.
- var environmentVariables = CreateGuestEnvironmentVariables(context.EnvironmentVariables, launchProfileEnvironmentVariables, certEnvVars);
+ var environmentVariables = CreateGuestEnvironmentVariables(
+ context.EnvironmentVariables,
+ launchProfileEnvironmentVariables,
+ certEnvVars,
+ defaultEnvironment: AppHostEnvironmentDefaults.DevelopmentEnvironmentName,
+ args: context.UnmatchedTokens);
environmentVariables["REMOTE_APP_HOST_SOCKET_PATH"] = socketPath;
environmentVariables["ASPIRE_PROJECT_DIRECTORY"] = directory.FullName;
environmentVariables["ASPIRE_APPHOST_FILEPATH"] = appHostFile.FullName;
@@ -587,37 +595,64 @@ await GenerateCodeViaRpcAsync(
}
}
- internal Dictionary GetServerEnvironmentVariables(DirectoryInfo directory)
+ internal Dictionary GetServerEnvironmentVariables(
+ DirectoryInfo directory,
+ string? defaultEnvironment = AppHostEnvironmentDefaults.DevelopmentEnvironmentName,
+ bool includeLaunchProfileEnvironmentVariables = true,
+ string[]? args = null)
{
- return GetServerEnvironmentVariables(ReadLaunchSettingsEnvironmentVariables(directory));
+ return GetServerEnvironmentVariables(
+ ReadLaunchSettingsEnvironmentVariables(directory),
+ defaultEnvironment,
+ includeLaunchProfileEnvironmentVariables,
+ args: args);
}
- private static Dictionary GetServerEnvironmentVariables(IDictionary? launchProfileEnvironmentVariables)
+ internal static Dictionary GetServerEnvironmentVariables(
+ IDictionary? launchProfileEnvironmentVariables,
+ string? defaultEnvironment = AppHostEnvironmentDefaults.DevelopmentEnvironmentName,
+ bool includeLaunchProfileEnvironmentVariables = true,
+ IReadOnlyDictionary? inheritedEnvironmentVariables = null,
+ string[]? args = null)
{
var envVars = new Dictionary();
- MergeLaunchProfileEnvironmentVariables(launchProfileEnvironmentVariables, envVars, defaultEnvironment: "Development");
+ MergeLaunchProfileEnvironmentVariables(launchProfileEnvironmentVariables, envVars, includeLaunchProfileEnvironmentVariables);
+ AppHostEnvironmentDefaults.ApplyEffectiveEnvironment(envVars, defaultEnvironment, inheritedEnvironmentVariables, args);
return envVars;
}
internal Dictionary CreateGuestEnvironmentVariables(
DirectoryInfo directory,
IDictionary contextEnvironmentVariables,
- IDictionary? additionalEnvironmentVariables = null)
+ IDictionary? additionalEnvironmentVariables = null,
+ string? defaultEnvironment = null,
+ bool includeLaunchProfileEnvironmentVariables = true,
+ string[]? args = null)
{
return CreateGuestEnvironmentVariables(
contextEnvironmentVariables,
ReadLaunchSettingsEnvironmentVariables(directory),
- additionalEnvironmentVariables);
+ additionalEnvironmentVariables,
+ defaultEnvironment,
+ includeLaunchProfileEnvironmentVariables,
+ args: args);
}
internal static Dictionary CreateGuestEnvironmentVariables(
IDictionary contextEnvironmentVariables,
IDictionary? launchProfileEnvironmentVariables,
- IDictionary? additionalEnvironmentVariables = null)
+ IDictionary? additionalEnvironmentVariables = null,
+ string? defaultEnvironment = null,
+ bool includeLaunchProfileEnvironmentVariables = true,
+ IReadOnlyDictionary? inheritedEnvironmentVariables = null,
+ string[]? args = null)
{
var environmentVariables = new Dictionary(contextEnvironmentVariables);
- MergeLaunchProfileEnvironmentVariables(launchProfileEnvironmentVariables, environmentVariables);
+ MergeLaunchProfileEnvironmentVariables(
+ launchProfileEnvironmentVariables,
+ environmentVariables,
+ includeLaunchProfileEnvironmentVariables);
if (additionalEnvironmentVariables is not null)
{
@@ -627,32 +662,28 @@ internal static Dictionary CreateGuestEnvironmentVariables(
}
}
+ AppHostEnvironmentDefaults.ApplyEffectiveEnvironment(environmentVariables, defaultEnvironment, inheritedEnvironmentVariables, args);
+
return environmentVariables;
}
private static void MergeLaunchProfileEnvironmentVariables(
IDictionary? launchProfileEnvironmentVariables,
IDictionary environmentVariables,
- string? defaultEnvironment = null)
+ bool includeLaunchProfileEnvironmentVariables = true)
{
if (launchProfileEnvironmentVariables is not null)
{
foreach (var (key, value) in launchProfileEnvironmentVariables)
{
+ if (!includeLaunchProfileEnvironmentVariables && AppHostEnvironmentDefaults.IsEnvironmentVariableName(key))
+ {
+ continue;
+ }
+
environmentVariables[key] = value;
}
}
-
- if (launchProfileEnvironmentVariables?.TryGetValue("ASPIRE_ENVIRONMENT", out var environment) == true)
- {
- environmentVariables["DOTNET_ENVIRONMENT"] = environment;
- environmentVariables["ASPNETCORE_ENVIRONMENT"] = environment;
- }
- else if (defaultEnvironment is not null)
- {
- environmentVariables["DOTNET_ENVIRONMENT"] = defaultEnvironment;
- environmentVariables["ASPNETCORE_ENVIRONMENT"] = defaultEnvironment;
- }
}
private Dictionary? ReadLaunchSettingsEnvironmentVariables(DirectoryInfo directory)
@@ -828,7 +859,11 @@ public async Task PublishAsync(PublishContext context, CancellationToken ca
// Read launch settings once and reuse them for both the temporary server and guest AppHost.
var launchProfileEnvironmentVariables = ReadLaunchSettingsEnvironmentVariables(directory);
- var launchSettingsEnvVars = GetServerEnvironmentVariables(launchProfileEnvironmentVariables);
+ var launchSettingsEnvVars = GetServerEnvironmentVariables(
+ launchProfileEnvironmentVariables,
+ defaultEnvironment: AppHostEnvironmentDefaults.ProductionEnvironmentName,
+ includeLaunchProfileEnvironmentVariables: false,
+ args: context.Arguments);
// Generate a backchannel socket path for CLI to connect to AppHost server
var backchannelSocketPath = GetBackchannelSocketPath();
@@ -907,7 +942,12 @@ await GenerateCodeViaRpcAsync(
// Pass the launch profile environment variables through to the guest AppHost so publish mode
// uses the same dashboard and resource service endpoints as the temporary .NET server.
- var environmentVariables = CreateGuestEnvironmentVariables(context.EnvironmentVariables, launchProfileEnvironmentVariables);
+ var environmentVariables = CreateGuestEnvironmentVariables(
+ context.EnvironmentVariables,
+ launchProfileEnvironmentVariables,
+ defaultEnvironment: AppHostEnvironmentDefaults.ProductionEnvironmentName,
+ includeLaunchProfileEnvironmentVariables: false,
+ args: context.Arguments);
environmentVariables["REMOTE_APP_HOST_SOCKET_PATH"] = jsonRpcSocketPath;
environmentVariables["ASPIRE_PROJECT_DIRECTORY"] = directory.FullName;
environmentVariables["ASPIRE_APPHOST_FILEPATH"] = appHostFile.FullName;
diff --git a/src/Aspire.Hosting/DistributedApplicationBuilder.cs b/src/Aspire.Hosting/DistributedApplicationBuilder.cs
index bceeacd410e..2ecc6d0323c 100644
--- a/src/Aspire.Hosting/DistributedApplicationBuilder.cs
+++ b/src/Aspire.Hosting/DistributedApplicationBuilder.cs
@@ -185,6 +185,13 @@ public DistributedApplicationBuilder(DistributedApplicationOptions options)
// so they're used to initialize some types created immediately, e.g. IHostEnvironment.
innerBuilderOptions.Args = options.Args;
+ // Pre-seed the configuration with ASPIRE_-prefixed environment variables.
+ // HostApplicationBuilder will then add DOTNET_-prefixed env vars and command line args on top.
+ // This gives us the priority order: --environment > DOTNET_ENVIRONMENT > ASPIRE_ENVIRONMENT > default.
+ var configuration = new ConfigurationManager();
+ configuration.AddEnvironmentVariables(prefix: "ASPIRE_");
+ innerBuilderOptions.Configuration = configuration;
+
LogBuilderConstructing(options, innerBuilderOptions);
_innerBuilder = new HostApplicationBuilder(innerBuilderOptions);
diff --git a/tests/Aspire.Cli.Tests/Projects/AppHostEnvironmentDefaultsTests.cs b/tests/Aspire.Cli.Tests/Projects/AppHostEnvironmentDefaultsTests.cs
new file mode 100644
index 00000000000..3f71e7157ec
--- /dev/null
+++ b/tests/Aspire.Cli.Tests/Projects/AppHostEnvironmentDefaultsTests.cs
@@ -0,0 +1,91 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Aspire.Cli.Projects;
+
+namespace Aspire.Cli.Tests.Projects;
+
+public class AppHostEnvironmentDefaultsTests
+{
+ private const string AspNetCoreEnvironmentVariableName = "ASPNETCORE_ENVIRONMENT";
+
+ [Fact]
+ public void ApplyEffectiveEnvironment_UsesDefaultWhenNoEnvironmentVariablesAreSet()
+ {
+ var env = new Dictionary();
+
+ AppHostEnvironmentDefaults.ApplyEffectiveEnvironment(env, AppHostEnvironmentDefaults.ProductionEnvironmentName);
+
+ Assert.Equal("Production", env["DOTNET_ENVIRONMENT"]);
+ Assert.False(env.ContainsKey(AspNetCoreEnvironmentVariableName));
+ }
+
+ [Fact]
+ public void ApplyEffectiveEnvironment_DotnetEnvironmentTakesPrecedenceOverAspireEnvironment()
+ {
+ var env = new Dictionary
+ {
+ [AppHostEnvironmentDefaults.DotNetEnvironmentVariableName] = "Production",
+ [AppHostEnvironmentDefaults.AspireEnvironmentVariableName] = "Staging"
+ };
+
+ AppHostEnvironmentDefaults.ApplyEffectiveEnvironment(env, AppHostEnvironmentDefaults.DevelopmentEnvironmentName);
+
+ Assert.Equal("Production", env["DOTNET_ENVIRONMENT"]);
+ Assert.False(env.ContainsKey(AspNetCoreEnvironmentVariableName));
+ Assert.Equal("Staging", env["ASPIRE_ENVIRONMENT"]);
+ }
+
+ [Fact]
+ public void ApplyEffectiveEnvironment_EnvironmentArgumentTakesPrecedenceOverEnvironmentVariables()
+ {
+ var env = new Dictionary
+ {
+ [AppHostEnvironmentDefaults.DotNetEnvironmentVariableName] = "Production",
+ [AppHostEnvironmentDefaults.AspireEnvironmentVariableName] = "Development"
+ };
+
+ AppHostEnvironmentDefaults.ApplyEffectiveEnvironment(
+ env,
+ AppHostEnvironmentDefaults.DevelopmentEnvironmentName,
+ args: ["--environment", "Staging"]);
+
+ Assert.Equal("Staging", env["DOTNET_ENVIRONMENT"]);
+ Assert.False(env.ContainsKey(AspNetCoreEnvironmentVariableName));
+ Assert.Equal("Development", env["ASPIRE_ENVIRONMENT"]);
+ }
+
+ [Fact]
+ public void ApplyEffectiveEnvironment_AspireEnvironmentTakesPrecedenceOverAspNetCoreEnvironment()
+ {
+ var env = new Dictionary
+ {
+ [AppHostEnvironmentDefaults.AspireEnvironmentVariableName] = "Testing",
+ [AspNetCoreEnvironmentVariableName] = "Staging"
+ };
+
+ AppHostEnvironmentDefaults.ApplyEffectiveEnvironment(env, AppHostEnvironmentDefaults.DevelopmentEnvironmentName);
+
+ Assert.Equal("Testing", env["DOTNET_ENVIRONMENT"]);
+ Assert.Equal("Staging", env["ASPNETCORE_ENVIRONMENT"]);
+ Assert.Equal("Testing", env["ASPIRE_ENVIRONMENT"]);
+ }
+
+ [Fact]
+ public void ApplyEffectiveEnvironment_UsesInheritedAspireEnvironmentWhenContextDoesNotSetOne()
+ {
+ var env = new Dictionary();
+ var inherited = new Dictionary
+ {
+ [AppHostEnvironmentDefaults.AspireEnvironmentVariableName] = "Staging"
+ };
+
+ AppHostEnvironmentDefaults.ApplyEffectiveEnvironment(
+ env,
+ AppHostEnvironmentDefaults.ProductionEnvironmentName,
+ inherited);
+
+ Assert.Equal("Staging", env["DOTNET_ENVIRONMENT"]);
+ Assert.False(env.ContainsKey(AspNetCoreEnvironmentVariableName));
+ }
+}
diff --git a/tests/Aspire.Cli.Tests/Projects/DotNetAppHostProjectTests.cs b/tests/Aspire.Cli.Tests/Projects/DotNetAppHostProjectTests.cs
new file mode 100644
index 00000000000..ef890ea450a
--- /dev/null
+++ b/tests/Aspire.Cli.Tests/Projects/DotNetAppHostProjectTests.cs
@@ -0,0 +1,300 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Aspire.Cli.Projects;
+using Aspire.Cli.Tests.TestServices;
+using Aspire.Cli.Tests.Utils;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Aspire.Cli.Tests.Projects;
+
+public class DotNetAppHostProjectTests(ITestOutputHelper outputHelper) : IDisposable
+{
+ private readonly TemporaryWorkspace _workspace = TemporaryWorkspace.Create(outputHelper);
+ private readonly List _serviceProviders = [];
+
+ public void Dispose()
+ {
+ foreach (var serviceProvider in _serviceProviders)
+ {
+ serviceProvider.Dispose();
+ }
+
+ _workspace.Dispose();
+ GC.SuppressFinalize(this);
+ }
+
+ [Fact]
+ public void ConfigureSingleFileRunEnvironment_DefaultsToDevelopmentForRun()
+ {
+ var appHostFile = CreateSingleFileAppHost();
+ var env = new Dictionary();
+
+ DotNetAppHostProject.ConfigureSingleFileRunEnvironment(
+ appHostFile,
+ env,
+ inheritedEnvironmentVariables: new Dictionary());
+
+ Assert.Equal("Development", env["DOTNET_ENVIRONMENT"]);
+ Assert.False(env.ContainsKey("ASPNETCORE_ENVIRONMENT"));
+ Assert.Equal("https://localhost:17193;http://localhost:15069", env["ASPNETCORE_URLS"]);
+ }
+
+ [Fact]
+ public void ConfigureSingleFilePublishEnvironment_DefaultsToProductionForPublish()
+ {
+ var appHostFile = CreateSingleFileAppHost();
+ var env = new Dictionary();
+
+ DotNetAppHostProject.ConfigureSingleFilePublishEnvironment(
+ appHostFile,
+ env,
+ inheritedEnvironmentVariables: new Dictionary());
+
+ Assert.Equal("Production", env["DOTNET_ENVIRONMENT"]);
+ Assert.False(env.ContainsKey("ASPNETCORE_ENVIRONMENT"));
+ Assert.Equal("https://localhost:17193;http://localhost:15069", env["ASPNETCORE_URLS"]);
+ }
+
+ [Fact]
+ public void ConfigureSingleFilePublishEnvironment_EnvironmentArgumentTakesPrecedenceOverDefaultEnvironment()
+ {
+ var appHostFile = CreateSingleFileAppHost();
+ var env = new Dictionary();
+
+ DotNetAppHostProject.ConfigureSingleFilePublishEnvironment(
+ appHostFile,
+ env,
+ inheritedEnvironmentVariables: new Dictionary(),
+ args: ["--environment", "Staging"]);
+
+ Assert.Equal("Staging", env["DOTNET_ENVIRONMENT"]);
+ Assert.False(env.ContainsKey("ASPNETCORE_ENVIRONMENT"));
+ Assert.Equal("https://localhost:17193;http://localhost:15069", env["ASPNETCORE_URLS"]);
+ }
+
+ [Fact]
+ public void ConfigureSingleFilePublishEnvironment_StripsLaunchProfileEnvironmentButKeepsEndpoints()
+ {
+ var appHostFile = CreateSingleFileAppHost();
+ File.WriteAllText(Path.Combine(appHostFile.DirectoryName!, "apphost.run.json"), """
+ {
+ "profiles": {
+ "https": {
+ "applicationUrl": "https://localhost:19000;http://localhost:15000",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development",
+ "DOTNET_ENVIRONMENT": "Development",
+ "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21000",
+ "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22000"
+ }
+ }
+ }
+ }
+ """);
+
+ var env = new Dictionary();
+
+ DotNetAppHostProject.ConfigureSingleFilePublishEnvironment(
+ appHostFile,
+ env,
+ inheritedEnvironmentVariables: new Dictionary());
+
+ Assert.Equal("Production", env["DOTNET_ENVIRONMENT"]);
+ Assert.False(env.ContainsKey("ASPNETCORE_ENVIRONMENT"));
+ Assert.Equal("https://localhost:19000;http://localhost:15000", env["ASPNETCORE_URLS"]);
+ Assert.Equal("https://localhost:21000", env["ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL"]);
+ Assert.Equal("https://localhost:22000", env["ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL"]);
+ }
+
+ [Fact]
+ public void ConfigureSingleFilePublishEnvironment_InheritedAspireEnvironmentOverridesDefaultEnvironment()
+ {
+ var appHostFile = CreateSingleFileAppHost();
+ var env = new Dictionary();
+
+ DotNetAppHostProject.ConfigureSingleFilePublishEnvironment(
+ appHostFile,
+ env,
+ inheritedEnvironmentVariables: new Dictionary
+ {
+ [AppHostEnvironmentDefaults.AspireEnvironmentVariableName] = "Staging"
+ });
+
+ Assert.Equal("Staging", env["DOTNET_ENVIRONMENT"]);
+ Assert.False(env.ContainsKey("ASPNETCORE_ENVIRONMENT"));
+ }
+
+ [Fact]
+ public async Task RunAsync_SingleFileAppHostWithoutRunJsonPassesDevelopmentEnvironmentToRunner()
+ {
+ var appHostFile = CreateSingleFileAppHost();
+ var runner = new TestDotNetCliRunner();
+ var project = CreateDotNetAppHostProject(runner);
+
+ runner.RunAsyncCallback = (projectFile, watch, noBuild, noRestore, args, env, _, options, _) =>
+ {
+ Assert.Equal(appHostFile.FullName, projectFile.FullName);
+ Assert.False(watch);
+ Assert.True(noBuild);
+ Assert.False(noRestore);
+ Assert.False(options.NoLaunchProfile);
+ Assert.Equal("Development", env!["DOTNET_ENVIRONMENT"]);
+ Assert.False(env.ContainsKey("ASPNETCORE_ENVIRONMENT"));
+ Assert.Equal("https://localhost:17193;http://localhost:15069", env["ASPNETCORE_URLS"]);
+ return Task.FromResult(0);
+ };
+
+ var exitCode = await project.RunAsync(new AppHostProjectContext
+ {
+ AppHostFile = appHostFile,
+ NoBuild = true,
+ NoRestore = false,
+ WorkingDirectory = _workspace.WorkspaceRoot,
+ EnvironmentVariables = new Dictionary()
+ }, CancellationToken.None);
+
+ Assert.Equal(0, exitCode);
+ }
+
+ [Fact]
+ public async Task RunAsync_SingleFileAppHostUsesEnvironmentArgumentWhenProvided()
+ {
+ var appHostFile = CreateSingleFileAppHost();
+ var runner = new TestDotNetCliRunner();
+ var project = CreateDotNetAppHostProject(runner);
+
+ runner.RunAsyncCallback = (projectFile, watch, noBuild, noRestore, args, env, _, options, _) =>
+ {
+ Assert.Equal(appHostFile.FullName, projectFile.FullName);
+ Assert.False(watch);
+ Assert.True(noBuild);
+ Assert.False(noRestore);
+ Assert.False(options.NoLaunchProfile);
+ Assert.Equal(["--environment", "Staging"], args);
+ Assert.Equal("Staging", env!["DOTNET_ENVIRONMENT"]);
+ Assert.False(env.ContainsKey("ASPNETCORE_ENVIRONMENT"));
+ return Task.FromResult(0);
+ };
+
+ var exitCode = await project.RunAsync(new AppHostProjectContext
+ {
+ AppHostFile = appHostFile,
+ NoBuild = true,
+ NoRestore = false,
+ UnmatchedTokens = ["--environment", "Staging"],
+ WorkingDirectory = _workspace.WorkspaceRoot,
+ EnvironmentVariables = new Dictionary()
+ }, CancellationToken.None);
+
+ Assert.Equal(0, exitCode);
+ }
+
+ [Fact]
+ public async Task PublishAsync_SingleFileAppHostStripsRunProfileEnvironmentBeforeInvokingRunner()
+ {
+ var appHostFile = CreateSingleFileAppHost();
+ File.WriteAllText(Path.Combine(appHostFile.DirectoryName!, "apphost.run.json"), """
+ {
+ "profiles": {
+ "https": {
+ "applicationUrl": "https://localhost:19000;http://localhost:15000",
+ "environmentVariables": {
+ "ASPIRE_ENVIRONMENT": "Development",
+ "ASPNETCORE_ENVIRONMENT": "Development",
+ "DOTNET_ENVIRONMENT": "Development",
+ "ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL": "https://localhost:21000",
+ "ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL": "https://localhost:22000"
+ }
+ }
+ }
+ }
+ """);
+
+ var runner = new TestDotNetCliRunner();
+ var project = CreateDotNetAppHostProject(runner);
+
+ runner.RunAsyncCallback = (projectFile, watch, noBuild, noRestore, args, env, _, options, _) =>
+ {
+ Assert.Equal(appHostFile.FullName, projectFile.FullName);
+ Assert.False(watch);
+ Assert.True(noBuild);
+ Assert.False(noRestore);
+ Assert.True(options.NoLaunchProfile);
+ Assert.Equal(["--operation", "publish"], args);
+ Assert.Equal("Production", env!["DOTNET_ENVIRONMENT"]);
+ Assert.False(env.ContainsKey("ASPNETCORE_ENVIRONMENT"));
+ Assert.Equal("https://localhost:19000;http://localhost:15000", env["ASPNETCORE_URLS"]);
+ Assert.Equal("https://localhost:21000", env["ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL"]);
+ Assert.Equal("https://localhost:22000", env["ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL"]);
+ Assert.False(env.ContainsKey("ASPIRE_ENVIRONMENT"));
+ return Task.FromResult(0);
+ };
+
+ var exitCode = await project.PublishAsync(new PublishContext
+ {
+ AppHostFile = appHostFile,
+ WorkingDirectory = _workspace.WorkspaceRoot,
+ Arguments = ["--operation", "publish"],
+ EnvironmentVariables = new Dictionary()
+ }, CancellationToken.None);
+
+ Assert.Equal(0, exitCode);
+ }
+
+ [Fact]
+ public async Task PublishAsync_SingleFileAppHostUsesEnvironmentArgumentWhenProvided()
+ {
+ var appHostFile = CreateSingleFileAppHost();
+ var runner = new TestDotNetCliRunner();
+ var project = CreateDotNetAppHostProject(runner);
+
+ runner.RunAsyncCallback = (projectFile, watch, noBuild, noRestore, args, env, _, options, _) =>
+ {
+ Assert.Equal(appHostFile.FullName, projectFile.FullName);
+ Assert.False(watch);
+ Assert.True(noBuild);
+ Assert.False(noRestore);
+ Assert.True(options.NoLaunchProfile);
+ Assert.Equal(["--operation", "publish", "--environment", "Staging"], args);
+ Assert.Equal("Staging", env!["DOTNET_ENVIRONMENT"]);
+ Assert.False(env.ContainsKey("ASPNETCORE_ENVIRONMENT"));
+ return Task.FromResult(0);
+ };
+
+ var exitCode = await project.PublishAsync(new PublishContext
+ {
+ AppHostFile = appHostFile,
+ WorkingDirectory = _workspace.WorkspaceRoot,
+ Arguments = ["--operation", "publish", "--environment", "Staging"],
+ EnvironmentVariables = new Dictionary()
+ }, CancellationToken.None);
+
+ Assert.Equal(0, exitCode);
+ }
+
+ private FileInfo CreateSingleFileAppHost()
+ {
+ var appHostPath = Path.Combine(_workspace.WorkspaceRoot.FullName, "apphost.cs");
+ File.WriteAllText(appHostPath, """
+ #:sdk Aspire.AppHost.Sdk@13.0.0
+
+ var builder = DistributedApplication.CreateBuilder(args);
+ builder.Build().Run();
+ """);
+
+ return new FileInfo(appHostPath);
+ }
+
+ private DotNetAppHostProject CreateDotNetAppHostProject(TestDotNetCliRunner runner)
+ {
+ var services = CliTestHelper.CreateServiceCollection(_workspace, outputHelper, options =>
+ {
+ options.DotNetCliRunnerFactory = _ => runner;
+ });
+
+ var provider = services.BuildServiceProvider();
+ _serviceProviders.Add(provider);
+ return provider.GetRequiredService();
+ }
+}
diff --git a/tests/Aspire.Cli.Tests/Projects/GuestAppHostProjectTests.cs b/tests/Aspire.Cli.Tests/Projects/GuestAppHostProjectTests.cs
index 380a0c555c8..5f8e9ccebf2 100644
--- a/tests/Aspire.Cli.Tests/Projects/GuestAppHostProjectTests.cs
+++ b/tests/Aspire.Cli.Tests/Projects/GuestAppHostProjectTests.cs
@@ -13,6 +13,8 @@ namespace Aspire.Cli.Tests.Projects;
public class GuestAppHostProjectTests(ITestOutputHelper outputHelper) : IDisposable
{
+ private const string AspNetCoreEnvironmentVariableName = "ASPNETCORE_ENVIRONMENT";
+
private readonly TemporaryWorkspace _workspace = TemporaryWorkspace.Create(outputHelper);
public void Dispose()
@@ -333,6 +335,62 @@ public void GetServerEnvironmentVariables_ParsesLaunchSettingsWithComments()
Assert.False(envVars.ContainsKey("ASPIRE_DASHBOARD_OTLP_HTTP_ENDPOINT_URL"));
}
+ [Fact]
+ public void GetServerEnvironmentVariables_UsesRequestedDefaultEnvironment()
+ {
+ var envVars = GuestAppHostProject.GetServerEnvironmentVariables(
+ launchProfileEnvironmentVariables: null,
+ defaultEnvironment: AppHostEnvironmentDefaults.ProductionEnvironmentName,
+ inheritedEnvironmentVariables: new Dictionary());
+
+ Assert.Equal("Production", envVars["DOTNET_ENVIRONMENT"]);
+ Assert.False(envVars.ContainsKey("ASPNETCORE_ENVIRONMENT"));
+ }
+
+ [Fact]
+ public void GetServerEnvironmentVariables_IgnoresLaunchProfileEnvironmentVariablesWhenRequested()
+ {
+ var envVars = GuestAppHostProject.GetServerEnvironmentVariables(
+ launchProfileEnvironmentVariables: new Dictionary
+ {
+ ["ASPNETCORE_URLS"] = "https://localhost:16319;http://localhost:16320",
+ ["ASPNETCORE_ENVIRONMENT"] = "Development",
+ ["DOTNET_ENVIRONMENT"] = "Development",
+ ["ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL"] = "https://localhost:17269",
+ ["ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL"] = "https://localhost:18269"
+ },
+ defaultEnvironment: AppHostEnvironmentDefaults.ProductionEnvironmentName,
+ includeLaunchProfileEnvironmentVariables: false,
+ inheritedEnvironmentVariables: new Dictionary());
+
+ Assert.Equal("Production", envVars["DOTNET_ENVIRONMENT"]);
+ Assert.False(envVars.ContainsKey("ASPNETCORE_ENVIRONMENT"));
+ Assert.Equal("https://localhost:16319;http://localhost:16320", envVars["ASPNETCORE_URLS"]);
+ Assert.Equal("https://localhost:17269", envVars["ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL"]);
+ Assert.Equal("https://localhost:18269", envVars["ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL"]);
+ Assert.False(envVars.ContainsKey("ASPIRE_ENVIRONMENT"));
+ }
+
+ [Fact]
+ public void GetServerEnvironmentVariables_EnvironmentArgumentTakesPrecedenceOverLaunchProfileEnvironmentVariables()
+ {
+ var envVars = GuestAppHostProject.GetServerEnvironmentVariables(
+ launchProfileEnvironmentVariables: new Dictionary
+ {
+ ["ASPNETCORE_URLS"] = "https://localhost:16319;http://localhost:16320",
+ ["ASPIRE_ENVIRONMENT"] = "Development",
+ ["ASPNETCORE_ENVIRONMENT"] = "Development",
+ ["DOTNET_ENVIRONMENT"] = "Development",
+ },
+ defaultEnvironment: AppHostEnvironmentDefaults.ProductionEnvironmentName,
+ inheritedEnvironmentVariables: new Dictionary(),
+ args: ["--environment", "Staging"]);
+
+ Assert.Equal("Staging", envVars["DOTNET_ENVIRONMENT"]);
+ Assert.Equal("Development", envVars["ASPNETCORE_ENVIRONMENT"]);
+ Assert.Equal("Development", envVars["ASPIRE_ENVIRONMENT"]);
+ }
+
[Fact]
public void CreateGuestEnvironmentVariables_MergesLaunchProfileContextAndAdditionalEnvironmentVariables()
{
@@ -370,12 +428,108 @@ public void CreateGuestEnvironmentVariables_MergesLaunchProfileContextAndAdditio
Assert.Equal("https://localhost:16319;http://localhost:16320", envVars["ASPNETCORE_URLS"]);
Assert.Equal("Staging", envVars["ASPIRE_ENVIRONMENT"]);
Assert.Equal("Staging", envVars["DOTNET_ENVIRONMENT"]);
- Assert.Equal("Staging", envVars["ASPNETCORE_ENVIRONMENT"]);
+ Assert.False(envVars.ContainsKey("ASPNETCORE_ENVIRONMENT"));
Assert.Equal("https://localhost:17269", envVars["ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL"]);
Assert.Equal("https://localhost:18269", envVars["ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL"]);
Assert.Equal("/tmp/certs", envVars["SSL_CERT_DIR"]);
}
+ [Fact]
+ public void CreateGuestEnvironmentVariables_IgnoresLaunchProfileEnvironmentVariablesWhenRequested()
+ {
+ var envVars = GuestAppHostProject.CreateGuestEnvironmentVariables(
+ contextEnvironmentVariables: new Dictionary(),
+ launchProfileEnvironmentVariables: new Dictionary
+ {
+ ["ASPNETCORE_URLS"] = "https://localhost:16319;http://localhost:16320",
+ ["ASPIRE_ENVIRONMENT"] = "Development",
+ ["ASPNETCORE_ENVIRONMENT"] = "Development",
+ ["DOTNET_ENVIRONMENT"] = "Development",
+ ["ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL"] = "https://localhost:17269",
+ ["ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL"] = "https://localhost:18269"
+ },
+ defaultEnvironment: AppHostEnvironmentDefaults.ProductionEnvironmentName,
+ includeLaunchProfileEnvironmentVariables: false,
+ inheritedEnvironmentVariables: new Dictionary());
+
+ Assert.Equal("Production", envVars["DOTNET_ENVIRONMENT"]);
+ Assert.False(envVars.ContainsKey("ASPNETCORE_ENVIRONMENT"));
+ Assert.Equal("https://localhost:16319;http://localhost:16320", envVars["ASPNETCORE_URLS"]);
+ Assert.Equal("https://localhost:17269", envVars["ASPIRE_DASHBOARD_OTLP_ENDPOINT_URL"]);
+ Assert.Equal("https://localhost:18269", envVars["ASPIRE_RESOURCE_SERVICE_ENDPOINT_URL"]);
+ Assert.False(envVars.ContainsKey("ASPIRE_ENVIRONMENT"));
+ }
+
+ [Fact]
+ public void CreateGuestEnvironmentVariables_EnvironmentArgumentTakesPrecedenceOverLaunchProfileEnvironmentVariables()
+ {
+ var envVars = GuestAppHostProject.CreateGuestEnvironmentVariables(
+ contextEnvironmentVariables: new Dictionary(),
+ launchProfileEnvironmentVariables: new Dictionary
+ {
+ ["ASPIRE_ENVIRONMENT"] = "Development",
+ ["ASPNETCORE_ENVIRONMENT"] = "Development",
+ ["DOTNET_ENVIRONMENT"] = "Development",
+ },
+ defaultEnvironment: AppHostEnvironmentDefaults.ProductionEnvironmentName,
+ inheritedEnvironmentVariables: new Dictionary(),
+ args: ["--environment", "Staging"]);
+
+ Assert.Equal("Staging", envVars["DOTNET_ENVIRONMENT"]);
+ Assert.Equal("Development", envVars["ASPNETCORE_ENVIRONMENT"]);
+ Assert.Equal("Development", envVars["ASPIRE_ENVIRONMENT"]);
+ }
+
+ [Fact]
+ public void CreateGuestEnvironmentVariables_InheritedAspireEnvironmentOverridesDefaultEnvironment()
+ {
+ var envVars = GuestAppHostProject.CreateGuestEnvironmentVariables(
+ contextEnvironmentVariables: new Dictionary(),
+ launchProfileEnvironmentVariables: null,
+ defaultEnvironment: AppHostEnvironmentDefaults.ProductionEnvironmentName,
+ inheritedEnvironmentVariables: new Dictionary
+ {
+ [AppHostEnvironmentDefaults.AspireEnvironmentVariableName] = "Staging"
+ });
+
+ Assert.Equal("Staging", envVars["DOTNET_ENVIRONMENT"]);
+ Assert.False(envVars.ContainsKey("ASPNETCORE_ENVIRONMENT"));
+ }
+
+ [Fact]
+ public void CreateGuestEnvironmentVariables_DotnetEnvironmentTakesPrecedenceOverAspireEnvironment()
+ {
+ var envVars = GuestAppHostProject.CreateGuestEnvironmentVariables(
+ contextEnvironmentVariables: new Dictionary
+ {
+ [AppHostEnvironmentDefaults.DotNetEnvironmentVariableName] = "Production",
+ [AppHostEnvironmentDefaults.AspireEnvironmentVariableName] = "Staging"
+ },
+ launchProfileEnvironmentVariables: null,
+ inheritedEnvironmentVariables: new Dictionary());
+
+ Assert.Equal("Production", envVars["DOTNET_ENVIRONMENT"]);
+ Assert.False(envVars.ContainsKey("ASPNETCORE_ENVIRONMENT"));
+ Assert.Equal("Staging", envVars["ASPIRE_ENVIRONMENT"]);
+ }
+
+ [Fact]
+ public void CreateGuestEnvironmentVariables_AspireEnvironmentTakesPrecedenceOverAspNetCoreEnvironment()
+ {
+ var envVars = GuestAppHostProject.CreateGuestEnvironmentVariables(
+ contextEnvironmentVariables: new Dictionary
+ {
+ [AppHostEnvironmentDefaults.AspireEnvironmentVariableName] = "Testing",
+ [AspNetCoreEnvironmentVariableName] = "Staging"
+ },
+ launchProfileEnvironmentVariables: null,
+ inheritedEnvironmentVariables: new Dictionary());
+
+ Assert.Equal("Testing", envVars["DOTNET_ENVIRONMENT"]);
+ Assert.Equal("Staging", envVars["ASPNETCORE_ENVIRONMENT"]);
+ Assert.Equal("Testing", envVars["ASPIRE_ENVIRONMENT"]);
+ }
+
private static GuestAppHostProject CreateGuestAppHostProject()
{
var language = new LanguageInfo(
diff --git a/tests/Aspire.Hosting.Tests/AspireEnvironmentTests.cs b/tests/Aspire.Hosting.Tests/AspireEnvironmentTests.cs
new file mode 100644
index 00000000000..ad32be12366
--- /dev/null
+++ b/tests/Aspire.Hosting.Tests/AspireEnvironmentTests.cs
@@ -0,0 +1,151 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using Microsoft.DotNet.RemoteExecutor;
+using Microsoft.Extensions.Hosting;
+
+namespace Aspire.Hosting.Tests;
+
+[Trait("Partition", "5")]
+public class AspireEnvironmentTests
+{
+ [Fact]
+ public void AspireEnvironmentSetsBuilderEnvironment()
+ {
+ var options = CreateEnvironmentOptions(aspireEnvironment: "Staging");
+
+ RemoteExecutor.Invoke(static () =>
+ {
+ var builder = DistributedApplication.CreateBuilder(new DistributedApplicationOptions { DisableDashboard = true });
+ Assert.Equal("Staging", builder.Environment.EnvironmentName);
+ }, options).Dispose();
+ }
+
+ [Fact]
+ public void DotnetEnvironmentTakesPrecedenceOverAspireEnvironment()
+ {
+ var options = CreateEnvironmentOptions(aspireEnvironment: "Staging", dotnetEnvironment: "Production");
+
+ RemoteExecutor.Invoke(static () =>
+ {
+ var builder = DistributedApplication.CreateBuilder(new DistributedApplicationOptions { DisableDashboard = true });
+ Assert.Equal("Production", builder.Environment.EnvironmentName);
+ }, options).Dispose();
+ }
+
+ [Fact]
+ public void DotnetEnvironmentTakesPrecedenceOverAspNetCoreEnvironment()
+ {
+ var options = CreateEnvironmentOptions(dotnetEnvironment: "Production", aspNetCoreEnvironment: "Staging");
+
+ RemoteExecutor.Invoke(static () =>
+ {
+ var builder = DistributedApplication.CreateBuilder(new DistributedApplicationOptions { DisableDashboard = true });
+ Assert.Equal("Production", builder.Environment.EnvironmentName);
+ }, options).Dispose();
+ }
+
+ [Fact]
+ public void AspireEnvironmentTakesPrecedenceOverAspNetCoreEnvironment()
+ {
+ var options = CreateEnvironmentOptions(aspireEnvironment: "Testing", aspNetCoreEnvironment: "Staging");
+
+ RemoteExecutor.Invoke(static () =>
+ {
+ var builder = DistributedApplication.CreateBuilder(new DistributedApplicationOptions { DisableDashboard = true });
+ Assert.Equal("Testing", builder.Environment.EnvironmentName);
+ }, options).Dispose();
+ }
+
+ [Fact]
+ public void AspNetCoreEnvironmentDoesNotSetBuilderEnvironment()
+ {
+ var options = CreateEnvironmentOptions(aspNetCoreEnvironment: "Staging");
+
+ RemoteExecutor.Invoke(static () =>
+ {
+ var builder = DistributedApplication.CreateBuilder(new DistributedApplicationOptions { DisableDashboard = true });
+ Assert.Equal("Production", builder.Environment.EnvironmentName);
+ }, options).Dispose();
+ }
+
+ [Fact]
+ public void EnvironmentFlagTakesPrecedenceOverAspireEnvironment()
+ {
+ var options = CreateEnvironmentOptions(aspireEnvironment: "Staging");
+
+ RemoteExecutor.Invoke(static () =>
+ {
+ var builder = DistributedApplication.CreateBuilder(new DistributedApplicationOptions
+ {
+ DisableDashboard = true,
+ Args = ["--environment", "Production"]
+ });
+ Assert.Equal("Production", builder.Environment.EnvironmentName);
+ }, options).Dispose();
+ }
+
+ [Fact]
+ public void DefaultEnvironmentIsProductionWithNoEnvVars()
+ {
+ var options = CreateEnvironmentOptions();
+
+ RemoteExecutor.Invoke(static () =>
+ {
+ var builder = DistributedApplication.CreateBuilder(new DistributedApplicationOptions { DisableDashboard = true });
+ Assert.Equal("Production", builder.Environment.EnvironmentName);
+ }, options).Dispose();
+ }
+
+ [Fact]
+ public void AspireEnvironmentSetsCustomEnvironmentName()
+ {
+ var options = CreateEnvironmentOptions(aspireEnvironment: "Testing");
+
+ RemoteExecutor.Invoke(static () =>
+ {
+ var builder = DistributedApplication.CreateBuilder(new DistributedApplicationOptions { DisableDashboard = true });
+ Assert.Equal("Testing", builder.Environment.EnvironmentName);
+ Assert.False(builder.Environment.IsDevelopment());
+ Assert.False(builder.Environment.IsProduction());
+ Assert.True(builder.Environment.IsEnvironment("Testing"));
+ }, options).Dispose();
+ }
+
+ private static RemoteInvokeOptions CreateEnvironmentOptions(
+ string? aspireEnvironment = null,
+ string? dotnetEnvironment = null,
+ string? aspNetCoreEnvironment = null)
+ {
+ var options = new RemoteInvokeOptions();
+
+ if (aspireEnvironment is not null)
+ {
+ options.StartInfo.Environment["ASPIRE_ENVIRONMENT"] = aspireEnvironment;
+ }
+ else
+ {
+ options.StartInfo.Environment.Remove("ASPIRE_ENVIRONMENT");
+ }
+
+ if (dotnetEnvironment is not null)
+ {
+ options.StartInfo.Environment["DOTNET_ENVIRONMENT"] = dotnetEnvironment;
+ }
+ else
+ {
+ options.StartInfo.Environment.Remove("DOTNET_ENVIRONMENT");
+ }
+
+ if (aspNetCoreEnvironment is not null)
+ {
+ options.StartInfo.Environment["ASPNETCORE_ENVIRONMENT"] = aspNetCoreEnvironment;
+ }
+ else
+ {
+ options.StartInfo.Environment.Remove("ASPNETCORE_ENVIRONMENT");
+ }
+
+ return options;
+ }
+}