diff --git a/src/Aspire.Cli/Projects/FallbackProjectParser.cs b/src/Aspire.Cli/Projects/FallbackProjectParser.cs
index 88c8501257a..3eb7b2eee40 100644
--- a/src/Aspire.Cli/Projects/FallbackProjectParser.cs
+++ b/src/Aspire.Cli/Projects/FallbackProjectParser.cs
@@ -3,97 +3,160 @@
using System.Text.Json;
using System.Text.Json.Nodes;
+using System.Text.RegularExpressions;
using System.Xml.Linq;
using Microsoft.Extensions.Logging;
namespace Aspire.Cli.Projects;
///
-/// Provides fallback XML parsing capabilities when MSBuild evaluation fails.
+/// Provides fallback parsing capabilities when MSBuild evaluation fails.
+/// Supports both .csproj XML files and .cs single-file apphost files.
/// Used primarily for AppHost projects with unresolvable SDK versions.
///
-internal sealed class FallbackProjectParser
+internal sealed partial class FallbackProjectParser
{
private readonly ILogger _logger;
+ [GeneratedRegex(@"#:sdk\s+Aspire\.AppHost\.Sdk@([\d\.\-a-zA-Z]+|\*)")]
+ private static partial Regex SdkDirectiveRegex();
+
+ [GeneratedRegex(@"#:package\s+([a-zA-Z0-9\._]+)@([\d\.\-a-zA-Z]+|\*)")]
+ private static partial Regex PackageDirectiveRegex();
+
public FallbackProjectParser(ILogger logger)
{
_logger = logger;
}
///
- /// Parses a project file using direct XML parsing to extract basic project information.
+ /// Parses a project file using direct parsing to extract basic project information.
/// Returns a synthetic JsonDocument that mimics MSBuild's GetProjectItemsAndProperties output.
+ /// Supports both .csproj XML files and .cs single-file apphost files.
///
public JsonDocument ParseProject(FileInfo projectFile)
{
try
{
- _logger.LogDebug("Parsing project file '{ProjectFile}' using fallback XML parser", projectFile.FullName);
-
- var doc = XDocument.Load(projectFile.FullName);
- var root = doc.Root;
+ _logger.LogDebug("Parsing project file '{ProjectFile}' using fallback parser", projectFile.FullName);
- if (root?.Name.LocalName != "Project")
+ // Detect file type and route to appropriate parser
+ if (string.Equals(projectFile.Extension, ".csproj", StringComparison.OrdinalIgnoreCase))
{
- throw new InvalidOperationException($"Invalid project file format: {projectFile.FullName}");
+ return ParseCsprojProjectFile(projectFile);
}
-
- // Extract SDK information
- var aspireHostingSdkVersion = ExtractAspireHostingSdkVersion(root);
-
- // Extract package references
- var packageReferences = ExtractPackageReferences(root);
-
- // Extract project references
- var projectReferences = ExtractProjectReferences(root, projectFile);
-
- // Build the synthetic JSON structure using JsonObject
- var rootObject = new JsonObject();
-
- // Items section
- var itemsObject = new JsonObject();
-
- // PackageReference items
- var packageRefArray = new JsonArray();
- foreach (var pkg in packageReferences)
+ else if (string.Equals(projectFile.Extension, ".cs", StringComparison.OrdinalIgnoreCase))
{
- var packageObj = new JsonObject();
- packageObj["Identity"] = JsonValue.Create(pkg.Identity);
- packageObj["Version"] = JsonValue.Create(pkg.Version);
- packageRefArray.Add((JsonNode?)packageObj);
+ return ParseCsAppHostFile(projectFile);
}
- itemsObject["PackageReference"] = packageRefArray;
-
- // ProjectReference items
- var projectRefArray = new JsonArray();
- foreach (var proj in projectReferences)
+ else
{
- var projectObj = new JsonObject();
- projectObj["Identity"] = JsonValue.Create(proj.Identity);
- projectObj["FullPath"] = JsonValue.Create(proj.FullPath);
- projectRefArray.Add((JsonNode?)projectObj);
+ throw new ProjectUpdaterException($"Unsupported project file type: {projectFile.Extension}. Expected .csproj or .cs file.");
}
- itemsObject["ProjectReference"] = projectRefArray;
-
- rootObject["Items"] = itemsObject;
-
- // Properties section
- var propertiesObject = new JsonObject();
- propertiesObject["AspireHostingSDKVersion"] = JsonValue.Create(aspireHostingSdkVersion);
- rootObject["Properties"] = propertiesObject;
-
- // Fallback flag
- rootObject["Fallback"] = JsonValue.Create(true);
-
- // Convert JsonObject to JsonDocument
- return JsonDocument.Parse(rootObject.ToJsonString());
+ }
+ catch (ProjectUpdaterException)
+ {
+ // Re-throw our custom exceptions
+ throw;
}
catch (Exception ex)
{
- _logger.LogError(ex, "Failed to parse project file '{ProjectFile}' using fallback XML parser", projectFile.FullName);
- throw new ProjectUpdaterException($"Failed to parse project file '{projectFile.FullName}' using fallback XML parser: {ex.Message}", ex);
+ _logger.LogError(ex, "Failed to parse project file '{ProjectFile}' using fallback parser", projectFile.FullName);
+ throw new ProjectUpdaterException($"Failed to parse project file '{projectFile.FullName}' using fallback parser: {ex.Message}", ex);
+ }
+ }
+
+ ///
+ /// Parses a .csproj XML project file to extract SDK and package information.
+ ///
+ private static JsonDocument ParseCsprojProjectFile(FileInfo projectFile)
+ {
+ var doc = XDocument.Load(projectFile.FullName);
+ var root = doc.Root;
+
+ if (root?.Name.LocalName != "Project")
+ {
+ throw new InvalidOperationException($"Invalid project file format: {projectFile.FullName}");
}
+
+ // Extract SDK information
+ var aspireHostingSdkVersion = ExtractAspireHostingSdkVersion(root);
+
+ // Extract package references
+ var packageReferences = ExtractPackageReferences(root);
+
+ // Extract project references
+ var projectReferences = ExtractProjectReferences(root, projectFile);
+
+ return BuildJsonDocument(aspireHostingSdkVersion, packageReferences, projectReferences);
+ }
+
+ ///
+ /// Parses a .cs single-file apphost to extract SDK and package information from directives.
+ ///
+ private static JsonDocument ParseCsAppHostFile(FileInfo projectFile)
+ {
+ var fileContent = File.ReadAllText(projectFile.FullName);
+
+ // Extract SDK version from #:sdk directive
+ var aspireHostingSdkVersion = ExtractSdkVersionFromDirective(fileContent);
+
+ // Extract package references from #:package directives
+ var packageReferences = ExtractPackageReferencesFromDirectives(fileContent);
+
+ // Single-file apphost projects don't have project references
+ var projectReferences = Array.Empty();
+
+ return BuildJsonDocument(aspireHostingSdkVersion, packageReferences, projectReferences);
+ }
+
+ ///
+ /// Builds a synthetic JsonDocument from extracted project information.
+ ///
+ private static JsonDocument BuildJsonDocument(
+ string? aspireHostingSdkVersion,
+ PackageReferenceInfo[] packageReferences,
+ ProjectReferenceInfo[] projectReferences)
+ {
+ var rootObject = new JsonObject();
+
+ // Items section
+ var itemsObject = new JsonObject();
+
+ // PackageReference items
+ var packageRefArray = new JsonArray();
+ foreach (var pkg in packageReferences)
+ {
+ var packageObj = new JsonObject();
+ packageObj["Identity"] = JsonValue.Create(pkg.Identity);
+ packageObj["Version"] = JsonValue.Create(pkg.Version);
+ packageRefArray.Add((JsonNode?)packageObj);
+ }
+ itemsObject["PackageReference"] = packageRefArray;
+
+ // ProjectReference items
+ var projectRefArray = new JsonArray();
+ foreach (var proj in projectReferences)
+ {
+ var projectObj = new JsonObject();
+ projectObj["Identity"] = JsonValue.Create(proj.Identity);
+ projectObj["FullPath"] = JsonValue.Create(proj.FullPath);
+ projectRefArray.Add((JsonNode?)projectObj);
+ }
+ itemsObject["ProjectReference"] = projectRefArray;
+
+ rootObject["Items"] = itemsObject;
+
+ // Properties section
+ var propertiesObject = new JsonObject();
+ propertiesObject["AspireHostingSDKVersion"] = JsonValue.Create(aspireHostingSdkVersion);
+ rootObject["Properties"] = propertiesObject;
+
+ // Fallback flag
+ rootObject["Fallback"] = JsonValue.Create(true);
+
+ // Convert JsonObject to JsonDocument
+ return JsonDocument.Parse(rootObject.ToJsonString());
}
private static string? ExtractAspireHostingSdkVersion(XElement projectRoot)
@@ -172,6 +235,51 @@ private static ProjectReferenceInfo[] ExtractProjectReferences(XElement projectR
return projectReferences.ToArray();
}
+
+ ///
+ /// Extracts the Aspire.AppHost.Sdk version from the #:sdk directive in a single-file apphost.
+ ///
+ private static string? ExtractSdkVersionFromDirective(string fileContent)
+ {
+ // Match: #:sdk Aspire.AppHost.Sdk@
+ // Where version can be a semantic version or wildcard (*)
+ var match = SdkDirectiveRegex().Match(fileContent);
+
+ if (match.Success)
+ {
+ return match.Groups[1].Value;
+ }
+
+ return null;
+ }
+
+ ///
+ /// Extracts package references from #:package directives in a single-file apphost.
+ ///
+ private static PackageReferenceInfo[] ExtractPackageReferencesFromDirectives(string fileContent)
+ {
+ var packageReferences = new List();
+
+ // Match: #:package @
+ // Where version can be a semantic version or wildcard (*)
+ var matches = PackageDirectiveRegex().Matches(fileContent);
+
+ foreach (Match match in matches)
+ {
+ var identity = match.Groups[1].Value;
+ var version = match.Groups[2].Value;
+
+ var packageRef = new PackageReferenceInfo
+ {
+ Identity = identity,
+ Version = version
+ };
+
+ packageReferences.Add(packageRef);
+ }
+
+ return packageReferences.ToArray();
+ }
}
internal record PackageReferenceInfo
diff --git a/src/Aspire.Cli/Projects/ProjectUpdater.cs b/src/Aspire.Cli/Projects/ProjectUpdater.cs
index 7277bc7f9c1..501d3e468d0 100644
--- a/src/Aspire.Cli/Projects/ProjectUpdater.cs
+++ b/src/Aspire.Cli/Projects/ProjectUpdater.cs
@@ -63,10 +63,10 @@ public async Task UpdateProjectAsync(FileInfo projectFile,
interactionService.DisplayEmptyLine();
}
- // Display warning if fallback XML parsing was used
+ // Display warning if fallback parsing was used
if (fallbackUsed)
{
- interactionService.DisplayMessage("warning", "[yellow]Note: Update plan generated using fallback XML parsing due to unresolvable AppHost SDK. Dependency analysis may have reduced accuracy.[/]");
+ interactionService.DisplayMessage("warning", $"[yellow]{UpdateCommandStrings.FallbackParsingWarning}[/]");
interactionService.DisplayEmptyLine();
}
@@ -157,7 +157,7 @@ private static bool IsGlobalNuGetConfig(string path)
await analyzeStep.Callback();
}
- return (context.UpdateSteps, context.FallbackXmlParsing);
+ return (context.UpdateSteps, context.FallbackParsing);
}
private const string ItemsAndPropertiesCacheKeyPrefix = "ItemsAndProperties";
@@ -202,12 +202,12 @@ private async Task GetItemsAndPropertiesWithFallbackAsync(FileInfo
catch (ProjectUpdaterException ex) when (IsAppHostProject(projectFile, context))
{
// Only use fallback for AppHost projects
- logger.LogWarning("Falling back to XML parsing for '{ProjectFile}'. Reason: {Message}", projectFile.FullName, ex.Message);
+ logger.LogWarning("Falling back to parsing for '{ProjectFile}'. Reason: {Message}", projectFile.FullName, ex.Message);
- if (!context.FallbackXmlParsing)
+ if (!context.FallbackParsing)
{
- context.FallbackXmlParsing = true;
- logger.LogWarning("Update plan will be generated using fallback XML parsing; dependency accuracy may be reduced.");
+ context.FallbackParsing = true;
+ logger.LogWarning("Update plan will be generated using fallback parsing; dependency accuracy may be reduced.");
}
return fallbackParser.ParseProject(projectFile);
@@ -863,7 +863,7 @@ internal sealed class UpdateContext(FileInfo appHostProjectFile, PackageChannel
public ConcurrentQueue UpdateSteps { get; } = new();
public ConcurrentQueue AnalyzeSteps { get; } = new();
public HashSet VisitedProjects { get; } = new();
- public bool FallbackXmlParsing { get; set; }
+ public bool FallbackParsing { get; set; }
}
internal abstract record UpdateStep(string Description, Func Callback)
diff --git a/src/Aspire.Cli/Resources/UpdateCommandStrings.Designer.cs b/src/Aspire.Cli/Resources/UpdateCommandStrings.Designer.cs
index 4ac9bed7349..e18496b9838 100644
--- a/src/Aspire.Cli/Resources/UpdateCommandStrings.Designer.cs
+++ b/src/Aspire.Cli/Resources/UpdateCommandStrings.Designer.cs
@@ -98,5 +98,6 @@ internal static string ProjectArgumentDescription {
internal static string MappingAddedFormat => ResourceManager.GetString("MappingAddedFormat", resourceCulture);
internal static string MappingRemovedFormat => ResourceManager.GetString("MappingRemovedFormat", resourceCulture);
internal static string MappingRetainedFormat => ResourceManager.GetString("MappingRetainedFormat", resourceCulture);
+ internal static string FallbackParsingWarning => ResourceManager.GetString("FallbackParsingWarning", resourceCulture);
}
}
diff --git a/src/Aspire.Cli/Resources/UpdateCommandStrings.resx b/src/Aspire.Cli/Resources/UpdateCommandStrings.resx
index 164b6857af6..a7dde2726fd 100644
--- a/src/Aspire.Cli/Resources/UpdateCommandStrings.resx
+++ b/src/Aspire.Cli/Resources/UpdateCommandStrings.resx
@@ -111,4 +111,7 @@
Mapping: {0}
+
+ Note: Update plan generated using fallback parsing due to unresolvable AppHost SDK. Dependency analysis may have reduced accuracy.
+
diff --git a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.cs.xlf b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.cs.xlf
index 31922d185d2..bc71025f673 100644
--- a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.cs.xlf
+++ b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.cs.xlf
@@ -77,6 +77,11 @@
Nepodařilo se aktualizovat odkaz na balíček pro {0} v projektu {1}.
+
+ Note: Update plan generated using fallback parsing due to unresolvable AppHost SDK. Dependency analysis may have reduced accuracy.
+ Note: Update plan generated using fallback parsing due to unresolvable AppHost SDK. Dependency analysis may have reduced accuracy.
+
+
Mapping: {0} (added)
Mapování: {0} (přidáno)
diff --git a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.de.xlf b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.de.xlf
index 05bf1b34eb8..37ce2c7e70f 100644
--- a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.de.xlf
+++ b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.de.xlf
@@ -77,6 +77,11 @@
Beim Aktualisieren des Paketverweises für {0} im Projekt {1} ist ein Fehler aufgetreten.
+
+ Note: Update plan generated using fallback parsing due to unresolvable AppHost SDK. Dependency analysis may have reduced accuracy.
+ Note: Update plan generated using fallback parsing due to unresolvable AppHost SDK. Dependency analysis may have reduced accuracy.
+
+
Mapping: {0} (added)
Zuordnung: {0} (hinzugefügt)
diff --git a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.es.xlf b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.es.xlf
index 2d83e9d6d78..0090072ba0e 100644
--- a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.es.xlf
+++ b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.es.xlf
@@ -77,6 +77,11 @@
No se pudo actualizar la referencia del paquete para {0} en el proyecto {1}.
+
+ Note: Update plan generated using fallback parsing due to unresolvable AppHost SDK. Dependency analysis may have reduced accuracy.
+ Note: Update plan generated using fallback parsing due to unresolvable AppHost SDK. Dependency analysis may have reduced accuracy.
+
+
Mapping: {0} (added)
Asignación: {0} (agregado)
diff --git a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.fr.xlf b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.fr.xlf
index 0ade8865774..3cb8ac57456 100644
--- a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.fr.xlf
+++ b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.fr.xlf
@@ -77,6 +77,11 @@
Échec de la mise à jour de la référence de package pour {0} dans le projet {1}.
+
+ Note: Update plan generated using fallback parsing due to unresolvable AppHost SDK. Dependency analysis may have reduced accuracy.
+ Note: Update plan generated using fallback parsing due to unresolvable AppHost SDK. Dependency analysis may have reduced accuracy.
+
+
Mapping: {0} (added)
Mappage : {0} (ajouté)
diff --git a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.it.xlf b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.it.xlf
index a89c47612f0..c8ac74cee07 100644
--- a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.it.xlf
+++ b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.it.xlf
@@ -77,6 +77,11 @@
Non è possibile aggiornare il riferimento al pacchetto per {0} nel progetto {1}.
+
+ Note: Update plan generated using fallback parsing due to unresolvable AppHost SDK. Dependency analysis may have reduced accuracy.
+ Note: Update plan generated using fallback parsing due to unresolvable AppHost SDK. Dependency analysis may have reduced accuracy.
+
+
Mapping: {0} (added)
Mapping: {0} (aggiunto)
diff --git a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.ja.xlf b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.ja.xlf
index ff647943924..db386eeef55 100644
--- a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.ja.xlf
+++ b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.ja.xlf
@@ -77,6 +77,11 @@
プロジェクト {1} の {0} に対するパッケージ参照の更新に失敗しました。
+
+ Note: Update plan generated using fallback parsing due to unresolvable AppHost SDK. Dependency analysis may have reduced accuracy.
+ Note: Update plan generated using fallback parsing due to unresolvable AppHost SDK. Dependency analysis may have reduced accuracy.
+
+
Mapping: {0} (added)
マッピング: {0} (追加済み)
diff --git a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.ko.xlf b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.ko.xlf
index 9773bb39b53..f7c23864ac9 100644
--- a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.ko.xlf
+++ b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.ko.xlf
@@ -77,6 +77,11 @@
프로젝트 {1}에서 {0}에 대한 패키지 참조를 업데이트하지 못 했습니다.
+
+ Note: Update plan generated using fallback parsing due to unresolvable AppHost SDK. Dependency analysis may have reduced accuracy.
+ Note: Update plan generated using fallback parsing due to unresolvable AppHost SDK. Dependency analysis may have reduced accuracy.
+
+
Mapping: {0} (added)
매핑: {0}(추가됨)
diff --git a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.pl.xlf b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.pl.xlf
index 33f6cbca0f7..727fe1f3fc2 100644
--- a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.pl.xlf
+++ b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.pl.xlf
@@ -77,6 +77,11 @@
Nie można zaktualizować odwołania do pakietu {0} w projekcie {1}.
+
+ Note: Update plan generated using fallback parsing due to unresolvable AppHost SDK. Dependency analysis may have reduced accuracy.
+ Note: Update plan generated using fallback parsing due to unresolvable AppHost SDK. Dependency analysis may have reduced accuracy.
+
+
Mapping: {0} (added)
Mapowanie: {0} (dodano)
diff --git a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.pt-BR.xlf b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.pt-BR.xlf
index 26e6aeb98d1..a1b80b9a975 100644
--- a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.pt-BR.xlf
+++ b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.pt-BR.xlf
@@ -77,6 +77,11 @@
Falha ao atualizar a referência de pacote para {0} no projeto {1}.
+
+ Note: Update plan generated using fallback parsing due to unresolvable AppHost SDK. Dependency analysis may have reduced accuracy.
+ Note: Update plan generated using fallback parsing due to unresolvable AppHost SDK. Dependency analysis may have reduced accuracy.
+
+
Mapping: {0} (added)
Mapeamento: {0} (adicionado)
diff --git a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.ru.xlf b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.ru.xlf
index ff91d82c2d7..450b502695c 100644
--- a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.ru.xlf
+++ b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.ru.xlf
@@ -77,6 +77,11 @@
Не удалось обновить ссылку на пакет для {0} в проекте {1}.
+
+ Note: Update plan generated using fallback parsing due to unresolvable AppHost SDK. Dependency analysis may have reduced accuracy.
+ Note: Update plan generated using fallback parsing due to unresolvable AppHost SDK. Dependency analysis may have reduced accuracy.
+
+
Mapping: {0} (added)
Сопоставление: {0} (добавлено)
diff --git a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.tr.xlf b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.tr.xlf
index 1b565e3c42f..8b53b4fa1d1 100644
--- a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.tr.xlf
+++ b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.tr.xlf
@@ -77,6 +77,11 @@
{1} projesinde {0} için paket başvurusu güncellenemedi.
+
+ Note: Update plan generated using fallback parsing due to unresolvable AppHost SDK. Dependency analysis may have reduced accuracy.
+ Note: Update plan generated using fallback parsing due to unresolvable AppHost SDK. Dependency analysis may have reduced accuracy.
+
+
Mapping: {0} (added)
Eşleme: {0} (eklendi)
diff --git a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.zh-Hans.xlf b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.zh-Hans.xlf
index eb496d06b16..d0663526544 100644
--- a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.zh-Hans.xlf
+++ b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.zh-Hans.xlf
@@ -77,6 +77,11 @@
未能更新项目 {1} 中 {0} 的包引用。
+
+ Note: Update plan generated using fallback parsing due to unresolvable AppHost SDK. Dependency analysis may have reduced accuracy.
+ Note: Update plan generated using fallback parsing due to unresolvable AppHost SDK. Dependency analysis may have reduced accuracy.
+
+
Mapping: {0} (added)
映射: {0} (已添加)
diff --git a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.zh-Hant.xlf b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.zh-Hant.xlf
index 15141f4ef52..11bc2dd1594 100644
--- a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.zh-Hant.xlf
+++ b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.zh-Hant.xlf
@@ -77,6 +77,11 @@
無法更新專案 {1} 中 {0} 的套件參考。
+
+ Note: Update plan generated using fallback parsing due to unresolvable AppHost SDK. Dependency analysis may have reduced accuracy.
+ Note: Update plan generated using fallback parsing due to unresolvable AppHost SDK. Dependency analysis may have reduced accuracy.
+
+
Mapping: {0} (added)
對應: {0} (已新增)
diff --git a/tests/Aspire.Cli.Tests/Projects/FallbackProjectParserTests.cs b/tests/Aspire.Cli.Tests/Projects/FallbackProjectParserTests.cs
index 69db21c45ae..53c01618087 100644
--- a/tests/Aspire.Cli.Tests/Projects/FallbackProjectParserTests.cs
+++ b/tests/Aspire.Cli.Tests/Projects/FallbackProjectParserTests.cs
@@ -13,16 +13,16 @@ public class FallbackProjectParserTests
public void ParseProject_ExtractsAspireAppHostSdk()
{
// Arrange
- var tempDir = Path.GetTempPath();
- var projectFile = Path.Combine(tempDir, $"Test{Guid.NewGuid()}.csproj");
- var projectContent = """
-
-
-
- """;
-
+ var tempDir = Directory.CreateTempSubdirectory();
try
{
+ var projectFile = Path.Combine(tempDir.FullName, $"Test{Guid.NewGuid()}.csproj");
+ var projectContent = """
+
+
+
+ """;
+
File.WriteAllText(projectFile, projectContent);
var parser = new FallbackProjectParser(NullLogger.Instance);
@@ -39,10 +39,7 @@ public void ParseProject_ExtractsAspireAppHostSdk()
}
finally
{
- if (File.Exists(projectFile))
- {
- File.Delete(projectFile);
- }
+ tempDir.Delete(recursive: true);
}
}
@@ -50,20 +47,20 @@ public void ParseProject_ExtractsAspireAppHostSdk()
public void ParseProject_ExtractsPackageReferences()
{
// Arrange
- var tempDir = Path.GetTempPath();
- var projectFile = Path.Combine(tempDir, $"Test{Guid.NewGuid()}.csproj");
- var projectContent = """
-
-
-
-
-
-
-
- """;
-
+ var tempDir = Directory.CreateTempSubdirectory();
try
{
+ var projectFile = Path.Combine(tempDir.FullName, $"Test{Guid.NewGuid()}.csproj");
+ var projectContent = """
+
+
+
+
+
+
+
+ """;
+
File.WriteAllText(projectFile, projectContent);
var parser = new FallbackProjectParser(NullLogger.Instance);
@@ -88,10 +85,7 @@ public void ParseProject_ExtractsPackageReferences()
}
finally
{
- if (File.Exists(projectFile))
- {
- File.Delete(projectFile);
- }
+ tempDir.Delete(recursive: true);
}
}
@@ -99,20 +93,20 @@ public void ParseProject_ExtractsPackageReferences()
public void ParseProject_ExtractsProjectReferences()
{
// Arrange
- var tempDir = Path.GetTempPath();
- var projectFile = Path.Combine(tempDir, $"Test{Guid.NewGuid()}.csproj");
- var projectContent = """
-
-
-
-
-
-
-
- """;
-
+ var tempDir = Directory.CreateTempSubdirectory();
try
{
+ var projectFile = Path.Combine(tempDir.FullName, $"Test{Guid.NewGuid()}.csproj");
+ var projectContent = """
+
+
+
+
+
+
+
+ """;
+
File.WriteAllText(projectFile, projectContent);
var parser = new FallbackProjectParser(NullLogger.Instance);
@@ -135,10 +129,7 @@ public void ParseProject_ExtractsProjectReferences()
}
finally
{
- if (File.Exists(projectFile))
- {
- File.Delete(projectFile);
- }
+ tempDir.Delete(recursive: true);
}
}
@@ -146,18 +137,18 @@ public void ParseProject_ExtractsProjectReferences()
public void ParseProject_InvalidXml_ThrowsProjectUpdaterException()
{
// Arrange
- var tempDir = Path.GetTempPath();
- var projectFile = Path.Combine(tempDir, $"Test{Guid.NewGuid()}.csproj");
- var invalidProjectContent = """
-
-
-
-
-
- """;
-
+ var tempDir = Directory.CreateTempSubdirectory();
try
{
+ var projectFile = Path.Combine(tempDir.FullName, $"Test{Guid.NewGuid()}.csproj");
+ var invalidProjectContent = """
+
+
+
+
+
+ """;
+
File.WriteAllText(projectFile, invalidProjectContent);
var parser = new FallbackProjectParser(NullLogger.Instance);
@@ -167,10 +158,262 @@ public void ParseProject_InvalidXml_ThrowsProjectUpdaterException()
}
finally
{
- if (File.Exists(projectFile))
- {
- File.Delete(projectFile);
- }
+ tempDir.Delete(recursive: true);
+ }
+ }
+
+ [Fact]
+ public void ParseProject_SingleFileAppHost_ExtractsAspireAppHostSdk()
+ {
+ // Arrange
+ var tempDir = Directory.CreateTempSubdirectory();
+ try
+ {
+ var projectFile = Path.Combine(tempDir.FullName, $"Test{Guid.NewGuid()}.cs");
+ var projectContent = """
+ #:sdk Aspire.AppHost.Sdk@13.0.0-preview.1.25519.5
+ #:package Aspire.Hosting.NodeJs@9.5.1
+
+ var builder = DistributedApplication.CreateBuilder(args);
+ builder.Build().Run();
+ """;
+
+ File.WriteAllText(projectFile, projectContent);
+ var parser = new FallbackProjectParser(NullLogger.Instance);
+
+ // Act
+ var result = parser.ParseProject(new FileInfo(projectFile));
+
+ // Assert
+ var properties = result.RootElement.GetProperty("Properties");
+ var sdkVersion = properties.GetProperty("AspireHostingSDKVersion").GetString();
+ Assert.Equal("13.0.0-preview.1.25519.5", sdkVersion);
+
+ // Should have fallback flag
+ Assert.True(result.RootElement.GetProperty("Fallback").GetBoolean());
+ }
+ finally
+ {
+ tempDir.Delete(recursive: true);
+ }
+ }
+
+ [Fact]
+ public void ParseProject_SingleFileAppHost_ExtractsPackageReferences()
+ {
+ // Arrange
+ var tempDir = Directory.CreateTempSubdirectory();
+ try
+ {
+ var projectFile = Path.Combine(tempDir.FullName, $"Test{Guid.NewGuid()}.cs");
+ var projectContent = """
+ #:sdk Aspire.AppHost.Sdk@13.0.0-preview.1.25519.5
+ #:package Aspire.Hosting.NodeJs@9.5.1
+ #:package Aspire.Hosting.Python@9.5.1
+ #:package Aspire.Hosting.Redis@9.5.1
+ #:package CommunityToolkit.Aspire.Hosting.NodeJS.Extensions@9.8.0
+
+ #pragma warning disable ASPIREHOSTINGPYTHON001
+
+ var builder = DistributedApplication.CreateBuilder(args);
+ var cache = builder.AddRedis("cache");
+ builder.Build().Run();
+ """;
+
+ File.WriteAllText(projectFile, projectContent);
+ var parser = new FallbackProjectParser(NullLogger.Instance);
+
+ // Act
+ var result = parser.ParseProject(new FileInfo(projectFile));
+
+ // Assert
+ var items = result.RootElement.GetProperty("Items");
+ var packageRefs = items.GetProperty("PackageReference").EnumerateArray().ToArray();
+
+ Assert.Equal(4, packageRefs.Length);
+
+ var nodeJsPkg = packageRefs.FirstOrDefault(p =>
+ p.GetProperty("Identity").GetString() == "Aspire.Hosting.NodeJs");
+ Assert.NotEqual(default(JsonElement), nodeJsPkg);
+ Assert.Equal("9.5.1", nodeJsPkg.GetProperty("Version").GetString());
+
+ var pythonPkg = packageRefs.FirstOrDefault(p =>
+ p.GetProperty("Identity").GetString() == "Aspire.Hosting.Python");
+ Assert.NotEqual(default(JsonElement), pythonPkg);
+ Assert.Equal("9.5.1", pythonPkg.GetProperty("Version").GetString());
+
+ var redisPkg = packageRefs.FirstOrDefault(p =>
+ p.GetProperty("Identity").GetString() == "Aspire.Hosting.Redis");
+ Assert.NotEqual(default(JsonElement), redisPkg);
+ Assert.Equal("9.5.1", redisPkg.GetProperty("Version").GetString());
+
+ var toolkitPkg = packageRefs.FirstOrDefault(p =>
+ p.GetProperty("Identity").GetString() == "CommunityToolkit.Aspire.Hosting.NodeJS.Extensions");
+ Assert.NotEqual(default(JsonElement), toolkitPkg);
+ Assert.Equal("9.8.0", toolkitPkg.GetProperty("Version").GetString());
+ }
+ finally
+ {
+ tempDir.Delete(recursive: true);
+ }
+ }
+
+ [Fact]
+ public void ParseProject_SingleFileAppHost_NoPackageReferences()
+ {
+ // Arrange
+ var tempDir = Directory.CreateTempSubdirectory();
+ try
+ {
+ var projectFile = Path.Combine(tempDir.FullName, $"Test{Guid.NewGuid()}.cs");
+ var projectContent = """
+ #:sdk Aspire.AppHost.Sdk@9.5.0
+
+ var builder = DistributedApplication.CreateBuilder(args);
+ builder.Build().Run();
+ """;
+
+ File.WriteAllText(projectFile, projectContent);
+ var parser = new FallbackProjectParser(NullLogger.Instance);
+
+ // Act
+ var result = parser.ParseProject(new FileInfo(projectFile));
+
+ // Assert
+ var items = result.RootElement.GetProperty("Items");
+ var packageRefs = items.GetProperty("PackageReference").EnumerateArray().ToArray();
+
+ Assert.Empty(packageRefs);
+ }
+ finally
+ {
+ tempDir.Delete(recursive: true);
+ }
+ }
+
+ [Fact]
+ public void ParseProject_SingleFileAppHost_WithWildcardVersion()
+ {
+ // Arrange
+ var tempDir = Directory.CreateTempSubdirectory();
+ try
+ {
+ var projectFile = Path.Combine(tempDir.FullName, $"Test{Guid.NewGuid()}.cs");
+ var projectContent = """
+ #:sdk Aspire.AppHost.Sdk@*
+ #:package Aspire.Hosting.Redis@*
+
+ var builder = DistributedApplication.CreateBuilder(args);
+ builder.Build().Run();
+ """;
+
+ File.WriteAllText(projectFile, projectContent);
+ var parser = new FallbackProjectParser(NullLogger.Instance);
+
+ // Act
+ var result = parser.ParseProject(new FileInfo(projectFile));
+
+ // Assert
+ var properties = result.RootElement.GetProperty("Properties");
+ var sdkVersion = properties.GetProperty("AspireHostingSDKVersion").GetString();
+ Assert.Equal("*", sdkVersion);
+
+ var items = result.RootElement.GetProperty("Items");
+ var packageRefs = items.GetProperty("PackageReference").EnumerateArray().ToArray();
+ Assert.Single(packageRefs);
+ Assert.Equal("*", packageRefs[0].GetProperty("Version").GetString());
+ }
+ finally
+ {
+ tempDir.Delete(recursive: true);
+ }
+ }
+
+ [Fact]
+ public void ParseProject_SingleFileAppHost_NoProjectReferences()
+ {
+ // Arrange - single-file apphosts don't support project references
+ var tempDir = Directory.CreateTempSubdirectory();
+ try
+ {
+ var projectFile = Path.Combine(tempDir.FullName, $"Test{Guid.NewGuid()}.cs");
+ var projectContent = """
+ #:sdk Aspire.AppHost.Sdk@9.5.0
+
+ var builder = DistributedApplication.CreateBuilder(args);
+ builder.Build().Run();
+ """;
+
+ File.WriteAllText(projectFile, projectContent);
+ var parser = new FallbackProjectParser(NullLogger.Instance);
+
+ // Act
+ var result = parser.ParseProject(new FileInfo(projectFile));
+
+ // Assert
+ var items = result.RootElement.GetProperty("Items");
+ var projectRefs = items.GetProperty("ProjectReference").EnumerateArray().ToArray();
+
+ Assert.Empty(projectRefs);
+ }
+ finally
+ {
+ tempDir.Delete(recursive: true);
+ }
+ }
+
+ [Fact]
+ public void ParseProject_SingleFileAppHost_NoSdkDirective()
+ {
+ // Arrange
+ var tempDir = Directory.CreateTempSubdirectory();
+ try
+ {
+ var projectFile = Path.Combine(tempDir.FullName, $"Test{Guid.NewGuid()}.cs");
+ var projectContent = """
+ // Missing SDK directive
+ var builder = DistributedApplication.CreateBuilder(args);
+ builder.Build().Run();
+ """;
+
+ File.WriteAllText(projectFile, projectContent);
+ var parser = new FallbackProjectParser(NullLogger.Instance);
+
+ // Act
+ var result = parser.ParseProject(new FileInfo(projectFile));
+
+ // Assert - should return null SDK version
+ var properties = result.RootElement.GetProperty("Properties");
+ var sdkVersion = properties.GetProperty("AspireHostingSDKVersion");
+ Assert.Equal(JsonValueKind.Null, sdkVersion.ValueKind);
+ }
+ finally
+ {
+ tempDir.Delete(recursive: true);
+ }
+ }
+
+ [Fact]
+ public void ParseProject_UnsupportedFileType_ThrowsProjectUpdaterException()
+ {
+ // Arrange
+ var tempDir = Directory.CreateTempSubdirectory();
+ try
+ {
+ var projectFile = Path.Combine(tempDir.FullName, $"Test{Guid.NewGuid()}.txt");
+ var projectContent = "Some random content";
+
+ File.WriteAllText(projectFile, projectContent);
+ var parser = new FallbackProjectParser(NullLogger.Instance);
+
+ // Act & Assert
+ var exception = Assert.Throws(() =>
+ parser.ParseProject(new FileInfo(projectFile)));
+ Assert.Contains("Unsupported project file type", exception.Message);
+ }
+ finally
+ {
+ tempDir.Delete(recursive: true);
}
}
}
\ No newline at end of file