Skip to content
Merged
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
2 changes: 1 addition & 1 deletion src/Tools/Shared/SecretsHelpers/MsBuildProjectFinder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ public string FindMsBuildProject(string project)

if (!File.Exists(projectPath))
{
throw new FileNotFoundException(SecretsHelpersResources.FormatError_ProjectPath_NotFound(projectPath));
throw new FileNotFoundException(SecretsHelpersResources.FormatError_File_NotFound(projectPath));
}

return projectPath;
Expand Down
6 changes: 3 additions & 3 deletions src/Tools/Shared/SecretsHelpers/ProjectIdResolver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -59,9 +59,9 @@ public string Resolve(string project, string configuration)
UseShellExecute = false,
ArgumentList =
{
"msbuild",
"build",
Copy link
Member

Choose a reason for hiding this comment

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

Are there any gotchas we should be on the lookout for as we move from msbuild to build for this? Also, I assume this exists because there are file-based app behaviors that are not available in msbuild?

Copy link
Member

Choose a reason for hiding this comment

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

msbuild is the 'raw' interface. it doesn't do multi-processing by default, doesn't do terminal logger (though you're not going to see that one), it doesn't do implicit restores, etc. build will do all of those (and it has the nice CLI args for some of the common properties) in addition to supporting the file-based apps. I'd say in general you should expect more churn in the behavior of build, but we do try to keep the contract pretty clean and consistent.

projectFile,
"/nologo",
"--no-restore",
"/t:_ExtractUserSecretsMetadata", // defined in SecretManager.targets
"/p:_UserSecretsMetadataFile=" + outputFile,
"/p:Configuration=" + configuration,
Expand All @@ -72,7 +72,7 @@ public string Resolve(string project, string configuration)
};

#if DEBUG
_reporter.Verbose($"Invoking '{psi.FileName} {psi.Arguments}'");
_reporter.Verbose($"Invoking '{psi.FileName} {string.Join(' ', psi.ArgumentList)}'");
#endif

using var process = new Process()
Expand Down
8 changes: 4 additions & 4 deletions src/Tools/Shared/SecretsHelpers/SecretsHelpersResources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -124,16 +124,16 @@
<value>Multiple MSBuild project files found in '{projectPath}'. Specify which to use with the --project option.</value>
</data>
<data name="Error_NoProjectsFound" xml:space="preserve">
<value>Could not find a MSBuild project file in '{projectPath}'. Specify which project to use with the --project option.</value>
<value>Could not find a MSBuild project file in '{projectPath}'. Specify which project to use with the --project option. Use --file option for file-based apps.</value>
</data>
<data name="Error_ProjectFailedToLoad" xml:space="preserve">
<value>Could not load the MSBuild project '{project}'.</value>
</data>
<data name="Error_ProjectMissingId" xml:space="preserve">
<value>Could not find the global property 'UserSecretsId' in MSBuild project '{project}'. Ensure this property is set in the project or use the '--id' command line option.</value>
</data>
<data name="Error_ProjectPath_NotFound" xml:space="preserve">
<value>The project file '{0}' does not exist.</value>
<data name="Error_File_NotFound" xml:space="preserve">
<value>The file '{0}' does not exist.</value>
</data>
<data name="Message_ProjectAlreadyInitialized" xml:space="preserve">
<value>The MSBuild project '{project}' has already been initialized with a UserSecretsId.</value>
Expand All @@ -144,4 +144,4 @@
<data name="Message_SetUserSecretsIdForProject" xml:space="preserve">
<value>Set UserSecretsId to '{userSecretsId}' for MSBuild project '{project}'.</value>
</data>
</root>
</root>
12 changes: 11 additions & 1 deletion src/Tools/dotnet-user-secrets/src/CommandLineOptions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ public class CommandLineOptions
public bool IsHelp { get; private set; }
public bool IsVerbose { get; private set; }
public string Project { get; private set; }
public string File { get; private set; }

public static CommandLineOptions Parse(string[] args, IConsole console)
{
Expand All @@ -36,6 +37,9 @@ public static CommandLineOptions Parse(string[] args, IConsole console)
var optionProject = app.Option("-p|--project <PROJECT>", "Path to project. Defaults to searching the current directory.",
CommandOptionType.SingleValue, inherited: true);

var optionFile = app.Option("-f|--file <FILE>", "Path to file-based app.",
CommandOptionType.SingleValue, inherited: true);

var optionConfig = app.Option("-c|--configuration <CONFIGURATION>", "The project configuration to use. Defaults to 'Debug'.",
CommandOptionType.SingleValue, inherited: true);

Expand All @@ -50,7 +54,7 @@ public static CommandLineOptions Parse(string[] args, IConsole console)
app.Command("remove", c => RemoveCommand.Configure(c, options));
app.Command("list", c => ListCommand.Configure(c, options));
app.Command("clear", c => ClearCommand.Configure(c, options));
app.Command("init", c => InitCommandFactory.Configure(c, options));
app.Command("init", c => InitCommandFactory.Configure(c, options, optionFile));

// Show help information if no subcommand/option was specified.
app.OnExecute(() => app.ShowHelp());
Expand All @@ -66,6 +70,12 @@ public static CommandLineOptions Parse(string[] args, IConsole console)
options.IsHelp = app.IsShowingInformation;
options.IsVerbose = optionVerbose.HasValue();
options.Project = optionProject.Value();
options.File = optionFile.Value();

if (options.File != null && options.Project != null)
{
throw new CommandParsingException(app, Resources.Error_ProjectAndFileOptions);
}

return options;
}
Expand Down
7 changes: 6 additions & 1 deletion src/Tools/dotnet-user-secrets/src/Internal/InitCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,13 +16,18 @@ public class InitCommandFactory : ICommand
{
public CommandLineOptions Options { get; }

internal static void Configure(CommandLineApplication command, CommandLineOptions options)
internal static void Configure(CommandLineApplication command, CommandLineOptions options, CommandOption optionFile)
{
command.Description = "Set a user secrets ID to enable secret storage";
command.HelpOption();

command.OnExecute(() =>
{
if (optionFile.HasValue())
{
throw new CommandParsingException(command, Resources.Error_InitNotSupportedForFileBasedApps);
}

options.Command = new InitCommandFactory(options);
});
}
Expand Down
2 changes: 1 addition & 1 deletion src/Tools/dotnet-user-secrets/src/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,6 @@ internal string ResolveId(CommandLineOptions options, IReporter reporter)
}

var resolver = new ProjectIdResolver(reporter, _workingDirectory);
return resolver.Resolve(options.Project, options.Configuration);
return resolver.Resolve(options.Project ?? options.File, options.Configuration);
}
}
16 changes: 8 additions & 8 deletions src/Tools/dotnet-user-secrets/src/Resources.resx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Expand Down Expand Up @@ -133,15 +133,9 @@ Use the '--help' flag to see info.</value>
<data name="Error_No_Secrets_Found" xml:space="preserve">
<value>No secrets configured for this application.</value>
</data>
<data name="Error_NoProjectsFound" xml:space="preserve">
<value>Could not find a MSBuild project file in '{projectPath}'. Specify which project to use with the --project option.</value>
</data>
<data name="Error_ProjectMissingId" xml:space="preserve">
<value>Could not find the global property 'UserSecretsId' in MSBuild project '{project}'. Ensure this property is set in the project or use the '--id' command line option.</value>
</data>
<data name="Error_ProjectPath_NotFound" xml:space="preserve">
<value>The project file '{path}' does not exist.</value>
</data>
<data name="Error_ProjectFailedToLoad" xml:space="preserve">
<value>Could not load the MSBuild project '{project}'.</value>
</data>
Expand Down Expand Up @@ -169,4 +163,10 @@ Use the '--help' flag to see info.</value>
<data name="Message_SetUserSecretsIdForProject" xml:space="preserve">
<value>Set UserSecretsId to '{userSecretsId}' for MSBuild project '{project}'.</value>
</data>
</root>
<data name="Error_ProjectAndFileOptions" xml:space="preserve">
<value>Cannot use both --file and --project options together.</value>
</data>
<data name="Error_InitNotSupportedForFileBasedApps" xml:space="preserve">
<value>Init command is currently not supported for file-based apps. Please add '#:property UserSecretsId=...' manually.</value>
Copy link
Member

