From 806a6bf6821acac5d657ac25fc18ec399cbacb36 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 18 Oct 2025 08:27:08 +0000 Subject: [PATCH 1/7] Initial plan From 0e5e3448f75620bc3e014883e04f0afe9fb09013 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 18 Oct 2025 08:47:43 +0000 Subject: [PATCH 2/7] Add IAppHostEnvironment interface and replace configuration access Co-authored-by: davidfowl <95136+davidfowl@users.noreply.github.com> --- src/Aspire.Hosting/AppHostEnvironment.cs | 66 ++++++++++++++ .../Dashboard/DashboardEventHandlers.cs | 7 +- .../Dashboard/DashboardOptions.cs | 6 +- .../Dashboard/DashboardService.cs | 7 +- src/Aspire.Hosting/Dcp/DcpExecutor.cs | 8 +- src/Aspire.Hosting/Dcp/DcpNameGenerator.cs | 9 +- .../DistributedApplicationBuilder.cs | 9 ++ .../DistributedApplicationLifecycle.cs | 5 +- src/Aspire.Hosting/IAppHostEnvironment.cs | 90 +++++++++++++++++++ .../IDistributedApplicationBuilder.cs | 5 ++ .../OtlpConfigurationExtensions.cs | 3 +- .../ParameterResourceBuilderExtensions.cs | 4 +- .../ProjectResourceBuilderExtensions.cs | 2 +- .../Internal/FileDeploymentStateManager.cs | 5 +- .../ResourceLoggerForwarderService.cs | 4 +- .../VersionChecking/VersionCheckService.cs | 6 +- src/Aspire.Hosting/VolumeNameGenerator.cs | 4 +- 17 files changed, 206 insertions(+), 34 deletions(-) create mode 100644 src/Aspire.Hosting/AppHostEnvironment.cs create mode 100644 src/Aspire.Hosting/IAppHostEnvironment.cs diff --git a/src/Aspire.Hosting/AppHostEnvironment.cs b/src/Aspire.Hosting/AppHostEnvironment.cs new file mode 100644 index 00000000000..1c40c4bed11 --- /dev/null +++ b/src/Aspire.Hosting/AppHostEnvironment.cs @@ -0,0 +1,66 @@ +// 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; + +/// +/// Provides information about the AppHost environment. +/// +internal sealed class AppHostEnvironment : IAppHostEnvironment +{ + private readonly IConfiguration _configuration; + private readonly IHostEnvironment _hostEnvironment; + + /// + /// Initializes a new instance of the class. + /// + /// The configuration. + /// The host environment. + public AppHostEnvironment(IConfiguration configuration, IHostEnvironment hostEnvironment) + { + _configuration = configuration; + _hostEnvironment = hostEnvironment; + } + + /// + public string ProjectName => _configuration["AppHost:DashboardApplicationName"] ?? _hostEnvironment.ApplicationName; + + /// + public string Directory => _configuration["AppHost:Directory"]!; + + /// + public string Path => _configuration["AppHost:Path"]!; + + /// + public string DashboardApplicationName => _configuration["AppHost:DashboardApplicationName"] ?? _hostEnvironment.ApplicationName; + + /// + public string Sha256 => _configuration["AppHost:Sha256"]!; + + /// + public string PathSha256 => _configuration["AppHost:PathSha256"]!; + + /// + public string ProjectNameSha256 => _configuration["AppHost:ProjectNameSha256"]!; + + /// + public string? ContainerHostname => _configuration["AppHost:ContainerHostname"]; + + /// + public string? DefaultLaunchProfileName => _configuration["AppHost:DefaultLaunchProfileName"]; + + /// + public string? OtlpApiKey => _configuration["AppHost:OtlpApiKey"]; + + /// + public string? BrowserToken => _configuration["AppHost:BrowserToken"]; + + /// + public string? ResourceServiceApiKey => _configuration["AppHost:ResourceService:ApiKey"]; + + /// + public string? ResourceServiceAuthMode => _configuration["AppHost:ResourceService:AuthMode"]; +} diff --git a/src/Aspire.Hosting/Dashboard/DashboardEventHandlers.cs b/src/Aspire.Hosting/Dashboard/DashboardEventHandlers.cs index ff229233c88..5dae59afa62 100644 --- a/src/Aspire.Hosting/Dashboard/DashboardEventHandlers.cs +++ b/src/Aspire.Hosting/Dashboard/DashboardEventHandlers.cs @@ -26,7 +26,8 @@ namespace Aspire.Hosting.Dashboard; -internal sealed class DashboardEventHandlers(IConfiguration configuration, +internal sealed class DashboardEventHandlers(IAppHostEnvironment appHostEnvironment, + IConfiguration configuration, IOptions dashboardOptions, ILogger distributedApplicationLogger, IDashboardEndpointProvider dashboardEndpointProvider, @@ -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; diff --git a/src/Aspire.Hosting/Dashboard/DashboardOptions.cs b/src/Aspire.Hosting/Dashboard/DashboardOptions.cs index 217b907bf75..4133d14139c 100644 --- a/src/Aspire.Hosting/Dashboard/DashboardOptions.cs +++ b/src/Aspire.Hosting/Dashboard/DashboardOptions.cs @@ -19,17 +19,17 @@ internal class DashboardOptions public bool? TelemetryOptOut { get; set; } } -internal class ConfigureDefaultDashboardOptions(IConfiguration configuration, IOptions dcpOptions) : IConfigureOptions +internal class ConfigureDefaultDashboardOptions(IAppHostEnvironment appHostEnvironment, IOptions dcpOptions, IConfiguration configuration) : IConfigureOptions { public void Configure(DashboardOptions options) { options.DashboardPath = dcpOptions.Value.DashboardPath; options.DashboardUrl = configuration[KnownConfigNames.AspNetCoreUrls]; - options.DashboardToken = configuration["AppHost:BrowserToken"]; + options.DashboardToken = appHostEnvironment.BrowserToken; 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"; diff --git a/src/Aspire.Hosting/Dashboard/DashboardService.cs b/src/Aspire.Hosting/Dashboard/DashboardService.cs index d3b3faad7bc..733684b39fd 100644 --- a/src/Aspire.Hosting/Dashboard/DashboardService.cs +++ b/src/Aspire.Hosting/Dashboard/DashboardService.cs @@ -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; @@ -20,7 +19,7 @@ namespace Aspire.Hosting.Dashboard; /// required beyond a single request. Longer-scoped data is stored in . /// [Authorize(Policy = ResourceServiceApiKeyAuthorization.PolicyName)] -internal sealed partial class DashboardService(DashboardServiceData serviceData, IHostEnvironment hostEnvironment, IHostApplicationLifetime hostApplicationLifetime, IConfiguration configuration, ILogger logger) +internal sealed partial class DashboardService(DashboardServiceData serviceData, IAppHostEnvironment appHostEnvironment, IHostApplicationLifetime hostApplicationLifetime, ILogger logger) : Aspire.DashboardService.Proto.V1.DashboardService.DashboardServiceBase { // gRPC has a maximum receive size of 4MB. Force logs into batches to avoid exceeding receive size. @@ -38,8 +37,8 @@ public override Task 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 { diff --git a/src/Aspire.Hosting/Dcp/DcpExecutor.cs b/src/Aspire.Hosting/Dcp/DcpExecutor.cs index 54326438ecc..b6e93821c13 100644 --- a/src/Aspire.Hosting/Dcp/DcpExecutor.cs +++ b/src/Aspire.Hosting/Dcp/DcpExecutor.cs @@ -93,11 +93,12 @@ internal sealed partial class DcpExecutor : IDcpExecutor, IConsoleLogsService, I private readonly record struct LogInformationEntry(string ResourceName, bool? LogsAvailable, bool? HasSubscribers); private readonly Channel _logInformationChannel = Channel.CreateUnbounded( new UnboundedChannelOptions { SingleReader = true }); + private readonly IAppHostEnvironment _appHostEnvironment; public DcpExecutor(ILogger logger, ILogger distributedApplicationLogger, DistributedApplicationModel model, - IHostEnvironment hostEnvironment, + IAppHostEnvironment appHostEnvironment, IKubernetesService kubernetesService, IConfiguration configuration, IDistributedApplicationEventing distributedApplicationEventing, @@ -126,16 +127,17 @@ public DcpExecutor(ILogger 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) { diff --git a/src/Aspire.Hosting/Dcp/DcpNameGenerator.cs b/src/Aspire.Hosting/Dcp/DcpNameGenerator.cs index d810e9e662e..7e6faf47c74 100644 --- a/src/Aspire.Hosting/Dcp/DcpNameGenerator.cs +++ b/src/Aspire.Hosting/Dcp/DcpNameGenerator.cs @@ -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; @@ -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 _options; - public DcpNameGenerator(IConfiguration configuration, IOptions options) + public DcpNameGenerator(IAppHostEnvironment appHostEnvironment, IOptions options) { - _configuration = configuration; + _appHostEnvironment = appHostEnvironment; _options = options; } @@ -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.Sha256.Substring(0, RandomNameSuffixLength).ToLowerInvariant(); return suffix; } diff --git a/src/Aspire.Hosting/DistributedApplicationBuilder.cs b/src/Aspire.Hosting/DistributedApplicationBuilder.cs index 9edcb5ff193..a2adfbc9d45 100644 --- a/src/Aspire.Hosting/DistributedApplicationBuilder.cs +++ b/src/Aspire.Hosting/DistributedApplicationBuilder.cs @@ -60,10 +60,14 @@ public class DistributedApplicationBuilder : IDistributedApplicationBuilder private readonly DistributedApplicationOptions _options; private readonly HostApplicationBuilder _innerBuilder; + private readonly AppHostEnvironment _appHostEnvironment; /// public IHostEnvironment Environment => _innerBuilder.Environment; + /// + public IAppHostEnvironment AppHostEnvironment => _appHostEnvironment; + /// public ConfigurationManager Configuration => _innerBuilder.Configuration; @@ -466,6 +470,11 @@ public DistributedApplicationBuilder(DistributedApplicationOptions options) } _innerBuilder.Services.AddSingleton(ExecutionContext); + _innerBuilder.Services.AddSingleton(); + + // Initialize the AppHostEnvironment for use within the builder + _appHostEnvironment = new AppHostEnvironment(_innerBuilder.Configuration, _innerBuilder.Environment); + LogBuilderConstructed(this); } diff --git a/src/Aspire.Hosting/DistributedApplicationLifecycle.cs b/src/Aspire.Hosting/DistributedApplicationLifecycle.cs index c2b1c24d77a..7e013629bbb 100644 --- a/src/Aspire.Hosting/DistributedApplicationLifecycle.cs +++ b/src/Aspire.Hosting/DistributedApplicationLifecycle.cs @@ -2,7 +2,6 @@ // 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; @@ -10,7 +9,7 @@ namespace Aspire.Hosting; internal sealed class DistributedApplicationLifecycle( ILogger logger, - IConfiguration configuration, + IAppHostEnvironment appHostEnvironment, DistributedApplicationExecutionContext executionContext, LocaleOverrideContext localeOverrideContext) : IHostedLifecycleService { @@ -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.Directory); } if (localeOverrideContext.OverrideErrorMessage is { Length: > 0 } localOverrideError) diff --git a/src/Aspire.Hosting/IAppHostEnvironment.cs b/src/Aspire.Hosting/IAppHostEnvironment.cs new file mode 100644 index 00000000000..a49617384ab --- /dev/null +++ b/src/Aspire.Hosting/IAppHostEnvironment.cs @@ -0,0 +1,90 @@ +// 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; + +/// +/// Provides information about the AppHost environment. +/// +public interface IAppHostEnvironment +{ + /// + /// Gets the name of the AppHost project. + /// + /// + /// This is the project name used in multiple places throughout the application, + /// including for generating resource names and configuration keys. + /// + string ProjectName { get; } + + /// + /// Gets the directory of the project where the app host is located. + /// + string Directory { get; } + + /// + /// Gets the full path to the app host. + /// + string Path { get; } + + /// + /// Gets the application name used for the dashboard. + /// + string DashboardApplicationName { get; } + + /// + /// Gets the SHA256 hash of the app host. + /// + /// + /// For backward compatibility, this uses mode-dependent logic: + /// - Publish mode: ProjectNameSha (stable across paths) + /// - Run mode: PathSha (disambiguates by path) + /// + string Sha256 { get; } + + /// + /// Gets the SHA256 hash based on the app host path. + /// + /// + /// Used for disambiguating projects with the same name in different locations (deployment state). + /// + string PathSha256 { get; } + + /// + /// Gets the SHA256 hash based on the project name. + /// + /// + /// Used for stable naming across deployments regardless of path (Azure Functions, Azure environments). + /// + string ProjectNameSha256 { get; } + + /// + /// Gets the container hostname. + /// + string? ContainerHostname { get; } + + /// + /// Gets the default launch profile name. + /// + string? DefaultLaunchProfileName { get; } + + /// + /// Gets the OTLP API key. + /// + string? OtlpApiKey { get; } + + /// + /// Gets the browser token for the dashboard. + /// + string? BrowserToken { get; } + + /// + /// Gets the resource service API key. + /// + string? ResourceServiceApiKey { get; } + + /// + /// Gets the resource service authentication mode. + /// + string? ResourceServiceAuthMode { get; } +} diff --git a/src/Aspire.Hosting/IDistributedApplicationBuilder.cs b/src/Aspire.Hosting/IDistributedApplicationBuilder.cs index c3da140e310..44b2f319dde 100644 --- a/src/Aspire.Hosting/IDistributedApplicationBuilder.cs +++ b/src/Aspire.Hosting/IDistributedApplicationBuilder.cs @@ -72,6 +72,11 @@ public interface IDistributedApplicationBuilder /// public IHostEnvironment Environment { get; } + /// + /// Provides information about the AppHost environment. + /// + public IAppHostEnvironment AppHostEnvironment { get; } + /// public IServiceCollection Services { get; } diff --git a/src/Aspire.Hosting/OtlpConfigurationExtensions.cs b/src/Aspire.Hosting/OtlpConfigurationExtensions.cs index 644361bf7fb..6d132e2d4cb 100644 --- a/src/Aspire.Hosting/OtlpConfigurationExtensions.cs +++ b/src/Aspire.Hosting/OtlpConfigurationExtensions.cs @@ -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(); + if (appHostEnvironment.OtlpApiKey is { } otlpApiKey) { context.EnvironmentVariables["OTEL_EXPORTER_OTLP_HEADERS"] = $"x-otlp-api-key={otlpApiKey}"; } diff --git a/src/Aspire.Hosting/ParameterResourceBuilderExtensions.cs b/src/Aspire.Hosting/ParameterResourceBuilderExtensions.cs index 010676c8a9a..1d40c124c3d 100644 --- a/src/Aspire.Hosting/ParameterResourceBuilderExtensions.cs +++ b/src/Aspire.Hosting/ParameterResourceBuilderExtensions.cs @@ -139,7 +139,7 @@ public static IResourceBuilder AddParameter(this IDistributed // If it needs persistence, wrap it in a UserSecretsParameterDefault if (persist && builder.ExecutionContext.IsRunMode && builder.AppHostAssembly is not null) { - value = new UserSecretsParameterDefault(builder.AppHostAssembly, builder.Environment.ApplicationName, name, value); + value = new UserSecretsParameterDefault(builder.AppHostAssembly, builder.AppHostEnvironment.ProjectName, name, value); } return builder.AddParameter( @@ -348,7 +348,7 @@ public static ParameterResource CreateGeneratedParameter( if (builder.ExecutionContext.IsRunMode && builder.AppHostAssembly is not null) { - parameterResource.Default = new UserSecretsParameterDefault(builder.AppHostAssembly, builder.Environment.ApplicationName, name, parameterResource.Default); + parameterResource.Default = new UserSecretsParameterDefault(builder.AppHostAssembly, builder.AppHostEnvironment.ProjectName, name, parameterResource.Default); } return parameterResource; diff --git a/src/Aspire.Hosting/ProjectResourceBuilderExtensions.cs b/src/Aspire.Hosting/ProjectResourceBuilderExtensions.cs index 9f9e10ddae5..ec67a7409e4 100644 --- a/src/Aspire.Hosting/ProjectResourceBuilderExtensions.cs +++ b/src/Aspire.Hosting/ProjectResourceBuilderExtensions.cs @@ -333,7 +333,7 @@ private static IResourceBuilder WithProjectDefaults(this IResou } else { - var appHostDefaultLaunchProfileName = builder.ApplicationBuilder.Configuration["AppHost:DefaultLaunchProfileName"] + var appHostDefaultLaunchProfileName = builder.ApplicationBuilder.AppHostEnvironment.DefaultLaunchProfileName ?? builder.ApplicationBuilder.Configuration["DOTNET_LAUNCH_PROFILE"]; if (!string.IsNullOrEmpty(appHostDefaultLaunchProfileName)) { diff --git a/src/Aspire.Hosting/Publishing/Internal/FileDeploymentStateManager.cs b/src/Aspire.Hosting/Publishing/Internal/FileDeploymentStateManager.cs index 9bdde9a3430..e3bac034340 100644 --- a/src/Aspire.Hosting/Publishing/Internal/FileDeploymentStateManager.cs +++ b/src/Aspire.Hosting/Publishing/Internal/FileDeploymentStateManager.cs @@ -5,7 +5,6 @@ using System.Text.Json; using System.Text.Json.Nodes; -using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -17,7 +16,7 @@ namespace Aspire.Hosting.Publishing.Internal; /// public sealed class FileDeploymentStateManager( ILogger logger, - IConfiguration configuration, + IAppHostEnvironment appHostEnvironment, IHostEnvironment hostEnvironment, IOptions publishingOptions) : IDeploymentStateManager { @@ -32,7 +31,7 @@ public sealed class FileDeploymentStateManager( private string? GetDeploymentStatePath() { // Use PathSha256 for deployment state to disambiguate projects with the same name in different locations - var appHostSha = configuration["AppHost:PathSha256"]; + var appHostSha = appHostEnvironment.PathSha256; if (string.IsNullOrEmpty(appHostSha)) { return null; diff --git a/src/Aspire.Hosting/ResourceLoggerForwarderService.cs b/src/Aspire.Hosting/ResourceLoggerForwarderService.cs index 1ece60c8375..e03e02804d1 100644 --- a/src/Aspire.Hosting/ResourceLoggerForwarderService.cs +++ b/src/Aspire.Hosting/ResourceLoggerForwarderService.cs @@ -15,7 +15,7 @@ namespace Aspire.Hosting; internal sealed class ResourceLoggerForwarderService( ResourceNotificationService resourceNotificationService, ResourceLoggerService resourceLoggerService, - IHostEnvironment hostEnvironment, + IAppHostEnvironment appHostEnvironment, ILoggerFactory loggerFactory) : BackgroundService { @@ -62,7 +62,7 @@ private async Task WatchResourceLogs(IResource resource, string resourceId, Canc { try { - var applicationName = hostEnvironment.ApplicationName; + var applicationName = appHostEnvironment.ProjectName; var logger = loggerFactory.CreateLogger($"{applicationName}.Resources.{resource.Name}"); await foreach (var logEvent in resourceLoggerService.WatchAsync(resourceId).WithCancellation(cancellationToken).ConfigureAwait(false)) { diff --git a/src/Aspire.Hosting/VersionChecking/VersionCheckService.cs b/src/Aspire.Hosting/VersionChecking/VersionCheckService.cs index 299111a7185..29aa86eaccc 100644 --- a/src/Aspire.Hosting/VersionChecking/VersionCheckService.cs +++ b/src/Aspire.Hosting/VersionChecking/VersionCheckService.cs @@ -27,6 +27,7 @@ internal sealed class VersionCheckService : BackgroundService private readonly IInteractionService _interactionService; private readonly ILogger _logger; private readonly IConfiguration _configuration; + private readonly IAppHostEnvironment _appHostEnvironment; private readonly DistributedApplicationOptions _options; private readonly IPackageFetcher _packageFetcher; private readonly DistributedApplicationExecutionContext _executionContext; @@ -34,12 +35,13 @@ internal sealed class VersionCheckService : BackgroundService private readonly SemVersion? _appHostVersion; public VersionCheckService(IInteractionService interactionService, ILogger logger, - IConfiguration configuration, DistributedApplicationOptions options, IPackageFetcher packageFetcher, + IConfiguration configuration, IAppHostEnvironment appHostEnvironment, DistributedApplicationOptions options, IPackageFetcher packageFetcher, DistributedApplicationExecutionContext executionContext, TimeProvider timeProvider, IPackageVersionProvider packageVersionProvider) { _interactionService = interactionService; _logger = logger; _configuration = configuration; + _appHostEnvironment = appHostEnvironment; _options = options; _packageFetcher = packageFetcher; _executionContext = executionContext; @@ -97,7 +99,7 @@ private async Task CheckForLatestAsync(CancellationToken cancellationToken) SemVersion? storedKnownLatestVersion = null; if (checkForLatestVersion) { - var appHostDirectory = _configuration["AppHost:Directory"]!; + var appHostDirectory = _appHostEnvironment.Directory; SecretsStore.TrySetUserSecret(_options.Assembly, LastCheckDateKey, now.ToString("o", CultureInfo.InvariantCulture)); packages = await _packageFetcher.TryFetchPackagesAsync(appHostDirectory, cancellationToken).ConfigureAwait(false); diff --git a/src/Aspire.Hosting/VolumeNameGenerator.cs b/src/Aspire.Hosting/VolumeNameGenerator.cs index 8b62f2c5ea5..3b2d5db357e 100644 --- a/src/Aspire.Hosting/VolumeNameGenerator.cs +++ b/src/Aspire.Hosting/VolumeNameGenerator.cs @@ -28,8 +28,8 @@ public static string Generate(IResourceBuilder builder, string suffix) whe // Create volume name like "{Sanitize(appname).Lower()}-{sha256.Lower()}-postgres-data" // Compute a short hash of the content root path to differentiate between multiple AppHost projects with similar volume names - var safeApplicationName = Sanitize(builder.ApplicationBuilder.Environment.ApplicationName).ToLowerInvariant(); - var applicationHash = builder.ApplicationBuilder.Configuration["AppHost:Sha256"]![..10].ToLowerInvariant(); + var safeApplicationName = Sanitize(builder.ApplicationBuilder.AppHostEnvironment.ProjectName).ToLowerInvariant(); + var applicationHash = builder.ApplicationBuilder.AppHostEnvironment.Sha256[..10].ToLowerInvariant(); var resourceName = builder.Resource.Name; return $"{safeApplicationName}-{applicationHash}-{resourceName}-{suffix}"; } From 532ae4ecdf5503da001328613209098904c1d781 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 18 Oct 2025 09:03:40 +0000 Subject: [PATCH 3/7] Add tests for IAppHostEnvironment and fix test files Co-authored-by: davidfowl <95136+davidfowl@users.noreply.github.com> --- .../DistributedApplicationTestingBuilder.cs | 4 + src/Aspire.Hosting/Dcp/DcpExecutor.cs | 1 + .../Dashboard/DashboardLifecycleHookTests.cs | 3 +- .../Dashboard/DashboardServiceTests.cs | 25 +++--- .../Dcp/DcpExecutorTests.cs | 6 +- .../DistributedApplicationBuilderTests.cs | 84 +++++++++++++++++++ .../Utils/TestAppHostEnvironment.cs | 33 ++++++++ .../VersionCheckServiceTests.cs | 5 +- 8 files changed, 143 insertions(+), 18 deletions(-) create mode 100644 tests/Aspire.Hosting.Tests/Utils/TestAppHostEnvironment.cs diff --git a/src/Aspire.Hosting.Testing/DistributedApplicationTestingBuilder.cs b/src/Aspire.Hosting.Testing/DistributedApplicationTestingBuilder.cs index 97ba3575ba2..470e5c48b4a 100644 --- a/src/Aspire.Hosting.Testing/DistributedApplicationTestingBuilder.cs +++ b/src/Aspire.Hosting.Testing/DistributedApplicationTestingBuilder.cs @@ -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; @@ -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; diff --git a/src/Aspire.Hosting/Dcp/DcpExecutor.cs b/src/Aspire.Hosting/Dcp/DcpExecutor.cs index b6e93821c13..3af29b0ee46 100644 --- a/src/Aspire.Hosting/Dcp/DcpExecutor.cs +++ b/src/Aspire.Hosting/Dcp/DcpExecutor.cs @@ -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; diff --git a/tests/Aspire.Hosting.Tests/Dashboard/DashboardLifecycleHookTests.cs b/tests/Aspire.Hosting.Tests/Dashboard/DashboardLifecycleHookTests.cs index 0aec7b47796..f732546be6e 100644 --- a/tests/Aspire.Hosting.Tests/Dashboard/DashboardLifecycleHookTests.cs +++ b/tests/Aspire.Hosting.Tests/Dashboard/DashboardLifecycleHookTests.cs @@ -505,6 +505,7 @@ private static DashboardEventHandlers CreateHook( var rewriter = new CodespacesUrlRewriter(codespacesOptions); return new DashboardEventHandlers( + new TestAppHostEnvironment(configuration), configuration, dashboardOptions, NullLogger.Instance, @@ -513,7 +514,7 @@ private static DashboardEventHandlers CreateHook( resourceNotificationService, resourceLoggerService, loggerFactory ?? NullLoggerFactory.Instance, - new DcpNameGenerator(configuration, Options.Create(new DcpOptions())), + new DcpNameGenerator(new TestAppHostEnvironment(configuration), Options.Create(new DcpOptions())), new TestHostApplicationLifetime(), new Hosting.Eventing.DistributedApplicationEventing(), rewriter diff --git a/tests/Aspire.Hosting.Tests/Dashboard/DashboardServiceTests.cs b/tests/Aspire.Hosting.Tests/Dashboard/DashboardServiceTests.cs index 6f27e61cc3d..81b2bfa3a59 100644 --- a/tests/Aspire.Hosting.Tests/Dashboard/DashboardServiceTests.cs +++ b/tests/Aspire.Hosting.Tests/Dashboard/DashboardServiceTests.cs @@ -40,7 +40,7 @@ public async Task WatchResourceConsoleLogs_NoFollow_ResultsEnd() var resourceNotificationService = CreateResourceNotificationService(resourceLoggerService); var dashboardServiceData = CreateDashboardServiceData(resourceLoggerService: resourceLoggerService, resourceNotificationService: resourceNotificationService); - var dashboardService = new DashboardServiceImpl(dashboardServiceData, new TestHostEnvironment(), new TestHostApplicationLifetime(), new ConfigurationBuilder().Build(), NullLogger.Instance); + var dashboardService = new DashboardServiceImpl(dashboardServiceData, new TestAppHostEnvironment(), new TestHostApplicationLifetime(), NullLogger.Instance); var logger = resourceLoggerService.GetLogger("test-resource"); @@ -93,7 +93,7 @@ public async Task WatchResourceConsoleLogs_LargePendingData_BatchResults() var resourceLoggerService = new ResourceLoggerService(); var resourceNotificationService = CreateResourceNotificationService(resourceLoggerService); var dashboardServiceData = CreateDashboardServiceData(resourceLoggerService: resourceLoggerService, resourceNotificationService: resourceNotificationService); - var dashboardService = new DashboardServiceImpl(dashboardServiceData, new TestHostEnvironment(), new TestHostApplicationLifetime(), new ConfigurationBuilder().Build(), NullLogger.Instance); + var dashboardService = new DashboardServiceImpl(dashboardServiceData, new TestAppHostEnvironment(), new TestHostApplicationLifetime(), NullLogger.Instance); var logger = resourceLoggerService.GetLogger("test-resource"); @@ -145,7 +145,7 @@ public async Task WatchResources_ResourceHasCommands_CommandsSentWithResponse() var resourceLoggerService = new ResourceLoggerService(); var resourceNotificationService = CreateResourceNotificationService(resourceLoggerService); using var dashboardServiceData = CreateDashboardServiceData(loggerFactory: loggerFactory, resourceLoggerService: resourceLoggerService, resourceNotificationService: resourceNotificationService); - var dashboardService = new DashboardServiceImpl(dashboardServiceData, new TestHostEnvironment(), new TestHostApplicationLifetime(), new ConfigurationBuilder().Build(), loggerFactory.CreateLogger()); + var dashboardService = new DashboardServiceImpl(dashboardServiceData, new TestAppHostEnvironment(), new TestHostApplicationLifetime(), loggerFactory.CreateLogger()); var testResource = new TestResource("test-resource"); using var applicationBuilder = TestDistributedApplicationBuilder.Create(testOutputHelper: testOutputHelper); @@ -230,7 +230,7 @@ public async Task WatchInteractions_PromptMessageBoxAsync_CompleteOnResponse(boo new ServiceCollection().BuildServiceProvider(), new Microsoft.Extensions.Configuration.ConfigurationBuilder().Build()); using var dashboardServiceData = CreateDashboardServiceData(loggerFactory: loggerFactory, interactionService: interactionService); - var dashboardService = new DashboardServiceImpl(dashboardServiceData, new TestHostEnvironment(), new TestHostApplicationLifetime(), new ConfigurationBuilder().Build(), loggerFactory.CreateLogger()); + var dashboardService = new DashboardServiceImpl(dashboardServiceData, new TestAppHostEnvironment(), new TestHostApplicationLifetime(), loggerFactory.CreateLogger()); var cts = new CancellationTokenSource(); var context = TestServerCallContext.Create(cancellationToken: cts.Token); @@ -300,7 +300,7 @@ public async Task WatchInteractions_NoExplicitLabel_LabelIsName() new ServiceCollection().BuildServiceProvider(), new Microsoft.Extensions.Configuration.ConfigurationBuilder().Build()); using var dashboardServiceData = CreateDashboardServiceData(loggerFactory: loggerFactory, interactionService: interactionService); - var dashboardService = new DashboardServiceImpl(dashboardServiceData, new TestHostEnvironment(), new TestHostApplicationLifetime(), new ConfigurationBuilder().Build(), loggerFactory.CreateLogger()); + var dashboardService = new DashboardServiceImpl(dashboardServiceData, new TestAppHostEnvironment(), new TestHostApplicationLifetime(), loggerFactory.CreateLogger()); var cts = new CancellationTokenSource(); var context = TestServerCallContext.Create(cancellationToken: cts.Token); @@ -347,7 +347,7 @@ public async Task WatchInteractions_PromptInputAsync_CompleteOnCancelResponse() new ServiceCollection().BuildServiceProvider(), new Microsoft.Extensions.Configuration.ConfigurationBuilder().Build()); using var dashboardServiceData = CreateDashboardServiceData(loggerFactory: loggerFactory, interactionService: interactionService); - var dashboardService = new DashboardServiceImpl(dashboardServiceData, new TestHostEnvironment(), new TestHostApplicationLifetime(), new ConfigurationBuilder().Build(), loggerFactory.CreateLogger()); + var dashboardService = new DashboardServiceImpl(dashboardServiceData, new TestAppHostEnvironment(), new TestHostApplicationLifetime(), loggerFactory.CreateLogger()); var cts = new CancellationTokenSource(); var context = TestServerCallContext.Create(cancellationToken: cts.Token); @@ -406,7 +406,7 @@ public async Task WatchInteractions_ReaderError_CompleteWithError() new ServiceCollection().BuildServiceProvider(), new Microsoft.Extensions.Configuration.ConfigurationBuilder().Build()); using var dashboardServiceData = CreateDashboardServiceData(loggerFactory: loggerFactory, interactionService: interactionService); - var dashboardService = new DashboardServiceImpl(dashboardServiceData, new TestHostEnvironment(), new TestHostApplicationLifetime(), new ConfigurationBuilder().Build(), loggerFactory.CreateLogger()); + var dashboardService = new DashboardServiceImpl(dashboardServiceData, new TestAppHostEnvironment(), new TestHostApplicationLifetime(), loggerFactory.CreateLogger()); var cts = new CancellationTokenSource(); var context = TestServerCallContext.Create(cancellationToken: cts.Token); @@ -443,7 +443,7 @@ public async Task WatchInteractions_WriterError_CompleteWithError() new ServiceCollection().BuildServiceProvider(), new Microsoft.Extensions.Configuration.ConfigurationBuilder().Build()); using var dashboardServiceData = CreateDashboardServiceData(loggerFactory: loggerFactory, interactionService: interactionService); - var dashboardService = new DashboardServiceImpl(dashboardServiceData, new TestHostEnvironment(), new TestHostApplicationLifetime(), new ConfigurationBuilder().Build(), loggerFactory.CreateLogger()); + var dashboardService = new DashboardServiceImpl(dashboardServiceData, new TestAppHostEnvironment(), new TestHostApplicationLifetime(), loggerFactory.CreateLogger()); var cts = new CancellationTokenSource(); var context = TestServerCallContext.Create(cancellationToken: cts.Token); @@ -500,9 +500,8 @@ public async Task GetApplicationInformation_ReadsFromConfiguration() }; var dashboardService = new DashboardServiceImpl( dashboardServiceData, - hostEnvironment, + new TestAppHostEnvironment(configuration, hostEnvironment), new TestHostApplicationLifetime(), - configuration, NullLogger.Instance); var context = TestServerCallContext.Create(); @@ -529,9 +528,8 @@ public async Task GetApplicationInformation_FallsBackToEnvironmentApplicationNam }; var dashboardService = new DashboardServiceImpl( dashboardServiceData, - hostEnvironment, + new TestAppHostEnvironment(configuration, hostEnvironment), new TestHostApplicationLifetime(), - configuration, NullLogger.Instance); var context = TestServerCallContext.Create(); @@ -559,9 +557,8 @@ public async Task GetApplicationInformation_StripsAppHostSuffix() var dashboardServiceData = CreateDashboardServiceData(); var dashboardService = new DashboardServiceImpl( dashboardServiceData, - new TestHostEnvironment(), + new TestAppHostEnvironment(configuration), new TestHostApplicationLifetime(), - configuration, NullLogger.Instance); var context = TestServerCallContext.Create(); diff --git a/tests/Aspire.Hosting.Tests/Dcp/DcpExecutorTests.cs b/tests/Aspire.Hosting.Tests/Dcp/DcpExecutorTests.cs index 426057108ec..69732ef4aa0 100644 --- a/tests/Aspire.Hosting.Tests/Dcp/DcpExecutorTests.cs +++ b/tests/Aspire.Hosting.Tests/Dcp/DcpExecutorTests.cs @@ -2014,12 +2014,14 @@ private static DcpExecutor CreateAppExecutor( resourceLoggerService ??= new ResourceLoggerService(); dcpOptions ??= new DcpOptions { DashboardPath = "./dashboard" }; + var testHostEnvironment = hostEnvironment ?? new TestHostEnvironment(); + var testAppHostEnvironment = new TestAppHostEnvironment(configuration, testHostEnvironment); return new DcpExecutor( NullLogger.Instance, NullLogger.Instance, distributedAppModel, - hostEnvironment ?? new TestHostEnvironment(), + testAppHostEnvironment, kubernetesService ?? new TestKubernetesService(), configuration, new Hosting.Eventing.DistributedApplicationEventing(), @@ -2031,7 +2033,7 @@ private static DcpExecutor CreateAppExecutor( }), resourceLoggerService, new TestDcpDependencyCheckService(), - new DcpNameGenerator(configuration, Options.Create(dcpOptions)), + new DcpNameGenerator(testAppHostEnvironment, Options.Create(dcpOptions)), events ?? new DcpExecutorEvents(), new Locations(), new DeveloperCertificateService(NullLogger.Instance)); diff --git a/tests/Aspire.Hosting.Tests/DistributedApplicationBuilderTests.cs b/tests/Aspire.Hosting.Tests/DistributedApplicationBuilderTests.cs index 65a47bd56e5..c0b90c7f6f3 100644 --- a/tests/Aspire.Hosting.Tests/DistributedApplicationBuilderTests.cs +++ b/tests/Aspire.Hosting.Tests/DistributedApplicationBuilderTests.cs @@ -240,6 +240,90 @@ public void LegacyShaUsesProjectNameShaInPublishMode() Assert.Equal(projectNameSha, legacySha); } + [Fact] + public void AppHostEnvironmentIsAvailableFromBuilder() + { + var appBuilder = DistributedApplication.CreateBuilder(); + + Assert.NotNull(appBuilder.AppHostEnvironment); + Assert.NotNull(appBuilder.AppHostEnvironment.ProjectName); + Assert.NotNull(appBuilder.AppHostEnvironment.Directory); + Assert.NotNull(appBuilder.AppHostEnvironment.Path); + Assert.NotNull(appBuilder.AppHostEnvironment.DashboardApplicationName); + Assert.NotNull(appBuilder.AppHostEnvironment.Sha256); + Assert.NotNull(appBuilder.AppHostEnvironment.PathSha256); + Assert.NotNull(appBuilder.AppHostEnvironment.ProjectNameSha256); + } + + [Fact] + public void AppHostEnvironmentIsAvailableFromDI() + { + var appBuilder = DistributedApplication.CreateBuilder(); + using var app = appBuilder.Build(); + + var appHostEnvironment = app.Services.GetRequiredService(); + Assert.NotNull(appHostEnvironment); + Assert.NotNull(appHostEnvironment.ProjectName); + Assert.NotNull(appHostEnvironment.Directory); + Assert.NotNull(appHostEnvironment.Path); + } + + [Fact] + public void AppHostEnvironmentProjectNameMatchesConfiguration() + { + var appBuilder = DistributedApplication.CreateBuilder(); + using var app = appBuilder.Build(); + + var appHostEnvironment = app.Services.GetRequiredService(); + var config = app.Services.GetRequiredService(); + + var configDashboardAppName = config["AppHost:DashboardApplicationName"]; + Assert.Equal(configDashboardAppName, appHostEnvironment.ProjectName); + Assert.Equal(configDashboardAppName, appHostEnvironment.DashboardApplicationName); + } + + [Fact] + public void AppHostEnvironmentDirectoryMatchesConfiguration() + { + var appBuilder = DistributedApplication.CreateBuilder(); + using var app = appBuilder.Build(); + + var appHostEnvironment = app.Services.GetRequiredService(); + var config = app.Services.GetRequiredService(); + + Assert.Equal(config["AppHost:Directory"], appHostEnvironment.Directory); + Assert.Equal(config["AppHost:Path"], appHostEnvironment.Path); + } + + [Fact] + public void AppHostEnvironmentSha256MatchesConfiguration() + { + var appBuilder = DistributedApplication.CreateBuilder(); + using var app = appBuilder.Build(); + + var appHostEnvironment = app.Services.GetRequiredService(); + var config = app.Services.GetRequiredService(); + + Assert.Equal(config["AppHost:Sha256"], appHostEnvironment.Sha256); + Assert.Equal(config["AppHost:PathSha256"], appHostEnvironment.PathSha256); + Assert.Equal(config["AppHost:ProjectNameSha256"], appHostEnvironment.ProjectNameSha256); + } + + [Fact] + public void AppHostEnvironmentSecurityConfigMatchesConfiguration() + { + var appBuilder = DistributedApplication.CreateBuilder(); + using var app = appBuilder.Build(); + + var appHostEnvironment = app.Services.GetRequiredService(); + var config = app.Services.GetRequiredService(); + + Assert.Equal(config["AppHost:OtlpApiKey"], appHostEnvironment.OtlpApiKey); + Assert.Equal(config["AppHost:BrowserToken"], appHostEnvironment.BrowserToken); + Assert.Equal(config["AppHost:ResourceService:ApiKey"], appHostEnvironment.ResourceServiceApiKey); + Assert.Equal(config["AppHost:ResourceService:AuthMode"], appHostEnvironment.ResourceServiceAuthMode); + } + private sealed class TestResource : IResource { public string Name => nameof(TestResource); diff --git a/tests/Aspire.Hosting.Tests/Utils/TestAppHostEnvironment.cs b/tests/Aspire.Hosting.Tests/Utils/TestAppHostEnvironment.cs new file mode 100644 index 00000000000..a8c460be141 --- /dev/null +++ b/tests/Aspire.Hosting.Tests/Utils/TestAppHostEnvironment.cs @@ -0,0 +1,33 @@ +// 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.Tests.Utils; + +internal sealed class TestAppHostEnvironment : IAppHostEnvironment +{ + private readonly IConfiguration? _configuration; + private readonly IHostEnvironment? _hostEnvironment; + + public TestAppHostEnvironment(IConfiguration? configuration = null, IHostEnvironment? hostEnvironment = null) + { + _configuration = configuration; + _hostEnvironment = hostEnvironment; + } + + public string ProjectName => _configuration?["AppHost:DashboardApplicationName"] ?? _hostEnvironment?.ApplicationName ?? "TestApp"; + public string Directory => _configuration?["AppHost:Directory"] ?? "/test"; + public string Path => _configuration?["AppHost:Path"] ?? "/test/TestApp"; + public string DashboardApplicationName => _configuration?["AppHost:DashboardApplicationName"] ?? _hostEnvironment?.ApplicationName ?? "TestApp"; + public string Sha256 => _configuration?["AppHost:Sha256"] ?? "0000000000000000000000000000000000000000000000000000000000000000"; + public string PathSha256 => _configuration?["AppHost:PathSha256"] ?? "0000000000000000000000000000000000000000000000000000000000000000"; + public string ProjectNameSha256 => _configuration?["AppHost:ProjectNameSha256"] ?? "0000000000000000000000000000000000000000000000000000000000000000"; + public string? ContainerHostname => _configuration?["AppHost:ContainerHostname"]; + public string? DefaultLaunchProfileName => _configuration?["AppHost:DefaultLaunchProfileName"]; + public string? OtlpApiKey => _configuration?["AppHost:OtlpApiKey"]; + public string? BrowserToken => _configuration?["AppHost:BrowserToken"]; + public string? ResourceServiceApiKey => _configuration?["AppHost:ResourceService:ApiKey"]; + public string? ResourceServiceAuthMode => _configuration?["AppHost:ResourceService:AuthMode"]; +} diff --git a/tests/Aspire.Hosting.Tests/VersionChecking/VersionCheckServiceTests.cs b/tests/Aspire.Hosting.Tests/VersionChecking/VersionCheckServiceTests.cs index fbfa5abdb7e..f4bffd27dbb 100644 --- a/tests/Aspire.Hosting.Tests/VersionChecking/VersionCheckServiceTests.cs +++ b/tests/Aspire.Hosting.Tests/VersionChecking/VersionCheckServiceTests.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Globalization; +using Aspire.Hosting.Tests.Utils; using Aspire.Hosting.VersionChecking; using Aspire.Shared; using Microsoft.AspNetCore.InternalTesting; @@ -251,10 +252,12 @@ private static VersionCheckService CreateVersionCheckService( DistributedApplicationOptions? options = null, IPackageVersionProvider? packageVersionProvider = null) { + var config = configuration ?? new ConfigurationManager(); return new VersionCheckService( interactionService ?? new TestInteractionService(), NullLogger.Instance, - configuration ?? new ConfigurationManager(), + config, + new TestAppHostEnvironment(config), options ?? new DistributedApplicationOptions(), packageFetcher ?? new TestPackageFetcher(), new DistributedApplicationExecutionContext(new DistributedApplicationOperation()), From 8e7e8faaff6661490f383f7ced50d4233964b794 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 18 Oct 2025 09:08:47 +0000 Subject: [PATCH 4/7] Fix ResourceLoggerForwarderServiceTests to use IAppHostEnvironment Co-authored-by: davidfowl <95136+davidfowl@users.noreply.github.com> --- .../ResourceLoggerForwarderServiceTests.cs | 30 +++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/tests/Aspire.Hosting.Testing.Tests/ResourceLoggerForwarderServiceTests.cs b/tests/Aspire.Hosting.Testing.Tests/ResourceLoggerForwarderServiceTests.cs index 2e1084139c8..2a9664ee530 100644 --- a/tests/Aspire.Hosting.Testing.Tests/ResourceLoggerForwarderServiceTests.cs +++ b/tests/Aspire.Hosting.Testing.Tests/ResourceLoggerForwarderServiceTests.cs @@ -32,8 +32,9 @@ public async Task ExecuteDoesNotThrowOperationCanceledWhenAppStoppingTokenSignal var resourceLoggerService = new ResourceLoggerService(); var resourceNotificationService = CreateResourceNotificationService(hostApplicationLifetime, resourceLoggerService); var hostEnvironment = new HostingEnvironment(); + var appHostEnvironment = new TestAppHostEnvironment(hostEnvironment: hostEnvironment); var loggerFactory = new NullLoggerFactory(); - var resourceLogForwarder = new ResourceLoggerForwarderService(resourceNotificationService, resourceLoggerService, hostEnvironment, loggerFactory); + var resourceLogForwarder = new ResourceLoggerForwarderService(resourceNotificationService, resourceLoggerService, appHostEnvironment, loggerFactory); await resourceLogForwarder.StartAsync(hostApplicationLifetime.ApplicationStopping); @@ -53,9 +54,10 @@ public async Task ResourceLogsAreForwardedToHostLogging() var resourceLoggerService = ConsoleLoggingTestHelpers.GetResourceLoggerService(); var resourceNotificationService = CreateResourceNotificationService(hostApplicationLifetime, resourceLoggerService); var hostEnvironment = new HostingEnvironment { ApplicationName = "TestApp.AppHost" }; + var appHostEnvironment = new TestAppHostEnvironment(hostEnvironment: hostEnvironment); var fakeLoggerProvider = new FakeLoggerProvider(); var fakeLoggerFactory = new LoggerFactory([fakeLoggerProvider, new XunitLoggerProvider(output)]); - var resourceLogForwarder = new ResourceLoggerForwarderService(resourceNotificationService, resourceLoggerService, hostEnvironment, fakeLoggerFactory); + var resourceLogForwarder = new ResourceLoggerForwarderService(resourceNotificationService, resourceLoggerService, appHostEnvironment, fakeLoggerFactory); var subscribedTcs = new TaskCompletionSource(); var subscriberLoop = Task.Run(async () => @@ -172,4 +174,28 @@ public void Dispose() _stoppedCts.Dispose(); } } + + private sealed class TestAppHostEnvironment : IAppHostEnvironment + { + private readonly IHostEnvironment? _hostEnvironment; + + public TestAppHostEnvironment(IHostEnvironment? hostEnvironment = null) + { + _hostEnvironment = hostEnvironment; + } + + public string ProjectName => _hostEnvironment?.ApplicationName ?? "TestApp"; + public string Directory => "/test"; + public string Path => "/test/TestApp"; + public string DashboardApplicationName => _hostEnvironment?.ApplicationName ?? "TestApp"; + public string Sha256 => "0000000000000000000000000000000000000000000000000000000000000000"; + public string PathSha256 => "0000000000000000000000000000000000000000000000000000000000000000"; + public string ProjectNameSha256 => "0000000000000000000000000000000000000000000000000000000000000000"; + public string? ContainerHostname => null; + public string? DefaultLaunchProfileName => null; + public string? OtlpApiKey => null; + public string? BrowserToken => null; + public string? ResourceServiceApiKey => null; + public string? ResourceServiceAuthMode => null; + } } From d7e6dafcc856f80bc1b6059dc069949756127c91 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 18 Oct 2025 16:23:48 +0000 Subject: [PATCH 5/7] Implement PR feedback: Rename properties and remove BrowserToken Co-authored-by: davidfowl <95136+davidfowl@users.noreply.github.com> --- src/Aspire.Hosting/AppHostEnvironment.cs | 11 ++++------ .../Dashboard/DashboardOptions.cs | 2 +- src/Aspire.Hosting/Dcp/DcpNameGenerator.cs | 2 +- .../DistributedApplicationLifecycle.cs | 2 +- src/Aspire.Hosting/IAppHostEnvironment.cs | 15 +++++-------- .../IDistributedApplicationBuilder.cs | 2 +- .../Internal/FileDeploymentStateManager.cs | 4 ++-- .../VersionChecking/VersionCheckService.cs | 2 +- src/Aspire.Hosting/VolumeNameGenerator.cs | 2 +- .../ResourceLoggerForwarderServiceTests.cs | 9 ++++---- .../DistributedApplicationBuilderTests.cs | 21 +++++++++---------- .../Utils/TestAppHostEnvironment.cs | 9 ++++---- 12 files changed, 35 insertions(+), 46 deletions(-) diff --git a/src/Aspire.Hosting/AppHostEnvironment.cs b/src/Aspire.Hosting/AppHostEnvironment.cs index 1c40c4bed11..a75372bf2c1 100644 --- a/src/Aspire.Hosting/AppHostEnvironment.cs +++ b/src/Aspire.Hosting/AppHostEnvironment.cs @@ -29,19 +29,19 @@ public AppHostEnvironment(IConfiguration configuration, IHostEnvironment hostEnv public string ProjectName => _configuration["AppHost:DashboardApplicationName"] ?? _hostEnvironment.ApplicationName; /// - public string Directory => _configuration["AppHost:Directory"]!; + public string ProjectDirectory => _configuration["AppHost:Directory"]!; /// - public string Path => _configuration["AppHost:Path"]!; + public string FullPath => _configuration["AppHost:Path"]!; /// public string DashboardApplicationName => _configuration["AppHost:DashboardApplicationName"] ?? _hostEnvironment.ApplicationName; /// - public string Sha256 => _configuration["AppHost:Sha256"]!; + public string DefaultHash => _configuration["AppHost:Sha256"]!; /// - public string PathSha256 => _configuration["AppHost:PathSha256"]!; + public string FullPathHash => _configuration["AppHost:PathSha256"]!; /// public string ProjectNameSha256 => _configuration["AppHost:ProjectNameSha256"]!; @@ -55,9 +55,6 @@ public AppHostEnvironment(IConfiguration configuration, IHostEnvironment hostEnv /// public string? OtlpApiKey => _configuration["AppHost:OtlpApiKey"]; - /// - public string? BrowserToken => _configuration["AppHost:BrowserToken"]; - /// public string? ResourceServiceApiKey => _configuration["AppHost:ResourceService:ApiKey"]; diff --git a/src/Aspire.Hosting/Dashboard/DashboardOptions.cs b/src/Aspire.Hosting/Dashboard/DashboardOptions.cs index 4133d14139c..6295e7e4d0a 100644 --- a/src/Aspire.Hosting/Dashboard/DashboardOptions.cs +++ b/src/Aspire.Hosting/Dashboard/DashboardOptions.cs @@ -25,7 +25,7 @@ public void Configure(DashboardOptions options) { options.DashboardPath = dcpOptions.Value.DashboardPath; options.DashboardUrl = configuration[KnownConfigNames.AspNetCoreUrls]; - options.DashboardToken = appHostEnvironment.BrowserToken; + options.DashboardToken = configuration["AppHost:BrowserToken"]; options.OtlpGrpcEndpointUrl = configuration.GetString(KnownConfigNames.DashboardOtlpGrpcEndpointUrl, KnownConfigNames.Legacy.DashboardOtlpGrpcEndpointUrl); options.OtlpHttpEndpointUrl = configuration.GetString(KnownConfigNames.DashboardOtlpHttpEndpointUrl, KnownConfigNames.Legacy.DashboardOtlpHttpEndpointUrl); diff --git a/src/Aspire.Hosting/Dcp/DcpNameGenerator.cs b/src/Aspire.Hosting/Dcp/DcpNameGenerator.cs index 7e6faf47c74..4ff298bd3a7 100644 --- a/src/Aspire.Hosting/Dcp/DcpNameGenerator.cs +++ b/src/Aspire.Hosting/Dcp/DcpNameGenerator.cs @@ -114,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 = _appHostEnvironment.Sha256.Substring(0, RandomNameSuffixLength).ToLowerInvariant(); + var suffix = _appHostEnvironment.DefaultHash.Substring(0, RandomNameSuffixLength).ToLowerInvariant(); return suffix; } diff --git a/src/Aspire.Hosting/DistributedApplicationLifecycle.cs b/src/Aspire.Hosting/DistributedApplicationLifecycle.cs index 7e013629bbb..283b25cae34 100644 --- a/src/Aspire.Hosting/DistributedApplicationLifecycle.cs +++ b/src/Aspire.Hosting/DistributedApplicationLifecycle.cs @@ -40,7 +40,7 @@ public Task StartingAsync(CancellationToken cancellationToken) if (executionContext.IsRunMode) { logger.LogInformation("Distributed application starting."); - logger.LogInformation("Application host directory is: {AppHostDirectory}", appHostEnvironment.Directory); + logger.LogInformation("Application host directory is: {AppHostDirectory}", appHostEnvironment.ProjectDirectory); } if (localeOverrideContext.OverrideErrorMessage is { Length: > 0 } localOverrideError) diff --git a/src/Aspire.Hosting/IAppHostEnvironment.cs b/src/Aspire.Hosting/IAppHostEnvironment.cs index a49617384ab..76e150d331f 100644 --- a/src/Aspire.Hosting/IAppHostEnvironment.cs +++ b/src/Aspire.Hosting/IAppHostEnvironment.cs @@ -20,12 +20,12 @@ public interface IAppHostEnvironment /// /// Gets the directory of the project where the app host is located. /// - string Directory { get; } + string ProjectDirectory { get; } /// /// Gets the full path to the app host. /// - string Path { get; } + string FullPath { get; } /// /// Gets the application name used for the dashboard. @@ -38,9 +38,9 @@ public interface IAppHostEnvironment /// /// For backward compatibility, this uses mode-dependent logic: /// - Publish mode: ProjectNameSha (stable across paths) - /// - Run mode: PathSha (disambiguates by path) + /// - Run mode: FullPathHash (disambiguates by path) /// - string Sha256 { get; } + string DefaultHash { get; } /// /// Gets the SHA256 hash based on the app host path. @@ -48,7 +48,7 @@ public interface IAppHostEnvironment /// /// Used for disambiguating projects with the same name in different locations (deployment state). /// - string PathSha256 { get; } + string FullPathHash { get; } /// /// Gets the SHA256 hash based on the project name. @@ -73,11 +73,6 @@ public interface IAppHostEnvironment /// string? OtlpApiKey { get; } - /// - /// Gets the browser token for the dashboard. - /// - string? BrowserToken { get; } - /// /// Gets the resource service API key. /// diff --git a/src/Aspire.Hosting/IDistributedApplicationBuilder.cs b/src/Aspire.Hosting/IDistributedApplicationBuilder.cs index 44b2f319dde..8dd9976881e 100644 --- a/src/Aspire.Hosting/IDistributedApplicationBuilder.cs +++ b/src/Aspire.Hosting/IDistributedApplicationBuilder.cs @@ -75,7 +75,7 @@ public interface IDistributedApplicationBuilder /// /// Provides information about the AppHost environment. /// - public IAppHostEnvironment AppHostEnvironment { get; } + public IAppHostEnvironment AppHostEnvironment => throw new NotImplementedException(); /// public IServiceCollection Services { get; } diff --git a/src/Aspire.Hosting/Publishing/Internal/FileDeploymentStateManager.cs b/src/Aspire.Hosting/Publishing/Internal/FileDeploymentStateManager.cs index e3bac034340..01c539c170e 100644 --- a/src/Aspire.Hosting/Publishing/Internal/FileDeploymentStateManager.cs +++ b/src/Aspire.Hosting/Publishing/Internal/FileDeploymentStateManager.cs @@ -30,8 +30,8 @@ public sealed class FileDeploymentStateManager( private string? GetDeploymentStatePath() { - // Use PathSha256 for deployment state to disambiguate projects with the same name in different locations - var appHostSha = appHostEnvironment.PathSha256; + // Use FullPathHash for deployment state to disambiguate projects with the same name in different locations + var appHostSha = appHostEnvironment.FullPathHash; if (string.IsNullOrEmpty(appHostSha)) { return null; diff --git a/src/Aspire.Hosting/VersionChecking/VersionCheckService.cs b/src/Aspire.Hosting/VersionChecking/VersionCheckService.cs index 29aa86eaccc..37abfc1fc78 100644 --- a/src/Aspire.Hosting/VersionChecking/VersionCheckService.cs +++ b/src/Aspire.Hosting/VersionChecking/VersionCheckService.cs @@ -99,7 +99,7 @@ private async Task CheckForLatestAsync(CancellationToken cancellationToken) SemVersion? storedKnownLatestVersion = null; if (checkForLatestVersion) { - var appHostDirectory = _appHostEnvironment.Directory; + var appHostDirectory = _appHostEnvironment.ProjectDirectory; SecretsStore.TrySetUserSecret(_options.Assembly, LastCheckDateKey, now.ToString("o", CultureInfo.InvariantCulture)); packages = await _packageFetcher.TryFetchPackagesAsync(appHostDirectory, cancellationToken).ConfigureAwait(false); diff --git a/src/Aspire.Hosting/VolumeNameGenerator.cs b/src/Aspire.Hosting/VolumeNameGenerator.cs index 3b2d5db357e..a76040a805e 100644 --- a/src/Aspire.Hosting/VolumeNameGenerator.cs +++ b/src/Aspire.Hosting/VolumeNameGenerator.cs @@ -29,7 +29,7 @@ public static string Generate(IResourceBuilder builder, string suffix) whe // Compute a short hash of the content root path to differentiate between multiple AppHost projects with similar volume names var safeApplicationName = Sanitize(builder.ApplicationBuilder.AppHostEnvironment.ProjectName).ToLowerInvariant(); - var applicationHash = builder.ApplicationBuilder.AppHostEnvironment.Sha256[..10].ToLowerInvariant(); + var applicationHash = builder.ApplicationBuilder.AppHostEnvironment.DefaultHash[..10].ToLowerInvariant(); var resourceName = builder.Resource.Name; return $"{safeApplicationName}-{applicationHash}-{resourceName}-{suffix}"; } diff --git a/tests/Aspire.Hosting.Testing.Tests/ResourceLoggerForwarderServiceTests.cs b/tests/Aspire.Hosting.Testing.Tests/ResourceLoggerForwarderServiceTests.cs index 2a9664ee530..9ffa6a22615 100644 --- a/tests/Aspire.Hosting.Testing.Tests/ResourceLoggerForwarderServiceTests.cs +++ b/tests/Aspire.Hosting.Testing.Tests/ResourceLoggerForwarderServiceTests.cs @@ -185,16 +185,15 @@ public TestAppHostEnvironment(IHostEnvironment? hostEnvironment = null) } public string ProjectName => _hostEnvironment?.ApplicationName ?? "TestApp"; - public string Directory => "/test"; - public string Path => "/test/TestApp"; + public string ProjectDirectory => "/test"; + public string FullPath => "/test/TestApp"; public string DashboardApplicationName => _hostEnvironment?.ApplicationName ?? "TestApp"; - public string Sha256 => "0000000000000000000000000000000000000000000000000000000000000000"; - public string PathSha256 => "0000000000000000000000000000000000000000000000000000000000000000"; + public string DefaultHash => "0000000000000000000000000000000000000000000000000000000000000000"; + public string FullPathHash => "0000000000000000000000000000000000000000000000000000000000000000"; public string ProjectNameSha256 => "0000000000000000000000000000000000000000000000000000000000000000"; public string? ContainerHostname => null; public string? DefaultLaunchProfileName => null; public string? OtlpApiKey => null; - public string? BrowserToken => null; public string? ResourceServiceApiKey => null; public string? ResourceServiceAuthMode => null; } diff --git a/tests/Aspire.Hosting.Tests/DistributedApplicationBuilderTests.cs b/tests/Aspire.Hosting.Tests/DistributedApplicationBuilderTests.cs index c0b90c7f6f3..ec80bae97ea 100644 --- a/tests/Aspire.Hosting.Tests/DistributedApplicationBuilderTests.cs +++ b/tests/Aspire.Hosting.Tests/DistributedApplicationBuilderTests.cs @@ -247,11 +247,11 @@ public void AppHostEnvironmentIsAvailableFromBuilder() Assert.NotNull(appBuilder.AppHostEnvironment); Assert.NotNull(appBuilder.AppHostEnvironment.ProjectName); - Assert.NotNull(appBuilder.AppHostEnvironment.Directory); - Assert.NotNull(appBuilder.AppHostEnvironment.Path); + Assert.NotNull(appBuilder.AppHostEnvironment.ProjectDirectory); + Assert.NotNull(appBuilder.AppHostEnvironment.FullPath); Assert.NotNull(appBuilder.AppHostEnvironment.DashboardApplicationName); - Assert.NotNull(appBuilder.AppHostEnvironment.Sha256); - Assert.NotNull(appBuilder.AppHostEnvironment.PathSha256); + Assert.NotNull(appBuilder.AppHostEnvironment.DefaultHash); + Assert.NotNull(appBuilder.AppHostEnvironment.FullPathHash); Assert.NotNull(appBuilder.AppHostEnvironment.ProjectNameSha256); } @@ -264,8 +264,8 @@ public void AppHostEnvironmentIsAvailableFromDI() var appHostEnvironment = app.Services.GetRequiredService(); Assert.NotNull(appHostEnvironment); Assert.NotNull(appHostEnvironment.ProjectName); - Assert.NotNull(appHostEnvironment.Directory); - Assert.NotNull(appHostEnvironment.Path); + Assert.NotNull(appHostEnvironment.ProjectDirectory); + Assert.NotNull(appHostEnvironment.FullPath); } [Fact] @@ -291,8 +291,8 @@ public void AppHostEnvironmentDirectoryMatchesConfiguration() var appHostEnvironment = app.Services.GetRequiredService(); var config = app.Services.GetRequiredService(); - Assert.Equal(config["AppHost:Directory"], appHostEnvironment.Directory); - Assert.Equal(config["AppHost:Path"], appHostEnvironment.Path); + Assert.Equal(config["AppHost:Directory"], appHostEnvironment.ProjectDirectory); + Assert.Equal(config["AppHost:Path"], appHostEnvironment.FullPath); } [Fact] @@ -304,8 +304,8 @@ public void AppHostEnvironmentSha256MatchesConfiguration() var appHostEnvironment = app.Services.GetRequiredService(); var config = app.Services.GetRequiredService(); - Assert.Equal(config["AppHost:Sha256"], appHostEnvironment.Sha256); - Assert.Equal(config["AppHost:PathSha256"], appHostEnvironment.PathSha256); + Assert.Equal(config["AppHost:Sha256"], appHostEnvironment.DefaultHash); + Assert.Equal(config["AppHost:PathSha256"], appHostEnvironment.FullPathHash); Assert.Equal(config["AppHost:ProjectNameSha256"], appHostEnvironment.ProjectNameSha256); } @@ -319,7 +319,6 @@ public void AppHostEnvironmentSecurityConfigMatchesConfiguration() var config = app.Services.GetRequiredService(); Assert.Equal(config["AppHost:OtlpApiKey"], appHostEnvironment.OtlpApiKey); - Assert.Equal(config["AppHost:BrowserToken"], appHostEnvironment.BrowserToken); Assert.Equal(config["AppHost:ResourceService:ApiKey"], appHostEnvironment.ResourceServiceApiKey); Assert.Equal(config["AppHost:ResourceService:AuthMode"], appHostEnvironment.ResourceServiceAuthMode); } diff --git a/tests/Aspire.Hosting.Tests/Utils/TestAppHostEnvironment.cs b/tests/Aspire.Hosting.Tests/Utils/TestAppHostEnvironment.cs index a8c460be141..ae364201767 100644 --- a/tests/Aspire.Hosting.Tests/Utils/TestAppHostEnvironment.cs +++ b/tests/Aspire.Hosting.Tests/Utils/TestAppHostEnvironment.cs @@ -18,16 +18,15 @@ public TestAppHostEnvironment(IConfiguration? configuration = null, IHostEnviron } public string ProjectName => _configuration?["AppHost:DashboardApplicationName"] ?? _hostEnvironment?.ApplicationName ?? "TestApp"; - public string Directory => _configuration?["AppHost:Directory"] ?? "/test"; - public string Path => _configuration?["AppHost:Path"] ?? "/test/TestApp"; + public string ProjectDirectory => _configuration?["AppHost:Directory"] ?? "/test"; + public string FullPath => _configuration?["AppHost:Path"] ?? "/test/TestApp"; public string DashboardApplicationName => _configuration?["AppHost:DashboardApplicationName"] ?? _hostEnvironment?.ApplicationName ?? "TestApp"; - public string Sha256 => _configuration?["AppHost:Sha256"] ?? "0000000000000000000000000000000000000000000000000000000000000000"; - public string PathSha256 => _configuration?["AppHost:PathSha256"] ?? "0000000000000000000000000000000000000000000000000000000000000000"; + public string DefaultHash => _configuration?["AppHost:Sha256"] ?? "0000000000000000000000000000000000000000000000000000000000000000"; + public string FullPathHash => _configuration?["AppHost:PathSha256"] ?? "0000000000000000000000000000000000000000000000000000000000000000"; public string ProjectNameSha256 => _configuration?["AppHost:ProjectNameSha256"] ?? "0000000000000000000000000000000000000000000000000000000000000000"; public string? ContainerHostname => _configuration?["AppHost:ContainerHostname"]; public string? DefaultLaunchProfileName => _configuration?["AppHost:DefaultLaunchProfileName"]; public string? OtlpApiKey => _configuration?["AppHost:OtlpApiKey"]; - public string? BrowserToken => _configuration?["AppHost:BrowserToken"]; public string? ResourceServiceApiKey => _configuration?["AppHost:ResourceService:ApiKey"]; public string? ResourceServiceAuthMode => _configuration?["AppHost:ResourceService:AuthMode"]; } From 308eecb5bd19cb31a69857b6d3daaf8585f999b6 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 18 Oct 2025 17:18:03 +0000 Subject: [PATCH 6/7] Rename ProjectNameSha256 to ProjectNameHash and use same instance in DI Co-authored-by: davidfowl <95136+davidfowl@users.noreply.github.com> --- .../AzureFunctionsProjectResourceExtensions.cs | 4 ++-- .../AzureEnvironmentResourceExtensions.cs | 4 ++-- src/Aspire.Hosting/AppHostEnvironment.cs | 2 +- src/Aspire.Hosting/DistributedApplicationBuilder.cs | 4 ++-- src/Aspire.Hosting/IAppHostEnvironment.cs | 2 +- .../ResourceLoggerForwarderServiceTests.cs | 2 +- .../DistributedApplicationBuilderTests.cs | 4 ++-- tests/Aspire.Hosting.Tests/Utils/TestAppHostEnvironment.cs | 2 +- 8 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/Aspire.Hosting.Azure.Functions/AzureFunctionsProjectResourceExtensions.cs b/src/Aspire.Hosting.Azure.Functions/AzureFunctionsProjectResourceExtensions.cs index 90020bd61d3..4590753cad2 100644 --- a/src/Aspire.Hosting.Azure.Functions/AzureFunctionsProjectResourceExtensions.cs +++ b/src/Aspire.Hosting.Azure.Functions/AzureFunctionsProjectResourceExtensions.cs @@ -250,8 +250,8 @@ public static IResourceBuilder WithReference WithResourceGroup( private static string CreateDefaultAzureEnvironmentName(this IDistributedApplicationBuilder builder) { - // Use ProjectNameSha256 for stable naming across deployments - var applicationHash = builder.Configuration["AppHost:ProjectNameSha256"]?[..5].ToLowerInvariant(); + // Use ProjectNameHash for stable naming across deployments + var applicationHash = builder.AppHostEnvironment.ProjectNameHash[..5].ToLowerInvariant(); return $"azure{applicationHash}"; } } diff --git a/src/Aspire.Hosting/AppHostEnvironment.cs b/src/Aspire.Hosting/AppHostEnvironment.cs index a75372bf2c1..0e07eafdb17 100644 --- a/src/Aspire.Hosting/AppHostEnvironment.cs +++ b/src/Aspire.Hosting/AppHostEnvironment.cs @@ -44,7 +44,7 @@ public AppHostEnvironment(IConfiguration configuration, IHostEnvironment hostEnv public string FullPathHash => _configuration["AppHost:PathSha256"]!; /// - public string ProjectNameSha256 => _configuration["AppHost:ProjectNameSha256"]!; + public string ProjectNameHash => _configuration["AppHost:ProjectNameSha256"]!; /// public string? ContainerHostname => _configuration["AppHost:ContainerHostname"]; diff --git a/src/Aspire.Hosting/DistributedApplicationBuilder.cs b/src/Aspire.Hosting/DistributedApplicationBuilder.cs index a2adfbc9d45..a90509e19bd 100644 --- a/src/Aspire.Hosting/DistributedApplicationBuilder.cs +++ b/src/Aspire.Hosting/DistributedApplicationBuilder.cs @@ -470,10 +470,10 @@ public DistributedApplicationBuilder(DistributedApplicationOptions options) } _innerBuilder.Services.AddSingleton(ExecutionContext); - _innerBuilder.Services.AddSingleton(); - // Initialize the AppHostEnvironment for use within the builder + // Initialize the AppHostEnvironment for use within the builder and register in DI _appHostEnvironment = new AppHostEnvironment(_innerBuilder.Configuration, _innerBuilder.Environment); + _innerBuilder.Services.AddSingleton(_appHostEnvironment); LogBuilderConstructed(this); } diff --git a/src/Aspire.Hosting/IAppHostEnvironment.cs b/src/Aspire.Hosting/IAppHostEnvironment.cs index 76e150d331f..0ddc677b57c 100644 --- a/src/Aspire.Hosting/IAppHostEnvironment.cs +++ b/src/Aspire.Hosting/IAppHostEnvironment.cs @@ -56,7 +56,7 @@ public interface IAppHostEnvironment /// /// Used for stable naming across deployments regardless of path (Azure Functions, Azure environments). /// - string ProjectNameSha256 { get; } + string ProjectNameHash { get; } /// /// Gets the container hostname. diff --git a/tests/Aspire.Hosting.Testing.Tests/ResourceLoggerForwarderServiceTests.cs b/tests/Aspire.Hosting.Testing.Tests/ResourceLoggerForwarderServiceTests.cs index 9ffa6a22615..be4f79d3e80 100644 --- a/tests/Aspire.Hosting.Testing.Tests/ResourceLoggerForwarderServiceTests.cs +++ b/tests/Aspire.Hosting.Testing.Tests/ResourceLoggerForwarderServiceTests.cs @@ -190,7 +190,7 @@ public TestAppHostEnvironment(IHostEnvironment? hostEnvironment = null) public string DashboardApplicationName => _hostEnvironment?.ApplicationName ?? "TestApp"; public string DefaultHash => "0000000000000000000000000000000000000000000000000000000000000000"; public string FullPathHash => "0000000000000000000000000000000000000000000000000000000000000000"; - public string ProjectNameSha256 => "0000000000000000000000000000000000000000000000000000000000000000"; + public string ProjectNameHash => "0000000000000000000000000000000000000000000000000000000000000000"; public string? ContainerHostname => null; public string? DefaultLaunchProfileName => null; public string? OtlpApiKey => null; diff --git a/tests/Aspire.Hosting.Tests/DistributedApplicationBuilderTests.cs b/tests/Aspire.Hosting.Tests/DistributedApplicationBuilderTests.cs index ec80bae97ea..44d34ebfbbe 100644 --- a/tests/Aspire.Hosting.Tests/DistributedApplicationBuilderTests.cs +++ b/tests/Aspire.Hosting.Tests/DistributedApplicationBuilderTests.cs @@ -252,7 +252,7 @@ public void AppHostEnvironmentIsAvailableFromBuilder() Assert.NotNull(appBuilder.AppHostEnvironment.DashboardApplicationName); Assert.NotNull(appBuilder.AppHostEnvironment.DefaultHash); Assert.NotNull(appBuilder.AppHostEnvironment.FullPathHash); - Assert.NotNull(appBuilder.AppHostEnvironment.ProjectNameSha256); + Assert.NotNull(appBuilder.AppHostEnvironment.ProjectNameHash); } [Fact] @@ -306,7 +306,7 @@ public void AppHostEnvironmentSha256MatchesConfiguration() Assert.Equal(config["AppHost:Sha256"], appHostEnvironment.DefaultHash); Assert.Equal(config["AppHost:PathSha256"], appHostEnvironment.FullPathHash); - Assert.Equal(config["AppHost:ProjectNameSha256"], appHostEnvironment.ProjectNameSha256); + Assert.Equal(config["AppHost:ProjectNameSha256"], appHostEnvironment.ProjectNameHash); } [Fact] diff --git a/tests/Aspire.Hosting.Tests/Utils/TestAppHostEnvironment.cs b/tests/Aspire.Hosting.Tests/Utils/TestAppHostEnvironment.cs index ae364201767..173b2ca2f3d 100644 --- a/tests/Aspire.Hosting.Tests/Utils/TestAppHostEnvironment.cs +++ b/tests/Aspire.Hosting.Tests/Utils/TestAppHostEnvironment.cs @@ -23,7 +23,7 @@ public TestAppHostEnvironment(IConfiguration? configuration = null, IHostEnviron public string DashboardApplicationName => _configuration?["AppHost:DashboardApplicationName"] ?? _hostEnvironment?.ApplicationName ?? "TestApp"; public string DefaultHash => _configuration?["AppHost:Sha256"] ?? "0000000000000000000000000000000000000000000000000000000000000000"; public string FullPathHash => _configuration?["AppHost:PathSha256"] ?? "0000000000000000000000000000000000000000000000000000000000000000"; - public string ProjectNameSha256 => _configuration?["AppHost:ProjectNameSha256"] ?? "0000000000000000000000000000000000000000000000000000000000000000"; + public string ProjectNameHash => _configuration?["AppHost:ProjectNameSha256"] ?? "0000000000000000000000000000000000000000000000000000000000000000"; public string? ContainerHostname => _configuration?["AppHost:ContainerHostname"]; public string? DefaultLaunchProfileName => _configuration?["AppHost:DefaultLaunchProfileName"]; public string? OtlpApiKey => _configuration?["AppHost:OtlpApiKey"]; From cc8a18654fa6971d17ab958c93ba520a6a11326b Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 18 Oct 2025 17:36:31 +0000 Subject: [PATCH 7/7] Replace IHostEnvironment with IAppHostEnvironment in Azure provisioning providers Co-authored-by: davidfowl <95136+davidfowl@users.noreply.github.com> --- .../BaseProvisioningContextProvider.cs | 5 ++-- .../PublishModeProvisioningContextProvider.cs | 7 +++-- .../RunModeProvisioningContextProvider.cs | 7 +++-- .../AzureDeployerTests.cs | 2 +- .../ProvisioningContextProviderTests.cs | 20 +++++++------- .../ProvisioningTestHelpers.cs | 27 ++++++++++++++++--- 6 files changed, 42 insertions(+), 26 deletions(-) diff --git a/src/Aspire.Hosting.Azure/Provisioning/Internal/BaseProvisioningContextProvider.cs b/src/Aspire.Hosting.Azure/Provisioning/Internal/BaseProvisioningContextProvider.cs index 5cc8bc07368..8a33eb9a94f 100644 --- a/src/Aspire.Hosting.Azure/Provisioning/Internal/BaseProvisioningContextProvider.cs +++ b/src/Aspire.Hosting.Azure/Provisioning/Internal/BaseProvisioningContextProvider.cs @@ -8,7 +8,6 @@ using Azure; using Azure.Core; using Azure.ResourceManager.Resources; -using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -20,7 +19,7 @@ namespace Aspire.Hosting.Azure.Provisioning.Internal; internal abstract partial class BaseProvisioningContextProvider( IInteractionService interactionService, IOptions options, - IHostEnvironment environment, + IAppHostEnvironment appHostEnvironment, ILogger logger, IArmClientProvider armClientProvider, IUserPrincipalProvider userPrincipalProvider, @@ -33,7 +32,7 @@ internal abstract partial class BaseProvisioningContextProvider( protected readonly IInteractionService _interactionService = interactionService; protected readonly AzureProvisionerOptions _options = options.Value; - protected readonly IHostEnvironment _environment = environment; + protected readonly IAppHostEnvironment _appHostEnvironment = appHostEnvironment; protected readonly ILogger _logger = logger; protected readonly IArmClientProvider _armClientProvider = armClientProvider; protected readonly IUserPrincipalProvider _userPrincipalProvider = userPrincipalProvider; diff --git a/src/Aspire.Hosting.Azure/Provisioning/Internal/PublishModeProvisioningContextProvider.cs b/src/Aspire.Hosting.Azure/Provisioning/Internal/PublishModeProvisioningContextProvider.cs index de2a5bdff9d..8a52e5cbe7e 100644 --- a/src/Aspire.Hosting.Azure/Provisioning/Internal/PublishModeProvisioningContextProvider.cs +++ b/src/Aspire.Hosting.Azure/Provisioning/Internal/PublishModeProvisioningContextProvider.cs @@ -8,7 +8,6 @@ using Aspire.Hosting.Azure.Resources; using Aspire.Hosting.Azure.Utils; using Aspire.Hosting.Publishing; -using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -21,7 +20,7 @@ namespace Aspire.Hosting.Azure.Provisioning.Internal; internal sealed class PublishModeProvisioningContextProvider( IInteractionService interactionService, IOptions options, - IHostEnvironment environment, + IAppHostEnvironment appHostEnvironment, ILogger logger, IArmClientProvider armClientProvider, IUserPrincipalProvider userPrincipalProvider, @@ -30,7 +29,7 @@ internal sealed class PublishModeProvisioningContextProvider( IPublishingActivityReporter activityReporter) : BaseProvisioningContextProvider( interactionService, options, - environment, + appHostEnvironment, logger, armClientProvider, userPrincipalProvider, @@ -48,7 +47,7 @@ protected override string GetDefaultResourceGroupName() var maxApplicationNameSize = ResourceGroupNameHelpers.MaxResourceGroupNameLength - prefix.Length - 1; // extra '-' - var normalizedApplicationName = ResourceGroupNameHelpers.NormalizeResourceGroupName(_environment.ApplicationName.ToLowerInvariant()); + var normalizedApplicationName = ResourceGroupNameHelpers.NormalizeResourceGroupName(_appHostEnvironment.ProjectName.ToLowerInvariant()); if (normalizedApplicationName.Length > maxApplicationNameSize) { normalizedApplicationName = normalizedApplicationName[..maxApplicationNameSize]; diff --git a/src/Aspire.Hosting.Azure/Provisioning/Internal/RunModeProvisioningContextProvider.cs b/src/Aspire.Hosting.Azure/Provisioning/Internal/RunModeProvisioningContextProvider.cs index a1556d01051..4c581764a60 100644 --- a/src/Aspire.Hosting.Azure/Provisioning/Internal/RunModeProvisioningContextProvider.cs +++ b/src/Aspire.Hosting.Azure/Provisioning/Internal/RunModeProvisioningContextProvider.cs @@ -7,7 +7,6 @@ using System.Text.Json.Nodes; using Aspire.Hosting.Azure.Resources; using Aspire.Hosting.Azure.Utils; -using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; @@ -19,7 +18,7 @@ namespace Aspire.Hosting.Azure.Provisioning.Internal; internal sealed class RunModeProvisioningContextProvider( IInteractionService interactionService, IOptions options, - IHostEnvironment environment, + IAppHostEnvironment appHostEnvironment, ILogger logger, IArmClientProvider armClientProvider, IUserPrincipalProvider userPrincipalProvider, @@ -27,7 +26,7 @@ internal sealed class RunModeProvisioningContextProvider( DistributedApplicationExecutionContext distributedApplicationExecutionContext) : BaseProvisioningContextProvider( interactionService, options, - environment, + appHostEnvironment, logger, armClientProvider, userPrincipalProvider, @@ -49,7 +48,7 @@ protected override string GetDefaultResourceGroupName() var maxApplicationNameSize = ResourceGroupNameHelpers.MaxResourceGroupNameLength - prefix.Length - suffix.Length - 2; // extra '-'s - var normalizedApplicationName = ResourceGroupNameHelpers.NormalizeResourceGroupName(_environment.ApplicationName.ToLowerInvariant()); + var normalizedApplicationName = ResourceGroupNameHelpers.NormalizeResourceGroupName(_appHostEnvironment.ProjectName.ToLowerInvariant()); if (normalizedApplicationName.Length > maxApplicationNameSize) { normalizedApplicationName = normalizedApplicationName[..maxApplicationNameSize]; diff --git a/tests/Aspire.Hosting.Azure.Tests/AzureDeployerTests.cs b/tests/Aspire.Hosting.Azure.Tests/AzureDeployerTests.cs index 4e56ca76ab8..702d7114129 100644 --- a/tests/Aspire.Hosting.Azure.Tests/AzureDeployerTests.cs +++ b/tests/Aspire.Hosting.Azure.Tests/AzureDeployerTests.cs @@ -854,7 +854,7 @@ private static void ConfigureTestServices(IDistributedApplicationTestingBuilder bool setDefaultProvisioningOptions = true) { var options = setDefaultProvisioningOptions ? ProvisioningTestHelpers.CreateOptions() : ProvisioningTestHelpers.CreateOptions(null, null, null); - var environment = ProvisioningTestHelpers.CreateEnvironment(); + var environment = ProvisioningTestHelpers.CreateAppHostEnvironment(); var logger = ProvisioningTestHelpers.CreateLogger(); armClientProvider ??= ProvisioningTestHelpers.CreateArmClientProvider(); var userPrincipalProvider = ProvisioningTestHelpers.CreateUserPrincipalProvider(); diff --git a/tests/Aspire.Hosting.Azure.Tests/ProvisioningContextProviderTests.cs b/tests/Aspire.Hosting.Azure.Tests/ProvisioningContextProviderTests.cs index 18b2d942d1f..27853700d0f 100644 --- a/tests/Aspire.Hosting.Azure.Tests/ProvisioningContextProviderTests.cs +++ b/tests/Aspire.Hosting.Azure.Tests/ProvisioningContextProviderTests.cs @@ -22,7 +22,7 @@ public async Task CreateProvisioningContextAsync_ReturnsValidContext() { // Arrange var options = ProvisioningTestHelpers.CreateOptions(); - var environment = ProvisioningTestHelpers.CreateEnvironment(); + var environment = ProvisioningTestHelpers.CreateAppHostEnvironment(); var logger = ProvisioningTestHelpers.CreateLogger(); var armClientProvider = ProvisioningTestHelpers.CreateArmClientProvider(); var userPrincipalProvider = ProvisioningTestHelpers.CreateUserPrincipalProvider(); @@ -60,7 +60,7 @@ public async Task CreateProvisioningContextAsync_ThrowsWhenSubscriptionIdMissing { // Arrange var options = ProvisioningTestHelpers.CreateOptions(subscriptionId: null); - var environment = ProvisioningTestHelpers.CreateEnvironment(); + var environment = ProvisioningTestHelpers.CreateAppHostEnvironment(); var logger = ProvisioningTestHelpers.CreateLogger(); var armClientProvider = ProvisioningTestHelpers.CreateArmClientProvider(); var userPrincipalProvider = ProvisioningTestHelpers.CreateUserPrincipalProvider(); @@ -88,7 +88,7 @@ public async Task CreateProvisioningContextAsync_ThrowsWhenLocationMissing() { // Arrange var options = ProvisioningTestHelpers.CreateOptions(location: null); - var environment = ProvisioningTestHelpers.CreateEnvironment(); + var environment = ProvisioningTestHelpers.CreateAppHostEnvironment(); var logger = ProvisioningTestHelpers.CreateLogger(); var armClientProvider = ProvisioningTestHelpers.CreateArmClientProvider(); var userPrincipalProvider = ProvisioningTestHelpers.CreateUserPrincipalProvider(); @@ -116,7 +116,7 @@ public async Task CreateProvisioningContextAsync_GeneratesResourceGroupNameWhenN { // Arrange var options = ProvisioningTestHelpers.CreateOptions(resourceGroup: null); - var environment = ProvisioningTestHelpers.CreateEnvironment(); + var environment = ProvisioningTestHelpers.CreateAppHostEnvironment(); var logger = ProvisioningTestHelpers.CreateLogger(); var armClientProvider = ProvisioningTestHelpers.CreateArmClientProvider(); var userPrincipalProvider = ProvisioningTestHelpers.CreateUserPrincipalProvider(); @@ -152,7 +152,7 @@ public async Task CreateProvisioningContextAsync_UsesProvidedResourceGroupName() // Arrange var resourceGroupName = "my-custom-rg"; var options = ProvisioningTestHelpers.CreateOptions(resourceGroup: resourceGroupName); - var environment = ProvisioningTestHelpers.CreateEnvironment(); + var environment = ProvisioningTestHelpers.CreateAppHostEnvironment(); var logger = ProvisioningTestHelpers.CreateLogger(); var armClientProvider = ProvisioningTestHelpers.CreateArmClientProvider(); var userPrincipalProvider = ProvisioningTestHelpers.CreateUserPrincipalProvider(); @@ -182,7 +182,7 @@ public async Task CreateProvisioningContextAsync_RetrievesUserPrincipal() { // Arrange var options = ProvisioningTestHelpers.CreateOptions(); - var environment = ProvisioningTestHelpers.CreateEnvironment(); + var environment = ProvisioningTestHelpers.CreateAppHostEnvironment(); var logger = ProvisioningTestHelpers.CreateLogger(); var armClientProvider = ProvisioningTestHelpers.CreateArmClientProvider(); var userPrincipalProvider = ProvisioningTestHelpers.CreateUserPrincipalProvider(); @@ -213,7 +213,7 @@ public async Task CreateProvisioningContextAsync_SetsCorrectTenant() { // Arrange var options = ProvisioningTestHelpers.CreateOptions(); - var environment = ProvisioningTestHelpers.CreateEnvironment(); + var environment = ProvisioningTestHelpers.CreateAppHostEnvironment(); var logger = ProvisioningTestHelpers.CreateLogger(); var armClientProvider = ProvisioningTestHelpers.CreateArmClientProvider(); var userPrincipalProvider = ProvisioningTestHelpers.CreateUserPrincipalProvider(); @@ -245,7 +245,7 @@ public async Task CreateProvisioningContextAsync_PromptsIfNoOptions() // Arrange var testInteractionService = new TestInteractionService(); var options = ProvisioningTestHelpers.CreateOptions(null, null, null); - var environment = ProvisioningTestHelpers.CreateEnvironment(); + var environment = ProvisioningTestHelpers.CreateAppHostEnvironment(); var logger = ProvisioningTestHelpers.CreateLogger(); var armClientProvider = ProvisioningTestHelpers.CreateArmClientProvider(); var userPrincipalProvider = ProvisioningTestHelpers.CreateUserPrincipalProvider(); @@ -332,7 +332,7 @@ public async Task CreateProvisioningContextAsync_Prompt_ValidatesSubAndResourceG { var testInteractionService = new TestInteractionService(); var options = ProvisioningTestHelpers.CreateOptions(null, null, null); - var environment = ProvisioningTestHelpers.CreateEnvironment(); + var environment = ProvisioningTestHelpers.CreateAppHostEnvironment(); var logger = ProvisioningTestHelpers.CreateLogger(); var armClientProvider = ProvisioningTestHelpers.CreateArmClientProvider(); var userPrincipalProvider = ProvisioningTestHelpers.CreateUserPrincipalProvider(); @@ -391,7 +391,7 @@ public async Task PublishMode_CreateProvisioningContextAsync_ReturnsValidContext { // Arrange var options = ProvisioningTestHelpers.CreateOptions(); - var environment = ProvisioningTestHelpers.CreateEnvironment(); + var environment = ProvisioningTestHelpers.CreateAppHostEnvironment(); var logger = ProvisioningTestHelpers.CreateLogger(); var armClientProvider = ProvisioningTestHelpers.CreateArmClientProvider(); var userPrincipalProvider = ProvisioningTestHelpers.CreateUserPrincipalProvider(); diff --git a/tests/Aspire.Hosting.Azure.Tests/ProvisioningTestHelpers.cs b/tests/Aspire.Hosting.Azure.Tests/ProvisioningTestHelpers.cs index 2c562b70566..bd15bb85ffd 100644 --- a/tests/Aspire.Hosting.Azure.Tests/ProvisioningTestHelpers.cs +++ b/tests/Aspire.Hosting.Azure.Tests/ProvisioningTestHelpers.cs @@ -95,13 +95,13 @@ public static IOptions CreatePublishingOptions( } /// - /// Creates a test host environment. + /// Creates a test AppHost environment. /// - public static IHostEnvironment CreateEnvironment() + public static IAppHostEnvironment CreateAppHostEnvironment() { - var environment = new TestHostEnvironment + var environment = new TestAppHostEnvironment { - ApplicationName = "TestApp" + ProjectName = "TestApp" }; return environment; } @@ -558,6 +558,25 @@ internal sealed class TestHostEnvironment : IHostEnvironment public IFileProvider ContentRootFileProvider { get; set; } = new NullFileProvider(); } +/// +/// Test implementation of . +/// +internal sealed class TestAppHostEnvironment : IAppHostEnvironment +{ + public string ProjectName { get; set; } = "TestApp"; + public string ProjectDirectory { get; set; } = "/test"; + public string FullPath { get; set; } = "/test/TestApp"; + public string DashboardApplicationName { get; set; } = "TestApp"; + public string DefaultHash { get; set; } = "0000000000000000000000000000000000000000000000000000000000000000"; + public string FullPathHash { get; set; } = "0000000000000000000000000000000000000000000000000000000000000000"; + public string ProjectNameHash { get; set; } = "0000000000000000000000000000000000000000000000000000000000000000"; + public string? ContainerHostname { get; set; } + public string? DefaultLaunchProfileName { get; set; } + public string? OtlpApiKey { get; set; } + public string? ResourceServiceApiKey { get; set; } + public string? ResourceServiceAuthMode { get; set; } +} + internal sealed class TestBicepCompiler : IBicepCompiler { public Task CompileBicepToArmAsync(string bicepFilePath, CancellationToken cancellationToken = default)