From aae82168d90243beeef74e641d1579e3175110e4 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 30 Nov 2025 22:30:58 +0000 Subject: [PATCH 1/6] Initial plan From 3242db04a6a786a2f015296f1ad135c8f75fe236 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 30 Nov 2025 22:47:09 +0000 Subject: [PATCH 2/6] Add IPackageMigration interface, PackageMigration implementation, and tests Co-authored-by: mitchdenny <513398+mitchdenny@users.noreply.github.com> --- src/Aspire.Cli/Packaging/PackageMigration.cs | 133 +++++ src/Aspire.Cli/Program.cs | 1 + .../Packaging/PackageMigrationTests.cs | 477 ++++++++++++++++++ tests/Aspire.Cli.Tests/Utils/CliTestHelper.cs | 7 + 4 files changed, 618 insertions(+) create mode 100644 src/Aspire.Cli/Packaging/PackageMigration.cs create mode 100644 tests/Aspire.Cli.Tests/Packaging/PackageMigrationTests.cs diff --git a/src/Aspire.Cli/Packaging/PackageMigration.cs b/src/Aspire.Cli/Packaging/PackageMigration.cs new file mode 100644 index 00000000000..79214798a4e --- /dev/null +++ b/src/Aspire.Cli/Packaging/PackageMigration.cs @@ -0,0 +1,133 @@ +// 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.Logging; +using Semver; + +namespace Aspire.Cli.Packaging; + +/// +/// Provides package migration information for upgrading or downgrading packages +/// when switching between different Aspire versions. +/// +internal interface IPackageMigration +{ + /// + /// Gets the replacement package ID for a package when migrating to a specific target Aspire hosting version. + /// + /// The target Aspire hosting SDK version being migrated to. + /// The package ID to check for migration. + /// + /// The replacement package ID if a migration rule exists for the given package and version; + /// null if no migration is needed for this package. + /// + /// Thrown when multiple migration rules match for the same package. + string? GetMigration(SemVersion targetHostingVersion, string packageId); +} + +/// +/// Implementation of that provides package migration rules +/// for migrating packages between different Aspire versions. +/// +internal sealed class PackageMigration : IPackageMigration +{ + private readonly ILogger _logger; + private readonly List<(Func VersionPredicate, string FromPackageId, string ToPackageId)> _migrationRules; + + /// + /// Initializes a new instance of the class. + /// + /// The logger instance. + public PackageMigration(ILogger logger) + { + _logger = logger; + _migrationRules = CreateMigrationRules(); + } + + /// + /// Creates the list of migration rules. + /// Each rule contains a version predicate, the source package ID, and the target package ID. + /// Rules are evaluated based on the target hosting version. + /// + private static List<(Func VersionPredicate, string FromPackageId, string ToPackageId)> CreateMigrationRules() + { + // Migration rules are defined here. Each rule specifies: + // - A predicate that determines if the rule applies based on the target version + // - The package ID being migrated from + // - The package ID being migrated to + // + // Both upwards (stable -> daily) and downwards (daily -> stable) migrations should be defined + // to support users switching between channels. + // + // Example migration rules (placeholder - replace with actual migrations): + // - When upgrading to version >= 10.0.0, migrate Aspire.Hosting.OldPackage to Aspire.Hosting.NewPackage + // - When downgrading to version < 10.0.0, migrate Aspire.Hosting.NewPackage to Aspire.Hosting.OldPackage + return + [ + // Add migration rules here as packages are renamed/replaced + // Example: + // (v => v >= SemVersion.Parse("10.0.0", SemVersionStyles.Strict), "Aspire.Hosting.OldPackage", "Aspire.Hosting.NewPackage"), + // (v => v < SemVersion.Parse("10.0.0", SemVersionStyles.Strict), "Aspire.Hosting.NewPackage", "Aspire.Hosting.OldPackage"), + ]; + } + + /// + public string? GetMigration(SemVersion targetHostingVersion, string packageId) + { + _logger.LogDebug("Checking migration rules for package '{PackageId}' targeting version '{TargetVersion}'", packageId, targetHostingVersion); + + // Find all matching rules for the given package ID and version + var matchingRules = _migrationRules + .Where(rule => string.Equals(rule.FromPackageId, packageId, StringComparison.OrdinalIgnoreCase) + && rule.VersionPredicate(targetHostingVersion)) + .ToList(); + + if (matchingRules.Count == 0) + { + _logger.LogDebug("No migration rules found for package '{PackageId}'", packageId); + return null; + } + + if (matchingRules.Count > 1) + { + var toPackages = string.Join(", ", matchingRules.Select(r => r.ToPackageId)); + _logger.LogError( + "Multiple migration rules found for package '{PackageId}' at version '{TargetVersion}'. Target packages: {ToPackages}", + packageId, targetHostingVersion, toPackages); + + throw new PackageMigrationException( + $"Multiple migration rules match for package '{packageId}' at version '{targetHostingVersion}'. " + + $"This is a configuration error. Matching target packages: {toPackages}"); + } + + var matchingRule = matchingRules[0]; + _logger.LogInformation( + "Migration rule found: '{FromPackageId}' -> '{ToPackageId}' for target version '{TargetVersion}'", + matchingRule.FromPackageId, matchingRule.ToPackageId, targetHostingVersion); + + return matchingRule.ToPackageId; + } +} + +/// +/// Exception thrown when there is an error in package migration processing. +/// +internal sealed class PackageMigrationException : Exception +{ + /// + /// Initializes a new instance of the class. + /// + /// The error message. + public PackageMigrationException(string message) : base(message) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The error message. + /// The inner exception. + public PackageMigrationException(string message, Exception innerException) : base(message, innerException) + { + } +} diff --git a/src/Aspire.Cli/Program.cs b/src/Aspire.Cli/Program.cs index bd763009c46..b32b2852221 100644 --- a/src/Aspire.Cli/Program.cs +++ b/src/Aspire.Cli/Program.cs @@ -167,6 +167,7 @@ private static async Task BuildApplicationAsync(string[] args) builder.Services.AddHostedService(sp => sp.GetRequiredService()); builder.Services.AddSingleton(); builder.Services.AddSingleton(); + builder.Services.AddSingleton(); builder.Services.AddSingleton(); builder.Services.AddMemoryCache(); diff --git a/tests/Aspire.Cli.Tests/Packaging/PackageMigrationTests.cs b/tests/Aspire.Cli.Tests/Packaging/PackageMigrationTests.cs new file mode 100644 index 00000000000..cdbfb4b2d01 --- /dev/null +++ b/tests/Aspire.Cli.Tests/Packaging/PackageMigrationTests.cs @@ -0,0 +1,477 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Aspire.Cli.Packaging; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Abstractions; +using Semver; + +namespace Aspire.Cli.Tests.Packaging; + +public class PackageMigrationTests +{ + private static ILogger CreateTestLogger() + { + return NullLogger.Instance; + } + + /// + /// Helper method to create a version predicate for versions >= threshold + /// + private static Func VersionAtLeast(string threshold) + { + var thresholdVersion = SemVersion.Parse(threshold, SemVersionStyles.Strict); + return v => SemVersion.ComparePrecedence(v, thresholdVersion) >= 0; + } + + /// + /// Helper method to create a version predicate for versions < threshold + /// + private static Func VersionBelow(string threshold) + { + var thresholdVersion = SemVersion.Parse(threshold, SemVersionStyles.Strict); + return v => SemVersion.ComparePrecedence(v, thresholdVersion) < 0; + } + + /// + /// Helper method to create a version predicate for versions in a range [min, max) + /// + private static Func VersionInRange(string minInclusive, string maxExclusive) + { + var minVersion = SemVersion.Parse(minInclusive, SemVersionStyles.Strict); + var maxVersion = SemVersion.Parse(maxExclusive, SemVersionStyles.Strict); + return v => SemVersion.ComparePrecedence(v, minVersion) >= 0 + && SemVersion.ComparePrecedence(v, maxVersion) < 0; + } + + #region GetMigration - Basic functionality tests + + [Fact] + public void GetMigration_WhenNoMatchingRule_ReturnsNull() + { + // Arrange + var migration = new PackageMigration(CreateTestLogger()); + var targetVersion = SemVersion.Parse("9.5.0", SemVersionStyles.Strict); + + // Act + var result = migration.GetMigration(targetVersion, "Aspire.Hosting.NonExistentPackage"); + + // Assert + Assert.Null(result); + } + + [Fact] + public void GetMigration_WhenPackageIdDoesNotExist_ReturnsNull() + { + // Arrange + var migration = new PackageMigration(CreateTestLogger()); + var targetVersion = SemVersion.Parse("10.0.0", SemVersionStyles.Strict); + + // Act + var result = migration.GetMigration(targetVersion, "SomeOtherPackage.NotAspire"); + + // Assert + Assert.Null(result); + } + + [Fact] + public void GetMigration_WithEmptyPackageId_ReturnsNull() + { + // Arrange + var migration = new PackageMigration(CreateTestLogger()); + var targetVersion = SemVersion.Parse("10.0.0", SemVersionStyles.Strict); + + // Act + var result = migration.GetMigration(targetVersion, string.Empty); + + // Assert + Assert.Null(result); + } + + #endregion + + #region GetMigration - Version matching tests + + [Fact] + public void GetMigration_WithPrereleaseVersion_EvaluatesPredicateCorrectly() + { + // Arrange + var migration = new PackageMigration(CreateTestLogger()); + var prereleaseVersion = SemVersion.Parse("10.0.0-preview.1", SemVersionStyles.Strict); + + // Act - testing that prerelease versions are handled without throwing + var result = migration.GetMigration(prereleaseVersion, "Aspire.Hosting.SomePackage"); + + // Assert + Assert.Null(result); // No migration rules defined, so should be null + } + + [Fact] + public void GetMigration_WithDailyBuildVersion_EvaluatesPredicateCorrectly() + { + // Arrange + var migration = new PackageMigration(CreateTestLogger()); + var dailyVersion = SemVersion.Parse("10.0.0-daily.12345.1", SemVersionStyles.Strict); + + // Act + var result = migration.GetMigration(dailyVersion, "Aspire.Hosting.SomePackage"); + + // Assert + Assert.Null(result); // No migration rules defined, so should be null + } + + #endregion + + #region GetMigration - Case insensitivity tests + + [Fact] + public void GetMigration_PackageIdComparison_IsCaseInsensitive() + { + // Arrange - Using a testable migration service with predefined rules + var migration = new TestablePackageMigration(CreateTestLogger(), [ + (VersionAtLeast("10.0.0"), "Aspire.Hosting.OldPackage", "Aspire.Hosting.NewPackage") + ]); + var targetVersion = SemVersion.Parse("10.0.0", SemVersionStyles.Strict); + + // Act + var lowerCaseResult = migration.GetMigration(targetVersion, "aspire.hosting.oldpackage"); + var upperCaseResult = migration.GetMigration(targetVersion, "ASPIRE.HOSTING.OLDPACKAGE"); + var mixedCaseResult = migration.GetMigration(targetVersion, "Aspire.Hosting.OldPackage"); + + // Assert + Assert.Equal("Aspire.Hosting.NewPackage", lowerCaseResult); + Assert.Equal("Aspire.Hosting.NewPackage", upperCaseResult); + Assert.Equal("Aspire.Hosting.NewPackage", mixedCaseResult); + } + + #endregion + + #region Upward migration tests (stable -> daily/preview) + + [Fact] + public void GetMigration_UpwardMigration_WhenTargetVersionMeetsThreshold_ReturnsMigratedPackage() + { + // Arrange - Simulating an upgrade scenario + var migration = new TestablePackageMigration(CreateTestLogger(), [ + (VersionAtLeast("10.0.0"), "Aspire.Hosting.LegacyPackage", "Aspire.Hosting.ModernPackage") + ]); + var targetVersion = SemVersion.Parse("10.0.0", SemVersionStyles.Strict); + + // Act + var result = migration.GetMigration(targetVersion, "Aspire.Hosting.LegacyPackage"); + + // Assert + Assert.Equal("Aspire.Hosting.ModernPackage", result); + } + + [Fact] + public void GetMigration_UpwardMigration_WhenTargetVersionExceedsThreshold_ReturnsMigratedPackage() + { + // Arrange - Simulating an upgrade scenario with version beyond threshold + var migration = new TestablePackageMigration(CreateTestLogger(), [ + (VersionAtLeast("10.0.0"), "Aspire.Hosting.LegacyPackage", "Aspire.Hosting.ModernPackage") + ]); + var targetVersion = SemVersion.Parse("10.5.0", SemVersionStyles.Strict); + + // Act + var result = migration.GetMigration(targetVersion, "Aspire.Hosting.LegacyPackage"); + + // Assert + Assert.Equal("Aspire.Hosting.ModernPackage", result); + } + + [Fact] + public void GetMigration_UpwardMigration_WhenTargetVersionBelowThreshold_ReturnsNull() + { + // Arrange - Simulating scenario where version doesn't meet threshold + var migration = new TestablePackageMigration(CreateTestLogger(), [ + (VersionAtLeast("10.0.0"), "Aspire.Hosting.LegacyPackage", "Aspire.Hosting.ModernPackage") + ]); + var targetVersion = SemVersion.Parse("9.5.0", SemVersionStyles.Strict); + + // Act + var result = migration.GetMigration(targetVersion, "Aspire.Hosting.LegacyPackage"); + + // Assert + Assert.Null(result); + } + + #endregion + + #region Downward migration tests (daily/preview -> stable) + + [Fact] + public void GetMigration_DownwardMigration_WhenTargetVersionBelowThreshold_ReturnsMigratedPackage() + { + // Arrange - Simulating a downgrade scenario (daily -> stable) + var migration = new TestablePackageMigration(CreateTestLogger(), [ + (VersionBelow("10.0.0"), "Aspire.Hosting.NewPackage", "Aspire.Hosting.OldPackage") + ]); + var targetVersion = SemVersion.Parse("9.5.0", SemVersionStyles.Strict); + + // Act + var result = migration.GetMigration(targetVersion, "Aspire.Hosting.NewPackage"); + + // Assert + Assert.Equal("Aspire.Hosting.OldPackage", result); + } + + [Fact] + public void GetMigration_DownwardMigration_WhenTargetVersionMeetsThreshold_ReturnsNull() + { + // Arrange - Simulating scenario where version doesn't require downgrade migration + var migration = new TestablePackageMigration(CreateTestLogger(), [ + (VersionBelow("10.0.0"), "Aspire.Hosting.NewPackage", "Aspire.Hosting.OldPackage") + ]); + var targetVersion = SemVersion.Parse("10.0.0", SemVersionStyles.Strict); + + // Act + var result = migration.GetMigration(targetVersion, "Aspire.Hosting.NewPackage"); + + // Assert + Assert.Null(result); + } + + #endregion + + #region Bidirectional migration tests + + [Fact] + public void GetMigration_BidirectionalMigrations_UpwardMigrationWorks() + { + // Arrange - Setup bidirectional migration rules + var migration = new TestablePackageMigration(CreateTestLogger(), [ + // Upward migration: when upgrading to 10.0.0+, migrate OldPackage -> NewPackage + (VersionAtLeast("10.0.0"), "Aspire.Hosting.OldPackage", "Aspire.Hosting.NewPackage"), + // Downward migration: when downgrading to < 10.0.0, migrate NewPackage -> OldPackage + (VersionBelow("10.0.0"), "Aspire.Hosting.NewPackage", "Aspire.Hosting.OldPackage") + ]); + + // Act - Test upward migration + var upgradeResult = migration.GetMigration( + SemVersion.Parse("10.0.0", SemVersionStyles.Strict), + "Aspire.Hosting.OldPackage"); + + // Assert + Assert.Equal("Aspire.Hosting.NewPackage", upgradeResult); + } + + [Fact] + public void GetMigration_BidirectionalMigrations_DownwardMigrationWorks() + { + // Arrange - Setup bidirectional migration rules + var migration = new TestablePackageMigration(CreateTestLogger(), [ + // Upward migration + (VersionAtLeast("10.0.0"), "Aspire.Hosting.OldPackage", "Aspire.Hosting.NewPackage"), + // Downward migration + (VersionBelow("10.0.0"), "Aspire.Hosting.NewPackage", "Aspire.Hosting.OldPackage") + ]); + + // Act - Test downward migration + var downgradeResult = migration.GetMigration( + SemVersion.Parse("9.5.0", SemVersionStyles.Strict), + "Aspire.Hosting.NewPackage"); + + // Assert + Assert.Equal("Aspire.Hosting.OldPackage", downgradeResult); + } + + [Fact] + public void GetMigration_BidirectionalMigrations_SamePackageNoMigrationNeeded() + { + // Arrange - Setup bidirectional migration rules + var migration = new TestablePackageMigration(CreateTestLogger(), [ + // Upward migration + (VersionAtLeast("10.0.0"), "Aspire.Hosting.OldPackage", "Aspire.Hosting.NewPackage"), + // Downward migration + (VersionBelow("10.0.0"), "Aspire.Hosting.NewPackage", "Aspire.Hosting.OldPackage") + ]); + + // Act - Old package at old version - no migration needed + var oldPackageAtOldVersion = migration.GetMigration( + SemVersion.Parse("9.5.0", SemVersionStyles.Strict), + "Aspire.Hosting.OldPackage"); + + // Act - New package at new version - no migration needed + var newPackageAtNewVersion = migration.GetMigration( + SemVersion.Parse("10.0.0", SemVersionStyles.Strict), + "Aspire.Hosting.NewPackage"); + + // Assert + Assert.Null(oldPackageAtOldVersion); + Assert.Null(newPackageAtNewVersion); + } + + #endregion + + #region Multiple rule match error tests + + [Fact] + public void GetMigration_WhenMultipleRulesMatch_ThrowsPackageMigrationException() + { + // Arrange - Setup conflicting rules + var migration = new TestablePackageMigration(CreateTestLogger(), [ + (VersionAtLeast("10.0.0"), "Aspire.Hosting.SomePackage", "Aspire.Hosting.TargetA"), + (VersionAtLeast("9.5.0"), "Aspire.Hosting.SomePackage", "Aspire.Hosting.TargetB") + ]); + var targetVersion = SemVersion.Parse("10.0.0", SemVersionStyles.Strict); + + // Act & Assert + var exception = Assert.Throws(() => + migration.GetMigration(targetVersion, "Aspire.Hosting.SomePackage")); + + Assert.Contains("Multiple migration rules match", exception.Message); + Assert.Contains("Aspire.Hosting.SomePackage", exception.Message); + Assert.Contains("10.0.0", exception.Message); + } + + [Fact] + public void GetMigration_WhenMultipleRulesMatch_ExceptionIncludesTargetPackages() + { + // Arrange - Setup conflicting rules + var migration = new TestablePackageMigration(CreateTestLogger(), [ + (_ => true, "Aspire.Hosting.ConflictingPackage", "Aspire.Hosting.Target1"), + (_ => true, "Aspire.Hosting.ConflictingPackage", "Aspire.Hosting.Target2") + ]); + var targetVersion = SemVersion.Parse("10.0.0", SemVersionStyles.Strict); + + // Act & Assert + var exception = Assert.Throws(() => + migration.GetMigration(targetVersion, "Aspire.Hosting.ConflictingPackage")); + + Assert.Contains("Target1", exception.Message); + Assert.Contains("Target2", exception.Message); + } + + #endregion + + #region Complex version predicate tests + + [Fact] + public void GetMigration_WithVersionRangePredicate_MatchesCorrectly() + { + // Arrange - Setup rule that only applies to specific version range [10.0.0, 11.0.0) + var migration = new TestablePackageMigration(CreateTestLogger(), [ + (VersionInRange("10.0.0", "11.0.0"), "Aspire.Hosting.RangePackage", "Aspire.Hosting.RangeTarget") + ]); + + // Act + var belowRange = migration.GetMigration(SemVersion.Parse("9.5.0", SemVersionStyles.Strict), "Aspire.Hosting.RangePackage"); + var inRange = migration.GetMigration(SemVersion.Parse("10.5.0", SemVersionStyles.Strict), "Aspire.Hosting.RangePackage"); + var aboveRange = migration.GetMigration(SemVersion.Parse("11.0.0", SemVersionStyles.Strict), "Aspire.Hosting.RangePackage"); + + // Assert + Assert.Null(belowRange); + Assert.Equal("Aspire.Hosting.RangeTarget", inRange); + Assert.Null(aboveRange); + } + + [Fact] + public void GetMigration_WithPrereleaseVersionPredicate_MatchesCorrectly() + { + // Arrange - Setup rule that includes prerelease versions + var previewThreshold = SemVersion.Parse("10.0.0-preview.1", SemVersionStyles.Strict); + var migration = new TestablePackageMigration(CreateTestLogger(), [ + (v => SemVersion.ComparePrecedence(v, previewThreshold) >= 0, + "Aspire.Hosting.PreviewPackage", "Aspire.Hosting.PreviewTarget") + ]); + + // Act + var stableBelow = migration.GetMigration(SemVersion.Parse("9.5.0", SemVersionStyles.Strict), "Aspire.Hosting.PreviewPackage"); + var previewMatch = migration.GetMigration(SemVersion.Parse("10.0.0-preview.1", SemVersionStyles.Strict), "Aspire.Hosting.PreviewPackage"); + var previewAbove = migration.GetMigration(SemVersion.Parse("10.0.0-preview.5", SemVersionStyles.Strict), "Aspire.Hosting.PreviewPackage"); + var stableAbove = migration.GetMigration(SemVersion.Parse("10.0.0", SemVersionStyles.Strict), "Aspire.Hosting.PreviewPackage"); + + // Assert + Assert.Null(stableBelow); + Assert.Equal("Aspire.Hosting.PreviewTarget", previewMatch); + Assert.Equal("Aspire.Hosting.PreviewTarget", previewAbove); + Assert.Equal("Aspire.Hosting.PreviewTarget", stableAbove); + } + + #endregion + + #region Multiple packages migration tests + + [Fact] + public void GetMigration_WithMultiplePackageMigrations_EachPackageResolvesCorrectly() + { + // Arrange - Setup rules for multiple packages + var migration = new TestablePackageMigration(CreateTestLogger(), [ + (VersionAtLeast("10.0.0"), "Aspire.Hosting.PackageA", "Aspire.Hosting.NewPackageA"), + (VersionAtLeast("10.0.0"), "Aspire.Hosting.PackageB", "Aspire.Hosting.NewPackageB"), + (VersionAtLeast("10.0.0"), "Aspire.Hosting.PackageC", "Aspire.Hosting.NewPackageC") + ]); + var targetVersion = SemVersion.Parse("10.0.0", SemVersionStyles.Strict); + + // Act + var resultA = migration.GetMigration(targetVersion, "Aspire.Hosting.PackageA"); + var resultB = migration.GetMigration(targetVersion, "Aspire.Hosting.PackageB"); + var resultC = migration.GetMigration(targetVersion, "Aspire.Hosting.PackageC"); + var resultUnknown = migration.GetMigration(targetVersion, "Aspire.Hosting.PackageD"); + + // Assert + Assert.Equal("Aspire.Hosting.NewPackageA", resultA); + Assert.Equal("Aspire.Hosting.NewPackageB", resultB); + Assert.Equal("Aspire.Hosting.NewPackageC", resultC); + Assert.Null(resultUnknown); + } + + #endregion + + #region Helper class for testing with custom rules + + /// + /// A testable version of PackageMigration that allows injecting custom migration rules for testing. + /// + private sealed class TestablePackageMigration : IPackageMigration + { + private readonly ILogger _logger; + private readonly List<(Func VersionPredicate, string FromPackageId, string ToPackageId)> _migrationRules; + + public TestablePackageMigration( + ILogger logger, + List<(Func VersionPredicate, string FromPackageId, string ToPackageId)> migrationRules) + { + _logger = logger; + _migrationRules = migrationRules; + } + + public string? GetMigration(SemVersion targetHostingVersion, string packageId) + { + _logger.LogDebug("Checking migration rules for package '{PackageId}' targeting version '{TargetVersion}'", packageId, targetHostingVersion); + + var matchingRules = _migrationRules + .Where(rule => string.Equals(rule.FromPackageId, packageId, StringComparison.OrdinalIgnoreCase) + && rule.VersionPredicate(targetHostingVersion)) + .ToList(); + + if (matchingRules.Count == 0) + { + _logger.LogDebug("No migration rules found for package '{PackageId}'", packageId); + return null; + } + + if (matchingRules.Count > 1) + { + var toPackages = string.Join(", ", matchingRules.Select(r => r.ToPackageId)); + _logger.LogError( + "Multiple migration rules found for package '{PackageId}' at version '{TargetVersion}'. Target packages: {ToPackages}", + packageId, targetHostingVersion, toPackages); + + throw new PackageMigrationException( + $"Multiple migration rules match for package '{packageId}' at version '{targetHostingVersion}'. " + + $"This is a configuration error. Matching target packages: {toPackages}"); + } + + var matchingRule = matchingRules[0]; + _logger.LogInformation( + "Migration rule found: '{FromPackageId}' -> '{ToPackageId}' for target version '{TargetVersion}'", + matchingRule.FromPackageId, matchingRule.ToPackageId, targetHostingVersion); + + return matchingRule.ToPackageId; + } + } + + #endregion +} diff --git a/tests/Aspire.Cli.Tests/Utils/CliTestHelper.cs b/tests/Aspire.Cli.Tests/Utils/CliTestHelper.cs index 554d9b59a58..51efb112188 100644 --- a/tests/Aspire.Cli.Tests/Utils/CliTestHelper.cs +++ b/tests/Aspire.Cli.Tests/Utils/CliTestHelper.cs @@ -89,6 +89,7 @@ public static IServiceCollection CreateServiceCollection(TemporaryWorkspace work services.AddSingleton(options.CliUpdateNotifierFactory); services.AddSingleton(options.DotNetSdkInstallerFactory); services.AddSingleton(options.PackagingServiceFactory); + services.AddSingleton(options.PackageMigrationFactory); services.AddSingleton(options.CliExecutionContextFactory); services.AddSingleton(options.DiskCacheFactory); services.AddSingleton(options.CliHostEnvironmentFactory); @@ -340,6 +341,12 @@ public ISolutionLocator CreateDefaultSolutionLocatorFactory(IServiceProvider ser return new PackagingService(executionContext, nuGetPackageCache, features, configuration); }; + public Func PackageMigrationFactory { get; set; } = (IServiceProvider serviceProvider) => + { + var logger = serviceProvider.GetRequiredService>(); + return new PackageMigration(logger); + }; + public Func DiskCacheFactory { get; set; } = (IServiceProvider serviceProvider) => new NullDiskCache(); public Func CliDownloaderFactory { get; set; } = (IServiceProvider serviceProvider) => From 1021c829f096527fe365a71583048bf40cf923a2 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 30 Nov 2025 22:57:06 +0000 Subject: [PATCH 3/6] Integrate IPackageMigration into ProjectUpdater with migration support Co-authored-by: mitchdenny <513398+mitchdenny@users.noreply.github.com> --- src/Aspire.Cli/DotNet/DotNetCliRunner.cs | 48 +++++ src/Aspire.Cli/Projects/ProjectUpdater.cs | 180 +++++++++++++++++- .../UpdateCommandStrings.Designer.cs | 2 + .../Resources/UpdateCommandStrings.resx | 6 + .../Resources/xlf/UpdateCommandStrings.cs.xlf | 10 + .../Resources/xlf/UpdateCommandStrings.de.xlf | 10 + .../Resources/xlf/UpdateCommandStrings.es.xlf | 10 + .../Resources/xlf/UpdateCommandStrings.fr.xlf | 10 + .../Resources/xlf/UpdateCommandStrings.it.xlf | 10 + .../Resources/xlf/UpdateCommandStrings.ja.xlf | 10 + .../Resources/xlf/UpdateCommandStrings.ko.xlf | 10 + .../Resources/xlf/UpdateCommandStrings.pl.xlf | 10 + .../xlf/UpdateCommandStrings.pt-BR.xlf | 10 + .../Resources/xlf/UpdateCommandStrings.ru.xlf | 10 + .../Resources/xlf/UpdateCommandStrings.tr.xlf | 10 + .../xlf/UpdateCommandStrings.zh-Hans.xlf | 10 + .../xlf/UpdateCommandStrings.zh-Hant.xlf | 10 + .../Projects/ProjectUpdaterTests.cs | 14 +- .../Templating/DotNetTemplateFactoryTests.cs | 3 + .../TestServices/TestDotNetCliRunner.cs | 8 + tests/Aspire.Cli.Tests/Utils/CliTestHelper.cs | 3 +- 21 files changed, 385 insertions(+), 9 deletions(-) diff --git a/src/Aspire.Cli/DotNet/DotNetCliRunner.cs b/src/Aspire.Cli/DotNet/DotNetCliRunner.cs index 9cad830834c..0aa3b02fb95 100644 --- a/src/Aspire.Cli/DotNet/DotNetCliRunner.cs +++ b/src/Aspire.Cli/DotNet/DotNetCliRunner.cs @@ -36,6 +36,7 @@ internal interface IDotNetCliRunner Task NewProjectAsync(string templateName, string name, string outputPath, string[] extraArgs, DotNetCliRunnerInvocationOptions options, CancellationToken cancellationToken); Task BuildAsync(FileInfo projectFilePath, DotNetCliRunnerInvocationOptions options, CancellationToken cancellationToken); Task AddPackageAsync(FileInfo projectFilePath, string packageName, string packageVersion, string? nugetSource, DotNetCliRunnerInvocationOptions options, CancellationToken cancellationToken); + Task RemovePackageAsync(FileInfo projectFilePath, string packageName, DotNetCliRunnerInvocationOptions options, CancellationToken cancellationToken); Task AddProjectToSolutionAsync(FileInfo solutionFile, FileInfo projectFile, DotNetCliRunnerInvocationOptions options, CancellationToken cancellationToken); Task<(int ExitCode, NuGetPackage[]? Packages)> SearchPackagesAsync(DirectoryInfo workingDirectory, string query, bool prerelease, int take, int skip, FileInfo? nugetConfigFile, bool useCache, DotNetCliRunnerInvocationOptions options, CancellationToken cancellationToken); Task<(int ExitCode, string[] ConfigPaths)> GetNuGetConfigPathsAsync(DirectoryInfo workingDirectory, DotNetCliRunnerInvocationOptions options, CancellationToken cancellationToken); @@ -831,6 +832,53 @@ public async Task AddPackageAsync(FileInfo projectFilePath, string packageN return result; } + public async Task RemovePackageAsync(FileInfo projectFilePath, string packageName, DotNetCliRunnerInvocationOptions options, CancellationToken cancellationToken) + { + using var activity = telemetry.ActivitySource.StartActivity(); + + var cliArgsList = new List + { + "remove" + }; + + // For single-file AppHost (apphost.cs), use --file switch instead of positional argument + var isSingleFileAppHost = projectFilePath.Name.Equals("apphost.cs", StringComparison.OrdinalIgnoreCase); + if (isSingleFileAppHost) + { + cliArgsList.AddRange(["package", "--file", projectFilePath.FullName]); + cliArgsList.Add(packageName); + } + else + { + cliArgsList.AddRange([projectFilePath.FullName, "package"]); + cliArgsList.Add(packageName); + } + + string[] cliArgs = [.. cliArgsList]; + + logger.LogInformation("Removing package {PackageName} from project {ProjectFilePath}", packageName, projectFilePath.FullName); + + var result = await ExecuteAsync( + args: cliArgs, + env: null, + projectFile: projectFilePath, + workingDirectory: projectFilePath.Directory!, + backchannelCompletionSource: null, + options: options, + cancellationToken: cancellationToken); + + if (result != 0) + { + logger.LogError("Failed to remove package {PackageName} from project {ProjectFilePath}. See debug logs for more details.", packageName, projectFilePath.FullName); + } + else + { + logger.LogInformation("Package {PackageName} removed from project {ProjectFilePath}", packageName, projectFilePath.FullName); + } + + return result; + } + public async Task AddProjectToSolutionAsync(FileInfo solutionFile, FileInfo projectFile, DotNetCliRunnerInvocationOptions options, CancellationToken cancellationToken) { using var activity = telemetry.ActivitySource.StartActivity(); diff --git a/src/Aspire.Cli/Projects/ProjectUpdater.cs b/src/Aspire.Cli/Projects/ProjectUpdater.cs index 12622e530fe..69fd4704e87 100644 --- a/src/Aspire.Cli/Projects/ProjectUpdater.cs +++ b/src/Aspire.Cli/Projects/ProjectUpdater.cs @@ -23,7 +23,7 @@ internal interface IProjectUpdater Task UpdateProjectAsync(FileInfo projectFile, PackageChannel channel, CancellationToken cancellationToken = default); } -internal sealed partial class ProjectUpdater(ILogger logger, IDotNetCliRunner runner, IInteractionService interactionService, IMemoryCache cache, CliExecutionContext executionContext, FallbackProjectParser fallbackParser) : IProjectUpdater +internal sealed partial class ProjectUpdater(ILogger logger, IDotNetCliRunner runner, IInteractionService interactionService, IMemoryCache cache, CliExecutionContext executionContext, FallbackProjectParser fallbackParser, IPackageMigration packageMigration) : IProjectUpdater { public async Task UpdateProjectAsync(FileInfo projectFile, PackageChannel channel, CancellationToken cancellationToken = default) { @@ -258,6 +258,13 @@ private async Task AnalyzeAppHostSdkAsync(UpdateContext context, CancellationTok var latestSdkPackage = await GetLatestVersionOfPackageAsync(context, "Aspire.AppHost.Sdk", cancellationToken); + // Set the target hosting version in the context for package migration decisions + if (latestSdkPackage is not null && SemVersion.TryParse(latestSdkPackage.Version, SemVersionStyles.Strict, out var targetVersion)) + { + context.TargetHostingVersion = targetVersion; + logger.LogDebug("Target hosting version set to: {TargetVersion}", targetVersion); + } + // Treat unparseable versions (including range expressions) like wildcards - always update them // Only skip if the version is a valid semantic version that matches the latest if (!string.IsNullOrEmpty(sdkVersion) && IsValidSemanticVersion(sdkVersion) && sdkVersion == latestSdkPackage?.Version) @@ -461,6 +468,22 @@ private static CentralPackageManagementInfo DetectCentralPackageManagement(FileI private async Task AnalyzePackageForTraditionalManagementAsync(string packageId, string packageVersion, FileInfo projectFile, UpdateContext context, CancellationToken cancellationToken) { + // Check if this package needs migration to a different package + if (context.TargetHostingVersion is not null) + { + var migrationTarget = packageMigration.GetMigration(context.TargetHostingVersion, packageId); + if (migrationTarget is not null) + { + logger.LogInformation("Migration detected: '{FromPackage}' -> '{ToPackage}' for target version '{TargetVersion}'", + packageId, migrationTarget, context.TargetHostingVersion); + + // Add steps to remove old package and add new package + await AddMigrationStepsForTraditionalManagementAsync( + packageId, packageVersion, migrationTarget, projectFile, context, cancellationToken); + return; + } + } + var latestPackage = await GetLatestVersionOfPackageAsync(context, packageId, cancellationToken); // Treat unparseable versions (including range expressions) like wildcards - always update them @@ -481,6 +504,32 @@ private async Task AnalyzePackageForTraditionalManagementAsync(string packageId, context.UpdateSteps.Enqueue(updateStep); } + private async Task AddMigrationStepsForTraditionalManagementAsync( + string fromPackageId, string fromVersion, string toPackageId, + FileInfo projectFile, UpdateContext context, CancellationToken cancellationToken) + { + // Get the latest version of the target package + var latestTargetPackage = await GetLatestVersionOfPackageAsync(context, toPackageId, cancellationToken); + + // Step 1: Remove the old package + var removeStep = new PackageMigrationRemoveStep( + string.Format(CultureInfo.InvariantCulture, UpdateCommandStrings.RemovePackageFormat, fromPackageId), + () => RemovePackageFromProject(projectFile, fromPackageId, cancellationToken), + fromPackageId, + fromVersion, + projectFile); + context.UpdateSteps.Enqueue(removeStep); + + // Step 2: Add the new package + var addStep = new PackageMigrationAddStep( + string.Format(CultureInfo.InvariantCulture, UpdateCommandStrings.AddPackageFormat, toPackageId, latestTargetPackage!.Version), + () => UpdatePackageReferenceInProject(projectFile, latestTargetPackage, cancellationToken), + toPackageId, + latestTargetPackage!.Version, + projectFile); + context.UpdateSteps.Enqueue(addStep); + } + private async Task AnalyzePackageForCentralPackageManagementAsync(string packageId, FileInfo projectFile, FileInfo directoryPackagesPropsFile, UpdateContext context, CancellationToken cancellationToken) { var currentVersion = await GetPackageVersionFromDirectoryPackagesPropsAsync(packageId, directoryPackagesPropsFile, projectFile, cancellationToken); @@ -491,6 +540,22 @@ private async Task AnalyzePackageForCentralPackageManagementAsync(string package return; } + // Check if this package needs migration to a different package + if (context.TargetHostingVersion is not null) + { + var migrationTarget = packageMigration.GetMigration(context.TargetHostingVersion, packageId); + if (migrationTarget is not null) + { + logger.LogInformation("Migration detected: '{FromPackage}' -> '{ToPackage}' for target version '{TargetVersion}'", + packageId, migrationTarget, context.TargetHostingVersion); + + // Add steps to remove old package and add new package + await AddMigrationStepsForCentralPackageManagementAsync( + packageId, currentVersion, migrationTarget, projectFile, directoryPackagesPropsFile, context, cancellationToken); + return; + } + } + var latestPackage = await GetLatestVersionOfPackageAsync(context, packageId, cancellationToken); // Treat unparseable versions (including range expressions) like wildcards - always update them @@ -511,6 +576,82 @@ private async Task AnalyzePackageForCentralPackageManagementAsync(string package context.UpdateSteps.Enqueue(updateStep); } + private async Task AddMigrationStepsForCentralPackageManagementAsync( + string fromPackageId, string fromVersion, string toPackageId, + FileInfo projectFile, FileInfo directoryPackagesPropsFile, UpdateContext context, CancellationToken cancellationToken) + { + // Get the latest version of the target package + var latestTargetPackage = await GetLatestVersionOfPackageAsync(context, toPackageId, cancellationToken); + + // Step 1: Remove the old package from Directory.Packages.props + var removeStep = new PackageMigrationRemoveStep( + string.Format(CultureInfo.InvariantCulture, UpdateCommandStrings.RemovePackageFormat, fromPackageId), + () => RemovePackageFromDirectoryPackagesProps(fromPackageId, directoryPackagesPropsFile), + fromPackageId, + fromVersion, + projectFile); + context.UpdateSteps.Enqueue(removeStep); + + // Step 2: Add the new package to Directory.Packages.props + var addStep = new PackageMigrationAddStep( + string.Format(CultureInfo.InvariantCulture, UpdateCommandStrings.AddPackageFormat, toPackageId, latestTargetPackage!.Version), + () => AddPackageToDirectoryPackagesProps(toPackageId, latestTargetPackage!.Version, directoryPackagesPropsFile), + toPackageId, + latestTargetPackage!.Version, + projectFile); + context.UpdateSteps.Enqueue(addStep); + } + + private async Task RemovePackageFromProject(FileInfo projectFile, string packageId, CancellationToken cancellationToken) + { + return await runner.RemovePackageAsync(projectFile, packageId, new(), cancellationToken); + } + + private static Task RemovePackageFromDirectoryPackagesProps(string packageId, FileInfo directoryPackagesPropsFile) + { + var doc = new XmlDocument { PreserveWhitespace = true }; + doc.Load(directoryPackagesPropsFile.FullName); + + var packageVersionNode = doc.SelectSingleNode($"/Project/ItemGroup/PackageVersion[@Include='{packageId}']"); + if (packageVersionNode?.ParentNode is not null) + { + packageVersionNode.ParentNode.RemoveChild(packageVersionNode); + doc.Save(directoryPackagesPropsFile.FullName); + } + + return Task.CompletedTask; + } + + private static Task AddPackageToDirectoryPackagesProps(string packageId, string version, FileInfo directoryPackagesPropsFile) + { + var doc = new XmlDocument { PreserveWhitespace = true }; + doc.Load(directoryPackagesPropsFile.FullName); + + // Find the ItemGroup that contains PackageVersion elements + var itemGroup = doc.SelectSingleNode("/Project/ItemGroup[PackageVersion]"); + if (itemGroup is null) + { + // Create a new ItemGroup if one doesn't exist + var project = doc.SelectSingleNode("/Project"); + if (project is null) + { + return Task.CompletedTask; + } + + itemGroup = doc.CreateElement("ItemGroup"); + project.AppendChild(itemGroup); + } + + // Create the new PackageVersion element + var packageVersionElement = doc.CreateElement("PackageVersion"); + packageVersionElement.SetAttribute("Include", packageId); + packageVersionElement.SetAttribute("Version", version); + itemGroup.AppendChild(packageVersionElement); + + doc.Save(directoryPackagesPropsFile.FullName); + return Task.CompletedTask; + } + private async Task GetPackageVersionFromDirectoryPackagesPropsAsync(string packageId, FileInfo directoryPackagesPropsFile, FileInfo projectFile, CancellationToken cancellationToken) { try @@ -884,6 +1025,11 @@ internal sealed class UpdateContext(FileInfo appHostProjectFile, PackageChannel public ConcurrentQueue AnalyzeSteps { get; } = new(); public HashSet VisitedProjects { get; } = new(); public bool FallbackParsing { get; set; } + /// + /// The target Aspire Hosting SDK version being updated to. + /// This is set during SDK analysis and used for package migration decisions. + /// + public SemVersion? TargetHostingVersion { get; set; } } internal abstract record UpdateStep(string Description, Func Callback) @@ -911,6 +1057,38 @@ public override string GetFormattedDisplayText() } } +/// +/// Represents a migration step to remove an old package during package migration. +/// +internal record PackageMigrationRemoveStep( + string Description, + Func Callback, + string PackageId, + string Version, + FileInfo ProjectFile) : UpdateStep(Description, Callback) +{ + public override string GetFormattedDisplayText() + { + return $"[bold red]Remove[/] [bold yellow]{PackageId}[/] [bold green]{Version.EscapeMarkup()}[/]"; + } +} + +/// +/// Represents a migration step to add a new package during package migration. +/// +internal record PackageMigrationAddStep( + string Description, + Func Callback, + string PackageId, + string Version, + FileInfo ProjectFile) : UpdateStep(Description, Callback) +{ + public override string GetFormattedDisplayText() + { + return $"[bold green]Add[/] [bold yellow]{PackageId}[/] [bold green]{Version.EscapeMarkup()}[/]"; + } +} + internal record AnalyzeStep(string Description, Func Callback); internal sealed class ProjectUpdaterException : System.Exception diff --git a/src/Aspire.Cli/Resources/UpdateCommandStrings.Designer.cs b/src/Aspire.Cli/Resources/UpdateCommandStrings.Designer.cs index 11324520d48..846e052a8f9 100644 --- a/src/Aspire.Cli/Resources/UpdateCommandStrings.Designer.cs +++ b/src/Aspire.Cli/Resources/UpdateCommandStrings.Designer.cs @@ -106,5 +106,7 @@ internal static string ProjectArgumentDescription { internal static string QualityOptionDescription => ResourceManager.GetString("QualityOptionDescription", resourceCulture); internal static string QualityOptionDescriptionWithStaging => ResourceManager.GetString("QualityOptionDescriptionWithStaging", resourceCulture); internal static string DotNetToolSelfUpdateMessage => ResourceManager.GetString("DotNetToolSelfUpdateMessage", resourceCulture); + internal static string RemovePackageFormat => ResourceManager.GetString("RemovePackageFormat", resourceCulture); + internal static string AddPackageFormat => ResourceManager.GetString("AddPackageFormat", resourceCulture); } } diff --git a/src/Aspire.Cli/Resources/UpdateCommandStrings.resx b/src/Aspire.Cli/Resources/UpdateCommandStrings.resx index 2b40fb44d18..e149a9a9760 100644 --- a/src/Aspire.Cli/Resources/UpdateCommandStrings.resx +++ b/src/Aspire.Cli/Resources/UpdateCommandStrings.resx @@ -135,4 +135,10 @@ To update the Aspire CLI when installed as a .NET tool, run: + + Remove package {0} + + + Add package {0} version {1} + diff --git a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.cs.xlf b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.cs.xlf index 98a9616bf3e..7523d82da56 100644 --- a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.cs.xlf +++ b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.cs.xlf @@ -2,6 +2,11 @@ + + Add package {0} version {1} + Add package {0} version {1} + + Added: {0} Přidáno: {0} @@ -167,6 +172,11 @@ Úroveň kvality, na kterou se má aktualizovat (stabilní, příprava, denní) + + Remove package {0} + Remove package {0} + + Removed: {0} Odebráno: {0} diff --git a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.de.xlf b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.de.xlf index 2908cd3090b..dde9fec97c4 100644 --- a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.de.xlf +++ b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.de.xlf @@ -2,6 +2,11 @@ + + Add package {0} version {1} + Add package {0} version {1} + + Added: {0} Hinzugefügt: {0} @@ -167,6 +172,11 @@ Qualitätsstufe, auf die aktualisiert werden soll (stable, staging, daily) + + Remove package {0} + Remove package {0} + + Removed: {0} Entfernt: {0} diff --git a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.es.xlf b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.es.xlf index 4412cf5c975..b79a560b8c3 100644 --- a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.es.xlf +++ b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.es.xlf @@ -2,6 +2,11 @@ + + Add package {0} version {1} + Add package {0} version {1} + + Added: {0} Agregado: {0} @@ -167,6 +172,11 @@ Nivel de calidad al que se va a actualizar (estable, de ensayo, diario) + + Remove package {0} + Remove package {0} + + Removed: {0} Quitado: {0} diff --git a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.fr.xlf b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.fr.xlf index 333130a4982..46f66f7d4b8 100644 --- a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.fr.xlf +++ b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.fr.xlf @@ -2,6 +2,11 @@ + + Add package {0} version {1} + Add package {0} version {1} + + Added: {0} Ajouté : {0} @@ -167,6 +172,11 @@ Niveau de qualité vers lequel effectuer une mise à jour (stable, gestion intermédiaire, quotidien) + + Remove package {0} + Remove package {0} + + Removed: {0} Supprimé : {0} diff --git a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.it.xlf b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.it.xlf index ce822b23102..8a275d53bfd 100644 --- a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.it.xlf +++ b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.it.xlf @@ -2,6 +2,11 @@ + + Add package {0} version {1} + Add package {0} version {1} + + Added: {0} Aggiunto: {0} @@ -167,6 +172,11 @@ Livello di qualità da aggiornare a (stabile, staging, giornaliero) + + Remove package {0} + Remove package {0} + + Removed: {0} Rimosso: {0} diff --git a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.ja.xlf b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.ja.xlf index fad84c2aeca..b48f8a2fa55 100644 --- a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.ja.xlf +++ b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.ja.xlf @@ -2,6 +2,11 @@ + + Add package {0} version {1} + Add package {0} version {1} + + Added: {0} 追加済み: {0} @@ -167,6 +172,11 @@ 更新先の品質レベル (安定、ステージング、毎日) + + Remove package {0} + Remove package {0} + + Removed: {0} 削除済み: {0} diff --git a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.ko.xlf b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.ko.xlf index f13f9893e70..bd4800032e0 100644 --- a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.ko.xlf +++ b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.ko.xlf @@ -2,6 +2,11 @@ + + Add package {0} version {1} + Add package {0} version {1} + + Added: {0} 추가됨: {0} @@ -167,6 +172,11 @@ 업데이트할 품질 수준(안정, 스테이징, 데일리) + + Remove package {0} + Remove package {0} + + Removed: {0} 제거됨: {0} diff --git a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.pl.xlf b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.pl.xlf index 62293be966c..05ec2b8aa48 100644 --- a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.pl.xlf +++ b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.pl.xlf @@ -2,6 +2,11 @@ + + Add package {0} version {1} + Add package {0} version {1} + + Added: {0} Dodano: {0} @@ -167,6 +172,11 @@ Poziom jakości, do którego należy zaktualizować (stabilny, przejściowy, dzienny) + + Remove package {0} + Remove package {0} + + Removed: {0} Usunięto: {0} diff --git a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.pt-BR.xlf b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.pt-BR.xlf index bfc7e7358b7..0c636efb5fb 100644 --- a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.pt-BR.xlf +++ b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.pt-BR.xlf @@ -2,6 +2,11 @@ + + Add package {0} version {1} + Add package {0} version {1} + + Added: {0} Adicionado: {0} @@ -167,6 +172,11 @@ Nível de qualidade para o qual atualizar (estável, preparo, diariamente) + + Remove package {0} + Remove package {0} + + Removed: {0} Removido: {0} diff --git a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.ru.xlf b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.ru.xlf index 0b96df9baee..9c59127fcf9 100644 --- a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.ru.xlf +++ b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.ru.xlf @@ -2,6 +2,11 @@ + + Add package {0} version {1} + Add package {0} version {1} + + Added: {0} Добавлено: {0} @@ -167,6 +172,11 @@ Уровень качества для обновления (стабильный, промежуточный, ежедневный) + + Remove package {0} + Remove package {0} + + Removed: {0} Удалено: {0} diff --git a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.tr.xlf b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.tr.xlf index 6c9851d5974..ab16c446812 100644 --- a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.tr.xlf +++ b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.tr.xlf @@ -2,6 +2,11 @@ + + Add package {0} version {1} + Add package {0} version {1} + + Added: {0} Eklendi: {0} @@ -167,6 +172,11 @@ Güncelleştirilecek kalite seviyesi (kararlı, hazırlama, günlük) + + Remove package {0} + Remove package {0} + + Removed: {0} Kaldırıldı: {0} diff --git a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.zh-Hans.xlf b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.zh-Hans.xlf index b29ce7b9284..e6c4082fe3a 100644 --- a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.zh-Hans.xlf +++ b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.zh-Hans.xlf @@ -2,6 +2,11 @@ + + Add package {0} version {1} + Add package {0} version {1} + + Added: {0} 已添加: {0} @@ -167,6 +172,11 @@ 要更新到的质量级别(稳定、暂存、每日) + + Remove package {0} + Remove package {0} + + Removed: {0} 已移除: {0} diff --git a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.zh-Hant.xlf b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.zh-Hant.xlf index 2be7eaa110a..8d5faa95493 100644 --- a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.zh-Hant.xlf +++ b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.zh-Hant.xlf @@ -2,6 +2,11 @@ + + Add package {0} version {1} + Add package {0} version {1} + + Added: {0} 已新增: {0} @@ -167,6 +172,11 @@ 要更新的品質等級 (穩定、暫存、每日) + + Remove package {0} + Remove package {0} + + Removed: {0} 已移除: {0} diff --git a/tests/Aspire.Cli.Tests/Projects/ProjectUpdaterTests.cs b/tests/Aspire.Cli.Tests/Projects/ProjectUpdaterTests.cs index 3e40fb823ee..3596aeb6987 100644 --- a/tests/Aspire.Cli.Tests/Projects/ProjectUpdaterTests.cs +++ b/tests/Aspire.Cli.Tests/Projects/ProjectUpdaterTests.cs @@ -1476,7 +1476,7 @@ await File.WriteAllTextAsync( var channels = await packagingService.GetChannelsAsync(); var selectedChannel = channels.Single(c => c.Name == "default"); - var projectUpdater = new ProjectUpdater(logger, runner, interactionService, cache, executionContext, fallbackParser); + var projectUpdater = new ProjectUpdater(logger, runner, interactionService, cache, executionContext, fallbackParser, new PackageMigration(Microsoft.Extensions.Logging.Abstractions.NullLogger.Instance)); var updateResult = await projectUpdater.UpdateProjectAsync(appHostProjectFile, selectedChannel).WaitAsync(CliTestConstants.DefaultTimeout); // Should not throw ProjectUpdaterException; should produce update steps including AppHost SDK @@ -1578,7 +1578,7 @@ await File.WriteAllTextAsync( var channels = await packagingService.GetChannelsAsync(); var selectedChannel = channels.Single(c => c.Name == "default"); - var projectUpdater = new ProjectUpdater(logger, runner, interactionService, cache, executionContext, fallbackParser); + var projectUpdater = new ProjectUpdater(logger, runner, interactionService, cache, executionContext, fallbackParser, new PackageMigration(Microsoft.Extensions.Logging.Abstractions.NullLogger.Instance)); var updateResult = await projectUpdater.UpdateProjectAsync(appHostProjectFile, selectedChannel).WaitAsync(CliTestConstants.DefaultTimeout); // Should discover package reference (version may be absent) and not crash @@ -1657,7 +1657,7 @@ await File.WriteAllTextAsync( var channels = await packagingService.GetChannelsAsync(); var selectedChannel = channels.Single(c => c.Name == "default"); - var projectUpdater = new ProjectUpdater(logger, runner, interactionService, cache, executionContext, fallbackParser); + var projectUpdater = new ProjectUpdater(logger, runner, interactionService, cache, executionContext, fallbackParser, new PackageMigration(Microsoft.Extensions.Logging.Abstractions.NullLogger.Instance)); // Should throw ProjectUpdaterException due to invalid XML await Assert.ThrowsAsync(() => @@ -1739,7 +1739,7 @@ await File.WriteAllTextAsync( var channels = await packagingService.GetChannelsAsync(); var selectedChannel = channels.Single(c => c.Name == "default"); - var projectUpdater = new ProjectUpdater(logger, runner, interactionService, cache, executionContext, fallbackParser); + var projectUpdater = new ProjectUpdater(logger, runner, interactionService, cache, executionContext, fallbackParser, new PackageMigration(Microsoft.Extensions.Logging.Abstractions.NullLogger.Instance)); var updateResult = await projectUpdater.UpdateProjectAsync(appHostProjectFile, selectedChannel).WaitAsync(CliTestConstants.DefaultTimeout); // Normal path unaffected - no updates needed since version is already current @@ -1818,7 +1818,7 @@ await File.WriteAllTextAsync( var channels = await packagingService.GetChannelsAsync(); var selectedChannel = channels.Single(c => c.Name == "default"); - var projectUpdater = new ProjectUpdater(logger, runner, interactionService, cache, executionContext, fallbackParser); + var projectUpdater = new ProjectUpdater(logger, runner, interactionService, cache, executionContext, fallbackParser, new PackageMigration(Microsoft.Extensions.Logging.Abstractions.NullLogger.Instance)); var updateResult = await projectUpdater.UpdateProjectAsync(appHostFile, selectedChannel).WaitAsync(CliTestConstants.DefaultTimeout); Assert.True(updateResult.UpdatedApplied); @@ -1901,7 +1901,7 @@ await File.WriteAllTextAsync( var channels = await packagingService.GetChannelsAsync(); var selectedChannel = channels.Single(c => c.Name == "default"); - var projectUpdater = new ProjectUpdater(logger, runner, interactionService, cache, executionContext, fallbackParser); + var projectUpdater = new ProjectUpdater(logger, runner, interactionService, cache, executionContext, fallbackParser, new PackageMigration(Microsoft.Extensions.Logging.Abstractions.NullLogger.Instance)); var updateResult = await projectUpdater.UpdateProjectAsync(appHostFile, selectedChannel).WaitAsync(CliTestConstants.DefaultTimeout); Assert.True(updateResult.UpdatedApplied); @@ -1989,7 +1989,7 @@ await File.WriteAllTextAsync( var channels = await packagingService.GetChannelsAsync(); var selectedChannel = channels.Single(c => c.Name == "default"); - var projectUpdater = new ProjectUpdater(logger, runner, interactionService, cache, executionContext, fallbackParser); + var projectUpdater = new ProjectUpdater(logger, runner, interactionService, cache, executionContext, fallbackParser, new PackageMigration(Microsoft.Extensions.Logging.Abstractions.NullLogger.Instance)); // This should not throw and should handle the * version gracefully var updateResult = await projectUpdater.UpdateProjectAsync(appHostProjectFile, selectedChannel).WaitAsync(CliTestConstants.DefaultTimeout); diff --git a/tests/Aspire.Cli.Tests/Templating/DotNetTemplateFactoryTests.cs b/tests/Aspire.Cli.Tests/Templating/DotNetTemplateFactoryTests.cs index f8a2cfe33f1..77469fc387e 100644 --- a/tests/Aspire.Cli.Tests/Templating/DotNetTemplateFactoryTests.cs +++ b/tests/Aspire.Cli.Tests/Templating/DotNetTemplateFactoryTests.cs @@ -414,6 +414,9 @@ public Task BuildAsync(FileInfo projectFile, DotNetCliRunnerInvocationOptio public Task AddPackageAsync(FileInfo projectFile, string packageName, string version, string? packageSourceUrl, DotNetCliRunnerInvocationOptions options, CancellationToken cancellationToken) => throw new NotImplementedException(); + public Task RemovePackageAsync(FileInfo projectFile, string packageName, DotNetCliRunnerInvocationOptions options, CancellationToken cancellationToken) + => throw new NotImplementedException(); + public Task AddProjectToSolutionAsync(FileInfo solutionFile, FileInfo projectFile, DotNetCliRunnerInvocationOptions options, CancellationToken cancellationToken) => throw new NotImplementedException(); diff --git a/tests/Aspire.Cli.Tests/TestServices/TestDotNetCliRunner.cs b/tests/Aspire.Cli.Tests/TestServices/TestDotNetCliRunner.cs index 0d4a63f112a..b68f8ee07ef 100644 --- a/tests/Aspire.Cli.Tests/TestServices/TestDotNetCliRunner.cs +++ b/tests/Aspire.Cli.Tests/TestServices/TestDotNetCliRunner.cs @@ -12,6 +12,7 @@ namespace Aspire.Cli.Tests.TestServices; internal sealed class TestDotNetCliRunner : IDotNetCliRunner { public Func? AddPackageAsyncCallback { get; set; } + public Func? RemovePackageAsyncCallback { get; set; } public Func? AddProjectToSolutionAsyncCallback { get; set; } public Func? BuildAsyncCallback { get; set; } public Func? CheckHttpCertificateAsyncCallback { get; set; } @@ -33,6 +34,13 @@ public Task AddPackageAsync(FileInfo projectFilePath, string packageName, s : throw new NotImplementedException(); } + public Task RemovePackageAsync(FileInfo projectFilePath, string packageName, DotNetCliRunnerInvocationOptions options, CancellationToken cancellationToken) + { + return RemovePackageAsyncCallback != null + ? Task.FromResult(RemovePackageAsyncCallback(projectFilePath, packageName, options, cancellationToken)) + : Task.FromResult(0); // If not overridden, just return success. + } + public Task AddProjectToSolutionAsync(FileInfo solutionFile, FileInfo projectFile, DotNetCliRunnerInvocationOptions options, CancellationToken cancellationToken) { return AddProjectToSolutionAsyncCallback != null diff --git a/tests/Aspire.Cli.Tests/Utils/CliTestHelper.cs b/tests/Aspire.Cli.Tests/Utils/CliTestHelper.cs index 51efb112188..85013933096 100644 --- a/tests/Aspire.Cli.Tests/Utils/CliTestHelper.cs +++ b/tests/Aspire.Cli.Tests/Utils/CliTestHelper.cs @@ -240,7 +240,8 @@ public ISolutionLocator CreateDefaultSolutionLocatorFactory(IServiceProvider ser var cache = serviceProvider.GetRequiredService(); var executionContext = serviceProvider.GetRequiredService(); var fallbackParser = serviceProvider.GetRequiredService(); - return new ProjectUpdater(logger, runner, interactionService, cache, executionContext, fallbackParser); + var packageMigration = serviceProvider.GetRequiredService(); + return new ProjectUpdater(logger, runner, interactionService, cache, executionContext, fallbackParser, packageMigration); }; public Func CliHostEnvironmentFactory { get; set; } = (IServiceProvider serviceProvider) => From 96ad8c52370802e4ad83757e6117ee61a3e7bfa1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sun, 30 Nov 2025 22:59:37 +0000 Subject: [PATCH 4/6] Address code review feedback: add warning for missing Project element Co-authored-by: mitchdenny <513398+mitchdenny@users.noreply.github.com> --- src/Aspire.Cli/Projects/ProjectUpdater.cs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/Aspire.Cli/Projects/ProjectUpdater.cs b/src/Aspire.Cli/Projects/ProjectUpdater.cs index 69fd4704e87..423489d55ab 100644 --- a/src/Aspire.Cli/Projects/ProjectUpdater.cs +++ b/src/Aspire.Cli/Projects/ProjectUpdater.cs @@ -622,7 +622,7 @@ private static Task RemovePackageFromDirectoryPackagesProps(string packageId, Fi return Task.CompletedTask; } - private static Task AddPackageToDirectoryPackagesProps(string packageId, string version, FileInfo directoryPackagesPropsFile) + private Task AddPackageToDirectoryPackagesProps(string packageId, string version, FileInfo directoryPackagesPropsFile) { var doc = new XmlDocument { PreserveWhitespace = true }; doc.Load(directoryPackagesPropsFile.FullName); @@ -635,6 +635,8 @@ private static Task AddPackageToDirectoryPackagesProps(string packageId, string var project = doc.SelectSingleNode("/Project"); if (project is null) { + logger.LogWarning("Could not find element in {DirectoryPackagesPropsFile}. Package '{PackageId}' will not be added.", + directoryPackagesPropsFile.FullName, packageId); return Task.CompletedTask; } From 7cd4277e6ef814bbe9037aa0f90c0309c8c146a5 Mon Sep 17 00:00:00 2001 From: Mitch Denny Date: Mon, 1 Dec 2025 11:07:01 +1100 Subject: [PATCH 5/6] WIP. --- src/Aspire.Cli/Packaging/PackageMigration.cs | 10 ++- src/Aspire.Cli/Projects/ProjectUpdater.cs | 84 ++++++++----------- .../UpdateCommandStrings.Designer.cs | 7 +- .../Resources/UpdateCommandStrings.resx | 3 + .../Resources/xlf/UpdateCommandStrings.cs.xlf | 5 ++ .../Resources/xlf/UpdateCommandStrings.de.xlf | 5 ++ .../Resources/xlf/UpdateCommandStrings.es.xlf | 5 ++ .../Resources/xlf/UpdateCommandStrings.fr.xlf | 5 ++ .../Resources/xlf/UpdateCommandStrings.it.xlf | 5 ++ .../Resources/xlf/UpdateCommandStrings.ja.xlf | 5 ++ .../Resources/xlf/UpdateCommandStrings.ko.xlf | 5 ++ .../Resources/xlf/UpdateCommandStrings.pl.xlf | 5 ++ .../xlf/UpdateCommandStrings.pt-BR.xlf | 5 ++ .../Resources/xlf/UpdateCommandStrings.ru.xlf | 5 ++ .../Resources/xlf/UpdateCommandStrings.tr.xlf | 5 ++ .../xlf/UpdateCommandStrings.zh-Hans.xlf | 5 ++ .../xlf/UpdateCommandStrings.zh-Hant.xlf | 5 ++ 17 files changed, 114 insertions(+), 55 deletions(-) diff --git a/src/Aspire.Cli/Packaging/PackageMigration.cs b/src/Aspire.Cli/Packaging/PackageMigration.cs index 79214798a4e..c6eddedd433 100644 --- a/src/Aspire.Cli/Packaging/PackageMigration.cs +++ b/src/Aspire.Cli/Packaging/PackageMigration.cs @@ -62,12 +62,14 @@ public PackageMigration(ILogger logger) // Example migration rules (placeholder - replace with actual migrations): // - When upgrading to version >= 10.0.0, migrate Aspire.Hosting.OldPackage to Aspire.Hosting.NewPackage // - When downgrading to version < 10.0.0, migrate Aspire.Hosting.NewPackage to Aspire.Hosting.OldPackage + + var version13 = SemVersion.Parse("13.0.0", SemVersionStyles.Strict); + return [ - // Add migration rules here as packages are renamed/replaced - // Example: - // (v => v >= SemVersion.Parse("10.0.0", SemVersionStyles.Strict), "Aspire.Hosting.OldPackage", "Aspire.Hosting.NewPackage"), - // (v => v < SemVersion.Parse("10.0.0", SemVersionStyles.Strict), "Aspire.Hosting.NewPackage", "Aspire.Hosting.OldPackage"), + // Aspire.Hosting.NodeJs was renamed to Aspire.Hosting.JavaScript in 13.0.0 + (v => v.ComparePrecedenceTo(version13) >= 0, "Aspire.Hosting.NodeJs", "Aspire.Hosting.JavaScript"), + (v => v.ComparePrecedenceTo(version13) < 0, "Aspire.Hosting.JavaScript", "Aspire.Hosting.NodeJs"), ]; } diff --git a/src/Aspire.Cli/Projects/ProjectUpdater.cs b/src/Aspire.Cli/Projects/ProjectUpdater.cs index 423489d55ab..749aeb0a97a 100644 --- a/src/Aspire.Cli/Projects/ProjectUpdater.cs +++ b/src/Aspire.Cli/Projects/ProjectUpdater.cs @@ -42,7 +42,7 @@ public async Task UpdateProjectAsync(FileInfo projectFile, // Group update steps by project for better visual organization var updateStepsByProject = updateSteps - .OfType() + .OfType() .GroupBy(step => step.ProjectFile.FullName) .ToList(); @@ -511,23 +511,20 @@ private async Task AddMigrationStepsForTraditionalManagementAsync( // Get the latest version of the target package var latestTargetPackage = await GetLatestVersionOfPackageAsync(context, toPackageId, cancellationToken); - // Step 1: Remove the old package - var removeStep = new PackageMigrationRemoveStep( - string.Format(CultureInfo.InvariantCulture, UpdateCommandStrings.RemovePackageFormat, fromPackageId), - () => RemovePackageFromProject(projectFile, fromPackageId, cancellationToken), + // Create a single migration step that removes the old package and adds the new one + var migrationStep = new PackageMigrationStep( + string.Format(CultureInfo.InvariantCulture, UpdateCommandStrings.MigratePackageFormat, fromPackageId, toPackageId), + async () => + { + await RemovePackageFromProject(projectFile, fromPackageId, cancellationToken); + await UpdatePackageReferenceInProject(projectFile, latestTargetPackage!, cancellationToken); + }, fromPackageId, fromVersion, - projectFile); - context.UpdateSteps.Enqueue(removeStep); - - // Step 2: Add the new package - var addStep = new PackageMigrationAddStep( - string.Format(CultureInfo.InvariantCulture, UpdateCommandStrings.AddPackageFormat, toPackageId, latestTargetPackage!.Version), - () => UpdatePackageReferenceInProject(projectFile, latestTargetPackage, cancellationToken), toPackageId, latestTargetPackage!.Version, projectFile); - context.UpdateSteps.Enqueue(addStep); + context.UpdateSteps.Enqueue(migrationStep); } private async Task AnalyzePackageForCentralPackageManagementAsync(string packageId, FileInfo projectFile, FileInfo directoryPackagesPropsFile, UpdateContext context, CancellationToken cancellationToken) @@ -583,23 +580,20 @@ private async Task AddMigrationStepsForCentralPackageManagementAsync( // Get the latest version of the target package var latestTargetPackage = await GetLatestVersionOfPackageAsync(context, toPackageId, cancellationToken); - // Step 1: Remove the old package from Directory.Packages.props - var removeStep = new PackageMigrationRemoveStep( - string.Format(CultureInfo.InvariantCulture, UpdateCommandStrings.RemovePackageFormat, fromPackageId), - () => RemovePackageFromDirectoryPackagesProps(fromPackageId, directoryPackagesPropsFile), + // Create a single migration step that removes the old package and adds the new one + var migrationStep = new PackageMigrationStep( + string.Format(CultureInfo.InvariantCulture, UpdateCommandStrings.MigratePackageFormat, fromPackageId, toPackageId), + async () => + { + await RemovePackageFromDirectoryPackagesProps(fromPackageId, directoryPackagesPropsFile); + await AddPackageToDirectoryPackagesProps(toPackageId, latestTargetPackage!.Version, directoryPackagesPropsFile); + }, fromPackageId, fromVersion, - projectFile); - context.UpdateSteps.Enqueue(removeStep); - - // Step 2: Add the new package to Directory.Packages.props - var addStep = new PackageMigrationAddStep( - string.Format(CultureInfo.InvariantCulture, UpdateCommandStrings.AddPackageFormat, toPackageId, latestTargetPackage!.Version), - () => AddPackageToDirectoryPackagesProps(toPackageId, latestTargetPackage!.Version, directoryPackagesPropsFile), toPackageId, latestTargetPackage!.Version, projectFile); - context.UpdateSteps.Enqueue(addStep); + context.UpdateSteps.Enqueue(migrationStep); } private async Task RemovePackageFromProject(FileInfo projectFile, string packageId, CancellationToken cancellationToken) @@ -1043,51 +1037,45 @@ internal abstract record UpdateStep(string Description, Func Callback) } /// -/// Represents an update step for a package reference, containing package and project information. +/// Represents an update step associated with a specific project file. /// -internal record PackageUpdateStep( +internal abstract record ProjectUpdateStep( string Description, Func Callback, - string PackageId, - string CurrentVersion, - string NewVersion, - FileInfo ProjectFile) : UpdateStep(Description, Callback) -{ - public override string GetFormattedDisplayText() - { - return $"[bold yellow]{PackageId}[/] [bold green]{CurrentVersion.EscapeMarkup()}[/] to [bold green]{NewVersion.EscapeMarkup()}[/]"; - } -} + FileInfo ProjectFile) : UpdateStep(Description, Callback); /// -/// Represents a migration step to remove an old package during package migration. +/// Represents an update step for a package reference, containing package and project information. /// -internal record PackageMigrationRemoveStep( +internal record PackageUpdateStep( string Description, Func Callback, string PackageId, - string Version, - FileInfo ProjectFile) : UpdateStep(Description, Callback) + string CurrentVersion, + string NewVersion, + FileInfo ProjectFile) : ProjectUpdateStep(Description, Callback, ProjectFile) { public override string GetFormattedDisplayText() { - return $"[bold red]Remove[/] [bold yellow]{PackageId}[/] [bold green]{Version.EscapeMarkup()}[/]"; + return $"[bold yellow]{PackageId}[/] [bold green]{CurrentVersion.EscapeMarkup()}[/] to [bold green]{NewVersion.EscapeMarkup()}[/]"; } } /// -/// Represents a migration step to add a new package during package migration. +/// Represents a migration step that replaces one package with another during package migration. /// -internal record PackageMigrationAddStep( +internal record PackageMigrationStep( string Description, Func Callback, - string PackageId, - string Version, - FileInfo ProjectFile) : UpdateStep(Description, Callback) + string FromPackageId, + string FromVersion, + string ToPackageId, + string ToVersion, + FileInfo ProjectFile) : ProjectUpdateStep(Description, Callback, ProjectFile) { public override string GetFormattedDisplayText() { - return $"[bold green]Add[/] [bold yellow]{PackageId}[/] [bold green]{Version.EscapeMarkup()}[/]"; + return $"[bold yellow]{FromPackageId}[/] [bold green]{FromVersion.EscapeMarkup()}[/] to [bold yellow]{ToPackageId}[/] [bold green]{ToVersion.EscapeMarkup()}[/]"; } } diff --git a/src/Aspire.Cli/Resources/UpdateCommandStrings.Designer.cs b/src/Aspire.Cli/Resources/UpdateCommandStrings.Designer.cs index 846e052a8f9..c0fb9742c87 100644 --- a/src/Aspire.Cli/Resources/UpdateCommandStrings.Designer.cs +++ b/src/Aspire.Cli/Resources/UpdateCommandStrings.Designer.cs @@ -105,8 +105,9 @@ internal static string ProjectArgumentDescription { internal static string ChannelOptionDescriptionWithStaging => ResourceManager.GetString("ChannelOptionDescriptionWithStaging", resourceCulture); internal static string QualityOptionDescription => ResourceManager.GetString("QualityOptionDescription", resourceCulture); internal static string QualityOptionDescriptionWithStaging => ResourceManager.GetString("QualityOptionDescriptionWithStaging", resourceCulture); - internal static string DotNetToolSelfUpdateMessage => ResourceManager.GetString("DotNetToolSelfUpdateMessage", resourceCulture); - internal static string RemovePackageFormat => ResourceManager.GetString("RemovePackageFormat", resourceCulture); - internal static string AddPackageFormat => ResourceManager.GetString("AddPackageFormat", resourceCulture); + internal static string DotNetToolSelfUpdateMessage => ResourceManager.GetString("DotNetToolSelfUpdateMessage", resourceCulture)!; + internal static string RemovePackageFormat => ResourceManager.GetString("RemovePackageFormat", resourceCulture)!; + internal static string AddPackageFormat => ResourceManager.GetString("AddPackageFormat", resourceCulture)!; + internal static string MigratePackageFormat => ResourceManager.GetString("MigratePackageFormat", resourceCulture)!; } } diff --git a/src/Aspire.Cli/Resources/UpdateCommandStrings.resx b/src/Aspire.Cli/Resources/UpdateCommandStrings.resx index e149a9a9760..541ddb549c0 100644 --- a/src/Aspire.Cli/Resources/UpdateCommandStrings.resx +++ b/src/Aspire.Cli/Resources/UpdateCommandStrings.resx @@ -141,4 +141,7 @@ Add package {0} version {1} + + Migrate package {0} to {1} + diff --git a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.cs.xlf b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.cs.xlf index 7523d82da56..d50573d81b1 100644 --- a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.cs.xlf +++ b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.cs.xlf @@ -117,6 +117,11 @@ Mapování: {0} + + Migrate package {0} to {1} + Migrate package {0} to {1} + + No Aspire AppHost project found. Would you like to update the Aspire CLI instead? Nenašel se žádný projekt Aspire AppHost. Chcete místo toho aktualizovat rozhraní CLI Aspire? diff --git a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.de.xlf b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.de.xlf index dde9fec97c4..3a6e9d62409 100644 --- a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.de.xlf +++ b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.de.xlf @@ -117,6 +117,11 @@ Zuordnung: {0} + + Migrate package {0} to {1} + Migrate package {0} to {1} + + No Aspire AppHost project found. Would you like to update the Aspire CLI instead? Es wurde kein Aspire-AppHost-Projekt gefunden. Möchten Sie stattdessen die Aspire-CLI aktualisieren? diff --git a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.es.xlf b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.es.xlf index b79a560b8c3..52916e92e19 100644 --- a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.es.xlf +++ b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.es.xlf @@ -117,6 +117,11 @@ Asignación: {0} + + Migrate package {0} to {1} + Migrate package {0} to {1} + + No Aspire AppHost project found. Would you like to update the Aspire CLI instead? No se encontró ningún proyecto de AppHost. ¿Quiere actualizar la CLI de Aspire en su lugar? diff --git a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.fr.xlf b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.fr.xlf index 46f66f7d4b8..b125dee1c77 100644 --- a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.fr.xlf +++ b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.fr.xlf @@ -117,6 +117,11 @@ Mappage : {0} + + Migrate package {0} to {1} + Migrate package {0} to {1} + + No Aspire AppHost project found. Would you like to update the Aspire CLI instead? Aucun projet Aspire AppHost trouvé. Voulez-vous plutôt mettre à jour l’interface CLI Aspire ? diff --git a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.it.xlf b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.it.xlf index 8a275d53bfd..3949881fbec 100644 --- a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.it.xlf +++ b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.it.xlf @@ -117,6 +117,11 @@ Mapping: {0} + + Migrate package {0} to {1} + Migrate package {0} to {1} + + No Aspire AppHost project found. Would you like to update the Aspire CLI instead? Nessun progetto Aspire AppHost trovato. Aggiornare invece l'interfaccia della riga di comando Aspire? diff --git a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.ja.xlf b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.ja.xlf index b48f8a2fa55..8c4ee6dfde8 100644 --- a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.ja.xlf +++ b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.ja.xlf @@ -117,6 +117,11 @@ マッピング: {0} + + Migrate package {0} to {1} + Migrate package {0} to {1} + + No Aspire AppHost project found. Would you like to update the Aspire CLI instead? Aspire AppHost プロジェクトが見つかりませんでした。代わりに Aspire CLI を更新しますか? diff --git a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.ko.xlf b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.ko.xlf index bd4800032e0..9f051698439 100644 --- a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.ko.xlf +++ b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.ko.xlf @@ -117,6 +117,11 @@ 매핑: {0} + + Migrate package {0} to {1} + Migrate package {0} to {1} + + No Aspire AppHost project found. Would you like to update the Aspire CLI instead? Azure AppHost 프로젝트를 찾을 수 없습니다. 대신 Aspire CLI를 업데이트하시겠어요? diff --git a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.pl.xlf b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.pl.xlf index 05ec2b8aa48..05b929ff6c0 100644 --- a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.pl.xlf +++ b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.pl.xlf @@ -117,6 +117,11 @@ Mapowanie: {0} + + Migrate package {0} to {1} + Migrate package {0} to {1} + + No Aspire AppHost project found. Would you like to update the Aspire CLI instead? Nie znaleziono projektu Aspire AppHost. Czy chcesz zamiast tego zaktualizować narzędzie Aspire CLI? diff --git a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.pt-BR.xlf b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.pt-BR.xlf index 0c636efb5fb..d42833d8ef6 100644 --- a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.pt-BR.xlf +++ b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.pt-BR.xlf @@ -117,6 +117,11 @@ Mapeamento: {0} + + Migrate package {0} to {1} + Migrate package {0} to {1} + + No Aspire AppHost project found. Would you like to update the Aspire CLI instead? Não foram encontrados projetos AppHost do Aspire. Você quer atualizar a CLI do Aspire em vez disso? diff --git a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.ru.xlf b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.ru.xlf index 9c59127fcf9..8394b9b3f82 100644 --- a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.ru.xlf +++ b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.ru.xlf @@ -117,6 +117,11 @@ Сопоставление: {0} + + Migrate package {0} to {1} + Migrate package {0} to {1} + + No Aspire AppHost project found. Would you like to update the Aspire CLI instead? Проект Aspire AppHost не найден. Хотите вместо этого обновить Aspire CLI? diff --git a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.tr.xlf b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.tr.xlf index ab16c446812..d67d8d6f60d 100644 --- a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.tr.xlf +++ b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.tr.xlf @@ -117,6 +117,11 @@ Eşleme: {0} + + Migrate package {0} to {1} + Migrate package {0} to {1} + + No Aspire AppHost project found. Would you like to update the Aspire CLI instead? Aspire AppHost projesi bulunamadı. Bunun yerine Aspire CLI'yı güncelleştirmek ister misiniz? diff --git a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.zh-Hans.xlf b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.zh-Hans.xlf index e6c4082fe3a..ca900fdb01a 100644 --- a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.zh-Hans.xlf +++ b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.zh-Hans.xlf @@ -117,6 +117,11 @@ 映射: {0} + + Migrate package {0} to {1} + Migrate package {0} to {1} + + No Aspire AppHost project found. Would you like to update the Aspire CLI instead? 找不到 Aspire AppHost 项目。是否改为更新 Aspire CLI? diff --git a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.zh-Hant.xlf b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.zh-Hant.xlf index 8d5faa95493..466d3e616f0 100644 --- a/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.zh-Hant.xlf +++ b/src/Aspire.Cli/Resources/xlf/UpdateCommandStrings.zh-Hant.xlf @@ -117,6 +117,11 @@ 對應: {0} + + Migrate package {0} to {1} + Migrate package {0} to {1} + + No Aspire AppHost project found. Would you like to update the Aspire CLI instead? 找不到任何 Aspire AppHost 專案。您是否要改為更新 Aspire CLI? From fb4c4141298c114f70512e2c3b9c2ce7771b65ae Mon Sep 17 00:00:00 2001 From: Mitch Denny Date: Mon, 1 Dec 2025 12:06:56 +1100 Subject: [PATCH 6/6] WIP --- src/Aspire.Cli/DotNet/DotNetCliRunner.cs | 31 +++++++++++-------- src/Aspire.Cli/Packaging/PackageMigration.cs | 2 ++ src/Aspire.Cli/Projects/ProjectUpdater.cs | 25 ++++++++++++++- .../Projects/ProjectUpdaterTests.cs | 8 ++--- .../Templating/DotNetTemplateFactoryTests.cs | 2 +- .../TestServices/TestDotNetCliRunner.cs | 4 +-- 6 files changed, 51 insertions(+), 21 deletions(-) diff --git a/src/Aspire.Cli/DotNet/DotNetCliRunner.cs b/src/Aspire.Cli/DotNet/DotNetCliRunner.cs index 0aa3b02fb95..4989454366c 100644 --- a/src/Aspire.Cli/DotNet/DotNetCliRunner.cs +++ b/src/Aspire.Cli/DotNet/DotNetCliRunner.cs @@ -35,7 +35,7 @@ internal interface IDotNetCliRunner Task<(int ExitCode, string? TemplateVersion)> InstallTemplateAsync(string packageName, string version, FileInfo? nugetConfigFile, string? nugetSource, bool force, DotNetCliRunnerInvocationOptions options, CancellationToken cancellationToken); Task NewProjectAsync(string templateName, string name, string outputPath, string[] extraArgs, DotNetCliRunnerInvocationOptions options, CancellationToken cancellationToken); Task BuildAsync(FileInfo projectFilePath, DotNetCliRunnerInvocationOptions options, CancellationToken cancellationToken); - Task AddPackageAsync(FileInfo projectFilePath, string packageName, string packageVersion, string? nugetSource, DotNetCliRunnerInvocationOptions options, CancellationToken cancellationToken); + Task AddPackageAsync(FileInfo projectFilePath, string packageName, string? packageVersion, string? nugetSource, DotNetCliRunnerInvocationOptions options, CancellationToken cancellationToken); Task RemovePackageAsync(FileInfo projectFilePath, string packageName, DotNetCliRunnerInvocationOptions options, CancellationToken cancellationToken); Task AddProjectToSolutionAsync(FileInfo solutionFile, FileInfo projectFile, DotNetCliRunnerInvocationOptions options, CancellationToken cancellationToken); Task<(int ExitCode, NuGetPackage[]? Packages)> SearchPackagesAsync(DirectoryInfo workingDirectory, string query, bool prerelease, int take, int skip, FileInfo? nugetConfigFile, bool useCache, DotNetCliRunnerInvocationOptions options, CancellationToken cancellationToken); @@ -771,7 +771,7 @@ public async Task BuildAsync(FileInfo projectFilePath, DotNetCliRunnerInvoc options: options, cancellationToken: cancellationToken); } - public async Task AddPackageAsync(FileInfo projectFilePath, string packageName, string packageVersion, string? nugetSource, DotNetCliRunnerInvocationOptions options, CancellationToken cancellationToken) + public async Task AddPackageAsync(FileInfo projectFilePath, string packageName, string? packageVersion, string? nugetSource, DotNetCliRunnerInvocationOptions options, CancellationToken cancellationToken) { using var activity = telemetry.ActivitySource.StartActivity(); @@ -785,16 +785,23 @@ public async Task AddPackageAsync(FileInfo projectFilePath, string packageN if (isSingleFileAppHost) { cliArgsList.AddRange(["package", "--file", projectFilePath.FullName]); - // For single-file AppHost, use packageName@version format + // For single-file AppHost, use packageName@version format (version required) + if (string.IsNullOrEmpty(packageVersion)) + { + throw new ArgumentException("Package version is required for single-file AppHost projects.", nameof(packageVersion)); + } cliArgsList.Add($"{packageName}@{packageVersion}"); } else { cliArgsList.AddRange([projectFilePath.FullName, "package"]); - // For non single-file scenarios, use separate --version flag cliArgsList.Add(packageName); - cliArgsList.Add("--version"); - cliArgsList.Add(packageVersion); + // Only add --version if a version is specified (for CPM, version comes from Directory.Packages.props) + if (!string.IsNullOrEmpty(packageVersion)) + { + cliArgsList.Add("--version"); + cliArgsList.Add(packageVersion); + } } if (string.IsNullOrEmpty(nugetSource)) @@ -836,21 +843,19 @@ public async Task RemovePackageAsync(FileInfo projectFilePath, string packa { using var activity = telemetry.ActivitySource.StartActivity(); - var cliArgsList = new List - { - "remove" - }; + var cliArgsList = new List(); - // For single-file AppHost (apphost.cs), use --file switch instead of positional argument + // For single-file AppHost (apphost.cs), use "dotnet package remove --file " var isSingleFileAppHost = projectFilePath.Name.Equals("apphost.cs", StringComparison.OrdinalIgnoreCase); if (isSingleFileAppHost) { - cliArgsList.AddRange(["package", "--file", projectFilePath.FullName]); + cliArgsList.AddRange(["package", "remove", "--file", projectFilePath.FullName]); cliArgsList.Add(packageName); } else { - cliArgsList.AddRange([projectFilePath.FullName, "package"]); + // For regular projects, use "dotnet remove package " + cliArgsList.AddRange(["remove", projectFilePath.FullName, "package"]); cliArgsList.Add(packageName); } diff --git a/src/Aspire.Cli/Packaging/PackageMigration.cs b/src/Aspire.Cli/Packaging/PackageMigration.cs index c6eddedd433..5ea6fcaec92 100644 --- a/src/Aspire.Cli/Packaging/PackageMigration.cs +++ b/src/Aspire.Cli/Packaging/PackageMigration.cs @@ -69,7 +69,9 @@ public PackageMigration(ILogger logger) [ // Aspire.Hosting.NodeJs was renamed to Aspire.Hosting.JavaScript in 13.0.0 (v => v.ComparePrecedenceTo(version13) >= 0, "Aspire.Hosting.NodeJs", "Aspire.Hosting.JavaScript"), + (v => v.ComparePrecedenceTo(version13) >= 0, "CommunityToolkit.Aspire.Hosting.NodeJs.Extensions", "CommunityToolkit.Aspire.Hosting.JavaScript.Extensions"), (v => v.ComparePrecedenceTo(version13) < 0, "Aspire.Hosting.JavaScript", "Aspire.Hosting.NodeJs"), + (v => v.ComparePrecedenceTo(version13) < 0, "CommunityToolkit.Aspire.Hosting.JavaScript.Extensions", "CommunityToolkit.Aspire.Hosting.NodeJs.Extensions") ]; } diff --git a/src/Aspire.Cli/Projects/ProjectUpdater.cs b/src/Aspire.Cli/Projects/ProjectUpdater.cs index 749aeb0a97a..55347bd5c82 100644 --- a/src/Aspire.Cli/Projects/ProjectUpdater.cs +++ b/src/Aspire.Cli/Projects/ProjectUpdater.cs @@ -448,7 +448,7 @@ private async Task AnalyzeProjectAsync(FileInfo projectFile, UpdateContext conte private static bool IsUpdatablePackage(string packageId) { - return packageId.StartsWith("Aspire."); + return packageId.StartsWith("Aspire.") || packageId.StartsWith("CommunityToolkit.Aspire."); } private static CentralPackageManagementInfo DetectCentralPackageManagement(FileInfo projectFile) @@ -581,12 +581,18 @@ private async Task AddMigrationStepsForCentralPackageManagementAsync( var latestTargetPackage = await GetLatestVersionOfPackageAsync(context, toPackageId, cancellationToken); // Create a single migration step that removes the old package and adds the new one + // For CPM, we need to update both Directory.Packages.props AND the project file's PackageReference var migrationStep = new PackageMigrationStep( string.Format(CultureInfo.InvariantCulture, UpdateCommandStrings.MigratePackageFormat, fromPackageId, toPackageId), async () => { + // Update Directory.Packages.props await RemovePackageFromDirectoryPackagesProps(fromPackageId, directoryPackagesPropsFile); await AddPackageToDirectoryPackagesProps(toPackageId, latestTargetPackage!.Version, directoryPackagesPropsFile); + + // Update the project file's PackageReference + await RemovePackageFromProject(projectFile, fromPackageId, cancellationToken); + await AddPackageReferenceToProject(projectFile, toPackageId, cancellationToken); }, fromPackageId, fromVersion, @@ -601,6 +607,23 @@ private async Task RemovePackageFromProject(FileInfo projectFile, string pa return await runner.RemovePackageAsync(projectFile, packageId, new(), cancellationToken); } + private async Task AddPackageReferenceToProject(FileInfo projectFile, string packageId, CancellationToken cancellationToken) + { + // For CPM projects, we add a PackageReference without a version (version comes from Directory.Packages.props) + var exitCode = await runner.AddPackageAsync( + projectFilePath: projectFile, + packageName: packageId, + packageVersion: null, + nugetSource: null, + options: new(), + cancellationToken: cancellationToken); + + if (exitCode != 0) + { + throw new ProjectUpdaterException(string.Format(CultureInfo.InvariantCulture, UpdateCommandStrings.FailedUpdatePackageReferenceFormat, packageId, projectFile.FullName)); + } + } + private static Task RemovePackageFromDirectoryPackagesProps(string packageId, FileInfo directoryPackagesPropsFile) { var doc = new XmlDocument { PreserveWhitespace = true }; diff --git a/tests/Aspire.Cli.Tests/Projects/ProjectUpdaterTests.cs b/tests/Aspire.Cli.Tests/Projects/ProjectUpdaterTests.cs index 3596aeb6987..62dd01b2ed0 100644 --- a/tests/Aspire.Cli.Tests/Projects/ProjectUpdaterTests.cs +++ b/tests/Aspire.Cli.Tests/Projects/ProjectUpdaterTests.cs @@ -152,7 +152,7 @@ await File.WriteAllTextAsync( """); - var packagesAddsExecuted = new List<(FileInfo ProjectFile, string PackageId, string PackageVersion, string? PackageSource)>(); + var packagesAddsExecuted = new List<(FileInfo ProjectFile, string PackageId, string? PackageVersion, string? PackageSource)>(); var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, config => { config.DotNetCliRunnerFactory = (sp) => @@ -289,7 +289,7 @@ await File.WriteAllTextAsync( """); - var packagesAddsExecuted = new List<(FileInfo ProjectFile, string PackageId, string PackageVersion, string? PackageSource)>(); + var packagesAddsExecuted = new List<(FileInfo ProjectFile, string PackageId, string? PackageVersion, string? PackageSource)>(); var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, config => { config.DotNetCliRunnerFactory = (sp) => @@ -448,7 +448,7 @@ await File.WriteAllTextAsync( """); - var packagesAddsExecuted = new List<(FileInfo ProjectFile, string PackageId, string PackageVersion, string? PackageSource)>(); + var packagesAddsExecuted = new List<(FileInfo ProjectFile, string PackageId, string? PackageVersion, string? PackageSource)>(); var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, config => { config.DotNetCliRunnerFactory = (sp) => @@ -2302,7 +2302,7 @@ await File.WriteAllTextAsync( """); - var packagesAddsExecuted = new List<(FileInfo ProjectFile, string PackageId, string PackageVersion, string? PackageSource)>(); + var packagesAddsExecuted = new List<(FileInfo ProjectFile, string PackageId, string? PackageVersion, string? PackageSource)>(); var services = CliTestHelper.CreateServiceCollection(workspace, outputHelper, config => { diff --git a/tests/Aspire.Cli.Tests/Templating/DotNetTemplateFactoryTests.cs b/tests/Aspire.Cli.Tests/Templating/DotNetTemplateFactoryTests.cs index 77469fc387e..5fc84e657ed 100644 --- a/tests/Aspire.Cli.Tests/Templating/DotNetTemplateFactoryTests.cs +++ b/tests/Aspire.Cli.Tests/Templating/DotNetTemplateFactoryTests.cs @@ -411,7 +411,7 @@ public Task NewProjectAsync(string templateName, string projectName, string public Task BuildAsync(FileInfo projectFile, DotNetCliRunnerInvocationOptions options, CancellationToken cancellationToken) => throw new NotImplementedException(); - public Task AddPackageAsync(FileInfo projectFile, string packageName, string version, string? packageSourceUrl, DotNetCliRunnerInvocationOptions options, CancellationToken cancellationToken) + public Task AddPackageAsync(FileInfo projectFile, string packageName, string? version, string? packageSourceUrl, DotNetCliRunnerInvocationOptions options, CancellationToken cancellationToken) => throw new NotImplementedException(); public Task RemovePackageAsync(FileInfo projectFile, string packageName, DotNetCliRunnerInvocationOptions options, CancellationToken cancellationToken) diff --git a/tests/Aspire.Cli.Tests/TestServices/TestDotNetCliRunner.cs b/tests/Aspire.Cli.Tests/TestServices/TestDotNetCliRunner.cs index b68f8ee07ef..c644a06faa4 100644 --- a/tests/Aspire.Cli.Tests/TestServices/TestDotNetCliRunner.cs +++ b/tests/Aspire.Cli.Tests/TestServices/TestDotNetCliRunner.cs @@ -11,7 +11,7 @@ namespace Aspire.Cli.Tests.TestServices; internal sealed class TestDotNetCliRunner : IDotNetCliRunner { - public Func? AddPackageAsyncCallback { get; set; } + public Func? AddPackageAsyncCallback { get; set; } public Func? RemovePackageAsyncCallback { get; set; } public Func? AddProjectToSolutionAsyncCallback { get; set; } public Func? BuildAsyncCallback { get; set; } @@ -27,7 +27,7 @@ internal sealed class TestDotNetCliRunner : IDotNetCliRunner public Func Projects)>? GetSolutionProjectsAsyncCallback { get; set; } public Func? AddProjectReferenceAsyncCallback { get; set; } - public Task AddPackageAsync(FileInfo projectFilePath, string packageName, string packageVersion, string? nugetSource, DotNetCliRunnerInvocationOptions options, CancellationToken cancellationToken) + public Task AddPackageAsync(FileInfo projectFilePath, string packageName, string? packageVersion, string? nugetSource, DotNetCliRunnerInvocationOptions options, CancellationToken cancellationToken) { return AddPackageAsyncCallback != null ? Task.FromResult(AddPackageAsyncCallback(projectFilePath, packageName, packageVersion, nugetSource, options, cancellationToken))