Skip to content
Draft
Show file tree
Hide file tree
Changes from 5 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
Original file line number Diff line number Diff line change
Expand Up @@ -234,6 +234,8 @@ private sealed class Builder(SuspendingDistributedApplicationFactory factory, Di

public IHostEnvironment Environment => innerBuilder.Environment;

public IAppHostEnvironment AppHostEnvironment => innerBuilder.AppHostEnvironment;

public IServiceCollection Services => innerBuilder.Services;

public DistributedApplicationExecutionContext ExecutionContext => innerBuilder.ExecutionContext;
Expand Down Expand Up @@ -386,6 +388,8 @@ static Assembly FindApplicationAssembly()

public IHostEnvironment Environment => _innerBuilder.Environment;

public IAppHostEnvironment AppHostEnvironment => _innerBuilder.AppHostEnvironment;

public IServiceCollection Services => _innerBuilder.Services;

public DistributedApplicationExecutionContext ExecutionContext => _innerBuilder.ExecutionContext;
Expand Down
63 changes: 63 additions & 0 deletions src/Aspire.Hosting/AppHostEnvironment.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;

namespace Aspire.Hosting;

/// <summary>
/// Provides information about the AppHost environment.
/// </summary>
internal sealed class AppHostEnvironment : IAppHostEnvironment
{
private readonly IConfiguration _configuration;
private readonly IHostEnvironment _hostEnvironment;

/// <summary>
/// Initializes a new instance of the <see cref="AppHostEnvironment"/> class.
/// </summary>
/// <param name="configuration">The configuration.</param>
/// <param name="hostEnvironment">The host environment.</param>
public AppHostEnvironment(IConfiguration configuration, IHostEnvironment hostEnvironment)
{
_configuration = configuration;
_hostEnvironment = hostEnvironment;
}

/// <inheritdoc />
public string ProjectName => _configuration["AppHost:DashboardApplicationName"] ?? _hostEnvironment.ApplicationName;

/// <inheritdoc />
public string ProjectDirectory => _configuration["AppHost:Directory"]!;

/// <inheritdoc />
public string FullPath => _configuration["AppHost:Path"]!;

/// <inheritdoc />
public string DashboardApplicationName => _configuration["AppHost:DashboardApplicationName"] ?? _hostEnvironment.ApplicationName;

/// <inheritdoc />
public string DefaultHash => _configuration["AppHost:Sha256"]!;

/// <inheritdoc />
public string FullPathHash => _configuration["AppHost:PathSha256"]!;

/// <inheritdoc />
public string ProjectNameSha256 => _configuration["AppHost:ProjectNameSha256"]!;

/// <inheritdoc />
public string? ContainerHostname => _configuration["AppHost:ContainerHostname"];

/// <inheritdoc />
public string? DefaultLaunchProfileName => _configuration["AppHost:DefaultLaunchProfileName"];

/// <inheritdoc />
public string? OtlpApiKey => _configuration["AppHost:OtlpApiKey"];

/// <inheritdoc />
public string? ResourceServiceApiKey => _configuration["AppHost:ResourceService:ApiKey"];

/// <inheritdoc />
public string? ResourceServiceAuthMode => _configuration["AppHost:ResourceService:AuthMode"];
}
7 changes: 4 additions & 3 deletions src/Aspire.Hosting/Dashboard/DashboardEventHandlers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@

namespace Aspire.Hosting.Dashboard;

internal sealed class DashboardEventHandlers(IConfiguration configuration,
internal sealed class DashboardEventHandlers(IAppHostEnvironment appHostEnvironment,
IConfiguration configuration,
IOptions<DashboardOptions> dashboardOptions,
ILogger<DistributedApplication> distributedApplicationLogger,
IDashboardEndpointProvider dashboardEndpointProvider,
Expand Down Expand Up @@ -525,8 +526,8 @@ internal async Task ConfigureEnvironmentVariables(EnvironmentCallbackContext con
}

// Configure resource service API key
if (string.Equals(configuration["AppHost:ResourceService:AuthMode"], nameof(ResourceServiceAuthMode.ApiKey), StringComparison.OrdinalIgnoreCase)
&& configuration["AppHost:ResourceService:ApiKey"] is { Length: > 0 } resourceServiceApiKey)
if (string.Equals(appHostEnvironment.ResourceServiceAuthMode, nameof(ResourceServiceAuthMode.ApiKey), StringComparison.OrdinalIgnoreCase)
&& appHostEnvironment.ResourceServiceApiKey is { Length: > 0 } resourceServiceApiKey)
{
context.EnvironmentVariables[DashboardConfigNames.ResourceServiceClientAuthModeName.EnvVarName] = nameof(ResourceServiceAuthMode.ApiKey);
context.EnvironmentVariables[DashboardConfigNames.ResourceServiceClientApiKeyName.EnvVarName] = resourceServiceApiKey;
Expand Down
4 changes: 2 additions & 2 deletions src/Aspire.Hosting/Dashboard/DashboardOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ internal class DashboardOptions
public bool? TelemetryOptOut { get; set; }
}

internal class ConfigureDefaultDashboardOptions(IConfiguration configuration, IOptions<DcpOptions> dcpOptions) : IConfigureOptions<DashboardOptions>
internal class ConfigureDefaultDashboardOptions(IAppHostEnvironment appHostEnvironment, IOptions<DcpOptions> dcpOptions, IConfiguration configuration) : IConfigureOptions<DashboardOptions>
{
public void Configure(DashboardOptions options)
{
Expand All @@ -29,7 +29,7 @@ public void Configure(DashboardOptions options)

options.OtlpGrpcEndpointUrl = configuration.GetString(KnownConfigNames.DashboardOtlpGrpcEndpointUrl, KnownConfigNames.Legacy.DashboardOtlpGrpcEndpointUrl);
options.OtlpHttpEndpointUrl = configuration.GetString(KnownConfigNames.DashboardOtlpHttpEndpointUrl, KnownConfigNames.Legacy.DashboardOtlpHttpEndpointUrl);
options.OtlpApiKey = configuration["AppHost:OtlpApiKey"];
options.OtlpApiKey = appHostEnvironment.OtlpApiKey;

options.AspNetCoreEnvironment = configuration["ASPNETCORE_ENVIRONMENT"] ?? "Production";

Expand Down
7 changes: 3 additions & 4 deletions src/Aspire.Hosting/Dashboard/DashboardService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
using Aspire.DashboardService.Proto.V1;
using Grpc.Core;
using Microsoft.AspNetCore.Authorization;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using static Aspire.Hosting.Interaction;
Expand All @@ -20,7 +19,7 @@ namespace Aspire.Hosting.Dashboard;
/// required beyond a single request. Longer-scoped data is stored in <see cref="DashboardServiceData"/>.
/// </remarks>
[Authorize(Policy = ResourceServiceApiKeyAuthorization.PolicyName)]
internal sealed partial class DashboardService(DashboardServiceData serviceData, IHostEnvironment hostEnvironment, IHostApplicationLifetime hostApplicationLifetime, IConfiguration configuration, ILogger<DashboardService> logger)
internal sealed partial class DashboardService(DashboardServiceData serviceData, IAppHostEnvironment appHostEnvironment, IHostApplicationLifetime hostApplicationLifetime, ILogger<DashboardService> logger)
: Aspire.DashboardService.Proto.V1.DashboardService.DashboardServiceBase
{
// gRPC has a maximum receive size of 4MB. Force logs into batches to avoid exceeding receive size.
Expand All @@ -38,8 +37,8 @@ public override Task<ApplicationInformationResponse> GetApplicationInformation(
ApplicationInformationRequest request,
ServerCallContext context)
{
// Read the application name from configuration if available, otherwise fall back to the environment
var applicationName = configuration["AppHost:DashboardApplicationName"] ?? hostEnvironment.ApplicationName;
// Use the dashboard application name from the AppHostEnvironment
var applicationName = appHostEnvironment.DashboardApplicationName;

return Task.FromResult(new ApplicationInformationResponse
{
Expand Down
9 changes: 6 additions & 3 deletions src/Aspire.Hosting/Dcp/DcpExecutor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
// The .NET Foundation licenses this file to you under the MIT license.

#pragma warning disable ASPIREEXTENSION001
#pragma warning disable IDE0005 // IConfiguration is used in this file
using System.Collections.Concurrent;
using System.Collections.Immutable;
using System.Data;
Expand Down Expand Up @@ -93,11 +94,12 @@ internal sealed partial class DcpExecutor : IDcpExecutor, IConsoleLogsService, I
private readonly record struct LogInformationEntry(string ResourceName, bool? LogsAvailable, bool? HasSubscribers);
private readonly Channel<LogInformationEntry> _logInformationChannel = Channel.CreateUnbounded<LogInformationEntry>(
new UnboundedChannelOptions { SingleReader = true });
private readonly IAppHostEnvironment _appHostEnvironment;

public DcpExecutor(ILogger<DcpExecutor> logger,
ILogger<DistributedApplication> distributedApplicationLogger,
DistributedApplicationModel model,
IHostEnvironment hostEnvironment,
IAppHostEnvironment appHostEnvironment,
IKubernetesService kubernetesService,
IConfiguration configuration,
IDistributedApplicationEventing distributedApplicationEventing,
Expand Down Expand Up @@ -126,16 +128,17 @@ public DcpExecutor(ILogger<DcpExecutor> logger,
_executionContext = executionContext;
_resourceState = new(model.Resources.ToDictionary(r => r.Name), _appResources);
_snapshotBuilder = new(_resourceState);
_normalizedApplicationName = NormalizeApplicationName(hostEnvironment.ApplicationName);
_normalizedApplicationName = NormalizeApplicationName(appHostEnvironment.ProjectName);
_locations = locations;
_developerCertificateService = developerCertificateService;
_appHostEnvironment = appHostEnvironment;

DeleteResourceRetryPipeline = DcpPipelineBuilder.BuildDeleteRetryPipeline(logger);
CreateServiceRetryPipeline = DcpPipelineBuilder.BuildCreateServiceRetryPipeline(options.Value, logger);
WatchResourceRetryPipeline = DcpPipelineBuilder.BuildWatchResourcePipeline(logger);
}

private string DefaultContainerHostName => _configuration["AppHost:ContainerHostname"] ?? _dcpInfo?.Containers?.ContainerHostName ?? "host.docker.internal";
private string DefaultContainerHostName => _appHostEnvironment.ContainerHostname ?? _dcpInfo?.Containers?.ContainerHostName ?? "host.docker.internal";

public async Task RunApplicationAsync(CancellationToken cancellationToken = default)
{
Expand Down
9 changes: 4 additions & 5 deletions src/Aspire.Hosting/Dcp/DcpNameGenerator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
using System.Collections.Immutable;
using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.Utils;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Options;

namespace Aspire.Hosting.Dcp;
Expand All @@ -16,12 +15,12 @@ internal sealed class DcpNameGenerator
// The length of 8 achieves that while keeping the names relatively short and readable.
// The second purpose of the suffix is to play a role of a unique OpenTelemetry service instance ID.
private const int RandomNameSuffixLength = 8;
private readonly IConfiguration _configuration;
private readonly IAppHostEnvironment _appHostEnvironment;
private readonly IOptions<DcpOptions> _options;

public DcpNameGenerator(IConfiguration configuration, IOptions<DcpOptions> options)
public DcpNameGenerator(IAppHostEnvironment appHostEnvironment, IOptions<DcpOptions> options)
{
_configuration = configuration;
_appHostEnvironment = appHostEnvironment;
_options = options;
}

Expand Down Expand Up @@ -115,7 +114,7 @@ public static string GetRandomNameSuffix()
public string GetProjectHashSuffix()
{
// Compute a short hash of the content root path to differentiate between multiple AppHost projects with similar resource names
var suffix = _configuration["AppHost:Sha256"]!.Substring(0, RandomNameSuffixLength).ToLowerInvariant();
var suffix = _appHostEnvironment.DefaultHash.Substring(0, RandomNameSuffixLength).ToLowerInvariant();
return suffix;
}

Expand Down
9 changes: 9 additions & 0 deletions src/Aspire.Hosting/DistributedApplicationBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -60,10 +60,14 @@ public class DistributedApplicationBuilder : IDistributedApplicationBuilder

private readonly DistributedApplicationOptions _options;
private readonly HostApplicationBuilder _innerBuilder;
private readonly AppHostEnvironment _appHostEnvironment;

/// <inheritdoc />
public IHostEnvironment Environment => _innerBuilder.Environment;

/// <inheritdoc />
public IAppHostEnvironment AppHostEnvironment => _appHostEnvironment;

/// <inheritdoc />
public ConfigurationManager Configuration => _innerBuilder.Configuration;

Expand Down Expand Up @@ -466,6 +470,11 @@ public DistributedApplicationBuilder(DistributedApplicationOptions options)
}

_innerBuilder.Services.AddSingleton(ExecutionContext);
_innerBuilder.Services.AddSingleton<IAppHostEnvironment, AppHostEnvironment>();

// Initialize the AppHostEnvironment for use within the builder
_appHostEnvironment = new AppHostEnvironment(_innerBuilder.Configuration, _innerBuilder.Environment);

LogBuilderConstructed(this);
}

Expand Down
5 changes: 2 additions & 3 deletions src/Aspire.Hosting/DistributedApplicationLifecycle.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,14 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System.Reflection;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;

namespace Aspire.Hosting;

internal sealed class DistributedApplicationLifecycle(
ILogger<DistributedApplication> logger,
IConfiguration configuration,
IAppHostEnvironment appHostEnvironment,
DistributedApplicationExecutionContext executionContext,
LocaleOverrideContext localeOverrideContext) : IHostedLifecycleService
{
Expand Down Expand Up @@ -41,7 +40,7 @@ public Task StartingAsync(CancellationToken cancellationToken)
if (executionContext.IsRunMode)
{
logger.LogInformation("Distributed application starting.");
logger.LogInformation("Application host directory is: {AppHostDirectory}", configuration["AppHost:Directory"]);
logger.LogInformation("Application host directory is: {AppHostDirectory}", appHostEnvironment.ProjectDirectory);
}

if (localeOverrideContext.OverrideErrorMessage is { Length: > 0 } localOverrideError)
Expand Down
85 changes: 85 additions & 0 deletions src/Aspire.Hosting/IAppHostEnvironment.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

namespace Aspire.Hosting;

/// <summary>
/// Provides information about the AppHost environment.
/// </summary>
public interface IAppHostEnvironment
{
/// <summary>
/// Gets the name of the AppHost project.
/// </summary>
/// <remarks>
/// This is the project name used in multiple places throughout the application,
/// including for generating resource names and configuration keys.
/// </remarks>
string ProjectName { get; }

/// <summary>
/// Gets the directory of the project where the app host is located.
/// </summary>
string ProjectDirectory { get; }

/// <summary>
/// Gets the full path to the app host.
/// </summary>
string FullPath { get; }

/// <summary>
/// Gets the application name used for the dashboard.
/// </summary>
string DashboardApplicationName { get; }

/// <summary>
/// Gets the SHA256 hash of the app host.
/// </summary>
/// <remarks>
/// For backward compatibility, this uses mode-dependent logic:
/// - Publish mode: ProjectNameSha (stable across paths)
/// - Run mode: FullPathHash (disambiguates by path)
/// </remarks>
string DefaultHash { get; }

/// <summary>
/// Gets the SHA256 hash based on the app host path.
/// </summary>
/// <remarks>
/// Used for disambiguating projects with the same name in different locations (deployment state).
/// </remarks>
string FullPathHash { get; }

/// <summary>
/// Gets the SHA256 hash based on the project name.
/// </summary>
/// <remarks>
/// Used for stable naming across deployments regardless of path (Azure Functions, Azure environments).
/// </remarks>
string ProjectNameSha256 { get; }
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ProjectNameHash

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in commit d7e6daf - renamed to ProjectNameHash


/// <summary>
/// Gets the container hostname.
/// </summary>
string? ContainerHostname { get; }

/// <summary>
/// Gets the default launch profile name.
/// </summary>
string? DefaultLaunchProfileName { get; }

/// <summary>
/// Gets the OTLP API key.
/// </summary>
string? OtlpApiKey { get; }

/// <summary>
/// Gets the resource service API key.
/// </summary>
string? ResourceServiceApiKey { get; }

/// <summary>
/// Gets the resource service authentication mode.
/// </summary>
string? ResourceServiceAuthMode { get; }
}
5 changes: 5 additions & 0 deletions src/Aspire.Hosting/IDistributedApplicationBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,11 @@ public interface IDistributedApplicationBuilder
/// <inheritdoc cref="HostApplicationBuilder.Environment" />
public IHostEnvironment Environment { get; }

/// <summary>
/// Provides information about the AppHost environment.
/// </summary>
public IAppHostEnvironment AppHostEnvironment => throw new NotImplementedException();

/// <inheritdoc cref="HostApplicationBuilder.Services" />
public IServiceCollection Services { get; }

Expand Down
3 changes: 2 additions & 1 deletion src/Aspire.Hosting/OtlpConfigurationExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,8 @@ private static void RegisterOtlpEnvironment(IResource resource, IConfiguration c
context.EnvironmentVariables["OTEL_RESOURCE_ATTRIBUTES"] = "service.instance.id={{- index .Annotations \"" + CustomResource.OtelServiceInstanceIdAnnotation + "\" -}}";
context.EnvironmentVariables["OTEL_SERVICE_NAME"] = "{{- index .Annotations \"" + CustomResource.OtelServiceNameAnnotation + "\" -}}";

if (configuration["AppHost:OtlpApiKey"] is { } otlpApiKey)
var appHostEnvironment = context.ExecutionContext.ServiceProvider.GetRequiredService<IAppHostEnvironment>();
if (appHostEnvironment.OtlpApiKey is { } otlpApiKey)
{
context.EnvironmentVariables["OTEL_EXPORTER_OTLP_HEADERS"] = $"x-otlp-api-key={otlpApiKey}";
}
Expand Down
Loading