|
1 | 1 | // Licensed to the .NET Foundation under one or more agreements. |
2 | 2 | // The .NET Foundation licenses this file to you under the MIT license. |
3 | 3 |
|
| 4 | +using System.Globalization; |
| 5 | +using System.Text.Json; |
4 | 6 | using System.Text.Json.Nodes; |
| 7 | +using Aspire.Cli.Resources; |
5 | 8 | using Aspire.Cli.Utils; |
6 | 9 | using Microsoft.Extensions.Configuration; |
7 | 10 | using Microsoft.Extensions.Logging; |
@@ -353,4 +356,91 @@ private static void FlattenJsonObject(JsonObject obj, Dictionary<string, string> |
353 | 356 | var configKey = key.Replace('.', ':'); |
354 | 357 | return Task.FromResult(configuration[configKey]); |
355 | 358 | } |
| 359 | + |
| 360 | + public Task<string?> GetConfigurationFromDirectoryAsync(string key, DirectoryInfo startDirectory, CancellationToken cancellationToken = default) |
| 361 | + { |
| 362 | + ArgumentNullException.ThrowIfNull(startDirectory); |
| 363 | + |
| 364 | + var configKey = key.Replace('.', ':'); |
| 365 | + |
| 366 | + // 1. Project-relative local settings: walk up from startDirectory. |
| 367 | + // Intentionally bypasses the process-wide IConfiguration (which is rooted at the |
| 368 | + // CLI's launch cwd via ConfigurationHelper.RegisterSettingsFiles) so that commands |
| 369 | + // that operate on a path other than cwd (e.g. `aspire update --apphost <elsewhere>`) |
| 370 | + // consult the project's own aspire.config.json instead of the caller's cwd. |
| 371 | + var localConfigPath = ConfigurationHelper.FindNearestConfigFilePath(startDirectory); |
| 372 | + if (localConfigPath is not null) |
| 373 | + { |
| 374 | + var localConfig = LoadSettingsFileForReading(localConfigPath); |
| 375 | + var localValue = localConfig[configKey]; |
| 376 | + if (!string.IsNullOrWhiteSpace(localValue)) |
| 377 | + { |
| 378 | + return Task.FromResult<string?>(localValue); |
| 379 | + } |
| 380 | + } |
| 381 | + |
| 382 | + // 2. Global settings file fallback (lower precedence). |
| 383 | + if (File.Exists(globalSettingsFile.FullName)) |
| 384 | + { |
| 385 | + var globalConfig = LoadSettingsFileForReading(globalSettingsFile.FullName); |
| 386 | + var globalValue = globalConfig[configKey]; |
| 387 | + if (!string.IsNullOrWhiteSpace(globalValue)) |
| 388 | + { |
| 389 | + return Task.FromResult<string?>(globalValue); |
| 390 | + } |
| 391 | + } |
| 392 | + |
| 393 | + return Task.FromResult<string?>(null); |
| 394 | + } |
| 395 | + |
| 396 | + /// <summary> |
| 397 | + /// Loads a single settings file into an isolated <see cref="IConfigurationRoot"/> for |
| 398 | + /// directory-scoped lookups, mirroring <c>ConfigurationHelper.AddSettingsFile</c>'s |
| 399 | + /// JSON-with-comments parsing and "throw on invalid JSON" behavior so directory-scoped |
| 400 | + /// reads fail loudly the same way startup-time loads do. |
| 401 | + /// </summary> |
| 402 | + private static IConfigurationRoot LoadSettingsFileForReading(string filePath) |
| 403 | + { |
| 404 | + string content; |
| 405 | + try |
| 406 | + { |
| 407 | + content = File.ReadAllText(filePath); |
| 408 | + } |
| 409 | + catch (IOException) |
| 410 | + { |
| 411 | + return new ConfigurationBuilder().Build(); |
| 412 | + } |
| 413 | + catch (UnauthorizedAccessException) |
| 414 | + { |
| 415 | + return new ConfigurationBuilder().Build(); |
| 416 | + } |
| 417 | + |
| 418 | + if (string.IsNullOrWhiteSpace(content)) |
| 419 | + { |
| 420 | + return new ConfigurationBuilder().Build(); |
| 421 | + } |
| 422 | + |
| 423 | + JsonNode? node; |
| 424 | + try |
| 425 | + { |
| 426 | + node = JsonNode.Parse(content, documentOptions: ConfigurationHelper.ParseOptions); |
| 427 | + } |
| 428 | + catch (JsonException ex) |
| 429 | + { |
| 430 | + throw new InvalidOperationException( |
| 431 | + string.Format(CultureInfo.CurrentCulture, ErrorStrings.InvalidJsonInConfigFile, filePath, ex.Message), |
| 432 | + ex); |
| 433 | + } |
| 434 | + |
| 435 | + if (node is not JsonObject) |
| 436 | + { |
| 437 | + return new ConfigurationBuilder().Build(); |
| 438 | + } |
| 439 | + |
| 440 | + var cleanJson = node.ToJsonString(); |
| 441 | + var bytes = System.Text.Encoding.UTF8.GetBytes(cleanJson); |
| 442 | + return new ConfigurationBuilder() |
| 443 | + .AddJsonStream(new MemoryStream(bytes)) |
| 444 | + .Build(); |
| 445 | + } |
356 | 446 | } |
0 commit comments