Skip to content

Conversation

@nkolev92
Copy link
Member

@nkolev92 nkolev92 commented Nov 26, 2025

Bug

Fixes: NuGet/Home#14534

Description

This PR implements the restore part of NuGet/Home#12124.
The short summary is:

  • Add an assets file format (version 4) that uses the alias as the pivot. Enable that assets file by default in all of code to ensure all tests are testing that code. Switch to the version 3, if SDKAnalysisLevel is 10.0.300 or lower or we have a legacy project.
  • Raise NU1018, whenever duplicate frameworks are attempted to be restored with the old resolver or an older version of the .NET SDK is used (by virtue of global.json usually when nuget.exe or msbuild.exe restore is being used). All of the duplicate framework checks have been moved into RestoreCommand out of SpecValidationUtility for simpicity. They're only done when the version of the .NET SDK doesn't support aliasing, when a legacy project, or we have a legacy algo is being used.

Some more helpers for the review:

  • src/NuGet.Core/NuGet.Build.Tasks/GetReferenceNearestTargetFrameworkTask.cs - We just add the current project's TargetFramework property and use it to pivot off that if there's a conflict. MSBuild equivalent in Add extra parameter to get nearest dotnet/msbuild#12932.
  • In PackageSpecExtensions, I turn on nullabilitty and add the method GetNearestTargetFramework. This is the single place that knows how to disambiguate by alias. Beyond the assets file, this is the meat and potatoes of the change. It is thoroughly tested.
  • In src/NuGet.Core/NuGet.DependencyResolver.Core/Providers/IDependencyProvider.cs, which is what gets called when anyone in restore needs a project or package, we plumb the target alias and use it to disambiguate.
    • src/NuGet.Core/NuGet.Commands/RestoreCommand/SourceRepositoryDependencyProvider.cs is packages, so we don't plumb the alias.
    • src/NuGet.Core/NuGet.ProjectModel/PackageSpecReferenceDependencyProvider.cs is for the projects, so this is where the majority of the work is. This is where GetNearestTargetFramework on a PackageSpec is called.
  • In src/NuGet.Core/NuGet.Commands/RestoreCommand/RequestFactory/RestoreArgs.cs, we downgrade the assets file version to v3 for legacy projects. By setting it here, we ensure it's done in proper restore, but ensuring the tests are actually using the new version.
  • In src/NuGet.Core/NuGet.DependencyResolver.Core/ResolverUtility.cs, the cache key now accounts for the alias as well.

Assets file and dg spec (packagespec) read/write changes pointers:

  • The PackageSpecWriter, which writes the package spec, has a legacy and "new" writer. This writer is used in 3 different places. The assets file file, the dg spec writer (dgspec.json in the obj folder) and the no-op hash. In order to avoid having to version the package spec, we only use the legacy writer in the assets file, when the version is 3. This ensures all of our tests are actually covering the new codepath and we don't accidentally break the legacy projects. The way the package spec writer is implemented probably allows the legacy projects to have v4 of the assets file, but I'd rather do that at a later point to minimize the risk of this current change.

  • Every time we write the package spec now, we write a "framework", which is basically the effective framework. For simplicity, we write this every time.

  • LockFileTarget.Name is what's being written as the key, so in LockFileBuilder, we set the Name based on the version. Similarly, the LockFileTarget reader, v3 and v4, will set the name based on the version. If V3, we fill out the TargetAlias, if V4, we fill out the Framework (which is not longer written).

  • In LockFileFormat, the default version is 4. Whenever we use V4 of the LockFileFormat (or assets file), we use the default package spec writer, with version 3, we use the legacy writer.

  • I cleaned up any global suppressions whenever I saw it fit. The fewer of these we have, the better.

PR Checklist

  • Meaningful title, helpful description and a linked NuGet/Home issue
  • Added tests
  • Link to an issue or pull request to update docs if this PR changes settings, environment variables, new feature, etc.

@nkolev92 nkolev92 force-pushed the dev-nkolev92-supportV4Format branch 3 times, most recently from 4827827 to e7cded1 Compare December 9, 2025 08:41
@nkolev92 nkolev92 force-pushed the dev-nkolev92-supportV4Format branch 3 times, most recently from 7cbe95e to 863e076 Compare December 10, 2025 17:28
JanProvaznik pushed a commit to dotnet/msbuild that referenced this pull request Dec 19, 2025
Fixes #