Choose a reason for hiding this comment

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

All this command does is generate a guid and stick it in the UserSecretsId in the user's project? Is that right?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes.

</data>
</root>
101 changes: 64 additions & 37 deletions src/Tools/dotnet-user-secrets/test/SecretManagerTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
using System.IO;
using System.Text;
using Microsoft.AspNetCore.InternalTesting;
using Microsoft.AspNetCore.Tools;
using Microsoft.Extensions.Configuration.UserSecrets;
using Microsoft.Extensions.Configuration.UserSecrets.Tests;
using Microsoft.Extensions.Tools.Internal;
Expand Down Expand Up @@ -64,7 +65,27 @@ public void Error_Project_DoesNotExist()
var secretManager = CreateProgram();

secretManager.RunInternal("list", "--project", projectPath);
Assert.Contains(Resources.FormatError_ProjectPath_NotFound(projectPath), _console.GetOutput());
Assert.Contains(SecretsHelpersResources.FormatError_File_NotFound(projectPath), _console.GetOutput());
}

[Fact]
public void Error_ProjectAndFileOptions()
{
var projectPath = Path.Combine(_fixture.GetTempSecretProject(), "does_not_exist", "TestProject.csproj");
var secretManager = CreateProgram();

secretManager.RunInternal("list", "--project", projectPath, "--file", projectPath);
Assert.Contains(Resources.Error_ProjectAndFileOptions, _console.GetOutput());
}

