Skip to content

Conversation

@zivkan
Copy link
Member

@zivkan zivkan commented Dec 23, 2025

Bug

Fixes: NuGet/Home#13752
FIxes: NuGet/Home#14695

Description

Shows the requested and resolved versions for each package. Removes the version for project references, making it more obvious that the graph node is indeed a project.

The above feature modified the same lines needed to fix running why on a legacy csproj, so both are being fixed in the same pull request, rather than two PRs that would need to be opened sequentially.

I used copilot to implement showing the requested version, but when reviewing its changes to DependencyGraphFinder.cs, I found the file's algorithm confusing. So, I commit copilot's changes unreviewed, then rewrote the class from scratch, with a few small changes in other files. Overall, it reduces product code by about 100 lines. If my rewrite isn't liked, we can revert the refactor commit from the branch, but then if someone has questions about the changes to DependencyGraphFinder, I may not be able to answer it.

The algorithm of the new class is, for each target in the assets file, create an in-memory LockFileTargetLibrary with the project's direct package and project references as dependencies, then recursively convert a LockFileTargetLibrary into a DependencyNode. It does this with no intermediate state, other than the "fake" LockFileTargetLibrary for the project's root. The tree is filtered for the package requested on the dotnet package why command line, but the way it's written, it will be trivial (change one if statement) to either show the complete unfiltered tree, or filter on multiple packages, or a partial package id match.

The implementation has a performance optimization, so it doesn't create the tree/graph and then filter as a second step. Instead, it returns nulls when the package is filtered out and there are no children. Dependencies that return null are removed, so when no dependency has the package, the node looks like it has no dependencies and so also gets removed if the package name also doesn't match. So, entire subtrees that don't contain the searched requested package avoid being created, with minimal (hopefully zero) memory allocations.

In order to have stronger guarantees on nullability, I modified DependencyNode to be an abstract class so PackageNodes must have both a requested version and resolved version.

The refactor found some tests that use an invalid assets file. I deleted these tests as the multi-rid nature of the tests are very difficult to replicate, but mostly test scenarios that theoretically possible, but not really practical. I'll comment on the two assets files explaining which each is infeasible, as justification why to delete the test rather than spending a lot of effort to fix it.

PR Checklist

@zivkan zivkan marked this pull request as ready for review December 24, 2025 01:00
@zivkan
Copy link
Member Author

zivkan commented Dec 24, 2025

Using the same example that @rainersigwald used in the original feature request, it looks like this now:

image

Big thanks to @Frulfump for the formatting idea that I really like and implemented.

Comment on lines -50 to -51
"net9.0/linux-x64": {
},
Copy link
Member Author

Choose a reason for hiding this comment

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

I deleted this file and the test that uses it, because RID specific targets still use the same top level package as the RIDless target, so it doesn't make sense for the rid specific target to not have the direct packages that the project defines under $.project.frameworks

Comment on lines -7 to -9
"dependencies": {
"Microsoft.NETCore.Platforms": "1.1.1",
"Microsoft.NETCore.Targets": "1.1.3"
Copy link
Member Author

Choose a reason for hiding this comment

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

I deleted this file and the test that uses it, because I believe it's an error for these packages to be listed as a dependency of another package, but then not be a library itself in the targets or libraries sections of the assets file. It was causing my refactor of DependerGraphFinder to crash.

However, I did find a real world project that had an issue like this: NuGet/Home#14698

So, perhaps we could restore this file and its test. But I don't understand the scenario that led to this. I assume this assets file was hand-edited, rather than copied unmodified after a restore. I've been unable to create a simple repro that I can use in a test.

@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 31, 2025
@zivkan zivkan 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 1, 2026
Copy link

@Frulfump Frulfump left a comment

Choose a reason for hiding this comment

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

Great to see this being implemented, can't wait to use it!

}
}
}
throw new Exception(Strings.WhyCommand_Error_InconsistentAssetsFile);

Choose a reason for hiding this comment

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

Not sure if applicable here but this would trigger CA2201 Don't throw reserved exception types CA2201
https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca2201 if that analyzer is enabled

@jeffkl jeffkl self-requested a review January 6, 2026 23:19
donnie-msft
donnie-msft previously approved these changes Jan 8, 2026

// if we have already traversed this node's children, continue
if (visited.Contains(currentPackageId))
if (userInputFrameworks.Count > 0 &&
Copy link
Contributor

Choose a reason for hiding this comment

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

nit: I prefer the conditions being on the newline. It may be what most of our repo does as well.

martinrrm
martinrrm previously approved these changes Jan 9, 2026
Copy link
Contributor

@martinrrm martinrrm left a comment

Choose a reason for hiding this comment

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

Left some comments not blocking this PR. Great refactor!

{
string resolved = pkgNode.ResolvedVersion.OriginalVersion ?? pkgNode.ResolvedVersion.ToString();
string requested = pkgNode.RequestedVersion.ToString("p", VersionRangeFormatter.Instance);
text = $"{node.Id}@{resolved} ({requested})";
Copy link
Contributor

Choose a reason for hiding this comment

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

I like how we are displaying the requested version, but is inconsistent with the format in other commands like dotnet package list

For example, a floating version would look like this in list:
image

And different in why
image

I'm not saying that one is better than the other, especially since the commands have a very different output, but we should be aiming to have the same display format for requested versions.

return false;

return string.Equals(x.Id, y.Id, StringComparison.CurrentCultureIgnoreCase);
return string.Equals(Id, other.Id, StringComparison.CurrentCultureIgnoreCase)
Copy link
Contributor

Choose a reason for hiding this comment

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

Do we need CurrentCulture for package Id's?

Copy link
Member Author

Choose a reason for hiding this comment

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

Good catch! We should be using OrdinalIgnoreCase

@zivkan zivkan dismissed stale reviews from martinrrm and donnie-msft via a699f68 January 11, 2026 21:20
@zivkan zivkan marked this pull request as draft January 14, 2026 23:08
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

6 participants