Skip to content

Commit 8f0b635

Browse files
authored
Implicitly set UserSecretsId for file-based apps (#50783)
1 parent 83fe7a5 commit 8f0b635

File tree

6 files changed

+231
-12
lines changed

6 files changed

+231
-12
lines changed

documentation/general/dotnet-run-file.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ Additionally, the implicit project file has the following customizations:
3232

3333
- `PublishAot` is set to `true`, see [`dotnet publish file.cs`](#other-commands) for more details.
3434

35+
- `UserSecretsId` is set to a hash of the entry point file path.
36+
3537
- [File-level directives](#directives-for-project-metadata) are applied.
3638

3739
- The following are virtual only, i.e., not preserved after [converting to a project](#grow-up):

src/Cli/dotnet/Commands/Project/Convert/ProjectConvertCommand.cs

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,16 @@ public override int Execute()
3131
var sourceFile = SourceFile.Load(file);
3232
var directives = VirtualProjectBuildingCommand.FindDirectives(sourceFile, reportAllErrors: !_force, DiagnosticBag.ThrowOnFirst());
3333

34+
// Create a project instance for evaluation.
35+
var projectCollection = new ProjectCollection();
36+
var command = new VirtualProjectBuildingCommand(
37+
entryPointFileFullPath: file,
38+
msbuildArgs: MSBuildArgs.FromOtherArgs([]))
39+
{
40+
Directives = directives,
41+
};
42+
var projectInstance = command.CreateProjectInstance(projectCollection);
43+
3444
// Find other items to copy over, e.g., default Content items like JSON files in Web apps.
3545
var includeItems = FindIncludedItems().ToList();
3646

@@ -61,7 +71,8 @@ public override int Execute()
6171
{
6272
using var stream = File.Open(projectFile, FileMode.Create, FileAccess.Write);
6373
using var writer = new StreamWriter(stream, Encoding.UTF8);
64-
VirtualProjectBuildingCommand.WriteProjectFile(writer, directives, isVirtualProject: false);
74+
VirtualProjectBuildingCommand.WriteProjectFile(writer, directives, isVirtualProject: false,
75+
userSecretsId: DetermineUserSecretsId());
6576
}
6677

6778
// Copy or move over included items.
@@ -112,14 +123,6 @@ void CopyFile(string source, string target)
112123
IEnumerable<(string FullPath, string RelativePath)> FindIncludedItems()
113124
{
114125
string entryPointFileDirectory = PathUtility.EnsureTrailingSlash(Path.GetDirectoryName(file)!);
115-
var projectCollection = new ProjectCollection();
116-
var command = new VirtualProjectBuildingCommand(
117-
entryPointFileFullPath: file,
118-
msbuildArgs: MSBuildArgs.FromOtherArgs([]))
119-
{
120-
Directives = directives,
121-
};
122-
var projectInstance = command.CreateProjectInstance(projectCollection);
123126

124127
// Include only items we know are files.
125128
string[] itemTypes = ["Content", "None", "Compile", "EmbeddedResource"];
@@ -151,6 +154,13 @@ void CopyFile(string source, string target)
151154
yield return (FullPath: itemFullPath, RelativePath: itemRelativePath);
152155
}
153156
}
157+
158+
string? DetermineUserSecretsId()
159+
{
160+
var implicitValue = projectInstance.GetPropertyValue("_ImplicitFileBasedProgramUserSecretsId");
161+
var actualValue = projectInstance.GetPropertyValue("UserSecretsId");
162+
return implicitValue == actualValue ? actualValue : null;
163+
}
154164
}
155165

156166
private string DetermineOutputDirectory(string file)

src/Cli/dotnet/Commands/Run/VirtualProjectBuildingCommand.cs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1138,7 +1138,8 @@ public static void WriteProjectFile(
11381138
bool isVirtualProject,
11391139
string? targetFilePath = null,
11401140
string? artifactsPath = null,
1141-
bool includeRuntimeConfigInformation = true)
1141+
bool includeRuntimeConfigInformation = true,
1142+
string? userSecretsId = null)
11421143
{
11431144
int processedDirectives = 0;
11441145

@@ -1255,6 +1256,13 @@ public static void WriteProjectFile(
12551256
}
12561257
}
12571258

1259+
if (userSecretsId != null && !customPropertyNames.Contains("UserSecretsId"))
1260+
{
1261+
writer.WriteLine($"""
1262+
<UserSecretsId>{EscapeValue(userSecretsId)}</UserSecretsId>
1263+
""");
1264+
}
1265+
12581266
// Write virtual-only properties.
12591267
if (isVirtualProject)
12601268
{

src/Tasks/Microsoft.NET.Build.Tasks/targets/Microsoft.NET.Sdk.props

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,10 @@ Copyright (c) .NET Foundation. All rights reserved.
129129

130130
<!-- Uncomment this once https://github.com/Microsoft/visualfsharp/issues/3207 gets fixed -->
131131
<!-- <WarningsAsErrors>$(WarningsAsErrors);NU1605</WarningsAsErrors> -->
132+
133+
<!-- Implicitly set UserSecretsId for file-based apps. -->
134+
<_ImplicitFileBasedProgramUserSecretsId Condition="'$(FileBasedProgram)' == 'true'">$(MSBuildProjectName)-$([MSBuild]::StableStringHash($(MSBuildProjectFullPath.ToLowerInvariant()), 'Sha256'))</_ImplicitFileBasedProgramUserSecretsId>
135+
<UserSecretsId Condition="'$(FileBasedProgram)' == 'true' and '$(UserSecretsId)' == ''">$(_ImplicitFileBasedProgramUserSecretsId)</UserSecretsId>
132136
</PropertyGroup>
133137

134138
<PropertyGroup>

test/dotnet.Tests/CommandTests/Project/Convert/DotnetProjectConvertTests.cs

Lines changed: 133 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using System.Security;
5+
using System.Text.RegularExpressions;
46
using Microsoft.CodeAnalysis.Text;
57
using Microsoft.DotNet.Cli.Commands;
68
using Microsoft.DotNet.Cli.Commands.Run;
@@ -56,9 +58,11 @@ public void SameAsTemplate()
5658
var dotnetProjectConvertProjectText = File.ReadAllText(dotnetProjectConvertProject);
5759
var dotnetNewConsoleProjectText = File.ReadAllText(dotnetNewConsoleProject);
5860

59-
// There are some differences: we add PublishAot=true.
61+
// There are some differences: we add PublishAot=true and UserSecretsId.
6062
var patchedDotnetProjectConvertProjectText = dotnetProjectConvertProjectText
6163
.Replace(" <PublishAot>true</PublishAot>" + Environment.NewLine, string.Empty);
64+
patchedDotnetProjectConvertProjectText = Regex.Replace(patchedDotnetProjectConvertProjectText,
65+
""" <UserSecretsId>[^<]*<\/UserSecretsId>""" + Environment.NewLine, string.Empty);
6266

6367
patchedDotnetProjectConvertProjectText.Should().Be(dotnetNewConsoleProjectText)
6468
.And.StartWith("""<Project Sdk="Microsoft.NET.Sdk">""");
@@ -648,7 +652,7 @@ public void ProcessingSucceeds()
648652
.Should().Be("Console.WriteLine();");
649653

650654
File.ReadAllText(Path.Join(testInstance.Path, "Program", "Program.csproj"))
651-
.Should().Be($"""
655+
.Should().Match($"""
652656
<Project Sdk="Microsoft.NET.Sdk">
653657
654658
<PropertyGroup>
@@ -657,6 +661,7 @@ public void ProcessingSucceeds()
657661
<ImplicitUsings>enable</ImplicitUsings>
658662
<Nullable>enable</Nullable>
659663
<PublishAot>true</PublishAot>
664+
<UserSecretsId>Program-*</UserSecretsId>
660665
</PropertyGroup>
661666
662667
<ItemGroup>
@@ -668,6 +673,132 @@ public void ProcessingSucceeds()
668673
""");
669674
}
670675

676+
[Theory, CombinatorialData]
677+
public void UserSecretsId_Overridden_ViaDirective(bool hasDirectiveBuildProps)
678+
{
679+
var testInstance = _testAssetsManager.CreateTestDirectory();
680+
File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), """
681+
#:property UserSecretsId=MyIdFromDirective
682+
Console.WriteLine();
683+
""");
684+
685+
if (hasDirectiveBuildProps)
686+
{
687+
File.WriteAllText(Path.Join(testInstance.Path, "Directory.Build.props"), """
688+
<Project>
689+
<PropertyGroup>
690+
<UserSecretsId>MyIdFromDirBuildProps</UserSecretsId>
691+
</PropertyGroup>
692+
</Project>
693+
""");
694+
}
695+
696+
new DotnetCommand(Log, "project", "convert", "Program.cs")
697+
.WithWorkingDirectory(testInstance.Path)
698+
.Execute()
699+
.Should().Pass();
700+
701+
File.ReadAllText(Path.Join(testInstance.Path, "Program", "Program.csproj"))
702+
.Should().Be($"""
703+
<Project Sdk="Microsoft.NET.Sdk">
704+
705+
<PropertyGroup>
706+
<OutputType>Exe</OutputType>
707+
<TargetFramework>{ToolsetInfo.CurrentTargetFramework}</TargetFramework>
708+
<ImplicitUsings>enable</ImplicitUsings>
709+
<Nullable>enable</Nullable>
710+
<PublishAot>true</PublishAot>
711+
<UserSecretsId>MyIdFromDirective</UserSecretsId>
712+
</PropertyGroup>
713+
714+
</Project>
715+
716+
""");
717+
}
718+
719+
[Fact]
720+
public void UserSecretsId_Overridden_ViaDirectoryBuildProps()
721+
{
722+
var testInstance = _testAssetsManager.CreateTestDirectory();
723+
724+
File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), """
725+
Console.WriteLine();
726+
""");
727+
File.WriteAllText(Path.Join(testInstance.Path, "Directory.Build.props"), """
728+
<Project>
729+
<PropertyGroup>
730+
<UserSecretsId>MyIdFromDirBuildProps</UserSecretsId>
731+
</PropertyGroup>
732+
</Project>
733+
""");
734+
735+
new DotnetCommand(Log, "project", "convert", "Program.cs")
736+
.WithWorkingDirectory(testInstance.Path)
737+
.Execute()
738+
.Should().Pass();
739+
740+
File.ReadAllText(Path.Join(testInstance.Path, "Program", "Program.csproj"))
741+
.Should().Be($"""
742+
<Project Sdk="Microsoft.NET.Sdk">
743+
744+
<PropertyGroup>
745+
<OutputType>Exe</OutputType>
746+
<TargetFramework>{ToolsetInfo.CurrentTargetFramework}</TargetFramework>
747+
<ImplicitUsings>enable</ImplicitUsings>
748+
<Nullable>enable</Nullable>
749+
<PublishAot>true</PublishAot>
750+
</PropertyGroup>
751+
752+
</Project>
753+
754+
""");
755+
}
756+
757+
[Theory, CombinatorialData]
758+
public void UserSecretsId_Overridden_SameAsImplicit(bool hasDirective, bool hasDirectiveBuildProps)
759+
{
760+
const string implicitValue = "$(MSBuildProjectName)-$([MSBuild]::StableStringHash($(MSBuildProjectFullPath.ToLowerInvariant()), 'Sha256'))";
761+
762+
var testInstance = _testAssetsManager.CreateTestDirectory();
763+
File.WriteAllText(Path.Join(testInstance.Path, "Program.cs"), $"""
764+
{(hasDirective ? $"#:property UserSecretsId={implicitValue}" : "")}
765+
Console.WriteLine();
766+
""");
767+
768+
if (hasDirectiveBuildProps)
769+
{
770+
File.WriteAllText(Path.Join(testInstance.Path, "Directory.Build.props"), $"""
771+
<Project>
772+
<PropertyGroup>
773+
<UserSecretsId>{SecurityElement.Escape(implicitValue)}</UserSecretsId>
774+
</PropertyGroup>
775+
</Project>
776+
""");
777+
}
778+
779+
new DotnetCommand(Log, "project", "convert", "Program.cs")
780+
.WithWorkingDirectory(testInstance.Path)
781+
.Execute()
782+
.Should().Pass();
783+
784+
File.ReadAllText(Path.Join(testInstance.Path, "Program", "Program.csproj"))
785+
.Should().Match($"""
786+
<Project Sdk="Microsoft.NET.Sdk">
787+
788+
<PropertyGroup>
789+
<OutputType>Exe</OutputType>
790+
<TargetFramework>{ToolsetInfo.CurrentTargetFramework}</TargetFramework>
791+
<ImplicitUsings>enable</ImplicitUsings>
792+
<Nullable>enable</Nullable>
793+
<PublishAot>true</PublishAot>
794+
<UserSecretsId>{(hasDirective ? SecurityElement.Escape(implicitValue) : "Program-*")}</UserSecretsId>
795+
</PropertyGroup>
796+
797+
</Project>
798+
799+
""");
800+
}
801+
671802
[Fact]
672803
public void Directives()
673804
{

test/dotnet.Tests/CommandTests/Run/RunFileTests.cs

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2191,6 +2191,70 @@ public void ProjectReference_Errors()
21912191
string.Format(CliStrings.MoreThanOneProjectInDirectory, Path.Join(testInstance.Path, "dir/"))));
21922192
}
21932193

2194+
[Theory] // https://github.com/dotnet/aspnetcore/issues/63440
2195+
[InlineData(true, null)]
2196+
[InlineData(false, null, Skip = "Needs https://github.com/dotnet/aspnetcore/pull/63496")]
2197+
[InlineData(true, "test-id")]
2198+
[InlineData(false, "test-id", Skip = "Needs https://github.com/dotnet/aspnetcore/pull/63496")]
2199+
public void UserSecrets(bool useIdArg, string? userSecretsId)
2200+
{
2201+
var testInstance = _testAssetsManager.CreateTestDirectory();
2202+
2203+
string code = $"""
2204+
#:package Microsoft.Extensions.Configuration.UserSecrets@{CSharpCompilerCommand.RuntimeVersion}
2205+
{(userSecretsId is null ? "" : $"#:property UserSecretsId={userSecretsId}")}
2206+
2207+
using Microsoft.Extensions.Configuration;
2208+
2209+
IConfigurationRoot config = new ConfigurationBuilder()
2210+
.AddUserSecrets<Program>()
2211+
.Build();
2212+
2213+
Console.WriteLine("v1");
2214+
Console.WriteLine(config.GetDebugView());
2215+
""";
2216+
2217+
var programPath = Path.Join(testInstance.Path, "Program.cs");
2218+
File.WriteAllText(programPath, code);
2219+
2220+
if (useIdArg)
2221+
{
2222+
if (userSecretsId == null)
2223+
{
2224+
var result = new DotnetCommand(Log, "build", "-getProperty:UserSecretsId", "Program.cs")
2225+
.WithWorkingDirectory(testInstance.Path)
2226+
.Execute();
2227+
result.Should().Pass();
2228+
userSecretsId = result.StdOut!.Trim();
2229+
}
2230+
2231+
new DotnetCommand(Log, "user-secrets", "set", "MySecret", "MyValue", "--id", userSecretsId)
2232+
.WithWorkingDirectory(testInstance.Path)
2233+
.Execute()
2234+
.Should().Pass();
2235+
}
2236+
else
2237+
{
2238+
new DotnetCommand(Log, "user-secrets", "set", "MySecret", "MyValue", "--file", "Program.cs")
2239+
.WithWorkingDirectory(testInstance.Path)
2240+
.Execute()
2241+
.Should().Pass();
2242+
}
2243+
2244+
Build(testInstance, BuildLevel.All, expectedOutput: """
2245+
v1
2246+
MySecret=MyValue (JsonConfigurationProvider for 'secrets.json' (Optional))
2247+
""");
2248+
2249+
code = code.Replace("v1", "v2");
2250+
File.WriteAllText(programPath, code);
2251+
2252+
Build(testInstance, BuildLevel.Csc, expectedOutput: """
2253+
v2
2254+
MySecret=MyValue (JsonConfigurationProvider for 'secrets.json' (Optional))
2255+
""");
2256+
}
2257+
21942258
/// <summary>
21952259
/// Verifies that msbuild-based runs use CSC args equivalent to csc-only runs.
21962260
/// Can regenerate CSC arguments template in <see cref="CSharpCompilerCommand"/>.

0 commit comments

Comments
 (0)