Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
168 changes: 168 additions & 0 deletions src/Aspire.Cli/Projects/AppHostEnvironmentDefaults.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Resolves the effective AppHost environment for CLI-launched processes.
/// </summary>
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";

/// <summary>
/// Determines whether the variable name should be treated as an environment-selection variable
/// when filtering launch profile values.
/// </summary>
internal static bool IsEnvironmentVariableName(string variableName) =>
variableName is DotNetEnvironmentVariableName or AspNetCoreEnvironmentVariableName or AspireEnvironmentVariableName;

/// <summary>
/// Applies the effective environment to the launch environment variables.
/// </summary>
/// <param name="environmentVariables">The environment variables passed to the launched process.</param>
/// <param name="defaultEnvironment">The fallback environment used when no explicit environment is provided.</param>
/// <param name="inheritedEnvironmentVariables">Optional inherited environment variables used by tests.</param>
/// <param name="args">Optional command-line arguments that may contain <c>--environment</c>.</param>
internal static void ApplyEffectiveEnvironment(
IDictionary<string, string> environmentVariables,
string? defaultEnvironment = null,
IReadOnlyDictionary<string, string?>? 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<string, string> environmentVariables,
IReadOnlyDictionary<string, string?>? 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<string, string> 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<string, string?>? 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;
}
}
88 changes: 80 additions & 8 deletions src/Aspire.Cli/Projects/DotNetAppHostProject.cs
Original file line number Diff line number Diff line change
Expand Up @@ -332,7 +332,7 @@ public async Task<int> 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
Expand Down Expand Up @@ -362,17 +362,89 @@ public async Task<int> RunAsync(AppHostProjectContext context, CancellationToken
}
}

private static void ConfigureSingleFileEnvironment(FileInfo appHostFile, Dictionary<string, string> env)
internal static void ConfigureSingleFileRunEnvironment(
FileInfo appHostFile,
Dictionary<string, string> env,
IReadOnlyDictionary<string, string?>? 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<string, string> env,
IReadOnlyDictionary<string, string?>? 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<string, string> 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<string, string> 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";
}

/// <inheritdoc />
Expand Down Expand Up @@ -461,7 +533,7 @@ public async Task<int> PublishAsync(PublishContext context, CancellationToken ca

if (isSingleFileAppHost)
{
ConfigureSingleFileEnvironment(effectiveAppHostFile, env);
ConfigureSingleFilePublishEnvironment(effectiveAppHostFile, env, args: context.Arguments);
}

return await _runner.RunAsync(
Expand Down
Loading
Loading