From 1b2090c6891dd46e61aac9eed28cacae873aa87c Mon Sep 17 00:00:00 2001 From: Chet Husk Date: Sun, 13 Jul 2025 10:42:12 -0500 Subject: [PATCH 01/12] initial plan --- documentation/specs/unified-configuration.md | 466 +++++++++++++++++++ 1 file changed, 466 insertions(+) create mode 100644 documentation/specs/unified-configuration.md diff --git a/documentation/specs/unified-configuration.md b/documentation/specs/unified-configuration.md new file mode 100644 index 000000000000..e215a6a6259f --- /dev/null +++ b/documentation/specs/unified-configuration.md @@ -0,0 +1,466 @@ +# Unified Configuration System for .NET CLI + +## Overview + +This specification outlines the plan to refactor the .NET CLI codebase to replace direct usage of global.json and environment variables with a unified configuration system based on Microsoft.Extensions.Configuration.IConfiguration. + +## Goals + +- Replace direct `Environment.GetEnvironmentVariable()` calls with a unified configuration system +- Replace direct `global.json` file reading with configuration providers +- Establish a clear configuration hierarchy and precedence +- Maintain backward compatibility with existing environment variables and global.json usage +- Provide a foundation for future configuration enhancements (e.g., dotnet.config support) + +## Current State Analysis + +The codebase currently has: +1. Direct `Environment.GetEnvironmentVariable()` calls throughout various classes +2. An `EnvironmentProvider` abstraction that wraps environment variable access +3. Custom `global.json` reading in several places (e.g., `GlobalJsonWorkloadSetsFile.cs`, `RuntimeConfig.cs`) +4. Some use of Microsoft.Extensions.Configuration in test infrastructure but not in the main CLI + +## Configuration Hierarchy + +The new unified configuration system will follow this precedence order (highest to lowest priority): + +1. **Command-line arguments** (handled separately by existing System.CommandLine infrastructure) +2. **Environment variables with DOTNET_ prefix** (e.g., `DOTNET_CLI_TELEMETRY_OPTOUT`) +3. **global.json** (custom configuration provider) +4. **dotnet.config** (future enhancement - INI configuration file) + +**Note:** System-level environment variables without the DOTNET_ prefix (e.g., `PATH`, `HOME`, `TEMP`) will continue to be accessed directly through the existing `IEnvironmentProvider` interface as they are not specific to .NET CLI configuration. + +## Implementation Plan + +### Phase 1: Infrastructure + +#### 1.1 Core Configuration Builder + +Create a centralized configuration builder that consolidates all configuration sources: + +```csharp +public class DotNetConfiguration +{ + public static IConfiguration Create(string workingDirectory = null) + { + var builder = new ConfigurationBuilder(); + + // Priority order (last wins): + // 1. dotnet.config (if it exists) + // 2. global.json (custom provider) + // 3. Environment variables with DOTNET_ prefix + // 4. Command line arguments (handled separately) + + workingDirectory ??= Directory.GetCurrentDirectory(); + + // Add dotnet.config if it exists (future enhancement) + var dotnetConfigPath = Path.Combine(workingDirectory, "dotnet.config"); + if (File.Exists(dotnetConfigPath)) + { + builder.AddIniFile(dotnetConfigPath, optional: true, reloadOnChange: false); + } + + // Add global.json with a custom configuration provider + builder.Add(new GlobalJsonConfigurationSource(workingDirectory)); + + // Add only DOTNET_ prefixed environment variables + builder.AddEnvironmentVariables("DOTNET_"); + + return builder.Build(); + } +} +``` + +#### 1.2 Global.json Configuration Provider + +Create a custom configuration provider for global.json files: + +```csharp +public class GlobalJsonConfigurationProvider : ConfigurationProvider +{ + private readonly string _path; + + public GlobalJsonConfigurationProvider(string workingDirectory) + { + _path = FindGlobalJson(workingDirectory); + } + + public override void Load() + { + Data.Clear(); + + if (_path == null || !File.Exists(_path)) + return; + + try + { + var json = File.ReadAllText(_path); + var document = JsonDocument.Parse(json); + + LoadGlobalJsonData(document.RootElement, ""); + } + catch (Exception ex) + { + throw new InvalidOperationException($"Error parsing global.json at {_path}", ex); + } + } + + private void LoadGlobalJsonData(JsonElement element, string prefix) + { + foreach (var property in element.EnumerateObject()) + { + var key = string.IsNullOrEmpty(prefix) + ? property.Name + : $"{prefix}:{property.Name}"; + + switch (property.Value.ValueKind) + { + case JsonValueKind.Object: + LoadGlobalJsonData(property.Value, key); + break; + case JsonValueKind.String: + Data[key] = property.Value.GetString(); + break; + case JsonValueKind.Number: + Data[key] = property.Value.GetRawText(); + break; + case JsonValueKind.True: + case JsonValueKind.False: + Data[key] = property.Value.GetBoolean().ToString(); + break; + } + } + } + + private string FindGlobalJson(string startDirectory) + { + var current = new DirectoryInfo(startDirectory); + while (current != null) + { + var globalJsonPath = Path.Combine(current.FullName, "global.json"); + if (File.Exists(globalJsonPath)) + return globalJsonPath; + current = current.Parent; + } + return null; + } +} + +public class GlobalJsonConfigurationSource : IConfigurationSource +{ + private readonly string _workingDirectory; + + public GlobalJsonConfigurationSource(string workingDirectory) + { + _workingDirectory = workingDirectory; + } + + public IConfigurationProvider Build(IConfigurationBuilder builder) + { + return new GlobalJsonConfigurationProvider(_workingDirectory); + } +} +``` + +#### 1.3 Configuration Service Abstraction + +Create a service interface for configuration access: + +```csharp +public interface IConfigurationService +{ + IConfiguration Configuration { get; } + string GetValue(string key, string defaultValue = null); + T GetValue(string key, T defaultValue = default); + bool GetBoolValue(string key, bool defaultValue = false); +} + +public class ConfigurationService : IConfigurationService +{ + public IConfiguration Configuration { get; } + + public ConfigurationService(IConfiguration configuration) + { + Configuration = configuration ?? throw new ArgumentNullException(nameof(configuration)); + } + + public string GetValue(string key, string defaultValue = null) + { + return Configuration[key] ?? defaultValue; + } + + public T GetValue(string key, T defaultValue = default) + { + return Configuration.GetValue(key, defaultValue); + } + + public bool GetBoolValue(string key, bool defaultValue = false) + { + var value = Configuration[key]; + if (string.IsNullOrEmpty(value)) + return defaultValue; + + return value.ToLowerInvariant() switch + { + "true" or "1" or "yes" => true, + "false" or "0" or "no" => false, + _ => defaultValue + }; + } +} +``` + +### Phase 2: Integration + +#### 2.1 Update Program.cs + +Update the main entry point to initialize configuration early: + +```csharp +public class Program +{ + public static IConfiguration GlobalConfiguration { get; private set; } + public static IConfigurationService ConfigurationService { get; private set; } + + public static int Main(string[] args) + { + // Initialize configuration early + GlobalConfiguration = DotNetConfiguration.Create(); + ConfigurationService = new ConfigurationService(GlobalConfiguration); + + // Replace direct env var calls with configuration + bool perfLogEnabled = ConfigurationService.GetBoolValue("DOTNET_CLI_PERF_LOG", false); + + // Continue with existing logic... + } +} +``` + +#### 2.2 Configuration-Based Environment Provider + +Create a bridge between the new configuration system and existing `IEnvironmentProvider` interface: + +```csharp +public class ConfigurationBasedEnvironmentProvider : IEnvironmentProvider +{ + private readonly IConfigurationService _configurationService; + private readonly IEnvironmentProvider _fallbackProvider; + + public ConfigurationBasedEnvironmentProvider( + IConfigurationService configurationService, + IEnvironmentProvider fallbackProvider = null) + { + _configurationService = configurationService; + _fallbackProvider = fallbackProvider ?? new EnvironmentProvider(); + } + + public string GetEnvironmentVariable(string name) + { + // For DOTNET_ prefixed variables, try configuration service first + if (name.StartsWith("DOTNET_", StringComparison.OrdinalIgnoreCase)) + { + var configValue = _configurationService.GetValue(name); + if (configValue != null) + return configValue; + } + + // For all other variables or if not found in config, use fallback provider + return _fallbackProvider.GetEnvironmentVariable(name); + } + + public bool GetEnvironmentVariableAsBool(string name, bool defaultValue) + { + // For DOTNET_ prefixed variables, use configuration service + if (name.StartsWith("DOTNET_", StringComparison.OrdinalIgnoreCase)) + { + return _configurationService.GetBoolValue(name, defaultValue); + } + + // For all other variables, use fallback provider + return _fallbackProvider.GetEnvironmentVariableAsBool(name, defaultValue); + } + + // Implement other members as pass-through to fallback provider + public IEnumerable ExecutableExtensions => _fallbackProvider.ExecutableExtensions; + + public string GetCommandPath(string commandName, params string[] extensions) + => _fallbackProvider.GetCommandPath(commandName, extensions); + + public string GetCommandPathFromRootPath(string rootPath, string commandName, params string[] extensions) + => _fallbackProvider.GetCommandPathFromRootPath(rootPath, commandName, extensions); + + public string GetCommandPathFromRootPath(string rootPath, string commandName, IEnumerable extensions) + => _fallbackProvider.GetCommandPathFromRootPath(rootPath, commandName, extensions); + + public string GetEnvironmentVariable(string variable, EnvironmentVariableTarget target) + => _fallbackProvider.GetEnvironmentVariable(variable, target); + + public void SetEnvironmentVariable(string variable, string value, EnvironmentVariableTarget target) + => _fallbackProvider.SetEnvironmentVariable(variable, value, target); +} +``` + +### Phase 3: Migration + +#### 3.1 Systematic Replacement + +Replace direct environment variable access patterns: + +**Before:** +```csharp +var value = Environment.GetEnvironmentVariable("DOTNET_CLI_TELEMETRY_OPTOUT"); +bool optOut = !string.IsNullOrEmpty(value) && + (value.Equals("true", StringComparison.OrdinalIgnoreCase) || + value.Equals("1")); +``` + +**After:** +```csharp +bool optOut = ConfigurationService.GetBoolValue("DOTNET_CLI_TELEMETRY_OPTOUT", false); +``` + +#### 3.2 Update Direct global.json Readers + +Classes that currently read global.json directly should be updated to use the configuration system: + +**Files to update:** +- `src/Cli/dotnet/Commands/Workload/GlobalJsonWorkloadSetFile.cs` +- `src/Cli/dotnet/RuntimeConfig.cs` +- Any other direct global.json readers found during implementation + +#### 3.3 Update Environment Provider Usages + +Update all instances where `IEnvironmentProvider` is used to use the new `ConfigurationBasedEnvironmentProvider`. + +### Phase 4: Testing & Validation + +#### 4.1 Backward Compatibility Testing + +Ensure all existing functionality continues to work: +- All DOTNET_ prefixed environment variables continue to be respected +- System environment variables (PATH, HOME, etc.) continue to work through the fallback provider +- global.json files are read correctly +- Precedence rules work as expected + +#### 4.2 Unit Tests + +Create comprehensive unit tests for: +- `DotNetConfiguration` class +- `GlobalJsonConfigurationProvider` +- `ConfigurationService` +- `ConfigurationBasedEnvironmentProvider` + +#### 4.3 Integration Tests + +Add integration tests to verify: +- Configuration hierarchy works correctly for DOTNET_ prefixed variables +- System environment variables continue to work through the fallback provider +- global.json files in various directory structures are found +- Environment variable override behavior works correctly + +## Package Dependencies + +The following NuGet package references will need to be added to relevant projects: + +- `Microsoft.Extensions.Configuration` +- `Microsoft.Extensions.Configuration.Abstractions` +- `Microsoft.Extensions.Configuration.EnvironmentVariables` +- `Microsoft.Extensions.Configuration.Json` +- `Microsoft.Extensions.Configuration.Ini` +- `Microsoft.Extensions.Configuration.Binder` + +**Note:** We will only add the `Microsoft.Extensions.Configuration.EnvironmentVariables` package but configure it to only process DOTNET_ prefixed variables. The `Microsoft.Extensions.Configuration.Ini` package is needed for future dotnet.config support. + +## Configuration Key Mapping + +### DOTNET_ Prefixed Environment Variables + +Only DOTNET_ prefixed environment variables will be included in the unified configuration system: +- `DOTNET_CLI_TELEMETRY_OPTOUT` → `DOTNET_CLI_TELEMETRY_OPTOUT` +- `DOTNET_NOLOGO` → `DOTNET_NOLOGO` +- `DOTNET_CLI_PERF_LOG` → `DOTNET_CLI_PERF_LOG` + +### System Environment Variables + +System-level environment variables without the DOTNET_ prefix (e.g., `PATH`, `HOME`, `TEMP`, `USER`) will continue to be accessed directly through the existing `IEnvironmentProvider` interface and will not be part of the unified configuration system. + +### Global.json Structure + +Global.json properties will be flattened using colon notation: + +```json +{ + "sdk": { + "version": "6.0.100" + }, + "msbuild-sdks": { + "Microsoft.Build.Traversal": "1.0.0" + } +} +``` + +Maps to: +- `sdk:version` → `"6.0.100"` +- `msbuild-sdks:Microsoft.Build.Traversal` → `"1.0.0"` + +## Error Handling + +- **Malformed global.json**: Log warning and continue without global.json configuration +- **Missing files**: Silently ignore missing optional configuration files +- **Configuration access errors**: Provide meaningful error messages and fallback to defaults + +## Future Enhancements + +### dotnet.config Support + +A future enhancement could add support for a `dotnet.config` INI file that provides project-specific configuration: + +```ini +[cli] +telemetryOptOut=true +noLogo=true + +[build] +configuration=Release +``` + +This would map to configuration keys like: +- `cli:telemetryOptOut` → `"true"` +- `cli:noLogo` → `"true"` +- `build:configuration` → `"Release"` + +### Configuration Schema Validation + +Consider adding schema validation for configuration files to provide better error messages. For INI files, this would involve validating known sections and keys. + +### Configuration Hot Reload + +For development scenarios, consider adding support for configuration hot reload when files change. + +## Breaking Changes + +This refactoring should not introduce any breaking changes as it maintains backward compatibility with: +- All existing DOTNET_ prefixed environment variables +- All system environment variables (accessed through fallback provider) +- All existing global.json file structures and locations +- All existing APIs and interfaces (through adapter patterns) + +## Implementation Timeline + +1. **Phase 1** (Infrastructure): 1-2 weeks +2. **Phase 2** (Integration): 1 week +3. **Phase 3** (Migration): 2-3 weeks (incremental, can be spread across multiple PRs) +4. **Phase 4** (Testing): 1 week + +Total estimated effort: 5-7 weeks + +## Success Criteria + +- [ ] All direct `Environment.GetEnvironmentVariable()` calls for DOTNET_ prefixed variables replaced +- [ ] All direct global.json reading replaced with configuration providers +- [ ] System environment variables continue to work through existing providers +- [ ] Comprehensive test coverage for new configuration system +- [ ] All existing functionality preserved (backward compatibility) +- [ ] Performance impact is negligible +- [ ] Documentation updated to reflect new configuration system From e220a7017fa55f3b84278aefcf2b5780db7fd4f4 Mon Sep 17 00:00:00 2001 From: Chet Husk Date: Sun, 13 Jul 2025 11:06:08 -0500 Subject: [PATCH 02/12] typed config, key mapping, etc --- documentation/specs/unified-configuration.md | 606 +++++++++++++++---- 1 file changed, 493 insertions(+), 113 deletions(-) diff --git a/documentation/specs/unified-configuration.md b/documentation/specs/unified-configuration.md index e215a6a6259f..18e078f9dc7b 100644 --- a/documentation/specs/unified-configuration.md +++ b/documentation/specs/unified-configuration.md @@ -35,9 +35,9 @@ The new unified configuration system will follow this precedence order (highest ### Phase 1: Infrastructure -#### 1.1 Core Configuration Builder +#### 1.1 Core Configuration Builder with Key Mapping -Create a centralized configuration builder that consolidates all configuration sources: +Create a centralized configuration builder that consolidates all configuration sources with key mapping: ```csharp public class DotNetConfiguration @@ -45,59 +45,68 @@ public class DotNetConfiguration public static IConfiguration Create(string workingDirectory = null) { var builder = new ConfigurationBuilder(); - + // Priority order (last wins): - // 1. dotnet.config (if it exists) - // 2. global.json (custom provider) - // 3. Environment variables with DOTNET_ prefix + // 1. dotnet.config (if it exists) - with section-based key mapping + // 2. global.json (custom provider with key mapping) + // 3. Environment variables with DOTNET_ prefix (with key mapping) // 4. Command line arguments (handled separately) - + workingDirectory ??= Directory.GetCurrentDirectory(); - + // Add dotnet.config if it exists (future enhancement) var dotnetConfigPath = Path.Combine(workingDirectory, "dotnet.config"); if (File.Exists(dotnetConfigPath)) { builder.AddIniFile(dotnetConfigPath, optional: true, reloadOnChange: false); } - - // Add global.json with a custom configuration provider + + // Add global.json with a custom configuration provider that maps keys builder.Add(new GlobalJsonConfigurationSource(workingDirectory)); - - // Add only DOTNET_ prefixed environment variables - builder.AddEnvironmentVariables("DOTNET_"); - + + // Add DOTNET_ prefixed environment variables with key mapping + builder.Add(new DotNetEnvironmentConfigurationSource()); + return builder.Build(); } } ``` -#### 1.2 Global.json Configuration Provider +#### 1.2 Enhanced Global.json Configuration Provider -Create a custom configuration provider for global.json files: +Create a custom configuration provider for global.json files with key mapping: ```csharp public class GlobalJsonConfigurationProvider : ConfigurationProvider { private readonly string _path; - + + private static readonly Dictionary GlobalJsonKeyMappings = new() + { + ["sdk:version"] = "sdk:version", + ["sdk:rollForward"] = "sdk:rollforward", + ["sdk:allowPrerelease"] = "sdk:allowprerelease", + ["msbuild-sdks"] = "msbuild:sdks", + // Add more mappings as the global.json schema evolves + }; + public GlobalJsonConfigurationProvider(string workingDirectory) { _path = FindGlobalJson(workingDirectory); } - + public override void Load() { Data.Clear(); - + if (_path == null || !File.Exists(_path)) return; - + try { var json = File.ReadAllText(_path); var document = JsonDocument.Parse(json); - + LoadGlobalJsonData(document.RootElement, ""); } catch (Exception ex) @@ -105,34 +114,58 @@ public class GlobalJsonConfigurationProvider : ConfigurationProvider throw new InvalidOperationException($"Error parsing global.json at {_path}", ex); } } - + private void LoadGlobalJsonData(JsonElement element, string prefix) { foreach (var property in element.EnumerateObject()) { - var key = string.IsNullOrEmpty(prefix) - ? property.Name + var rawKey = string.IsNullOrEmpty(prefix) + ? property.Name : $"{prefix}:{property.Name}"; - + switch (property.Value.ValueKind) { case JsonValueKind.Object: - LoadGlobalJsonData(property.Value, key); + LoadGlobalJsonData(property.Value, rawKey); break; case JsonValueKind.String: - Data[key] = property.Value.GetString(); - break; case JsonValueKind.Number: - Data[key] = property.Value.GetRawText(); - break; case JsonValueKind.True: case JsonValueKind.False: - Data[key] = property.Value.GetBoolean().ToString(); + // Map to canonical key format + var canonicalKey = MapGlobalJsonKey(rawKey); + Data[canonicalKey] = GetValueAsString(property.Value); break; } } } - + + private string MapGlobalJsonKey(string rawKey) + { + // Check for exact mapping first + if (GlobalJsonKeyMappings.TryGetValue(rawKey, out var mapped)) + return mapped; + + // For msbuild-sdks, convert to msbuild:sdks:packagename format + if (rawKey.StartsWith("msbuild-sdks:")) + return rawKey.Replace("msbuild-sdks:", "msbuild:sdks:"); + + // Default: convert to lowercase and normalize separators + return rawKey.ToLowerInvariant().Replace("-", ":"); + } + + private string GetValueAsString(JsonElement element) + { + return element.ValueKind switch + { + JsonValueKind.String => element.GetString(), + JsonValueKind.Number => element.GetRawText(), + JsonValueKind.True => "true", + JsonValueKind.False => "false", + _ => element.GetRawText() + }; + } + private string FindGlobalJson(string startDirectory) { var current = new DirectoryInfo(startDirectory); @@ -146,61 +179,101 @@ public class GlobalJsonConfigurationProvider : ConfigurationProvider return null; } } +``` -public class GlobalJsonConfigurationSource : IConfigurationSource +#### 1.3 Environment Variable Configuration Provider with Key Mapping + +Create a custom environment variable provider that maps DOTNET_ variables to canonical keys: + +```csharp +public class DotNetEnvironmentConfigurationProvider : ConfigurationProvider { - private readonly string _workingDirectory; - - public GlobalJsonConfigurationSource(string workingDirectory) + private static readonly Dictionary EnvironmentKeyMappings = new() + { + ["DOTNET_HOST_PATH"] = "dotnet:host:path", + ["DOTNET_CLI_TELEMETRY_OPTOUT"] = "dotnet:cli:telemetry:optout", + ["DOTNET_NOLOGO"] = "dotnet:cli:nologo", + ["DOTNET_CLI_PERF_LOG"] = "dotnet:cli:perf:log", + ["DOTNET_MULTILEVEL_LOOKUP"] = "dotnet:host:multilevel:lookup", + ["DOTNET_ROLL_FORWARD"] = "dotnet:runtime:rollforward", + ["DOTNET_ROLL_FORWARD_ON_NO_CANDIDATE_FX"] = "dotnet:runtime:rollforward:onnocandidate", + ["DOTNET_CLI_HOME"] = "dotnet:cli:home", + ["DOTNET_CLI_CONTEXT_VERBOSE"] = "dotnet:cli:context:verbose", + ["DOTNET_SKIP_FIRST_TIME_EXPERIENCE"] = "dotnet:cli:firsttime:skip", + ["DOTNET_ADD_GLOBAL_TOOLS_TO_PATH"] = "dotnet:tools:addtopath", + // Add more mappings as needed + }; + + public override void Load() { - _workingDirectory = workingDirectory; + Data.Clear(); + + foreach (var mapping in EnvironmentKeyMappings) + { + var value = Environment.GetEnvironmentVariable(mapping.Key); + if (!string.IsNullOrEmpty(value)) + { + Data[mapping.Value] = value; + } + } } - +} + +public class DotNetEnvironmentConfigurationSource : IConfigurationSource +{ public IConfigurationProvider Build(IConfigurationBuilder builder) { - return new GlobalJsonConfigurationProvider(_workingDirectory); + return new DotNetEnvironmentConfigurationProvider(); } } ``` -#### 1.3 Configuration Service Abstraction +#### 1.4 Updated Configuration Service with Key Lookup Helper -Create a service interface for configuration access: +Update the configuration service to provide helper methods for common lookups: ```csharp public interface IConfigurationService { IConfiguration Configuration { get; } - string GetValue(string key, string defaultValue = null); - T GetValue(string key, T defaultValue = default); - bool GetBoolValue(string key, bool defaultValue = false); + + // Generic value access using canonical keys + string GetValue(string canonicalKey, string defaultValue = null); + T GetValue(string canonicalKey, T defaultValue = default); + bool GetBoolValue(string canonicalKey, bool defaultValue = false); + + // Helper methods for common configuration values + bool IsTelemetryOptOut(); + bool IsNoLogo(); + string GetHostPath(); + string GetSdkVersion(); } public class ConfigurationService : IConfigurationService { public IConfiguration Configuration { get; } - + public ConfigurationService(IConfiguration configuration) { Configuration = configuration ?? throw new ArgumentNullException(nameof(configuration)); } - - public string GetValue(string key, string defaultValue = null) + + public string GetValue(string canonicalKey, string defaultValue = null) { - return Configuration[key] ?? defaultValue; + return Configuration[canonicalKey] ?? defaultValue; } - - public T GetValue(string key, T defaultValue = default) + + public T GetValue(string canonicalKey, T defaultValue = default) { - return Configuration.GetValue(key, defaultValue); + return Configuration.GetValue(canonicalKey, defaultValue); } - - public bool GetBoolValue(string key, bool defaultValue = false) + + public bool GetBoolValue(string canonicalKey, bool defaultValue = false) { - var value = Configuration[key]; + var value = Configuration[canonicalKey]; if (string.IsNullOrEmpty(value)) return defaultValue; - + return value.ToLowerInvariant() switch { "true" or "1" or "yes" => true, @@ -208,6 +281,12 @@ public class ConfigurationService : IConfigurationService _ => defaultValue }; } + + // Helper methods for common configuration values + public bool IsTelemetryOptOut() => GetBoolValue("dotnet:cli:telemetry:optout", false); + public bool IsNoLogo() => GetBoolValue("dotnet:cli:nologo", false); + public string GetHostPath() => GetValue("dotnet:host:path"); + public string GetSdkVersion() => GetValue("sdk:version"); } ``` @@ -215,29 +294,30 @@ public class ConfigurationService : IConfigurationService #### 2.1 Update Program.cs -Update the main entry point to initialize configuration early: +Update the main entry point to initialize configuration early with canonical key access: ```csharp public class Program { public static IConfiguration GlobalConfiguration { get; private set; } public static IConfigurationService ConfigurationService { get; private set; } - + public static int Main(string[] args) { // Initialize configuration early GlobalConfiguration = DotNetConfiguration.Create(); ConfigurationService = new ConfigurationService(GlobalConfiguration); - - // Replace direct env var calls with configuration - bool perfLogEnabled = ConfigurationService.GetBoolValue("DOTNET_CLI_PERF_LOG", false); - + + // Replace direct env var calls with configuration using canonical keys + bool perfLogEnabled = ConfigurationService.GetBoolValue("dotnet:cli:perf:log", false); + bool noLogo = ConfigurationService.IsNoLogo(); + // Continue with existing logic... } } ``` -#### 2.2 Configuration-Based Environment Provider +#### 2.2 Configuration-Based Environment Provider with Key Mapping Create a bridge between the new configuration system and existing `IEnvironmentProvider` interface: @@ -246,56 +326,71 @@ public class ConfigurationBasedEnvironmentProvider : IEnvironmentProvider { private readonly IConfigurationService _configurationService; private readonly IEnvironmentProvider _fallbackProvider; - + + // Reverse mapping from environment variable names to canonical keys + private static readonly Dictionary EnvironmentToCanonicalMappings = new() + { + ["DOTNET_HOST_PATH"] = "dotnet:host:path", + ["DOTNET_CLI_TELEMETRY_OPTOUT"] = "dotnet:cli:telemetry:optout", + ["DOTNET_NOLOGO"] = "dotnet:cli:nologo", + ["DOTNET_CLI_PERF_LOG"] = "dotnet:cli:perf:log", + ["DOTNET_MULTILEVEL_LOOKUP"] = "dotnet:host:multilevel:lookup", + ["DOTNET_ROLL_FORWARD"] = "dotnet:runtime:rollforward", + ["DOTNET_ROLL_FORWARD_ON_NO_CANDIDATE_FX"] = "dotnet:runtime:rollforward:onnocandidate", + // Add more mappings as needed + }; + public ConfigurationBasedEnvironmentProvider( - IConfigurationService configurationService, + IConfigurationService configurationService, IEnvironmentProvider fallbackProvider = null) { _configurationService = configurationService; _fallbackProvider = fallbackProvider ?? new EnvironmentProvider(); } - + public string GetEnvironmentVariable(string name) { - // For DOTNET_ prefixed variables, try configuration service first - if (name.StartsWith("DOTNET_", StringComparison.OrdinalIgnoreCase)) + // For DOTNET_ prefixed variables, try configuration service first using canonical key + if (name.StartsWith("DOTNET_", StringComparison.OrdinalIgnoreCase) && + EnvironmentToCanonicalMappings.TryGetValue(name, out var canonicalKey)) { - var configValue = _configurationService.GetValue(name); + var configValue = _configurationService.GetValue(canonicalKey); if (configValue != null) return configValue; } - + // For all other variables or if not found in config, use fallback provider return _fallbackProvider.GetEnvironmentVariable(name); } - + public bool GetEnvironmentVariableAsBool(string name, bool defaultValue) { - // For DOTNET_ prefixed variables, use configuration service - if (name.StartsWith("DOTNET_", StringComparison.OrdinalIgnoreCase)) + // For DOTNET_ prefixed variables, use configuration service with canonical key + if (name.StartsWith("DOTNET_", StringComparison.OrdinalIgnoreCase) && + EnvironmentToCanonicalMappings.TryGetValue(name, out var canonicalKey)) { - return _configurationService.GetBoolValue(name, defaultValue); + return _configurationService.GetBoolValue(canonicalKey, defaultValue); } - + // For all other variables, use fallback provider return _fallbackProvider.GetEnvironmentVariableAsBool(name, defaultValue); } - + // Implement other members as pass-through to fallback provider public IEnumerable ExecutableExtensions => _fallbackProvider.ExecutableExtensions; - + public string GetCommandPath(string commandName, params string[] extensions) => _fallbackProvider.GetCommandPath(commandName, extensions); - + public string GetCommandPathFromRootPath(string rootPath, string commandName, params string[] extensions) => _fallbackProvider.GetCommandPathFromRootPath(rootPath, commandName, extensions); - + public string GetCommandPathFromRootPath(string rootPath, string commandName, IEnumerable extensions) => _fallbackProvider.GetCommandPathFromRootPath(rootPath, commandName, extensions); - + public string GetEnvironmentVariable(string variable, EnvironmentVariableTarget target) => _fallbackProvider.GetEnvironmentVariable(variable, target); - + public void SetEnvironmentVariable(string variable, string value, EnvironmentVariableTarget target) => _fallbackProvider.SetEnvironmentVariable(variable, value, target); } @@ -305,19 +400,33 @@ public class ConfigurationBasedEnvironmentProvider : IEnvironmentProvider #### 3.1 Systematic Replacement -Replace direct environment variable access patterns: +Replace direct environment variable access patterns using canonical keys: **Before:** ```csharp var value = Environment.GetEnvironmentVariable("DOTNET_CLI_TELEMETRY_OPTOUT"); -bool optOut = !string.IsNullOrEmpty(value) && - (value.Equals("true", StringComparison.OrdinalIgnoreCase) || +bool optOut = !string.IsNullOrEmpty(value) && + (value.Equals("true", StringComparison.OrdinalIgnoreCase) || value.Equals("1")); ``` -**After:** +**After (using canonical key):** ```csharp -bool optOut = ConfigurationService.GetBoolValue("DOTNET_CLI_TELEMETRY_OPTOUT", false); +bool optOut = ConfigurationService.GetBoolValue("dotnet:cli:telemetry:optout", false); +// Or using the helper method: +bool optOut = ConfigurationService.IsTelemetryOptOut(); +``` + +**Before:** +```csharp +var hostPath = Environment.GetEnvironmentVariable("DOTNET_HOST_PATH"); +``` + +**After (using canonical key):** +```csharp +var hostPath = ConfigurationService.GetValue("dotnet:host:path"); +// Or using the helper method: +var hostPath = ConfigurationService.GetHostPath(); ``` #### 3.2 Update Direct global.json Readers @@ -370,25 +479,19 @@ The following NuGet package references will need to be added to relevant project - `Microsoft.Extensions.Configuration.Ini` - `Microsoft.Extensions.Configuration.Binder` -**Note:** We will only add the `Microsoft.Extensions.Configuration.EnvironmentVariables` package but configure it to only process DOTNET_ prefixed variables. The `Microsoft.Extensions.Configuration.Ini` package is needed for future dotnet.config support. - -## Configuration Key Mapping - -### DOTNET_ Prefixed Environment Variables - -Only DOTNET_ prefixed environment variables will be included in the unified configuration system: -- `DOTNET_CLI_TELEMETRY_OPTOUT` → `DOTNET_CLI_TELEMETRY_OPTOUT` -- `DOTNET_NOLOGO` → `DOTNET_NOLOGO` -- `DOTNET_CLI_PERF_LOG` → `DOTNET_CLI_PERF_LOG` - -### System Environment Variables - -System-level environment variables without the DOTNET_ prefix (e.g., `PATH`, `HOME`, `TEMP`, `USER`) will continue to be accessed directly through the existing `IEnvironmentProvider` interface and will not be part of the unified configuration system. +**Note:** We will create a custom environment variable provider that only processes DOTNET_ prefixed variables and maps them to canonical keys. The `Microsoft.Extensions.Configuration.Ini` package is needed for future dotnet.config support. -### Global.json Structure +## Key Mapping Reference -Global.json properties will be flattened using colon notation: +### Environment Variables → Canonical Keys +- `DOTNET_CLI_TELEMETRY_OPTOUT` → `dotnet:cli:telemetry:optout` +- `DOTNET_NOLOGO` → `dotnet:cli:nologo` +- `DOTNET_HOST_PATH` → `dotnet:host:path` +- `DOTNET_CLI_PERF_LOG` → `dotnet:cli:perf:log` +- `DOTNET_MULTILEVEL_LOOKUP` → `dotnet:host:multilevel:lookup` +- `DOTNET_ROLL_FORWARD` → `dotnet:runtime:rollforward` +### Global.json → Canonical Keys ```json { "sdk": { @@ -400,9 +503,291 @@ Global.json properties will be flattened using colon notation: } ``` -Maps to: +Maps to canonical keys: - `sdk:version` → `"6.0.100"` -- `msbuild-sdks:Microsoft.Build.Traversal` → `"1.0.0"` +- `msbuild:sdks:Microsoft.Build.Traversal` → `"1.0.0"` + +### INI Configuration → Canonical Keys +```ini +[dotnet.cli] +telemetryOptOut=true +nologo=true + +[sdk] +version=6.0.100 +``` + +Maps to canonical keys: +- `dotnet:cli:telemetry:optout` → `"true"` +- `dotnet:cli:nologo` → `"true"` +- `sdk:version` → `"6.0.100"` + +**Note:** System-level environment variables without the DOTNET_ prefix (e.g., `PATH`, `HOME`, `TEMP`, `USER`) will continue to be accessed directly through the existing `IEnvironmentProvider` interface and will not be part of the unified configuration system. + +## Typed Configuration Models + +### Functional Configuration Groupings + +Based on analysis of the existing codebase, the following functional groupings have been identified for typed configuration models: + +#### 1. **CLI User Experience Configuration** +Settings that control the CLI's user interface and interaction behavior: + +```csharp +public class CliUserExperienceConfiguration +{ + public bool TelemetryOptOut { get; set; } = false; + public bool NoLogo { get; set; } = false; + public bool ForceUtf8Encoding { get; set; } = false; + public string? UILanguage { get; set; } + public string? TelemetryProfile { get; set; } +} +``` + +**Environment Variables Mapped:** +- `DOTNET_CLI_TELEMETRY_OPTOUT` → `TelemetryOptOut` +- `DOTNET_NOLOGO` → `NoLogo` +- `DOTNET_CLI_FORCE_UTF8_ENCODING` → `ForceUtf8Encoding` +- `DOTNET_CLI_UI_LANGUAGE` → `UILanguage` +- `DOTNET_CLI_TELEMETRY_PROFILE` → `TelemetryProfile` + +#### 2. **Runtime Host Configuration** +Settings that control .NET runtime host behavior: + +```csharp +public class RuntimeHostConfiguration +{ + public string? HostPath { get; set; } + public bool MultilevelLookup { get; set; } = true; + public string? RollForward { get; set; } + public string? RollForwardOnNoCandidateFx { get; set; } + public string? Root { get; set; } + public string? RootX86 { get; set; } +} +``` + +**Environment Variables Mapped:** +- `DOTNET_HOST_PATH` → `HostPath` +- `DOTNET_MULTILEVEL_LOOKUP` → `MultilevelLookup` +- `DOTNET_ROLL_FORWARD` → `RollForward` +- `DOTNET_ROLL_FORWARD_ON_NO_CANDIDATE_FX` → `RollForwardOnNoCandidateFx` +- `DOTNET_ROOT` → `Root` +- `DOTNET_ROOT(x86)` → `RootX86` + +#### 3. **Build and MSBuild Configuration** +Settings that control build system behavior: + +```csharp +public class BuildConfiguration +{ + public bool RunMSBuildOutOfProc { get; set; } = false; + public bool UseMSBuildServer { get; set; } = false; + public string? ConfigureMSBuildTerminalLogger { get; set; } + public bool DisablePublishAndPackRelease { get; set; } = false; + public bool LazyPublishAndPackReleaseForSolutions { get; set; } = false; +} +``` + +**Environment Variables Mapped:** +- `DOTNET_CLI_RUN_MSBUILD_OUTOFPROC` → `RunMSBuildOutOfProc` +- `DOTNET_CLI_USE_MSBUILD_SERVER` → `UseMSBuildServer` +- `DOTNET_CLI_CONFIGURE_MSBUILD_TERMINAL_LOGGER` → `ConfigureMSBuildTerminalLogger` +- `DOTNET_CLI_DISABLE_PUBLISH_AND_PACK_RELEASE` → `DisablePublishAndPackRelease` +- `DOTNET_CLI_LAZY_PUBLISH_AND_PACK_RELEASE_FOR_SOLUTIONS` → `LazyPublishAndPackReleaseForSolutions` + +#### 4. **SDK Resolver Configuration** +Settings that control SDK resolution and discovery: + +```csharp +public class SdkResolverConfiguration +{ + public bool EnableLog { get; set; } = false; + public string? SdksDirectory { get; set; } + public string? SdksVersion { get; set; } + public string? CliDirectory { get; set; } +} +``` + +**Environment Variables Mapped:** +- `DOTNET_MSBUILD_SDK_RESOLVER_ENABLE_LOG` → `EnableLog` +- `DOTNET_MSBUILD_SDK_RESOLVER_SDKS_DIR` → `SdksDirectory` +- `DOTNET_MSBUILD_SDK_RESOLVER_SDKS_VER` → `SdksVersion` +- `DOTNET_MSBUILD_SDK_RESOLVER_CLI_DIR` → `CliDirectory` + +#### 5. **Workload Configuration** +Settings that control workload management and behavior: + +```csharp +public class WorkloadConfiguration +{ + public bool UpdateNotifyDisable { get; set; } = false; + public int UpdateNotifyIntervalHours { get; set; } = 24; + public bool DisablePackGroups { get; set; } = false; + public bool SkipIntegrityCheck { get; set; } = false; + public string[]? ManifestRoots { get; set; } + public string[]? PackRoots { get; set; } + public bool ManifestIgnoreDefaultRoots { get; set; } = false; +} +``` + +**Environment Variables Mapped:** +- `DOTNET_CLI_WORKLOAD_UPDATE_NOTIFY_DISABLE` → `UpdateNotifyDisable` +- `DOTNET_CLI_WORKLOAD_UPDATE_NOTIFY_INTERVAL_HOURS` → `UpdateNotifyIntervalHours` +- `DOTNET_CLI_WORKLOAD_DISABLE_PACK_GROUPS` → `DisablePackGroups` +- `DOTNET_SKIP_WORKLOAD_INTEGRITY_CHECK` → `SkipIntegrityCheck` +- `DOTNETSDK_WORKLOAD_MANIFEST_ROOTS` → `ManifestRoots` +- `DOTNETSDK_WORKLOAD_PACK_ROOTS` → `PackRoots` +- `DOTNETSDK_WORKLOAD_MANIFEST_IGNORE_DEFAULT_ROOTS` → `ManifestIgnoreDefaultRoots` + +#### 6. **First Time Use Configuration** +Settings that control first-time user experience setup: + +```csharp +public class FirstTimeUseConfiguration +{ + public bool GenerateAspNetCertificate { get; set; } = true; + public bool AddGlobalToolsToPath { get; set; } = true; + public bool SkipFirstTimeExperience { get; set; } = false; +} +``` + +**Environment Variables Mapped:** +- `DOTNET_GENERATE_ASPNET_CERTIFICATE` → `GenerateAspNetCertificate` +- `DOTNET_ADD_GLOBAL_TOOLS_TO_PATH` → `AddGlobalToolsToPath` +- `DOTNET_SKIP_FIRST_TIME_EXPERIENCE` → `SkipFirstTimeExperience` + +#### 7. **Development and Debugging Configuration** +Settings that control development tools and debugging features: + +```csharp +public class DevelopmentConfiguration +{ + public bool PerfLogEnabled { get; set; } = false; + public string? PerfLogCount { get; set; } + public string? CliHome { get; set; } + public bool ContextVerbose { get; set; } = false; + public bool AllowTargetingPackCaching { get; set; } = false; +} +``` + +**Environment Variables Mapped:** +- `DOTNET_CLI_PERF_LOG` → `PerfLogEnabled` +- `DOTNET_PERF_LOG_COUNT` → `PerfLogCount` +- `DOTNET_CLI_HOME` → `CliHome` +- `DOTNET_CLI_CONTEXT_VERBOSE` → `ContextVerbose` +- `DOTNETSDK_ALLOW_TARGETING_PACK_CACHING` → `AllowTargetingPackCaching` + +#### 8. **Tool and Global Tool Configuration** +Settings that control global tools behavior: + +```csharp +public class ToolConfiguration +{ + public bool AllowManifestInRoot { get; set; } = false; +} +``` + +**Environment Variables Mapped:** +- `DOTNET_TOOLS_ALLOW_MANIFEST_IN_ROOT` → `AllowManifestInRoot` + +### Typed Configuration Service Interface + +```csharp +public interface ITypedConfigurationService +{ + IConfiguration RawConfiguration { get; } + + // Typed configuration access + CliUserExperienceConfiguration CliUserExperience { get; } + RuntimeHostConfiguration RuntimeHost { get; } + BuildConfiguration Build { get; } + SdkResolverConfiguration SdkResolver { get; } + WorkloadConfiguration Workload { get; } + FirstTimeUseConfiguration FirstTimeUse { get; } + DevelopmentConfiguration Development { get; } + ToolConfiguration Tool { get; } + + // Event for configuration changes (if we support hot reload) + event EventHandler? ConfigurationChanged; +} + +public class TypedConfigurationService : ITypedConfigurationService +{ + public IConfiguration RawConfiguration { get; } + + public CliUserExperienceConfiguration CliUserExperience { get; } + public RuntimeHostConfiguration RuntimeHost { get; } + public BuildConfiguration Build { get; } + public SdkResolverConfiguration SdkResolver { get; } + public WorkloadConfiguration Workload { get; } + public FirstTimeUseConfiguration FirstTimeUse { get; } + public DevelopmentConfiguration Development { get; } + public ToolConfiguration Tool { get; } + + public event EventHandler? ConfigurationChanged; + + public TypedConfigurationService(IConfiguration configuration) + { + RawConfiguration = configuration; + + // Bind configuration sections to typed models + CliUserExperience = new CliUserExperienceConfiguration(); + configuration.GetSection("dotnet:cli").Bind(CliUserExperience); + + RuntimeHost = new RuntimeHostConfiguration(); + configuration.GetSection("dotnet:host").Bind(RuntimeHost); + + Build = new BuildConfiguration(); + configuration.GetSection("dotnet:build").Bind(Build); + + SdkResolver = new SdkResolverConfiguration(); + configuration.GetSection("dotnet:sdkresolver").Bind(SdkResolver); + + Workload = new WorkloadConfiguration(); + configuration.GetSection("dotnet:workload").Bind(Workload); + + FirstTimeUse = new FirstTimeUseConfiguration(); + configuration.GetSection("dotnet:firsttime").Bind(FirstTimeUse); + + Development = new DevelopmentConfiguration(); + configuration.GetSection("dotnet:development").Bind(Development); + + Tool = new ToolConfiguration(); + configuration.GetSection("dotnet:tool").Bind(Tool); + } +} +``` + +### Benefits of Typed Configuration Models + +1. **Intellisense and Compile-time Safety**: Developers get full IDE support and compile-time checking +2. **Logical Grouping**: Related settings are grouped together functionally +3. **Type Safety**: Boolean values are actual booleans, integers are integers, etc. +4. **Default Values**: Clear default values are defined in the model classes +5. **Discoverability**: Developers can explore configuration options through the object model +6. **Validation**: Can add data annotations for validation +7. **Documentation**: Each property can have XML documentation + +### Usage Examples + +```csharp +// Instead of: +bool telemetryOptOut = configService.GetBoolValue("dotnet:cli:telemetry:optout", false); +bool noLogo = configService.GetBoolValue("dotnet:cli:nologo", false); + +// Developers can write: +var cliConfig = typedConfigService.CliUserExperience; +bool telemetryOptOut = cliConfig.TelemetryOptOut; +bool noLogo = cliConfig.NoLogo; + +// Or for the common case: +if (typedConfigService.CliUserExperience.TelemetryOptOut) +{ + // Skip telemetry +} +``` + +This approach eliminates the need to remember canonical key names and provides a much more developer-friendly API. ## Error Handling @@ -446,21 +831,16 @@ This refactoring should not introduce any breaking changes as it maintains backw - All existing global.json file structures and locations - All existing APIs and interfaces (through adapter patterns) -## Implementation Timeline - -1. **Phase 1** (Infrastructure): 1-2 weeks -2. **Phase 2** (Integration): 1 week -3. **Phase 3** (Migration): 2-3 weeks (incremental, can be spread across multiple PRs) -4. **Phase 4** (Testing): 1 week - -Total estimated effort: 5-7 weeks ## Success Criteria - [ ] All direct `Environment.GetEnvironmentVariable()` calls for DOTNET_ prefixed variables replaced - [ ] All direct global.json reading replaced with configuration providers - [ ] System environment variables continue to work through existing providers -- [ ] Comprehensive test coverage for new configuration system +- [ ] **Key mapping system implemented and tested for all configuration sources** +- [ ] **Canonical key format consistently used throughout the codebase** +- [ ] **Helper methods available for common configuration lookups** +- [ ] Comprehensive test coverage for new configuration system including key mapping - [ ] All existing functionality preserved (backward compatibility) - [ ] Performance impact is negligible -- [ ] Documentation updated to reflect new configuration system +- [ ] Documentation updated to reflect new configuration system and canonical key format From 275443f150367f511e7d306f933658c60023792f Mon Sep 17 00:00:00 2001 From: Chet Husk Date: Sun, 13 Jul 2025 11:28:17 -0500 Subject: [PATCH 03/12] lazy acces, AOT --- documentation/specs/unified-configuration.md | 385 +++++++++++++++---- 1 file changed, 300 insertions(+), 85 deletions(-) diff --git a/documentation/specs/unified-configuration.md b/documentation/specs/unified-configuration.md index 18e078f9dc7b..b64981e0900b 100644 --- a/documentation/specs/unified-configuration.md +++ b/documentation/specs/unified-configuration.md @@ -35,11 +35,14 @@ The new unified configuration system will follow this precedence order (highest ### Phase 1: Infrastructure -#### 1.1 Core Configuration Builder with Key Mapping +#### 1.1 Core Configuration Builder with Strongly-Typed Configuration -Create a centralized configuration builder that consolidates all configuration sources with key mapping: +Create a centralized configuration builder in the Microsoft.Extensions.Configuration.DotnetCli project: ```csharp +// src/Microsoft.Extensions.Configuration.DotnetCli/Services/DotNetConfiguration.cs +namespace Microsoft.Extensions.Configuration.DotnetCli.Services; + public class DotNetConfiguration { public static IConfiguration Create(string workingDirectory = null) @@ -69,14 +72,41 @@ public class DotNetConfiguration return builder.Build(); } + + public static DotNetConfigurationRoot CreateTyped(string workingDirectory = null) + { + var configuration = Create(workingDirectory); + return new DotNetConfigurationRoot(configuration); + } + + // Lightweight factory for scenarios that only need basic configuration access + public static DotNetConfigurationRoot CreateMinimal(string workingDirectory = null) + { + var builder = new ConfigurationBuilder(); + workingDirectory ??= Directory.GetCurrentDirectory(); + + // Only add environment variables for minimal overhead + builder.Add(new DotNetEnvironmentConfigurationSource()); + + var configuration = builder.Build(); + return new DotNetConfigurationRoot(configuration); + } } ``` +**Performance Considerations:** +- **Lazy Initialization**: All strongly-typed configuration properties use `Lazy` to defer expensive binding operations until first access +- **Minimal Factory**: `CreateMinimal()` provides a lightweight option that only loads environment variables +- **Provider Ordering**: Most expensive providers (global.json file I/O) are added last to minimize impact when not needed + #### 1.2 Enhanced Global.json Configuration Provider Create a custom configuration provider for global.json files with key mapping: ```csharp +// src/Microsoft.Extensions.Configuration.DotnetCli/Providers/GlobalJsonConfigurationProvider.cs +namespace Microsoft.Extensions.Configuration.DotnetCli.Providers; + public class GlobalJsonConfigurationProvider : ConfigurationProvider { private readonly string _path; @@ -186,6 +216,9 @@ public class GlobalJsonConfigurationProvider : ConfigurationProvider Create a custom environment variable provider that maps DOTNET_ variables to canonical keys: ```csharp +// src/Microsoft.Extensions.Configuration.DotnetCli/Providers/DotNetEnvironmentConfigurationProvider.cs +namespace Microsoft.Extensions.Configuration.DotnetCli.Providers; + public class DotNetEnvironmentConfigurationProvider : ConfigurationProvider { private static readonly Dictionary EnvironmentKeyMappings = new() @@ -228,40 +261,77 @@ public class DotNetEnvironmentConfigurationSource : IConfigurationSource } ``` -#### 1.4 Updated Configuration Service with Key Lookup Helper +#### 1.4 Strongly-Typed Configuration Root with Lazy Initialization -Update the configuration service to provide helper methods for common lookups: +Create a strongly-typed configuration service that uses lazy initialization and the configuration binding source generator: ```csharp -public interface IConfigurationService -{ - IConfiguration Configuration { get; } +// src/Microsoft.Extensions.Configuration.DotnetCli/Services/DotNetConfigurationRoot.cs +namespace Microsoft.Extensions.Configuration.DotnetCli.Services; - // Generic value access using canonical keys - string GetValue(string canonicalKey, string defaultValue = null); - T GetValue(string canonicalKey, T defaultValue = default); - bool GetBoolValue(string canonicalKey, bool defaultValue = false); +using Microsoft.Extensions.Configuration.DotnetCli.Models; - // Helper methods for common configuration values - bool IsTelemetryOptOut(); - bool IsNoLogo(); - string GetHostPath(); - string GetSdkVersion(); -} - -public class ConfigurationService : IConfigurationService +public class DotNetConfigurationRoot { - public IConfiguration Configuration { get; } - - public ConfigurationService(IConfiguration configuration) + private readonly IConfiguration _configuration; + + // Lazy initialization for each configuration section + private readonly Lazy _cliUserExperience; + private readonly Lazy _runtimeHost; + private readonly Lazy _build; + private readonly Lazy _sdkResolver; + private readonly Lazy _workload; + private readonly Lazy _firstTimeUse; + private readonly Lazy _development; + private readonly Lazy _tool; + + public DotNetConfigurationRoot(IConfiguration configuration) { - Configuration = configuration ?? throw new ArgumentNullException(nameof(configuration)); + _configuration = configuration ?? throw new ArgumentNullException(nameof(configuration)); + + // Initialize lazy factories - configuration binding only happens on first access + _cliUserExperience = new Lazy(() => + GetConfigurationSection("CliUserExperience") ?? new()); + _runtimeHost = new Lazy(() => + GetConfigurationSection("RuntimeHost") ?? new()); + _build = new Lazy(() => + GetConfigurationSection("Build") ?? new()); + _sdkResolver = new Lazy(() => + GetConfigurationSection("SdkResolver") ?? new()); + _workload = new Lazy(() => + GetConfigurationSection("Workload") ?? new()); + _firstTimeUse = new Lazy(() => + GetConfigurationSection("FirstTimeUse") ?? new()); + _development = new Lazy(() => + GetConfigurationSection("Development") ?? new()); + _tool = new Lazy(() => + GetConfigurationSection("Tool") ?? new()); } - public string GetValue(string canonicalKey, string defaultValue = null) + public IConfiguration RawConfiguration => _configuration; + + // Lazy-loaded strongly-typed configuration properties + public CliUserExperienceConfiguration CliUserExperience => _cliUserExperience.Value; + public RuntimeHostConfiguration RuntimeHost => _runtimeHost.Value; + public BuildConfiguration Build => _build.Value; + public SdkResolverConfiguration SdkResolver => _sdkResolver.Value; + public WorkloadConfiguration Workload => _workload.Value; + public FirstTimeUseConfiguration FirstTimeUse => _firstTimeUse.Value; + public DevelopmentConfiguration Development => _development.Value; + public ToolConfiguration Tool => _tool.Value; + + // Generic value access for backward compatibility + public string? GetValue(string key, string? defaultValue = null) => _configuration[key] ?? defaultValue; + public T GetValue(string key, T defaultValue = default) => _configuration.GetValue(key, defaultValue); + + private T? GetConfigurationSection(string sectionName) where T : class, new() { - return Configuration[canonicalKey] ?? defaultValue; + var section = _configuration.GetSection(sectionName); + // Uses configuration binding source generator for AOT-friendly binding + return section.Exists() ? section.Get() : null; } +} +``` public T GetValue(string canonicalKey, T defaultValue = default) { @@ -479,7 +549,71 @@ The following NuGet package references will need to be added to relevant project - `Microsoft.Extensions.Configuration.Ini` - `Microsoft.Extensions.Configuration.Binder` -**Note:** We will create a custom environment variable provider that only processes DOTNET_ prefixed variables and maps them to canonical keys. The `Microsoft.Extensions.Configuration.Ini` package is needed for future dotnet.config support. +### Microsoft.Extensions.Configuration.DotnetCli Project + +Create a new project `src/Microsoft.Extensions.Configuration.DotnetCli/Microsoft.Extensions.Configuration.DotnetCli.csproj`: + +```xml + + + + net8.0 + 12.0 + enable + true + false + + + + + + + + + + + +``` + +**Key Features:** +- **Configuration Binding Source Generator**: `EnableConfigurationBindingGenerator=true` enables AOT and trim-friendly configuration binding +- **Strongly-typed Configuration**: Uses `ConfigurationBinder.Get()` for type-safe configuration access +- **.NET 8+ Features**: Takes advantage of C# 12 interceptors for source generation + +**Note:** The configuration binding source generator provides Native AOT and trim-friendly configuration binding without reflection. This is essential for performance and compatibility with .NET's trimming and AOT compilation features. + +### Project Structure + +The Microsoft.Extensions.Configuration.DotnetCli project will be organized as follows: + +``` +src/Microsoft.Extensions.Configuration.DotnetCli/ +├── Microsoft.Extensions.Configuration.DotnetCli.csproj +├── Models/ +│ ├── CliUserExperienceConfiguration.cs +│ ├── RuntimeHostConfiguration.cs +│ ├── BuildConfiguration.cs +│ ├── SdkResolverConfiguration.cs +│ ├── WorkloadConfiguration.cs +│ ├── FirstTimeUseConfiguration.cs +│ ├── DevelopmentConfiguration.cs +│ └── ToolConfiguration.cs +├── Providers/ +│ ├── GlobalJsonConfigurationProvider.cs +│ ├── GlobalJsonConfigurationSource.cs +│ ├── DotNetEnvironmentConfigurationProvider.cs +│ └── DotNetEnvironmentConfigurationSource.cs +└── Services/ + ├── DotNetConfiguration.cs + ├── DotNetConfigurationRoot.cs + ├── IDotNetConfigurationService.cs + └── DotNetConfigurationService.cs +``` + +This structure separates concerns into logical groupings: +- **Models/**: Strongly-typed configuration model classes optimized for source generator +- **Providers/**: Custom configuration providers for global.json and DOTNET_ environment variables +- **Services/**: Main configuration services and builders for consumer applications ## Key Mapping Reference @@ -526,6 +660,10 @@ Maps to canonical keys: ## Typed Configuration Models +### Configuration Binding Source Generator Compatibility + +All configuration model classes are designed to work with the configuration binding source generator, providing AOT and trim-friendly configuration binding through `ConfigurationBinder.Get()` calls. + ### Functional Configuration Groupings Based on analysis of the existing codebase, the following functional groupings have been identified for typed configuration models: @@ -534,7 +672,10 @@ Based on analysis of the existing codebase, the following functional groupings h Settings that control the CLI's user interface and interaction behavior: ```csharp -public class CliUserExperienceConfiguration +// src/Microsoft.Extensions.Configuration.DotnetCli/Models/CliUserExperienceConfiguration.cs +namespace Microsoft.Extensions.Configuration.DotnetCli.Models; + +public sealed class CliUserExperienceConfiguration { public bool TelemetryOptOut { get; set; } = false; public bool NoLogo { get; set; } = false; @@ -555,7 +696,10 @@ public class CliUserExperienceConfiguration Settings that control .NET runtime host behavior: ```csharp -public class RuntimeHostConfiguration +// src/Microsoft.Extensions.Configuration.DotnetCli/Models/RuntimeHostConfiguration.cs +namespace Microsoft.Extensions.Configuration.DotnetCli.Models; + +public sealed class RuntimeHostConfiguration { public string? HostPath { get; set; } public bool MultilevelLookup { get; set; } = true; @@ -578,7 +722,10 @@ public class RuntimeHostConfiguration Settings that control build system behavior: ```csharp -public class BuildConfiguration +// src/Microsoft.Extensions.Configuration.DotnetCli/Models/BuildConfiguration.cs +namespace Microsoft.Extensions.Configuration.DotnetCli.Models; + +public sealed class BuildConfiguration { public bool RunMSBuildOutOfProc { get; set; } = false; public bool UseMSBuildServer { get; set; } = false; @@ -599,7 +746,10 @@ public class BuildConfiguration Settings that control SDK resolution and discovery: ```csharp -public class SdkResolverConfiguration +// src/Microsoft.Extensions.Configuration.DotnetCli/Models/SdkResolverConfiguration.cs +namespace Microsoft.Extensions.Configuration.DotnetCli.Models; + +public sealed class SdkResolverConfiguration { public bool EnableLog { get; set; } = false; public string? SdksDirectory { get; set; } @@ -618,7 +768,10 @@ public class SdkResolverConfiguration Settings that control workload management and behavior: ```csharp -public class WorkloadConfiguration +// src/Microsoft.Extensions.Configuration.DotnetCli/Models/WorkloadConfiguration.cs +namespace Microsoft.Extensions.Configuration.DotnetCli.Models; + +public sealed class WorkloadConfiguration { public bool UpdateNotifyDisable { get; set; } = false; public int UpdateNotifyIntervalHours { get; set; } = 24; @@ -643,7 +796,10 @@ public class WorkloadConfiguration Settings that control first-time user experience setup: ```csharp -public class FirstTimeUseConfiguration +// src/Microsoft.Extensions.Configuration.DotnetCli/Models/FirstTimeUseConfiguration.cs +namespace Microsoft.Extensions.Configuration.DotnetCli.Models; + +public sealed class FirstTimeUseConfiguration { public bool GenerateAspNetCertificate { get; set; } = true; public bool AddGlobalToolsToPath { get; set; } = true; @@ -660,7 +816,10 @@ public class FirstTimeUseConfiguration Settings that control development tools and debugging features: ```csharp -public class DevelopmentConfiguration +// src/Microsoft.Extensions.Configuration.DotnetCli/Models/DevelopmentConfiguration.cs +namespace Microsoft.Extensions.Configuration.DotnetCli.Models; + +public sealed class DevelopmentConfiguration { public bool PerfLogEnabled { get; set; } = false; public string? PerfLogCount { get; set; } @@ -681,7 +840,10 @@ public class DevelopmentConfiguration Settings that control global tools behavior: ```csharp -public class ToolConfiguration +// src/Microsoft.Extensions.Configuration.DotnetCli/Models/ToolConfiguration.cs +namespace Microsoft.Extensions.Configuration.DotnetCli.Models; + +public sealed class ToolConfiguration { public bool AllowManifestInRoot { get; set; } = false; } @@ -690,14 +852,19 @@ public class ToolConfiguration **Environment Variables Mapped:** - `DOTNET_TOOLS_ALLOW_MANIFEST_IN_ROOT` → `AllowManifestInRoot` -### Typed Configuration Service Interface +### Strongly-Typed Configuration Service with Source Generator ```csharp -public interface ITypedConfigurationService +// src/Microsoft.Extensions.Configuration.DotnetCli/Services/IDotNetConfigurationService.cs +namespace Microsoft.Extensions.Configuration.DotnetCli.Services; + +using Microsoft.Extensions.Configuration.DotnetCli.Models; + +public interface IDotNetConfigurationService { IConfiguration RawConfiguration { get; } - // Typed configuration access + // Strongly-typed configuration access CliUserExperienceConfiguration CliUserExperience { get; } RuntimeHostConfiguration RuntimeHost { get; } BuildConfiguration Build { get; } @@ -706,33 +873,59 @@ public interface ITypedConfigurationService FirstTimeUseConfiguration FirstTimeUse { get; } DevelopmentConfiguration Development { get; } ToolConfiguration Tool { get; } - - // Event for configuration changes (if we support hot reload) - event EventHandler? ConfigurationChanged; } -public class TypedConfigurationService : ITypedConfigurationService +// src/Microsoft.Extensions.Configuration.DotnetCli/Services/DotNetConfigurationService.cs +public class DotNetConfigurationService : IDotNetConfigurationService { - public IConfiguration RawConfiguration { get; } - - public CliUserExperienceConfiguration CliUserExperience { get; } - public RuntimeHostConfiguration RuntimeHost { get; } - public BuildConfiguration Build { get; } - public SdkResolverConfiguration SdkResolver { get; } - public WorkloadConfiguration Workload { get; } - public FirstTimeUseConfiguration FirstTimeUse { get; } - public DevelopmentConfiguration Development { get; } - public ToolConfiguration Tool { get; } - - public event EventHandler? ConfigurationChanged; - - public TypedConfigurationService(IConfiguration configuration) + private readonly IConfiguration _configuration; + + // Lazy initialization for each configuration section + private readonly Lazy _cliUserExperience; + private readonly Lazy _runtimeHost; + private readonly Lazy _build; + private readonly Lazy _sdkResolver; + private readonly Lazy _workload; + private readonly Lazy _firstTimeUse; + private readonly Lazy _development; + private readonly Lazy _tool; + + public IConfiguration RawConfiguration => _configuration; + + // Lazy-loaded strongly-typed configuration properties + public CliUserExperienceConfiguration CliUserExperience => _cliUserExperience.Value; + public RuntimeHostConfiguration RuntimeHost => _runtimeHost.Value; + public BuildConfiguration Build => _build.Value; + public SdkResolverConfiguration SdkResolver => _sdkResolver.Value; + public WorkloadConfiguration Workload => _workload.Value; + public FirstTimeUseConfiguration FirstTimeUse => _firstTimeUse.Value; + public DevelopmentConfiguration Development => _development.Value; + public ToolConfiguration Tool => _tool.Value; + + public DotNetConfigurationService(IConfiguration configuration) { - RawConfiguration = configuration; - - // Bind configuration sections to typed models - CliUserExperience = new CliUserExperienceConfiguration(); - configuration.GetSection("dotnet:cli").Bind(CliUserExperience); + _configuration = configuration ?? throw new ArgumentNullException(nameof(configuration)); + + // Initialize lazy factories - configuration binding only happens on first access + _cliUserExperience = new Lazy(() => + _configuration.GetSection("CliUserExperience").Get() ?? new()); + _runtimeHost = new Lazy(() => + _configuration.GetSection("RuntimeHost").Get() ?? new()); + _build = new Lazy(() => + _configuration.GetSection("Build").Get() ?? new()); + _sdkResolver = new Lazy(() => + _configuration.GetSection("SdkResolver").Get() ?? new()); + _workload = new Lazy(() => + _configuration.GetSection("Workload").Get() ?? new()); + _firstTimeUse = new Lazy(() => + _configuration.GetSection("FirstTimeUse").Get() ?? new()); + _development = new Lazy(() => + _configuration.GetSection("Development").Get() ?? new()); + _tool = new Lazy(() => + _configuration.GetSection("Tool").Get() ?? new()); + } +} +``` RuntimeHost = new RuntimeHostConfiguration(); configuration.GetSection("dotnet:host").Bind(RuntimeHost); @@ -758,33 +951,49 @@ public class TypedConfigurationService : ITypedConfigurationService } ``` -### Benefits of Typed Configuration Models +### Benefits of Strongly-Typed Configuration with Lazy Initialization -1. **Intellisense and Compile-time Safety**: Developers get full IDE support and compile-time checking -2. **Logical Grouping**: Related settings are grouped together functionally -3. **Type Safety**: Boolean values are actual booleans, integers are integers, etc. -4. **Default Values**: Clear default values are defined in the model classes -5. **Discoverability**: Developers can explore configuration options through the object model -6. **Validation**: Can add data annotations for validation -7. **Documentation**: Each property can have XML documentation +1. **Minimal Startup Cost**: Lazy initialization means configuration binding only happens when properties are actually accessed +2. **Native AOT and Trim-Friendly**: Source generator eliminates reflection, making the code compatible with Native AOT compilation and trimming +3. **Performance**: Generated code is faster than reflection-based binding at runtime, and lazy loading reduces unnecessary work +4. **Memory Efficiency**: Configuration sections are only allocated when needed, reducing memory pressure during startup +5. **Intellisense and Compile-time Safety**: Developers get full IDE support and compile-time checking +6. **Logical Grouping**: Related settings are grouped together functionally +7. **Type Safety**: Boolean values are actual booleans, integers are integers, etc. +8. **Default Values**: Clear default values are defined in the model classes +9. **Discoverability**: Developers can explore configuration options through the object model +10. **Validation**: Can add data annotations for validation +11. **Documentation**: Each property can have XML documentation +12. **Source Generation**: Using `ConfigurationBinder.Get()` with `EnableConfigurationBindingGenerator=true` generates efficient binding code -### Usage Examples +### Usage Examples with Lazy Initialization ```csharp -// Instead of: -bool telemetryOptOut = configService.GetBoolValue("dotnet:cli:telemetry:optout", false); -bool noLogo = configService.GetBoolValue("dotnet:cli:nologo", false); +// Fast startup - configuration service creation is very lightweight +var config = DotNetConfiguration.CreateTyped(); + +// First access triggers lazy binding of only the CliUserExperience section +if (config.CliUserExperience.TelemetryOptOut) +{ + // Skip telemetry - only this section is bound, others remain uninitialized +} -// Developers can write: -var cliConfig = typedConfigService.CliUserExperience; -bool telemetryOptOut = cliConfig.TelemetryOptOut; -bool noLogo = cliConfig.NoLogo; +// Subsequent access to the same section is fast (cached) +bool noLogo = config.CliUserExperience.NoLogo; -// Or for the common case: -if (typedConfigService.CliUserExperience.TelemetryOptOut) +// Other sections are only bound when first accessed +string? hostPath = config.RuntimeHost.HostPath; // RuntimeHost section bound here +bool enableLogs = config.SdkResolver.EnableLog; // SdkResolver section bound here + +// For scenarios that only need environment variables (fastest startup): +var minimalConfig = DotNetConfiguration.CreateMinimal(); +if (minimalConfig.CliUserExperience.TelemetryOptOut) { - // Skip telemetry + // Only environment variable provider was initialized } + +// Instead of error-prone string-based access: +// bool telemetryOptOut = configService.GetBoolValue("dotnet:cli:telemetry:optout", false); ``` This approach eliminates the need to remember canonical key names and provides a much more developer-friendly API. @@ -834,13 +1043,19 @@ This refactoring should not introduce any breaking changes as it maintains backw ## Success Criteria +- [ ] **Microsoft.Extensions.Configuration.DotnetCli project created with proper structure** +- [ ] **Configuration binding source generator enabled (`EnableConfigurationBindingGenerator=true`)** +- [ ] **All configuration models are `sealed` classes optimized for source generator** +- [ ] **Lazy initialization implemented for all configuration sections** +- [ ] **Unused configuration sections have zero runtime cost** +- [ ] **Strongly-typed configuration binding using `ConfigurationBinder.Get()` throughout** - [ ] All direct `Environment.GetEnvironmentVariable()` calls for DOTNET_ prefixed variables replaced - [ ] All direct global.json reading replaced with configuration providers - [ ] System environment variables continue to work through existing providers -- [ ] **Key mapping system implemented and tested for all configuration sources** -- [ ] **Canonical key format consistently used throughout the codebase** -- [ ] **Helper methods available for common configuration lookups** -- [ ] Comprehensive test coverage for new configuration system including key mapping +- [ ] **Functional grouping of configuration settings implemented** +- [ ] **Type-safe configuration access with compile-time checking** +- [ ] **AOT and trim-friendly configuration system (no reflection)** +- [ ] Comprehensive test coverage for new configuration system including source generator scenarios - [ ] All existing functionality preserved (backward compatibility) -- [ ] Performance impact is negligible -- [ ] Documentation updated to reflect new configuration system and canonical key format +- [ ] **Performance improved due to lazy loading and source generation** +- [ ] Documentation updated to reflect strongly-typed configuration system From 04e2e9772eaa01b4d44a6c5f643d3167f0807562 Mon Sep 17 00:00:00 2001 From: Chet Husk Date: Sun, 13 Jul 2025 11:44:11 -0500 Subject: [PATCH 04/12] initial impl and hookup --- Directory.Packages.props | 4 + eng/Versions.props | 4 + sdk.slnx | 1 + .../ConfigurationBasedEnvironmentProvider.cs | 139 ++++++++++++++++++ .../DotNetConfigurationFactory.cs | 59 ++++++++ src/Cli/dotnet/Program.cs | 5 +- src/Cli/dotnet/dotnet.csproj | 1 + ....Extensions.Configuration.DotnetCli.csproj | 18 +++ .../Models/BuildConfiguration.cs | 40 +++++ .../Models/CliUserExperienceConfiguration.cs | 40 +++++ .../Models/DevelopmentConfiguration.cs | 40 +++++ .../Models/FirstTimeUseConfiguration.cs | 28 ++++ .../Models/RuntimeHostConfiguration.cs | 46 ++++++ .../Models/SdkResolverConfiguration.cs | 34 +++++ .../Models/ToolConfiguration.cs | 16 ++ .../Models/WorkloadConfiguration.cs | 52 +++++++ .../DotNetEnvironmentConfigurationProvider.cs | 81 ++++++++++ .../DotNetEnvironmentConfigurationSource.cs | 15 ++ .../GlobalJsonConfigurationProvider.cs | 112 ++++++++++++++ .../GlobalJsonConfigurationSource.cs | 22 +++ .../Services/DotNetConfiguration.cs | 73 +++++++++ .../Services/DotNetConfigurationService.cs | 59 ++++++++ .../Services/IDotNetConfigurationService.cs | 57 +++++++ 23 files changed, 945 insertions(+), 1 deletion(-) create mode 100644 src/Cli/dotnet/Configuration/ConfigurationBasedEnvironmentProvider.cs create mode 100644 src/Cli/dotnet/Configuration/DotNetConfigurationFactory.cs create mode 100644 src/Microsoft.Extensions.Configuration.DotnetCli/Microsoft.Extensions.Configuration.DotnetCli.csproj create mode 100644 src/Microsoft.Extensions.Configuration.DotnetCli/Models/BuildConfiguration.cs create mode 100644 src/Microsoft.Extensions.Configuration.DotnetCli/Models/CliUserExperienceConfiguration.cs create mode 100644 src/Microsoft.Extensions.Configuration.DotnetCli/Models/DevelopmentConfiguration.cs create mode 100644 src/Microsoft.Extensions.Configuration.DotnetCli/Models/FirstTimeUseConfiguration.cs create mode 100644 src/Microsoft.Extensions.Configuration.DotnetCli/Models/RuntimeHostConfiguration.cs create mode 100644 src/Microsoft.Extensions.Configuration.DotnetCli/Models/SdkResolverConfiguration.cs create mode 100644 src/Microsoft.Extensions.Configuration.DotnetCli/Models/ToolConfiguration.cs create mode 100644 src/Microsoft.Extensions.Configuration.DotnetCli/Models/WorkloadConfiguration.cs create mode 100644 src/Microsoft.Extensions.Configuration.DotnetCli/Providers/DotNetEnvironmentConfigurationProvider.cs create mode 100644 src/Microsoft.Extensions.Configuration.DotnetCli/Providers/DotNetEnvironmentConfigurationSource.cs create mode 100644 src/Microsoft.Extensions.Configuration.DotnetCli/Providers/GlobalJsonConfigurationProvider.cs create mode 100644 src/Microsoft.Extensions.Configuration.DotnetCli/Providers/GlobalJsonConfigurationSource.cs create mode 100644 src/Microsoft.Extensions.Configuration.DotnetCli/Services/DotNetConfiguration.cs create mode 100644 src/Microsoft.Extensions.Configuration.DotnetCli/Services/DotNetConfigurationService.cs create mode 100644 src/Microsoft.Extensions.Configuration.DotnetCli/Services/IDotNetConfigurationService.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index 104dedfd9eaf..e10167b6516c 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -44,6 +44,10 @@ + + + + diff --git a/eng/Versions.props b/eng/Versions.props index 99e97cedaae8..0282cc1656a5 100644 --- a/eng/Versions.props +++ b/eng/Versions.props @@ -103,6 +103,10 @@ 10.0.0-preview.7.25359.101 10.0.0-preview.7.25359.101 10.0.0-preview.7.25359.101 + 10.0.0-preview.7.25359.101 + 10.0.0-preview.7.25359.101 + 10.0.0-preview.7.25359.101 + 10.0.0-preview.7.25359.101 10.0.0-preview.7.25359.101 10.0.0-preview.7.25359.101 10.0.0-preview.7.25359.101 diff --git a/sdk.slnx b/sdk.slnx index 09939f5d6b46..99cb8e704055 100644 --- a/sdk.slnx +++ b/sdk.slnx @@ -27,6 +27,7 @@ + diff --git a/src/Cli/dotnet/Configuration/ConfigurationBasedEnvironmentProvider.cs b/src/Cli/dotnet/Configuration/ConfigurationBasedEnvironmentProvider.cs new file mode 100644 index 000000000000..22f23fc866fc --- /dev/null +++ b/src/Cli/dotnet/Configuration/ConfigurationBasedEnvironmentProvider.cs @@ -0,0 +1,139 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System; +using System.Collections.Generic; +using Microsoft.DotNet.Cli.Utils; +using Microsoft.Extensions.Configuration.DotnetCli.Services; + +namespace Microsoft.DotNet.Cli.Configuration +{ + /// + /// Bridge between the new configuration system and existing IEnvironmentProvider interface. + /// Provides backward compatibility while enabling migration to the new configuration system. + /// + public class ConfigurationBasedEnvironmentProvider : IEnvironmentProvider + { + private readonly IDotNetConfigurationService _configurationService; + private readonly IEnvironmentProvider _fallbackProvider; + + // Reverse mapping from environment variable names to canonical keys + private static readonly Dictionary EnvironmentToCanonicalMappings = new() + { + ["DOTNET_HOST_PATH"] = "RuntimeHost:HostPath", + ["DOTNET_CLI_TELEMETRY_OPTOUT"] = "CliUserExperience:TelemetryOptOut", + ["DOTNET_NOLOGO"] = "CliUserExperience:NoLogo", + ["DOTNET_CLI_FORCE_UTF8_ENCODING"] = "CliUserExperience:ForceUtf8Encoding", + ["DOTNET_CLI_UI_LANGUAGE"] = "CliUserExperience:UILanguage", + ["DOTNET_CLI_TELEMETRY_PROFILE"] = "CliUserExperience:TelemetryProfile", + ["DOTNET_CLI_PERF_LOG"] = "Development:PerfLogEnabled", + ["DOTNET_PERF_LOG_COUNT"] = "Development:PerfLogCount", + ["DOTNET_CLI_HOME"] = "Development:CliHome", + ["DOTNET_CLI_CONTEXT_VERBOSE"] = "Development:ContextVerbose", + ["DOTNET_MULTILEVEL_LOOKUP"] = "RuntimeHost:MultilevelLookup", + ["DOTNET_ROLL_FORWARD"] = "RuntimeHost:RollForward", + ["DOTNET_ROLL_FORWARD_ON_NO_CANDIDATE_FX"] = "RuntimeHost:RollForwardOnNoCandidateFx", + ["DOTNET_ROOT"] = "RuntimeHost:Root", + ["DOTNET_ROOT(x86)"] = "RuntimeHost:RootX86", + ["DOTNET_CLI_RUN_MSBUILD_OUTOFPROC"] = "Build:RunMSBuildOutOfProc", + ["DOTNET_CLI_USE_MSBUILD_SERVER"] = "Build:UseMSBuildServer", + ["DOTNET_CLI_CONFIGURE_MSBUILD_TERMINAL_LOGGER"] = "Build:ConfigureMSBuildTerminalLogger", + ["DOTNET_CLI_DISABLE_PUBLISH_AND_PACK_RELEASE"] = "Build:DisablePublishAndPackRelease", + ["DOTNET_CLI_LAZY_PUBLISH_AND_PACK_RELEASE_FOR_SOLUTIONS"] = "Build:LazyPublishAndPackReleaseForSolutions", + ["DOTNET_MSBUILD_SDK_RESOLVER_ENABLE_LOG"] = "SdkResolver:EnableLog", + ["DOTNET_MSBUILD_SDK_RESOLVER_SDKS_DIR"] = "SdkResolver:SdksDirectory", + ["DOTNET_MSBUILD_SDK_RESOLVER_SDKS_VER"] = "SdkResolver:SdksVersion", + ["DOTNET_MSBUILD_SDK_RESOLVER_CLI_DIR"] = "SdkResolver:CliDirectory", + ["DOTNET_CLI_WORKLOAD_UPDATE_NOTIFY_DISABLE"] = "Workload:UpdateNotifyDisable", + ["DOTNET_CLI_WORKLOAD_UPDATE_NOTIFY_INTERVAL_HOURS"] = "Workload:UpdateNotifyIntervalHours", + ["DOTNET_CLI_WORKLOAD_DISABLE_PACK_GROUPS"] = "Workload:DisablePackGroups", + ["DOTNET_SKIP_WORKLOAD_INTEGRITY_CHECK"] = "Workload:SkipIntegrityCheck", + ["DOTNETSDK_WORKLOAD_MANIFEST_ROOTS"] = "Workload:ManifestRoots", + ["DOTNETSDK_WORKLOAD_PACK_ROOTS"] = "Workload:PackRoots", + ["DOTNETSDK_WORKLOAD_MANIFEST_IGNORE_DEFAULT_ROOTS"] = "Workload:ManifestIgnoreDefaultRoots", + ["DOTNETSDK_ALLOW_TARGETING_PACK_CACHING"] = "Development:AllowTargetingPackCaching", + ["DOTNET_GENERATE_ASPNET_CERTIFICATE"] = "FirstTimeUse:GenerateAspNetCertificate", + ["DOTNET_ADD_GLOBAL_TOOLS_TO_PATH"] = "FirstTimeUse:AddGlobalToolsToPath", + ["DOTNET_SKIP_FIRST_TIME_EXPERIENCE"] = "FirstTimeUse:SkipFirstTimeExperience", + ["DOTNET_TOOLS_ALLOW_MANIFEST_IN_ROOT"] = "Tool:AllowManifestInRoot", + }; + + public ConfigurationBasedEnvironmentProvider( + IDotNetConfigurationService configurationService, + IEnvironmentProvider? fallbackProvider = null) + { + _configurationService = configurationService ?? throw new ArgumentNullException(nameof(configurationService)); + _fallbackProvider = fallbackProvider ?? new EnvironmentProvider(); + } + + public IEnumerable ExecutableExtensions => _fallbackProvider.ExecutableExtensions; + + public string? GetCommandPath(string commandName, params string[] extensions) + { + return _fallbackProvider.GetCommandPath(commandName, extensions); + } + + public string? GetCommandPathFromRootPath(string rootPath, string commandName, params string[] extensions) + { + return _fallbackProvider.GetCommandPathFromRootPath(rootPath, commandName, extensions); + } + + public string? GetCommandPathFromRootPath(string rootPath, string commandName, IEnumerable extensions) + { + return _fallbackProvider.GetCommandPathFromRootPath(rootPath, commandName, extensions); + } + + public string? GetEnvironmentVariable(string name) + { + // First try to get from the new configuration system + if (EnvironmentToCanonicalMappings.TryGetValue(name, out var canonicalKey)) + { + var value = _configurationService.RawConfiguration[canonicalKey]; + if (!string.IsNullOrEmpty(value)) + { + return value; + } + } + + // Fall back to direct environment variable access + return _fallbackProvider.GetEnvironmentVariable(name); + } + + public bool GetEnvironmentVariableAsBool(string name, bool defaultValue) + { + var value = GetEnvironmentVariable(name); + if (string.IsNullOrEmpty(value)) + { + return defaultValue; + } + + return value.ToLowerInvariant() switch + { + "true" or "1" or "yes" => true, + "false" or "0" or "no" => false, + _ => defaultValue + }; + } + + public int? GetEnvironmentVariableAsNullableInt(string name) + { + var value = GetEnvironmentVariable(name); + if (string.IsNullOrEmpty(value)) + { + return null; + } + + return int.TryParse(value, out var result) ? result : null; + } + + public string? GetEnvironmentVariable(string variable, EnvironmentVariableTarget target) + { + return _fallbackProvider.GetEnvironmentVariable(variable, target); + } + + public void SetEnvironmentVariable(string variable, string value, EnvironmentVariableTarget target) + { + _fallbackProvider.SetEnvironmentVariable(variable, value, target); + } + } +} diff --git a/src/Cli/dotnet/Configuration/DotNetConfigurationFactory.cs b/src/Cli/dotnet/Configuration/DotNetConfigurationFactory.cs new file mode 100644 index 000000000000..84b7df596371 --- /dev/null +++ b/src/Cli/dotnet/Configuration/DotNetConfigurationFactory.cs @@ -0,0 +1,59 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Configuration.DotnetCli.Services; +using Microsoft.Extensions.Configuration.DotnetCli.Models; +using Microsoft.Extensions.Configuration.DotnetCli.Providers; +using Microsoft.Extensions.Configuration; +using System.IO; + +namespace Microsoft.DotNet.Cli.Configuration +{ + /// + /// Factory for creating and configuring the .NET CLI configuration service. + /// This is the main entry point for the unified configuration system. + /// + public static class DotNetConfigurationFactory + { + /// + /// Creates and configures the .NET CLI configuration service with default providers. + /// This method follows the layered configuration approach: environment variables override global.json, + /// and configuration is loaded lazily for performance. + /// + /// The working directory to search for global.json files. Defaults to current directory. + /// A configured IDotNetConfigurationService instance + public static IDotNetConfigurationService Create(string? workingDirectory = null) + { + workingDirectory ??= Directory.GetCurrentDirectory(); + + var configurationBuilder = new ConfigurationBuilder(); + + // Configuration sources are added in reverse precedence order + // Last added has highest precedence + + // 1. global.json (custom provider with key mapping) - lowest precedence + configurationBuilder.Add(new GlobalJsonConfigurationSource(workingDirectory)); + + // 2. Environment variables (custom provider with key mapping) - highest precedence + configurationBuilder.Add(new DotNetEnvironmentConfigurationSource()); + + var configuration = configurationBuilder.Build(); + + return new DotNetConfigurationService(configuration); + } + + /// + /// Creates a minimal configuration service with only environment variables. + /// This is useful for scenarios where global.json lookup is not needed or desirable, + /// such as in performance-critical paths or testing scenarios. + /// + /// A minimal IDotNetConfigurationService instance + public static IDotNetConfigurationService CreateMinimal() + { + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.Add(new DotNetEnvironmentConfigurationSource()); + var configuration = configurationBuilder.Build(); + return new DotNetConfigurationService(configuration); + } + } +} diff --git a/src/Cli/dotnet/Program.cs b/src/Cli/dotnet/Program.cs index fa3fc09973f0..e17e45367e96 100644 --- a/src/Cli/dotnet/Program.cs +++ b/src/Cli/dotnet/Program.cs @@ -10,6 +10,7 @@ using Microsoft.DotNet.Cli.CommandFactory.CommandResolution; using Microsoft.DotNet.Cli.Commands.Run; using Microsoft.DotNet.Cli.Commands.Workload; +using Microsoft.DotNet.Cli.Configuration; using Microsoft.DotNet.Cli.Extensions; using Microsoft.DotNet.Cli.ShellShim; using Microsoft.DotNet.Cli.Telemetry; @@ -177,7 +178,9 @@ internal static int ProcessArgs(string[] args, TimeSpan startupTime) { PerformanceLogEventSource.Log.FirstTimeConfigurationStart(); - var environmentProvider = new EnvironmentProvider(); + // Initialize the new configuration-based environment provider + var configurationService = DotNetConfigurationFactory.Create(); + var environmentProvider = new ConfigurationBasedEnvironmentProvider(configurationService); bool generateAspNetCertificate = environmentProvider.GetEnvironmentVariableAsBool(EnvironmentVariableNames.DOTNET_GENERATE_ASPNET_CERTIFICATE, defaultValue: true); bool telemetryOptout = environmentProvider.GetEnvironmentVariableAsBool(EnvironmentVariableNames.TELEMETRY_OPTOUT, defaultValue: CompileOptions.TelemetryOptOutDefault); diff --git a/src/Cli/dotnet/dotnet.csproj b/src/Cli/dotnet/dotnet.csproj index d255b33e93a6..491571974ffa 100644 --- a/src/Cli/dotnet/dotnet.csproj +++ b/src/Cli/dotnet/dotnet.csproj @@ -43,6 +43,7 @@ + diff --git a/src/Microsoft.Extensions.Configuration.DotnetCli/Microsoft.Extensions.Configuration.DotnetCli.csproj b/src/Microsoft.Extensions.Configuration.DotnetCli/Microsoft.Extensions.Configuration.DotnetCli.csproj new file mode 100644 index 000000000000..eb0b5c457cf7 --- /dev/null +++ b/src/Microsoft.Extensions.Configuration.DotnetCli/Microsoft.Extensions.Configuration.DotnetCli.csproj @@ -0,0 +1,18 @@ + + + + $(SdkTargetFramework) + 12.0 + enable + true + + + + + + + + + + + diff --git a/src/Microsoft.Extensions.Configuration.DotnetCli/Models/BuildConfiguration.cs b/src/Microsoft.Extensions.Configuration.DotnetCli/Models/BuildConfiguration.cs new file mode 100644 index 000000000000..32761bb5b8ac --- /dev/null +++ b/src/Microsoft.Extensions.Configuration.DotnetCli/Models/BuildConfiguration.cs @@ -0,0 +1,40 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.Configuration.DotnetCli.Models; + +/// +/// Configuration settings that control build system behavior. +/// +public sealed class BuildConfiguration +{ + /// + /// Gets or sets whether to run MSBuild out of process. + /// Mapped from DOTNET_CLI_RUN_MSBUILD_OUTOFPROC environment variable. + /// + public bool RunMSBuildOutOfProc { get; set; } = false; + + /// + /// Gets or sets whether to use the MSBuild server for builds. + /// Mapped from DOTNET_CLI_USE_MSBUILD_SERVER environment variable. + /// + public bool UseMSBuildServer { get; set; } = false; + + /// + /// Gets or sets the configuration for the MSBuild terminal logger. + /// Mapped from DOTNET_CLI_CONFIGURE_MSBUILD_TERMINAL_LOGGER environment variable. + /// + public string? ConfigureMSBuildTerminalLogger { get; set; } + + /// + /// Gets or sets whether to disable publish and pack release configuration. + /// Mapped from DOTNET_CLI_DISABLE_PUBLISH_AND_PACK_RELEASE environment variable. + /// + public bool DisablePublishAndPackRelease { get; set; } = false; + + /// + /// Gets or sets whether to enable lazy publish and pack release for solutions. + /// Mapped from DOTNET_CLI_LAZY_PUBLISH_AND_PACK_RELEASE_FOR_SOLUTIONS environment variable. + /// + public bool LazyPublishAndPackReleaseForSolutions { get; set; } = false; +} diff --git a/src/Microsoft.Extensions.Configuration.DotnetCli/Models/CliUserExperienceConfiguration.cs b/src/Microsoft.Extensions.Configuration.DotnetCli/Models/CliUserExperienceConfiguration.cs new file mode 100644 index 000000000000..b1ee1f63688c --- /dev/null +++ b/src/Microsoft.Extensions.Configuration.DotnetCli/Models/CliUserExperienceConfiguration.cs @@ -0,0 +1,40 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.Configuration.DotnetCli.Models; + +/// +/// Configuration settings that control the CLI's user interface and interaction behavior. +/// +public sealed class CliUserExperienceConfiguration +{ + /// + /// Gets or sets whether telemetry collection is disabled. + /// Mapped from DOTNET_CLI_TELEMETRY_OPTOUT environment variable. + /// + public bool TelemetryOptOut { get; set; } = false; + + /// + /// Gets or sets whether to suppress the .NET logo on startup. + /// Mapped from DOTNET_NOLOGO environment variable. + /// + public bool NoLogo { get; set; } = false; + + /// + /// Gets or sets whether to force UTF-8 encoding for console output. + /// Mapped from DOTNET_CLI_FORCE_UTF8_ENCODING environment variable. + /// + public bool ForceUtf8Encoding { get; set; } = false; + + /// + /// Gets or sets the UI language for the CLI. + /// Mapped from DOTNET_CLI_UI_LANGUAGE environment variable. + /// + public string? UILanguage { get; set; } + + /// + /// Gets or sets the telemetry profile for data collection. + /// Mapped from DOTNET_CLI_TELEMETRY_PROFILE environment variable. + /// + public string? TelemetryProfile { get; set; } +} diff --git a/src/Microsoft.Extensions.Configuration.DotnetCli/Models/DevelopmentConfiguration.cs b/src/Microsoft.Extensions.Configuration.DotnetCli/Models/DevelopmentConfiguration.cs new file mode 100644 index 000000000000..ab442308c9d3 --- /dev/null +++ b/src/Microsoft.Extensions.Configuration.DotnetCli/Models/DevelopmentConfiguration.cs @@ -0,0 +1,40 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.Configuration.DotnetCli.Models; + +/// +/// Configuration settings that control development tools and debugging features. +/// +public sealed class DevelopmentConfiguration +{ + /// + /// Gets or sets whether performance logging is enabled. + /// Mapped from DOTNET_CLI_PERF_LOG environment variable. + /// + public bool PerfLogEnabled { get; set; } = false; + + /// + /// Gets or sets the number of performance log entries to collect. + /// Mapped from DOTNET_PERF_LOG_COUNT environment variable. + /// + public string? PerfLogCount { get; set; } + + /// + /// Gets or sets the CLI home directory for configuration and data. + /// Mapped from DOTNET_CLI_HOME environment variable. + /// + public string? CliHome { get; set; } + + /// + /// Gets or sets whether to enable verbose context logging. + /// Mapped from DOTNET_CLI_CONTEXT_VERBOSE environment variable. + /// + public bool ContextVerbose { get; set; } = false; + + /// + /// Gets or sets whether to allow targeting pack caching. + /// Mapped from DOTNETSDK_ALLOW_TARGETING_PACK_CACHING environment variable. + /// + public bool AllowTargetingPackCaching { get; set; } = false; +} diff --git a/src/Microsoft.Extensions.Configuration.DotnetCli/Models/FirstTimeUseConfiguration.cs b/src/Microsoft.Extensions.Configuration.DotnetCli/Models/FirstTimeUseConfiguration.cs new file mode 100644 index 000000000000..54d813f3f202 --- /dev/null +++ b/src/Microsoft.Extensions.Configuration.DotnetCli/Models/FirstTimeUseConfiguration.cs @@ -0,0 +1,28 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.Configuration.DotnetCli.Models; + +/// +/// Configuration settings that control first-time user experience setup. +/// +public sealed class FirstTimeUseConfiguration +{ + /// + /// Gets or sets whether to generate ASP.NET Core HTTPS development certificates. + /// Mapped from DOTNET_GENERATE_ASPNET_CERTIFICATE environment variable. + /// + public bool GenerateAspNetCertificate { get; set; } = true; + + /// + /// Gets or sets whether to add global tools to the PATH. + /// Mapped from DOTNET_ADD_GLOBAL_TOOLS_TO_PATH environment variable. + /// + public bool AddGlobalToolsToPath { get; set; } = true; + + /// + /// Gets or sets whether to skip the first-time experience setup. + /// Mapped from DOTNET_SKIP_FIRST_TIME_EXPERIENCE environment variable. + /// + public bool SkipFirstTimeExperience { get; set; } = false; +} diff --git a/src/Microsoft.Extensions.Configuration.DotnetCli/Models/RuntimeHostConfiguration.cs b/src/Microsoft.Extensions.Configuration.DotnetCli/Models/RuntimeHostConfiguration.cs new file mode 100644 index 000000000000..5b2f5dc41058 --- /dev/null +++ b/src/Microsoft.Extensions.Configuration.DotnetCli/Models/RuntimeHostConfiguration.cs @@ -0,0 +1,46 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.Configuration.DotnetCli.Models; + +/// +/// Configuration settings that control .NET runtime host behavior. +/// +public sealed class RuntimeHostConfiguration +{ + /// + /// Gets or sets the path to the .NET host executable. + /// Mapped from DOTNET_HOST_PATH environment variable. + /// + public string? HostPath { get; set; } + + /// + /// Gets or sets whether to enable multilevel lookup for shared frameworks. + /// Mapped from DOTNET_MULTILEVEL_LOOKUP environment variable. + /// + public bool MultilevelLookup { get; set; } = true; + + /// + /// Gets or sets the roll-forward policy for framework version selection. + /// Mapped from DOTNET_ROLL_FORWARD environment variable. + /// + public string? RollForward { get; set; } + + /// + /// Gets or sets the roll-forward policy when no candidate framework is found. + /// Mapped from DOTNET_ROLL_FORWARD_ON_NO_CANDIDATE_FX environment variable. + /// + public string? RollForwardOnNoCandidateFx { get; set; } + + /// + /// Gets or sets the root directory for .NET installations. + /// Mapped from DOTNET_ROOT environment variable. + /// + public string? Root { get; set; } + + /// + /// Gets or sets the root directory for x86 .NET installations. + /// Mapped from DOTNET_ROOT(x86) environment variable. + /// + public string? RootX86 { get; set; } +} diff --git a/src/Microsoft.Extensions.Configuration.DotnetCli/Models/SdkResolverConfiguration.cs b/src/Microsoft.Extensions.Configuration.DotnetCli/Models/SdkResolverConfiguration.cs new file mode 100644 index 000000000000..2769c5038607 --- /dev/null +++ b/src/Microsoft.Extensions.Configuration.DotnetCli/Models/SdkResolverConfiguration.cs @@ -0,0 +1,34 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.Configuration.DotnetCli.Models; + +/// +/// Configuration settings that control SDK resolution and discovery. +/// +public sealed class SdkResolverConfiguration +{ + /// + /// Gets or sets whether to enable SDK resolver logging. + /// Mapped from DOTNET_MSBUILD_SDK_RESOLVER_ENABLE_LOG environment variable. + /// + public bool EnableLog { get; set; } = false; + + /// + /// Gets or sets the directory containing SDKs. + /// Mapped from DOTNET_MSBUILD_SDK_RESOLVER_SDKS_DIR environment variable. + /// + public string? SdksDirectory { get; set; } + + /// + /// Gets or sets the SDK version to use. + /// Mapped from DOTNET_MSBUILD_SDK_RESOLVER_SDKS_VER environment variable. + /// + public string? SdksVersion { get; set; } + + /// + /// Gets or sets the CLI directory for SDK resolution. + /// Mapped from DOTNET_MSBUILD_SDK_RESOLVER_CLI_DIR environment variable. + /// + public string? CliDirectory { get; set; } +} diff --git a/src/Microsoft.Extensions.Configuration.DotnetCli/Models/ToolConfiguration.cs b/src/Microsoft.Extensions.Configuration.DotnetCli/Models/ToolConfiguration.cs new file mode 100644 index 000000000000..89c4fd4d6fc3 --- /dev/null +++ b/src/Microsoft.Extensions.Configuration.DotnetCli/Models/ToolConfiguration.cs @@ -0,0 +1,16 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.Configuration.DotnetCli.Models; + +/// +/// Configuration settings that control global tools behavior. +/// +public sealed class ToolConfiguration +{ + /// + /// Gets or sets whether to allow tool manifests in the repository root. + /// Mapped from DOTNET_TOOLS_ALLOW_MANIFEST_IN_ROOT environment variable. + /// + public bool AllowManifestInRoot { get; set; } = false; +} diff --git a/src/Microsoft.Extensions.Configuration.DotnetCli/Models/WorkloadConfiguration.cs b/src/Microsoft.Extensions.Configuration.DotnetCli/Models/WorkloadConfiguration.cs new file mode 100644 index 000000000000..19a5a723e692 --- /dev/null +++ b/src/Microsoft.Extensions.Configuration.DotnetCli/Models/WorkloadConfiguration.cs @@ -0,0 +1,52 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.Configuration.DotnetCli.Models; + +/// +/// Configuration settings that control workload management and behavior. +/// +public sealed class WorkloadConfiguration +{ + /// + /// Gets or sets whether to disable workload update notifications. + /// Mapped from DOTNET_CLI_WORKLOAD_UPDATE_NOTIFY_DISABLE environment variable. + /// + public bool UpdateNotifyDisable { get; set; } = false; + + /// + /// Gets or sets the interval in hours between workload update notifications. + /// Mapped from DOTNET_CLI_WORKLOAD_UPDATE_NOTIFY_INTERVAL_HOURS environment variable. + /// + public int UpdateNotifyIntervalHours { get; set; } = 24; + + /// + /// Gets or sets whether to disable workload pack groups. + /// Mapped from DOTNET_CLI_WORKLOAD_DISABLE_PACK_GROUPS environment variable. + /// + public bool DisablePackGroups { get; set; } = false; + + /// + /// Gets or sets whether to skip workload integrity checks. + /// Mapped from DOTNET_SKIP_WORKLOAD_INTEGRITY_CHECK environment variable. + /// + public bool SkipIntegrityCheck { get; set; } = false; + + /// + /// Gets or sets the manifest root directories for workloads. + /// Mapped from DOTNETSDK_WORKLOAD_MANIFEST_ROOTS environment variable. + /// + public string[]? ManifestRoots { get; set; } + + /// + /// Gets or sets the pack root directories for workloads. + /// Mapped from DOTNETSDK_WORKLOAD_PACK_ROOTS environment variable. + /// + public string[]? PackRoots { get; set; } + + /// + /// Gets or sets whether to ignore default manifest roots. + /// Mapped from DOTNETSDK_WORKLOAD_MANIFEST_IGNORE_DEFAULT_ROOTS environment variable. + /// + public bool ManifestIgnoreDefaultRoots { get; set; } = false; +} diff --git a/src/Microsoft.Extensions.Configuration.DotnetCli/Providers/DotNetEnvironmentConfigurationProvider.cs b/src/Microsoft.Extensions.Configuration.DotnetCli/Providers/DotNetEnvironmentConfigurationProvider.cs new file mode 100644 index 000000000000..60e2cea93855 --- /dev/null +++ b/src/Microsoft.Extensions.Configuration.DotnetCli/Providers/DotNetEnvironmentConfigurationProvider.cs @@ -0,0 +1,81 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.Configuration.DotnetCli.Providers; + +/// +/// Configuration provider that maps DOTNET_ prefixed environment variables to canonical keys. +/// +public class DotNetEnvironmentConfigurationProvider : ConfigurationProvider +{ + private static readonly Dictionary EnvironmentKeyMappings = new() + { + ["DOTNET_HOST_PATH"] = "RuntimeHost:HostPath", + ["DOTNET_CLI_TELEMETRY_OPTOUT"] = "CliUserExperience:TelemetryOptOut", + ["DOTNET_NOLOGO"] = "CliUserExperience:NoLogo", + ["DOTNET_CLI_FORCE_UTF8_ENCODING"] = "CliUserExperience:ForceUtf8Encoding", + ["DOTNET_CLI_UI_LANGUAGE"] = "CliUserExperience:UILanguage", + ["DOTNET_CLI_TELEMETRY_PROFILE"] = "CliUserExperience:TelemetryProfile", + ["DOTNET_CLI_PERF_LOG"] = "Development:PerfLogEnabled", + ["DOTNET_PERF_LOG_COUNT"] = "Development:PerfLogCount", + ["DOTNET_CLI_HOME"] = "Development:CliHome", + ["DOTNET_CLI_CONTEXT_VERBOSE"] = "Development:ContextVerbose", + ["DOTNET_MULTILEVEL_LOOKUP"] = "RuntimeHost:MultilevelLookup", + ["DOTNET_ROLL_FORWARD"] = "RuntimeHost:RollForward", + ["DOTNET_ROLL_FORWARD_ON_NO_CANDIDATE_FX"] = "RuntimeHost:RollForwardOnNoCandidateFx", + ["DOTNET_ROOT"] = "RuntimeHost:Root", + ["DOTNET_ROOT(x86)"] = "RuntimeHost:RootX86", + ["DOTNET_CLI_RUN_MSBUILD_OUTOFPROC"] = "Build:RunMSBuildOutOfProc", + ["DOTNET_CLI_USE_MSBUILD_SERVER"] = "Build:UseMSBuildServer", + ["DOTNET_CLI_CONFIGURE_MSBUILD_TERMINAL_LOGGER"] = "Build:ConfigureMSBuildTerminalLogger", + ["DOTNET_CLI_DISABLE_PUBLISH_AND_PACK_RELEASE"] = "Build:DisablePublishAndPackRelease", + ["DOTNET_CLI_LAZY_PUBLISH_AND_PACK_RELEASE_FOR_SOLUTIONS"] = "Build:LazyPublishAndPackReleaseForSolutions", + ["DOTNET_MSBUILD_SDK_RESOLVER_ENABLE_LOG"] = "SdkResolver:EnableLog", + ["DOTNET_MSBUILD_SDK_RESOLVER_SDKS_DIR"] = "SdkResolver:SdksDirectory", + ["DOTNET_MSBUILD_SDK_RESOLVER_SDKS_VER"] = "SdkResolver:SdksVersion", + ["DOTNET_MSBUILD_SDK_RESOLVER_CLI_DIR"] = "SdkResolver:CliDirectory", + ["DOTNET_CLI_WORKLOAD_UPDATE_NOTIFY_DISABLE"] = "Workload:UpdateNotifyDisable", + ["DOTNET_CLI_WORKLOAD_UPDATE_NOTIFY_INTERVAL_HOURS"] = "Workload:UpdateNotifyIntervalHours", + ["DOTNET_CLI_WORKLOAD_DISABLE_PACK_GROUPS"] = "Workload:DisablePackGroups", + ["DOTNET_SKIP_WORKLOAD_INTEGRITY_CHECK"] = "Workload:SkipIntegrityCheck", + ["DOTNETSDK_WORKLOAD_MANIFEST_ROOTS"] = "Workload:ManifestRoots", + ["DOTNETSDK_WORKLOAD_PACK_ROOTS"] = "Workload:PackRoots", + ["DOTNETSDK_WORKLOAD_MANIFEST_IGNORE_DEFAULT_ROOTS"] = "Workload:ManifestIgnoreDefaultRoots", + ["DOTNETSDK_ALLOW_TARGETING_PACK_CACHING"] = "Development:AllowTargetingPackCaching", + ["DOTNET_GENERATE_ASPNET_CERTIFICATE"] = "FirstTimeUse:GenerateAspNetCertificate", + ["DOTNET_ADD_GLOBAL_TOOLS_TO_PATH"] = "FirstTimeUse:AddGlobalToolsToPath", + ["DOTNET_SKIP_FIRST_TIME_EXPERIENCE"] = "FirstTimeUse:SkipFirstTimeExperience", + ["DOTNET_TOOLS_ALLOW_MANIFEST_IN_ROOT"] = "Tool:AllowManifestInRoot", + }; + + public override void Load() + { + Data.Clear(); + + foreach (var mapping in EnvironmentKeyMappings) + { + var value = Environment.GetEnvironmentVariable(mapping.Key); + if (!string.IsNullOrEmpty(value)) + { + Data[mapping.Value] = value; + } + } + + // Handle array-type environment variables (semicolon-separated) + HandleArrayEnvironmentVariable("DOTNETSDK_WORKLOAD_MANIFEST_ROOTS", "Workload:ManifestRoots"); + HandleArrayEnvironmentVariable("DOTNETSDK_WORKLOAD_PACK_ROOTS", "Workload:PackRoots"); + } + + private void HandleArrayEnvironmentVariable(string envVar, string configKey) + { + var value = Environment.GetEnvironmentVariable(envVar); + if (!string.IsNullOrEmpty(value)) + { + var parts = value.Split(';', StringSplitOptions.RemoveEmptyEntries); + for (int i = 0; i < parts.Length; i++) + { + Data[$"{configKey}:{i}"] = parts[i]; + } + } + } +} diff --git a/src/Microsoft.Extensions.Configuration.DotnetCli/Providers/DotNetEnvironmentConfigurationSource.cs b/src/Microsoft.Extensions.Configuration.DotnetCli/Providers/DotNetEnvironmentConfigurationSource.cs new file mode 100644 index 000000000000..8e5d83464df3 --- /dev/null +++ b/src/Microsoft.Extensions.Configuration.DotnetCli/Providers/DotNetEnvironmentConfigurationSource.cs @@ -0,0 +1,15 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.Configuration.DotnetCli.Providers; + +/// +/// Configuration source for DOTNET_ prefixed environment variables. +/// +public class DotNetEnvironmentConfigurationSource : IConfigurationSource +{ + public IConfigurationProvider Build(IConfigurationBuilder builder) + { + return new DotNetEnvironmentConfigurationProvider(); + } +} diff --git a/src/Microsoft.Extensions.Configuration.DotnetCli/Providers/GlobalJsonConfigurationProvider.cs b/src/Microsoft.Extensions.Configuration.DotnetCli/Providers/GlobalJsonConfigurationProvider.cs new file mode 100644 index 000000000000..0ebb8a4378e4 --- /dev/null +++ b/src/Microsoft.Extensions.Configuration.DotnetCli/Providers/GlobalJsonConfigurationProvider.cs @@ -0,0 +1,112 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Text.Json; + +namespace Microsoft.Extensions.Configuration.DotnetCli.Providers; + +/// +/// Configuration provider that reads global.json files and maps keys to the canonical format. +/// +public class GlobalJsonConfigurationProvider : ConfigurationProvider +{ + private readonly string? _path; + + private static readonly Dictionary GlobalJsonKeyMappings = new() + { + ["sdk:version"] = "sdk:version", + ["sdk:rollForward"] = "sdk:rollforward", + ["sdk:allowPrerelease"] = "sdk:allowprerelease", + ["msbuild-sdks"] = "msbuild:sdks", + // Add more mappings as the global.json schema evolves + }; + + public GlobalJsonConfigurationProvider(string workingDirectory) + { + _path = FindGlobalJson(workingDirectory); + } + + public override void Load() + { + Data.Clear(); + + if (_path == null || !File.Exists(_path)) + return; + + try + { + var json = File.ReadAllText(_path); + var document = JsonDocument.Parse(json); + + LoadGlobalJsonData(document.RootElement, ""); + } + catch (Exception ex) + { + throw new InvalidOperationException($"Error parsing global.json at {_path}", ex); + } + } + + private void LoadGlobalJsonData(JsonElement element, string prefix) + { + foreach (var property in element.EnumerateObject()) + { + var rawKey = string.IsNullOrEmpty(prefix) + ? property.Name + : $"{prefix}:{property.Name}"; + + switch (property.Value.ValueKind) + { + case JsonValueKind.Object: + LoadGlobalJsonData(property.Value, rawKey); + break; + case JsonValueKind.String: + case JsonValueKind.Number: + case JsonValueKind.True: + case JsonValueKind.False: + // Map to canonical key format + var canonicalKey = MapGlobalJsonKey(rawKey); + Data[canonicalKey] = GetValueAsString(property.Value); + break; + } + } + } + + private string MapGlobalJsonKey(string rawKey) + { + // Check for exact mapping first + if (GlobalJsonKeyMappings.TryGetValue(rawKey, out var mapped)) + return mapped; + + // For msbuild-sdks, convert to msbuild:sdks:packagename format + if (rawKey.StartsWith("msbuild-sdks:")) + return rawKey.Replace("msbuild-sdks:", "msbuild:sdks:"); + + // Default: convert to lowercase and normalize separators + return rawKey.ToLowerInvariant().Replace("-", ":"); + } + + private string GetValueAsString(JsonElement element) + { + return element.ValueKind switch + { + JsonValueKind.String => element.GetString()!, + JsonValueKind.Number => element.GetRawText(), + JsonValueKind.True => "true", + JsonValueKind.False => "false", + _ => element.GetRawText() + }; + } + + private string? FindGlobalJson(string startDirectory) + { + var current = new DirectoryInfo(startDirectory); + while (current != null) + { + var globalJsonPath = Path.Combine(current.FullName, "global.json"); + if (File.Exists(globalJsonPath)) + return globalJsonPath; + current = current.Parent; + } + return null; + } +} diff --git a/src/Microsoft.Extensions.Configuration.DotnetCli/Providers/GlobalJsonConfigurationSource.cs b/src/Microsoft.Extensions.Configuration.DotnetCli/Providers/GlobalJsonConfigurationSource.cs new file mode 100644 index 000000000000..3c1fafe09d78 --- /dev/null +++ b/src/Microsoft.Extensions.Configuration.DotnetCli/Providers/GlobalJsonConfigurationSource.cs @@ -0,0 +1,22 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.Configuration.DotnetCli.Providers; + +/// +/// Configuration source for global.json files. +/// +public class GlobalJsonConfigurationSource : IConfigurationSource +{ + private readonly string _workingDirectory; + + public GlobalJsonConfigurationSource(string workingDirectory) + { + _workingDirectory = workingDirectory ?? throw new ArgumentNullException(nameof(workingDirectory)); + } + + public IConfigurationProvider Build(IConfigurationBuilder builder) + { + return new GlobalJsonConfigurationProvider(_workingDirectory); + } +} diff --git a/src/Microsoft.Extensions.Configuration.DotnetCli/Services/DotNetConfiguration.cs b/src/Microsoft.Extensions.Configuration.DotnetCli/Services/DotNetConfiguration.cs new file mode 100644 index 000000000000..b218d28a7734 --- /dev/null +++ b/src/Microsoft.Extensions.Configuration.DotnetCli/Services/DotNetConfiguration.cs @@ -0,0 +1,73 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Configuration.DotnetCli.Providers; + +namespace Microsoft.Extensions.Configuration.DotnetCli.Services; + +/// +/// Factory for creating .NET CLI configuration instances. +/// +public static class DotNetConfiguration +{ + /// + /// Creates a complete configuration instance with all providers. + /// + /// The working directory to search for configuration files. Defaults to current directory. + /// A configured IConfiguration instance. + public static IConfiguration Create(string? workingDirectory = null) + { + var builder = new ConfigurationBuilder(); + + // Priority order (last wins): + // 1. dotnet.config (if it exists) - future enhancement + // 2. global.json (custom provider with key mapping) + // 3. Environment variables with DOTNET_ prefix (with key mapping) + // 4. Command line arguments (handled separately) + + workingDirectory ??= Directory.GetCurrentDirectory(); + + // Add dotnet.config if it exists (future enhancement) + var dotnetConfigPath = Path.Combine(workingDirectory, "dotnet.config"); + if (File.Exists(dotnetConfigPath)) + { + builder.AddIniFile(dotnetConfigPath, optional: true, reloadOnChange: false); + } + + // Add global.json with a custom configuration provider that maps keys + builder.Add(new GlobalJsonConfigurationSource(workingDirectory)); + + // Add DOTNET_ prefixed environment variables with key mapping + builder.Add(new DotNetEnvironmentConfigurationSource()); + + return builder.Build(); + } + + /// + /// Creates a strongly-typed configuration service with all providers. + /// + /// The working directory to search for configuration files. Defaults to current directory. + /// A strongly-typed configuration service. + public static IDotNetConfigurationService CreateTyped(string? workingDirectory = null) + { + var configuration = Create(workingDirectory); + return new DotNetConfigurationService(configuration); + } + + /// + /// Creates a minimal configuration service that only loads environment variables. + /// This is the fastest option for scenarios that don't need file-based configuration. + /// + /// The working directory (unused for minimal configuration). + /// A minimal strongly-typed configuration service. + public static IDotNetConfigurationService CreateMinimal(string? workingDirectory = null) + { + var builder = new ConfigurationBuilder(); + + // Only add environment variables for minimal overhead + builder.Add(new DotNetEnvironmentConfigurationSource()); + + var configuration = builder.Build(); + return new DotNetConfigurationService(configuration); + } +} diff --git a/src/Microsoft.Extensions.Configuration.DotnetCli/Services/DotNetConfigurationService.cs b/src/Microsoft.Extensions.Configuration.DotnetCli/Services/DotNetConfigurationService.cs new file mode 100644 index 000000000000..1b4217fa7066 --- /dev/null +++ b/src/Microsoft.Extensions.Configuration.DotnetCli/Services/DotNetConfigurationService.cs @@ -0,0 +1,59 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Configuration.DotnetCli.Models; + +namespace Microsoft.Extensions.Configuration.DotnetCli.Services; + +/// +/// Strongly-typed configuration service for .NET CLI with lazy initialization. +/// +public class DotNetConfigurationService : IDotNetConfigurationService +{ + private readonly IConfiguration _configuration; + + // Lazy initialization for each configuration section + private readonly Lazy _cliUserExperience; + private readonly Lazy _runtimeHost; + private readonly Lazy _build; + private readonly Lazy _sdkResolver; + private readonly Lazy _workload; + private readonly Lazy _firstTimeUse; + private readonly Lazy _development; + private readonly Lazy _tool; + + public IConfiguration RawConfiguration => _configuration; + + // Lazy-loaded strongly-typed configuration properties + public CliUserExperienceConfiguration CliUserExperience => _cliUserExperience.Value; + public RuntimeHostConfiguration RuntimeHost => _runtimeHost.Value; + public BuildConfiguration Build => _build.Value; + public SdkResolverConfiguration SdkResolver => _sdkResolver.Value; + public WorkloadConfiguration Workload => _workload.Value; + public FirstTimeUseConfiguration FirstTimeUse => _firstTimeUse.Value; + public DevelopmentConfiguration Development => _development.Value; + public ToolConfiguration Tool => _tool.Value; + + public DotNetConfigurationService(IConfiguration configuration) + { + _configuration = configuration ?? throw new ArgumentNullException(nameof(configuration)); + + // Initialize lazy factories - configuration binding only happens on first access + _cliUserExperience = new Lazy(() => + _configuration.GetSection("CliUserExperience").Get() ?? new()); + _runtimeHost = new Lazy(() => + _configuration.GetSection("RuntimeHost").Get() ?? new()); + _build = new Lazy(() => + _configuration.GetSection("Build").Get() ?? new()); + _sdkResolver = new Lazy(() => + _configuration.GetSection("SdkResolver").Get() ?? new()); + _workload = new Lazy(() => + _configuration.GetSection("Workload").Get() ?? new()); + _firstTimeUse = new Lazy(() => + _configuration.GetSection("FirstTimeUse").Get() ?? new()); + _development = new Lazy(() => + _configuration.GetSection("Development").Get() ?? new()); + _tool = new Lazy(() => + _configuration.GetSection("Tool").Get() ?? new()); + } +} diff --git a/src/Microsoft.Extensions.Configuration.DotnetCli/Services/IDotNetConfigurationService.cs b/src/Microsoft.Extensions.Configuration.DotnetCli/Services/IDotNetConfigurationService.cs new file mode 100644 index 000000000000..a15db541778a --- /dev/null +++ b/src/Microsoft.Extensions.Configuration.DotnetCli/Services/IDotNetConfigurationService.cs @@ -0,0 +1,57 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Configuration.DotnetCli.Models; + +namespace Microsoft.Extensions.Configuration.DotnetCli.Services; + +/// +/// Interface for strongly-typed .NET CLI configuration service. +/// +public interface IDotNetConfigurationService +{ + /// + /// Gets the underlying IConfiguration instance for advanced scenarios. + /// + IConfiguration RawConfiguration { get; } + + /// + /// Gets CLI user experience configuration settings. + /// + CliUserExperienceConfiguration CliUserExperience { get; } + + /// + /// Gets runtime host configuration settings. + /// + RuntimeHostConfiguration RuntimeHost { get; } + + /// + /// Gets build and MSBuild configuration settings. + /// + BuildConfiguration Build { get; } + + /// + /// Gets SDK resolver configuration settings. + /// + SdkResolverConfiguration SdkResolver { get; } + + /// + /// Gets workload management configuration settings. + /// + WorkloadConfiguration Workload { get; } + + /// + /// Gets first-time use experience configuration settings. + /// + FirstTimeUseConfiguration FirstTimeUse { get; } + + /// + /// Gets development and debugging configuration settings. + /// + DevelopmentConfiguration Development { get; } + + /// + /// Gets global tools configuration settings. + /// + ToolConfiguration Tool { get; } +} From 8cf5ddc331d94f96bf8600dd21ec5d06bbeb459c Mon Sep 17 00:00:00 2001 From: Chet Husk Date: Sun, 13 Jul 2025 11:55:14 -0500 Subject: [PATCH 05/12] use typed config in program.cs --- src/Cli/dotnet/Program.cs | 13 +++++++------ ...rosoft.Extensions.Configuration.DotnetCli.csproj | 4 ++++ .../Models/CliUserExperienceConfiguration.cs | 4 +++- .../Models/WorkloadConfiguration.cs | 2 +- 4 files changed, 15 insertions(+), 8 deletions(-) diff --git a/src/Cli/dotnet/Program.cs b/src/Cli/dotnet/Program.cs index e17e45367e96..ea39e51c2f60 100644 --- a/src/Cli/dotnet/Program.cs +++ b/src/Cli/dotnet/Program.cs @@ -182,13 +182,14 @@ internal static int ProcessArgs(string[] args, TimeSpan startupTime) var configurationService = DotNetConfigurationFactory.Create(); var environmentProvider = new ConfigurationBasedEnvironmentProvider(configurationService); - bool generateAspNetCertificate = environmentProvider.GetEnvironmentVariableAsBool(EnvironmentVariableNames.DOTNET_GENERATE_ASPNET_CERTIFICATE, defaultValue: true); - bool telemetryOptout = environmentProvider.GetEnvironmentVariableAsBool(EnvironmentVariableNames.TELEMETRY_OPTOUT, defaultValue: CompileOptions.TelemetryOptOutDefault); - bool addGlobalToolsToPath = environmentProvider.GetEnvironmentVariableAsBool(EnvironmentVariableNames.DOTNET_ADD_GLOBAL_TOOLS_TO_PATH, defaultValue: true); - bool nologo = environmentProvider.GetEnvironmentVariableAsBool(EnvironmentVariableNames.DOTNET_NOLOGO, defaultValue: false); - bool skipWorkloadIntegrityCheck = environmentProvider.GetEnvironmentVariableAsBool(EnvironmentVariableNames.DOTNET_SKIP_WORKLOAD_INTEGRITY_CHECK, + // Use typed configuration directly instead of environment variable calls + bool generateAspNetCertificate = configurationService.FirstTimeUse.GenerateAspNetCertificate; + bool telemetryOptout = configurationService.CliUserExperience.TelemetryOptOut; + bool addGlobalToolsToPath = configurationService.FirstTimeUse.AddGlobalToolsToPath; + bool nologo = configurationService.CliUserExperience.NoLogo; + bool skipWorkloadIntegrityCheck = configurationService.Workload.SkipIntegrityCheck || // Default the workload integrity check skip to true if the command is being ran in CI. Otherwise, false. - defaultValue: new CIEnvironmentDetectorForTelemetry().IsCIEnvironment()); + new CIEnvironmentDetectorForTelemetry().IsCIEnvironment(); ReportDotnetHomeUsage(environmentProvider); diff --git a/src/Microsoft.Extensions.Configuration.DotnetCli/Microsoft.Extensions.Configuration.DotnetCli.csproj b/src/Microsoft.Extensions.Configuration.DotnetCli/Microsoft.Extensions.Configuration.DotnetCli.csproj index eb0b5c457cf7..1e9c02fa0fd4 100644 --- a/src/Microsoft.Extensions.Configuration.DotnetCli/Microsoft.Extensions.Configuration.DotnetCli.csproj +++ b/src/Microsoft.Extensions.Configuration.DotnetCli/Microsoft.Extensions.Configuration.DotnetCli.csproj @@ -7,6 +7,10 @@ true + + + + diff --git a/src/Microsoft.Extensions.Configuration.DotnetCli/Models/CliUserExperienceConfiguration.cs b/src/Microsoft.Extensions.Configuration.DotnetCli/Models/CliUserExperienceConfiguration.cs index b1ee1f63688c..46618744d900 100644 --- a/src/Microsoft.Extensions.Configuration.DotnetCli/Models/CliUserExperienceConfiguration.cs +++ b/src/Microsoft.Extensions.Configuration.DotnetCli/Models/CliUserExperienceConfiguration.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using Microsoft.DotNet.Cli; + namespace Microsoft.Extensions.Configuration.DotnetCli.Models; /// @@ -12,7 +14,7 @@ public sealed class CliUserExperienceConfiguration /// Gets or sets whether telemetry collection is disabled. /// Mapped from DOTNET_CLI_TELEMETRY_OPTOUT environment variable. /// - public bool TelemetryOptOut { get; set; } = false; + public bool TelemetryOptOut { get; set; } = CompileOptions.TelemetryOptOutDefault; /// /// Gets or sets whether to suppress the .NET logo on startup. diff --git a/src/Microsoft.Extensions.Configuration.DotnetCli/Models/WorkloadConfiguration.cs b/src/Microsoft.Extensions.Configuration.DotnetCli/Models/WorkloadConfiguration.cs index 19a5a723e692..85e988fc1406 100644 --- a/src/Microsoft.Extensions.Configuration.DotnetCli/Models/WorkloadConfiguration.cs +++ b/src/Microsoft.Extensions.Configuration.DotnetCli/Models/WorkloadConfiguration.cs @@ -18,7 +18,7 @@ public sealed class WorkloadConfiguration /// Gets or sets the interval in hours between workload update notifications. /// Mapped from DOTNET_CLI_WORKLOAD_UPDATE_NOTIFY_INTERVAL_HOURS environment variable. /// - public int UpdateNotifyIntervalHours { get; set; } = 24; + public int UpdateNotifyIntervalHours { get; set; } = 24; // Default to check once per day /// /// Gets or sets whether to disable workload pack groups. From de50731a57e156ca7d8add5998900afa1677098d Mon Sep 17 00:00:00 2001 From: Chet Husk Date: Sun, 13 Jul 2025 12:32:55 -0500 Subject: [PATCH 06/12] make nuget package downloader use the unified config too --- src/Cli/dotnet/Commands/Pack/PackCommand.cs | 4 +++- .../Package/Add/PackageAddCommandParser.cs | 6 ++++-- .../dotnet/Commands/Publish/PublishCommand.cs | 4 +++- .../ToolInstallGlobalOrToolPathCommand.cs | 4 +++- .../Workload/Install/FileBasedInstaller.cs | 9 +++++++-- .../Install/NetSdkMsiInstallerClient.cs | 9 +++++++-- .../Workload/Install/WorkloadInstallCommand.cs | 9 ++++++--- .../Workload/Install/WorkloadManifestUpdater.cs | 9 ++++++--- .../Workload/Update/WorkloadUpdateCommand.cs | 9 ++++++--- .../Commands/Workload/WorkloadCommandBase.cs | 9 ++++++--- .../Workload/WorkloadIntegrityChecker.cs | 3 +++ .../NuGetPackageDownloader.cs | 8 +++++--- src/Cli/dotnet/ReleasePropertyProjectLocator.cs | 11 +++++++---- .../dotnet/ToolPackage/ToolPackageDownloader.cs | 4 ++++ .../Models/NuGetConfiguration.cs | 17 +++++++++++++++++ .../DotNetEnvironmentConfigurationProvider.cs | 1 + .../Services/DotNetConfigurationService.cs | 4 ++++ .../Services/IDotNetConfigurationService.cs | 5 +++++ 18 files changed, 97 insertions(+), 28 deletions(-) create mode 100644 src/Microsoft.Extensions.Configuration.DotnetCli/Models/NuGetConfiguration.cs diff --git a/src/Cli/dotnet/Commands/Pack/PackCommand.cs b/src/Cli/dotnet/Commands/Pack/PackCommand.cs index 8e1b4aa8b81d..355e95ee314d 100644 --- a/src/Cli/dotnet/Commands/Pack/PackCommand.cs +++ b/src/Cli/dotnet/Commands/Pack/PackCommand.cs @@ -3,6 +3,7 @@ using System.CommandLine; using Microsoft.DotNet.Cli.Commands.Restore; +using Microsoft.DotNet.Cli.Configuration; using Microsoft.DotNet.Cli.Extensions; using Microsoft.DotNet.Cli.Utils; @@ -27,11 +28,12 @@ public static PackCommand FromParseResult(ParseResult parseResult, string? msbui var msbuildArgs = parseResult.OptionValuesToBeForwarded(PackCommandParser.GetCommand()).Concat(parseResult.GetValue(PackCommandParser.SlnOrProjectArgument) ?? []); + var configurationService = DotNetConfigurationFactory.Create(); ReleasePropertyProjectLocator projectLocator = new(parseResult, MSBuildPropertyNames.PACK_RELEASE, new ReleasePropertyProjectLocator.DependentCommandOptions( parseResult.GetValue(PackCommandParser.SlnOrProjectArgument), parseResult.HasOption(PackCommandParser.ConfigurationOption) ? parseResult.GetValue(PackCommandParser.ConfigurationOption) : null - ) + ), configurationService ); bool noRestore = parseResult.HasOption(PackCommandParser.NoRestoreOption) || parseResult.HasOption(PackCommandParser.NoBuildOption); diff --git a/src/Cli/dotnet/Commands/Package/Add/PackageAddCommandParser.cs b/src/Cli/dotnet/Commands/Package/Add/PackageAddCommandParser.cs index 8a504a9e01aa..fc97a3061e2b 100644 --- a/src/Cli/dotnet/Commands/Package/Add/PackageAddCommandParser.cs +++ b/src/Cli/dotnet/Commands/Package/Add/PackageAddCommandParser.cs @@ -6,7 +6,9 @@ using System.CommandLine; using System.CommandLine.Completions; using System.CommandLine.Parsing; +using Microsoft.DotNet.Cli.Configuration; using Microsoft.DotNet.Cli.Extensions; +using Microsoft.Extensions.Configuration.DotnetCli.Services; using Microsoft.Extensions.EnvironmentAbstractions; using NuGet.Versioning; @@ -115,7 +117,7 @@ public static async Task> QueryNuGet(string packageStem, boo { try { - var downloader = new NuGetPackageDownloader.NuGetPackageDownloader(packageInstallDir: new DirectoryPath()); + var downloader = new NuGetPackageDownloader.NuGetPackageDownloader(packageInstallDir: new DirectoryPath(), configurationService: DotNetConfigurationFactory.Create()); var versions = await downloader.GetPackageIdsAsync(packageStem, allowPrerelease, cancellationToken: cancellationToken); return versions; } @@ -129,7 +131,7 @@ internal static async Task> QueryVersionsForPackage(st { try { - var downloader = new NuGetPackageDownloader.NuGetPackageDownloader(packageInstallDir: new DirectoryPath()); + var downloader = new NuGetPackageDownloader.NuGetPackageDownloader(packageInstallDir: new DirectoryPath(), configurationService: DotNetConfigurationFactory.Create()); var versions = await downloader.GetPackageVersionsAsync(new(packageId), versionFragment, allowPrerelease, cancellationToken: cancellationToken); return versions; } diff --git a/src/Cli/dotnet/Commands/Publish/PublishCommand.cs b/src/Cli/dotnet/Commands/Publish/PublishCommand.cs index eab6dee108b3..1773427cc8ca 100644 --- a/src/Cli/dotnet/Commands/Publish/PublishCommand.cs +++ b/src/Cli/dotnet/Commands/Publish/PublishCommand.cs @@ -4,6 +4,7 @@ using System.CommandLine; using Microsoft.DotNet.Cli.Commands.Restore; using Microsoft.DotNet.Cli.Commands.Run; +using Microsoft.DotNet.Cli.Configuration; using Microsoft.DotNet.Cli.Extensions; using Microsoft.DotNet.Cli.Utils; @@ -56,12 +57,13 @@ public static CommandBase FromParseResult(ParseResult parseResult, string? msbui NoCache = true, }, (msbuildArgs, msbuildPath) => { + var configurationService = DotNetConfigurationFactory.Create(); var options = new ReleasePropertyProjectLocator.DependentCommandOptions( nonBinLogArgs, parseResult.HasOption(PublishCommandParser.ConfigurationOption) ? parseResult.GetValue(PublishCommandParser.ConfigurationOption) : null, parseResult.HasOption(PublishCommandParser.FrameworkOption) ? parseResult.GetValue(PublishCommandParser.FrameworkOption) : null ); - var projectLocator = new ReleasePropertyProjectLocator(parseResult, MSBuildPropertyNames.PUBLISH_RELEASE, options); + var projectLocator = new ReleasePropertyProjectLocator(parseResult, MSBuildPropertyNames.PUBLISH_RELEASE, options, configurationService); var releaseModeProperties = projectLocator.GetCustomDefaultConfigurationValueIfSpecified(); return new PublishCommand( msbuildArgs: msbuildArgs.CloneWithAdditionalProperties(releaseModeProperties), diff --git a/src/Cli/dotnet/Commands/Tool/Install/ToolInstallGlobalOrToolPathCommand.cs b/src/Cli/dotnet/Commands/Tool/Install/ToolInstallGlobalOrToolPathCommand.cs index 0a23fad43910..c3c860f57b75 100644 --- a/src/Cli/dotnet/Commands/Tool/Install/ToolInstallGlobalOrToolPathCommand.cs +++ b/src/Cli/dotnet/Commands/Tool/Install/ToolInstallGlobalOrToolPathCommand.cs @@ -5,9 +5,11 @@ using System.CommandLine; using System.Transactions; +using Microsoft.DotNet.Cli.Configuration; using Microsoft.DotNet.Cli.NuGetPackageDownloader; using Microsoft.DotNet.Cli.ToolPackage; using Microsoft.DotNet.Cli.Utils; +using Microsoft.Extensions.Configuration.DotnetCli.Services; using Microsoft.Extensions.EnvironmentAbstractions; using NuGet.Common; using NuGet.Frameworks; @@ -96,7 +98,7 @@ public ToolInstallGlobalOrToolPathCommand( NoCache: parseResult.GetValue(ToolCommandRestorePassThroughOptions.NoCacheOption) || parseResult.GetValue(ToolCommandRestorePassThroughOptions.NoHttpCacheOption), IgnoreFailedSources: parseResult.GetValue(ToolCommandRestorePassThroughOptions.IgnoreFailedSourcesOption), Interactive: parseResult.GetValue(ToolCommandRestorePassThroughOptions.InteractiveRestoreOption)); - nugetPackageDownloader ??= new NuGetPackageDownloader.NuGetPackageDownloader(tempDir, verboseLogger: new NullLogger(), restoreActionConfig: restoreActionConfig, verbosityOptions: _verbosity, verifySignatures: verifySignatures ?? true); + nugetPackageDownloader ??= new NuGetPackageDownloader.NuGetPackageDownloader(tempDir, DotNetConfigurationFactory.Create(), verboseLogger: new NullLogger(), restoreActionConfig: restoreActionConfig, verbosityOptions: _verbosity, verifySignatures: verifySignatures ?? true); _shellShimTemplateFinder = new ShellShimTemplateFinder(nugetPackageDownloader, tempDir, packageSourceLocation); _store = store; diff --git a/src/Cli/dotnet/Commands/Workload/Install/FileBasedInstaller.cs b/src/Cli/dotnet/Commands/Workload/Install/FileBasedInstaller.cs index 99681825c16f..27ef7d9246fa 100644 --- a/src/Cli/dotnet/Commands/Workload/Install/FileBasedInstaller.cs +++ b/src/Cli/dotnet/Commands/Workload/Install/FileBasedInstaller.cs @@ -7,11 +7,13 @@ using System.Text.Json; using Microsoft.DotNet.Cli.Commands.Workload.Config; using Microsoft.DotNet.Cli.Commands.Workload.Install.WorkloadInstallRecords; +using Microsoft.DotNet.Cli.Configuration; using Microsoft.DotNet.Cli.Extensions; using Microsoft.DotNet.Cli.NuGetPackageDownloader; using Microsoft.DotNet.Cli.ToolPackage; using Microsoft.DotNet.Cli.Utils; using Microsoft.DotNet.NativeWrapper; +using Microsoft.Extensions.Configuration.DotnetCli.Services; using Microsoft.Extensions.EnvironmentAbstractions; using Microsoft.NET.Sdk.WorkloadManifestReader; using NuGet.Common; @@ -60,8 +62,11 @@ public FileBasedInstaller(IReporter reporter, ILogger logger = verbosity.IsDetailedOrDiagnostic() ? new NuGetConsoleLogger() : new NullLogger(); _restoreActionConfig = restoreActionConfig; _nugetPackageDownloader = nugetPackageDownloader ?? - new NuGetPackageDownloader.NuGetPackageDownloader(_tempPackagesDir, filePermissionSetter: null, - new FirstPartyNuGetPackageSigningVerifier(), logger, + new NuGetPackageDownloader.NuGetPackageDownloader(_tempPackagesDir, + DotNetConfigurationFactory.Create(), + filePermissionSetter: null, + firstPartyNuGetPackageSigningVerifier: new FirstPartyNuGetPackageSigningVerifier(), + verboseLogger: logger, restoreActionConfig: _restoreActionConfig, verbosityOptions: nugetPackageDownloaderVerbosity); bool userLocal = WorkloadFileBasedInstall.IsUserLocal(_dotnetDir, sdkFeatureBand.ToString()); diff --git a/src/Cli/dotnet/Commands/Workload/Install/NetSdkMsiInstallerClient.cs b/src/Cli/dotnet/Commands/Workload/Install/NetSdkMsiInstallerClient.cs index 26d7c3e27ee4..b903964c38b8 100644 --- a/src/Cli/dotnet/Commands/Workload/Install/NetSdkMsiInstallerClient.cs +++ b/src/Cli/dotnet/Commands/Workload/Install/NetSdkMsiInstallerClient.cs @@ -7,11 +7,13 @@ using System.Runtime.Versioning; using Microsoft.DotNet.Cli.Commands.Workload.Config; using Microsoft.DotNet.Cli.Commands.Workload.Install.WorkloadInstallRecords; +using Microsoft.DotNet.Cli.Configuration; using Microsoft.DotNet.Cli.Installer.Windows; using Microsoft.DotNet.Cli.NuGetPackageDownloader; using Microsoft.DotNet.Cli.ToolPackage; using Microsoft.DotNet.Cli.Utils; using Microsoft.DotNet.Cli.Utils.Extensions; +using Microsoft.Extensions.Configuration.DotnetCli.Services; using Microsoft.Extensions.EnvironmentAbstractions; using Microsoft.NET.Sdk.WorkloadManifestReader; using Microsoft.Win32.Msi; @@ -1128,8 +1130,11 @@ public static NetSdkMsiInstallerClient Create( DirectoryPath tempPackagesDir = new(string.IsNullOrWhiteSpace(tempDirPath) ? PathUtilities.CreateTempSubdirectory() : tempDirPath); nugetPackageDownloader = new NuGetPackageDownloader.NuGetPackageDownloader(tempPackagesDir, - filePermissionSetter: null, new FirstPartyNuGetPackageSigningVerifier(), - new NullLogger(), restoreActionConfig: restoreActionConfig); + DotNetConfigurationFactory.Create(), + filePermissionSetter: null, + firstPartyNuGetPackageSigningVerifier: new FirstPartyNuGetPackageSigningVerifier(), + verboseLogger: new NullLogger(), + restoreActionConfig: restoreActionConfig); } return new NetSdkMsiInstallerClient(elevationContext, logger, verifySignatures, workloadResolver, sdkFeatureBand, nugetPackageDownloader, diff --git a/src/Cli/dotnet/Commands/Workload/Install/WorkloadInstallCommand.cs b/src/Cli/dotnet/Commands/Workload/Install/WorkloadInstallCommand.cs index 15049f909b09..637bf68e49e9 100644 --- a/src/Cli/dotnet/Commands/Workload/Install/WorkloadInstallCommand.cs +++ b/src/Cli/dotnet/Commands/Workload/Install/WorkloadInstallCommand.cs @@ -5,11 +5,13 @@ using System.CommandLine; using System.Text.Json; +using Microsoft.DotNet.Cli.Configuration; using Microsoft.DotNet.Cli.Extensions; using Microsoft.DotNet.Cli.NuGetPackageDownloader; using Microsoft.DotNet.Cli.ToolPackage; using Microsoft.DotNet.Cli.Utils; using Microsoft.DotNet.Cli.Utils.Extensions; +using Microsoft.Extensions.Configuration.DotnetCli.Services; using Microsoft.Extensions.EnvironmentAbstractions; using Microsoft.NET.Sdk.WorkloadManifestReader; using NuGet.Common; @@ -112,10 +114,11 @@ public override int Execute() { var packageDownloader = IsPackageDownloaderProvided ? PackageDownloader : new NuGetPackageDownloader.NuGetPackageDownloader( TempPackagesDirectory, + DotNetConfigurationFactory.Create(), filePermissionSetter: null, - new FirstPartyNuGetPackageSigningVerifier(), - new NullLogger(), - NullReporter.Instance, + firstPartyNuGetPackageSigningVerifier: new FirstPartyNuGetPackageSigningVerifier(), + verboseLogger: new NullLogger(), + reporter: NullReporter.Instance, restoreActionConfig: RestoreActionConfiguration, verifySignatures: VerifySignatures); diff --git a/src/Cli/dotnet/Commands/Workload/Install/WorkloadManifestUpdater.cs b/src/Cli/dotnet/Commands/Workload/Install/WorkloadManifestUpdater.cs index 8ed500786ec7..a19cab6a3771 100644 --- a/src/Cli/dotnet/Commands/Workload/Install/WorkloadManifestUpdater.cs +++ b/src/Cli/dotnet/Commands/Workload/Install/WorkloadManifestUpdater.cs @@ -6,11 +6,13 @@ using System.Text.Json; using Microsoft.DotNet.Cli.Commands.Workload.Install.WorkloadInstallRecords; using Microsoft.DotNet.Cli.Commands.Workload.List; +using Microsoft.DotNet.Cli.Configuration; using Microsoft.DotNet.Cli.NuGetPackageDownloader; using Microsoft.DotNet.Cli.ToolPackage; using Microsoft.DotNet.Cli.Utils; using Microsoft.DotNet.Cli.Utils.Extensions; using Microsoft.DotNet.Configurer; +using Microsoft.Extensions.Configuration.DotnetCli.Services; using Microsoft.Extensions.EnvironmentAbstractions; using Microsoft.NET.Sdk.WorkloadManifestReader; using NuGet.Common; @@ -66,10 +68,11 @@ private static WorkloadManifestUpdater GetInstance(string userProfileDir) var workloadResolver = WorkloadResolver.Create(workloadManifestProvider, dotnetPath, sdkVersion, userProfileDir); var tempPackagesDir = new DirectoryPath(PathUtilities.CreateTempSubdirectory()); var nugetPackageDownloader = new NuGetPackageDownloader.NuGetPackageDownloader(tempPackagesDir, + DotNetConfigurationFactory.Create(), filePermissionSetter: null, - new FirstPartyNuGetPackageSigningVerifier(), - new NullLogger(), - reporter, + firstPartyNuGetPackageSigningVerifier: new FirstPartyNuGetPackageSigningVerifier(), + verboseLogger: new NullLogger(), + reporter: reporter, verifySignatures: SignCheck.IsDotNetSigned()); var installer = WorkloadInstallerFactory.GetWorkloadInstaller(reporter, new SdkFeatureBand(sdkVersion), workloadResolver, VerbosityOptions.normal, userProfileDir, verifySignatures: false); diff --git a/src/Cli/dotnet/Commands/Workload/Update/WorkloadUpdateCommand.cs b/src/Cli/dotnet/Commands/Workload/Update/WorkloadUpdateCommand.cs index a8d35305f974..02f78b88a4a2 100644 --- a/src/Cli/dotnet/Commands/Workload/Update/WorkloadUpdateCommand.cs +++ b/src/Cli/dotnet/Commands/Workload/Update/WorkloadUpdateCommand.cs @@ -6,9 +6,11 @@ using System.CommandLine; using System.Text.Json; using Microsoft.DotNet.Cli.Commands.Workload.Install; +using Microsoft.DotNet.Cli.Configuration; using Microsoft.DotNet.Cli.NuGetPackageDownloader; using Microsoft.DotNet.Cli.ToolPackage; using Microsoft.DotNet.Cli.Utils; +using Microsoft.Extensions.Configuration.DotnetCli.Services; using Microsoft.Extensions.EnvironmentAbstractions; using Microsoft.NET.Sdk.WorkloadManifestReader; using NuGet.Common; @@ -85,10 +87,11 @@ public override int Execute() { var packageDownloader = IsPackageDownloaderProvided ? PackageDownloader : new NuGetPackageDownloader.NuGetPackageDownloader( TempPackagesDirectory, + DotNetConfigurationFactory.Create(), filePermissionSetter: null, - new FirstPartyNuGetPackageSigningVerifier(), - new NullLogger(), - NullReporter.Instance, + firstPartyNuGetPackageSigningVerifier: new FirstPartyNuGetPackageSigningVerifier(), + verboseLogger: new NullLogger(), + reporter: NullReporter.Instance, restoreActionConfig: RestoreActionConfiguration, verifySignatures: VerifySignatures); diff --git a/src/Cli/dotnet/Commands/Workload/WorkloadCommandBase.cs b/src/Cli/dotnet/Commands/Workload/WorkloadCommandBase.cs index 44b441349be3..8e5327112a02 100644 --- a/src/Cli/dotnet/Commands/Workload/WorkloadCommandBase.cs +++ b/src/Cli/dotnet/Commands/Workload/WorkloadCommandBase.cs @@ -3,9 +3,11 @@ using System.CommandLine; using Microsoft.DotNet.Cli.Commands.Workload.Install; +using Microsoft.DotNet.Cli.Configuration; using Microsoft.DotNet.Cli.Extensions; using Microsoft.DotNet.Cli.NuGetPackageDownloader; using Microsoft.DotNet.Cli.Utils; +using Microsoft.Extensions.Configuration.DotnetCli.Services; using Microsoft.Extensions.EnvironmentAbstractions; using NuGet.Common; @@ -120,10 +122,11 @@ public WorkloadCommandBase( IsPackageDownloaderProvided = false; PackageDownloader = new NuGetPackageDownloader.NuGetPackageDownloader( TempPackagesDirectory, + DotNetConfigurationFactory.Create(), filePermissionSetter: null, - new FirstPartyNuGetPackageSigningVerifier(), - nugetLogger, - Reporter, + firstPartyNuGetPackageSigningVerifier: new FirstPartyNuGetPackageSigningVerifier(), + verboseLogger: nugetLogger, + reporter: Reporter, restoreActionConfig: RestoreActionConfiguration, verifySignatures: VerifySignatures, shouldUsePackageSourceMapping: true); diff --git a/src/Cli/dotnet/Commands/Workload/WorkloadIntegrityChecker.cs b/src/Cli/dotnet/Commands/Workload/WorkloadIntegrityChecker.cs index add9f8a7a261..4f522d0ca0a0 100644 --- a/src/Cli/dotnet/Commands/Workload/WorkloadIntegrityChecker.cs +++ b/src/Cli/dotnet/Commands/Workload/WorkloadIntegrityChecker.cs @@ -4,7 +4,9 @@ #nullable disable using Microsoft.DotNet.Cli.Commands.Workload.Install; +using Microsoft.DotNet.Cli.Configuration; using Microsoft.DotNet.Cli.Utils; +using Microsoft.Extensions.Configuration.DotnetCli.Services; using Microsoft.Extensions.EnvironmentAbstractions; using Microsoft.NET.Sdk.WorkloadManifestReader; using NuGet.Common; @@ -21,6 +23,7 @@ public static void RunFirstUseCheck(IReporter reporter) var tempPackagesDirectory = new DirectoryPath(PathUtilities.CreateTempSubdirectory()); var packageDownloader = new NuGetPackageDownloader.NuGetPackageDownloader( tempPackagesDirectory, + DotNetConfigurationFactory.Create(), verboseLogger: new NullLogger(), verifySignatures: verifySignatures); diff --git a/src/Cli/dotnet/NugetPackageDownloader/NuGetPackageDownloader.cs b/src/Cli/dotnet/NugetPackageDownloader/NuGetPackageDownloader.cs index 8ac89af40212..7c6809fd18cc 100644 --- a/src/Cli/dotnet/NugetPackageDownloader/NuGetPackageDownloader.cs +++ b/src/Cli/dotnet/NugetPackageDownloader/NuGetPackageDownloader.cs @@ -4,9 +4,11 @@ #nullable disable using System.Collections.Concurrent; +using Microsoft.DotNet.Cli.Configuration; using Microsoft.DotNet.Cli.NugetPackageDownloader; using Microsoft.DotNet.Cli.ToolPackage; using Microsoft.DotNet.Cli.Utils; +using Microsoft.Extensions.Configuration.DotnetCli.Services; using Microsoft.Extensions.EnvironmentAbstractions; using NuGet.Common; using NuGet.Configuration; @@ -51,6 +53,7 @@ internal class NuGetPackageDownloader : INuGetPackageDownloader public NuGetPackageDownloader( DirectoryPath packageInstallDir, + IDotNetConfigurationService configurationService, IFilePermissionSetter filePermissionSetter = null, IFirstPartyNuGetPackageSigningVerifier firstPartyNuGetPackageSigningVerifier = null, ILogger verboseLogger = null, @@ -72,9 +75,8 @@ public NuGetPackageDownloader( _restoreActionConfig = restoreActionConfig ?? new RestoreActionConfig(); _retryTimer = timer; _sourceRepositories = new(); - // If windows or env variable is set, verify signatures - _verifySignatures = verifySignatures && (OperatingSystem.IsWindows() ? true - : bool.TryParse(Environment.GetEnvironmentVariable(NuGetSignatureVerificationEnabler.DotNetNuGetSignatureVerification), out var shouldVerifySignature) ? shouldVerifySignature : OperatingSystem.IsLinux()); + // Use configuration service for signature verification + _verifySignatures = verifySignatures && configurationService.NuGet.SignatureVerificationEnabled; _cacheSettings = new SourceCacheContext { diff --git a/src/Cli/dotnet/ReleasePropertyProjectLocator.cs b/src/Cli/dotnet/ReleasePropertyProjectLocator.cs index ee9ce79b1593..811fdec47650 100644 --- a/src/Cli/dotnet/ReleasePropertyProjectLocator.cs +++ b/src/Cli/dotnet/ReleasePropertyProjectLocator.cs @@ -6,6 +6,7 @@ using System.Diagnostics; using Microsoft.Build.Execution; using Microsoft.DotNet.Cli.Utils; +using Microsoft.Extensions.Configuration.DotnetCli.Services; using Microsoft.NET.Build.Tasks; using Microsoft.VisualStudio.SolutionPersistence.Model; @@ -31,6 +32,7 @@ public DependentCommandOptions(IEnumerable? slnOrProjectArgs, string? co private readonly ParseResult _parseResult; private readonly string _propertyToCheck; + private readonly IDotNetConfigurationService _configurationService; DependentCommandOptions _options; private readonly IEnumerable _slnOrProjectArgs; @@ -45,9 +47,10 @@ public DependentCommandOptions(IEnumerable? slnOrProjectArgs, string? co public ReleasePropertyProjectLocator( ParseResult parseResult, string propertyToCheck, - DependentCommandOptions commandOptions + DependentCommandOptions commandOptions, + IDotNetConfigurationService configurationService ) - => (_parseResult, _propertyToCheck, _options, _slnOrProjectArgs) = (parseResult, propertyToCheck, commandOptions, commandOptions.SlnOrProjectArgs); + => (_parseResult, _propertyToCheck, _options, _slnOrProjectArgs, _configurationService) = (parseResult, propertyToCheck, commandOptions, commandOptions.SlnOrProjectArgs, configurationService); /// /// Return dotnet CLI command-line parameters (or an empty list) to change configuration based on ... @@ -58,7 +61,7 @@ DependentCommandOptions commandOptions { // Setup Debug.Assert(_propertyToCheck == MSBuildPropertyNames.PUBLISH_RELEASE || _propertyToCheck == MSBuildPropertyNames.PACK_RELEASE, "Only PackRelease or PublishRelease are currently expected."); - if (string.Equals(Environment.GetEnvironmentVariable(EnvironmentVariableNames.DISABLE_PUBLISH_AND_PACK_RELEASE), "true", StringComparison.OrdinalIgnoreCase)) + if (_configurationService.Build.DisablePublishAndPackRelease) { return null; } @@ -160,7 +163,7 @@ DependentCommandOptions commandOptions HashSet configValues = []; object projectDataLock = new(); - if (string.Equals(Environment.GetEnvironmentVariable(EnvironmentVariableNames.DOTNET_CLI_LAZY_PUBLISH_AND_PACK_RELEASE_FOR_SOLUTIONS), "true", StringComparison.OrdinalIgnoreCase)) + if (_configurationService.Build.LazyPublishAndPackReleaseForSolutions) { // Evaluate only one project for speed if this environment variable is used. Will break more customers if enabled (adding 8.0 project to SLN with other project TFMs with no Publish or PackRelease.) return GetSingleProjectFromSolution(sln, slnFullPath, globalProps); diff --git a/src/Cli/dotnet/ToolPackage/ToolPackageDownloader.cs b/src/Cli/dotnet/ToolPackage/ToolPackageDownloader.cs index 4ed187cc92dc..c56cc81cc3e1 100644 --- a/src/Cli/dotnet/ToolPackage/ToolPackageDownloader.cs +++ b/src/Cli/dotnet/ToolPackage/ToolPackageDownloader.cs @@ -1,9 +1,11 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. +using Microsoft.DotNet.Cli.Configuration; using Microsoft.DotNet.Cli.Extensions; using Microsoft.DotNet.Cli.NuGetPackageDownloader; using Microsoft.DotNet.Cli.Utils; +using Microsoft.Extensions.Configuration.DotnetCli.Services; using Microsoft.Extensions.EnvironmentAbstractions; using Microsoft.TemplateEngine.Utils; using NuGet.Client; @@ -41,8 +43,10 @@ protected override INuGetPackageDownloader CreateNuGetPackageDownloader( verboseLogger = new NuGetConsoleLogger(); } + var configurationService = DotNetConfigurationFactory.Create(); return new NuGetPackageDownloader.NuGetPackageDownloader( new DirectoryPath(), + configurationService, verboseLogger: verboseLogger, verifySignatures: verifySignatures, shouldUsePackageSourceMapping: true, diff --git a/src/Microsoft.Extensions.Configuration.DotnetCli/Models/NuGetConfiguration.cs b/src/Microsoft.Extensions.Configuration.DotnetCli/Models/NuGetConfiguration.cs new file mode 100644 index 000000000000..d49db7afbfa3 --- /dev/null +++ b/src/Microsoft.Extensions.Configuration.DotnetCli/Models/NuGetConfiguration.cs @@ -0,0 +1,17 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.Configuration.DotnetCli.Models; + +/// +/// Configuration settings that control NuGet package management behavior. +/// +public sealed class NuGetConfiguration +{ + /// + /// Gets or sets whether NuGet signature verification is enabled. + /// Mapped from DOTNET_NUGET_SIGNATURE_VERIFICATION environment variable. + /// Defaults to true on Windows and Linux, false elsewhere. + /// + public bool SignatureVerificationEnabled { get; set; } = OperatingSystem.IsWindows() || OperatingSystem.IsLinux(); +} diff --git a/src/Microsoft.Extensions.Configuration.DotnetCli/Providers/DotNetEnvironmentConfigurationProvider.cs b/src/Microsoft.Extensions.Configuration.DotnetCli/Providers/DotNetEnvironmentConfigurationProvider.cs index 60e2cea93855..037c75ae4a35 100644 --- a/src/Microsoft.Extensions.Configuration.DotnetCli/Providers/DotNetEnvironmentConfigurationProvider.cs +++ b/src/Microsoft.Extensions.Configuration.DotnetCli/Providers/DotNetEnvironmentConfigurationProvider.cs @@ -46,6 +46,7 @@ public class DotNetEnvironmentConfigurationProvider : ConfigurationProvider ["DOTNET_ADD_GLOBAL_TOOLS_TO_PATH"] = "FirstTimeUse:AddGlobalToolsToPath", ["DOTNET_SKIP_FIRST_TIME_EXPERIENCE"] = "FirstTimeUse:SkipFirstTimeExperience", ["DOTNET_TOOLS_ALLOW_MANIFEST_IN_ROOT"] = "Tool:AllowManifestInRoot", + ["DOTNET_NUGET_SIGNATURE_VERIFICATION"] = "NuGet:SignatureVerificationEnabled", }; public override void Load() diff --git a/src/Microsoft.Extensions.Configuration.DotnetCli/Services/DotNetConfigurationService.cs b/src/Microsoft.Extensions.Configuration.DotnetCli/Services/DotNetConfigurationService.cs index 1b4217fa7066..76ab74ad1477 100644 --- a/src/Microsoft.Extensions.Configuration.DotnetCli/Services/DotNetConfigurationService.cs +++ b/src/Microsoft.Extensions.Configuration.DotnetCli/Services/DotNetConfigurationService.cs @@ -21,6 +21,7 @@ public class DotNetConfigurationService : IDotNetConfigurationService private readonly Lazy _firstTimeUse; private readonly Lazy _development; private readonly Lazy _tool; + private readonly Lazy _nuget; public IConfiguration RawConfiguration => _configuration; @@ -33,6 +34,7 @@ public class DotNetConfigurationService : IDotNetConfigurationService public FirstTimeUseConfiguration FirstTimeUse => _firstTimeUse.Value; public DevelopmentConfiguration Development => _development.Value; public ToolConfiguration Tool => _tool.Value; + public NuGetConfiguration NuGet => _nuget.Value; public DotNetConfigurationService(IConfiguration configuration) { @@ -55,5 +57,7 @@ public DotNetConfigurationService(IConfiguration configuration) _configuration.GetSection("Development").Get() ?? new()); _tool = new Lazy(() => _configuration.GetSection("Tool").Get() ?? new()); + _nuget = new Lazy(() => + _configuration.GetSection("NuGet").Get() ?? new()); } } diff --git a/src/Microsoft.Extensions.Configuration.DotnetCli/Services/IDotNetConfigurationService.cs b/src/Microsoft.Extensions.Configuration.DotnetCli/Services/IDotNetConfigurationService.cs index a15db541778a..b9db5fa0852e 100644 --- a/src/Microsoft.Extensions.Configuration.DotnetCli/Services/IDotNetConfigurationService.cs +++ b/src/Microsoft.Extensions.Configuration.DotnetCli/Services/IDotNetConfigurationService.cs @@ -54,4 +54,9 @@ public interface IDotNetConfigurationService /// Gets global tools configuration settings. /// ToolConfiguration Tool { get; } + + /// + /// Gets NuGet package management configuration settings. + /// + NuGetConfiguration NuGet { get; } } From e696b0fa31d73c2504de38e42a9a88addf0d920b Mon Sep 17 00:00:00 2001 From: Chet Husk Date: Sun, 13 Jul 2025 12:57:43 -0500 Subject: [PATCH 07/12] cache repeated accesses --- .../dotnet/Commands/Test/TestCommandParser.cs | 46 ++------------ .../DotNetConfigurationFactory.cs | 38 +++++++++++- .../Models/TestConfiguration.cs | 17 +++++ .../Providers/DotNetConfigurationProvider.cs | 62 +++++++++++++++++++ .../Providers/DotNetConfigurationSource.cs | 45 ++++++++++++++ .../Services/DotNetConfiguration.cs | 8 +-- .../Services/DotNetConfigurationService.cs | 4 ++ .../Services/IDotNetConfigurationService.cs | 5 ++ 8 files changed, 175 insertions(+), 50 deletions(-) create mode 100644 src/Microsoft.Extensions.Configuration.DotnetCli/Models/TestConfiguration.cs create mode 100644 src/Microsoft.Extensions.Configuration.DotnetCli/Providers/DotNetConfigurationProvider.cs create mode 100644 src/Microsoft.Extensions.Configuration.DotnetCli/Providers/DotNetConfigurationSource.cs diff --git a/src/Cli/dotnet/Commands/Test/TestCommandParser.cs b/src/Cli/dotnet/Commands/Test/TestCommandParser.cs index 9cf9b5e7bafe..56c5c5f0023d 100644 --- a/src/Cli/dotnet/Commands/Test/TestCommandParser.cs +++ b/src/Cli/dotnet/Commands/Test/TestCommandParser.cs @@ -5,6 +5,8 @@ using System.Diagnostics; using Microsoft.DotNet.Cli.Extensions; using Microsoft.Extensions.Configuration; +using Microsoft.DotNet.Cli.Configuration; +using Microsoft.Extensions.Configuration.DotnetCli.Services; namespace Microsoft.DotNet.Cli.Commands.Test; @@ -166,48 +168,8 @@ public static Command GetCommand() public static string GetTestRunnerName() { - var builder = new ConfigurationBuilder(); - - string? dotnetConfigPath = GetDotnetConfigPath(Environment.CurrentDirectory); - if (!File.Exists(dotnetConfigPath)) - { - return CliConstants.VSTest; - } - - builder.AddIniFile(dotnetConfigPath); - - IConfigurationRoot config = builder.Build(); - var testSection = config.GetSection("dotnet.test.runner"); - - if (!testSection.Exists()) - { - return CliConstants.VSTest; - } - - string? runnerNameSection = testSection["name"]; - - if (string.IsNullOrEmpty(runnerNameSection)) - { - return CliConstants.VSTest; - } - - return runnerNameSection; - } - - private static string? GetDotnetConfigPath(string? startDir) - { - string? directory = startDir; - while (directory != null) - { - string dotnetConfigPath = Path.Combine(directory, "dotnet.config"); - if (File.Exists(dotnetConfigPath)) - { - return dotnetConfigPath; - } - - directory = Path.GetDirectoryName(directory); - } - return null; + var configurationService = DotNetConfigurationFactory.Create(); + return configurationService.Test.RunnerName; } private static Command ConstructCommand() diff --git a/src/Cli/dotnet/Configuration/DotNetConfigurationFactory.cs b/src/Cli/dotnet/Configuration/DotNetConfigurationFactory.cs index 84b7df596371..14938c497492 100644 --- a/src/Cli/dotnet/Configuration/DotNetConfigurationFactory.cs +++ b/src/Cli/dotnet/Configuration/DotNetConfigurationFactory.cs @@ -5,6 +5,7 @@ using Microsoft.Extensions.Configuration.DotnetCli.Models; using Microsoft.Extensions.Configuration.DotnetCli.Providers; using Microsoft.Extensions.Configuration; +using System.Collections.Concurrent; using System.IO; namespace Microsoft.DotNet.Cli.Configuration @@ -15,14 +16,44 @@ namespace Microsoft.DotNet.Cli.Configuration /// public static class DotNetConfigurationFactory { + // Cache for the default configuration service instance + private static readonly Lazy _defaultInstance = new(() => CreateInternal(null), LazyThreadSafetyMode.ExecutionAndPublication); + + // Cache for configuration services by working directory + private static readonly ConcurrentDictionary> _instancesByDirectory = new(); + /// /// Creates and configures the .NET CLI configuration service with default providers. /// This method follows the layered configuration approach: environment variables override global.json, /// and configuration is loaded lazily for performance. + /// Results are cached to avoid repeated expensive configuration building. /// /// The working directory to search for global.json files. Defaults to current directory. /// A configured IDotNetConfigurationService instance public static IDotNetConfigurationService Create(string? workingDirectory = null) + { + if (workingDirectory == null) + { + // Use the default cached instance for null working directory + return _defaultInstance.Value; + } + + // Normalize the working directory path for consistent caching + var normalizedPath = Path.GetFullPath(workingDirectory); + + // Get or create a cached instance for this specific working directory + var lazyInstance = _instancesByDirectory.GetOrAdd(normalizedPath, + path => new Lazy(() => CreateInternal(path), LazyThreadSafetyMode.ExecutionAndPublication)); + + return lazyInstance.Value; + } + + /// + /// Internal method that performs the actual configuration creation without caching. + /// + /// The working directory to search for configuration files. + /// A configured IDotNetConfigurationService instance + private static IDotNetConfigurationService CreateInternal(string? workingDirectory) { workingDirectory ??= Directory.GetCurrentDirectory(); @@ -31,10 +62,13 @@ public static IDotNetConfigurationService Create(string? workingDirectory = null // Configuration sources are added in reverse precedence order // Last added has highest precedence - // 1. global.json (custom provider with key mapping) - lowest precedence + // 1. dotnet.config (custom provider with key mapping) - lowest precedence + configurationBuilder.Add(new DotNetConfigurationSource(workingDirectory)); + + // 2. global.json (custom provider with key mapping) - medium precedence configurationBuilder.Add(new GlobalJsonConfigurationSource(workingDirectory)); - // 2. Environment variables (custom provider with key mapping) - highest precedence + // 3. Environment variables (custom provider with key mapping) - highest precedence configurationBuilder.Add(new DotNetEnvironmentConfigurationSource()); var configuration = configurationBuilder.Build(); diff --git a/src/Microsoft.Extensions.Configuration.DotnetCli/Models/TestConfiguration.cs b/src/Microsoft.Extensions.Configuration.DotnetCli/Models/TestConfiguration.cs new file mode 100644 index 000000000000..14d1b866a9e4 --- /dev/null +++ b/src/Microsoft.Extensions.Configuration.DotnetCli/Models/TestConfiguration.cs @@ -0,0 +1,17 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.Configuration.DotnetCli.Models; + +/// +/// Configuration settings that control test runner behavior. +/// +public sealed class TestConfiguration +{ + /// + /// Gets or sets the test runner name to use. + /// Mapped from dotnet.config file: [dotnet.test.runner] name=VALUE + /// Defaults to "VSTest" if not specified. + /// + public string RunnerName { get; set; } = "VSTest"; +} diff --git a/src/Microsoft.Extensions.Configuration.DotnetCli/Providers/DotNetConfigurationProvider.cs b/src/Microsoft.Extensions.Configuration.DotnetCli/Providers/DotNetConfigurationProvider.cs new file mode 100644 index 000000000000..b855eee64b9d --- /dev/null +++ b/src/Microsoft.Extensions.Configuration.DotnetCli/Providers/DotNetConfigurationProvider.cs @@ -0,0 +1,62 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Configuration.Ini; + +namespace Microsoft.Extensions.Configuration.DotnetCli.Providers; + +/// +/// Configuration provider for dotnet.config INI files with key mapping. +/// Maps dotnet.config keys to the expected configuration structure. +/// +public class DotNetConfigurationProvider : IniConfigurationProvider +{ + private static readonly Dictionary KeyMappings = new(StringComparer.OrdinalIgnoreCase) + { + // Map INI section:key to expected configuration path + ["dotnet.test.runner:name"] = "Test:RunnerName", + + // Future mappings can be added here for other dotnet.config settings + // ["dotnet.example.section:key"] = "ConfigSection:Property", + }; + + /// + /// Initializes a new instance of the class. + /// + /// The configuration source. + public DotNetConfigurationProvider(DotNetConfigurationSource source) : base(source) + { + } + + /// + /// Loads configuration data with key transformation. + /// + public override void Load() + { + // Load the INI file normally first + base.Load(); + + // Transform keys according to our mapping + var transformedData = new Dictionary(StringComparer.OrdinalIgnoreCase); + + foreach (var kvp in Data) + { + string key = kvp.Key; + string? value = kvp.Value; + + // Check if we have a mapping for this key + if (KeyMappings.TryGetValue(key, out string? mappedKey)) + { + transformedData[mappedKey] = value; + } + else + { + // Keep unmapped keys as-is + transformedData[key] = value; + } + } + + // Replace the data with transformed keys + Data = transformedData; + } +} diff --git a/src/Microsoft.Extensions.Configuration.DotnetCli/Providers/DotNetConfigurationSource.cs b/src/Microsoft.Extensions.Configuration.DotnetCli/Providers/DotNetConfigurationSource.cs new file mode 100644 index 000000000000..9d65197f355f --- /dev/null +++ b/src/Microsoft.Extensions.Configuration.DotnetCli/Providers/DotNetConfigurationSource.cs @@ -0,0 +1,45 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Configuration.Ini; + +namespace Microsoft.Extensions.Configuration.DotnetCli.Providers; + +/// +/// Configuration source for dotnet.config INI files with key mapping. +/// +public class DotNetConfigurationSource : IniConfigurationSource +{ + public DotNetConfigurationSource(string workingDirectory) + { + Path = FindDotNetConfigPath(workingDirectory); + Optional = true; // Make it optional since dotnet.config may not exist + } + /// + /// Builds the configuration provider for dotnet.config files. + /// + /// The configuration builder. + /// The configuration provider. + public override IConfigurationProvider Build(IConfigurationBuilder builder) + { + EnsureDefaults(builder); + return new DotNetConfigurationProvider(this); + } + + private static string FindDotNetConfigPath(string? workingDirectory = null) + { + string? directory = workingDirectory ?? Directory.GetCurrentDirectory(); + // Search for dotnet.config in the current directory and upwards + while (directory != null) + { + string dotnetConfigPath = System.IO.Path.Combine(directory, "dotnet.config"); + if (File.Exists(dotnetConfigPath)) + { + return dotnetConfigPath; + } + + directory = System.IO.Path.GetDirectoryName(directory); + } + return "dotnet.config"; // Return default path even if not found + } +} diff --git a/src/Microsoft.Extensions.Configuration.DotnetCli/Services/DotNetConfiguration.cs b/src/Microsoft.Extensions.Configuration.DotnetCli/Services/DotNetConfiguration.cs index b218d28a7734..f43c33dc7893 100644 --- a/src/Microsoft.Extensions.Configuration.DotnetCli/Services/DotNetConfiguration.cs +++ b/src/Microsoft.Extensions.Configuration.DotnetCli/Services/DotNetConfiguration.cs @@ -27,12 +27,8 @@ public static IConfiguration Create(string? workingDirectory = null) workingDirectory ??= Directory.GetCurrentDirectory(); - // Add dotnet.config if it exists (future enhancement) - var dotnetConfigPath = Path.Combine(workingDirectory, "dotnet.config"); - if (File.Exists(dotnetConfigPath)) - { - builder.AddIniFile(dotnetConfigPath, optional: true, reloadOnChange: false); - } + // Add dotnet.config with custom key mapping + builder.Add(new DotNetConfigurationSource(workingDirectory)); // Add global.json with a custom configuration provider that maps keys builder.Add(new GlobalJsonConfigurationSource(workingDirectory)); diff --git a/src/Microsoft.Extensions.Configuration.DotnetCli/Services/DotNetConfigurationService.cs b/src/Microsoft.Extensions.Configuration.DotnetCli/Services/DotNetConfigurationService.cs index 76ab74ad1477..52c5cee7a53c 100644 --- a/src/Microsoft.Extensions.Configuration.DotnetCli/Services/DotNetConfigurationService.cs +++ b/src/Microsoft.Extensions.Configuration.DotnetCli/Services/DotNetConfigurationService.cs @@ -22,6 +22,7 @@ public class DotNetConfigurationService : IDotNetConfigurationService private readonly Lazy _development; private readonly Lazy _tool; private readonly Lazy _nuget; + private readonly Lazy _test; public IConfiguration RawConfiguration => _configuration; @@ -35,6 +36,7 @@ public class DotNetConfigurationService : IDotNetConfigurationService public DevelopmentConfiguration Development => _development.Value; public ToolConfiguration Tool => _tool.Value; public NuGetConfiguration NuGet => _nuget.Value; + public TestConfiguration Test => _test.Value; public DotNetConfigurationService(IConfiguration configuration) { @@ -59,5 +61,7 @@ public DotNetConfigurationService(IConfiguration configuration) _configuration.GetSection("Tool").Get() ?? new()); _nuget = new Lazy(() => _configuration.GetSection("NuGet").Get() ?? new()); + _test = new Lazy(() => + _configuration.GetSection("Test").Get() ?? new()); } } diff --git a/src/Microsoft.Extensions.Configuration.DotnetCli/Services/IDotNetConfigurationService.cs b/src/Microsoft.Extensions.Configuration.DotnetCli/Services/IDotNetConfigurationService.cs index b9db5fa0852e..a4b67f6e2780 100644 --- a/src/Microsoft.Extensions.Configuration.DotnetCli/Services/IDotNetConfigurationService.cs +++ b/src/Microsoft.Extensions.Configuration.DotnetCli/Services/IDotNetConfigurationService.cs @@ -59,4 +59,9 @@ public interface IDotNetConfigurationService /// Gets NuGet package management configuration settings. /// NuGetConfiguration NuGet { get; } + + /// + /// Gets test runner configuration settings. + /// + TestConfiguration Test { get; } } From e472bc40b2af12b20975c0d3f97516c02b107797 Mon Sep 17 00:00:00 2001 From: Chet Husk Date: Sun, 13 Jul 2025 13:37:00 -0500 Subject: [PATCH 08/12] fix compilation errors in tests --- documentation/specs/unified-configuration.md | 8 +- src/Cli/dotnet/Commands/Pack/PackCommand.cs | 6 +- .../Package/Add/PackageAddCommandParser.cs | 75 +++++----- .../dotnet/Commands/Publish/PublishCommand.cs | 5 +- .../dotnet/Commands/Test/TestCommandParser.cs | 9 +- .../ToolInstallGlobalOrToolPathCommand.cs | 63 ++++---- .../Workload/Install/FileBasedInstaller.cs | 74 +++++----- .../Install/NetSdkMsiInstallerClient.cs | 84 +++++------ .../Install/WorkloadInstallCommand.cs | 27 ++-- .../Install/WorkloadManifestUpdater.cs | 27 ++-- .../Workload/Update/WorkloadUpdateCommand.cs | 32 ++-- .../Commands/Workload/WorkloadCommandBase.cs | 3 - .../Workload/WorkloadIntegrityChecker.cs | 5 - .../ConfigurationBasedEnvironmentProvider.cs | 139 ------------------ .../DotNetConfigurationFactory.cs | 93 ------------ .../NuGetPackageDownloader.cs | 123 +++++++--------- .../WorkloadUnixFilePermissionsFileList.cs | 2 +- src/Cli/dotnet/Program.cs | 26 ++-- .../dotnet/ReleasePropertyProjectLocator.cs | 8 +- .../ShellShim/ShellShimTemplateFinder.cs | 8 +- .../ToolPackage/ToolPackageDownloader.cs | 4 - .../{Services => }/DotNetConfiguration.cs | 10 +- .../DotNetConfigurationFactory.cs | 88 +++++++++++ .../DotNetConfigurationService.cs | 8 +- .../Services/IDotNetConfigurationService.cs | 67 --------- .../NuGetPackageInstallerExtractTests.cs | 21 ++- .../NuGetPackageInstallerTests.cs | 66 ++++++--- .../UnixFilePermissionsTests.cs | 2 +- .../ToolPackageDownloaderMock2.cs | 2 +- ...ToolInstallGlobalOrToolPathCommandTests.cs | 20 +-- 30 files changed, 437 insertions(+), 668 deletions(-) delete mode 100644 src/Cli/dotnet/Configuration/ConfigurationBasedEnvironmentProvider.cs delete mode 100644 src/Cli/dotnet/Configuration/DotNetConfigurationFactory.cs rename src/Microsoft.Extensions.Configuration.DotnetCli/{Services => }/DotNetConfiguration.cs (87%) create mode 100644 src/Microsoft.Extensions.Configuration.DotnetCli/DotNetConfigurationFactory.cs rename src/Microsoft.Extensions.Configuration.DotnetCli/{Services => }/DotNetConfigurationService.cs (92%) delete mode 100644 src/Microsoft.Extensions.Configuration.DotnetCli/Services/IDotNetConfigurationService.cs diff --git a/documentation/specs/unified-configuration.md b/documentation/specs/unified-configuration.md index b64981e0900b..3f1ab3fc0ad0 100644 --- a/documentation/specs/unified-configuration.md +++ b/documentation/specs/unified-configuration.md @@ -606,7 +606,7 @@ src/Microsoft.Extensions.Configuration.DotnetCli/ └── Services/ ├── DotNetConfiguration.cs ├── DotNetConfigurationRoot.cs - ├── IDotNetConfigurationService.cs + ├── DotNetConfigurationService.cs └── DotNetConfigurationService.cs ``` @@ -855,12 +855,12 @@ public sealed class ToolConfiguration ### Strongly-Typed Configuration Service with Source Generator ```csharp -// src/Microsoft.Extensions.Configuration.DotnetCli/Services/IDotNetConfigurationService.cs +// src/Microsoft.Extensions.Configuration.DotnetCli/Services/DotNetConfigurationService.cs namespace Microsoft.Extensions.Configuration.DotnetCli.Services; using Microsoft.Extensions.Configuration.DotnetCli.Models; -public interface IDotNetConfigurationService +public interface DotNetConfigurationService { IConfiguration RawConfiguration { get; } @@ -876,7 +876,7 @@ public interface IDotNetConfigurationService } // src/Microsoft.Extensions.Configuration.DotnetCli/Services/DotNetConfigurationService.cs -public class DotNetConfigurationService : IDotNetConfigurationService +public class DotNetConfigurationService : DotNetConfigurationService { private readonly IConfiguration _configuration; diff --git a/src/Cli/dotnet/Commands/Pack/PackCommand.cs b/src/Cli/dotnet/Commands/Pack/PackCommand.cs index 355e95ee314d..1ab9f223d40a 100644 --- a/src/Cli/dotnet/Commands/Pack/PackCommand.cs +++ b/src/Cli/dotnet/Commands/Pack/PackCommand.cs @@ -3,9 +3,9 @@ using System.CommandLine; using Microsoft.DotNet.Cli.Commands.Restore; -using Microsoft.DotNet.Cli.Configuration; using Microsoft.DotNet.Cli.Extensions; using Microsoft.DotNet.Cli.Utils; +using Microsoft.Extensions.Configuration.DotnetCli; namespace Microsoft.DotNet.Cli.Commands.Pack; @@ -28,13 +28,11 @@ public static PackCommand FromParseResult(ParseResult parseResult, string? msbui var msbuildArgs = parseResult.OptionValuesToBeForwarded(PackCommandParser.GetCommand()).Concat(parseResult.GetValue(PackCommandParser.SlnOrProjectArgument) ?? []); - var configurationService = DotNetConfigurationFactory.Create(); ReleasePropertyProjectLocator projectLocator = new(parseResult, MSBuildPropertyNames.PACK_RELEASE, new ReleasePropertyProjectLocator.DependentCommandOptions( parseResult.GetValue(PackCommandParser.SlnOrProjectArgument), parseResult.HasOption(PackCommandParser.ConfigurationOption) ? parseResult.GetValue(PackCommandParser.ConfigurationOption) : null - ), configurationService - ); + )); bool noRestore = parseResult.HasOption(PackCommandParser.NoRestoreOption) || parseResult.HasOption(PackCommandParser.NoBuildOption); var parsedMSBuildArgs = MSBuildArgs.AnalyzeMSBuildArguments( diff --git a/src/Cli/dotnet/Commands/Package/Add/PackageAddCommandParser.cs b/src/Cli/dotnet/Commands/Package/Add/PackageAddCommandParser.cs index fc97a3061e2b..e393fd389e6c 100644 --- a/src/Cli/dotnet/Commands/Package/Add/PackageAddCommandParser.cs +++ b/src/Cli/dotnet/Commands/Package/Add/PackageAddCommandParser.cs @@ -1,14 +1,11 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -#nullable disable - using System.CommandLine; using System.CommandLine.Completions; using System.CommandLine.Parsing; -using Microsoft.DotNet.Cli.Configuration; using Microsoft.DotNet.Cli.Extensions; -using Microsoft.Extensions.Configuration.DotnetCli.Services; +using Microsoft.Extensions.Configuration.DotnetCli; using Microsoft.Extensions.EnvironmentAbstractions; using NuGet.Versioning; @@ -16,36 +13,6 @@ namespace Microsoft.DotNet.Cli.Commands.Package.Add; public static class PackageAddCommandParser { - public static readonly Argument CmdPackageArgument = CommonArguments.RequiredPackageIdentityArgument() - .AddCompletions((context) => - { - // we should take --prerelease flags into account for version completion - var allowPrerelease = context.ParseResult.GetValue(PrereleaseOption); - return QueryNuGet(context.WordToComplete, allowPrerelease, CancellationToken.None).Result.Select(packageId => new CompletionItem(packageId)); - }); - - public static readonly Option VersionOption = new DynamicForwardedOption("--version", "-v") - { - Description = CliCommandStrings.CmdVersionDescription, - HelpName = CliCommandStrings.CmdVersion - }.ForwardAsSingle(o => $"--version {o}") - .AddCompletions((context) => - { - // we can only do version completion if we have a package id - if (context.ParseResult.GetValue(CmdPackageArgument) is { HasVersion: false } packageId) - { - // we should take --prerelease flags into account for version completion - var allowPrerelease = context.ParseResult.GetValue(PrereleaseOption); - return QueryVersionsForPackage(packageId.Id, context.WordToComplete, allowPrerelease, CancellationToken.None) - .Result - .Select(version => new CompletionItem(version.ToNormalizedString())); - } - else - { - return []; - } - }); - public static readonly Option FrameworkOption = new ForwardedOption("--framework", "-f") { Description = CliCommandStrings.PackageAddCmdFrameworkDescription, @@ -78,6 +45,36 @@ public static class PackageAddCommandParser Arity = ArgumentArity.Zero }.ForwardAs("--prerelease"); + public static readonly Argument CmdPackageArgument = CommonArguments.RequiredPackageIdentityArgument() + .AddCompletions((context) => + { + // we should take --prerelease flags into account for version completion + var allowPrerelease = context.ParseResult.GetValue(PrereleaseOption); + return QueryNuGet(context.WordToComplete, allowPrerelease, CancellationToken.None).Result.Select(packageId => new CompletionItem(packageId)); + }); + + public static readonly Option VersionOption = new DynamicForwardedOption("--version", "-v") + { + Description = CliCommandStrings.CmdVersionDescription, + HelpName = CliCommandStrings.CmdVersion + }.ForwardAsSingle(o => $"--version {o}") + .AddCompletions((context) => + { + // we can only do version completion if we have a package id + if (context.ParseResult.GetValue(CmdPackageArgument) is { HasVersion: false } packageId) + { + // we should take --prerelease flags into account for version completion + var allowPrerelease = context.ParseResult.GetValue(PrereleaseOption); + return QueryVersionsForPackage(packageId.Id, context.WordToComplete, allowPrerelease, CancellationToken.None) + .Result + .Select(version => new CompletionItem(version.ToNormalizedString())); + } + else + { + return []; + } + }); + private static readonly Command Command = ConstructCommand(); public static Command GetCommand() @@ -107,17 +104,18 @@ private static Command ConstructCommand() private static void DisallowVersionIfPackageIdentityHasVersionValidator(OptionResult result) { - if (result.Parent.GetValue(CmdPackageArgument).HasVersion) + if (result.Parent!.GetValue(CmdPackageArgument).HasVersion) { result.AddError(CliCommandStrings.ValidationFailedDuplicateVersion); } } - public static async Task> QueryNuGet(string packageStem, bool allowPrerelease, CancellationToken cancellationToken) + // Only called during tab-completions, so this is allowed can hack/create singleton members like the configuration/downloader + internal static async Task> QueryNuGet(string packageStem, bool allowPrerelease, CancellationToken cancellationToken) { try { - var downloader = new NuGetPackageDownloader.NuGetPackageDownloader(packageInstallDir: new DirectoryPath(), configurationService: DotNetConfigurationFactory.Create()); + var downloader = new NuGetPackageDownloader.NuGetPackageDownloader(packageInstallDir: new DirectoryPath()); var versions = await downloader.GetPackageIdsAsync(packageStem, allowPrerelease, cancellationToken: cancellationToken); return versions; } @@ -127,11 +125,12 @@ public static async Task> QueryNuGet(string packageStem, boo } } + // Only called during tab-completions, so this is allowed can hack/create singleton members like the configuration/downloader internal static async Task> QueryVersionsForPackage(string packageId, string versionFragment, bool allowPrerelease, CancellationToken cancellationToken) { try { - var downloader = new NuGetPackageDownloader.NuGetPackageDownloader(packageInstallDir: new DirectoryPath(), configurationService: DotNetConfigurationFactory.Create()); + var downloader = new NuGetPackageDownloader.NuGetPackageDownloader(packageInstallDir: new DirectoryPath()); var versions = await downloader.GetPackageVersionsAsync(new(packageId), versionFragment, allowPrerelease, cancellationToken: cancellationToken); return versions; } diff --git a/src/Cli/dotnet/Commands/Publish/PublishCommand.cs b/src/Cli/dotnet/Commands/Publish/PublishCommand.cs index 1773427cc8ca..f472fb805dfc 100644 --- a/src/Cli/dotnet/Commands/Publish/PublishCommand.cs +++ b/src/Cli/dotnet/Commands/Publish/PublishCommand.cs @@ -4,9 +4,9 @@ using System.CommandLine; using Microsoft.DotNet.Cli.Commands.Restore; using Microsoft.DotNet.Cli.Commands.Run; -using Microsoft.DotNet.Cli.Configuration; using Microsoft.DotNet.Cli.Extensions; using Microsoft.DotNet.Cli.Utils; +using Microsoft.Extensions.Configuration.DotnetCli; namespace Microsoft.DotNet.Cli.Commands.Publish; @@ -57,13 +57,12 @@ public static CommandBase FromParseResult(ParseResult parseResult, string? msbui NoCache = true, }, (msbuildArgs, msbuildPath) => { - var configurationService = DotNetConfigurationFactory.Create(); var options = new ReleasePropertyProjectLocator.DependentCommandOptions( nonBinLogArgs, parseResult.HasOption(PublishCommandParser.ConfigurationOption) ? parseResult.GetValue(PublishCommandParser.ConfigurationOption) : null, parseResult.HasOption(PublishCommandParser.FrameworkOption) ? parseResult.GetValue(PublishCommandParser.FrameworkOption) : null ); - var projectLocator = new ReleasePropertyProjectLocator(parseResult, MSBuildPropertyNames.PUBLISH_RELEASE, options, configurationService); + var projectLocator = new ReleasePropertyProjectLocator(parseResult, MSBuildPropertyNames.PUBLISH_RELEASE, options); var releaseModeProperties = projectLocator.GetCustomDefaultConfigurationValueIfSpecified(); return new PublishCommand( msbuildArgs: msbuildArgs.CloneWithAdditionalProperties(releaseModeProperties), diff --git a/src/Cli/dotnet/Commands/Test/TestCommandParser.cs b/src/Cli/dotnet/Commands/Test/TestCommandParser.cs index 56c5c5f0023d..3358c9471eb0 100644 --- a/src/Cli/dotnet/Commands/Test/TestCommandParser.cs +++ b/src/Cli/dotnet/Commands/Test/TestCommandParser.cs @@ -5,8 +5,7 @@ using System.Diagnostics; using Microsoft.DotNet.Cli.Extensions; using Microsoft.Extensions.Configuration; -using Microsoft.DotNet.Cli.Configuration; -using Microsoft.Extensions.Configuration.DotnetCli.Services; +using Microsoft.Extensions.Configuration.DotnetCli; namespace Microsoft.DotNet.Cli.Commands.Test; @@ -166,11 +165,7 @@ public static Command GetCommand() return Command; } - public static string GetTestRunnerName() - { - var configurationService = DotNetConfigurationFactory.Create(); - return configurationService.Test.RunnerName; - } + public static string GetTestRunnerName() => DotNetConfigurationFactory.Create().Test.RunnerName; private static Command ConstructCommand() { diff --git a/src/Cli/dotnet/Commands/Tool/Install/ToolInstallGlobalOrToolPathCommand.cs b/src/Cli/dotnet/Commands/Tool/Install/ToolInstallGlobalOrToolPathCommand.cs index c3c860f57b75..488f8589b1a8 100644 --- a/src/Cli/dotnet/Commands/Tool/Install/ToolInstallGlobalOrToolPathCommand.cs +++ b/src/Cli/dotnet/Commands/Tool/Install/ToolInstallGlobalOrToolPathCommand.cs @@ -1,15 +1,10 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -#nullable disable - using System.CommandLine; -using System.Transactions; -using Microsoft.DotNet.Cli.Configuration; using Microsoft.DotNet.Cli.NuGetPackageDownloader; using Microsoft.DotNet.Cli.ToolPackage; using Microsoft.DotNet.Cli.Utils; -using Microsoft.Extensions.Configuration.DotnetCli.Services; using Microsoft.Extensions.EnvironmentAbstractions; using NuGet.Common; using NuGet.Frameworks; @@ -18,9 +13,9 @@ using Microsoft.DotNet.Cli.Extensions; using Microsoft.DotNet.Cli.ShellShim; using Microsoft.DotNet.Cli.Commands.Tool.Update; -using Microsoft.DotNet.Cli.Commands.Tool.Common; using Microsoft.DotNet.Cli.Commands.Tool.Uninstall; using Microsoft.DotNet.Cli.Commands.Tool.List; +using Microsoft.Extensions.Configuration.DotnetCli; namespace Microsoft.DotNet.Cli.Commands.Tool.Install; @@ -28,7 +23,7 @@ namespace Microsoft.DotNet.Cli.Commands.Tool.Install; internal delegate (IToolPackageStore, IToolPackageStoreQuery, IToolPackageDownloader) CreateToolPackageStoresAndDownloader( DirectoryPath? nonGlobalLocation = null, - IEnumerable forwardRestoreArguments = null); + IEnumerable? forwardRestoreArguments = null); internal class ToolInstallGlobalOrToolPathCommand : CommandBase { @@ -37,22 +32,22 @@ internal class ToolInstallGlobalOrToolPathCommand : CommandBase private readonly CreateShellShimRepository _createShellShimRepository; private readonly CreateToolPackageStoresAndDownloaderAndUninstaller _createToolPackageStoreDownloaderUninstaller; private readonly ShellShimTemplateFinder _shellShimTemplateFinder; - private readonly IToolPackageStoreQuery _store; + private readonly IToolPackageStoreQuery? _store; private readonly PackageId? _packageId; - private readonly string _configFilePath; - private readonly string _framework; - private readonly string[] _source; - private readonly string[] _addSource; + private readonly string? _configFilePath; + private readonly string? _framework; + private readonly string[]? _source; + private readonly string[]? _addSource; private readonly bool _global; private readonly VerbosityOptions _verbosity; - private readonly string _toolPath; - private readonly string _architectureOption; + private readonly string? _toolPath; + private readonly string? _architectureOption; private readonly IEnumerable _forwardRestoreArguments; private readonly bool _allowRollForward; private readonly bool _allowPackageDowngrade; private readonly bool _updateAll; - private readonly string _currentWorkingDirectory; + private readonly string? _currentWorkingDirectory; private readonly bool? _verifySignatures; internal readonly RestoreActionConfig restoreActionConfig; @@ -60,13 +55,13 @@ internal class ToolInstallGlobalOrToolPathCommand : CommandBase public ToolInstallGlobalOrToolPathCommand( ParseResult parseResult, PackageId? packageId = null, - CreateToolPackageStoresAndDownloaderAndUninstaller createToolPackageStoreDownloaderUninstaller = null, - CreateShellShimRepository createShellShimRepository = null, - IEnvironmentPathInstruction environmentPathInstruction = null, - IReporter reporter = null, - INuGetPackageDownloader nugetPackageDownloader = null, - IToolPackageStoreQuery store = null, - string currentWorkingDirectory = null, + CreateToolPackageStoresAndDownloaderAndUninstaller? createToolPackageStoreDownloaderUninstaller = null, + CreateShellShimRepository? createShellShimRepository = null, + IEnvironmentPathInstruction? environmentPathInstruction = null, + IReporter? reporter = null, + INuGetPackageDownloader? nugetPackageDownloader = null, + IToolPackageStoreQuery? store = null, + string? currentWorkingDirectory = null, bool? verifySignatures = null) : base(parseResult) { @@ -98,7 +93,7 @@ public ToolInstallGlobalOrToolPathCommand( NoCache: parseResult.GetValue(ToolCommandRestorePassThroughOptions.NoCacheOption) || parseResult.GetValue(ToolCommandRestorePassThroughOptions.NoHttpCacheOption), IgnoreFailedSources: parseResult.GetValue(ToolCommandRestorePassThroughOptions.IgnoreFailedSourcesOption), Interactive: parseResult.GetValue(ToolCommandRestorePassThroughOptions.InteractiveRestoreOption)); - nugetPackageDownloader ??= new NuGetPackageDownloader.NuGetPackageDownloader(tempDir, DotNetConfigurationFactory.Create(), verboseLogger: new NullLogger(), restoreActionConfig: restoreActionConfig, verbosityOptions: _verbosity, verifySignatures: verifySignatures ?? true); + nugetPackageDownloader ??= new NuGetPackageDownloader.NuGetPackageDownloader(tempDir, verboseLogger: new NullLogger(), restoreActionConfig: restoreActionConfig, verbosityOptions: _verbosity, verifySignatures: verifySignatures ?? true); _shellShimTemplateFinder = new ShellShimTemplateFinder(nugetPackageDownloader, tempDir, packageSourceLocation); _store = store; @@ -140,7 +135,9 @@ public override int Execute() } else { - return ExecuteInstallCommand((PackageId)_packageId); + // this is safe because the parser definition will never let there be a + // case where updateAll isn't passed _and_ no packageId is passed + return ExecuteInstallCommand((PackageId)_packageId!); } } @@ -164,7 +161,7 @@ private int ExecuteInstallCommand(PackageId packageId) var appHostSourceDirectory = ShellShimTemplateFinder.GetDefaultAppHostSourceDirectory(); IShellShimRepository shellShimRepository = _createShellShimRepository(appHostSourceDirectory, toolPath); - IToolPackage oldPackageNullable = GetOldPackage(toolPackageStoreQuery, packageId); + IToolPackage? oldPackageNullable = GetOldPackage(toolPackageStoreQuery, packageId); if (oldPackageNullable != null) { @@ -174,7 +171,7 @@ private int ExecuteInstallCommand(PackageId packageId) { _reporter.WriteLine(string.Format(CliCommandStrings.ToolAlreadyInstalled, oldPackageNullable.Id, oldPackageNullable.Version.ToNormalizedString()).Green()); return 0; - } + } } TransactionalAction.Run(() => @@ -204,7 +201,7 @@ private int ExecuteInstallCommand(PackageId packageId) EnsureVersionIsHigher(oldPackageNullable, newInstalledPackage, _allowPackageDowngrade); - NuGetFramework framework; + NuGetFramework? framework; if (string.IsNullOrEmpty(_framework) && newInstalledPackage.Frameworks.Count() > 0) { framework = newInstalledPackage.Frameworks @@ -252,7 +249,7 @@ private static bool ToolVersionAlreadyInstalled(IToolPackage oldPackageNullable, return oldPackageNullable != null && oldPackageNullable.Version == nuGetVersion; } - private static void EnsureVersionIsHigher(IToolPackage oldPackageNullable, IToolPackage newInstalledPackage, bool allowDowngrade) + private static void EnsureVersionIsHigher(IToolPackage? oldPackageNullable, IToolPackage newInstalledPackage, bool allowDowngrade) { if (oldPackageNullable != null && newInstalledPackage.Version < oldPackageNullable.Version && !allowDowngrade) { @@ -305,7 +302,7 @@ private static void RunWithHandlingUninstallError(Action uninstallAction, Packag { try { - uninstallAction(); + uninstallAction(); } catch (Exception ex) when (ToolUninstallCommandLowLevelErrorConverter.ShouldConvertToUserFacingError(ex)) @@ -335,9 +332,9 @@ private static void RunWithHandlingUninstallError(Action uninstallAction, Packag return configFile; } - private static IToolPackage GetOldPackage(IToolPackageStoreQuery toolPackageStoreQuery, PackageId packageId) + private static IToolPackage? GetOldPackage(IToolPackageStoreQuery toolPackageStoreQuery, PackageId packageId) { - IToolPackage oldPackageNullable; + IToolPackage? oldPackageNullable; try { oldPackageNullable = toolPackageStoreQuery.EnumeratePackageVersions(packageId).SingleOrDefault(); @@ -357,7 +354,7 @@ private static IToolPackage GetOldPackage(IToolPackageStoreQuery toolPackageStor return oldPackageNullable; } - private void PrintSuccessMessage(IToolPackage oldPackage, IToolPackage newInstalledPackage) + private void PrintSuccessMessage(IToolPackage? oldPackage, IToolPackage newInstalledPackage) { if (!_verbosity.IsQuiet()) { @@ -383,7 +380,7 @@ private void PrintSuccessMessage(IToolPackage oldPackage, IToolPackage newInstal { _reporter.WriteLine( string.Format( - + newInstalledPackage.Version.IsPrerelease ? CliCommandStrings.UpdateSucceededPreVersionNoChange : CliCommandStrings.UpdateSucceededStableVersionNoChange, newInstalledPackage.Id, newInstalledPackage.Version).Green()); diff --git a/src/Cli/dotnet/Commands/Workload/Install/FileBasedInstaller.cs b/src/Cli/dotnet/Commands/Workload/Install/FileBasedInstaller.cs index 27ef7d9246fa..046b7ba6c8d8 100644 --- a/src/Cli/dotnet/Commands/Workload/Install/FileBasedInstaller.cs +++ b/src/Cli/dotnet/Commands/Workload/Install/FileBasedInstaller.cs @@ -1,19 +1,17 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -#nullable disable - using System.Collections.Concurrent; +using System.Diagnostics; using System.Text.Json; using Microsoft.DotNet.Cli.Commands.Workload.Config; using Microsoft.DotNet.Cli.Commands.Workload.Install.WorkloadInstallRecords; -using Microsoft.DotNet.Cli.Configuration; using Microsoft.DotNet.Cli.Extensions; using Microsoft.DotNet.Cli.NuGetPackageDownloader; using Microsoft.DotNet.Cli.ToolPackage; using Microsoft.DotNet.Cli.Utils; using Microsoft.DotNet.NativeWrapper; -using Microsoft.Extensions.Configuration.DotnetCli.Services; +using Microsoft.Extensions.Configuration.DotnetCli; using Microsoft.Extensions.EnvironmentAbstractions; using Microsoft.NET.Sdk.WorkloadManifestReader; using NuGet.Common; @@ -32,15 +30,15 @@ internal class FileBasedInstaller : IInstaller private const string InstalledWorkloadSetsDir = "InstalledWorkloadSets"; private const string HistoryDir = "history"; protected readonly string _dotnetDir; - protected readonly string _userProfileDir; + protected readonly string? _userProfileDir; protected readonly string _workloadRootDir; protected readonly DirectoryPath _tempPackagesDir; private readonly INuGetPackageDownloader _nugetPackageDownloader; private IWorkloadResolver _workloadResolver; private readonly SdkFeatureBand _sdkFeatureBand; private readonly FileBasedInstallationRecordRepository _installationRecordRepository; - private readonly PackageSourceLocation _packageSourceLocation; - private readonly RestoreActionConfig _restoreActionConfig; + private readonly PackageSourceLocation? _packageSourceLocation; + private readonly RestoreActionConfig? _restoreActionConfig; public int ExitCode => 0; @@ -48,24 +46,24 @@ public FileBasedInstaller(IReporter reporter, SdkFeatureBand sdkFeatureBand, IWorkloadResolver workloadResolver, string userProfileDir, - INuGetPackageDownloader nugetPackageDownloader = null, - string dotnetDir = null, - string tempDirPath = null, + INuGetPackageDownloader? nugetPackageDownloader = null, + string? dotnetDir = null, + string? tempDirPath = null, VerbosityOptions verbosity = VerbosityOptions.normal, - PackageSourceLocation packageSourceLocation = null, - RestoreActionConfig restoreActionConfig = null, + PackageSourceLocation? packageSourceLocation = null, + RestoreActionConfig? restoreActionConfig = null, VerbosityOptions nugetPackageDownloaderVerbosity = VerbosityOptions.normal) { _userProfileDir = userProfileDir; - _dotnetDir = dotnetDir ?? Path.GetDirectoryName(Environment.ProcessPath); + _dotnetDir = dotnetDir ?? Path.GetDirectoryName(Environment.ProcessPath)!; _tempPackagesDir = new DirectoryPath(tempDirPath ?? PathUtilities.CreateTempSubdirectory()); ILogger logger = verbosity.IsDetailedOrDiagnostic() ? new NuGetConsoleLogger() : new NullLogger(); _restoreActionConfig = restoreActionConfig; _nugetPackageDownloader = nugetPackageDownloader ?? - new NuGetPackageDownloader.NuGetPackageDownloader(_tempPackagesDir, + new NuGetPackageDownloader.NuGetPackageDownloader(_tempPackagesDir, DotNetConfigurationFactory.Create(), filePermissionSetter: null, - firstPartyNuGetPackageSigningVerifier: new FirstPartyNuGetPackageSigningVerifier(), + firstPartyNuGetPackageSigningVerifier: new FirstPartyNuGetPackageSigningVerifier(), verboseLogger: logger, restoreActionConfig: _restoreActionConfig, verbosityOptions: nugetPackageDownloaderVerbosity); @@ -97,10 +95,10 @@ IEnumerable GetPacksInWorkloads(IEnumerable workloadIds) .Select(packId => _workloadResolver.TryGetPackInfo(packId)) .Where(pack => pack != null); - return packs; + return packs!; } - public WorkloadSet InstallWorkloadSet(ITransactionContext context, string workloadSetVersion, DirectoryPath? offlineCache = null) + public WorkloadSet? InstallWorkloadSet(ITransactionContext context, string workloadSetVersion, DirectoryPath? offlineCache = null) { string workloadSetPackageVersion = WorkloadSetVersion.ToWorkloadSetPackageVersion(workloadSetVersion, out SdkFeatureBand workloadSetFeatureBand); var workloadSetPackageId = GetManifestPackageId(new ManifestId(WorkloadManifestUpdater.WorkloadSetManifestId), workloadSetFeatureBand); @@ -128,9 +126,9 @@ public WorkloadSet InstallWorkloadSet(ITransactionContext context, string worklo return WorkloadSet.FromWorkloadSetFolder(workloadSetPath, workloadSetVersion, _sdkFeatureBand); } - public WorkloadSet GetWorkloadSetContents(string workloadSetVersion) => GetWorkloadSetContentsAsync(workloadSetVersion).GetAwaiter().GetResult(); + public WorkloadSet? GetWorkloadSetContents(string workloadSetVersion) => GetWorkloadSetContentsAsync(workloadSetVersion).GetAwaiter().GetResult(); - public async Task GetWorkloadSetContentsAsync(string workloadSetVersion) + public async Task GetWorkloadSetContentsAsync(string workloadSetVersion) { string workloadSetPackageVersion = WorkloadSetVersion.ToWorkloadSetPackageVersion(workloadSetVersion, out var workloadSetFeatureBand); var packagePath = await _nugetPackageDownloader.DownloadPackageAsync(GetManifestPackageId(new ManifestId(WorkloadManifestUpdater.WorkloadSetManifestId), workloadSetFeatureBand), @@ -196,7 +194,7 @@ public void InstallWorkloads(IEnumerable workloadIds, SdkFeatureBand { if (!Directory.Exists(Path.GetDirectoryName(packInfo.Path))) { - Directory.CreateDirectory(Path.GetDirectoryName(packInfo.Path)); + Directory.CreateDirectory(Path.GetDirectoryName(packInfo.Path)!); } if (IsSingleFilePack(packInfo)) @@ -277,6 +275,8 @@ string GetManifestInstallDirForFeatureBand(string sdkFeatureBand) public void InstallWorkloadManifest(ManifestVersionUpdate manifestUpdate, ITransactionContext transactionContext, DirectoryPath? offlineCache = null) { + Debug.Assert(manifestUpdate.NewFeatureBand is not null, "Manifest update must have a feature band"); + Debug.Assert(manifestUpdate.NewVersion is not null, "Manifest update must have a version"); var newManifestPath = Path.Combine(GetManifestInstallDirForFeatureBand(manifestUpdate.NewFeatureBand), manifestUpdate.ManifestId.ToString(), manifestUpdate.NewVersion.ToString()); _reporter.WriteLine(string.Format(CliCommandStrings.InstallingWorkloadManifest, manifestUpdate.ManifestId, manifestUpdate.NewVersion)); @@ -305,8 +305,8 @@ public void InstallWorkloadManifest(ManifestVersionUpdate manifestUpdate, ITrans void InstallPackage(PackageId packageId, string packageVersion, string targetFolder, ITransactionContext transactionContext, DirectoryPath? offlineCache) { - string packagePath = null; - string tempBackupDir = null; + string? packagePath = null; + string? tempBackupDir = null; bool directoryExists = Directory.Exists(targetFolder) && Directory.GetFileSystemEntries(targetFolder).Any(); transactionContext.Run( @@ -408,7 +408,8 @@ public void GarbageCollect(Func getResolverForWorkloa var featureBandsWithWorkloadInstallRecords = _installationRecordRepository.GetFeatureBandsWithInstallationRecords(); - var installedSdkFeatureBands = NETCoreSdkResolverNativeWrapper.GetAvailableSdks(_dotnetDir).Select(sdkDir => new SdkFeatureBand(Path.GetFileName(sdkDir))).ToHashSet(); + // If we can't get SDKs, something has gone _very_ wrong. That's why we ! here. + var installedSdkFeatureBands = NETCoreSdkResolverNativeWrapper.GetAvailableSdks(_dotnetDir)!.Select(sdkDir => new SdkFeatureBand(Path.GetFileName(sdkDir))).ToHashSet(); // Tests will often use a dotnet folder without any SDKs installed. To work around this, always add the current feature band to the list of installed feature bands installedSdkFeatureBands.Add(_sdkFeatureBand); @@ -423,8 +424,7 @@ public void GarbageCollect(Func getResolverForWorkloa // Get the feature band of the workload set WorkloadSetVersion.ToWorkloadSetPackageVersion(workloadSetVersion, out var workloadSetFeatureBand); - List referencingFeatureBands; - if (!workloadSetInstallRecords.TryGetValue((workloadSetVersion, workloadSetFeatureBand), out referencingFeatureBands)) + if (!workloadSetInstallRecords.TryGetValue((workloadSetVersion, workloadSetFeatureBand), out List? referencingFeatureBands)) { // If there are no install records for a workload set that is on disk, then ignore it. It is probably a baseline workload set. continue; @@ -540,18 +540,18 @@ public void GarbageCollect(Func getResolverForWorkloa File.Delete(GetPackInstallRecordPath(packId, packVersion, featureBand)); } - var installationRecordDirectory = Path.GetDirectoryName(GetPackInstallRecordPath(packId, packVersion, featureBandsToRemove.First())); + var installationRecordDirectory = Path.GetDirectoryName(GetPackInstallRecordPath(packId, packVersion, featureBandsToRemove.First()))!; if (!Directory.GetFileSystemEntries(installationRecordDirectory).Any()) { // There are no installation records for the workload pack anymore, so we can delete the pack - var packToDelete = JsonSerializer.Deserialize(jsonPackInfo); + var packToDelete = JsonSerializer.Deserialize(jsonPackInfo)!; DeletePack(packToDelete); // Delete now-empty pack installation record directory Directory.Delete(installationRecordDirectory); // And delete the parent directory if it's also empty - string packIdDirectory = Path.GetDirectoryName(installationRecordDirectory); + string packIdDirectory = Path.GetDirectoryName(installationRecordDirectory)!; if (!Directory.GetFileSystemEntries(packIdDirectory).Any()) { Directory.Delete(packIdDirectory); @@ -592,7 +592,7 @@ public void UpdateInstallMode(SdkFeatureBand sdkFeatureBand, bool? newMode) private void UpdateInstallState(SdkFeatureBand sdkFeatureBand, Action update) { string path = Path.Combine(WorkloadInstallType.GetInstallStateFolder(sdkFeatureBand, _workloadRootDir), "default.json"); - Directory.CreateDirectory(Path.GetDirectoryName(path)); + Directory.CreateDirectory(Path.GetDirectoryName(path)!); var installStateContents = InstallStateContents.FromPath(path); update(installStateContents); File.WriteAllText(path, installStateContents.ToString()); @@ -647,7 +647,7 @@ public IEnumerable GetWorkloadHistoryRecords(string sdkFe public void Shutdown() { // Perform any additional cleanup here that's intended to run at the end of the command, regardless - // of success or failure. For file based installs, there shouldn't be any additional work to + // of success or failure. For file based installs, there shouldn't be any additional work to // perform. } @@ -679,7 +679,7 @@ public async Task ExtractManifestAsync(string nupkgPath, string targetPath) { Directory.Delete(targetPath, true); } - Directory.CreateDirectory(Path.GetDirectoryName(targetPath)); + Directory.CreateDirectory(Path.GetDirectoryName(targetPath)!); FileAccessRetrier.RetryOnMoveAccessFailure(() => DirectoryPath.MoveDirectory(Path.Combine(extractionPath, "data"), targetPath)); } finally @@ -715,7 +715,7 @@ private void DeletePack(PackInfo packInfo) else { Directory.Delete(packInfo.Path, true); - var packIdDir = Path.GetDirectoryName(packInfo.Path); + var packIdDir = Path.GetDirectoryName(packInfo.Path)!; if (!Directory.EnumerateFileSystemEntries(packIdDir).Any()) { Directory.Delete(packIdDir, true); @@ -733,7 +733,7 @@ string GetWorkloadSetInstallRecordPath(string workloadSetVersion, SdkFeatureBand void WriteWorkloadSetInstallationRecord(string workloadSetVersion, SdkFeatureBand workloadSetFeatureBand, SdkFeatureBand referencingFeatureBand) { var path = GetWorkloadSetInstallRecordPath(workloadSetVersion, workloadSetFeatureBand, referencingFeatureBand); - Directory.CreateDirectory(Path.GetDirectoryName(path)); + Directory.CreateDirectory(Path.GetDirectoryName(path)!); using var _ = File.Create(path); } @@ -784,7 +784,7 @@ private string GetManifestInstallRecordPath(ManifestId manifestId, ManifestVersi void WriteManifestInstallationRecord(ManifestId manifestId, ManifestVersion manifestVersion, SdkFeatureBand featureBand, SdkFeatureBand referencingFeatureBand) { var path = GetManifestInstallRecordPath(manifestId, manifestVersion, featureBand, referencingFeatureBand); - Directory.CreateDirectory(Path.GetDirectoryName(path)); + Directory.CreateDirectory(Path.GetDirectoryName(path)!); using var _ = File.Create(path); } @@ -877,7 +877,7 @@ private void WritePackInstallationRecord(PackInfo packInfo, SdkFeatureBand featu var path = GetPackInstallRecordPath(new WorkloadPackId(packInfo.ResolvedPackageId), packInfo.Version, featureBand); if (!Directory.Exists(Path.GetDirectoryName(path))) { - Directory.CreateDirectory(Path.GetDirectoryName(path)); + Directory.CreateDirectory(Path.GetDirectoryName(path)!); } File.WriteAllText(path, JsonSerializer.Serialize(packInfo)); } @@ -889,12 +889,12 @@ private void DeletePackInstallationRecord(PackInfo packInfo, SdkFeatureBand feat { File.Delete(packInstallRecord); - var packRecordVersionDir = Path.GetDirectoryName(packInstallRecord); + var packRecordVersionDir = Path.GetDirectoryName(packInstallRecord)!; if (!Directory.GetFileSystemEntries(packRecordVersionDir).Any()) { Directory.Delete(packRecordVersionDir); - var packRecordIdDir = Path.GetDirectoryName(packRecordVersionDir); + var packRecordIdDir = Path.GetDirectoryName(packRecordVersionDir)!; if (!Directory.GetFileSystemEntries(packRecordIdDir).Any()) { Directory.Delete(packRecordIdDir); diff --git a/src/Cli/dotnet/Commands/Workload/Install/NetSdkMsiInstallerClient.cs b/src/Cli/dotnet/Commands/Workload/Install/NetSdkMsiInstallerClient.cs index b903964c38b8..26578818e69b 100644 --- a/src/Cli/dotnet/Commands/Workload/Install/NetSdkMsiInstallerClient.cs +++ b/src/Cli/dotnet/Commands/Workload/Install/NetSdkMsiInstallerClient.cs @@ -1,19 +1,16 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -#nullable disable - using System.Globalization; using System.Runtime.Versioning; using Microsoft.DotNet.Cli.Commands.Workload.Config; using Microsoft.DotNet.Cli.Commands.Workload.Install.WorkloadInstallRecords; -using Microsoft.DotNet.Cli.Configuration; using Microsoft.DotNet.Cli.Installer.Windows; using Microsoft.DotNet.Cli.NuGetPackageDownloader; using Microsoft.DotNet.Cli.ToolPackage; using Microsoft.DotNet.Cli.Utils; using Microsoft.DotNet.Cli.Utils.Extensions; -using Microsoft.Extensions.Configuration.DotnetCli.Services; +using Microsoft.Extensions.Configuration.DotnetCli; using Microsoft.Extensions.EnvironmentAbstractions; using Microsoft.NET.Sdk.WorkloadManifestReader; using Microsoft.Win32.Msi; @@ -34,7 +31,7 @@ internal partial class NetSdkMsiInstallerClient : MsiInstallerBase, IInstaller private bool _shutdown; - private readonly PackageSourceLocation _packageSourceLocation; + private readonly PackageSourceLocation? _packageSourceLocation; private readonly string _dependent; @@ -45,10 +42,10 @@ public NetSdkMsiInstallerClient(InstallElevationContextBase elevationContext, bool verifySignatures, IWorkloadResolver workloadResolver, SdkFeatureBand sdkFeatureBand, - INuGetPackageDownloader nugetPackageDownloader = null, + INuGetPackageDownloader nugetPackageDownloader, VerbosityOptions verbosity = VerbosityOptions.normal, - PackageSourceLocation packageSourceLocation = null, - IReporter reporter = null) : base(elevationContext, logger, verifySignatures, reporter) + PackageSourceLocation? packageSourceLocation = null, + IReporter? reporter = null) : base(elevationContext, logger, verifySignatures, reporter) { _packageSourceLocation = packageSourceLocation; _nugetPackageDownloader = nugetPackageDownloader; @@ -104,16 +101,16 @@ public IEnumerable GetDownloads(IEnumerable worklo } // Wrap the setup logger in an IReporter so it can be passed to the garbage collector - private class SetupLogReporter(ISetupLogger setupLogger) : IReporter + private class SetupLogReporter(ISetupLogger? setupLogger) : IReporter { - private readonly ISetupLogger _setupLogger = setupLogger; + private readonly ISetupLogger? _setupLogger = setupLogger; // SetupLogger doesn't have a way of writing a message that shouldn't include a newline. So if this method is used a message may be split across multiple lines, // but that's probably better than not writing a message at all or throwing an exception - public void Write(string message) => _setupLogger.LogMessage(message); - public void WriteLine(string message) => _setupLogger.LogMessage(message); - public void WriteLine() => _setupLogger.LogMessage(""); - public void WriteLine(string format, params object[] args) => _setupLogger.LogMessage(string.Format(format, args)); + public void Write(string message) => _setupLogger?.LogMessage(message); + public void WriteLine(string message) => _setupLogger?.LogMessage(message); + public void WriteLine() => _setupLogger?.LogMessage(""); + public void WriteLine(string format, params object?[] args) => _setupLogger?.LogMessage(string.Format(format, args)); } /// @@ -306,7 +303,7 @@ public void GarbageCollect(Func getResolverForWorkloa } } - public WorkloadSet InstallWorkloadSet(ITransactionContext context, string workloadSetVersion, DirectoryPath? offlineCache) + public WorkloadSet? InstallWorkloadSet(ITransactionContext context, string workloadSetVersion, DirectoryPath? offlineCache) { ReportPendingReboot(); @@ -315,7 +312,7 @@ public WorkloadSet InstallWorkloadSet(ITransactionContext context, string worklo context.Run( action: () => { - DetectState state = DetectPackage(msi.ProductCode, out Version installedVersion); + DetectState state = DetectPackage(msi.ProductCode, out Version? installedVersion); InstallAction plannedAction = PlanPackage(msi, state, InstallAction.Install, installedVersion); if (plannedAction == InstallAction.Install) @@ -330,7 +327,7 @@ public WorkloadSet InstallWorkloadSet(ITransactionContext context, string worklo }, rollback: () => { - DetectState state = DetectPackage(msi.ProductCode, out Version installedVersion); + DetectState state = DetectPackage(msi.ProductCode, out Version? installedVersion); InstallAction plannedAction = PlanPackage(msi, state, InstallAction.Uninstall, installedVersion); if (plannedAction == InstallAction.Uninstall) @@ -428,7 +425,7 @@ private void RemoveWorkloadSets(List workloadSetsToRemove, Di { foreach (WorkloadSetRecord record in workloadSetsToRemove) { - DetectState state = DetectPackage(record.ProductCode, out Version _); + DetectState state = DetectPackage(record.ProductCode, out Version? _); if (state == DetectState.Present) { string msiNuGetPackageId = $"Microsoft.NET.Workloads.{record.WorkloadSetFeatureBand}.Msi.{HostArchitecture}"; @@ -454,7 +451,7 @@ private void RemoveWorkloadManifests(List manifestToRemo { foreach (WorkloadManifestRecord record in manifestToRemove) { - DetectState state = DetectPackage(record.ProductCode, out Version _); + DetectState state = DetectPackage(record.ProductCode, out Version? _); if (state == DetectState.Present) { string msiNuGetPackageId = $"{record.ManifestId}.Manifest-{record.ManifestFeatureBand}.Msi.{HostArchitecture}"; @@ -484,7 +481,7 @@ private void RemoveWorkloadPacks(List packsToRemove, Directo // if a previous removal was interrupted. We can't safely clean up orphaned records because it's too expensive // to query all installed components and determine the product codes associated with the component that // created the record. - DetectState state = DetectPackage(record.ProductCode, out Version _); + DetectState state = DetectPackage(record.ProductCode, out Version? _); if (state == DetectState.Present) { @@ -577,7 +574,7 @@ void InstallWorkloadManifestImplementation(ManifestVersionUpdate manifestUpdate, // Retrieve the payload from the MSI package cache. MsiPayload msi = GetCachedMsiPayload(msiPackageId, msiPackageVersion, offlineCache); VerifyPackage(msi); - DetectState state = DetectPackage(msi.ProductCode, out Version installedVersion); + DetectState state = DetectPackage(msi.ProductCode, out Version? installedVersion); InstallAction plannedAction = PlanPackage(msi, state, action, installedVersion); ExecutePackage(msi, plannedAction, msiPackageId); @@ -597,7 +594,7 @@ public void RepairWorkloads(IEnumerable workloadIds, SdkFeatureBand // Retrieve the payload from the MSI package cache. MsiPayload msi = GetCachedMsiPayload(aquirableMsi.NuGetPackageId, aquirableMsi.NuGetPackageVersion, offlineCache); VerifyPackage(msi); - DetectState state = DetectPackage(msi, out Version installedVersion); + DetectState state = DetectPackage(msi, out Version? installedVersion); InstallAction plannedAction = PlanPackage(msi, state, InstallAction.Repair, installedVersion); ExecutePackage(msi, plannedAction, aquirableMsi.NuGetPackageId); @@ -629,7 +626,7 @@ public void InstallWorkloads(IEnumerable workloadIds, SdkFeatureBand // Retrieve the payload from the MSI package cache. MsiPayload msi = GetCachedMsiPayload(msiToInstall.NuGetPackageId, msiToInstall.NuGetPackageVersion, offlineCache); VerifyPackage(msi); - DetectState state = DetectPackage(msi, out Version installedVersion); + DetectState state = DetectPackage(msi, out Version? installedVersion); InstallAction plannedAction = PlanPackage(msi, state, InstallAction.Install, installedVersion); if (plannedAction == InstallAction.Install) { @@ -690,7 +687,7 @@ void RollBackMsiInstall(WorkloadDownload msiToRollback, DirectoryPath? offlineCa } // Make sure the MSI is actually installed. - DetectState state = DetectPackage(msi, out Version installedVersion); + DetectState state = DetectPackage(msi, out Version? installedVersion); InstallAction plannedAction = PlanPackage(msi, state, InstallAction.Uninstall, installedVersion); // The previous steps would have logged the final action. If the verdict is not to uninstall we can exit. @@ -728,7 +725,10 @@ public void Shutdown() Log?.LogMessage("Shutdown completed."); Log?.LogMessage($"Restart required: {Restart}"); - ((TimestampedFileLogger)Log).Dispose(); + if (Log is TimestampedFileLogger t) + { + t.Dispose(); + } _shutdown = true; } @@ -764,7 +764,7 @@ public async Task ExtractManifestAsync(string nupkgPath, string targetPath) if (Directory.Exists(extractedManifestPath)) { Log?.LogMessage($"ExtractManifestAsync: Copying manifest from '{extractionPath}' to '{targetPath}'"); - Directory.CreateDirectory(Path.GetDirectoryName(targetPath)); + Directory.CreateDirectory(Path.GetDirectoryName(targetPath)!); FileAccessRetrier.RetryOnMoveAccessFailure(() => DirectoryPath.MoveDirectory(extractedManifestPath, targetPath)); } else @@ -796,8 +796,8 @@ public async Task ExtractManifestAsync(string nupkgPath, string targetPath) var manifestsFolder = Path.Combine(msiExtractionPath, "dotnet", "sdk-manifests"); - string manifestFolder = null; - string manifestsFeatureBandFolder = Directory.GetDirectories(manifestsFolder).SingleOrDefault(); + string? manifestFolder = null; + string? manifestsFeatureBandFolder = Directory.GetDirectories(manifestsFolder).SingleOrDefault(); if (manifestsFeatureBandFolder != null) { manifestFolder = Directory.GetDirectories(manifestsFeatureBandFolder).SingleOrDefault(); @@ -831,7 +831,7 @@ private void LogPackInfo(PackInfo packInfo) /// The product code of the MSI to detect. /// If detected, contains the version of the installed MSI. /// The detect state of the specified MSI. - private DetectState DetectPackage(string productCode, out Version installedVersion) + private DetectState DetectPackage(string productCode, out Version? installedVersion) { installedVersion = default; uint error = WindowsInstaller.GetProductInfo(productCode, InstallProperty.VERSIONSTRING, out string versionValue); @@ -861,7 +861,7 @@ private DetectState DetectPackage(string productCode, out Version installedVersi /// The MSI package to detect. /// If detected, contains the version of the installed MSI. /// The detect state of the specified MSI. - private DetectState DetectPackage(MsiPayload msi, out Version installedVersion) + private DetectState DetectPackage(MsiPayload msi, out Version? installedVersion) { return DetectPackage(msi.ProductCode, out installedVersion); } @@ -873,7 +873,7 @@ private DetectState DetectPackage(MsiPayload msi, out Version installedVersion) /// The detected state of the package. /// The requested action to perform. /// The action that will be performed. - private InstallAction PlanPackage(MsiPayload msi, DetectState state, InstallAction requestedAction, Version installedVersion) + private InstallAction PlanPackage(MsiPayload msi, DetectState state, InstallAction requestedAction, Version? installedVersion) { InstallAction plannedAction = InstallAction.None; @@ -966,7 +966,7 @@ private string ExtractPackage(string packageId, string packageVersion, Directory /// Gets a set of all the installed SDK feature bands. /// /// A List of all the installed SDK feature bands. - private static IEnumerable GetInstalledFeatureBands(ISetupLogger log = null) + private static IEnumerable GetInstalledFeatureBands(ISetupLogger? log = null) { HashSet installedFeatureBands = []; foreach (string sdkVersion in GetInstalledSdkVersions()) @@ -1020,7 +1020,7 @@ private MsiPayload GetCachedMsiPayload(string packageId, string packageVersion, /// The action to perform. /// A friendly name to display to the user when reporting progress. If no value is provided, the MSI /// filename will be used. - private void ExecutePackage(MsiPayload msi, InstallAction action, string displayName = null) + private void ExecutePackage(MsiPayload msi, InstallAction action, string? displayName = null) { uint error = Error.SUCCESS; string logFile = GetMsiLogName(msi, action); @@ -1073,7 +1073,7 @@ private uint ExecuteWithProgress(string progressLabel, Func installDelegat if (installTask.IsFaulted) { Reporter.WriteLine(" Failed"); - throw installTask.Exception.InnerException; + throw installTask.Exception.InnerException ?? installTask.Exception; } error = installTask.Result; @@ -1112,12 +1112,12 @@ public static NetSdkMsiInstallerClient Create( bool verifySignatures, SdkFeatureBand sdkFeatureBand, IWorkloadResolver workloadResolver, - INuGetPackageDownloader nugetPackageDownloader = null, + INuGetPackageDownloader? nugetPackageDownloader = null, VerbosityOptions verbosity = VerbosityOptions.normal, - PackageSourceLocation packageSourceLocation = null, - IReporter reporter = null, - string tempDirPath = null, - RestoreActionConfig restoreActionConfig = null, + PackageSourceLocation? packageSourceLocation = null, + IReporter? reporter = null, + string? tempDirPath = null, + RestoreActionConfig? restoreActionConfig = null, bool shouldLog = true) { ISynchronizingLogger logger = @@ -1131,9 +1131,9 @@ public static NetSdkMsiInstallerClient Create( nugetPackageDownloader = new NuGetPackageDownloader.NuGetPackageDownloader(tempPackagesDir, DotNetConfigurationFactory.Create(), - filePermissionSetter: null, + filePermissionSetter: null, firstPartyNuGetPackageSigningVerifier: new FirstPartyNuGetPackageSigningVerifier(), - verboseLogger: new NullLogger(), + verboseLogger: new NullLogger(), restoreActionConfig: restoreActionConfig); } @@ -1152,7 +1152,7 @@ private void ReportPendingReboot() } } - private void OnProcessExit(object sender, EventArgs e) + private void OnProcessExit(object? sender, EventArgs e) { if (!_shutdown) { diff --git a/src/Cli/dotnet/Commands/Workload/Install/WorkloadInstallCommand.cs b/src/Cli/dotnet/Commands/Workload/Install/WorkloadInstallCommand.cs index 637bf68e49e9..97d75df7ec34 100644 --- a/src/Cli/dotnet/Commands/Workload/Install/WorkloadInstallCommand.cs +++ b/src/Cli/dotnet/Commands/Workload/Install/WorkloadInstallCommand.cs @@ -1,17 +1,14 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -#nullable disable - using System.CommandLine; using System.Text.Json; -using Microsoft.DotNet.Cli.Configuration; using Microsoft.DotNet.Cli.Extensions; using Microsoft.DotNet.Cli.NuGetPackageDownloader; using Microsoft.DotNet.Cli.ToolPackage; using Microsoft.DotNet.Cli.Utils; using Microsoft.DotNet.Cli.Utils.Extensions; -using Microsoft.Extensions.Configuration.DotnetCli.Services; +using Microsoft.Extensions.Configuration.DotnetCli; using Microsoft.Extensions.EnvironmentAbstractions; using Microsoft.NET.Sdk.WorkloadManifestReader; using NuGet.Common; @@ -29,21 +26,21 @@ internal class WorkloadInstallCommand : InstallingWorkloadCommand public WorkloadInstallCommand( ParseResult parseResult, - IReporter reporter = null, - IWorkloadResolverFactory workloadResolverFactory = null, - IInstaller workloadInstaller = null, - INuGetPackageDownloader nugetPackageDownloader = null, - IWorkloadManifestUpdater workloadManifestUpdater = null, - string tempDirPath = null, - IReadOnlyCollection workloadIds = null, + IReporter? reporter = null, + IWorkloadResolverFactory? workloadResolverFactory = null, + IInstaller? workloadInstaller = null, + INuGetPackageDownloader? nugetPackageDownloader = null, + IWorkloadManifestUpdater? workloadManifestUpdater = null, + string? tempDirPath = null, + IReadOnlyCollection? workloadIds = null, bool? skipWorkloadManifestUpdate = null) : base(parseResult, reporter: reporter, workloadResolverFactory: workloadResolverFactory, workloadInstaller: workloadInstaller, nugetPackageDownloader: nugetPackageDownloader, workloadManifestUpdater: workloadManifestUpdater, tempDirPath: tempDirPath, verbosityOptions: WorkloadInstallCommandParser.VerbosityOption) { _skipManifestUpdate = skipWorkloadManifestUpdate ?? parseResult.GetValue(WorkloadInstallCommandParser.SkipManifestUpdateOption); - var unprocessedWorkloadIds = workloadIds ?? parseResult.GetValue(WorkloadInstallCommandParser.WorkloadIdArgument); - if (unprocessedWorkloadIds?.Any(id => id.Contains('@')) == true) + IEnumerable unprocessedWorkloadIds = workloadIds ?? parseResult.GetRequiredValue(WorkloadInstallCommandParser.WorkloadIdArgument); + if (unprocessedWorkloadIds.Any(id => id.Contains('@')) == true) { _workloadIds = unprocessedWorkloadIds.Select(id => id.Split('@')[0]).ToList().AsReadOnly(); if (SpecifiedWorkloadSetVersionOnCommandLine) @@ -209,7 +206,7 @@ public override int Execute() return _workloadInstaller.ExitCode; } - private void InstallWorkloads(WorkloadHistoryRecorder recorder, IReadOnlyCollection filteredWorkloadIds) + private void InstallWorkloads(WorkloadHistoryRecorder? recorder, IReadOnlyCollection filteredWorkloadIds) { // Normally we want to validate that the workload IDs specified were valid. However, if there is a global.json file with a workload // set version specified, and we might install that workload version, then we don't do that check here, because we might not have the right @@ -310,7 +307,7 @@ internal static void TryRunGarbageCollection(IInstaller workloadInstaller, IRepo } private async Task> GetPackageDownloadUrlsAsync(IEnumerable workloadIds, bool skipManifestUpdate, bool includePreview, - IReporter reporter = null, INuGetPackageDownloader packageDownloader = null) + IReporter? reporter = null, INuGetPackageDownloader? packageDownloader = null) { reporter ??= Reporter; packageDownloader ??= PackageDownloader; diff --git a/src/Cli/dotnet/Commands/Workload/Install/WorkloadManifestUpdater.cs b/src/Cli/dotnet/Commands/Workload/Install/WorkloadManifestUpdater.cs index a19cab6a3771..b5d16ec646c5 100644 --- a/src/Cli/dotnet/Commands/Workload/Install/WorkloadManifestUpdater.cs +++ b/src/Cli/dotnet/Commands/Workload/Install/WorkloadManifestUpdater.cs @@ -1,18 +1,15 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -#nullable disable - using System.Text.Json; using Microsoft.DotNet.Cli.Commands.Workload.Install.WorkloadInstallRecords; using Microsoft.DotNet.Cli.Commands.Workload.List; -using Microsoft.DotNet.Cli.Configuration; using Microsoft.DotNet.Cli.NuGetPackageDownloader; using Microsoft.DotNet.Cli.ToolPackage; using Microsoft.DotNet.Cli.Utils; using Microsoft.DotNet.Cli.Utils.Extensions; using Microsoft.DotNet.Configurer; -using Microsoft.Extensions.Configuration.DotnetCli.Services; +using Microsoft.Extensions.Configuration.DotnetCli; using Microsoft.Extensions.EnvironmentAbstractions; using Microsoft.NET.Sdk.WorkloadManifestReader; using NuGet.Common; @@ -30,8 +27,8 @@ internal class WorkloadManifestUpdater : IWorkloadManifestUpdater private readonly INuGetPackageDownloader _nugetPackageDownloader; private readonly SdkFeatureBand _sdkFeatureBand; private readonly string _userProfileDir; - private readonly PackageSourceLocation _packageSourceLocation; - private readonly Func _getEnvironmentVariable; + private readonly PackageSourceLocation? _packageSourceLocation; + private readonly Func _getEnvironmentVariable; private readonly IWorkloadInstallationRecordRepository _workloadRecordRepo; private readonly IWorkloadManifestInstaller _workloadManifestInstaller; private readonly bool _displayManifestUpdates; @@ -42,8 +39,8 @@ public WorkloadManifestUpdater(IReporter reporter, string userProfileDir, IWorkloadInstallationRecordRepository workloadRecordRepo, IWorkloadManifestInstaller workloadManifestInstaller, - PackageSourceLocation packageSourceLocation = null, - Func getEnvironmentVariable = null, + PackageSourceLocation? packageSourceLocation = null, + Func? getEnvironmentVariable = null, bool displayManifestUpdates = true, SdkFeatureBand? sdkFeatureBand = null) { @@ -62,7 +59,7 @@ public WorkloadManifestUpdater(IReporter reporter, private static WorkloadManifestUpdater GetInstance(string userProfileDir) { var reporter = new NullReporter(); - var dotnetPath = Path.GetDirectoryName(Environment.ProcessPath); + var dotnetPath = Path.GetDirectoryName(Environment.ProcessPath)!; var sdkVersion = Product.Version; var workloadManifestProvider = new SdkDirectoryWorkloadManifestProvider(dotnetPath, sdkVersion, userProfileDir, SdkDirectoryWorkloadManifestProvider.GetGlobalJsonPath(Environment.CurrentDirectory)); var workloadResolver = WorkloadResolver.Create(workloadManifestProvider, dotnetPath, sdkVersion, userProfileDir); @@ -89,7 +86,7 @@ public async Task UpdateAdvertisingManifestsAsync(bool includePreviews, bool use } else { - // this updates all the manifests + // this updates all the manifests var manifests = _workloadResolver.GetInstalledManifests(); await Task.WhenAll(manifests.Select(manifest => UpdateAdvertisingManifestAsync(manifest, includePreviews, offlineCache))).ConfigureAwait(false); WriteUpdatableWorkloadsFile(); @@ -143,7 +140,7 @@ private void WriteUpdatableWorkloadsFile() var jsonContent = JsonSerializer.Serialize(updatableWorkloads.Select(workload => workload.ToString()).ToArray()); if (Directory.Exists(Path.GetDirectoryName(filePath))) { - Directory.CreateDirectory(Path.GetDirectoryName(filePath)); + Directory.CreateDirectory(Path.GetDirectoryName(filePath)!); } File.WriteAllText(filePath, jsonContent); } @@ -180,7 +177,7 @@ public static void AdvertiseWorkloadUpdates() } } - public string GetAdvertisedWorkloadSetVersion() + public string? GetAdvertisedWorkloadSetVersion() { var advertisedPath = GetAdvertisingManifestPath(_sdkFeatureBand, new ManifestId(WorkloadSetManifestId)); var workloadSetVersionFilePath = Path.Combine(advertisedPath, Constants.workloadSetVersionFileName); @@ -237,7 +234,7 @@ public IEnumerable GetUpdatableWorkloadsToAdvertise(IEnumerable CalculateManifestRollbacks(string rollbackDefinitionFilePath, WorkloadHistoryRecorder recorder = null) + public IEnumerable CalculateManifestRollbacks(string rollbackDefinitionFilePath, WorkloadHistoryRecorder? recorder = null) { var currentManifestIds = GetInstalledManifestIds(); var manifestRollbacks = ParseRollbackDefinitionFile(rollbackDefinitionFilePath, _sdkFeatureBand); @@ -312,10 +309,10 @@ public async Task> GetManifestPackageDownloadsAsyn private IEnumerable GetInstalledManifestIds() => _workloadResolver.GetInstalledManifests().Select(manifest => new ManifestId(manifest.Id)); - private async Task UpdateManifestWithVersionAsync(string id, bool includePreviews, SdkFeatureBand band, NuGetVersion packageVersion = null, DirectoryPath? offlineCache = null) + private async Task UpdateManifestWithVersionAsync(string id, bool includePreviews, SdkFeatureBand band, NuGetVersion? packageVersion = null, DirectoryPath? offlineCache = null) { var manifestId = new ManifestId(id); - string packagePath = null; + string? packagePath = null; try { var manifestPackageId = _workloadManifestInstaller.GetManifestPackageId(manifestId, band); diff --git a/src/Cli/dotnet/Commands/Workload/Update/WorkloadUpdateCommand.cs b/src/Cli/dotnet/Commands/Workload/Update/WorkloadUpdateCommand.cs index 02f78b88a4a2..b059add3c698 100644 --- a/src/Cli/dotnet/Commands/Workload/Update/WorkloadUpdateCommand.cs +++ b/src/Cli/dotnet/Commands/Workload/Update/WorkloadUpdateCommand.cs @@ -1,16 +1,13 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -#nullable disable - using System.CommandLine; using System.Text.Json; using Microsoft.DotNet.Cli.Commands.Workload.Install; -using Microsoft.DotNet.Cli.Configuration; using Microsoft.DotNet.Cli.NuGetPackageDownloader; using Microsoft.DotNet.Cli.ToolPackage; using Microsoft.DotNet.Cli.Utils; -using Microsoft.Extensions.Configuration.DotnetCli.Services; +using Microsoft.Extensions.Configuration.DotnetCli; using Microsoft.Extensions.EnvironmentAbstractions; using Microsoft.NET.Sdk.WorkloadManifestReader; using NuGet.Common; @@ -28,14 +25,14 @@ internal class WorkloadUpdateCommand : InstallingWorkloadCommand private readonly bool _shouldShutdownInstaller; public WorkloadUpdateCommand( ParseResult parseResult, - IReporter reporter = null, - IWorkloadResolverFactory workloadResolverFactory = null, - IInstaller workloadInstaller = null, - INuGetPackageDownloader nugetPackageDownloader = null, - IWorkloadManifestUpdater workloadManifestUpdater = null, - string tempDirPath = null, + IReporter? reporter = null, + IWorkloadResolverFactory? workloadResolverFactory = null, + IInstaller? workloadInstaller = null, + INuGetPackageDownloader? nugetPackageDownloader = null, + IWorkloadManifestUpdater? workloadManifestUpdater = null, + string? tempDirPath = null, bool isRestoring = false, - WorkloadHistoryRecorder recorder = null, + WorkloadHistoryRecorder? recorder = null, bool? shouldUseWorkloadSetsFromGlobalJson = null) : base(parseResult, reporter: reporter, workloadResolverFactory: workloadResolverFactory, workloadInstaller: workloadInstaller, nugetPackageDownloader: nugetPackageDownloader, workloadManifestUpdater: workloadManifestUpdater, @@ -57,12 +54,15 @@ public WorkloadUpdateCommand( _workloadManifestUpdater = _workloadManifestUpdaterFromConstructor ?? new WorkloadManifestUpdater(resolvedReporter, _workloadResolver, PackageDownloader, _userProfileDir, _workloadInstaller.GetWorkloadInstallationRecordRepository(), _workloadInstaller, _packageSourceLocation, sdkFeatureBand: _sdkFeatureBand); - _recorder = recorder; - if (_recorder is null) + + if (recorder is null) { _recorder = new(_workloadResolver, _workloadInstaller, () => _workloadResolverFactory.CreateForWorkloadSet(_dotnetPath, _sdkVersion.ToString(), _userProfileDir, null)); _recorder.HistoryRecord.CommandName = "update"; - + } + else + { + _recorder = recorder; } _fromHistorySpecified = parseResult.GetValue(WorkloadUpdateCommandParser.FromHistoryOption); @@ -207,7 +207,7 @@ private async Task DownloadToOfflineCacheAsync(DirectoryPath offlineCache, bool await GetDownloads(GetUpdatableWorkloads(), skipManifestUpdate: false, includePreviews, offlineCache.Value); } - private async Task> GetUpdatablePackageUrlsAsync(bool includePreview, IReporter reporter = null, INuGetPackageDownloader packageDownloader = null) + private async Task> GetUpdatablePackageUrlsAsync(bool includePreview, IReporter? reporter = null, INuGetPackageDownloader? packageDownloader = null) { reporter ??= Reporter; packageDownloader ??= PackageDownloader; @@ -222,7 +222,7 @@ private async Task> GetUpdatablePackageUrlsAsync(bool includ return urls; } - private IEnumerable GetUpdatableWorkloads(IReporter reporter = null) + private IEnumerable? GetUpdatableWorkloads(IReporter? reporter = null) { reporter ??= Reporter; var workloads = FromHistory ? _WorkloadHistoryRecord.InstalledWorkloads.Select(s => new WorkloadId(s)) : GetInstalledWorkloads(_fromPreviousSdk); diff --git a/src/Cli/dotnet/Commands/Workload/WorkloadCommandBase.cs b/src/Cli/dotnet/Commands/Workload/WorkloadCommandBase.cs index 8e5327112a02..bbd17f5a4916 100644 --- a/src/Cli/dotnet/Commands/Workload/WorkloadCommandBase.cs +++ b/src/Cli/dotnet/Commands/Workload/WorkloadCommandBase.cs @@ -3,11 +3,9 @@ using System.CommandLine; using Microsoft.DotNet.Cli.Commands.Workload.Install; -using Microsoft.DotNet.Cli.Configuration; using Microsoft.DotNet.Cli.Extensions; using Microsoft.DotNet.Cli.NuGetPackageDownloader; using Microsoft.DotNet.Cli.Utils; -using Microsoft.Extensions.Configuration.DotnetCli.Services; using Microsoft.Extensions.EnvironmentAbstractions; using NuGet.Common; @@ -122,7 +120,6 @@ public WorkloadCommandBase( IsPackageDownloaderProvided = false; PackageDownloader = new NuGetPackageDownloader.NuGetPackageDownloader( TempPackagesDirectory, - DotNetConfigurationFactory.Create(), filePermissionSetter: null, firstPartyNuGetPackageSigningVerifier: new FirstPartyNuGetPackageSigningVerifier(), verboseLogger: nugetLogger, diff --git a/src/Cli/dotnet/Commands/Workload/WorkloadIntegrityChecker.cs b/src/Cli/dotnet/Commands/Workload/WorkloadIntegrityChecker.cs index 4f522d0ca0a0..accb3f4726be 100644 --- a/src/Cli/dotnet/Commands/Workload/WorkloadIntegrityChecker.cs +++ b/src/Cli/dotnet/Commands/Workload/WorkloadIntegrityChecker.cs @@ -1,12 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -#nullable disable - using Microsoft.DotNet.Cli.Commands.Workload.Install; -using Microsoft.DotNet.Cli.Configuration; using Microsoft.DotNet.Cli.Utils; -using Microsoft.Extensions.Configuration.DotnetCli.Services; using Microsoft.Extensions.EnvironmentAbstractions; using Microsoft.NET.Sdk.WorkloadManifestReader; using NuGet.Common; @@ -23,7 +19,6 @@ public static void RunFirstUseCheck(IReporter reporter) var tempPackagesDirectory = new DirectoryPath(PathUtilities.CreateTempSubdirectory()); var packageDownloader = new NuGetPackageDownloader.NuGetPackageDownloader( tempPackagesDirectory, - DotNetConfigurationFactory.Create(), verboseLogger: new NullLogger(), verifySignatures: verifySignatures); diff --git a/src/Cli/dotnet/Configuration/ConfigurationBasedEnvironmentProvider.cs b/src/Cli/dotnet/Configuration/ConfigurationBasedEnvironmentProvider.cs deleted file mode 100644 index 22f23fc866fc..000000000000 --- a/src/Cli/dotnet/Configuration/ConfigurationBasedEnvironmentProvider.cs +++ /dev/null @@ -1,139 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using System; -using System.Collections.Generic; -using Microsoft.DotNet.Cli.Utils; -using Microsoft.Extensions.Configuration.DotnetCli.Services; - -namespace Microsoft.DotNet.Cli.Configuration -{ - /// - /// Bridge between the new configuration system and existing IEnvironmentProvider interface. - /// Provides backward compatibility while enabling migration to the new configuration system. - /// - public class ConfigurationBasedEnvironmentProvider : IEnvironmentProvider - { - private readonly IDotNetConfigurationService _configurationService; - private readonly IEnvironmentProvider _fallbackProvider; - - // Reverse mapping from environment variable names to canonical keys - private static readonly Dictionary EnvironmentToCanonicalMappings = new() - { - ["DOTNET_HOST_PATH"] = "RuntimeHost:HostPath", - ["DOTNET_CLI_TELEMETRY_OPTOUT"] = "CliUserExperience:TelemetryOptOut", - ["DOTNET_NOLOGO"] = "CliUserExperience:NoLogo", - ["DOTNET_CLI_FORCE_UTF8_ENCODING"] = "CliUserExperience:ForceUtf8Encoding", - ["DOTNET_CLI_UI_LANGUAGE"] = "CliUserExperience:UILanguage", - ["DOTNET_CLI_TELEMETRY_PROFILE"] = "CliUserExperience:TelemetryProfile", - ["DOTNET_CLI_PERF_LOG"] = "Development:PerfLogEnabled", - ["DOTNET_PERF_LOG_COUNT"] = "Development:PerfLogCount", - ["DOTNET_CLI_HOME"] = "Development:CliHome", - ["DOTNET_CLI_CONTEXT_VERBOSE"] = "Development:ContextVerbose", - ["DOTNET_MULTILEVEL_LOOKUP"] = "RuntimeHost:MultilevelLookup", - ["DOTNET_ROLL_FORWARD"] = "RuntimeHost:RollForward", - ["DOTNET_ROLL_FORWARD_ON_NO_CANDIDATE_FX"] = "RuntimeHost:RollForwardOnNoCandidateFx", - ["DOTNET_ROOT"] = "RuntimeHost:Root", - ["DOTNET_ROOT(x86)"] = "RuntimeHost:RootX86", - ["DOTNET_CLI_RUN_MSBUILD_OUTOFPROC"] = "Build:RunMSBuildOutOfProc", - ["DOTNET_CLI_USE_MSBUILD_SERVER"] = "Build:UseMSBuildServer", - ["DOTNET_CLI_CONFIGURE_MSBUILD_TERMINAL_LOGGER"] = "Build:ConfigureMSBuildTerminalLogger", - ["DOTNET_CLI_DISABLE_PUBLISH_AND_PACK_RELEASE"] = "Build:DisablePublishAndPackRelease", - ["DOTNET_CLI_LAZY_PUBLISH_AND_PACK_RELEASE_FOR_SOLUTIONS"] = "Build:LazyPublishAndPackReleaseForSolutions", - ["DOTNET_MSBUILD_SDK_RESOLVER_ENABLE_LOG"] = "SdkResolver:EnableLog", - ["DOTNET_MSBUILD_SDK_RESOLVER_SDKS_DIR"] = "SdkResolver:SdksDirectory", - ["DOTNET_MSBUILD_SDK_RESOLVER_SDKS_VER"] = "SdkResolver:SdksVersion", - ["DOTNET_MSBUILD_SDK_RESOLVER_CLI_DIR"] = "SdkResolver:CliDirectory", - ["DOTNET_CLI_WORKLOAD_UPDATE_NOTIFY_DISABLE"] = "Workload:UpdateNotifyDisable", - ["DOTNET_CLI_WORKLOAD_UPDATE_NOTIFY_INTERVAL_HOURS"] = "Workload:UpdateNotifyIntervalHours", - ["DOTNET_CLI_WORKLOAD_DISABLE_PACK_GROUPS"] = "Workload:DisablePackGroups", - ["DOTNET_SKIP_WORKLOAD_INTEGRITY_CHECK"] = "Workload:SkipIntegrityCheck", - ["DOTNETSDK_WORKLOAD_MANIFEST_ROOTS"] = "Workload:ManifestRoots", - ["DOTNETSDK_WORKLOAD_PACK_ROOTS"] = "Workload:PackRoots", - ["DOTNETSDK_WORKLOAD_MANIFEST_IGNORE_DEFAULT_ROOTS"] = "Workload:ManifestIgnoreDefaultRoots", - ["DOTNETSDK_ALLOW_TARGETING_PACK_CACHING"] = "Development:AllowTargetingPackCaching", - ["DOTNET_GENERATE_ASPNET_CERTIFICATE"] = "FirstTimeUse:GenerateAspNetCertificate", - ["DOTNET_ADD_GLOBAL_TOOLS_TO_PATH"] = "FirstTimeUse:AddGlobalToolsToPath", - ["DOTNET_SKIP_FIRST_TIME_EXPERIENCE"] = "FirstTimeUse:SkipFirstTimeExperience", - ["DOTNET_TOOLS_ALLOW_MANIFEST_IN_ROOT"] = "Tool:AllowManifestInRoot", - }; - - public ConfigurationBasedEnvironmentProvider( - IDotNetConfigurationService configurationService, - IEnvironmentProvider? fallbackProvider = null) - { - _configurationService = configurationService ?? throw new ArgumentNullException(nameof(configurationService)); - _fallbackProvider = fallbackProvider ?? new EnvironmentProvider(); - } - - public IEnumerable ExecutableExtensions => _fallbackProvider.ExecutableExtensions; - - public string? GetCommandPath(string commandName, params string[] extensions) - { - return _fallbackProvider.GetCommandPath(commandName, extensions); - } - - public string? GetCommandPathFromRootPath(string rootPath, string commandName, params string[] extensions) - { - return _fallbackProvider.GetCommandPathFromRootPath(rootPath, commandName, extensions); - } - - public string? GetCommandPathFromRootPath(string rootPath, string commandName, IEnumerable extensions) - { - return _fallbackProvider.GetCommandPathFromRootPath(rootPath, commandName, extensions); - } - - public string? GetEnvironmentVariable(string name) - { - // First try to get from the new configuration system - if (EnvironmentToCanonicalMappings.TryGetValue(name, out var canonicalKey)) - { - var value = _configurationService.RawConfiguration[canonicalKey]; - if (!string.IsNullOrEmpty(value)) - { - return value; - } - } - - // Fall back to direct environment variable access - return _fallbackProvider.GetEnvironmentVariable(name); - } - - public bool GetEnvironmentVariableAsBool(string name, bool defaultValue) - { - var value = GetEnvironmentVariable(name); - if (string.IsNullOrEmpty(value)) - { - return defaultValue; - } - - return value.ToLowerInvariant() switch - { - "true" or "1" or "yes" => true, - "false" or "0" or "no" => false, - _ => defaultValue - }; - } - - public int? GetEnvironmentVariableAsNullableInt(string name) - { - var value = GetEnvironmentVariable(name); - if (string.IsNullOrEmpty(value)) - { - return null; - } - - return int.TryParse(value, out var result) ? result : null; - } - - public string? GetEnvironmentVariable(string variable, EnvironmentVariableTarget target) - { - return _fallbackProvider.GetEnvironmentVariable(variable, target); - } - - public void SetEnvironmentVariable(string variable, string value, EnvironmentVariableTarget target) - { - _fallbackProvider.SetEnvironmentVariable(variable, value, target); - } - } -} diff --git a/src/Cli/dotnet/Configuration/DotNetConfigurationFactory.cs b/src/Cli/dotnet/Configuration/DotNetConfigurationFactory.cs deleted file mode 100644 index 14938c497492..000000000000 --- a/src/Cli/dotnet/Configuration/DotNetConfigurationFactory.cs +++ /dev/null @@ -1,93 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using Microsoft.Extensions.Configuration.DotnetCli.Services; -using Microsoft.Extensions.Configuration.DotnetCli.Models; -using Microsoft.Extensions.Configuration.DotnetCli.Providers; -using Microsoft.Extensions.Configuration; -using System.Collections.Concurrent; -using System.IO; - -namespace Microsoft.DotNet.Cli.Configuration -{ - /// - /// Factory for creating and configuring the .NET CLI configuration service. - /// This is the main entry point for the unified configuration system. - /// - public static class DotNetConfigurationFactory - { - // Cache for the default configuration service instance - private static readonly Lazy _defaultInstance = new(() => CreateInternal(null), LazyThreadSafetyMode.ExecutionAndPublication); - - // Cache for configuration services by working directory - private static readonly ConcurrentDictionary> _instancesByDirectory = new(); - - /// - /// Creates and configures the .NET CLI configuration service with default providers. - /// This method follows the layered configuration approach: environment variables override global.json, - /// and configuration is loaded lazily for performance. - /// Results are cached to avoid repeated expensive configuration building. - /// - /// The working directory to search for global.json files. Defaults to current directory. - /// A configured IDotNetConfigurationService instance - public static IDotNetConfigurationService Create(string? workingDirectory = null) - { - if (workingDirectory == null) - { - // Use the default cached instance for null working directory - return _defaultInstance.Value; - } - - // Normalize the working directory path for consistent caching - var normalizedPath = Path.GetFullPath(workingDirectory); - - // Get or create a cached instance for this specific working directory - var lazyInstance = _instancesByDirectory.GetOrAdd(normalizedPath, - path => new Lazy(() => CreateInternal(path), LazyThreadSafetyMode.ExecutionAndPublication)); - - return lazyInstance.Value; - } - - /// - /// Internal method that performs the actual configuration creation without caching. - /// - /// The working directory to search for configuration files. - /// A configured IDotNetConfigurationService instance - private static IDotNetConfigurationService CreateInternal(string? workingDirectory) - { - workingDirectory ??= Directory.GetCurrentDirectory(); - - var configurationBuilder = new ConfigurationBuilder(); - - // Configuration sources are added in reverse precedence order - // Last added has highest precedence - - // 1. dotnet.config (custom provider with key mapping) - lowest precedence - configurationBuilder.Add(new DotNetConfigurationSource(workingDirectory)); - - // 2. global.json (custom provider with key mapping) - medium precedence - configurationBuilder.Add(new GlobalJsonConfigurationSource(workingDirectory)); - - // 3. Environment variables (custom provider with key mapping) - highest precedence - configurationBuilder.Add(new DotNetEnvironmentConfigurationSource()); - - var configuration = configurationBuilder.Build(); - - return new DotNetConfigurationService(configuration); - } - - /// - /// Creates a minimal configuration service with only environment variables. - /// This is useful for scenarios where global.json lookup is not needed or desirable, - /// such as in performance-critical paths or testing scenarios. - /// - /// A minimal IDotNetConfigurationService instance - public static IDotNetConfigurationService CreateMinimal() - { - var configurationBuilder = new ConfigurationBuilder(); - configurationBuilder.Add(new DotNetEnvironmentConfigurationSource()); - var configuration = configurationBuilder.Build(); - return new DotNetConfigurationService(configuration); - } - } -} diff --git a/src/Cli/dotnet/NugetPackageDownloader/NuGetPackageDownloader.cs b/src/Cli/dotnet/NugetPackageDownloader/NuGetPackageDownloader.cs index 7c6809fd18cc..fcb5c0ff30d5 100644 --- a/src/Cli/dotnet/NugetPackageDownloader/NuGetPackageDownloader.cs +++ b/src/Cli/dotnet/NugetPackageDownloader/NuGetPackageDownloader.cs @@ -1,14 +1,10 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -#nullable disable - using System.Collections.Concurrent; -using Microsoft.DotNet.Cli.Configuration; -using Microsoft.DotNet.Cli.NugetPackageDownloader; using Microsoft.DotNet.Cli.ToolPackage; using Microsoft.DotNet.Cli.Utils; -using Microsoft.Extensions.Configuration.DotnetCli.Services; +using Microsoft.Extensions.Configuration.DotnetCli; using Microsoft.Extensions.EnvironmentAbstractions; using NuGet.Common; using NuGet.Configuration; @@ -20,7 +16,6 @@ namespace Microsoft.DotNet.Cli.NuGetPackageDownloader; -// TODO: Never name a class the same name as the namespace. Update either for easier type resolution. internal class NuGetPackageDownloader : INuGetPackageDownloader { private readonly SourceCacheContext _cacheSettings; @@ -32,7 +27,6 @@ internal class NuGetPackageDownloader : INuGetPackageDownloader private readonly ILogger _verboseLogger; private readonly DirectoryPath _packageInstallDir; private readonly RestoreActionConfig _restoreActionConfig; - private readonly Func> _retryTimer; /// /// Reporter would output to the console regardless @@ -45,25 +39,24 @@ internal class NuGetPackageDownloader : INuGetPackageDownloader /// /// If true, the package downloader will verify the signatures of the packages it downloads. - /// Temporarily disabled for macOS and Linux. + /// Temporarily disabled for macOS and Linux. /// private readonly bool _verifySignatures; private readonly VerbosityOptions _verbosityOptions; - private readonly string _currentWorkingDirectory; + private readonly string? _currentWorkingDirectory; public NuGetPackageDownloader( DirectoryPath packageInstallDir, - IDotNetConfigurationService configurationService, - IFilePermissionSetter filePermissionSetter = null, - IFirstPartyNuGetPackageSigningVerifier firstPartyNuGetPackageSigningVerifier = null, - ILogger verboseLogger = null, - IReporter reporter = null, - RestoreActionConfig restoreActionConfig = null, - Func> timer = null, + DotNetCliConfiguration? configurationService = null, + IFilePermissionSetter? filePermissionSetter = null, + IFirstPartyNuGetPackageSigningVerifier? firstPartyNuGetPackageSigningVerifier = null, + ILogger? verboseLogger = null, + IReporter? reporter = null, + RestoreActionConfig? restoreActionConfig = null, bool verifySignatures = false, bool shouldUsePackageSourceMapping = false, VerbosityOptions verbosityOptions = VerbosityOptions.normal, - string currentWorkingDirectory = null) + string? currentWorkingDirectory = null) { _currentWorkingDirectory = currentWorkingDirectory; _packageInstallDir = packageInstallDir; @@ -73,10 +66,9 @@ public NuGetPackageDownloader( new FirstPartyNuGetPackageSigningVerifier(); _filePermissionSetter = filePermissionSetter ?? new FilePermissionSetter(); _restoreActionConfig = restoreActionConfig ?? new RestoreActionConfig(); - _retryTimer = timer; _sourceRepositories = new(); // Use configuration service for signature verification - _verifySignatures = verifySignatures && configurationService.NuGet.SignatureVerificationEnabled; + _verifySignatures = verifySignatures && (configurationService ?? DotNetConfigurationFactory.Create()).NuGet.SignatureVerificationEnabled; _cacheSettings = new SourceCacheContext { @@ -92,19 +84,19 @@ public NuGetPackageDownloader( } public async Task DownloadPackageAsync(PackageId packageId, - NuGetVersion packageVersion = null, - PackageSourceLocation packageSourceLocation = null, + NuGetVersion? packageVersion = null, + PackageSourceLocation? packageSourceLocation = null, bool includePreview = false, bool? includeUnlisted = null, DirectoryPath? downloadFolder = null, - PackageSourceMapping packageSourceMapping = null) + PackageSourceMapping? packageSourceMapping = null) { CancellationToken cancellationToken = CancellationToken.None; (var source, var resolvedPackageVersion) = await GetPackageSourceAndVersion(packageId, packageVersion, packageSourceLocation, includePreview, includeUnlisted ?? packageVersion is not null, packageSourceMapping).ConfigureAwait(false); - FindPackageByIdResource resource = null; + FindPackageByIdResource? resource = null; SourceRepository repository = GetSourceRepository(source); resource = await repository.GetResourceAsync(cancellationToken) @@ -122,9 +114,9 @@ public async Task DownloadPackageAsync(PackageId packageId, throw new ArgumentException($"Package download folder must be specified either via {nameof(NuGetPackageDownloader)} constructor or via {nameof(downloadFolder)} method argument."); } var pathResolver = new VersionFolderPathResolver(resolvedDownloadFolder); - + string nupkgPath = pathResolver.GetPackageFilePath(packageId.ToString(), resolvedPackageVersion); - Directory.CreateDirectory(Path.GetDirectoryName(nupkgPath)); + Directory.CreateDirectory(Path.GetDirectoryName(nupkgPath)!); using FileStream destinationStream = File.Create(nupkgPath); bool success = await ExponentialRetry.ExecuteWithRetryOnFailure(async () => await resource.CopyNupkgToStreamAsync( @@ -140,7 +132,7 @@ public async Task DownloadPackageAsync(PackageId packageId, { throw new NuGetPackageInstallerException( string.Format("Downloading {0} version {1} failed", packageId, - packageVersion.ToNormalizedString())); + resolvedPackageVersion.ToNormalizedString())); } // Delete file if verification fails @@ -204,9 +196,9 @@ await repository.GetResourceAsync().ConfigureAwait( } } - public async Task GetPackageUrl(PackageId packageId, - NuGetVersion packageVersion = null, - PackageSourceLocation packageSourceLocation = null, + public async Task GetPackageUrl(PackageId packageId, + NuGetVersion? packageVersion = null, + PackageSourceLocation? packageSourceLocation = null, bool includePreview = false) { (var source, var resolvedPackageVersion) = await GetPackageSourceAndVersion(packageId, packageVersion, packageSourceLocation, includePreview).ConfigureAwait(false); @@ -220,10 +212,12 @@ public async Task GetPackageUrl(PackageId packageId, ); } - ServiceIndexResourceV3 serviceIndexResource = repository.GetResourceAsync().Result; - IReadOnlyList packageBaseAddress = - serviceIndexResource?.GetServiceEntryUris(ServiceTypes.PackageBaseAddress); - + ServiceIndexResourceV3? serviceIndexResource = await repository.GetResourceAsync(); + if (serviceIndexResource is null) + { + return null; + } + IReadOnlyList packageBaseAddress = serviceIndexResource.GetServiceEntryUris(ServiceTypes.PackageBaseAddress); return GetNupkgUrl(packageBaseAddress[0].ToString(), packageId, resolvedPackageVersion); } @@ -248,9 +242,8 @@ public async Task> ExtractPackageAsync(string packagePath, D if (!OperatingSystem.IsWindows()) { - string workloadUnixFilePermissions = allFilesInPackage.SingleOrDefault(p => - Path.GetRelativePath(targetFolder.Value, p).Equals("data/UnixFilePermissions.xml", - StringComparison.OrdinalIgnoreCase)); + string? workloadUnixFilePermissions = + allFilesInPackage.SingleOrDefault(p => Path.GetRelativePath(targetFolder.Value, p).Equals("data/UnixFilePermissions.xml", StringComparison.OrdinalIgnoreCase)); if (workloadUnixFilePermissions != default) { @@ -275,11 +268,11 @@ public async Task> GetLatestVersionsOfPackag } private async Task<(PackageSource, NuGetVersion)> GetPackageSourceAndVersion(PackageId packageId, - NuGetVersion packageVersion = null, - PackageSourceLocation packageSourceLocation = null, + NuGetVersion? packageVersion = null, + PackageSourceLocation? packageSourceLocation = null, bool includePreview = false, bool includeUnlisted = false, - PackageSourceMapping packageSourceMapping = null) + PackageSourceMapping? packageSourceMapping = null) { CancellationToken cancellationToken = CancellationToken.None; @@ -348,30 +341,26 @@ private static bool PackageIsInAllowList(IEnumerable files) return true; } - private IEnumerable LoadOverrideSources(PackageSourceLocation packageSourceLocation = null) + private IEnumerable LoadOverrideSources(PackageSourceLocation? packageSourceLocation = null) { - foreach (string source in packageSourceLocation?.SourceFeedOverrides) - { - if (string.IsNullOrWhiteSpace(source)) - { - continue; - } + var sources = packageSourceLocation?.SourceFeedOverrides + .Where(source => !string.IsNullOrWhiteSpace(source)) + .Select(source => new PackageSource(source)); - PackageSource packageSource = new(source); - if (packageSource.TrySourceAsUri == null) + foreach (PackageSource source in sources ?? []) + { + if (source.TrySourceAsUri == null) { _verboseLogger.LogWarning(string.Format( CliStrings.FailedToLoadNuGetSource, - source)); + source.Source)); continue; } - - yield return packageSource; + yield return source; } - } - private List LoadDefaultSources(PackageId packageId, PackageSourceLocation packageSourceLocation = null, PackageSourceMapping packageSourceMapping = null) + private List LoadDefaultSources(PackageId packageId, PackageSourceLocation? packageSourceLocation = null, PackageSourceMapping? packageSourceMapping = null) { List defaultSources = []; string currentDirectory = _currentWorkingDirectory ?? Directory.GetCurrentDirectory(); @@ -413,7 +402,7 @@ private List LoadDefaultSources(PackageId packageId, PackageSourc if (packageSourceLocation?.AdditionalSourceFeed?.Any() ?? false) { - foreach (string source in packageSourceLocation?.AdditionalSourceFeed) + foreach (string source in packageSourceLocation?.AdditionalSourceFeed ?? []) { if (string.IsNullOrWhiteSpace(source)) { @@ -441,7 +430,7 @@ private List LoadDefaultSources(PackageId packageId, PackageSourc return defaultSources; } - public IEnumerable LoadNuGetSources(PackageId packageId, PackageSourceLocation packageSourceLocation = null, PackageSourceMapping packageSourceMapping = null) + public IEnumerable LoadNuGetSources(PackageId packageId, PackageSourceLocation? packageSourceLocation = null, PackageSourceMapping? packageSourceMapping = null) { var sources = (packageSourceLocation?.SourceFeedOverrides.Any() ?? false) ? LoadOverrideSources(packageSourceLocation) : @@ -607,9 +596,10 @@ await Task.WhenAll( return latestVersions.Take(numberOfResults); } - public async Task GetBestPackageVersionAsync(PackageId packageId, + public async Task GetBestPackageVersionAsync( + PackageId packageId, VersionRange versionRange, - PackageSourceLocation packageSourceLocation = null) + PackageSourceLocation? packageSourceLocation = null) { if (versionRange.MinVersion != null && versionRange.MaxVersion != null && versionRange.MinVersion == versionRange.MaxVersion) { @@ -621,9 +611,10 @@ public async Task GetBestPackageVersionAsync(PackageId packageId, .version; } - public async Task<(NuGetVersion version, PackageSource source)> GetBestPackageVersionAndSourceAsync(PackageId packageId, + public async Task<(NuGetVersion version, PackageSource source)> GetBestPackageVersionAndSourceAsync( + PackageId packageId, VersionRange versionRange, - PackageSourceLocation packageSourceLocation = null) + PackageSourceLocation? packageSourceLocation = null) { CancellationToken cancellationToken = CancellationToken.None; IPackageSearchMetadata packageMetadata; @@ -665,7 +656,7 @@ bool TryGetPackageMetadata( } atLeastOneSourceValid = true; - IPackageSearchMetadata matchedVersion = + IPackageSearchMetadata? matchedVersion = sourceAndFoundPackages.foundPackages.FirstOrDefault(package => package.Identity.Version == packageVersion); if (matchedVersion != null) @@ -757,13 +748,13 @@ bool TryGetPackageMetadata( } public async Task GetLatestPackageVersion(PackageId packageId, - PackageSourceLocation packageSourceLocation = null, + PackageSourceLocation? packageSourceLocation = null, bool includePreview = false) { return (await GetLatestPackageVersions(packageId, numberOfResults: 1, packageSourceLocation, includePreview)).First(); } - public async Task> GetLatestPackageVersions(PackageId packageId, int numberOfResults, PackageSourceLocation packageSourceLocation = null, bool includePreview = false) + public async Task> GetLatestPackageVersions(PackageId packageId, int numberOfResults, PackageSourceLocation? packageSourceLocation = null, bool includePreview = false) { CancellationToken cancellationToken = CancellationToken.None; IEnumerable packagesSources = LoadNuGetSources(packageId, packageSourceLocation); @@ -773,7 +764,7 @@ public async Task> GetLatestPackageVersions(PackageId result.Item2.Identity.Version); } - public async Task> GetPackageIdsAsync(string idStem, bool allowPrerelease, PackageSourceLocation packageSourceLocation = null, CancellationToken cancellationToken = default) + public async Task> GetPackageIdsAsync(string idStem, bool allowPrerelease, PackageSourceLocation? packageSourceLocation = null, CancellationToken cancellationToken = default) { // grab allowed sources for the package in question PackageId packageId = new(idStem); @@ -788,7 +779,7 @@ public async Task> GetPackageIdsAsync(string idStem, bool al return packageIdLists.SelectMany(v => v).Distinct().OrderDescending(); } - public async Task> GetPackageVersionsAsync(PackageId packageId, string versionPrefix = null, bool allowPrerelease = false, PackageSourceLocation packageSourceLocation = null, CancellationToken cancellationToken = default) + public async Task> GetPackageVersionsAsync(PackageId packageId, string? versionPrefix = null, bool allowPrerelease = false, PackageSourceLocation? packageSourceLocation = null, CancellationToken cancellationToken = default) { // grab allowed sources for the package in question IEnumerable packagesSources = LoadNuGetSources(packageId, packageSourceLocation); @@ -815,7 +806,7 @@ private async Task> GetAutocompleteAsync(Packa // only exposed for testing internal static TimeSpan CliCompletionsTimeout { get; set; } = TimeSpan.FromMilliseconds(500); - private async Task> GetPackageVersionsForSource(AutoCompleteResource autocomplete, PackageId packageId, string versionPrefix, bool allowPrerelease, CancellationToken cancellationToken) + private async Task> GetPackageVersionsForSource(AutoCompleteResource autocomplete, PackageId packageId, string? versionPrefix, bool allowPrerelease, CancellationToken cancellationToken) { try { @@ -855,7 +846,7 @@ private static async Task> GetPackageIdsForSource(AutoComple private SourceRepository GetSourceRepository(PackageSource source) { - if (!_sourceRepositories.TryGetValue(source, out SourceRepository value)) + if (!_sourceRepositories.TryGetValue(source, out SourceRepository? value)) { value = Repository.Factory.GetCoreV3(source); _sourceRepositories.AddOrUpdate(source, _ => value, (_, _) => value); diff --git a/src/Cli/dotnet/NugetPackageDownloader/WorkloadUnixFilePermissionsFileList.cs b/src/Cli/dotnet/NugetPackageDownloader/WorkloadUnixFilePermissionsFileList.cs index 8f8ceb39ad62..2ccef2997acf 100644 --- a/src/Cli/dotnet/NugetPackageDownloader/WorkloadUnixFilePermissionsFileList.cs +++ b/src/Cli/dotnet/NugetPackageDownloader/WorkloadUnixFilePermissionsFileList.cs @@ -7,7 +7,7 @@ using System.Xml; using System.Xml.Serialization; -namespace Microsoft.DotNet.Cli.NugetPackageDownloader; +namespace Microsoft.DotNet.Cli.NuGetPackageDownloader; [Serializable] [DesignerCategory("code")] diff --git a/src/Cli/dotnet/Program.cs b/src/Cli/dotnet/Program.cs index ea39e51c2f60..3ae8a49312d5 100644 --- a/src/Cli/dotnet/Program.cs +++ b/src/Cli/dotnet/Program.cs @@ -10,13 +10,13 @@ using Microsoft.DotNet.Cli.CommandFactory.CommandResolution; using Microsoft.DotNet.Cli.Commands.Run; using Microsoft.DotNet.Cli.Commands.Workload; -using Microsoft.DotNet.Cli.Configuration; using Microsoft.DotNet.Cli.Extensions; using Microsoft.DotNet.Cli.ShellShim; using Microsoft.DotNet.Cli.Telemetry; using Microsoft.DotNet.Cli.Utils; using Microsoft.DotNet.Cli.Utils.Extensions; using Microsoft.DotNet.Configurer; +using Microsoft.Extensions.Configuration.DotnetCli; using Microsoft.Extensions.EnvironmentAbstractions; using NuGet.Frameworks; using CommandResult = System.CommandLine.Parsing.CommandResult; @@ -179,19 +179,18 @@ internal static int ProcessArgs(string[] args, TimeSpan startupTime) PerformanceLogEventSource.Log.FirstTimeConfigurationStart(); // Initialize the new configuration-based environment provider - var configurationService = DotNetConfigurationFactory.Create(); - var environmentProvider = new ConfigurationBasedEnvironmentProvider(configurationService); + var configuration = DotNetConfigurationFactory.Create(); // Use typed configuration directly instead of environment variable calls - bool generateAspNetCertificate = configurationService.FirstTimeUse.GenerateAspNetCertificate; - bool telemetryOptout = configurationService.CliUserExperience.TelemetryOptOut; - bool addGlobalToolsToPath = configurationService.FirstTimeUse.AddGlobalToolsToPath; - bool nologo = configurationService.CliUserExperience.NoLogo; - bool skipWorkloadIntegrityCheck = configurationService.Workload.SkipIntegrityCheck || + bool generateAspNetCertificate = configuration.FirstTimeUse.GenerateAspNetCertificate; + bool telemetryOptout = configuration.CliUserExperience.TelemetryOptOut; + bool addGlobalToolsToPath = configuration.FirstTimeUse.AddGlobalToolsToPath; + bool nologo = configuration.CliUserExperience.NoLogo; + bool skipWorkloadIntegrityCheck = configuration.Workload.SkipIntegrityCheck || // Default the workload integrity check skip to true if the command is being ran in CI. Otherwise, false. new CIEnvironmentDetectorForTelemetry().IsCIEnvironment(); - ReportDotnetHomeUsage(environmentProvider); + ReportDotnetHomeUsage(configuration); var isDotnetBeingInvokedFromNativeInstaller = false; if (parseResult.CommandResult.Command.Name.Equals(Parser.InstallSuccessCommand.Name)) @@ -221,7 +220,7 @@ internal static int ProcessArgs(string[] args, TimeSpan startupTime) toolPathSentinel, isDotnetBeingInvokedFromNativeInstaller, dotnetFirstRunConfiguration, - environmentProvider, + new EnvironmentProvider(), performanceData, skipFirstTimeUseCheck: getStarOptionPassed); PerformanceLogEventSource.Log.FirstTimeConfigurationStop(); @@ -353,10 +352,9 @@ private static int AdjustExitCode(ParseResult parseResult, int exitCode) return exitCode; } - private static void ReportDotnetHomeUsage(IEnvironmentProvider provider) + private static void ReportDotnetHomeUsage(DotNetCliConfiguration config) { - var home = provider.GetEnvironmentVariable(CliFolderPathCalculator.DotnetHomeVariableName); - if (string.IsNullOrEmpty(home)) + if (string.IsNullOrEmpty(config.Development.CliHome)) { return; } @@ -364,7 +362,7 @@ private static void ReportDotnetHomeUsage(IEnvironmentProvider provider) Reporter.Verbose.WriteLine( string.Format( LocalizableStrings.DotnetCliHomeUsed, - home, + config.Development.CliHome, CliFolderPathCalculator.DotnetHomeVariableName)); } diff --git a/src/Cli/dotnet/ReleasePropertyProjectLocator.cs b/src/Cli/dotnet/ReleasePropertyProjectLocator.cs index 811fdec47650..18f3e118cde5 100644 --- a/src/Cli/dotnet/ReleasePropertyProjectLocator.cs +++ b/src/Cli/dotnet/ReleasePropertyProjectLocator.cs @@ -6,7 +6,7 @@ using System.Diagnostics; using Microsoft.Build.Execution; using Microsoft.DotNet.Cli.Utils; -using Microsoft.Extensions.Configuration.DotnetCli.Services; +using Microsoft.Extensions.Configuration.DotnetCli; using Microsoft.NET.Build.Tasks; using Microsoft.VisualStudio.SolutionPersistence.Model; @@ -32,7 +32,7 @@ public DependentCommandOptions(IEnumerable? slnOrProjectArgs, string? co private readonly ParseResult _parseResult; private readonly string _propertyToCheck; - private readonly IDotNetConfigurationService _configurationService; + private readonly DotNetCliConfiguration _configurationService; DependentCommandOptions _options; private readonly IEnumerable _slnOrProjectArgs; @@ -48,9 +48,9 @@ public ReleasePropertyProjectLocator( ParseResult parseResult, string propertyToCheck, DependentCommandOptions commandOptions, - IDotNetConfigurationService configurationService + DotNetCliConfiguration? configurationService = null ) - => (_parseResult, _propertyToCheck, _options, _slnOrProjectArgs, _configurationService) = (parseResult, propertyToCheck, commandOptions, commandOptions.SlnOrProjectArgs, configurationService); + => (_parseResult, _propertyToCheck, _options, _slnOrProjectArgs, _configurationService) = (parseResult, propertyToCheck, commandOptions, commandOptions.SlnOrProjectArgs, configurationService ?? DotNetConfigurationFactory.Create()); /// /// Return dotnet CLI command-line parameters (or an empty list) to change configuration based on ... diff --git a/src/Cli/dotnet/ShellShim/ShellShimTemplateFinder.cs b/src/Cli/dotnet/ShellShim/ShellShimTemplateFinder.cs index 935f9e59098b..c6b22ffcb48b 100644 --- a/src/Cli/dotnet/ShellShim/ShellShimTemplateFinder.cs +++ b/src/Cli/dotnet/ShellShim/ShellShimTemplateFinder.cs @@ -1,14 +1,11 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -#nullable disable - using Microsoft.DotNet.Cli.NuGetPackageDownloader; using Microsoft.DotNet.Cli.ToolPackage; using Microsoft.DotNet.Cli.Utils; using Microsoft.Extensions.EnvironmentAbstractions; using NuGet.Frameworks; -using NuGet.Versioning; namespace Microsoft.DotNet.Cli.ShellShim; @@ -21,7 +18,7 @@ internal class ShellShimTemplateFinder( private readonly INuGetPackageDownloader _nugetPackageDownloader = nugetPackageDownloader; private readonly PackageSourceLocation _packageSourceLocation = packageSourceLocation; - public async Task ResolveAppHostSourceDirectoryAsync(string archOption, NuGetFramework targetFramework, Architecture arch) + public async Task ResolveAppHostSourceDirectoryAsync(string? archOption, NuGetFramework? targetFramework, Architecture arch) { string rid; var validRids = new string[] { "win-x64", "win-arm64", "osx-x64", "osx-arm64" }; @@ -51,8 +48,7 @@ public async Task ResolveAppHostSourceDirectoryAsync(string archOption, } var packageId = new PackageId($"microsoft.netcore.app.host.{rid}"); - NuGetVersion packageVersion = null; - var packagePath = await _nugetPackageDownloader.DownloadPackageAsync(packageId, packageVersion, packageSourceLocation: _packageSourceLocation); + var packagePath = await _nugetPackageDownloader.DownloadPackageAsync(packageId, null, packageSourceLocation: _packageSourceLocation); _ = await _nugetPackageDownloader.ExtractPackageAsync(packagePath, _tempDir); return Path.Combine(_tempDir.Value, "runtimes", rid, "native"); diff --git a/src/Cli/dotnet/ToolPackage/ToolPackageDownloader.cs b/src/Cli/dotnet/ToolPackage/ToolPackageDownloader.cs index c56cc81cc3e1..4ed187cc92dc 100644 --- a/src/Cli/dotnet/ToolPackage/ToolPackageDownloader.cs +++ b/src/Cli/dotnet/ToolPackage/ToolPackageDownloader.cs @@ -1,11 +1,9 @@ // Copyright (c) .NET Foundation and contributors. All rights reserved. // Licensed under the MIT license. See LICENSE file in the project root for full license information. -using Microsoft.DotNet.Cli.Configuration; using Microsoft.DotNet.Cli.Extensions; using Microsoft.DotNet.Cli.NuGetPackageDownloader; using Microsoft.DotNet.Cli.Utils; -using Microsoft.Extensions.Configuration.DotnetCli.Services; using Microsoft.Extensions.EnvironmentAbstractions; using Microsoft.TemplateEngine.Utils; using NuGet.Client; @@ -43,10 +41,8 @@ protected override INuGetPackageDownloader CreateNuGetPackageDownloader( verboseLogger = new NuGetConsoleLogger(); } - var configurationService = DotNetConfigurationFactory.Create(); return new NuGetPackageDownloader.NuGetPackageDownloader( new DirectoryPath(), - configurationService, verboseLogger: verboseLogger, verifySignatures: verifySignatures, shouldUsePackageSourceMapping: true, diff --git a/src/Microsoft.Extensions.Configuration.DotnetCli/Services/DotNetConfiguration.cs b/src/Microsoft.Extensions.Configuration.DotnetCli/DotNetConfiguration.cs similarity index 87% rename from src/Microsoft.Extensions.Configuration.DotnetCli/Services/DotNetConfiguration.cs rename to src/Microsoft.Extensions.Configuration.DotnetCli/DotNetConfiguration.cs index f43c33dc7893..2b1450e79495 100644 --- a/src/Microsoft.Extensions.Configuration.DotnetCli/Services/DotNetConfiguration.cs +++ b/src/Microsoft.Extensions.Configuration.DotnetCli/DotNetConfiguration.cs @@ -3,7 +3,7 @@ using Microsoft.Extensions.Configuration.DotnetCli.Providers; -namespace Microsoft.Extensions.Configuration.DotnetCli.Services; +namespace Microsoft.Extensions.Configuration.DotnetCli; /// /// Factory for creating .NET CLI configuration instances. @@ -44,10 +44,10 @@ public static IConfiguration Create(string? workingDirectory = null) /// /// The working directory to search for configuration files. Defaults to current directory. /// A strongly-typed configuration service. - public static IDotNetConfigurationService CreateTyped(string? workingDirectory = null) + public static DotNetCliConfiguration CreateTyped(string? workingDirectory = null) { var configuration = Create(workingDirectory); - return new DotNetConfigurationService(configuration); + return new DotNetCliConfiguration(configuration); } /// @@ -56,7 +56,7 @@ public static IDotNetConfigurationService CreateTyped(string? workingDirectory = /// /// The working directory (unused for minimal configuration). /// A minimal strongly-typed configuration service. - public static IDotNetConfigurationService CreateMinimal(string? workingDirectory = null) + public static DotNetCliConfiguration CreateMinimal(string? workingDirectory = null) { var builder = new ConfigurationBuilder(); @@ -64,6 +64,6 @@ public static IDotNetConfigurationService CreateMinimal(string? workingDirectory builder.Add(new DotNetEnvironmentConfigurationSource()); var configuration = builder.Build(); - return new DotNetConfigurationService(configuration); + return new DotNetCliConfiguration(configuration); } } diff --git a/src/Microsoft.Extensions.Configuration.DotnetCli/DotNetConfigurationFactory.cs b/src/Microsoft.Extensions.Configuration.DotnetCli/DotNetConfigurationFactory.cs new file mode 100644 index 000000000000..803bd3e693aa --- /dev/null +++ b/src/Microsoft.Extensions.Configuration.DotnetCli/DotNetConfigurationFactory.cs @@ -0,0 +1,88 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.Configuration.DotnetCli.Providers; +using System.Collections.Concurrent; + +namespace Microsoft.Extensions.Configuration.DotnetCli; + +/// +/// Factory for creating and configuring the .NET CLI configuration service. +/// This is the main entry point for the unified configuration system. +/// +public static class DotNetConfigurationFactory +{ + // Cache for the default configuration service instance + private static readonly Lazy _defaultInstance = new(() => CreateInternal(null), LazyThreadSafetyMode.ExecutionAndPublication); + + // Cache for configuration services by working directory + private static readonly ConcurrentDictionary> _instancesByDirectory = new(); + + /// + /// Creates and configures the .NET CLI configuration service with default providers. + /// This method follows the layered configuration approach: environment variables override global.json, + /// and configuration is loaded lazily for performance. + /// Results are cached to avoid repeated expensive configuration building. + /// + /// The working directory to search for global.json files. Defaults to current directory. + /// A configured DotNetCliConfiguration instance + public static DotNetCliConfiguration Create(string? workingDirectory = null) + { + if (workingDirectory == null) + { + // Use the default cached instance for null working directory + return _defaultInstance.Value; + } + + // Normalize the working directory path for consistent caching + var normalizedPath = Path.GetFullPath(workingDirectory); + + // Get or create a cached instance for this specific working directory + var lazyInstance = _instancesByDirectory.GetOrAdd(normalizedPath, + path => new Lazy(() => CreateInternal(path), LazyThreadSafetyMode.ExecutionAndPublication)); + + return lazyInstance.Value; + } + + /// + /// Internal method that performs the actual configuration creation without caching. + /// + /// The working directory to search for configuration files. + /// A configured DotNetConfigurationService instance + private static DotNetCliConfiguration CreateInternal(string? workingDirectory) + { + workingDirectory ??= Directory.GetCurrentDirectory(); + + var configurationBuilder = new ConfigurationBuilder(); + + // Configuration sources are added in reverse precedence order + // Last added has highest precedence + + // 1. dotnet.config (custom provider with key mapping) - lowest precedence + configurationBuilder.Add(new DotNetConfigurationSource(workingDirectory)); + + // 2. global.json (custom provider with key mapping) - medium precedence + configurationBuilder.Add(new GlobalJsonConfigurationSource(workingDirectory)); + + // 3. Environment variables (custom provider with key mapping) - highest precedence + configurationBuilder.Add(new DotNetEnvironmentConfigurationSource()); + + var configuration = configurationBuilder.Build(); + + return new DotNetCliConfiguration(configuration); + } + + /// + /// Creates a minimal configuration service with only environment variables. + /// This is useful for scenarios where global.json lookup is not needed or desirable, + /// such as in performance-critical paths or testing scenarios. + /// + /// A minimal DotNetConfigurationService instance + public static DotNetCliConfiguration CreateMinimal() + { + var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.Add(new DotNetEnvironmentConfigurationSource()); + var configuration = configurationBuilder.Build(); + return new DotNetCliConfiguration(configuration); + } +} diff --git a/src/Microsoft.Extensions.Configuration.DotnetCli/Services/DotNetConfigurationService.cs b/src/Microsoft.Extensions.Configuration.DotnetCli/DotNetConfigurationService.cs similarity index 92% rename from src/Microsoft.Extensions.Configuration.DotnetCli/Services/DotNetConfigurationService.cs rename to src/Microsoft.Extensions.Configuration.DotnetCli/DotNetConfigurationService.cs index 52c5cee7a53c..0cec13f1f232 100644 --- a/src/Microsoft.Extensions.Configuration.DotnetCli/Services/DotNetConfigurationService.cs +++ b/src/Microsoft.Extensions.Configuration.DotnetCli/DotNetConfigurationService.cs @@ -3,12 +3,12 @@ using Microsoft.Extensions.Configuration.DotnetCli.Models; -namespace Microsoft.Extensions.Configuration.DotnetCli.Services; +namespace Microsoft.Extensions.Configuration.DotnetCli; /// -/// Strongly-typed configuration service for .NET CLI with lazy initialization. +/// Strongly-typed configuration for .NET CLI with lazy initialization. /// -public class DotNetConfigurationService : IDotNetConfigurationService +public class DotNetCliConfiguration { private readonly IConfiguration _configuration; @@ -38,7 +38,7 @@ public class DotNetConfigurationService : IDotNetConfigurationService public NuGetConfiguration NuGet => _nuget.Value; public TestConfiguration Test => _test.Value; - public DotNetConfigurationService(IConfiguration configuration) + public DotNetCliConfiguration(IConfiguration configuration) { _configuration = configuration ?? throw new ArgumentNullException(nameof(configuration)); diff --git a/src/Microsoft.Extensions.Configuration.DotnetCli/Services/IDotNetConfigurationService.cs b/src/Microsoft.Extensions.Configuration.DotnetCli/Services/IDotNetConfigurationService.cs deleted file mode 100644 index a4b67f6e2780..000000000000 --- a/src/Microsoft.Extensions.Configuration.DotnetCli/Services/IDotNetConfigurationService.cs +++ /dev/null @@ -1,67 +0,0 @@ -// Licensed to the .NET Foundation under one or more agreements. -// The .NET Foundation licenses this file to you under the MIT license. - -using Microsoft.Extensions.Configuration.DotnetCli.Models; - -namespace Microsoft.Extensions.Configuration.DotnetCli.Services; - -/// -/// Interface for strongly-typed .NET CLI configuration service. -/// -public interface IDotNetConfigurationService -{ - /// - /// Gets the underlying IConfiguration instance for advanced scenarios. - /// - IConfiguration RawConfiguration { get; } - - /// - /// Gets CLI user experience configuration settings. - /// - CliUserExperienceConfiguration CliUserExperience { get; } - - /// - /// Gets runtime host configuration settings. - /// - RuntimeHostConfiguration RuntimeHost { get; } - - /// - /// Gets build and MSBuild configuration settings. - /// - BuildConfiguration Build { get; } - - /// - /// Gets SDK resolver configuration settings. - /// - SdkResolverConfiguration SdkResolver { get; } - - /// - /// Gets workload management configuration settings. - /// - WorkloadConfiguration Workload { get; } - - /// - /// Gets first-time use experience configuration settings. - /// - FirstTimeUseConfiguration FirstTimeUse { get; } - - /// - /// Gets development and debugging configuration settings. - /// - DevelopmentConfiguration Development { get; } - - /// - /// Gets global tools configuration settings. - /// - ToolConfiguration Tool { get; } - - /// - /// Gets NuGet package management configuration settings. - /// - NuGetConfiguration NuGet { get; } - - /// - /// Gets test runner configuration settings. - /// - TestConfiguration Test { get; } -} diff --git a/test/Microsoft.DotNet.PackageInstall.Tests/NuGetPackageInstallerExtractTests.cs b/test/Microsoft.DotNet.PackageInstall.Tests/NuGetPackageInstallerExtractTests.cs index 4d76a2484948..7cabcff704e2 100644 --- a/test/Microsoft.DotNet.PackageInstall.Tests/NuGetPackageInstallerExtractTests.cs +++ b/test/Microsoft.DotNet.PackageInstall.Tests/NuGetPackageInstallerExtractTests.cs @@ -1,10 +1,9 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -#nullable disable - using Microsoft.DotNet.Cli.NuGetPackageDownloader; using Microsoft.DotNet.Cli.ToolPackage; +using Microsoft.Extensions.Configuration.DotnetCli; using Microsoft.Extensions.EnvironmentAbstractions; using NuGet.Versioning; @@ -12,8 +11,11 @@ namespace Microsoft.DotNet.PackageInstall.Tests { public class NuGetPackageInstallerExtractTests : SdkTest { + private readonly DotNetCliConfiguration _config; + public NuGetPackageInstallerExtractTests(ITestOutputHelper log) : base(log) { + _config = DotNetConfigurationFactory.CreateMinimal(); } [Fact] @@ -23,8 +25,10 @@ public async Task ItCanExtractNugetPackage() string packageVersion = ToolsetInfo.GetNewtonsoftJsonPackageVersion(); NuGetTestLogger logger = new(Log); NuGetPackageDownloader installer = - new(new DirectoryPath(Directory.GetCurrentDirectory()), null, - new MockFirstPartyNuGetPackageSigningVerifier(), logger, restoreActionConfig: new RestoreActionConfig(NoCache: true)); + new(new DirectoryPath(Directory.GetCurrentDirectory()), + firstPartyNuGetPackageSigningVerifier: new MockFirstPartyNuGetPackageSigningVerifier(), + verboseLogger: logger, + restoreActionConfig: new RestoreActionConfig(NoCache: true)); string packagePath = await installer.DownloadPackageAsync(new PackageId(packageId), new NuGetVersion(packageVersion)); string targetPath = Path.Combine(Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()), @@ -44,8 +48,9 @@ public void ItCanGetAllFilesNeedToSetExecutablePermission() NuGetPackageDownloader installer = new( new DirectoryPath(Directory.GetCurrentDirectory()), - null, - new MockFirstPartyNuGetPackageSigningVerifier(), logger, restoreActionConfig: new RestoreActionConfig(NoCache: true)); + firstPartyNuGetPackageSigningVerifier: new MockFirstPartyNuGetPackageSigningVerifier(), + verboseLogger: logger, + restoreActionConfig: new RestoreActionConfig(NoCache: true)); var allFiles = new List() { "/ExtractedPackage/Microsoft.Android.Sdk.Darwin.nuspec", @@ -67,7 +72,9 @@ public void GivenPackageNotInAllowListItCannotGetAllFilesNeedToSetExecutablePerm NuGetTestLogger logger = new(Log); NuGetPackageDownloader installer = new(new DirectoryPath(Directory.GetCurrentDirectory()), null, - new MockFirstPartyNuGetPackageSigningVerifier(), logger, restoreActionConfig: new RestoreActionConfig(NoCache: true)); + firstPartyNuGetPackageSigningVerifier: new MockFirstPartyNuGetPackageSigningVerifier(), + verboseLogger: logger, + restoreActionConfig: new RestoreActionConfig(NoCache: true)); var allFiles = new List() { "/ExtractedPackage/Not.In.Allow.List.nuspec", diff --git a/test/Microsoft.DotNet.PackageInstall.Tests/NuGetPackageInstallerTests.cs b/test/Microsoft.DotNet.PackageInstall.Tests/NuGetPackageInstallerTests.cs index 8e0693efdad9..19a7bae8c60d 100644 --- a/test/Microsoft.DotNet.PackageInstall.Tests/NuGetPackageInstallerTests.cs +++ b/test/Microsoft.DotNet.PackageInstall.Tests/NuGetPackageInstallerTests.cs @@ -1,8 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -#nullable disable - using System.Collections.ObjectModel; using System.Reflection; using System.Security.Cryptography; @@ -36,11 +34,18 @@ public NuGetPackageInstallerTests(ITestOutputHelper log) : base(log) _tempDirectory = GetUniqueTempProjectPathEachTest(); _logger = new NuGetTestLogger(); _installer = - new NuGetPackageDownloader(_tempDirectory, null, new MockFirstPartyNuGetPackageSigningVerifier(), _logger, - restoreActionConfig: new RestoreActionConfig(NoCache: true), timer: () => ExponentialRetry.Timer(ExponentialRetry.TestingIntervals)); + new NuGetPackageDownloader( + packageInstallDir: _tempDirectory, + firstPartyNuGetPackageSigningVerifier: new MockFirstPartyNuGetPackageSigningVerifier(), + verboseLogger: _logger, + restoreActionConfig: new RestoreActionConfig(NoCache: true)); _toolInstaller = - new NuGetPackageDownloader(_tempDirectory, null, new MockFirstPartyNuGetPackageSigningVerifier(), _logger, - restoreActionConfig: new RestoreActionConfig(NoCache: true), timer: () => ExponentialRetry.Timer(ExponentialRetry.TestingIntervals), shouldUsePackageSourceMapping: true); + new NuGetPackageDownloader( + packageInstallDir: _tempDirectory, + firstPartyNuGetPackageSigningVerifier: new MockFirstPartyNuGetPackageSigningVerifier(), + verboseLogger: _logger, + restoreActionConfig: new RestoreActionConfig(NoCache: true), + shouldUsePackageSourceMapping: true); } [Fact] @@ -76,8 +81,11 @@ await Assert.ThrowsAsync(() => public async Task GivenAFailedSourceAndIgnoreFailedSourcesItShouldNotThrowFatalProtocolException() { var installer = - new NuGetPackageDownloader(_tempDirectory, null, new MockFirstPartyNuGetPackageSigningVerifier(), - _logger, restoreActionConfig: new RestoreActionConfig(IgnoreFailedSources: true, NoCache: true)); + new NuGetPackageDownloader( + packageInstallDir: _tempDirectory, + firstPartyNuGetPackageSigningVerifier: new MockFirstPartyNuGetPackageSigningVerifier(), + verboseLogger: _logger, + restoreActionConfig: new RestoreActionConfig(IgnoreFailedSources: true, NoCache: true)); // should not throw FatalProtocolException // when there is at least one valid source, it should pass. @@ -145,7 +153,7 @@ public async Task GivenAConfigFileRootDirectoryPackageInstallSucceedsViaFindingN [Fact] public async Task GivenNoPackageVersionItCanInstallLatestVersionOfPackage() { - NuGetVersion packageVersion = null; + NuGetVersion? packageVersion = null; string packagePath = await _installer.DownloadPackageAsync( TestPackageId, packageVersion, @@ -232,9 +240,12 @@ public async Task WhenPassedIncludePreviewItInstallSucceeds() public async Task GivenANonSignedSdkItShouldPrintMessageOnce() { BufferedReporter bufferedReporter = new(); - NuGetPackageDownloader nuGetPackageDownloader = new(_tempDirectory, null, - new MockFirstPartyNuGetPackageSigningVerifier(), - _logger, bufferedReporter, restoreActionConfig: new RestoreActionConfig(NoCache: true)); + NuGetPackageDownloader nuGetPackageDownloader = new( + packageInstallDir: _tempDirectory, + firstPartyNuGetPackageSigningVerifier: new MockFirstPartyNuGetPackageSigningVerifier(), + verboseLogger: _logger, + reporter: bufferedReporter, + restoreActionConfig: new RestoreActionConfig(NoCache: true)); await nuGetPackageDownloader.DownloadPackageAsync( TestPackageId, new NuGetVersion(TestPackageVersion), @@ -256,9 +267,13 @@ await nuGetPackageDownloader.DownloadPackageAsync( public async Task GivenANonSignedSdkItShouldNotPrintMessageInQuiet() { BufferedReporter bufferedReporter = new BufferedReporter(); - NuGetPackageDownloader nuGetPackageDownloader = new NuGetPackageDownloader(_tempDirectory, null, - new MockFirstPartyNuGetPackageSigningVerifier(), - _logger, bufferedReporter, restoreActionConfig: new RestoreActionConfig(NoCache: true), verbosityOptions: VerbosityOptions.quiet); + NuGetPackageDownloader nuGetPackageDownloader = new NuGetPackageDownloader( + packageInstallDir: _tempDirectory, + firstPartyNuGetPackageSigningVerifier: new MockFirstPartyNuGetPackageSigningVerifier(), + verboseLogger: _logger, + reporter: bufferedReporter, + restoreActionConfig: new RestoreActionConfig(NoCache: true), + verbosityOptions: VerbosityOptions.quiet); await nuGetPackageDownloader.DownloadPackageAsync( TestPackageId, new NuGetVersion(TestPackageVersion), @@ -281,20 +296,22 @@ public void ItShouldHaveUpdateToDateCertificateSha() var samplePackage = DownloadSamplePackage(new PackageId("Microsoft.iOS.Ref")); var firstPartyNuGetPackageSigningVerifier = new FirstPartyNuGetPackageSigningVerifier(); - string shaFromPackage = GetShaFromSamplePackage(samplePackage); + string? shaFromPackage = GetShaFromSamplePackage(samplePackage); firstPartyNuGetPackageSigningVerifier._firstPartyCertificateThumbprints.Contains(shaFromPackage).Should() .BeTrue( $"Add {shaFromPackage} to the _firstPartyCertificateThumbprints of FirstPartyNuGetPackageSigningVerifier class. More info https://aka.ms/netsdkinternal-certificate-rotate"); } - private string DownloadSamplePackage(PackageId packageId) + private string DownloadSamplePackage(PackageId _) { - NuGetPackageDownloader nuGetPackageDownloader = new(_tempDirectory, null, - new MockFirstPartyNuGetPackageSigningVerifier(), - _logger, restoreActionConfig: new RestoreActionConfig(NoCache: true)); + NuGetPackageDownloader nuGetPackageDownloader = new( + packageInstallDir: _tempDirectory, + firstPartyNuGetPackageSigningVerifier: new MockFirstPartyNuGetPackageSigningVerifier(), + verboseLogger: _logger, + restoreActionConfig: new RestoreActionConfig(NoCache: true)); - return ExponentialRetry.ExecuteWithRetry( + return ExponentialRetry.ExecuteWithRetry( action: DownloadMostRecentSamplePackageFromPublicFeed, shouldStopRetry: result => result != null, maxRetryCount: 3, @@ -309,12 +326,13 @@ string DownloadMostRecentSamplePackageFromPublicFeed() return nuGetPackageDownloader.DownloadPackageAsync( new PackageId("Microsoft.iOS.Ref"), null, includePreview: true, packageSourceLocation: new PackageSourceLocation( - sourceFeedOverrides: new[] { "https://api.nuget.org/v3/index.json" })).GetAwaiter() + sourceFeedOverrides: ["https://api.nuget.org/v3/index.json"])).GetAwaiter() .GetResult(); } catch (Exception) { - return null; + Log.WriteLine("Failed to download sample package from public feed - this is catastrophic for the test, as it relies on the package being present."); + throw; } } } @@ -390,6 +408,6 @@ private static FilePath GenerateRandomNugetConfigFilePath() } private static string GetTestLocalFeedPath() => - Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location), "TestAssetLocalNugetFeed"); + Path.Combine(Path.GetDirectoryName(Assembly.GetExecutingAssembly().Location)!, "TestAssetLocalNugetFeed"); } } diff --git a/test/Microsoft.DotNet.PackageInstall.Tests/UnixFilePermissionsTests.cs b/test/Microsoft.DotNet.PackageInstall.Tests/UnixFilePermissionsTests.cs index 5d71dc5b4808..177d71e7a291 100644 --- a/test/Microsoft.DotNet.PackageInstall.Tests/UnixFilePermissionsTests.cs +++ b/test/Microsoft.DotNet.PackageInstall.Tests/UnixFilePermissionsTests.cs @@ -1,7 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using Microsoft.DotNet.Cli.NugetPackageDownloader; +using Microsoft.DotNet.Cli.NuGetPackageDownloader; namespace Microsoft.DotNet.PackageInstall.Tests { diff --git a/test/Microsoft.DotNet.Tools.Tests.ComponentMocks/ToolPackageDownloaderMock2.cs b/test/Microsoft.DotNet.Tools.Tests.ComponentMocks/ToolPackageDownloaderMock2.cs index 50bdac18df00..a22fa2227d4d 100644 --- a/test/Microsoft.DotNet.Tools.Tests.ComponentMocks/ToolPackageDownloaderMock2.cs +++ b/test/Microsoft.DotNet.Tools.Tests.ComponentMocks/ToolPackageDownloaderMock2.cs @@ -23,7 +23,7 @@ internal class ToolPackageDownloaderMock2 : ToolPackageDownloaderBase List? _packages = null; - public ToolPackageDownloaderMock2(IToolPackageStore store, string runtimeJsonPathForTests, string currentWorkingDirectory, IFileSystem fileSystem) : base(store, runtimeJsonPathForTests, currentWorkingDirectory, fileSystem) + public ToolPackageDownloaderMock2(IToolPackageStore store, string runtimeJsonPathForTests, string? currentWorkingDirectory, IFileSystem fileSystem) : base(store, runtimeJsonPathForTests, currentWorkingDirectory, fileSystem) { } diff --git a/test/dotnet.Tests/CommandTests/Tool/Install/ToolInstallGlobalOrToolPathCommandTests.cs b/test/dotnet.Tests/CommandTests/Tool/Install/ToolInstallGlobalOrToolPathCommandTests.cs index 43184db6b930..3f31984b8a16 100644 --- a/test/dotnet.Tests/CommandTests/Tool/Install/ToolInstallGlobalOrToolPathCommandTests.cs +++ b/test/dotnet.Tests/CommandTests/Tool/Install/ToolInstallGlobalOrToolPathCommandTests.cs @@ -1,8 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -#nullable disable - using System.CommandLine; using System.Text.Json; using Microsoft.DotNet.Cli; @@ -79,8 +77,6 @@ public ToolInstallGlobalOrToolPathCommandTests(ITestOutputHelper log): base(log) fileSystem: _fileSystem); _createToolPackageStoreDownloaderUninstaller = (location, forwardArguments, workingDirectory) => (_toolPackageStore, _toolPackageStoreQuery, _toolPackageDownloader, _toolPackageUninstallerMock); - - _parseResult = Parser.Instance.Parse($"dotnet tool install -g {PackageId}"); } @@ -121,7 +117,8 @@ public void WhenDuplicateSourceIsPassedIgnore() nugetConfig: new FilePath(Path.Combine(testAsset.Path, "NuGet.config")), rootConfigDirectory: new DirectoryPath(testAsset.Path), additionalSourceFeeds: [duplicateSource]); - var nuGetPackageDownloader = new NuGetPackageDownloader(new DirectoryPath(testAsset.Path)); + var nuGetPackageDownloader = new NuGetPackageDownloader( + packageInstallDir: new DirectoryPath(testAsset.Path)); var sources = nuGetPackageDownloader.LoadNuGetSources(new Cli.ToolPackage.PackageId(PackageId), packageSourceLocation); // There should only be one source @@ -148,7 +145,7 @@ public void WhenRunWithPackageIdItShouldCreateValidShim() // It is hard to simulate shell behavior. Only Assert shim can point to executable dll _fileSystem.File.Exists(ExpectedCommandPath()).Should().BeTrue(); var deserializedFakeShim = JsonSerializer.Deserialize( - _fileSystem.File.ReadAllText(ExpectedCommandPath())); + _fileSystem.File.ReadAllText(ExpectedCommandPath()))!; _fileSystem.File.Exists(deserializedFakeShim.ExecutablePath).Should().BeTrue(); } @@ -215,7 +212,7 @@ public void WhenRunWithPackageIdWithSourceItShouldCreateValidShim() .Should().BeTrue(); var deserializedFakeShim = JsonSerializer.Deserialize( - _fileSystem.File.ReadAllText(ExpectedCommandPath())); + _fileSystem.File.ReadAllText(ExpectedCommandPath()))!; _fileSystem.File.Exists(deserializedFakeShim.ExecutablePath).Should().BeTrue(); } @@ -558,7 +555,7 @@ public void WhenInstallWithLowerVersionWithAllowDowngradeOptionItShouldDowngrade public void WhenInstallWithLowerVersionItShouldFail() { AddLowerToolPackageVersionToFeed(); - + ParseResult result = Parser.Instance.Parse($"dotnet tool install -g {PackageId} --version {PackageVersion}"); var toolInstallGlobalOrToolPathCommand = new ToolInstallGlobalOrToolPathCommand( @@ -640,7 +637,10 @@ public void WhenRunWithValidVersionItShouldInterpretAsNuGetExactVersion(string v return (toolPackageStore, toolPackageStore, toolPackageDownloader, toolPackageUninstaller); }, createShellShimRepository: _createShellShimRepository, - nugetPackageDownloader: new NuGetPackageDownloader(new DirectoryPath(PathUtilities.CreateTempSubdirectory()), verifySignatures: false, currentWorkingDirectory: testDir), + nugetPackageDownloader: new NuGetPackageDownloader( + packageInstallDir: new DirectoryPath(PathUtilities.CreateTempSubdirectory()), + verifySignatures: false, + currentWorkingDirectory: testDir), currentWorkingDirectory: testDir, verifySignatures: false); @@ -753,7 +753,7 @@ private void AddLowerToolPackageVersionToFeed() }); } - + private void AddHigherToolPackageVersionToFeed() { _toolPackageDownloader.AddMockPackage(new MockFeedPackage From 81a779d80426439f172b949ff221bb964c5438e8 Mon Sep 17 00:00:00 2001 From: Chet Husk Date: Sun, 13 Jul 2025 16:40:54 -0500 Subject: [PATCH 09/12] prevent blowups on startup due to boolean parsing in generated config --- eng/common/tools.ps1 | 4 ++-- eng/common/tools.sh | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/eng/common/tools.ps1 b/eng/common/tools.ps1 index 40f0aa86128e..a023ab9fa097 100644 --- a/eng/common/tools.ps1 +++ b/eng/common/tools.ps1 @@ -165,7 +165,7 @@ function InitializeDotNetCli([bool]$install, [bool]$createSdkLocationFile) { # Disable telemetry on CI. if ($ci) { - $env:DOTNET_CLI_TELEMETRY_OPTOUT=1 + $env:DOTNET_CLI_TELEMETRY_OPTOUT=$true } # Find the first path on %PATH% that contains the dotnet.exe @@ -293,7 +293,7 @@ function InstallDotNet([string] $dotnetRoot, if ($runtime -eq "aspnetcore") { $runtimePath = $runtimePath + "\Microsoft.AspNetCore.App" } if ($runtime -eq "windowsdesktop") { $runtimePath = $runtimePath + "\Microsoft.WindowsDesktop.App" } $runtimePath = $runtimePath + "\" + $version - + $dotnetVersionLabel = "runtime toolset '$runtime/$architecture v$version'" if (Test-Path $runtimePath) { diff --git a/eng/common/tools.sh b/eng/common/tools.sh index 3def02a638d2..dc16d2adf5d6 100755 --- a/eng/common/tools.sh +++ b/eng/common/tools.sh @@ -123,7 +123,7 @@ function InitializeDotNetCli { # Disable telemetry on CI if [[ $ci == true ]]; then - export DOTNET_CLI_TELEMETRY_OPTOUT=1 + export DOTNET_CLI_TELEMETRY_OPTOUT=true fi # LTTNG is the logging infrastructure used by Core CLR. Need this variable set From e017a4d59d60daa6baf26110676e78fe43f3fb6a Mon Sep 17 00:00:00 2001 From: Chet Husk Date: Sun, 13 Jul 2025 17:10:44 -0500 Subject: [PATCH 10/12] custom value object for bool to make binding maybe work? --- .../Models/BuildConfiguration.cs | 8 +-- .../Models/CliUserExperienceConfiguration.cs | 6 +- .../Models/DevelopmentConfiguration.cs | 6 +- .../Models/FirstTimeUseConfiguration.cs | 6 +- .../Models/FlexibleBool.cs | 61 +++++++++++++++++++ .../Models/NuGetConfiguration.cs | 2 +- .../Models/RuntimeHostConfiguration.cs | 2 +- .../Models/SdkResolverConfiguration.cs | 2 +- .../Models/ToolConfiguration.cs | 2 +- .../Models/WorkloadConfiguration.cs | 8 +-- 10 files changed, 82 insertions(+), 21 deletions(-) create mode 100644 src/Microsoft.Extensions.Configuration.DotnetCli/Models/FlexibleBool.cs diff --git a/src/Microsoft.Extensions.Configuration.DotnetCli/Models/BuildConfiguration.cs b/src/Microsoft.Extensions.Configuration.DotnetCli/Models/BuildConfiguration.cs index 32761bb5b8ac..9047eeaf17e0 100644 --- a/src/Microsoft.Extensions.Configuration.DotnetCli/Models/BuildConfiguration.cs +++ b/src/Microsoft.Extensions.Configuration.DotnetCli/Models/BuildConfiguration.cs @@ -12,13 +12,13 @@ public sealed class BuildConfiguration /// Gets or sets whether to run MSBuild out of process. /// Mapped from DOTNET_CLI_RUN_MSBUILD_OUTOFPROC environment variable. /// - public bool RunMSBuildOutOfProc { get; set; } = false; + public FlexibleBool RunMSBuildOutOfProc { get; set; } = false; /// /// Gets or sets whether to use the MSBuild server for builds. /// Mapped from DOTNET_CLI_USE_MSBUILD_SERVER environment variable. /// - public bool UseMSBuildServer { get; set; } = false; + public FlexibleBool UseMSBuildServer { get; set; } = false; /// /// Gets or sets the configuration for the MSBuild terminal logger. @@ -30,11 +30,11 @@ public sealed class BuildConfiguration /// Gets or sets whether to disable publish and pack release configuration. /// Mapped from DOTNET_CLI_DISABLE_PUBLISH_AND_PACK_RELEASE environment variable. /// - public bool DisablePublishAndPackRelease { get; set; } = false; + public FlexibleBool DisablePublishAndPackRelease { get; set; } = false; /// /// Gets or sets whether to enable lazy publish and pack release for solutions. /// Mapped from DOTNET_CLI_LAZY_PUBLISH_AND_PACK_RELEASE_FOR_SOLUTIONS environment variable. /// - public bool LazyPublishAndPackReleaseForSolutions { get; set; } = false; + public FlexibleBool LazyPublishAndPackReleaseForSolutions { get; set; } = false; } diff --git a/src/Microsoft.Extensions.Configuration.DotnetCli/Models/CliUserExperienceConfiguration.cs b/src/Microsoft.Extensions.Configuration.DotnetCli/Models/CliUserExperienceConfiguration.cs index 46618744d900..6408dff931ac 100644 --- a/src/Microsoft.Extensions.Configuration.DotnetCli/Models/CliUserExperienceConfiguration.cs +++ b/src/Microsoft.Extensions.Configuration.DotnetCli/Models/CliUserExperienceConfiguration.cs @@ -14,19 +14,19 @@ public sealed class CliUserExperienceConfiguration /// Gets or sets whether telemetry collection is disabled. /// Mapped from DOTNET_CLI_TELEMETRY_OPTOUT environment variable. /// - public bool TelemetryOptOut { get; set; } = CompileOptions.TelemetryOptOutDefault; + public FlexibleBool TelemetryOptOut { get; set; } = CompileOptions.TelemetryOptOutDefault; /// /// Gets or sets whether to suppress the .NET logo on startup. /// Mapped from DOTNET_NOLOGO environment variable. /// - public bool NoLogo { get; set; } = false; + public FlexibleBool NoLogo { get; set; } = false; /// /// Gets or sets whether to force UTF-8 encoding for console output. /// Mapped from DOTNET_CLI_FORCE_UTF8_ENCODING environment variable. /// - public bool ForceUtf8Encoding { get; set; } = false; + public FlexibleBool ForceUtf8Encoding { get; set; } = false; /// /// Gets or sets the UI language for the CLI. diff --git a/src/Microsoft.Extensions.Configuration.DotnetCli/Models/DevelopmentConfiguration.cs b/src/Microsoft.Extensions.Configuration.DotnetCli/Models/DevelopmentConfiguration.cs index ab442308c9d3..48d14d8e297e 100644 --- a/src/Microsoft.Extensions.Configuration.DotnetCli/Models/DevelopmentConfiguration.cs +++ b/src/Microsoft.Extensions.Configuration.DotnetCli/Models/DevelopmentConfiguration.cs @@ -12,7 +12,7 @@ public sealed class DevelopmentConfiguration /// Gets or sets whether performance logging is enabled. /// Mapped from DOTNET_CLI_PERF_LOG environment variable. /// - public bool PerfLogEnabled { get; set; } = false; + public FlexibleBool PerfLogEnabled { get; set; } = false; /// /// Gets or sets the number of performance log entries to collect. @@ -30,11 +30,11 @@ public sealed class DevelopmentConfiguration /// Gets or sets whether to enable verbose context logging. /// Mapped from DOTNET_CLI_CONTEXT_VERBOSE environment variable. /// - public bool ContextVerbose { get; set; } = false; + public FlexibleBool ContextVerbose { get; set; } = false; /// /// Gets or sets whether to allow targeting pack caching. /// Mapped from DOTNETSDK_ALLOW_TARGETING_PACK_CACHING environment variable. /// - public bool AllowTargetingPackCaching { get; set; } = false; + public FlexibleBool AllowTargetingPackCaching { get; set; } = false; } diff --git a/src/Microsoft.Extensions.Configuration.DotnetCli/Models/FirstTimeUseConfiguration.cs b/src/Microsoft.Extensions.Configuration.DotnetCli/Models/FirstTimeUseConfiguration.cs index 54d813f3f202..9673c3d6aa1f 100644 --- a/src/Microsoft.Extensions.Configuration.DotnetCli/Models/FirstTimeUseConfiguration.cs +++ b/src/Microsoft.Extensions.Configuration.DotnetCli/Models/FirstTimeUseConfiguration.cs @@ -12,17 +12,17 @@ public sealed class FirstTimeUseConfiguration /// Gets or sets whether to generate ASP.NET Core HTTPS development certificates. /// Mapped from DOTNET_GENERATE_ASPNET_CERTIFICATE environment variable. /// - public bool GenerateAspNetCertificate { get; set; } = true; + public FlexibleBool GenerateAspNetCertificate { get; set; } = true; /// /// Gets or sets whether to add global tools to the PATH. /// Mapped from DOTNET_ADD_GLOBAL_TOOLS_TO_PATH environment variable. /// - public bool AddGlobalToolsToPath { get; set; } = true; + public FlexibleBool AddGlobalToolsToPath { get; set; } = true; /// /// Gets or sets whether to skip the first-time experience setup. /// Mapped from DOTNET_SKIP_FIRST_TIME_EXPERIENCE environment variable. /// - public bool SkipFirstTimeExperience { get; set; } = false; + public FlexibleBool SkipFirstTimeExperience { get; set; } = false; } diff --git a/src/Microsoft.Extensions.Configuration.DotnetCli/Models/FlexibleBool.cs b/src/Microsoft.Extensions.Configuration.DotnetCli/Models/FlexibleBool.cs new file mode 100644 index 000000000000..2da3b23e1471 --- /dev/null +++ b/src/Microsoft.Extensions.Configuration.DotnetCli/Models/FlexibleBool.cs @@ -0,0 +1,61 @@ +using System.Diagnostics.CodeAnalysis; + +namespace Microsoft.Extensions.Configuration.DotnetCli; + +public struct FlexibleBool +{ + private string _originalValue; + private readonly bool _value; + + public FlexibleBool(bool value) + { + _originalValue = value.ToString(); + _value = value; + } + public FlexibleBool(int value) + { + _originalValue = value.ToString(); + _value = value != 0; + } + + public FlexibleBool(string value) + { + _originalValue = value; + if (bool.TryParse(value, out var result)) + { + _value = result; + } + else if (string.Equals(value, "yes", StringComparison.OrdinalIgnoreCase)) + { + _value = true; + } + else if (string.Equals(value, "no", StringComparison.OrdinalIgnoreCase)) + { + _value = false; + } + else if (int.TryParse(value, out var intValue)) + { + _value = intValue != 0; // Treat non-zero as true, zero as false + } + else + { + throw new ArgumentException($"Invalid boolean value: {value}"); + } + } + + public override string ToString() + { + return _originalValue.ToString(); + } + + public static implicit operator bool(FlexibleBool flexibleBool) => flexibleBool._value; + public static implicit operator FlexibleBool(bool value) => new(value); + public static implicit operator FlexibleBool(int value) => new(value); + public static implicit operator FlexibleBool(string value) => new(value); + public static bool operator true(FlexibleBool myBoolString) => (bool)myBoolString; + public static bool operator false(FlexibleBool myBoolString) => !(bool)myBoolString; + + public override bool Equals([NotNullWhen(true)] object? obj) => base.Equals(obj) && obj is FlexibleBool other && _value == other._value; + public override int GetHashCode() => _originalValue.GetHashCode(); + +} diff --git a/src/Microsoft.Extensions.Configuration.DotnetCli/Models/NuGetConfiguration.cs b/src/Microsoft.Extensions.Configuration.DotnetCli/Models/NuGetConfiguration.cs index d49db7afbfa3..abe73e8788b2 100644 --- a/src/Microsoft.Extensions.Configuration.DotnetCli/Models/NuGetConfiguration.cs +++ b/src/Microsoft.Extensions.Configuration.DotnetCli/Models/NuGetConfiguration.cs @@ -13,5 +13,5 @@ public sealed class NuGetConfiguration /// Mapped from DOTNET_NUGET_SIGNATURE_VERIFICATION environment variable. /// Defaults to true on Windows and Linux, false elsewhere. /// - public bool SignatureVerificationEnabled { get; set; } = OperatingSystem.IsWindows() || OperatingSystem.IsLinux(); + public FlexibleBool SignatureVerificationEnabled { get; set; } = OperatingSystem.IsWindows() || OperatingSystem.IsLinux(); } diff --git a/src/Microsoft.Extensions.Configuration.DotnetCli/Models/RuntimeHostConfiguration.cs b/src/Microsoft.Extensions.Configuration.DotnetCli/Models/RuntimeHostConfiguration.cs index 5b2f5dc41058..f81e691c22c6 100644 --- a/src/Microsoft.Extensions.Configuration.DotnetCli/Models/RuntimeHostConfiguration.cs +++ b/src/Microsoft.Extensions.Configuration.DotnetCli/Models/RuntimeHostConfiguration.cs @@ -18,7 +18,7 @@ public sealed class RuntimeHostConfiguration /// Gets or sets whether to enable multilevel lookup for shared frameworks. /// Mapped from DOTNET_MULTILEVEL_LOOKUP environment variable. /// - public bool MultilevelLookup { get; set; } = true; + public FlexibleBool MultilevelLookup { get; set; } = false; /// /// Gets or sets the roll-forward policy for framework version selection. diff --git a/src/Microsoft.Extensions.Configuration.DotnetCli/Models/SdkResolverConfiguration.cs b/src/Microsoft.Extensions.Configuration.DotnetCli/Models/SdkResolverConfiguration.cs index 2769c5038607..c4864b1cc4bd 100644 --- a/src/Microsoft.Extensions.Configuration.DotnetCli/Models/SdkResolverConfiguration.cs +++ b/src/Microsoft.Extensions.Configuration.DotnetCli/Models/SdkResolverConfiguration.cs @@ -12,7 +12,7 @@ public sealed class SdkResolverConfiguration /// Gets or sets whether to enable SDK resolver logging. /// Mapped from DOTNET_MSBUILD_SDK_RESOLVER_ENABLE_LOG environment variable. /// - public bool EnableLog { get; set; } = false; + public FlexibleBool EnableLog { get; set; } = false; /// /// Gets or sets the directory containing SDKs. diff --git a/src/Microsoft.Extensions.Configuration.DotnetCli/Models/ToolConfiguration.cs b/src/Microsoft.Extensions.Configuration.DotnetCli/Models/ToolConfiguration.cs index 89c4fd4d6fc3..a390b8d5d556 100644 --- a/src/Microsoft.Extensions.Configuration.DotnetCli/Models/ToolConfiguration.cs +++ b/src/Microsoft.Extensions.Configuration.DotnetCli/Models/ToolConfiguration.cs @@ -12,5 +12,5 @@ public sealed class ToolConfiguration /// Gets or sets whether to allow tool manifests in the repository root. /// Mapped from DOTNET_TOOLS_ALLOW_MANIFEST_IN_ROOT environment variable. /// - public bool AllowManifestInRoot { get; set; } = false; + public FlexibleBool AllowManifestInRoot { get; set; } = false; } diff --git a/src/Microsoft.Extensions.Configuration.DotnetCli/Models/WorkloadConfiguration.cs b/src/Microsoft.Extensions.Configuration.DotnetCli/Models/WorkloadConfiguration.cs index 85e988fc1406..f7bc90bd3efb 100644 --- a/src/Microsoft.Extensions.Configuration.DotnetCli/Models/WorkloadConfiguration.cs +++ b/src/Microsoft.Extensions.Configuration.DotnetCli/Models/WorkloadConfiguration.cs @@ -12,7 +12,7 @@ public sealed class WorkloadConfiguration /// Gets or sets whether to disable workload update notifications. /// Mapped from DOTNET_CLI_WORKLOAD_UPDATE_NOTIFY_DISABLE environment variable. /// - public bool UpdateNotifyDisable { get; set; } = false; + public FlexibleBool UpdateNotifyDisable { get; set; } = false; /// /// Gets or sets the interval in hours between workload update notifications. @@ -24,13 +24,13 @@ public sealed class WorkloadConfiguration /// Gets or sets whether to disable workload pack groups. /// Mapped from DOTNET_CLI_WORKLOAD_DISABLE_PACK_GROUPS environment variable. /// - public bool DisablePackGroups { get; set; } = false; + public FlexibleBool DisablePackGroups { get; set; } = false; /// /// Gets or sets whether to skip workload integrity checks. /// Mapped from DOTNET_SKIP_WORKLOAD_INTEGRITY_CHECK environment variable. /// - public bool SkipIntegrityCheck { get; set; } = false; + public FlexibleBool SkipIntegrityCheck { get; set; } = false; /// /// Gets or sets the manifest root directories for workloads. @@ -48,5 +48,5 @@ public sealed class WorkloadConfiguration /// Gets or sets whether to ignore default manifest roots. /// Mapped from DOTNETSDK_WORKLOAD_MANIFEST_IGNORE_DEFAULT_ROOTS environment variable. /// - public bool ManifestIgnoreDefaultRoots { get; set; } = false; + public FlexibleBool ManifestIgnoreDefaultRoots { get; set; } = false; } From c2f37d4c58e7ddd14ac4c0a34794695eb71ee4ed Mon Sep 17 00:00:00 2001 From: Chet Husk Date: Sun, 13 Jul 2025 21:57:09 -0500 Subject: [PATCH 11/12] Back to normal booleans on config models, pushing knowledge into the config providers --- .../Models/BuildConfiguration.cs | 8 +-- .../Models/CliUserExperienceConfiguration.cs | 6 +- .../Models/DevelopmentConfiguration.cs | 6 +- .../Models/FirstTimeUseConfiguration.cs | 6 +- .../Models/FlexibleBool.cs | 61 ------------------- .../Models/NuGetConfiguration.cs | 2 +- .../Models/RuntimeHostConfiguration.cs | 2 +- .../Models/SdkResolverConfiguration.cs | 2 +- .../Models/ToolConfiguration.cs | 2 +- .../Models/WorkloadConfiguration.cs | 8 +-- .../DotNetEnvironmentConfigurationProvider.cs | 40 +++++++++++- 11 files changed, 60 insertions(+), 83 deletions(-) delete mode 100644 src/Microsoft.Extensions.Configuration.DotnetCli/Models/FlexibleBool.cs diff --git a/src/Microsoft.Extensions.Configuration.DotnetCli/Models/BuildConfiguration.cs b/src/Microsoft.Extensions.Configuration.DotnetCli/Models/BuildConfiguration.cs index 9047eeaf17e0..32761bb5b8ac 100644 --- a/src/Microsoft.Extensions.Configuration.DotnetCli/Models/BuildConfiguration.cs +++ b/src/Microsoft.Extensions.Configuration.DotnetCli/Models/BuildConfiguration.cs @@ -12,13 +12,13 @@ public sealed class BuildConfiguration /// Gets or sets whether to run MSBuild out of process. /// Mapped from DOTNET_CLI_RUN_MSBUILD_OUTOFPROC environment variable. /// - public FlexibleBool RunMSBuildOutOfProc { get; set; } = false; + public bool RunMSBuildOutOfProc { get; set; } = false; /// /// Gets or sets whether to use the MSBuild server for builds. /// Mapped from DOTNET_CLI_USE_MSBUILD_SERVER environment variable. /// - public FlexibleBool UseMSBuildServer { get; set; } = false; + public bool UseMSBuildServer { get; set; } = false; /// /// Gets or sets the configuration for the MSBuild terminal logger. @@ -30,11 +30,11 @@ public sealed class BuildConfiguration /// Gets or sets whether to disable publish and pack release configuration. /// Mapped from DOTNET_CLI_DISABLE_PUBLISH_AND_PACK_RELEASE environment variable. /// - public FlexibleBool DisablePublishAndPackRelease { get; set; } = false; + public bool DisablePublishAndPackRelease { get; set; } = false; /// /// Gets or sets whether to enable lazy publish and pack release for solutions. /// Mapped from DOTNET_CLI_LAZY_PUBLISH_AND_PACK_RELEASE_FOR_SOLUTIONS environment variable. /// - public FlexibleBool LazyPublishAndPackReleaseForSolutions { get; set; } = false; + public bool LazyPublishAndPackReleaseForSolutions { get; set; } = false; } diff --git a/src/Microsoft.Extensions.Configuration.DotnetCli/Models/CliUserExperienceConfiguration.cs b/src/Microsoft.Extensions.Configuration.DotnetCli/Models/CliUserExperienceConfiguration.cs index 6408dff931ac..46618744d900 100644 --- a/src/Microsoft.Extensions.Configuration.DotnetCli/Models/CliUserExperienceConfiguration.cs +++ b/src/Microsoft.Extensions.Configuration.DotnetCli/Models/CliUserExperienceConfiguration.cs @@ -14,19 +14,19 @@ public sealed class CliUserExperienceConfiguration /// Gets or sets whether telemetry collection is disabled. /// Mapped from DOTNET_CLI_TELEMETRY_OPTOUT environment variable. /// - public FlexibleBool TelemetryOptOut { get; set; } = CompileOptions.TelemetryOptOutDefault; + public bool TelemetryOptOut { get; set; } = CompileOptions.TelemetryOptOutDefault; /// /// Gets or sets whether to suppress the .NET logo on startup. /// Mapped from DOTNET_NOLOGO environment variable. /// - public FlexibleBool NoLogo { get; set; } = false; + public bool NoLogo { get; set; } = false; /// /// Gets or sets whether to force UTF-8 encoding for console output. /// Mapped from DOTNET_CLI_FORCE_UTF8_ENCODING environment variable. /// - public FlexibleBool ForceUtf8Encoding { get; set; } = false; + public bool ForceUtf8Encoding { get; set; } = false; /// /// Gets or sets the UI language for the CLI. diff --git a/src/Microsoft.Extensions.Configuration.DotnetCli/Models/DevelopmentConfiguration.cs b/src/Microsoft.Extensions.Configuration.DotnetCli/Models/DevelopmentConfiguration.cs index 48d14d8e297e..ab442308c9d3 100644 --- a/src/Microsoft.Extensions.Configuration.DotnetCli/Models/DevelopmentConfiguration.cs +++ b/src/Microsoft.Extensions.Configuration.DotnetCli/Models/DevelopmentConfiguration.cs @@ -12,7 +12,7 @@ public sealed class DevelopmentConfiguration /// Gets or sets whether performance logging is enabled. /// Mapped from DOTNET_CLI_PERF_LOG environment variable. /// - public FlexibleBool PerfLogEnabled { get; set; } = false; + public bool PerfLogEnabled { get; set; } = false; /// /// Gets or sets the number of performance log entries to collect. @@ -30,11 +30,11 @@ public sealed class DevelopmentConfiguration /// Gets or sets whether to enable verbose context logging. /// Mapped from DOTNET_CLI_CONTEXT_VERBOSE environment variable. /// - public FlexibleBool ContextVerbose { get; set; } = false; + public bool ContextVerbose { get; set; } = false; /// /// Gets or sets whether to allow targeting pack caching. /// Mapped from DOTNETSDK_ALLOW_TARGETING_PACK_CACHING environment variable. /// - public FlexibleBool AllowTargetingPackCaching { get; set; } = false; + public bool AllowTargetingPackCaching { get; set; } = false; } diff --git a/src/Microsoft.Extensions.Configuration.DotnetCli/Models/FirstTimeUseConfiguration.cs b/src/Microsoft.Extensions.Configuration.DotnetCli/Models/FirstTimeUseConfiguration.cs index 9673c3d6aa1f..54d813f3f202 100644 --- a/src/Microsoft.Extensions.Configuration.DotnetCli/Models/FirstTimeUseConfiguration.cs +++ b/src/Microsoft.Extensions.Configuration.DotnetCli/Models/FirstTimeUseConfiguration.cs @@ -12,17 +12,17 @@ public sealed class FirstTimeUseConfiguration /// Gets or sets whether to generate ASP.NET Core HTTPS development certificates. /// Mapped from DOTNET_GENERATE_ASPNET_CERTIFICATE environment variable. /// - public FlexibleBool GenerateAspNetCertificate { get; set; } = true; + public bool GenerateAspNetCertificate { get; set; } = true; /// /// Gets or sets whether to add global tools to the PATH. /// Mapped from DOTNET_ADD_GLOBAL_TOOLS_TO_PATH environment variable. /// - public FlexibleBool AddGlobalToolsToPath { get; set; } = true; + public bool AddGlobalToolsToPath { get; set; } = true; /// /// Gets or sets whether to skip the first-time experience setup. /// Mapped from DOTNET_SKIP_FIRST_TIME_EXPERIENCE environment variable. /// - public FlexibleBool SkipFirstTimeExperience { get; set; } = false; + public bool SkipFirstTimeExperience { get; set; } = false; } diff --git a/src/Microsoft.Extensions.Configuration.DotnetCli/Models/FlexibleBool.cs b/src/Microsoft.Extensions.Configuration.DotnetCli/Models/FlexibleBool.cs deleted file mode 100644 index 2da3b23e1471..000000000000 --- a/src/Microsoft.Extensions.Configuration.DotnetCli/Models/FlexibleBool.cs +++ /dev/null @@ -1,61 +0,0 @@ -using System.Diagnostics.CodeAnalysis; - -namespace Microsoft.Extensions.Configuration.DotnetCli; - -public struct FlexibleBool -{ - private string _originalValue; - private readonly bool _value; - - public FlexibleBool(bool value) - { - _originalValue = value.ToString(); - _value = value; - } - public FlexibleBool(int value) - { - _originalValue = value.ToString(); - _value = value != 0; - } - - public FlexibleBool(string value) - { - _originalValue = value; - if (bool.TryParse(value, out var result)) - { - _value = result; - } - else if (string.Equals(value, "yes", StringComparison.OrdinalIgnoreCase)) - { - _value = true; - } - else if (string.Equals(value, "no", StringComparison.OrdinalIgnoreCase)) - { - _value = false; - } - else if (int.TryParse(value, out var intValue)) - { - _value = intValue != 0; // Treat non-zero as true, zero as false - } - else - { - throw new ArgumentException($"Invalid boolean value: {value}"); - } - } - - public override string ToString() - { - return _originalValue.ToString(); - } - - public static implicit operator bool(FlexibleBool flexibleBool) => flexibleBool._value; - public static implicit operator FlexibleBool(bool value) => new(value); - public static implicit operator FlexibleBool(int value) => new(value); - public static implicit operator FlexibleBool(string value) => new(value); - public static bool operator true(FlexibleBool myBoolString) => (bool)myBoolString; - public static bool operator false(FlexibleBool myBoolString) => !(bool)myBoolString; - - public override bool Equals([NotNullWhen(true)] object? obj) => base.Equals(obj) && obj is FlexibleBool other && _value == other._value; - public override int GetHashCode() => _originalValue.GetHashCode(); - -} diff --git a/src/Microsoft.Extensions.Configuration.DotnetCli/Models/NuGetConfiguration.cs b/src/Microsoft.Extensions.Configuration.DotnetCli/Models/NuGetConfiguration.cs index abe73e8788b2..d49db7afbfa3 100644 --- a/src/Microsoft.Extensions.Configuration.DotnetCli/Models/NuGetConfiguration.cs +++ b/src/Microsoft.Extensions.Configuration.DotnetCli/Models/NuGetConfiguration.cs @@ -13,5 +13,5 @@ public sealed class NuGetConfiguration /// Mapped from DOTNET_NUGET_SIGNATURE_VERIFICATION environment variable. /// Defaults to true on Windows and Linux, false elsewhere. /// - public FlexibleBool SignatureVerificationEnabled { get; set; } = OperatingSystem.IsWindows() || OperatingSystem.IsLinux(); + public bool SignatureVerificationEnabled { get; set; } = OperatingSystem.IsWindows() || OperatingSystem.IsLinux(); } diff --git a/src/Microsoft.Extensions.Configuration.DotnetCli/Models/RuntimeHostConfiguration.cs b/src/Microsoft.Extensions.Configuration.DotnetCli/Models/RuntimeHostConfiguration.cs index f81e691c22c6..546f63e5f987 100644 --- a/src/Microsoft.Extensions.Configuration.DotnetCli/Models/RuntimeHostConfiguration.cs +++ b/src/Microsoft.Extensions.Configuration.DotnetCli/Models/RuntimeHostConfiguration.cs @@ -18,7 +18,7 @@ public sealed class RuntimeHostConfiguration /// Gets or sets whether to enable multilevel lookup for shared frameworks. /// Mapped from DOTNET_MULTILEVEL_LOOKUP environment variable. /// - public FlexibleBool MultilevelLookup { get; set; } = false; + public bool MultilevelLookup { get; set; } = false; /// /// Gets or sets the roll-forward policy for framework version selection. diff --git a/src/Microsoft.Extensions.Configuration.DotnetCli/Models/SdkResolverConfiguration.cs b/src/Microsoft.Extensions.Configuration.DotnetCli/Models/SdkResolverConfiguration.cs index c4864b1cc4bd..2769c5038607 100644 --- a/src/Microsoft.Extensions.Configuration.DotnetCli/Models/SdkResolverConfiguration.cs +++ b/src/Microsoft.Extensions.Configuration.DotnetCli/Models/SdkResolverConfiguration.cs @@ -12,7 +12,7 @@ public sealed class SdkResolverConfiguration /// Gets or sets whether to enable SDK resolver logging. /// Mapped from DOTNET_MSBUILD_SDK_RESOLVER_ENABLE_LOG environment variable. /// - public FlexibleBool EnableLog { get; set; } = false; + public bool EnableLog { get; set; } = false; /// /// Gets or sets the directory containing SDKs. diff --git a/src/Microsoft.Extensions.Configuration.DotnetCli/Models/ToolConfiguration.cs b/src/Microsoft.Extensions.Configuration.DotnetCli/Models/ToolConfiguration.cs index a390b8d5d556..89c4fd4d6fc3 100644 --- a/src/Microsoft.Extensions.Configuration.DotnetCli/Models/ToolConfiguration.cs +++ b/src/Microsoft.Extensions.Configuration.DotnetCli/Models/ToolConfiguration.cs @@ -12,5 +12,5 @@ public sealed class ToolConfiguration /// Gets or sets whether to allow tool manifests in the repository root. /// Mapped from DOTNET_TOOLS_ALLOW_MANIFEST_IN_ROOT environment variable. /// - public FlexibleBool AllowManifestInRoot { get; set; } = false; + public bool AllowManifestInRoot { get; set; } = false; } diff --git a/src/Microsoft.Extensions.Configuration.DotnetCli/Models/WorkloadConfiguration.cs b/src/Microsoft.Extensions.Configuration.DotnetCli/Models/WorkloadConfiguration.cs index f7bc90bd3efb..85e988fc1406 100644 --- a/src/Microsoft.Extensions.Configuration.DotnetCli/Models/WorkloadConfiguration.cs +++ b/src/Microsoft.Extensions.Configuration.DotnetCli/Models/WorkloadConfiguration.cs @@ -12,7 +12,7 @@ public sealed class WorkloadConfiguration /// Gets or sets whether to disable workload update notifications. /// Mapped from DOTNET_CLI_WORKLOAD_UPDATE_NOTIFY_DISABLE environment variable. /// - public FlexibleBool UpdateNotifyDisable { get; set; } = false; + public bool UpdateNotifyDisable { get; set; } = false; /// /// Gets or sets the interval in hours between workload update notifications. @@ -24,13 +24,13 @@ public sealed class WorkloadConfiguration /// Gets or sets whether to disable workload pack groups. /// Mapped from DOTNET_CLI_WORKLOAD_DISABLE_PACK_GROUPS environment variable. /// - public FlexibleBool DisablePackGroups { get; set; } = false; + public bool DisablePackGroups { get; set; } = false; /// /// Gets or sets whether to skip workload integrity checks. /// Mapped from DOTNET_SKIP_WORKLOAD_INTEGRITY_CHECK environment variable. /// - public FlexibleBool SkipIntegrityCheck { get; set; } = false; + public bool SkipIntegrityCheck { get; set; } = false; /// /// Gets or sets the manifest root directories for workloads. @@ -48,5 +48,5 @@ public sealed class WorkloadConfiguration /// Gets or sets whether to ignore default manifest roots. /// Mapped from DOTNETSDK_WORKLOAD_MANIFEST_IGNORE_DEFAULT_ROOTS environment variable. /// - public FlexibleBool ManifestIgnoreDefaultRoots { get; set; } = false; + public bool ManifestIgnoreDefaultRoots { get; set; } = false; } diff --git a/src/Microsoft.Extensions.Configuration.DotnetCli/Providers/DotNetEnvironmentConfigurationProvider.cs b/src/Microsoft.Extensions.Configuration.DotnetCli/Providers/DotNetEnvironmentConfigurationProvider.cs index 037c75ae4a35..d21978065cfd 100644 --- a/src/Microsoft.Extensions.Configuration.DotnetCli/Providers/DotNetEnvironmentConfigurationProvider.cs +++ b/src/Microsoft.Extensions.Configuration.DotnetCli/Providers/DotNetEnvironmentConfigurationProvider.cs @@ -48,6 +48,29 @@ public class DotNetEnvironmentConfigurationProvider : ConfigurationProvider ["DOTNET_TOOLS_ALLOW_MANIFEST_IN_ROOT"] = "Tool:AllowManifestInRoot", ["DOTNET_NUGET_SIGNATURE_VERIFICATION"] = "NuGet:SignatureVerificationEnabled", }; + private static readonly HashSet ConfigKeysToTreatAsBooleans = [ + "CliUserExperience:TelemetryOptOut", + "CliUserExperience:NoLogo", + "CliUserExperience:ForceUtf8Encoding", + "Development:PerfLogEnabled", + "Development:ContextVerbose", + "Development:AllowTargetingPackCaching", + "RuntimeHost:MultilevelLookup", + "Build:RunMSBuildOutOfProc", + "Build:UseMSBuildServer", + "Build:DisablePublishAndPackRelease", + "Build:LazyPublishAndPackReleaseForSolutions", + "SdkResolver:EnableLog", + "Workload:UpdateNotifyDisable", + "Workload:DisablePackGroups", + "Workload:SkipIntegrityCheck", + "Workload:ManifestIgnoreDefaultRoots", + "FirstTimeUse:GenerateAspNetCertificate", + "FirstTimeUse:AddGlobalToolsToPath", + "FirstTimeUse:SkipFirstTimeExperience", + "Tool:AllowManifestInRoot", + "NuGet:SignatureVerificationEnabled" + ]; public override void Load() { @@ -58,7 +81,7 @@ public override void Load() var value = Environment.GetEnvironmentVariable(mapping.Key); if (!string.IsNullOrEmpty(value)) { - Data[mapping.Value] = value; + Data[mapping.Value] = CoerceValueIfNecessary(mapping.Value, value); } } @@ -67,6 +90,21 @@ public override void Load() HandleArrayEnvironmentVariable("DOTNETSDK_WORKLOAD_PACK_ROOTS", "Workload:PackRoots"); } + private string CoerceValueIfNecessary(string configKey, string envValue) => ConfigKeysToTreatAsBooleans.Contains(configKey) + ? GetFlexibleBool(envValue).ToString() + : envValue; + + private bool GetFlexibleBool(string envValue) => + bool.TryParse(envValue, out var boolValue) + ? boolValue + : int.TryParse(envValue, out var intValue) ? intValue > 0 + : envValue switch + { + "yes" or "y" => true, + "no" or "n" => false, + _ => throw new FormatException($"Invalid boolean value: {envValue}") + }; + private void HandleArrayEnvironmentVariable(string envVar, string configKey) { var value = Environment.GetEnvironmentVariable(envVar); From 595351e44e9f1bae6f229c9305377c43c850ac95 Mon Sep 17 00:00:00 2001 From: Chet Husk Date: Tue, 15 Jul 2025 21:28:27 -0500 Subject: [PATCH 12/12] fix config provider file system lookups so that we're actually using data from them --- .../DotNetConfigurationFactory.cs | 1 + .../Providers/DotNetConfigurationSource.cs | 20 +---- .../GlobalJsonConfigurationProvider.cs | 81 +++---------------- .../GlobalJsonConfigurationSource.cs | 14 ++-- 4 files changed, 25 insertions(+), 91 deletions(-) diff --git a/src/Microsoft.Extensions.Configuration.DotnetCli/DotNetConfigurationFactory.cs b/src/Microsoft.Extensions.Configuration.DotnetCli/DotNetConfigurationFactory.cs index 803bd3e693aa..eb42e9a99bcf 100644 --- a/src/Microsoft.Extensions.Configuration.DotnetCli/DotNetConfigurationFactory.cs +++ b/src/Microsoft.Extensions.Configuration.DotnetCli/DotNetConfigurationFactory.cs @@ -54,6 +54,7 @@ private static DotNetCliConfiguration CreateInternal(string? workingDirectory) workingDirectory ??= Directory.GetCurrentDirectory(); var configurationBuilder = new ConfigurationBuilder(); + configurationBuilder.SetBasePath(workingDirectory); // Configuration sources are added in reverse precedence order // Last added has highest precedence diff --git a/src/Microsoft.Extensions.Configuration.DotnetCli/Providers/DotNetConfigurationSource.cs b/src/Microsoft.Extensions.Configuration.DotnetCli/Providers/DotNetConfigurationSource.cs index 9d65197f355f..084341cdefc4 100644 --- a/src/Microsoft.Extensions.Configuration.DotnetCli/Providers/DotNetConfigurationSource.cs +++ b/src/Microsoft.Extensions.Configuration.DotnetCli/Providers/DotNetConfigurationSource.cs @@ -12,8 +12,9 @@ public class DotNetConfigurationSource : IniConfigurationSource { public DotNetConfigurationSource(string workingDirectory) { - Path = FindDotNetConfigPath(workingDirectory); + Path = System.IO.Path.Combine(workingDirectory, "dotnet.config"); Optional = true; // Make it optional since dotnet.config may not exist + ResolveFileProvider(); } /// /// Builds the configuration provider for dotnet.config files. @@ -25,21 +26,4 @@ public override IConfigurationProvider Build(IConfigurationBuilder builder) EnsureDefaults(builder); return new DotNetConfigurationProvider(this); } - - private static string FindDotNetConfigPath(string? workingDirectory = null) - { - string? directory = workingDirectory ?? Directory.GetCurrentDirectory(); - // Search for dotnet.config in the current directory and upwards - while (directory != null) - { - string dotnetConfigPath = System.IO.Path.Combine(directory, "dotnet.config"); - if (File.Exists(dotnetConfigPath)) - { - return dotnetConfigPath; - } - - directory = System.IO.Path.GetDirectoryName(directory); - } - return "dotnet.config"; // Return default path even if not found - } } diff --git a/src/Microsoft.Extensions.Configuration.DotnetCli/Providers/GlobalJsonConfigurationProvider.cs b/src/Microsoft.Extensions.Configuration.DotnetCli/Providers/GlobalJsonConfigurationProvider.cs index 0ebb8a4378e4..ec05de8794d1 100644 --- a/src/Microsoft.Extensions.Configuration.DotnetCli/Providers/GlobalJsonConfigurationProvider.cs +++ b/src/Microsoft.Extensions.Configuration.DotnetCli/Providers/GlobalJsonConfigurationProvider.cs @@ -1,17 +1,15 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System.Text.Json; +using Microsoft.Extensions.Configuration.Json; namespace Microsoft.Extensions.Configuration.DotnetCli.Providers; /// /// Configuration provider that reads global.json files and maps keys to the canonical format. /// -public class GlobalJsonConfigurationProvider : ConfigurationProvider +public class GlobalJsonConfigurationProvider : JsonConfigurationProvider { - private readonly string? _path; - private static readonly Dictionary GlobalJsonKeyMappings = new() { ["sdk:version"] = "sdk:version", @@ -21,54 +19,26 @@ public class GlobalJsonConfigurationProvider : ConfigurationProvider // Add more mappings as the global.json schema evolves }; - public GlobalJsonConfigurationProvider(string workingDirectory) + public GlobalJsonConfigurationProvider(GlobalJsonConfigurationSource source) : base(source) { - _path = FindGlobalJson(workingDirectory); } public override void Load() { - Data.Clear(); - - if (_path == null || !File.Exists(_path)) - return; + base.Load(); + // Transform keys according to our mapping + var transformedData = new Dictionary(Data.Count, StringComparer.OrdinalIgnoreCase); - try - { - var json = File.ReadAllText(_path); - var document = JsonDocument.Parse(json); - - LoadGlobalJsonData(document.RootElement, ""); - } - catch (Exception ex) + foreach (var kvp in Data) { - throw new InvalidOperationException($"Error parsing global.json at {_path}", ex); + string key = kvp.Key; + string? value = kvp.Value; + var mappedKey = MapGlobalJsonKey(key); + transformedData[mappedKey] = value; } - } - private void LoadGlobalJsonData(JsonElement element, string prefix) - { - foreach (var property in element.EnumerateObject()) - { - var rawKey = string.IsNullOrEmpty(prefix) - ? property.Name - : $"{prefix}:{property.Name}"; - - switch (property.Value.ValueKind) - { - case JsonValueKind.Object: - LoadGlobalJsonData(property.Value, rawKey); - break; - case JsonValueKind.String: - case JsonValueKind.Number: - case JsonValueKind.True: - case JsonValueKind.False: - // Map to canonical key format - var canonicalKey = MapGlobalJsonKey(rawKey); - Data[canonicalKey] = GetValueAsString(property.Value); - break; - } - } + // Replace the data with transformed keys + Data = transformedData; } private string MapGlobalJsonKey(string rawKey) @@ -84,29 +54,4 @@ private string MapGlobalJsonKey(string rawKey) // Default: convert to lowercase and normalize separators return rawKey.ToLowerInvariant().Replace("-", ":"); } - - private string GetValueAsString(JsonElement element) - { - return element.ValueKind switch - { - JsonValueKind.String => element.GetString()!, - JsonValueKind.Number => element.GetRawText(), - JsonValueKind.True => "true", - JsonValueKind.False => "false", - _ => element.GetRawText() - }; - } - - private string? FindGlobalJson(string startDirectory) - { - var current = new DirectoryInfo(startDirectory); - while (current != null) - { - var globalJsonPath = Path.Combine(current.FullName, "global.json"); - if (File.Exists(globalJsonPath)) - return globalJsonPath; - current = current.Parent; - } - return null; - } } diff --git a/src/Microsoft.Extensions.Configuration.DotnetCli/Providers/GlobalJsonConfigurationSource.cs b/src/Microsoft.Extensions.Configuration.DotnetCli/Providers/GlobalJsonConfigurationSource.cs index 3c1fafe09d78..3b6983a3812a 100644 --- a/src/Microsoft.Extensions.Configuration.DotnetCli/Providers/GlobalJsonConfigurationSource.cs +++ b/src/Microsoft.Extensions.Configuration.DotnetCli/Providers/GlobalJsonConfigurationSource.cs @@ -1,22 +1,26 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using Microsoft.Extensions.Configuration.Json; + namespace Microsoft.Extensions.Configuration.DotnetCli.Providers; /// /// Configuration source for global.json files. /// -public class GlobalJsonConfigurationSource : IConfigurationSource +public class GlobalJsonConfigurationSource : JsonConfigurationSource { - private readonly string _workingDirectory; public GlobalJsonConfigurationSource(string workingDirectory) { - _workingDirectory = workingDirectory ?? throw new ArgumentNullException(nameof(workingDirectory)); + Path = System.IO.Path.Combine(workingDirectory, "global.json"); + Optional = true; + ResolveFileProvider(); } - public IConfigurationProvider Build(IConfigurationBuilder builder) + public override IConfigurationProvider Build(IConfigurationBuilder builder) { - return new GlobalJsonConfigurationProvider(_workingDirectory); + EnsureDefaults(builder); + return new GlobalJsonConfigurationProvider(this); } }