feat(dotnet): add structured metadata.dotnet with per-TFM capabilities - #36678
Open
Redth wants to merge 3 commits into
Open
feat(dotnet): add structured metadata.dotnet with per-TFM capabilities#36678Redth wants to merge 3 commits into
Redth wants to merge 3 commits into
Conversation
Adds a stable, namespaced metadata.dotnet shape to the @nx/dotnet analyzer's output, covering project-level capabilities (test, executable, packable, publishable, tool) plus per-evaluated-target- framework facts (TargetFramework short name, framework/platform identifier+version, RuntimeIdentifier/RuntimeIdentifiers) and the project's evaluated PackageId. The builder (DotnetMetadataBuilder) is pure and reuses the analyzer's already-grouped MSBuild inner-build nodes rather than re-evaluating or re-parsing project XML: one evaluation per target framework is collected in CollectTargetFrameworkEvaluations, ordered to match the project's declared TargetFrameworks sequence, and falls back to the primary node for legacy/non-SDK-style projects. Capability derivation reuses the exact same signals the analyzer already uses for target generation (now shared via ProjectUtilities instead of duplicated): IsTestProject/Microsoft.NET.Test.Sdk/ Microsoft.Testing.* for Test, and OutputType=Exe for Executable. Publishable falls back to Executable only when IsPublishable is unevaluated, so SDK-forced-off test projects are correctly reported as not publishable despite OutputType=Exe. Packable defaults to true when IsPublishable is unevaluated. PackageId falls back to AssemblyName, matching the NuGet packaging SDK's own default resolution. RuntimeIdentifiers and duplicate TargetFramework entries are deterministically deduplicated. This intentionally does not add projectType inference, automatic tags, NxTags, host/OS routing, selector tags, wrappers, or target variants, per discussion nrwl#36676. It also does not duplicate the package-dependency/CPM work in nrwl#36469; the two overlap only in that both touch Analyzer.cs's per-node property/package-reference collection, for unrelated purposes. Refs nrwl#36676 Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
👷 Deploy request for nx-docs pending review.Visit the deploys page to approve it
|
👷 Deploy request for nx-dev pending review.Visit the deploys page to approve it
|
…ect e2e coverage - Add a per-target-framework PackageId to DotnetTargetFrameworkMetadata, resolved the same way as the project-level value (evaluated PackageId, falling back to AssemblyName). - Only surface a project-level PackageId when every evaluated target framework agrees on it; previously the first non-null value was used even when a conditional PackageId disagreed across frameworks, silently misrepresenting the others. DotnetMetadataBuilder.ResolveProjectPackageId now returns null on disagreement so callers must consult each TargetFrameworks entry instead. - Add an analyzer-level end-to-end test (DotnetMetadataAnalyzerEndToEndTests) that writes a real temporary multi-target project to disk and drives the actual Analyzer.AnalyzeWorkspace entry point, covering MSBuild property collection, a conditional per-TFM PackageReference, and the public metadata.dotnet JSON serialization shape — complementing the existing pure-builder tests, which never exercise real MSBuild evaluation. - Update the TS metadata mirror and docs to describe the new per-TFM packageId field and the project-level ambiguity/null-when-divergent behavior. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 697756d3-0a78-4585-b95d-85326a71430d
The serialization e2e test asserted the absence of a metadata.projectType key, but projectType (if ever added) would be a sibling of metadata on the project node itself, not a key nested inside metadata, so the assertion never exercised anything meaningful. This PR is also independent of nrwl#36679, which may add projectType separately; asserting its absence here isn't this PR's concern to make or enforce. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 697756d3-0a78-4585-b95d-85326a71430d
Member
|
This is really nicely built. The per-TFM model is right, per-framework capabilities carry information targets can't express (targets are project-scoped), and omitting the project-level Two changes and it's good to go:
Nice touches worth keeping through any rework: reusing the existing |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Current Behavior
The
@nx/dotnetanalyzer infers Nx targets (build, test, publish, pack, etc.) from evaluatedMSBuild state, but it does not expose any of the underlying evaluated facts as project metadata.
Consumers that want to know a project's target framework(s), RID(s), or capabilities (is it a
test project? executable? packable? a .NET tool?) have no structured way to get that information
— they'd have to re-run/re-parse MSBuild themselves.
This addresses item 1 from #36676.
Expected Behavior
The analyzer now publishes a small, stable, namespaced
metadata.dotnetshape on every .NETproject node:
{ "dotnet": { "packageId": "MyCompany.MyLibrary", "capabilities": { "test": false, "executable": false, "packable": true, "publishable": false, "tool": false }, "targetFrameworks": [ { "packageId": "MyCompany.MyLibrary", "targetFramework": "net9.0", "targetFrameworkIdentifier": ".NETCoreApp", "targetFrameworkVersion": "v9.0", "runtimeIdentifiers": [], "capabilities": { "test": false, "executable": false, "packable": true, "publishable": false, "tool": false } } ] } }capabilitiesare the logical OR across every evaluated target framework.targetFrameworkshas one entry per evaluated MSBuild inner-build node (one per TFM formulti-targeted projects), each with its own framework/platform identifiers, evaluated
RuntimeIdentifier/RuntimeIdentifiers, and its own non-aggregated capabilities.packageIdis the evaluatedPackageId, falling back toAssemblyName(matching theNuGet packaging SDK's own default resolution). Each
targetFrameworksentry has its ownper-framework
packageId; the project-levelpackageIdis only set when every targetframework that evaluates one agrees on the same value, and is omitted (
undefined/absent fromJSON) when a conditional
PackageIddisagrees across frameworks — a single project-level valuewould otherwise silently misrepresent one of the frameworks. Consumers that need the identity
for a specific framework should always read it from that framework's own entry.
Design notes / scope boundaries (per discussion #36676):
CollectTargetFrameworkEvaluationscollectsproperties/package-references from the analyzer's already-grouped MSBuild inner-build nodes
(the same nodes multi-targeted projects already produce); it never re-parses project XML or
triggers additional MSBuild evaluation. Ordering matches the project's declared
TargetFrameworkssequence (read once from the shared outer-build property, since thatproperty isn't batched per inner build), with a fallback to the primary node for legacy/non-SDK
projects.
testreuses the exact sameIsTestProject/Microsoft.NET.Test.Sdk/Microsoft.Testing.*check the analyzer already uses to decide whether to emit a
testtarget (moved toProjectUtilitiesso both call sites share one implementation). This is evaluated per targetframework, so a conditional test package reference on only one TFM is captured correctly.
executablereuses the existingOutputType == "Exe"check.publishablereads evaluatedIsPublishable, falling back toexecutableonly whenunevaluated — this matters because the SDK forces
IsPublishable=falsefor test projectseven though their
OutputTypeisExe, and this implementation correctly reports those asnot publishable rather than naively deriving publishable from executable.
packablereads evaluatedIsPackable, defaulting totruewhen unevaluated (matching theNuGet packaging SDK's own default).
toolreads evaluatedPackAsTool.projectTypeinference, automatic tags,
NxTags, host/OS routing, selector tags, wrappers, or targetvariants — those are separate, more opinionated items from the discussion and out of scope
here.
RuntimeIdentifiersare split/trimmed/deduplicated(
OrdinalIgnoreCase), and duplicateTargetFrameworkentries across nodes are collapsed toone.
DotnetProjectMetadata/DotnetCapabilities/DotnetTargetFrameworkMetadataTypeScriptmirror was added to
ProjectMetadatainpackages/nx/src/config/workspace-json-project-json.ts(matching the existing
js?: {...}namespace precedent) purely for typed consumption; no newruntime behavior lives on the TS side.
Overlap with #36469: that PR adds Central Package Management support and touches
Analyzer.cs/PackageReference.csfor package-dependency resolution. This PR also touchesAnalyzer.cs(the same per-node property/package-reference collection helpers), but for anunrelated purpose — evaluated per-TFM metadata rather than package/CPM resolution. The two are
independent and this PR does not duplicate or depend on #36469's package-dependency/CPM work
(no external/referenced-package nodes are added here;
packageIdis the project's own identity,not a dependency).
Testing
DotnetMetadataBuilderTests.cscovering: single-target capabilitycombinations, multi-target OR-aggregation with per-framework capability variance (including a
conditional-test-package-per-TFM scenario),
RuntimeIdentifier/RuntimeIdentifierssplit/trim/dedup, framework/platform fact capture, per-TFM
PackageIdresolution/fallback,project-level
PackageIdagreement/disagreement (including anAssemblyName-fallbackdisagreement case), and duplicate-
TargetFrameworkcollapsing. These follow the existingpure-unit-test convention in this test project (plain dictionaries/lists, no real MSBuild
project graph evaluation).
DotnetMetadataAnalyzerEndToEndTests.csdrive the realAnalyzer.AnalyzeWorkspaceentry point against an actual temporary multi-targeted project ondisk (
net8.0;net9.0, with a conditional per-TFMMicrosoft.NET.Test.Sdkpackage reference andan explicit
OutputType=Exeon one framework only), verifying both the object-graph shape andthe actual public
metadata.dotnetJSON serialization output (via the same camelCase/null-omitting
JsonSerializerOptionsthe analyzer'sProgram.csuses). This exercises realMSBuild property collection and per-TFM package-reference conditions that the pure-builder
tests above don't cover, since those drive
DotnetMetadataBuilderdirectly withhand-constructed dictionaries. A
MSBuildTestRegistrationmodule initializer registers MSBuildonce for the test assembly, mirroring
Program.cs's own registration.packages/dotnet/analyzer.Testspass (dotnet test).dotnet buildonpackages/dotnet/analyzer/analyzer.Testssucceeds with 0 warnings/errors.Related Issue(s)
Addresses item 1 of #36676 (a discussion, not a closable
issue, so no "Fixes #" here). Related but independent: #36469.