From 7a524a600dea934099dfc3ec6ae5a4764a6807ec Mon Sep 17 00:00:00 2001 From: Nikolche Kolev Date: Thu, 19 Mar 2026 15:27:14 -0700 Subject: [PATCH 1/3] GetReferenceNearest disambiguation --- .../PackagesLockFileUtilities.cs | 14 +- .../RestoreCommand_PackagesLockFileTests.cs | 191 ++++++++++++++++++ 2 files changed, 201 insertions(+), 4 deletions(-) diff --git a/src/NuGet.Core/NuGet.ProjectModel/ProjectLockFile/PackagesLockFileUtilities.cs b/src/NuGet.Core/NuGet.ProjectModel/ProjectLockFile/PackagesLockFileUtilities.cs index a40ba670ba0..d7dfb454adc 100644 --- a/src/NuGet.Core/NuGet.ProjectModel/ProjectLockFile/PackagesLockFileUtilities.cs +++ b/src/NuGet.Core/NuGet.ProjectModel/ProjectLockFile/PackagesLockFileUtilities.cs @@ -229,14 +229,20 @@ public static LockFileValidationResult IsLockFileValid(DependencyGraphSpec dgSpe else { // This does not consider ATF. - p2pSpecTargetFrameworkInformation = NuGetFrameworkUtility.GetNearest(p2pSpec.TargetFrameworks, restoreMetadataFramework.FrameworkName, e => e.FrameworkName); + p2pSpecTargetFrameworkInformation = p2pSpec.GetNearestTargetFramework(targetFrameworkInformation.FrameworkName, targetFrameworkInformation.TargetAlias); + if (p2pSpecTargetFrameworkInformation.FrameworkName == null) + { + if (targetFrameworkInformation.FrameworkName is AssetTargetFallbackFramework atfFramework) + { + p2pSpecTargetFrameworkInformation = p2pSpec.GetNearestTargetFramework(atfFramework.AsFallbackFramework(), targetFrameworkInformation.TargetAlias); + } + } } // No compatible framework found - if (p2pSpecTargetFrameworkInformation != null) + if (p2pSpecTargetFrameworkInformation != null && p2pSpecTargetFrameworkInformation.FrameworkName != null) { // We need to compare the main framework only. Ignoring fallbacks. - var p2pSpecProjectRestoreMetadataFrameworkInfo = p2pSpec.RestoreMetadata.TargetFrameworks.FirstOrDefault( - t => NuGetFramework.Comparer.Equals(p2pSpecTargetFrameworkInformation.FrameworkName, t.FrameworkName)); + var p2pSpecProjectRestoreMetadataFrameworkInfo = p2pSpec.RestoreMetadata.TargetFrameworks.FirstOrDefault(e => e.TargetAlias == p2pSpecTargetFrameworkInformation.TargetAlias); if (p2pSpecProjectRestoreMetadataFrameworkInfo != null) { diff --git a/test/NuGet.Core.FuncTests/NuGet.Commands.FuncTest/RestoreCommand_PackagesLockFileTests.cs b/test/NuGet.Core.FuncTests/NuGet.Commands.FuncTest/RestoreCommand_PackagesLockFileTests.cs index 394173e4c51..fea9e6b4342 100644 --- a/test/NuGet.Core.FuncTests/NuGet.Commands.FuncTest/RestoreCommand_PackagesLockFileTests.cs +++ b/test/NuGet.Core.FuncTests/NuGet.Commands.FuncTest/RestoreCommand_PackagesLockFileTests.cs @@ -3,6 +3,7 @@ #nullable disable +using System; using System.Collections.Generic; using System.IO; using System.Linq; @@ -1678,5 +1679,195 @@ public async Task RestoreCommand_PackagesLockFile_InLockedMode_WithAliasedFramew logger.ErrorMessages.Single().Should().Contain("NU1004"); logger.ErrorMessages.Single().Should().Contain("The project reference project2 has changed"); } + + // Verifies project reference through ATF, lock file creation, and locked mode + [Fact] + public async Task RestoreCommand_PackagesLockFile_ProjectReferenceWithATF_LockedModeSucceeds() + { + using var pathContext = new SimpleTestPathContext(); + var logger = new TestLogger(); + + // Create packages + var pkgA = new SimpleTestPackageContext("PackageA", "1.0.0"); + await SimpleTestPackageUtility.CreatePackagesAsync(pathContext.PackageSource, pkgA); + + var project2Spec = @" + { + ""frameworks"": { + ""net472"": { + ""dependencies"": { + ""PackageA"": { + ""version"": ""[1.0.0,)"", + ""target"": ""Package"", + } + } + } + } + }"; + + var project2 = ProjectTestHelpers.GetPackageSpecWithProjectNameAndSpec("Project2", pathContext.SolutionRoot, project2Spec); + + var project1Spec = @" + { + ""frameworks"": { + ""net10.0"": { + ""assetTargetFallback"": true, + ""imports"": [ ""net472"" ], + ""warn"": true, + ""dependencies"": { + } + } + } + }"; + + var project1 = ProjectTestHelpers.GetPackageSpecWithProjectNameAndSpec("Project1", pathContext.SolutionRoot, project1Spec); + project1 = project1.WithTestProjectReference(project2); + + var lockFilePath = Path.Combine(Path.GetDirectoryName(project1.RestoreMetadata.ProjectPath)!, PackagesLockFileFormat.LockFileName); + project1.RestoreMetadata.RestoreLockProperties = new RestoreLockProperties( + restorePackagesWithLockFile: "true", + lockFilePath, + restoreLockedMode: false); + + var result = await new RestoreCommand(ProjectTestHelpers.CreateRestoreRequest(pathContext, logger, project1, project2)).ExecuteAsync(); + result.Success.Should().BeTrue(because: string.Join(Environment.NewLine, result.LockFile.LogMessages.Select(e => e.Message))); + await result.CommitAsync(logger, CancellationToken.None); + + // Verify lock file was created and has correct alias-aware structure + File.Exists(lockFilePath).Should().BeTrue(); + var packagesLockFile = PackagesLockFileFormat.Read(lockFilePath); + packagesLockFile.Targets.Should().HaveCount(1); + packagesLockFile.Targets[0].Dependencies.Should().HaveCount(2); + + // Enable locked mode + project1.RestoreMetadata.RestoreLockProperties = new RestoreLockProperties( + restorePackagesWithLockFile: "true", + lockFilePath, + restoreLockedMode: true); + logger.Clear(); + + // Second restore in locked mode + // Lock file validation does not consider ATF for project references (PackagesLockFileUtilities.cs), + // so locked mode fails with NU1004 when the project reference requires ATF. + result = await new RestoreCommand(ProjectTestHelpers.CreateRestoreRequest(pathContext, logger, project1, project2)).ExecuteAsync(); + result.Success.Should().BeTrue(logger.ShowErrors()); + } + + // P1 (apple;banana net10.0 with ATF net472) -> Project2 (apple;banana net472) -> PackageA + // Verifies project reference through alias disambiguation with ATF, lock file creation, and locked mode + [Fact] + public async Task RestoreCommand_PackagesLockFile_WithAliasesOfSameFramework_ProjectReferenceWithATF_LockedModeSucceeds() + { + using var pathContext = new SimpleTestPathContext(); + var logger = new TestLogger(); + + // Create packages + var pkgA = new SimpleTestPackageContext("PackageA", "1.0.0"); + var pkgB = new SimpleTestPackageContext("PackageB", "1.0.0"); + await SimpleTestPackageUtility.CreatePackagesAsync(pathContext.PackageSource, pkgA, pkgB); + + string apple = nameof(apple); + string banana = nameof(banana); + + // Create Project2 spec with apple;banana aliases both targeting net472 + var project2Spec = @" + { + ""frameworks"": { + ""apple"": { + ""framework"": ""net472"", + ""targetAlias"": ""apple"", + ""dependencies"": { + ""PackageA"": { + ""version"": ""[1.0.0,)"", + ""target"": ""Package"", + } + } + }, + ""banana"": { + ""framework"": ""net472"", + ""targetAlias"": ""banana"", + ""dependencies"": { + ""PackageB"": { + ""version"": ""[1.0.0,)"", + ""target"": ""Package"", + } + } + } + } + }"; + + var project2 = ProjectTestHelpers.GetPackageSpecWithProjectNameAndSpec("Project2", pathContext.SolutionRoot, project2Spec); + + // Create Project1 spec with apple;banana aliases, both net10.0 with ATF for net472 + var project1Spec = @" + { + ""frameworks"": { + ""apple"": { + ""framework"": ""net10.0"", + ""targetAlias"": ""apple"", + ""assetTargetFallback"": true, + ""imports"": [ ""net472"" ], + ""warn"": true, + ""dependencies"": { + } + }, + ""banana"": { + ""framework"": ""net10.0"", + ""targetAlias"": ""banana"", + ""assetTargetFallback"": true, + ""imports"": [ ""net472"" ], + ""warn"": true, + ""dependencies"": { + } + } + } + }"; + + var project1 = ProjectTestHelpers.GetPackageSpecWithProjectNameAndSpec("Project1", pathContext.SolutionRoot, project1Spec); + project1 = project1.WithTestProjectReference(project2); + + // Enable lock file on Project1 + var lockFilePath = Path.Combine(Path.GetDirectoryName(project1.RestoreMetadata.ProjectPath)!, PackagesLockFileFormat.LockFileName); + project1.RestoreMetadata.RestoreLockProperties = new RestoreLockProperties( + restorePackagesWithLockFile: "true", + lockFilePath, + restoreLockedMode: false); + + // First restore - generates lock file + var result = await new RestoreCommand(ProjectTestHelpers.CreateRestoreRequest(pathContext, logger, project1, project2)).ExecuteAsync(); + result.Success.Should().BeTrue(because: string.Join(Environment.NewLine, result.LockFile.LogMessages.Select(e => e.Message))); + await result.CommitAsync(logger, CancellationToken.None); + + // Verify lock file was created and has correct alias-aware structure + File.Exists(lockFilePath).Should().BeTrue(); + var packagesLockFile = PackagesLockFileFormat.Read(lockFilePath); + packagesLockFile.Targets.Should().HaveCount(2); + packagesLockFile.Targets.Should().Contain(t => t.TargetAlias == apple); + packagesLockFile.Targets.Should().Contain(t => t.TargetAlias == banana); + packagesLockFile.Targets[0].Dependencies.Should().HaveCount(2); + packagesLockFile.Targets[1].Dependencies.Should().HaveCount(2); + + // Both aliases should resolve Project2 through alias disambiguation with ATF + var appleTarget = result.LockFile.GetTarget(apple, null); + appleTarget.Should().NotBeNull(); + appleTarget.Libraries.Should().Contain(e => e.Name!.Equals("Project2")); + + var bananaTarget = result.LockFile.GetTarget(banana, null); + bananaTarget.Should().NotBeNull(); + bananaTarget.Libraries.Should().Contain(e => e.Name!.Equals("Project2")); + + // Enable locked mode + project1.RestoreMetadata.RestoreLockProperties = new RestoreLockProperties( + restorePackagesWithLockFile: "true", + lockFilePath, + restoreLockedMode: true); + logger.Clear(); + + // Second restore in locked mode + // Lock file validation does not consider ATF for project references (PackagesLockFileUtilities.cs), + // so locked mode fails with NU1004 when the project reference requires ATF. + result = await new RestoreCommand(ProjectTestHelpers.CreateRestoreRequest(pathContext, logger, project1, project2)).ExecuteAsync(); + result.Success.Should().BeTrue(logger.ShowErrors()); + } } } From 552b529a0f69e1bec903d1953e4d984792d4f0b2 Mon Sep 17 00:00:00 2001 From: Nikolche Kolev Date: Tue, 21 Apr 2026 13:37:04 -0700 Subject: [PATCH 2/3] cleanup --- .../ProjectLockFile/PackagesLockFileUtilities.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/NuGet.Core/NuGet.ProjectModel/ProjectLockFile/PackagesLockFileUtilities.cs b/src/NuGet.Core/NuGet.ProjectModel/ProjectLockFile/PackagesLockFileUtilities.cs index d7dfb454adc..2dc3efe0eb4 100644 --- a/src/NuGet.Core/NuGet.ProjectModel/ProjectLockFile/PackagesLockFileUtilities.cs +++ b/src/NuGet.Core/NuGet.ProjectModel/ProjectLockFile/PackagesLockFileUtilities.cs @@ -228,7 +228,6 @@ public static LockFileValidationResult IsLockFileValid(DependencyGraphSpec dgSpe } else { - // This does not consider ATF. p2pSpecTargetFrameworkInformation = p2pSpec.GetNearestTargetFramework(targetFrameworkInformation.FrameworkName, targetFrameworkInformation.TargetAlias); if (p2pSpecTargetFrameworkInformation.FrameworkName == null) { @@ -241,7 +240,7 @@ public static LockFileValidationResult IsLockFileValid(DependencyGraphSpec dgSpe // No compatible framework found if (p2pSpecTargetFrameworkInformation != null && p2pSpecTargetFrameworkInformation.FrameworkName != null) { - // We need to compare the main framework only. Ignoring fallbacks. + // Get it based on the matching alias. The appropriate target framework information has already been calculated. var p2pSpecProjectRestoreMetadataFrameworkInfo = p2pSpec.RestoreMetadata.TargetFrameworks.FirstOrDefault(e => e.TargetAlias == p2pSpecTargetFrameworkInformation.TargetAlias); if (p2pSpecProjectRestoreMetadataFrameworkInfo != null) From c13fb09e58f6f9908242435025d4fa4f5f80d5c7 Mon Sep 17 00:00:00 2001 From: Nikolche Kolev Date: Wed, 22 Apr 2026 17:48:18 -0700 Subject: [PATCH 3/3] remove redundant comment --- .../RestoreCommand_PackagesLockFileTests.cs | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/NuGet.Core.FuncTests/NuGet.Commands.FuncTest/RestoreCommand_PackagesLockFileTests.cs b/test/NuGet.Core.FuncTests/NuGet.Commands.FuncTest/RestoreCommand_PackagesLockFileTests.cs index fea9e6b4342..d9fef1c76dd 100644 --- a/test/NuGet.Core.FuncTests/NuGet.Commands.FuncTest/RestoreCommand_PackagesLockFileTests.cs +++ b/test/NuGet.Core.FuncTests/NuGet.Commands.FuncTest/RestoreCommand_PackagesLockFileTests.cs @@ -1747,8 +1747,6 @@ public async Task RestoreCommand_PackagesLockFile_ProjectReferenceWithATF_Locked logger.Clear(); // Second restore in locked mode - // Lock file validation does not consider ATF for project references (PackagesLockFileUtilities.cs), - // so locked mode fails with NU1004 when the project reference requires ATF. result = await new RestoreCommand(ProjectTestHelpers.CreateRestoreRequest(pathContext, logger, project1, project2)).ExecuteAsync(); result.Success.Should().BeTrue(logger.ShowErrors()); }