[Fact]
public void Error_InitFile()
{
var dir = _fixture.CreateFileBasedApp(null);
var secretManager = CreateProgram();

secretManager.RunInternal("init", "--file", Path.Combine(dir, "app.cs"));
Assert.Contains(Resources.Error_InitNotSupportedForFileBasedApps, _console.GetOutput());
}

[Fact]
Expand All @@ -81,9 +102,10 @@ public void SupportsRelativePaths()
}

[Theory]
[InlineData(true)]
[InlineData(false)]
public void SetSecrets(bool fromCurrentDirectory)
[InlineData(false, true)]
[InlineData(false, false)]
[InlineData(true, false)]
public void SetSecrets(bool fromCurrentDirectory, bool fileBasedApp)
Copy link
Member

Choose a reason for hiding this comment

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

Should we error if you try to set secrets on a file-based app from a directory that the file is not in? Based on the options here, it seems like calling the below would be invalid?

$ pwd
/user/some/dir
$ dotnet user-secrets set "foo" "bar" --file /user/some/otherdir/file.cs

Copy link
Member Author

@jjonescz jjonescz Sep 4, 2025

Choose a reason for hiding this comment

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

That is currently valid, the test verifies that (fromCurrentDirectory: false, fileBasedApp: true). What's invalid is when no --project and no --file is specified - that works if you are in the project's directory, but not for file-based apps (there could be multiple file-based apps in the directory).

Copy link
Member

Choose a reason for hiding this comment

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

What's invalid is when no --project and no --file is specified - that works if you are in the project's directory, but not for file-based apps (there could be multiple file-based apps in the directory).

Is there test coverage for this case? No project file in the current directory, but there are file-based app(s) in the current directory, and no --project and no --file arg is passed?

Copy link
Member Author

Choose a reason for hiding this comment

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

I will add a test for this, thanks.

{
var secrets = new KeyValuePair<string, string>[]
{
Expand All @@ -95,18 +117,22 @@ public void SetSecrets(bool fromCurrentDirectory)
new KeyValuePair<string, string>("--twoDashedKey", "--twoDashedValue")
};

var projectPath = _fixture.GetTempSecretProject();
var projectPath = fileBasedApp
? _fixture.GetTempFileBasedApp(out _)
: _fixture.GetTempSecretProject();
var dir = fromCurrentDirectory
? projectPath
: Path.GetTempPath();
ReadOnlySpan<string> pathArgs = fromCurrentDirectory
? []
: (fileBasedApp
? ["-f", Path.Join(projectPath, "app.cs")]
: ["-p", projectPath]);
var secretManager = new Program(_console, dir);

foreach (var secret in secrets)
{
var parameters = fromCurrentDirectory ?
new string[] { "set", secret.Key, secret.Value, "--verbose" } :
new string[] { "set", secret.Key, secret.Value, "-p", projectPath, "--verbose" };
secretManager.RunInternal(parameters);
secretManager.RunInternal(["set", secret.Key, secret.Value, .. pathArgs, "--verbose"]);
}

foreach (var keyValue in secrets)
Expand All @@ -117,10 +143,7 @@ public void SetSecrets(bool fromCurrentDirectory)
}

