Skip to content

Add support for flat launch settings #49769

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 1 addition & 4 deletions src/BuiltInTools/dotnet-watch/Browser/BrowserConnector.cs
Original file line number Diff line number Diff line change
Expand Up @@ -299,11 +299,8 @@ public bool IsServerSupported(ProjectGraphNode projectNode, HotReloadAppModel ap

private LaunchSettingsProfile GetLaunchProfile(ProjectOptions projectOptions)
{
var projectDirectory = Path.GetDirectoryName(projectOptions.ProjectPath);
Debug.Assert(projectDirectory != null);

return (projectOptions.NoLaunchProfile == true
? null : LaunchSettingsProfile.ReadLaunchProfile(projectDirectory, projectOptions.LaunchProfileName, context.Reporter)) ?? new();
? null : LaunchSettingsProfile.ReadLaunchProfile(projectOptions.ProjectPath, projectOptions.LaunchProfileName, context.Reporter)) ?? new();
}
}
}
18 changes: 14 additions & 4 deletions src/BuiltInTools/dotnet-watch/Process/LaunchSettingsProfile.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,10 @@
// The .NET Foundation licenses this file to you under the MIT license.


using System.Diagnostics;
using System.Text.Json;
using System.Text.Json.Serialization;
using Microsoft.DotNet.Cli.Commands.Run;

namespace Microsoft.DotNet.Watch
{
Expand All @@ -22,12 +24,20 @@ internal sealed class LaunchSettingsProfile
public bool LaunchBrowser { get; init; }
public string? LaunchUrl { get; init; }

internal static LaunchSettingsProfile? ReadLaunchProfile(string projectDirectory, string? launchProfileName, IReporter reporter)
internal static LaunchSettingsProfile? ReadLaunchProfile(string projectPath, string? launchProfileName, IReporter reporter)
{
var launchSettingsPath = Path.Combine(projectDirectory, "Properties", "launchSettings.json");
var projectDirectory = Path.GetDirectoryName(projectPath);
Debug.Assert(projectDirectory != null);

var launchSettingsPath = CommonRunHelpers.GetPropertiesLaunchSettingsPath(projectDirectory, "Properties");
if (!File.Exists(launchSettingsPath))
{
return null;
var projectNameWithoutExtension = Path.GetFileNameWithoutExtension(projectPath);
launchSettingsPath = CommonRunHelpers.GetFlatLaunchSettingsPath(projectDirectory, projectNameWithoutExtension);
if (!File.Exists(launchSettingsPath))
{
return null;
}
}

LaunchSettingsJson? launchSettings;
Expand All @@ -39,7 +49,7 @@ internal sealed class LaunchSettingsProfile
}
catch (Exception ex)
{
reporter.Verbose($"Error reading launchSettings.json: {ex}.");
reporter.Verbose($"Error reading '{launchSettingsPath}': {ex}.");
return null;
}

Expand Down
10 changes: 6 additions & 4 deletions src/Cli/dotnet/Commands/CliCommandStrings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -663,7 +663,8 @@ dotnet.config is a name don't translate.</comment>
<value>Do not use arguments specified in launch profile to run the application.</value>
</data>
<data name="CommandOptionNoLaunchProfileDescription" xml:space="preserve">
<value>Do not attempt to use launchSettings.json to configure the application.</value>
<value>Do not attempt to use launchSettings.json or [app].run.json to configure the application.</value>
<comment>{Locked="launchSettings.json"}{Locked=".run.json"}</comment>
</data>
<data name="CommandOptionProjectDescription" xml:space="preserve">
<value>The path to the project file to run (defaults to the current directory if there is only one project).</value>
Expand Down Expand Up @@ -731,8 +732,8 @@ dotnet.config is a name don't translate.</comment>
<value>Description</value>
</data>
<data name="DeserializationExceptionMessage" xml:space="preserve">
<value>An error was encountered when reading launchSettings.json.
{0}</value>
<value>An error was encountered when reading '{0}': {1}</value>
<comment>{0} is file path. {1} is exception message.</comment>
</data>
<data name="DetailDescription" xml:space="preserve">
<value>Show detail result of the query.</value>
Expand Down Expand Up @@ -1672,7 +1673,8 @@ The default is to publish a framework-dependent application.</value>
{1}</value>
</data>
<data name="RunCommandExceptionCouldNotLocateALaunchSettingsFile" xml:space="preserve">
<value>The specified launch profile '{0}' could not be located.</value>
<value>Cannot use launch profile '{0}' because the launch settings file could not be located. Locations tried:
{1}</value>
</data>
<data name="RunCommandExceptionMultipleProjects" xml:space="preserve">
<value>Specify which project file to use because {0} contains more than one project file.</value>
Expand Down
6 changes: 6 additions & 0 deletions src/Cli/dotnet/Commands/Run/CommonRunHelpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,4 +36,10 @@ public static Dictionary<string, string> GetGlobalPropertiesFromArgs(string[] ar
}
return globalProperties;
}

