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; + } +}