From 4b60beda6359c4d39126e60e83cb2da173fcb445 Mon Sep 17 00:00:00 2001 From: SimaTian Date: Mon, 23 Feb 2026 17:52:50 +0100 Subject: [PATCH 1/3] Refactor CliFolderPathCalculatorCore to support env var injection Make CliFolderPathCalculatorCore non-static with a Func constructor parameter for environment variable reads. This enables MSBuild tasks to route env var reads through TaskEnvironment instead of using process-global Environment.GetEnvironmentVariable. - Default constructor uses Environment.GetEnvironmentVariable (no behavior change) - New constructor accepts a custom env var reader delegate - Static convenience methods (GetDotnetUserProfileFolderPathFromSystemEnvironment, GetDotnetHomePathFromSystemEnvironment) for callers that use process env vars - All existing callsites updated to use static convenience methods MSBuild task migration branches (merge-group-11) will rebase onto this and use the instance constructor with TaskEnvironment.GetEnvironmentVariable. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../CliFolderPathCalculator.cs | 2 +- src/Common/CliFolderPathCalculatorCore.cs | 37 +++++++++++++++++-- src/RazorSdk/Tool/ServerCommand.cs | 2 +- .../MSBuildSdkResolver.cs | 2 +- .../WorkloadSdkResolver.cs | 2 +- .../ProcessFrameworkReferences.cs | 4 +- .../ShowMissingWorkloads.cs | 2 +- 7 files changed, 40 insertions(+), 11 deletions(-) diff --git a/src/Cli/Microsoft.DotNet.Configurer/CliFolderPathCalculator.cs b/src/Cli/Microsoft.DotNet.Configurer/CliFolderPathCalculator.cs index 705ee86a2ab3..1a38fec3b8ee 100644 --- a/src/Cli/Microsoft.DotNet.Configurer/CliFolderPathCalculator.cs +++ b/src/Cli/Microsoft.DotNet.Configurer/CliFolderPathCalculator.cs @@ -46,7 +46,7 @@ public static string DotnetHomePath { get { - return CliFolderPathCalculatorCore.GetDotnetHomePath() + return CliFolderPathCalculatorCore.GetDotnetHomePathFromSystemEnvironment() ?? throw new ConfigurationException( string.Format( LocalizableStrings.FailedToDetermineUserHomeDirectory, diff --git a/src/Common/CliFolderPathCalculatorCore.cs b/src/Common/CliFolderPathCalculatorCore.cs index ccdac9fd0282..e6c3aeb144fd 100644 --- a/src/Common/CliFolderPathCalculatorCore.cs +++ b/src/Common/CliFolderPathCalculatorCore.cs @@ -3,12 +3,31 @@ namespace Microsoft.DotNet.Configurer { - static class CliFolderPathCalculatorCore + class CliFolderPathCalculatorCore { public const string DotnetHomeVariableName = "DOTNET_CLI_HOME"; public const string DotnetProfileDirectoryName = ".dotnet"; - public static string? GetDotnetUserProfileFolderPath() + private readonly Func _getEnvironmentVariable; + + /// + /// Creates an instance that reads environment variables from the process environment. + /// + public CliFolderPathCalculatorCore() + : this(Environment.GetEnvironmentVariable) + { + } + + /// + /// Creates an instance that reads environment variables via the supplied delegate. + /// Use this from MSBuild tasks to route reads through TaskEnvironment. + /// + public CliFolderPathCalculatorCore(Func getEnvironmentVariable) + { + _getEnvironmentVariable = getEnvironmentVariable ?? throw new ArgumentNullException(nameof(getEnvironmentVariable)); + } + + public string? GetDotnetUserProfileFolderPath() { string? homePath = GetDotnetHomePath(); if (homePath is null) @@ -19,9 +38,9 @@ static class CliFolderPathCalculatorCore return Path.Combine(homePath, DotnetProfileDirectoryName); } - public static string? GetDotnetHomePath() + public string? GetDotnetHomePath() { - var home = Environment.GetEnvironmentVariable(DotnetHomeVariableName); + var home = _getEnvironmentVariable(DotnetHomeVariableName); if (string.IsNullOrEmpty(home)) { home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile); @@ -33,5 +52,15 @@ static class CliFolderPathCalculatorCore return home; } + + // Static convenience methods for callers that use process environment variables. + + private static readonly CliFolderPathCalculatorCore s_default = new(); + + public static string? GetDotnetUserProfileFolderPathFromSystemEnvironment() + => s_default.GetDotnetUserProfileFolderPath(); + + public static string? GetDotnetHomePathFromSystemEnvironment() + => s_default.GetDotnetHomePath(); } } diff --git a/src/RazorSdk/Tool/ServerCommand.cs b/src/RazorSdk/Tool/ServerCommand.cs index 6b091b014ac6..85fcc7870554 100644 --- a/src/RazorSdk/Tool/ServerCommand.cs +++ b/src/RazorSdk/Tool/ServerCommand.cs @@ -169,7 +169,7 @@ internal static string GetPidFilePath() var path = Environment.GetEnvironmentVariable("DOTNET_BUILD_PIDFILE_DIRECTORY"); if (string.IsNullOrEmpty(path)) { - var homePath = CliFolderPathCalculatorCore.GetDotnetHomePath(); + var homePath = CliFolderPathCalculatorCore.GetDotnetHomePathFromSystemEnvironment(); if (homePath is null) { // Couldn't locate the user profile directory. Bail. diff --git a/src/Resolvers/Microsoft.DotNet.MSBuildSdkResolver/MSBuildSdkResolver.cs b/src/Resolvers/Microsoft.DotNet.MSBuildSdkResolver/MSBuildSdkResolver.cs index ae38c8e2f61c..b7223cdf9f6d 100644 --- a/src/Resolvers/Microsoft.DotNet.MSBuildSdkResolver/MSBuildSdkResolver.cs +++ b/src/Resolvers/Microsoft.DotNet.MSBuildSdkResolver/MSBuildSdkResolver.cs @@ -308,7 +308,7 @@ private sealed class CachedState }; // First check if requested SDK resolves to a workload SDK pack - string? userProfileDir = CliFolderPathCalculatorCore.GetDotnetUserProfileFolderPath(); + string? userProfileDir = CliFolderPathCalculatorCore.GetDotnetUserProfileFolderPathFromSystemEnvironment(); ResolutionResult? workloadResult = null; if (dotnetRoot is not null && netcoreSdkVersion is not null) { diff --git a/src/Resolvers/Microsoft.NET.Sdk.WorkloadMSBuildSdkResolver/WorkloadSdkResolver.cs b/src/Resolvers/Microsoft.NET.Sdk.WorkloadMSBuildSdkResolver/WorkloadSdkResolver.cs index c2b483303823..bb676f155f8c 100644 --- a/src/Resolvers/Microsoft.NET.Sdk.WorkloadMSBuildSdkResolver/WorkloadSdkResolver.cs +++ b/src/Resolvers/Microsoft.NET.Sdk.WorkloadMSBuildSdkResolver/WorkloadSdkResolver.cs @@ -62,7 +62,7 @@ private class CachedState resolverContext.State = cachedState; } - string? userProfileDir = CliFolderPathCalculatorCore.GetDotnetUserProfileFolderPath(); + string? userProfileDir = CliFolderPathCalculatorCore.GetDotnetUserProfileFolderPathFromSystemEnvironment(); ResolutionResult? result = null; if (cachedState.DotnetRootPath is not null && cachedState.SdkVersion is not null) { diff --git a/src/Tasks/Microsoft.NET.Build.Tasks/ProcessFrameworkReferences.cs b/src/Tasks/Microsoft.NET.Build.Tasks/ProcessFrameworkReferences.cs index ff790291ce1b..00be1e5f5bb2 100644 --- a/src/Tasks/Microsoft.NET.Build.Tasks/ProcessFrameworkReferences.cs +++ b/src/Tasks/Microsoft.NET.Build.Tasks/ProcessFrameworkReferences.cs @@ -1124,7 +1124,7 @@ IEnumerable GetPackFolders() if (!string.IsNullOrEmpty(NetCoreRoot) && !string.IsNullOrEmpty(NETCoreSdkVersion)) { if (WorkloadFileBasedInstall.IsUserLocal(NetCoreRoot, NETCoreSdkVersion) && - CliFolderPathCalculatorCore.GetDotnetUserProfileFolderPath() is { } userProfileDir) + CliFolderPathCalculatorCore.GetDotnetUserProfileFolderPathFromSystemEnvironment() is { } userProfileDir) { yield return Path.Combine(userProfileDir, "packs"); } @@ -1177,7 +1177,7 @@ private Lazy LazyCreateWorkloadResolver() { return new(() => { - string? userProfileDir = CliFolderPathCalculatorCore.GetDotnetUserProfileFolderPath(); + string? userProfileDir = CliFolderPathCalculatorCore.GetDotnetUserProfileFolderPathFromSystemEnvironment(); // When running MSBuild tasks, the current directory is always the project directory, so we can use that as the // starting point to search for global.json diff --git a/src/Tasks/Microsoft.NET.Build.Tasks/ShowMissingWorkloads.cs b/src/Tasks/Microsoft.NET.Build.Tasks/ShowMissingWorkloads.cs index e25457948c0d..319cec645344 100644 --- a/src/Tasks/Microsoft.NET.Build.Tasks/ShowMissingWorkloads.cs +++ b/src/Tasks/Microsoft.NET.Build.Tasks/ShowMissingWorkloads.cs @@ -38,7 +38,7 @@ protected override void ExecuteCore() { if (MissingWorkloadPacks.Any()) { - string userProfileDir = CliFolderPathCalculatorCore.GetDotnetUserProfileFolderPath(); + string userProfileDir = CliFolderPathCalculatorCore.GetDotnetUserProfileFolderPathFromSystemEnvironment(); // When running MSBuild tasks, the current directory is always the project directory, so we can use that as the // starting point to search for global.json From be29117b4b5a05efdee28e4877446cab64d10121 Mon Sep 17 00:00:00 2001 From: SimaTian Date: Mon, 23 Feb 2026 18:15:56 +0100 Subject: [PATCH 2/3] Remove static convenience methods per review feedback Replace static singleton + convenience methods with direct 'new CliFolderPathCalculatorCore()' at each callsite, so all callers follow the same pattern: new up the thing, then ask it for information. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../CliFolderPathCalculator.cs | 2 +- src/Common/CliFolderPathCalculatorCore.cs | 9 --------- src/RazorSdk/Tool/ServerCommand.cs | 2 +- .../MSBuildSdkResolver.cs | 2 +- .../WorkloadSdkResolver.cs | 2 +- .../ProcessFrameworkReferences.cs | 4 ++-- .../Microsoft.NET.Build.Tasks/ShowMissingWorkloads.cs | 2 +- 7 files changed, 7 insertions(+), 16 deletions(-) diff --git a/src/Cli/Microsoft.DotNet.Configurer/CliFolderPathCalculator.cs b/src/Cli/Microsoft.DotNet.Configurer/CliFolderPathCalculator.cs index 1a38fec3b8ee..a0ea4b86be96 100644 --- a/src/Cli/Microsoft.DotNet.Configurer/CliFolderPathCalculator.cs +++ b/src/Cli/Microsoft.DotNet.Configurer/CliFolderPathCalculator.cs @@ -46,7 +46,7 @@ public static string DotnetHomePath { get { - return CliFolderPathCalculatorCore.GetDotnetHomePathFromSystemEnvironment() + return new CliFolderPathCalculatorCore().GetDotnetHomePath() ?? throw new ConfigurationException( string.Format( LocalizableStrings.FailedToDetermineUserHomeDirectory, diff --git a/src/Common/CliFolderPathCalculatorCore.cs b/src/Common/CliFolderPathCalculatorCore.cs index e6c3aeb144fd..8cf438139621 100644 --- a/src/Common/CliFolderPathCalculatorCore.cs +++ b/src/Common/CliFolderPathCalculatorCore.cs @@ -53,14 +53,5 @@ public CliFolderPathCalculatorCore(Func getEnvironmentVariable) return home; } - // Static convenience methods for callers that use process environment variables. - - private static readonly CliFolderPathCalculatorCore s_default = new(); - - public static string? GetDotnetUserProfileFolderPathFromSystemEnvironment() - => s_default.GetDotnetUserProfileFolderPath(); - - public static string? GetDotnetHomePathFromSystemEnvironment() - => s_default.GetDotnetHomePath(); } } diff --git a/src/RazorSdk/Tool/ServerCommand.cs b/src/RazorSdk/Tool/ServerCommand.cs index 85fcc7870554..e120c6ca7c4d 100644 --- a/src/RazorSdk/Tool/ServerCommand.cs +++ b/src/RazorSdk/Tool/ServerCommand.cs @@ -169,7 +169,7 @@ internal static string GetPidFilePath() var path = Environment.GetEnvironmentVariable("DOTNET_BUILD_PIDFILE_DIRECTORY"); if (string.IsNullOrEmpty(path)) { - var homePath = CliFolderPathCalculatorCore.GetDotnetHomePathFromSystemEnvironment(); + var homePath = new CliFolderPathCalculatorCore().GetDotnetHomePath(); if (homePath is null) { // Couldn't locate the user profile directory. Bail. diff --git a/src/Resolvers/Microsoft.DotNet.MSBuildSdkResolver/MSBuildSdkResolver.cs b/src/Resolvers/Microsoft.DotNet.MSBuildSdkResolver/MSBuildSdkResolver.cs index b7223cdf9f6d..5633b7268d6b 100644 --- a/src/Resolvers/Microsoft.DotNet.MSBuildSdkResolver/MSBuildSdkResolver.cs +++ b/src/Resolvers/Microsoft.DotNet.MSBuildSdkResolver/MSBuildSdkResolver.cs @@ -308,7 +308,7 @@ private sealed class CachedState }; // First check if requested SDK resolves to a workload SDK pack - string? userProfileDir = CliFolderPathCalculatorCore.GetDotnetUserProfileFolderPathFromSystemEnvironment(); + string? userProfileDir = new CliFolderPathCalculatorCore().GetDotnetUserProfileFolderPath(); ResolutionResult? workloadResult = null; if (dotnetRoot is not null && netcoreSdkVersion is not null) { diff --git a/src/Resolvers/Microsoft.NET.Sdk.WorkloadMSBuildSdkResolver/WorkloadSdkResolver.cs b/src/Resolvers/Microsoft.NET.Sdk.WorkloadMSBuildSdkResolver/WorkloadSdkResolver.cs index bb676f155f8c..c88f71f1decd 100644 --- a/src/Resolvers/Microsoft.NET.Sdk.WorkloadMSBuildSdkResolver/WorkloadSdkResolver.cs +++ b/src/Resolvers/Microsoft.NET.Sdk.WorkloadMSBuildSdkResolver/WorkloadSdkResolver.cs @@ -62,7 +62,7 @@ private class CachedState resolverContext.State = cachedState; } - string? userProfileDir = CliFolderPathCalculatorCore.GetDotnetUserProfileFolderPathFromSystemEnvironment(); + string? userProfileDir = new CliFolderPathCalculatorCore().GetDotnetUserProfileFolderPath(); ResolutionResult? result = null; if (cachedState.DotnetRootPath is not null && cachedState.SdkVersion is not null) { diff --git a/src/Tasks/Microsoft.NET.Build.Tasks/ProcessFrameworkReferences.cs b/src/Tasks/Microsoft.NET.Build.Tasks/ProcessFrameworkReferences.cs index 00be1e5f5bb2..84576a9cb58c 100644 --- a/src/Tasks/Microsoft.NET.Build.Tasks/ProcessFrameworkReferences.cs +++ b/src/Tasks/Microsoft.NET.Build.Tasks/ProcessFrameworkReferences.cs @@ -1124,7 +1124,7 @@ IEnumerable GetPackFolders() if (!string.IsNullOrEmpty(NetCoreRoot) && !string.IsNullOrEmpty(NETCoreSdkVersion)) { if (WorkloadFileBasedInstall.IsUserLocal(NetCoreRoot, NETCoreSdkVersion) && - CliFolderPathCalculatorCore.GetDotnetUserProfileFolderPathFromSystemEnvironment() is { } userProfileDir) + new CliFolderPathCalculatorCore().GetDotnetUserProfileFolderPath() is { } userProfileDir) { yield return Path.Combine(userProfileDir, "packs"); } @@ -1177,7 +1177,7 @@ private Lazy LazyCreateWorkloadResolver() { return new(() => { - string? userProfileDir = CliFolderPathCalculatorCore.GetDotnetUserProfileFolderPathFromSystemEnvironment(); + string? userProfileDir = new CliFolderPathCalculatorCore().GetDotnetUserProfileFolderPath(); // When running MSBuild tasks, the current directory is always the project directory, so we can use that as the // starting point to search for global.json diff --git a/src/Tasks/Microsoft.NET.Build.Tasks/ShowMissingWorkloads.cs b/src/Tasks/Microsoft.NET.Build.Tasks/ShowMissingWorkloads.cs index 319cec645344..f79c0e2e746f 100644 --- a/src/Tasks/Microsoft.NET.Build.Tasks/ShowMissingWorkloads.cs +++ b/src/Tasks/Microsoft.NET.Build.Tasks/ShowMissingWorkloads.cs @@ -38,7 +38,7 @@ protected override void ExecuteCore() { if (MissingWorkloadPacks.Any()) { - string userProfileDir = CliFolderPathCalculatorCore.GetDotnetUserProfileFolderPathFromSystemEnvironment(); + string userProfileDir = new CliFolderPathCalculatorCore().GetDotnetUserProfileFolderPath(); // When running MSBuild tasks, the current directory is always the project directory, so we can use that as the // starting point to search for global.json From 1aa3a281b6974091d25caa009915fb6b238703f3 Mon Sep 17 00:00:00 2001 From: SimaTian Date: Tue, 24 Feb 2026 10:11:08 +0100 Subject: [PATCH 3/3] Refactor FrameworkReferenceResolver to support env var injection Make FrameworkReferenceResolver non-static with a Func constructor parameter for environment variable reads. This replaces the call to DotNetReferenceAssembliesPathResolver.Resolve() (a runtime library method that uses process-global Environment.GetEnvironmentVariable) with a delegate-based read using the runtime's own DotNetReferenceAssembliesPathEnv constant. - Default constructor uses Environment.GetEnvironmentVariable (no behavior change) - New constructor accepts a custom env var reader delegate - All env var reads (DOTNET_REFERENCE_ASSEMBLIES_PATH, ProgramFiles) go through delegate - GenerateDepsFile callsite updated to use new FrameworkReferenceResolver() MSBuild task migration branch (merge-group-2) will rebase onto this and use new FrameworkReferenceResolver(TaskEnvironment.GetEnvironmentVariable). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../FrameworkReferenceResolver.cs | 33 +++++++++++++++---- .../GenerateDepsFile.cs | 2 +- 2 files changed, 28 insertions(+), 7 deletions(-) diff --git a/src/Tasks/Microsoft.NET.Build.Tasks/FrameworkReferenceResolver.cs b/src/Tasks/Microsoft.NET.Build.Tasks/FrameworkReferenceResolver.cs index c1486460ec37..d0e3c58f2fab 100644 --- a/src/Tasks/Microsoft.NET.Build.Tasks/FrameworkReferenceResolver.cs +++ b/src/Tasks/Microsoft.NET.Build.Tasks/FrameworkReferenceResolver.cs @@ -7,12 +7,33 @@ namespace Microsoft.NET.Build.Tasks { - internal static class FrameworkReferenceResolver + internal class FrameworkReferenceResolver { - public static string GetDefaultReferenceAssembliesPath() + private readonly Func _getEnvironmentVariable; + + /// + /// Creates an instance that reads environment variables from the process environment. + /// + public FrameworkReferenceResolver() + : this(Environment.GetEnvironmentVariable) + { + } + + /// + /// Creates an instance that reads environment variables via the supplied delegate. + /// Use this from MSBuild tasks to route reads through TaskEnvironment. + /// + public FrameworkReferenceResolver(Func getEnvironmentVariable) + { + _getEnvironmentVariable = getEnvironmentVariable ?? throw new ArgumentNullException(nameof(getEnvironmentVariable)); + } + + public string GetDefaultReferenceAssembliesPath() { - // Allow setting the reference assemblies path via an environment variable - var referenceAssembliesPath = DotNetReferenceAssembliesPathResolver.Resolve(); + // Allow setting the reference assemblies path via an environment variable. + // We read this directly instead of calling DotNetReferenceAssembliesPathResolver.Resolve() + // because that runtime method uses process-global Environment.GetEnvironmentVariable. + var referenceAssembliesPath = _getEnvironmentVariable(DotNetReferenceAssembliesPathResolver.DotNetReferenceAssembliesPathEnv); if (!string.IsNullOrEmpty(referenceAssembliesPath)) { @@ -28,12 +49,12 @@ public static string GetDefaultReferenceAssembliesPath() // References assemblies are in %ProgramFiles(x86)% on // 64 bit machines - var programFiles = Environment.GetEnvironmentVariable("ProgramFiles(x86)"); + var programFiles = _getEnvironmentVariable("ProgramFiles(x86)"); if (string.IsNullOrEmpty(programFiles)) { // On 32 bit machines they are in %ProgramFiles% - programFiles = Environment.GetEnvironmentVariable("ProgramFiles"); + programFiles = _getEnvironmentVariable("ProgramFiles"); } if (string.IsNullOrEmpty(programFiles)) diff --git a/src/Tasks/Microsoft.NET.Build.Tasks/GenerateDepsFile.cs b/src/Tasks/Microsoft.NET.Build.Tasks/GenerateDepsFile.cs index e3245a4f5103..ea70b0da6fbf 100644 --- a/src/Tasks/Microsoft.NET.Build.Tasks/GenerateDepsFile.cs +++ b/src/Tasks/Microsoft.NET.Build.Tasks/GenerateDepsFile.cs @@ -244,7 +244,7 @@ bool ShouldIncludeRuntimeAsset(ITaskItem item) .WithReferenceProjectInfos(referenceProjects) .WithRuntimePackAssets(runtimePackAssets) .WithCompilationOptions(compilationOptions) - .WithReferenceAssembliesPath(FrameworkReferenceResolver.GetDefaultReferenceAssembliesPath()) + .WithReferenceAssembliesPath(new FrameworkReferenceResolver().GetDefaultReferenceAssembliesPath()) .WithPackagesThatWereFiltered(GetFilteredPackages()); if (CompileReferences.Length > 0)