### Context

Design: NuGet/Home#12124

To allow duplicate frameworks in aliasing, the project reference
protocol nearest framework selection needs to be updated to support
matching by alias as well.

Relevant part: 

https://github.com/NuGet/Home/blob/dev-nkolev92-tfmaliases/accepted/2025/Multiple-Equivalent-Framework-Support-TFM-As-Aliases.md#project-to-project-references

NuGet/NuGet.Client#7011 NuGet.Client side
adding the parameter.
NuGet/NuGet.Client#6972 will add the full
implementation at a later point.

### Changes Made

- Pass CurrentProjectTargetFrameworkProperty if
GetReferenceNearestTargetFrameworkTaskSupportsTargetFrameworKPropertyParameter
is set.
- If
GetReferenceNearestTargetFrameworkTaskSupportsTargetFrameworKPropertyParameter
is not set, but
GetReferenceNearestTargetFrameworkTaskSupportsTargetPlatformParameter is
set, call the old variation.
- Otherwise calls the last variation.

### Testing

- Manual testing.
- I'd be happy to add tests if someone can point me in the right
direction.
### Notes

The idea here is to get ahead of things. Currently aliasing work can't
be end to end tested because it requires an msbuild change. It makes it
really hard to validate the NuGet changes are enough and good, but this
is the only change needed on the msbuild side.

---------