public static string GetPropertiesLaunchSettingsPath(string directoryPath, string propertiesDirectoryName)
Copy link
Preview

Copilot AI Jul 15, 2025

Choose a reason for hiding this comment

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

[nitpick] The file path helpers use both Path.Combine and Path.Join in this class. Consider unifying on one approach (e.g. Path.Combine) for consistency and clarity.

Copilot uses AI. Check for mistakes.

Copy link
Member Author

Choose a reason for hiding this comment

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

The existing helper used Path.Combine and I opted to preserve that to avoid a break.

=> Path.Combine(directoryPath, propertiesDirectoryName, "launchSettings.json");

public static string GetFlatLaunchSettingsPath(string directoryPath, string projectNameWithoutExtension)
=> Path.Join(directoryPath, $"{projectNameWithoutExtension}.run.json");
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,9 @@ static LaunchSettingsManager()
};
}

public static LaunchSettingsApplyResult TryApplyLaunchSettings(string launchSettingsJsonContents, string? profileName = null)
public static LaunchSettingsApplyResult TryApplyLaunchSettings(string launchSettingsPath, string? profileName = null)
{
var launchSettingsJsonContents = File.ReadAllText(launchSettingsPath);
try
{
var jsonDocumentOptions = new JsonDocumentOptions
Expand Down Expand Up @@ -115,7 +116,7 @@ public static LaunchSettingsApplyResult TryApplyLaunchSettings(string launchSett
}
catch (JsonException ex)
{
return new LaunchSettingsApplyResult(false, string.Format(CliCommandStrings.DeserializationExceptionMessage, ex.Message));
return new LaunchSettingsApplyResult(false, string.Format(CliCommandStrings.DeserializationExceptionMessage, launchSettingsPath, ex.Message));
}
}

Expand Down
51 changes: 34 additions & 17 deletions src/Cli/dotnet/Commands/Run/RunCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -200,13 +200,9 @@ internal bool TryGetLaunchProfileSettingsIfNeeded(out ProjectLaunchSettingsModel
return true;
}

var launchSettingsPath = ReadCodeFromStdin ? null : TryFindLaunchSettings(ProjectFileFullPath ?? EntryPointFileFullPath!);
if (!File.Exists(launchSettingsPath))
var launchSettingsPath = ReadCodeFromStdin ? null : TryFindLaunchSettings(projectOrEntryPointFilePath: ProjectFileFullPath ?? EntryPointFileFullPath!, launchProfile: LaunchProfile);
if (launchSettingsPath is null)
{
if (!string.IsNullOrEmpty(LaunchProfile))
{
Reporter.Error.WriteLine(string.Format(CliCommandStrings.RunCommandExceptionCouldNotLocateALaunchSettingsFile, launchSettingsPath).Bold().Red());
}
return true;
}

Expand All @@ -219,8 +215,7 @@ internal bool TryGetLaunchProfileSettingsIfNeeded(out ProjectLaunchSettingsModel

try
{
var launchSettingsFileContents = File.ReadAllText(launchSettingsPath);
var applyResult = LaunchSettingsManager.TryApplyLaunchSettings(launchSettingsFileContents, LaunchProfile);
var applyResult = LaunchSettingsManager.TryApplyLaunchSettings(launchSettingsPath, LaunchProfile);
if (!applyResult.Success)
{
Reporter.Error.WriteLine(string.Format(CliCommandStrings.RunCommandExceptionCouldNotApplyLaunchSettings, profileName, applyResult.FailureReason).Bold().Red());
Expand All @@ -239,13 +234,9 @@ internal bool TryGetLaunchProfileSettingsIfNeeded(out ProjectLaunchSettingsModel

return true;

static string? TryFindLaunchSettings(string projectOrEntryPointFilePath)
static string? TryFindLaunchSettings(string projectOrEntryPointFilePath, string? launchProfile)
{
var buildPathContainer = File.Exists(projectOrEntryPointFilePath) ? Path.GetDirectoryName(projectOrEntryPointFilePath) : projectOrEntryPointFilePath;
if (buildPathContainer is null)
{
return null;
}
var buildPathContainer = File.Exists(projectOrEntryPointFilePath) ? Path.GetDirectoryName(projectOrEntryPointFilePath)! : projectOrEntryPointFilePath;

string propsDirectory;

Expand All @@ -261,8 +252,28 @@ internal bool TryGetLaunchProfileSettingsIfNeeded(out ProjectLaunchSettingsModel
propsDirectory = "Properties";
}

var launchSettingsPath = Path.Combine(buildPathContainer, propsDirectory, "launchSettings.json");
return launchSettingsPath;
var launchSettingsPath = CommonRunHelpers.GetPropertiesLaunchSettingsPath(buildPathContainer, propsDirectory);
if (File.Exists(launchSettingsPath))
{
return launchSettingsPath;
}

string appName = Path.GetFileNameWithoutExtension(projectOrEntryPointFilePath);
string runJsonPath = CommonRunHelpers.GetFlatLaunchSettingsPath(buildPathContainer, appName);
if (File.Exists(runJsonPath))
{
return runJsonPath;
}

if (!string.IsNullOrEmpty(launchProfile))
{
Reporter.Error.WriteLine(string.Format(CliCommandStrings.RunCommandExceptionCouldNotLocateALaunchSettingsFile, launchProfile, $"""
{launchSettingsPath}
{runJsonPath}
""").Red());
Copy link
Preview

Copilot AI Jul 15, 2025

Choose a reason for hiding this comment

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

[nitpick] Error messages elsewhere use .Bold().Red() for emphasis before writing to the reporter. For consistency, consider adding .Bold() here as well.

Suggested change
""").Red());
""").Bold().Red());

Copilot uses AI. Check for mistakes.

Copy link
Member Author

Choose a reason for hiding this comment

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

This is not a hard error (in fact, the command continues to run in spite of the error), so I feel like bolding it is not necessary.

}

return null;
}
}

Expand Down Expand Up @@ -552,6 +563,7 @@ public static RunCommand FromParseResult(ParseResult parseResult)
string? projectFilePath = DiscoverProjectFilePath(projectOption, readCodeFromStdin, ref args, out string? entryPointFilePath);

bool noBuild = parseResult.HasOption(RunCommandParser.NoBuildOption);
string launchProfile = parseResult.GetValue(RunCommandParser.LaunchProfileOption) ?? string.Empty;

if (readCodeFromStdin && entryPointFilePath != null)
{
Expand All @@ -562,6 +574,11 @@ public static RunCommand FromParseResult(ParseResult parseResult)
throw new GracefulException(CliCommandStrings.InvalidOptionForStdin, RunCommandParser.NoBuildOption.Name);
}

if (!string.IsNullOrWhiteSpace(launchProfile))
{
throw new GracefulException(CliCommandStrings.InvalidOptionForStdin, RunCommandParser.LaunchProfileOption.Name);
}

// If '-' is specified as the input file, read all text from stdin into a temporary file and use that as the entry point.
// We create a new directory for each file so other files are not included in the compilation.
// We fail if the file already exists to avoid reusing the same file for multiple stdin runs (in case the random name is duplicate).
Expand All @@ -584,7 +601,7 @@ public static RunCommand FromParseResult(ParseResult parseResult)
noBuild: noBuild,
projectFileFullPath: projectFilePath,
entryPointFileFullPath: entryPointFilePath,
launchProfile: parseResult.GetValue(RunCommandParser.LaunchProfileOption) ?? string.Empty,
launchProfile: launchProfile,
noLaunchProfile: parseResult.HasOption(RunCommandParser.NoLaunchProfileOption),
noLaunchProfileArguments: parseResult.HasOption(RunCommandParser.NoLaunchProfileArgumentsOption),
noRestore: parseResult.HasOption(RunCommandParser.NoRestoreOption) || parseResult.HasOption(RunCommandParser.NoBuildOption),
Expand Down
14 changes: 9 additions & 5 deletions src/Cli/dotnet/Commands/Test/SolutionAndProjectUtility.cs
Original file line number Diff line number Diff line change
Expand Up @@ -248,7 +248,7 @@ public static IEnumerable<ParallelizableTestModuleGroupWithSequentialInnerModule
string projectFullPath = project.GetPropertyValue(ProjectProperties.ProjectFullPath);

// TODO: Support --launch-profile and pass it here.
var launchSettings = TryGetLaunchProfileSettings(Path.GetDirectoryName(projectFullPath)!, project.GetPropertyValue(ProjectProperties.AppDesignerFolder), noLaunchProfile, profileName: null);
var launchSettings = TryGetLaunchProfileSettings(Path.GetDirectoryName(projectFullPath)!, Path.GetFileNameWithoutExtension(projectFullPath), project.GetPropertyValue(ProjectProperties.AppDesignerFolder), noLaunchProfile, profileName: null);

return new TestModule(runProperties, PathUtility.FixFilePath(projectFullPath), targetFramework, isTestingPlatformApplication, isTestProject, launchSettings, project.GetPropertyValue(ProjectProperties.TargetPath));

Expand All @@ -270,20 +270,24 @@ static RunProperties GetRunProperties(ProjectInstance project, ICollection<ILogg
}
}

private static ProjectLaunchSettingsModel? TryGetLaunchProfileSettings(string projectDirectory, string appDesignerFolder, bool noLaunchProfile, string? profileName)
private static ProjectLaunchSettingsModel? TryGetLaunchProfileSettings(string projectDirectory, string projectNameWithoutExtension, string appDesignerFolder, bool noLaunchProfile, string? profileName)
{
if (noLaunchProfile)
{
return null;
}

var launchSettingsPath = Path.Combine(projectDirectory, appDesignerFolder, "launchSettings.json");
var launchSettingsPath = CommonRunHelpers.GetPropertiesLaunchSettingsPath(projectDirectory, appDesignerFolder);
if (!File.Exists(launchSettingsPath))
{
return null;
launchSettingsPath = CommonRunHelpers.GetFlatLaunchSettingsPath(projectDirectory, projectNameWithoutExtension);
if (!File.Exists(launchSettingsPath))
{
return null;
}
}

var result = LaunchSettingsManager.TryApplyLaunchSettings(File.ReadAllText(launchSettingsPath), profileName);
var result = LaunchSettingsManager.TryApplyLaunchSettings(launchSettingsPath, profileName);
if (!result.Success)
{
Reporter.Error.WriteLine(string.Format(CliCommandStrings.RunCommandExceptionCouldNotApplyLaunchSettings, profileName, result.FailureReason).Bold().Red());
Expand Down
20 changes: 10 additions & 10 deletions src/Cli/dotnet/Commands/xlf/CliCommandStrings.cs.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

20 changes: 10 additions & 10 deletions src/Cli/dotnet/Commands/xlf/CliCommandStrings.de.xlf

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading
Loading