// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
#pragma warning disable ASPIREPIPELINES003
#pragma warning disable ASPIREPIPELINES001
#pragma warning disable ASPIREPIPELINES002
#pragma warning disable ASPIREPIPELINES004
#pragma warning disable ASPIRECONTAINERRUNTIME001
#pragma warning disable ASPIREFILESYSTEM001
#pragma warning disable ASPIREUSERSECRETS001
#pragma warning disable ASPIREWATCH001
using System.Diagnostics;
using System.Reflection;
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.Backchannel;
using Aspire.Hosting.Cli;
using Aspire.Hosting.Dashboard;
using Aspire.Hosting.Dcp;
using Aspire.Hosting.Dcp.Process;
using Aspire.Hosting.Devcontainers;
using Aspire.Hosting.Devcontainers.Codespaces;
using Aspire.Hosting.Diagnostics;
using Aspire.Hosting.Eventing;
using Aspire.Hosting.Health;
using Aspire.Hosting.Lifecycle;
using Aspire.Hosting.Orchestrator;
using Aspire.Hosting.Utils;
using Aspire.Hosting.Pipelines;
using Aspire.Hosting.Pipelines.Internal;
using Aspire.Hosting.Publishing;
using Aspire.Hosting.UserSecrets;
using Aspire.Shared;
using Aspire.Shared.UserSecrets;
using Microsoft.Extensions.Configuration.UserSecrets;
using Aspire.Hosting.VersionChecking;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Diagnostics.HealthChecks;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using OpenTelemetry.Exporter;
using OpenTelemetry.Resources;
using OpenTelemetry.Trace;
namespace Aspire.Hosting;
/// <summary>
/// A builder for creating instances of <see cref="DistributedApplication"/>.
/// </summary>
/// <remarks>
/// <para>
/// The <see cref="DistributedApplicationBuilder"/> is the primary implementation of
/// <see cref="IDistributedApplicationBuilder"/> within Aspire. Typically a developer
/// would interact with instances of this class via the <see cref="IDistributedApplicationBuilder"/>
/// interface which was created using one of the <see cref="DistributedApplication.CreateBuilder(string[])"/>
/// overloads.
/// </para>
/// <para>
/// For more information on how to configure the <see cref="DistributedApplication" /> using the
/// the builder pattern see <see cref="IDistributedApplicationBuilder" />.
/// </para>
/// </remarks>
public class DistributedApplicationBuilder : IDistributedApplicationBuilder
{
private const string HostingDiagnosticListenerName = "Aspire.Hosting";
private const string ApplicationBuildingEventName = "DistributedApplicationBuilding";
private const string ApplicationBuiltEventName = "DistributedApplicationBuilt";
private const string BuilderConstructingEventName = "DistributedApplicationBuilderConstructing";
private const string BuilderConstructedEventName = "DistributedApplicationBuilderConstructed";
private readonly DistributedApplicationOptions _options;
private readonly HostApplicationBuilder _innerBuilder;
private readonly IUserSecretsManager _userSecretsManager;
private readonly FileSystemService _directoryService;
/// <inheritdoc />
public IHostEnvironment Environment => _innerBuilder.Environment;
/// <inheritdoc />
public ConfigurationManager Configuration => _innerBuilder.Configuration;
/// <inheritdoc />
public IServiceCollection Services => _innerBuilder.Services;
/// <inheritdoc />
public string AppHostDirectory { get; }
/// <inheritdoc />
public string AppHostPath { get; }
/// <inheritdoc />
public Assembly? AppHostAssembly => _options.Assembly;
/// <inheritdoc />
public DistributedApplicationExecutionContext ExecutionContext { get; }
/// <inheritdoc />
public IResourceCollection Resources { get; } = new ResourceCollection();
/// <inheritdoc />
public IDistributedApplicationEventing Eventing { get; } = new DistributedApplicationEventing();
/// <inheritdoc />
public IDistributedApplicationPipeline Pipeline { get; } = new DistributedApplicationPipeline();
/// <inheritdoc />
public IFileSystemService FileSystemService => _directoryService;
/// <inheritdoc />
public IUserSecretsManager UserSecretsManager => _userSecretsManager;
/// <summary>
/// Initializes a new instance of the <see cref="DistributedApplicationBuilder"/> class with the specified options.
/// </summary>
/// <param name="args">The arguments provided to the builder.</param>
/// <remarks>
/// <para>
/// Developers will not typically construct an instance of the <see cref="DistributedApplicationBuilder"/>
/// class themselves and will instead use the <see cref="DistributedApplication.CreateBuilder(string[])"/>.
/// This constructor is public to allow for some testing around extensibility scenarios.
/// </para>
/// </remarks>
public DistributedApplicationBuilder(string[] args) : this(new DistributedApplicationOptions { Args = args })
{
ArgumentNullException.ThrowIfNull(args);
}
// This is here because in the constructor of DistributedApplicationBuilder we inject
// DistributedApplicationExecutionContext. This is a class that is used to expose contextual
// values in various callbacks and is a central location to access useful services like IServiceProvider.
private readonly DistributedApplicationExecutionContextOptions _executionContextOptions;
private DistributedApplicationExecutionContextOptions BuildExecutionContextOptions()
{
var operationConfiguration = _innerBuilder.Configuration["AppHost:Operation"];
if (operationConfiguration is null)
{
return _innerBuilder.Configuration["Publishing:Publisher"] switch
{
{ } publisher => new DistributedApplicationExecutionContextOptions(DistributedApplicationOperation.Publish, publisher),
_ => new DistributedApplicationExecutionContextOptions(DistributedApplicationOperation.Run) { RunConfiguration = BuildRunConfiguration() }
};
}
return _innerBuilder.Configuration["AppHost:Operation"]?.ToLowerInvariant() switch
{
"run" => new DistributedApplicationExecutionContextOptions(DistributedApplicationOperation.Run) { RunConfiguration = BuildRunConfiguration() },
"publish" or "inspect" => new DistributedApplicationExecutionContextOptions(DistributedApplicationOperation.Publish, _innerBuilder.Configuration["Publishing:Publisher"] ?? "manifest"),
_ => throw new DistributedApplicationException("Invalid operation specified. Valid operations are 'publish', 'run', or 'inspect'.")
};
}
private RunConfiguration BuildRunConfiguration()
{
// Only "true" and "false" (case-insensitively) are accepted. bool.TryParse rejects everything else,
// including values some configuration sources emit for booleans such as "1" or "yes". An unusable
// value must never fail an otherwise valid run, so anything unrecognized falls back to the default.
return new RunConfiguration
{
WatchEnabled = bool.TryParse(_innerBuilder.Configuration["AppHost:Run:WatchEnabled"], out var watchEnabled) && watchEnabled
};
}
/// <summary>
/// Initializes a new instance of the <see cref="DistributedApplicationBuilder"/> class with the specified options.
/// </summary>
/// <param name="options">The options for the distributed application.</param>
/// <remarks>
/// <para>
/// Developers will not typically construct an instance of the <see cref="DistributedApplicationBuilder"/>
/// class themselves and will instead use the <see cref="DistributedApplication.CreateBuilder(string[])"/>.
/// This constructor is public to allow for some testing around extensibility scenarios.
/// </para>
/// <para>
/// This constructor generates an instance of the <see cref="IDistributedApplicationBuilder"/> interface
/// which is very similar to the instance that is returned from <see cref="DistributedApplication.CreateBuilder(string[])"/>
/// however it is not guaranteed to be 100% consistent. For typical usage it is recommended that the
/// <see cref="DistributedApplication.CreateBuilder(string[])"/> method is to create instances of
/// the <see cref="IDistributedApplicationBuilder"/> interface.
/// </para>
/// </remarks>
public DistributedApplicationBuilder(DistributedApplicationOptions options)
{
ArgumentNullException.ThrowIfNull(options);
ProfilingTelemetry.RecordAppHostStartupEvent(ProfilingTelemetry.Events.AppHostBuilderConstructing);
_options = options;
var innerBuilderOptions = new HostApplicationBuilderSettings();
// Args are set later in config with switch mappings. But specify them when creating the builder
// so they're used to initialize some types created immediately, e.g. IHostEnvironment.
innerBuilderOptions.Args = options.Args;
// Pre-seed the configuration with ASPIRE_-prefixed environment variables.
// HostApplicationBuilder will then add DOTNET_-prefixed env vars and command line args on top.
// This gives us the priority order: --environment > DOTNET_ENVIRONMENT > ASPIRE_ENVIRONMENT > default.
var configuration = new ConfigurationManager();
configuration.AddEnvironmentVariables(prefix: "ASPIRE_");
innerBuilderOptions.Configuration = configuration;
LogBuilderConstructing(options, innerBuilderOptions);
_innerBuilder = new HostApplicationBuilder(innerBuilderOptions);
var configuredUserSecretsId = _innerBuilder.Configuration[KnownConfigNames.AspireUserSecretsId];
var userSecretsId = ResolveUserSecretsId(AppHostAssembly, _innerBuilder.Configuration);
AddConfiguredUserSecrets(_innerBuilder.Configuration, AppHostAssembly, configuredUserSecretsId, _innerBuilder.Environment.IsDevelopment());
_innerBuilder.Services.AddSingleton(TimeProvider.System);
_innerBuilder.Services.AddSingleton<BackchannelLoggerProvider>();
_innerBuilder.Services.AddSingleton<ILoggerProvider>(sp => sp.GetRequiredService<BackchannelLoggerProvider>());
_innerBuilder.Logging.AddFilter("Microsoft.Hosting.Lifetime", LogLevel.Warning);
_innerBuilder.Logging.AddFilter("Microsoft.AspNetCore.Server.Kestrel", LogLevel.Error);
_innerBuilder.Logging.AddFilter("Grpc.AspNetCore.Server.ServerCallHandler", LogLevel.Error);
// Allow warnings from Aspire's dashboard code. We control this code and want to be able to log warnings to help troubleshoot issues in the dashboard.
// For example, misconfigured icons from the apphost are logged as warnings, and we want those to be visible to users.
// The volume of logs from this category should be low, so it shouldn't cause too much noise.
_innerBuilder.Logging.AddFilter("Aspire.Hosting.Dashboard", LogLevel.Warning);
// Third-party dashboard categories (e.g. Microsoft.AspNetCore, Grpc) are routed under
// Aspire.Hosting.Dashboard.ThirdParty so they can be filtered with a single rule.
_innerBuilder.Logging.AddFilter("Aspire.Hosting.Dashboard.ThirdParty", LogLevel.Error);
// This is to reduce log noise when we activate health checks for resources which may not yet be
// fully initialized. For example a database which is not yet created.
// Only suppress these logs when the dashboard is enabled, as the dashboard provides visibility into health check failures.
// When the dashboard is disabled (e.g., in tests), these logs are valuable for troubleshooting.
if (options.DashboardEnabled)
{
_innerBuilder.Logging.AddFilter("Microsoft.Extensions.Diagnostics.HealthChecks.DefaultHealthCheckService", LogLevel.None);
}
// This is so that we can see certificate errors in the resource server in the console logs.
// See: https://github.com/microsoft/aspire/issues/2914
_innerBuilder.Logging.AddFilter("Microsoft.AspNetCore.Server.Kestrel.Core.KestrelServer", LogLevel.Warning);
// Add the logging configuration again to allow the user to override the defaults
_innerBuilder.Logging.AddConfiguration(_innerBuilder.Configuration.GetSection("Logging"));
// The CLI sets ASPIRE_LOGLEVEL to control the default log level for Aspire processes
// without polluting child processes (unlike Logging__LogLevel__Default which cascades
// through DCP into project processes and overrides their appsettings.json configuration).
var aspireLogLevelValue = _innerBuilder.Configuration[KnownConfigNames.AspireLogLevel];
if (aspireLogLevelValue is not null && Enum.TryParse<LogLevel>(aspireLogLevelValue, ignoreCase: true, out var aspireLogLevel))
{
_innerBuilder.Logging.SetMinimumLevel(aspireLogLevel);
_innerBuilder.Services.Configure<LoggerFilterOptions>(options =>
{
options.Rules.Add(new LoggerFilterRule(providerName: null, categoryName: null, logLevel: aspireLogLevel, filter: null));
});
}
AppHostDirectory = options.ProjectDirectory ?? _innerBuilder.Environment.ContentRootPath;
var appHostName = options.ProjectName ?? _innerBuilder.Environment.ApplicationName;
var appHostPath = Path.Join(AppHostDirectory, appHostName);
// Normalize the AppHost path for consistent behavior across platforms and execution contexts
AppHostPath = Path.GetFullPath(appHostPath);
// Get the actual AppHost file path (with .csproj or .cs extension)
var appHostFilePath = options.AppHostFilePath;
var assemblyMetadata = AppHostAssembly?.GetCustomAttributes<AssemblyMetadataAttribute>();
var aspireDir = ResolveAspireStorePath(assemblyMetadata, AppHostDirectory);
ConfigurePipelineOptions(options);
// Compute the dashboard application name - use DashboardApplicationName if set for file-based apps,
// otherwise fall back to the environment's ApplicationName
var dashboardApplicationName = options.DashboardApplicationName ?? _innerBuilder.Environment.ApplicationName;
_innerBuilder.Configuration.AddInMemoryCollection(new Dictionary<string, string?>
{
// Make the app host directory available to the application via configuration
["AppHost:Directory"] = AppHostDirectory,
["AppHost:Path"] = AppHostPath,
["AppHost:FilePath"] = appHostFilePath,
["AppHost:DashboardApplicationName"] = dashboardApplicationName,
[AspireStore.AspireStorePathKeyName] = aspireDir
});
_executionContextOptions = BuildExecutionContextOptions();
ExecutionContext = new DistributedApplicationExecutionContext(_executionContextOptions);
// Compute path, deployment-state, and project-name identities for different use cases:
// - PathSha: Historical directory identity used by persistent run-mode resources.
// - DeploymentStatePathSha: Source-file-specific identity used by deployment state.
// - ProjectNameSha: For stable naming across deployments regardless of path (Azure Functions, Azure environments)
string appHostPathSha;
string deploymentStatePathSha;
string? legacyDeploymentStatePathSha = null;
string appHostProjectNameSha;
string appHostSha; // Legacy value, computed based on mode
// Check if AppHostSha is already configured (e.g., for testing scenarios)
var configuredAppHostSha = _innerBuilder.Configuration["AppHostSha"];
if (!string.IsNullOrEmpty(configuredAppHostSha))
{
// For backward compatibility with tests
appHostPathSha = configuredAppHostSha;
deploymentStatePathSha = configuredAppHostSha;
appHostProjectNameSha = configuredAppHostSha;
appHostSha = configuredAppHostSha;
}
else
{
var appHostPathShaBytes = SHA256.HashData(Encoding.UTF8.GetBytes(AppHostPath.ToLowerInvariant()));
appHostPathSha = Convert.ToHexString(appHostPathShaBytes);
// Source-file and polyglot AppHosts can share a host process and project directory,
// so use the actual source file to keep their deployment state isolated.
var isSourceFileAppHost = !string.IsNullOrEmpty(appHostFilePath) &&
!string.Equals(Path.GetExtension(appHostFilePath), ".csproj", StringComparison.OrdinalIgnoreCase);
var appHostIdentityPath = isSourceFileAppHost
? PathNormalizer.ResolveToFilesystemPath(Path.GetFullPath(appHostFilePath!))
: AppHostPath;
var normalizedAppHostIdentityPath = isSourceFileAppHost && !OperatingSystem.IsWindows()
? appHostIdentityPath
: appHostIdentityPath.ToLowerInvariant();
var deploymentStatePathShaBytes = SHA256.HashData(Encoding.UTF8.GetBytes(normalizedAppHostIdentityPath));
deploymentStatePathSha = Convert.ToHexString(deploymentStatePathShaBytes);
if (!string.Equals(deploymentStatePathSha, appHostPathSha, StringComparison.Ordinal))
{
legacyDeploymentStatePathSha = appHostPathSha;
}
// Compute ProjectNameSha
var appHostProjectNameShaBytes = SHA256.HashData(Encoding.UTF8.GetBytes(appHostName));
appHostProjectNameSha = Convert.ToHexString(appHostProjectNameShaBytes);
// For backward compatibility, AppHost:Sha256 uses the old logic:
// - Publish mode: ProjectNameSha (stable across paths)
// - Run mode: PathSha (disambiguates by path)
if (ExecutionContext.IsPublishMode)
{
appHostSha = appHostProjectNameSha;
}
else
{
appHostSha = appHostPathSha;
}
}
_innerBuilder.Configuration.AddInMemoryCollection(new Dictionary<string, string?>
{
// Historical path identity used by persistent run-mode resources.
["AppHost:PathSha256"] = appHostPathSha,
// Source-file-specific deployment identity and its migration fallback.
["AppHost:DeploymentStatePathSha256"] = deploymentStatePathSha,
["AppHost:LegacyDeploymentStatePathSha256"] = legacyDeploymentStatePathSha,
// ProjectNameSha for Azure Functions and Azure environments (stable naming)
["AppHost:ProjectNameSha256"] = appHostProjectNameSha,
// Legacy Sha256 for backward compatibility (mode-dependent)
["AppHost:Sha256"] = appHostSha
});
// Load deployment state early in the configuration chain if in publish mode
// This must happen before command line args are added so they can override saved state
if (ExecutionContext.IsPublishMode)
{
LoadDeploymentState(deploymentStatePathSha, legacyDeploymentStatePathSha);
}
// Core things
// Create and register the directory service (first, so it can be used by other services)
_directoryService = new FileSystemService(_innerBuilder.Configuration);
_innerBuilder.Services.AddSingleton<IFileSystemService>(sp =>
{
_directoryService.SetLogger(sp.GetRequiredService<ILogger<FileSystemService>>());
return _directoryService;
});
// Create and register the user secrets manager (uses the userSecretsId resolved at top of constructor)
var userSecretsFactory = new UserSecretsManagerFactory(_directoryService);
_userSecretsManager = !string.IsNullOrEmpty(userSecretsId)
? userSecretsFactory.GetOrCreateFromId(userSecretsId)
: NoopUserSecretsManager.Instance;
// Always register IUserSecretsManager so dependencies can resolve
_innerBuilder.Services.AddSingleton(_userSecretsManager);
_innerBuilder.Services.AddSingleton(sp => new DistributedApplicationModel(Resources));
_innerBuilder.Services.AddSingleton<PipelineExecutor>();
_innerBuilder.Services.AddHostedService<PipelineExecutor>(sp => sp.GetRequiredService<PipelineExecutor>());
_innerBuilder.Services.AddHostedService<DistributedApplicationLifecycle>();
_innerBuilder.Services.AddHostedService<VersionCheckService>();
_innerBuilder.Services.AddSingleton<IPackageFetcher, PackageFetcher>();
_innerBuilder.Services.AddSingleton<IPackageVersionProvider, PackageVersionProvider>();
_innerBuilder.Services.AddSingleton(options);
_innerBuilder.Services.AddSingleton<ResourceNotificationService>();
_innerBuilder.Services.AddSingleton<ResourceLoggerService>();
_innerBuilder.Services.AddSingleton<ResourceCommandService>(s => new ResourceCommandService(s.GetRequiredService<ResourceNotificationService>(), s.GetRequiredService<ResourceLoggerService>(), s));
_innerBuilder.Services.TryAddSingleton<IProcessRunner, DefaultProcessRunner>();
_innerBuilder.Services.AddSingleton<InteractionService>();
_innerBuilder.Services.AddSingleton<IInteractionService>(sp => sp.GetRequiredService<InteractionService>());
_innerBuilder.Services.AddSingleton<ParameterProcessor>(static sp =>
{
var parameterProcessor = ActivatorUtilities.CreateInstance<ParameterProcessor>(sp);
// Wire the AppHost-scoped redaction history after construction (not through the public constructor) so
// the processor records resolved secret values as they are assigned/replaced. This populates the
// describe/watch redaction set from startup, independent of any backchannel connection, while keeping
// ParameterProcessor's public constructor unchanged (https://github.com/microsoft/aspire/issues/19241).
parameterProcessor.SecretRedactionHistory = sp.GetRequiredService<SecretRedactionHistory>();
return parameterProcessor;
});
_innerBuilder.Services.AddSingleton<IDistributedApplicationEventing>(Eventing);
_innerBuilder.Services.AddSingleton<LocaleOverrideContext>();
_innerBuilder.Services.AddHealthChecks();
_innerBuilder.Services.AddHttpClient();
// Add the manifest publishing step to the pipeline
Pipeline.AddManifestPublishing();
_innerBuilder.Services.Configure<ResourceNotificationServiceOptions>(o =>
{
// Default to stopping on dependency failure if the dashboard is disabled. As there's no way to see or easily recover
// from a failure in that case.
o.DefaultWaitBehavior = options.DisableDashboard ? WaitBehavior.StopOnResourceUnavailable : WaitBehavior.WaitOnResourceUnavailable;
});
_innerBuilder.Services.AddSingleton<IAspireStore, AspireStore>(sp =>
{
var configuration = sp.GetRequiredService<IConfiguration>();
var aspireDir = configuration[AspireStore.AspireStorePathKeyName];
if (string.IsNullOrWhiteSpace(aspireDir))
{
throw new InvalidOperationException($"Could not determine an appropriate location for local storage. Set the {AspireStore.AspireStorePathKeyName} setting to a folder where the App Host content should be stored.");
}
var directoryService = sp.GetRequiredService<IFileSystemService>();
return new AspireStore(Path.Combine(aspireDir, ".aspire"), directoryService);
});
#pragma warning disable ASPIRECERTIFICATES001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
_innerBuilder.Services.AddSingleton<IDeveloperCertificateService, DeveloperCertificateService>();
#pragma warning restore ASPIRECERTIFICATES001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
// Shared DCP things (even though DCP isn't used in 'publish' and 'inspect' mode
// we still honour the DCP options around container runtime selection.
_innerBuilder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IConfigureOptions<DcpOptions>, ConfigureDefaultDcpOptions>());
_innerBuilder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IValidateOptions<DcpOptions>, ValidateDcpOptions>());
// Aspire CLI support
_innerBuilder.Services.AddHostedService<CliOrphanDetector>();
_innerBuilder.Services.AddSingleton<BackchannelService>();
_innerBuilder.Services.AddHostedService<BackchannelService>(sp => sp.GetRequiredService<BackchannelService>());
_innerBuilder.Services.AddSingleton<ProfilingTelemetry>();
_innerBuilder.Services.AddSingleton<AppHostStartupState>();
_innerBuilder.Services.AddSingleton<AuxiliaryBackchannelService>();
_innerBuilder.Services.AddHostedService<AuxiliaryBackchannelService>(sp => sp.GetRequiredService<AuxiliaryBackchannelService>());
// Shared by every per-connection AuxiliaryBackchannelRpcTarget so the describe/watch secret redaction set
// outlives an individual connection (https://github.com/microsoft/aspire/issues/19241).
_innerBuilder.Services.AddSingleton<SecretRedactionHistory>();
_innerBuilder.Services.AddSingleton<AppHostRpcTarget>();
_innerBuilder.Services.AddSingleton<IInteractionFileUploadStore, Dashboard.InteractionFileUploadStore>();
ConfigureHealthChecks();
if (ExecutionContext.IsRunMode)
{
// Dashboard
if (!options.DisableDashboard)
{
if (!IsDashboardUnsecured(_innerBuilder.Configuration))
{
// Passed to apps as a standard OTEL attribute to include in OTLP requests and the dashboard to validate.
// Set a random API key for the OTLP exporter if one isn't already present in configuration.
// If a key is generated, it's stored in the user secrets store so that it will be auto-loaded
// on subsequent runs and not recreated. This is important to ensure it doesn't change the state
// of persistent containers (as a new key would be a spec change).
_userSecretsManager.GetOrSetSecret(_innerBuilder.Configuration, "AppHost:OtlpApiKey", TokenGenerator.GenerateToken);
// Set a random API key for the Dashboard Telemetry API if one isn't already present in configuration.
_userSecretsManager.GetOrSetSecret(_innerBuilder.Configuration, "AppHost:DashboardApiKey", TokenGenerator.GenerateToken);
// Determine the frontend browser token.
if (_innerBuilder.Configuration.GetString(KnownConfigNames.DashboardFrontendBrowserToken,
KnownConfigNames.Legacy.DashboardFrontendBrowserToken, fallbackOnEmpty: true) is not { } browserToken)
{
// No browser token was specified in configuration, so generate one.
browserToken = TokenGenerator.GenerateToken();
}
_innerBuilder.Configuration.AddInMemoryCollection(
new Dictionary<string, string?>
{
["AppHost:BrowserToken"] = browserToken
}
);
// Determine the resource service API key.
var apiKey = _innerBuilder.Configuration.GetString(KnownConfigNames.DashboardResourceServiceClientApiKey,
KnownConfigNames.Legacy.DashboardResourceServiceClientApiKey, fallbackOnEmpty: true);
// If no API key was specified in configuration, generate one.
apiKey ??= TokenGenerator.GenerateToken();
_innerBuilder.Configuration.AddInMemoryCollection(
new Dictionary<string, string?>
{
["AppHost:ResourceService:AuthMode"] = nameof(ResourceServiceAuthMode.ApiKey),
["AppHost:ResourceService:ApiKey"] = apiKey
}
);
}
else
{
// The dashboard is enabled but is unsecured. Set auth mode config setting to reflect this state.
_innerBuilder.Configuration.AddInMemoryCollection(
new Dictionary<string, string?>
{
["AppHost:ResourceService:AuthMode"] = nameof(ResourceServiceAuthMode.Unsecured)
}
);
}
_innerBuilder.Services.AddOptions<TransportOptions>().ValidateOnStart().PostConfigure(MapTransportOptionsFromCustomKeys);
_innerBuilder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IValidateOptions<TransportOptions>, TransportOptionsValidator>());
_innerBuilder.Services.AddSingleton<DashboardServiceHost>();
_innerBuilder.Services.AddHostedService(sp => sp.GetRequiredService<DashboardServiceHost>());
_innerBuilder.Services.AddSingleton<IDashboardEndpointProvider, HostDashboardEndpointProvider>();
_innerBuilder.Services.AddEventingSubscriber<DashboardEventHandlers>();
_innerBuilder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IConfigureOptions<DashboardOptions>, ConfigureDefaultDashboardOptions>());
}
if (options.EnableResourceLogging)
{
// This must be added before DcpHostService to ensure that it can subscribe to the ResourceNotificationService and ResourceLoggerService
_innerBuilder.Services.AddHostedService<ResourceLoggerForwarderService>();
}
// Devcontainers & Codespaces & SSH Remote
_innerBuilder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IConfigureOptions<CodespacesOptions>, ConfigureCodespacesOptions>());
_innerBuilder.Services.AddSingleton<CodespacesUrlRewriter>();
_innerBuilder.Services.AddHostedService<CodespacesResourceUrlRewriterService>();
_innerBuilder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IConfigureOptions<DevcontainersOptions>, ConfigureDevcontainersOptions>());
_innerBuilder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<IConfigureOptions<SshRemoteOptions>, ConfigureSshRemoteOptions>());
_innerBuilder.Services.AddSingleton<DevcontainerSettingsWriter>();
_innerBuilder.Services.TryAddEventingSubscriber<DevcontainerPortForwardingEventingSubscriber>();
// Required command validation for resources
#pragma warning disable ASPIRECOMMAND001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
_innerBuilder.Services.TryAddSingleton<IRequiredCommandValidator, RequiredCommandValidator>();
#pragma warning restore ASPIRECOMMAND001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed.
_innerBuilder.Services.TryAddEventingSubscriber<RequiredCommandValidationEventingSubscriber>();
// Terminal host binary path resolution (WithTerminal)
_innerBuilder.Services.TryAddEventingSubscriber<TerminalHostEventingSubscriber>();
// Terminal host failure diagnostics (WithTerminal): unhides the failed host
// resource and writes an actionable diagnostic to its console log when a
// terminal host transitions to a terminal-failure state. Most common cause is
// a CLI/AppHost version mismatch (old bundled aspire-managed without the
// 'terminalhost' subcommand). See TerminalHostFailureDiagnosticService.
_innerBuilder.Services.AddHostedService<TerminalHostFailureDiagnosticService>();
}
ConfigureProfilingTelemetry();
if (ExecutionContext.IsRunMode)
{
// Orchestrator
_innerBuilder.Services.AddSingleton<ApplicationOrchestrator>();
_innerBuilder.Services.AddHostedService<OrchestratorHostService>();
// DCP stuff
_innerBuilder.Services.AddSingleton<DcpAppResourceStore>();
_innerBuilder.Services.AddSingleton<ProxylessEndpointPortAllocator>();
_innerBuilder.Services.AddSingleton<ExecutableConfigurationResolver>();
_innerBuilder.Services.AddSingleton<ExecutableLaunchPolicy>();
_innerBuilder.Services.AddSingleton<ExecutableCreator>();
_innerBuilder.Services.AddSingleton<ContainerCreator>();
_innerBuilder.Services.AddSingleton<DcpExecutor>();
_innerBuilder.Services.AddSingleton<IDcpExecutor>(sp => sp.GetRequiredService<DcpExecutor>());
_innerBuilder.Services.AddSingleton<DcpExecutorEvents>();
_innerBuilder.Services.AddSingleton<DcpHost>();
_innerBuilder.Services.AddSingleton<IDcpDependencyCheckService, DcpDependencyCheck>();
_innerBuilder.Services.AddSingleton<DcpNameGenerator>();
// Locations now uses IFileSystemService for DCP session storage
_innerBuilder.Services.AddSingleton<Locations>();
_innerBuilder.Services.AddSingleton<IKubernetesService, KubernetesService>();
Eventing.Subscribe<BeforeStartEvent>(BuiltInDistributedApplicationEventSubscriptionHandlers.InitializeDcpAnnotations);
Eventing.Subscribe<BeforeStartEvent>(BuiltInDistributedApplicationEventSubscriptionHandlers.WarnPersistentContainersWithoutUserSecrets);
}
// Publishing support
Eventing.Subscribe<BeforeStartEvent>(BuiltInDistributedApplicationEventSubscriptionHandlers.MutateHttp2TransportAsync);
_innerBuilder.Services.AddKeyedSingleton<IContainerRuntime, DockerContainerRuntime>(KnownContainerRuntimes.Docker);
_innerBuilder.Services.AddKeyedSingleton<IContainerRuntime, PodmanContainerRuntime>(KnownContainerRuntimes.Podman);
_innerBuilder.Services.AddSingleton<IContainerRuntimeResolver, ContainerRuntimeResolver>();
_innerBuilder.Services.AddSingleton<IResourceContainerImageManager, ResourceContainerImageManager>();
_innerBuilder.Services.AddSingleton<PipelineActivityReporter>();
_innerBuilder.Services.AddSingleton<IPipelineActivityReporter, PipelineActivityReporter>(sp => sp.GetRequiredService<PipelineActivityReporter>());
_innerBuilder.Services.AddSingleton<IPipelineOutputService, PipelineOutputService>();
_innerBuilder.Services.AddSingleton(Pipeline);
// Configure pipeline logging options
_innerBuilder.Services.Configure<PipelineLoggingOptions>(options =>
{
var config = _innerBuilder.Configuration;
options.MinimumLogLevel = config["Pipeline:LogLevel"]?.ToLowerInvariant() switch
{
"trace" => LogLevel.Trace,
"debug" => LogLevel.Debug,
"info" or "information" => LogLevel.Information,
"warn" or "warning" => LogLevel.Warning,
"error" => LogLevel.Error,
"crit" or "critical" => LogLevel.Critical,
_ => LogLevel.Information
};
options.IncludeExceptionDetails = config.GetBool("Pipeline:IncludeExceptionDetails") ?? false;
});
_innerBuilder.Services.AddSingleton<ILoggerProvider, PipelineLoggerProvider>();
// Configure logging filter using the PipelineLoggingOptions
_innerBuilder.Services.AddOptions<LoggerFilterOptions>().Configure<IOptions<PipelineLoggingOptions>>((filterLoggingOptions, pipelineLoggingOptions) =>
{
filterLoggingOptions.AddFilter<PipelineLoggerProvider>((level) => level >= pipelineLoggingOptions.Value.MinimumLogLevel);
});
// Register IDeploymentStateManager based on execution context
if (ExecutionContext.IsPublishMode)
{
_innerBuilder.Services.TryAddSingleton<IDeploymentStateManager, FileDeploymentStateManager>();
}
else
{
_innerBuilder.Services.TryAddSingleton<IDeploymentStateManager, UserSecretsDeploymentStateManager>();
}
Eventing.Subscribe<BeforeStartEvent>(BuiltInDistributedApplicationEventSubscriptionHandlers.ExcludeDashboardFromManifestAsync);
// Overwrite registry if override specified in options
if (!string.IsNullOrEmpty(options.ContainerRegistryOverride))
{
Eventing.Subscribe<BeforeStartEvent>((e, ct) => BuiltInDistributedApplicationEventSubscriptionHandlers.UpdateContainerRegistryAsync(e, options));
}
_innerBuilder.Services.AddSingleton(ExecutionContext);
LogBuilderConstructed(this);
ProfilingTelemetry.RecordAppHostStartupEvent(ProfilingTelemetry.Events.AppHostBuilderConstructed, _innerBuilder.Configuration);
}
private void ConfigureHealthChecks()
{
_innerBuilder.Services.AddSingleton<IValidateOptions<HealthCheckServiceOptions>>(sp =>
{
var appModel = sp.GetRequiredService<DistributedApplicationModel>();
var logger = sp.GetRequiredService<ILogger<DistributedApplicationBuilder>>();
// Generic message (we update it in the callback to make it more specific).
var failureMessage = "A health check registration is missing. Check logs for more details.";
return new ValidateOptions<HealthCheckServiceOptions>(null, (options) =>
{
var resourceHealthChecks = appModel.Resources.SelectMany(
r => r.Annotations.OfType<HealthCheckAnnotation>().Select(hca => new { Resource = r, Annotation = hca })
);
var healthCheckRegistrationKeys = options.Registrations.Select(hcr => hcr.Name).ToHashSet();
var missingResourceHealthChecks = resourceHealthChecks.Where(rhc => !healthCheckRegistrationKeys.Contains(rhc.Annotation.Key));
foreach (var missingResourceHealthCheck in missingResourceHealthChecks)
{
sp.GetRequiredService<ILogger<DistributedApplicationBuilder>>().LogCritical(
"The health check '{Key}' is not registered and is required for resource '{ResourceName}'.",
missingResourceHealthCheck.Annotation.Key,
missingResourceHealthCheck.Resource.Name);
}
return !missingResourceHealthChecks.Any();
}, failureMessage);
});
_innerBuilder.Services.AddSingleton<IConfigureOptions<HealthCheckPublisherOptions>>(sp =>
{
return new ConfigureOptions<HealthCheckPublisherOptions>(options =>
{
if (ExecutionContext.IsPublishMode)
{
// In publish mode we don't run any checks.
options.Predicate = (check) => false;
}
});
});
if (ExecutionContext.IsRunMode)
{
_innerBuilder.Services.AddSingleton<ResourceHealthCheckService>();
_innerBuilder.Services.AddHostedService<ResourceHealthCheckService>(sp => sp.GetRequiredService<ResourceHealthCheckService>());
}
}
private void ConfigureProfilingTelemetry()
{
if (!ShouldConfigureProfilingTelemetry())
{
return;
}
var resourceBuilder = OpenTelemetry.Resources.ResourceBuilder.CreateDefault()
.AddService(
serviceName: "aspire-apphost",
serviceVersion: GetAppHostServiceVersion())
.AddAttributes(ProfilingTelemetry.CreateAppHostResourceAttributes(AppHostPath, ExecutionContext.Operation.ToString()));
_innerBuilder.Services.AddOpenTelemetry()
.WithTracing(builder =>
{
builder
.AddSource(ProfilingTelemetry.ActivitySourceName)
.SetResourceBuilder(resourceBuilder);
if (!string.IsNullOrEmpty(_innerBuilder.Configuration[KnownOtelConfigNames.ExporterOtlpEndpoint]))
{
builder.AddOtlpExporter();
}
else
{
var (url, protocol) = OtlpEndpointResolver.ResolveOtlpEndpoint(_innerBuilder.Configuration);
builder.AddOtlpExporter(options =>
{
options.Endpoint = new Uri(url);
options.Protocol = protocol switch
{
"http/protobuf" => OtlpExportProtocol.HttpProtobuf,
_ => OtlpExportProtocol.Grpc
};
if (_innerBuilder.Configuration["AppHost:OtlpApiKey"] is { } otlpApiKey)
{
options.Headers = $"x-otlp-api-key={otlpApiKey}";
}
});
}
});
}
private bool ShouldConfigureProfilingTelemetry()
{
// Dashboard OTLP is normally configured for app telemetry. Profiling
// spans are high-cardinality diagnostics, so only export them when requested.
// This intentionally supports publish/deploy/inspect operations as well as
// run so profiling can follow the full CLI/AppHost/pipeline operation.
var profilingEnabled =
_innerBuilder.Configuration.GetBool(KnownConfigNames.ProfilingEnabled) ??
_innerBuilder.Configuration.GetBool(KnownConfigNames.Legacy.StartupProfilingEnabled);
if (profilingEnabled is not true)
{
return false;
}
if (!string.IsNullOrEmpty(_innerBuilder.Configuration[KnownOtelConfigNames.ExporterOtlpEndpoint]))
{
return true;
}
var dashboardOtlpGrpcUrl = _innerBuilder.Configuration.GetString(KnownConfigNames.DashboardOtlpGrpcEndpointUrl, KnownConfigNames.Legacy.DashboardOtlpGrpcEndpointUrl);
var dashboardOtlpHttpUrl = _innerBuilder.Configuration.GetString(KnownConfigNames.DashboardOtlpHttpEndpointUrl, KnownConfigNames.Legacy.DashboardOtlpHttpEndpointUrl);
return !string.IsNullOrEmpty(dashboardOtlpGrpcUrl) || !string.IsNullOrEmpty(dashboardOtlpHttpUrl);
}
private static string? GetAppHostServiceVersion()
{
return typeof(DistributedApplication).Assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion;
}
private void MapTransportOptionsFromCustomKeys(TransportOptions options)
{
if (Configuration.GetBool(KnownConfigNames.AllowUnsecuredTransport) is { } allowUnsecuredTransport)
{
options.AllowUnsecureTransport = allowUnsecuredTransport;
}
}
private static bool IsDashboardUnsecured(IConfiguration configuration)
{
return configuration.GetBool(KnownConfigNames.DashboardUnsecuredAllowAnonymous, KnownConfigNames.Legacy.DashboardUnsecuredAllowAnonymous) ?? false;
}
private void ConfigurePipelineOptions(DistributedApplicationOptions options)
{
var switchMappings = new Dictionary<string, string>()
{
// Legacy mappings for backward compatibility
{ "--operation", "AppHost:Operation" },
{ "--publisher", "Publishing:Publisher" },
// Pipeline options (valid for aspire do based commands)
{ "--step", "Pipeline:Step" },
{ "--list-steps", "Pipeline:ListSteps" },
{ "--output-path", "Pipeline:OutputPath" },
{ "--log-level", "Pipeline:LogLevel" },
{ "--include-exception-details", "Pipeline:IncludeExceptionDetails" },
// TODO: Rename this to something related to deployment state
{ "--clear-cache", "Pipeline:ClearCache" },
{ "--yes", "Pipeline:SkipConfirmation" },
// DCP Publisher options, we should only process these in run mode
{ "--dcp-cli-path", "DcpPublisher:CliPath" },
{ "--dcp-container-runtime", "DcpPublisher:ContainerRuntime" },
{ "--dcp-dependency-check-timeout", "DcpPublisher:DependencyCheckTimeout" },
{ "--dcp-dashboard-path", "DcpPublisher:DashboardPath" }
};
_innerBuilder.Configuration.AddCommandLine(options.Args ?? [], switchMappings);
// Configure PipelineOptions from the Pipeline section
_innerBuilder.Services.Configure<PipelineOptions>(_innerBuilder.Configuration.GetSection("Pipeline"));
// Handle backward compatibility for --publisher manifest to support `azd` scenarios
var publisher = _innerBuilder.Configuration["Publishing:Publisher"];
if (string.Equals(publisher, "manifest", StringComparison.OrdinalIgnoreCase))
{
// If no explicit --step was provided, set it to run only the manifest step
if (string.IsNullOrEmpty(_innerBuilder.Configuration["Pipeline:Step"]))
{
_innerBuilder.Configuration["Pipeline:Step"] = "publish-manifest";
}
// If no explicit operation was set, default to Publish mode
if (string.IsNullOrEmpty(_innerBuilder.Configuration["AppHost:Operation"]))
{
_innerBuilder.Configuration["AppHost:Operation"] = "Publish";
}
}
}
/// <inheritdoc />
public DistributedApplication Build()
{
ProfilingTelemetry.RecordAppHostStartupEvent(ProfilingTelemetry.Events.AppHostBuildStarted, _innerBuilder.Configuration);
LogAppBuilding(this);
// ResourceCollection enforces unique names on Add/Insert/Set, but IResourceCollection
// could have a different implementation that doesn't. Validate as a safety net.
foreach (var duplicateResourceName in Resources.GroupBy(r => r.Name, StringComparers.ResourceName)
.Where(g => g.Count() > 1)
.Select(g => g.Key))
{
throw new DistributedApplicationException($"Multiple resources with the name '{duplicateResourceName}'. Resource names are case-insensitive.");
}
// Validate resource names. Resources added directly to the collection bypass AddResource validation.
foreach (var resource in Resources)
{
ValidateResourceName(resource);
}
var application = new DistributedApplication(_innerBuilder.Build());
_executionContextOptions.Services = application.Services.GetRequiredService<IServiceProvider>();
LogAppBuilt(application);
ProfilingTelemetry.RecordAppHostStartupEvent(ProfilingTelemetry.Events.AppHostBuildCompleted, _innerBuilder.Configuration);
return application;
}
/// <inheritdoc />
public IResourceBuilder<T> AddResource<T>(T resource) where T : IResource
{
ArgumentNullException.ThrowIfNull(resource);
ValidateResourceName(resource);
Resources.Add(resource);
return CreateResourceBuilder(resource);
}
/// <inheritdoc />
public IResourceBuilder<T> CreateResourceBuilder<T>(T resource) where T : IResource
{
ArgumentNullException.ThrowIfNull(resource);
var builder = new DistributedApplicationResourceBuilder<T>(this, resource);
return builder;
}
internal static string? ResolveUserSecretsId(Assembly? appHostAssembly, IConfiguration configuration)
{
ArgumentNullException.ThrowIfNull(configuration);
// An explicitly configured value should win over any assembly-level UserSecretsId so guest AppHosts can
// direct secrets to their synthetic store instead of the generated server project's store.
var configuredUserSecretsId = configuration[KnownConfigNames.AspireUserSecretsId];
var assemblyUserSecretsId = appHostAssembly?.GetCustomAttribute<UserSecretsIdAttribute>()?.UserSecretsId;
return string.IsNullOrWhiteSpace(configuredUserSecretsId) ? assemblyUserSecretsId : configuredUserSecretsId;
}
internal static void AddConfiguredUserSecrets(IConfigurationManager configuration, Assembly? appHostAssembly, string? configuredUserSecretsId, bool isDevelopment)
{
ArgumentNullException.ThrowIfNull(configuration);
// Add explicitly configured user secrets early so they have the same precedence as the default AddUserSecrets
// called by HostApplicationBuilder for .NET projects, and can replace any assembly-level store when needed.
// Only add in Development environment, matching the behavior of the default AddUserSecrets.
if (!string.IsNullOrWhiteSpace(configuredUserSecretsId) && isDevelopment)
{
// Remove only the file-backed source for the assembly-derived user-secrets ID so the configured
// user-secrets store replaces that single source without affecting other configuration providers.
RemoveUserSecretsSource(configuration, appHostAssembly?.GetCustomAttribute<UserSecretsIdAttribute>()?.UserSecretsId);
configuration.AddUserSecrets(configuredUserSecretsId);
}
}
internal static void RemoveUserSecretsSource(IConfigurationManager configuration, string? userSecretsId)
{
ArgumentNullException.ThrowIfNull(configuration);
if (string.IsNullOrWhiteSpace(userSecretsId))
{
return;
}
var userSecretsFilePath = Path.GetFullPath(UserSecretsPathHelper.GetSecretsPathFromSecretsId(userSecretsId));
for (var i = configuration.Sources.Count - 1; i >= 0; i--)
{
if (configuration.Sources[i] is not FileConfigurationSource fileSource)
{
continue;
}
if (fileSource.Path is null)
{
continue;
}
var filePath = fileSource.FileProvider?.GetFileInfo(fileSource.Path).PhysicalPath;
if (filePath is not null && PathsEqual(filePath, userSecretsFilePath))
{
configuration.Sources.RemoveAt(i);
}
}
}
private static void ValidateResourceName(IResource resource)
{
if (!resource.TryGetLastAnnotation<NameValidationPolicyAnnotation>(out var policy))
{
policy = NameValidationPolicyAnnotation.Default;
}
ModelName.ValidateName(nameof(Aspire.Hosting.ApplicationModel.Resource), resource.Name, policy.MaxLength, policy.ValidateStartsWithLetter, policy.ValidateAllowedCharacters, policy.ValidateNoConsecutiveHyphens, policy.ValidateNoTrailingHyphen);
}
private static bool PathsEqual(string left, string right) =>
string.Equals(Path.GetFullPath(left), Path.GetFullPath(right), OperatingSystem.IsWindows() ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal);
private static DiagnosticListener LogBuilderConstructing(DistributedApplicationOptions appBuilderOptions, HostApplicationBuilderSettings hostBuilderOptions)
{
var diagnosticListener = new DiagnosticListener(HostingDiagnosticListenerName);
if (diagnosticListener.IsEnabled() && diagnosticListener.IsEnabled(BuilderConstructingEventName))
{
diagnosticListener.Write(BuilderConstructingEventName, (appBuilderOptions, hostBuilderOptions));
}
return diagnosticListener;
}
private static DiagnosticListener LogBuilderConstructed(DistributedApplicationBuilder builder)
{
var diagnosticListener = new DiagnosticListener(HostingDiagnosticListenerName);
if (diagnosticListener.IsEnabled() && diagnosticListener.IsEnabled(BuilderConstructedEventName))
{
diagnosticListener.Write(BuilderConstructedEventName, builder);
}
return diagnosticListener;
}
private static DiagnosticListener LogAppBuilding(DistributedApplicationBuilder appBuilder)
{
var diagnosticListener = new DiagnosticListener(HostingDiagnosticListenerName);
if (diagnosticListener.IsEnabled() && diagnosticListener.IsEnabled(ApplicationBuildingEventName))
{
diagnosticListener.Write(ApplicationBuildingEventName, appBuilder);
}
return diagnosticListener;
}
private static DiagnosticListener LogAppBuilt(DistributedApplication app)
{
var diagnosticListener = new DiagnosticListener(HostingDiagnosticListenerName);
if (diagnosticListener.IsEnabled() && diagnosticListener.IsEnabled(ApplicationBuiltEventName))
{
diagnosticListener.Write(ApplicationBuiltEventName, app);
}
return diagnosticListener;
}
/// <summary>
/// Loads deployment state from the filesystem based on the app host SHA and environment name.
/// Only loads if ClearCache is false.
/// </summary>
/// <param name="appHostSha">The current SHA hash of the app host.</param>
/// <param name="legacyAppHostSha">The previous SHA hash used for source-file AppHosts.</param>
private void LoadDeploymentState(string appHostSha, string? legacyAppHostSha)
{
// Only load if ClearCache is false
var clearCache = _innerBuilder.Configuration.GetValue<bool>("Pipeline:ClearCache");
if (clearCache)
{
return;
}
var environment = _innerBuilder.Environment.EnvironmentName.ToLowerInvariant();
try
{
// GetStatePath validates the environment name and throws ArgumentException for names
// outside [a-zA-Z0-9_-]. Compute the paths inside the try so an unusual environment name
// degrades to skipping this best-effort load instead of failing builder construction.
var deploymentStatePath = FileDeploymentStateManager.GetStatePath(
_innerBuilder.Configuration,
appHostSha,
environment)!;
var legacyDeploymentStatePath = string.IsNullOrEmpty(legacyAppHostSha)
? null
: FileDeploymentStateManager.GetStatePath(
_innerBuilder.Configuration,
legacyAppHostSha,
environment);
var effectiveState = FileDeploymentStateManager.LoadEffectiveState(
deploymentStatePath,
legacyDeploymentStatePath);
if (effectiveState.Count > 0)
{
var flattenedState = JsonFlattener.FlattenJsonObject(effectiveState);
using var stream = new MemoryStream(Encoding.UTF8.GetBytes(flattenedState.ToJsonString()));
_innerBuilder.Configuration.AddJsonStream(stream);
}
}
catch (Exception ex) when (ex is ArgumentException or IOException or UnauthorizedAccessException or JsonException or InvalidDataException or FormatException or TimeoutException)
{
Debug.WriteLine($"Failed to load deployment state for environment '{environment}': {ex}");
}
}
/// <summary>
/// Gets the metadata value for the specified key from the assembly metadata.
/// </summary>
/// <param name="assemblyMetadata">The assembly metadata.</param>
/// <param name="key">The key to look for.</param>
/// <returns>The metadata value if found; otherwise, null.</returns>
private static string? GetMetadataValue(IEnumerable<AssemblyMetadataAttribute>? assemblyMetadata, string key) =>
assemblyMetadata?.FirstOrDefault(a => string.Equals(a.Key, key, StringComparison.OrdinalIgnoreCase))?.Value;
private static string ResolveAspireStorePath(IEnumerable<AssemblyMetadataAttribute>? assemblyMetadata, string appHostDirectory)
{
var baseIntermediateOutputPath = GetMetadataValue(assemblyMetadata, "AppHostProjectBaseIntermediateOutputPath");
if (!string.IsNullOrEmpty(baseIntermediateOutputPath))
{
return baseIntermediateOutputPath;
}
// File-based and dynamically loaded AppHosts do not have the MSBuild intermediate output
// metadata that normal project AppHosts get. Use the AppHost directory as the root so
// IAspireStore resolves to the workspace-local .aspire folder instead of creating a
// .NET-style obj directory for non-.NET AppHosts.
return Path.GetFullPath(appHostDirectory);
}
}