Co-authored-by: Copilot <[email protected]>
@dotnet-policy-service dotnet-policy-service bot added the Status:No recent activity PRs that have not had any recent activity and will be closed if the label is not removed label Dec 20, 2025
@nkolev92 nkolev92 reopened this Jan 15, 2026
@dotnet-policy-service dotnet-policy-service bot removed the Status:No recent activity PRs that have not had any recent activity and will be closed if the label is not removed label Jan 15, 2026
@nkolev92 nkolev92 force-pushed the dev-nkolev92-supportV4Format branch 4 times, most recently from 2c2a38f to 2d62c8b Compare January 16, 2026 22:18
@nkolev92 nkolev92 force-pushed the dev-nkolev92-supportV4Format branch 3 times, most recently from 91829c0 to 1d505fc Compare January 24, 2026 00:57
@nkolev92 nkolev92 requested a review from Copilot January 26, 2026 19:26
@nkolev92 nkolev92 force-pushed the dev-nkolev92-supportV4Format branch from 1d505fc to 0db929a Compare January 26, 2026 19:30
@nkolev92 nkolev92 marked this pull request as ready for review January 26, 2026 19:30
@nkolev92 nkolev92 requested a review from a team as a code owner January 26, 2026 19:30
@nkolev92 nkolev92 requested a review from jeffkl January 26, 2026 19:31
@nkolev92 nkolev92 changed the title Full support for aliasing Full restore support for aliasing - make the assets file aliasing aware Jan 26, 2026
Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR implements the restore component of framework aliasing support (from NuGet/Home#12124), enabling projects to restore duplicate target frameworks differentiated by aliases. The implementation includes a new assets file format (version 4) that pivots on aliases instead of framework names, with automatic downgrade to version 3 for legacy projects or older .NET SDK versions.

Changes:

  • Introduces assets file format v4 with alias-based pivoting
  • Adds framework alias disambiguation logic in restore and MSBuild tasks
  • Implements NU1018 error for duplicate frameworks when using legacy resolver or older SDKs

Reviewed changes

Copilot reviewed 70 out of 71 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
src/NuGet.Core/NuGet.ProjectModel/PackageSpecExtensions.cs Adds GetNearestTargetFramework method for alias-based framework matching
src/NuGet.Core/NuGet.ProjectModel/PackageSpecWriter.cs Updates writer to support both legacy and alias-based format output
src/NuGet.Core/NuGet.ProjectModel/LockFile/LockFileFormat.cs Changes default version to 4 with conditional legacy writer usage
src/NuGet.Core/NuGet.Commands/RestoreCommand/RestoreCommand.cs Adds duplicate framework validation and version determination logic
src/NuGet.Core/NuGet.DependencyResolver.Core/LibraryRangeCacheKey.cs Extends cache key to include alias parameter
src/NuGet.Core/NuGet.Build.Tasks/GetReferenceNearestTargetFrameworkTask.cs Adds CurrentProjectTargetFrameworkProperty for disambiguation
test files Comprehensive test coverage for aliasing scenarios
Files not reviewed (1)
  • src/NuGet.Core/NuGet.Commands/Strings.Designer.cs: Language not supported
Comments suppressed due to low confidence (3)

test/NuGet.Core.Tests/NuGet.Commands.Test/Project2ProjectInLockFileTests.cs:1

  • Duplicate comment "// Act" on consecutive lines. Remove the duplicate on line 278.
    test/NuGet.Clients.Tests/NuGet.PackageManagement.VisualStudio.Test/Services/NuGetProjectManagerServiceTests.cs:1
  • Incomplete or corrupted comment text. "ock" should likely be removed or the complete intended comment should be provided.
    test/NuGet.Core.FuncTests/Dotnet.Integration.Test/DotnetRestoreTests.cs:1
  • Duplicate assertions on lines 3651-3653 and 3655-3657. The second set of assertions is redundant and should be removed.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 70 out of 71 changed files in this pull request and generated 3 comments.

Files not reviewed (1)
  • src/NuGet.Core/NuGet.Commands/Strings.Designer.cs: Language not supported

@Nigusu-Allehu Nigusu-Allehu self-requested a review January 26, 2026 21:38
request.FallbackRuntimes.UnionWith(FallbackRuntimes);
request.LockFileVersion = LockFileFormat.Version;
// Use v3 for classic csproj.
request.LockFileVersion = request.Project.RestoreMetadata?.UsingMicrosoftNETSdk == true ? LockFileFormat.Version : 3;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we have a constant for the value of 3 here?

if (spec.RestoreMetadata.TargetFrameworks.Count > 1)
{
var aliases = spec.TargetFrameworks.Select(e => e.TargetAlias);
var aliases = spec.TargetFrameworks.Select(e => e.TargetAlias).ToList();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can this just be moved out of the if now that its needed in two places? Is it possible for this call to be happening twice sometimes?

RuntimeIdentifier = targetGraph.RuntimeIdentifier,
TargetAlias = targetGraph.TargetAlias,
};
var target = lockFile.Version >= 4 ?
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should also be a constant IMO

Suggested change
var target = lockFile.Version >= 4 ?
var target = lockFile.Version >= LockFileFormat.Version4 ?

{
// For NETCore put everything under a TFM section
// Projects are included for NETCore
bool isV4LockFile = lockFile.Version >= 4;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
bool isV4LockFile = lockFile.Version >= 4;
bool isV4OrGreatorLockFile = lockFile.Version >= 4;

bool success = true;

// Check for duplicate frameworks. The new dependency resolver supports aliasing, but the legacy one does not.
if (_request.Project.TargetFrameworks.Count != new HashSet<NuGetFramework>(_request.Project.TargetFrameworks.Select(e => e.FrameworkName)).Count)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would GroupBy() be better and more obvious what this code is doing?

}

var library = _dependencyProvider.GetLibrary(libraryIdentity, targetFramework);
var library = _dependencyProvider.GetLibrary(libraryIdentity, targetFramework, alias: null);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think in some places we're passing null as alias for a default and other places we're passing string.Empty. Since we declare the parameter as nullable, we should probably just pass null but still treat string.Empty as nothing? Or does string.Empty mean something else and its okay that we're not consistently passing one or the other?

return LibraryRange.Equals(other.LibraryRange)
&& Framework.Equals(other.Framework);
&& Framework.Equals(other.Framework)
&& StringComparer.Ordinal.Equals(Alias, other.Alias);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is alias case sensitive?

Suggested change
&& StringComparer.Ordinal.Equals(Alias, other.Alias);
&& StringComparer.OrdinalIgnoreCase.Equals(Alias, other.Alias);

{
foreach (var target in lockFile.Targets)
{
target.TargetFramework = lockFile.PackageSpec.GetTargetFramework(target.TargetAlias).FrameworkName;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this need to guard against GetTargetFramework() returning null?

// Legacy is really supposed to be 3.

// PackageSpec probably doesn't need to be versioned to be honest.
// But the lock file does.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Clean up this comment, especially the next line...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Change the assets file format to support aliasing

4 participants