_console.ClearOutput();
var args = fromCurrentDirectory
? new string[] { "list", "--verbose" }
: new string[] { "list", "-p", projectPath, "--verbose" };
secretManager.RunInternal(args);
secretManager.RunInternal(["list", .. pathArgs, "--verbose"]);
foreach (var keyValue in secrets)
{
Assert.Contains(
Expand All @@ -132,21 +155,24 @@ public void SetSecrets(bool fromCurrentDirectory)
_console.ClearOutput();
foreach (var secret in secrets)
{
var parameters = fromCurrentDirectory ?
new string[] { "remove", secret.Key, "--verbose" } :
new string[] { "remove", secret.Key, "-p", projectPath, "--verbose" };
secretManager.RunInternal(parameters);
secretManager.RunInternal(["remove", secret.Key, .. pathArgs, "--verbose"]);
}

// Verify secrets are removed.
_console.ClearOutput();
args = fromCurrentDirectory
? new string[] { "list", "--verbose" }
: new string[] { "list", "-p", projectPath, "--verbose" };
secretManager.RunInternal(args);
secretManager.RunInternal(["list", .. pathArgs, "--verbose"]);
Assert.Contains(Resources.Error_No_Secrets_Found, _console.GetOutput());
}

[Fact]
public void SetSecrets_FileBasedAppInCurrentDirectory()
{
var directoryPath = _fixture.GetTempFileBasedApp(out _);
var secretManager = new Program(_console, directoryPath);
secretManager.RunInternal("set", "key1", "value1", "--verbose");
Assert.Contains(SecretsHelpersResources.FormatError_NoProjectsFound(directoryPath), _console.GetOutput());
}

[Fact]
public void SetSecret_Update_Existing_Secret()
{
Expand Down Expand Up @@ -268,16 +294,25 @@ public void List_Empty_Secrets_File()
}

[Theory]
[InlineData(true)]
[InlineData(false)]
public void Clear_Secrets(bool fromCurrentDirectory)
[InlineData(false, true)]
[InlineData(false, false)]
[InlineData(true, false)]
public void Clear_Secrets(bool fromCurrentDirectory, bool fileBasedApp)
{
var projectPath = _fixture.GetTempSecretProject();
var projectPath = fileBasedApp
? _fixture.GetTempFileBasedApp(out _)
: _fixture.GetTempSecretProject();

var dir = fromCurrentDirectory
? projectPath
: Path.GetTempPath();

ReadOnlySpan<string> pathArgs = fromCurrentDirectory
? []
: (fileBasedApp
? ["-f", Path.Join(projectPath, "app.cs")]
: ["-p", projectPath]);

var secretManager = new Program(_console, dir);

var secrets = new KeyValuePair<string, string>[]
Expand All @@ -290,10 +325,7 @@ public void Clear_Secrets(bool fromCurrentDirectory)

foreach (var secret in secrets)
{
var parameters = fromCurrentDirectory ?
new string[] { "set", secret.Key, secret.Value, "--verbose" } :
new string[] { "set", secret.Key, secret.Value, "-p", projectPath, "--verbose" };
secretManager.RunInternal(parameters);
secretManager.RunInternal(["set", secret.Key, secret.Value, .. pathArgs, "--verbose"]);
}

foreach (var keyValue in secrets)
Expand All @@ -305,10 +337,7 @@ public void Clear_Secrets(bool fromCurrentDirectory)

// Verify secrets are persisted.
_console.ClearOutput();
var args = fromCurrentDirectory ?
new string[] { "list", "--verbose" } :
new string[] { "list", "-p", projectPath, "--verbose" };
secretManager.RunInternal(args);
secretManager.RunInternal(["list", .. pathArgs, "--verbose"]);
foreach (var keyValue in secrets)
{
Assert.Contains(
Expand All @@ -318,11 +347,9 @@ public void Clear_Secrets(bool fromCurrentDirectory)

// Clear secrets.
_console.ClearOutput();
args = fromCurrentDirectory ? new string[] { "clear", "--verbose" } : new string[] { "clear", "-p", projectPath, "--verbose" };
secretManager.RunInternal(args);
secretManager.RunInternal(["clear", .. pathArgs, "--verbose"]);

args = fromCurrentDirectory ? new string[] { "list", "--verbose" } : new string[] { "list", "-p", projectPath, "--verbose" };
secretManager.RunInternal(args);
secretManager.RunInternal(["list", .. pathArgs, "--verbose"]);
Assert.Contains(Resources.Error_No_Secrets_Found, _console.GetOutput());
}

Expand Down
Loading
Loading