File: ApplicationModel\ProjectLaunchConfigurationFactory.cs
Web Access
Project: src\src\Aspire.Hosting\Aspire.Hosting.csproj (Aspire.Hosting)
// 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 ASPIREEXTENSION001
#pragma warning disable ASPIREPROJECTS001
 
namespace Aspire.Hosting.ApplicationModel;
 
/// <summary>
/// Builds the <see cref="ProjectLaunchConfiguration"/> that Aspire hands to the IDE for a .NET resource.
/// </summary>
internal static class ProjectLaunchConfigurationFactory
{
    public static ProjectLaunchConfiguration Create(IResource resource, string mode)
    {
        if (!resource.TryGetProjectMetadata(out var projectMetadata))
        {
            throw new InvalidOperationException(
                $"Resource '{resource.Name}' cannot produce a \"{KnownLaunchConfigurationTypes.Project}\" launch configuration because it has no project metadata. " +
                $"The \"{KnownLaunchConfigurationTypes.Project}\" launch configuration type is reserved for .NET project resources; use a resource that carries {nameof(IProjectMetadata)} or a different launch configuration type.");
        }
 
        return Create(resource, projectMetadata, mode);
    }
 
    public static ProjectLaunchConfiguration Create(IResource resource, IProjectMetadata projectMetadata, string mode)
    {
        resource.TryGetLastAnnotation<ProjectLaunchDefaultsAnnotation>(out var launchDefaults);
        var projectLaunchConfiguration = new ProjectLaunchConfiguration
        {
            Type = GetLaunchConfigurationType(resource, projectMetadata),
            ProjectPath = projectMetadata.ProjectPath,
            Mode = mode,
            BuildConfiguration = launchDefaults?.BuildConfiguration,
            BuildEnvironment = projectMetadata.BuildEnvironment.Count == 0
                ? null
                : projectMetadata.BuildEnvironment.ToDictionary(),
            BuildWorkingDirectory = projectMetadata.BuildWorkingDirectory,
            SuppressBuild = ShouldSuppressIdeBuild(resource, projectMetadata),
            // The launch profile selection lives on the resource rather than on the project metadata, so it
            // can only be resolved when the configuration is produced, not when debug support is registered.
            DisableLaunchProfile = resource.TryGetLastAnnotation<ExcludeLaunchProfileAnnotation>(out _)
        };
 
        // Use the effective launch profile which has fallback logic
        if (!projectLaunchConfiguration.DisableLaunchProfile && resource.GetEffectiveLaunchProfile() is NamedLaunchProfile namedLaunchProfile)
        {
            projectLaunchConfiguration.LaunchProfile = namedLaunchProfile.Name;
        }
 
        return projectLaunchConfiguration;
    }
 
    internal static string GetLaunchConfigurationType(IResource resource, IProjectMetadata projectMetadata) =>
        ShouldSuppressIdeBuild(resource, projectMetadata)
            ? KnownLaunchConfigurationTypes.ProjectWithExternalBuild
            : KnownLaunchConfigurationTypes.Project;
 
    private static bool ShouldSuppressIdeBuild(IResource resource, IProjectMetadata projectMetadata)
    {
        // Generated metadata for classic ProjectResource instances has long used SuppressBuild=true to keep
        // the DCP process path from rebuilding outputs already produced with the AppHost. The IDE contract
        // historically still rebuilt missing output and honored force-build requests for those resources.
        // Preserve that behavior while allowing Project v2 executable resources to delegate build ownership
        // to their coordinated build.
        return resource is not ProjectResource && projectMetadata.SuppressBuild;
    }
}