Skip to content

feat(repo): add a .NET example for @nx/dotnet - #36701

Draft
AgentEnder wants to merge 13 commits into
masterfrom
examples/dotnet-openapi-typescript
Draft

feat(repo): add a .NET example for @nx/dotnet#36701
AgentEnder wants to merge 13 commits into
masterfrom
examples/dotnet-openapi-typescript

Conversation

@AgentEnder

@AgentEnder AgentEnder commented Aug 18, 2026

Copy link
Copy Markdown
Member

Scope changed since this PR opened. It began as a full-stack OpenAPI to TypeScript codegen example. That story is moving to nrwl/nx-examples, where it can sit next to a real front end and a deployable app. What is left here is a small example of @nx/dotnet itself.

What

examples/dotnet/basic — a standalone Nx + pnpm workspace that dogfoods @nx/dotnet from this repo through link: dependencies, the same way examples/react/basic does.

Three projects, and no project.json anywhere:

apps/Api             minimal web API
libs/Catalog         class library the API references
tests/Catalog.Tests  xunit project covering the library

Registration is automatic: the nested nx.json is what tools/workspace-plugin/src/plugins/examples-plugin.ts globs for.

What it demonstrates

Everything comes from the .csproj files. Verified against the graph rather than described:

Target Which projects get it
build, build:release, restore, clean, watch all three
test test projects, so Catalog.Tests only
run, publish executable projects, so Api only
pack library projects, so Catalog only

<ProjectReference> becomes a graph edge, so nx build Api builds Catalog first — ordering falls out of the project files rather than out of Nx config.

nx.json adds "dependsOn": ["...", "restore"] to build. The inferred build runs dotnet build --no-restore, and pnpm validate has to work from a clean checkout. The "..." keeps the inferred ^build and puts the restore ahead of it.

One line outside examples/

{ "plugin": "@nx/dotnet", "exclude": ["examples/**/*"], ... }

Without it the root graph infers targets for the example's .csproj files, including run, which the affected sweep executes — against a workspace that has not been installed. This matches what @nx/js/typescript and @nx/playwright/plugin already do for examples/**.

Earlier revisions of this PR also touched examples-plugin.ts and added an @nx/oxlint exclude. Both are gone: with no interior project.json or package.json, nothing from the example enters this repo's graph, so the sandbox inputs and outputs it needed no longer apply. Confirmed by removing each and re-checking.

Verification

  • pnpm validate green from a clean checkout, xunit test passes
  • root format:check and sync:check pass
  • no interior project appears in the root graph

Notes

  • Targets net9.0 to match the SDK pinned in mise.toml.
  • The branch name still says openapi-typescript from the original scope. Renaming it would detach this PR, so it stays as is.

A standalone example workspace showing full-stack type safety across the
C#/TypeScript boundary, dogfooding @nx/dotnet from this repo via link:
dependencies like examples/react/basic does.

The pipeline:
- Api:build writes apps/Api/openapi/Api.json during dotnet build, via
  <OpenApiDocumentsDirectory> plus Microsoft.Extensions.ApiDescription.Server,
  so there is no separate extraction step
- Api:codegen runs openapi-generator-cli over that document
- api-client:build compiles the generated client plus a checked-in
  assert-types.ts that pins the contract
- web:build compiles a small consumer; that edge comes from an ordinary
  package.json dependency, not from Nx config

Two declarations worth reading:
- nx.json adds the document directory to build's outputs with a "..." spread,
  so it rides the build cache instead of going missing on a cache hit
- codegen and api-client:build hash with dependentTasksOutputFiles, because the
  document is gitignored and a path input would match nothing and cache forever

Targets net9.0 to match the SDK pinned in mise.toml.
@netlify

netlify Bot commented Aug 18, 2026

Copy link
Copy Markdown

Deploy Preview for nx-dev ready!

Name Link
🔨 Latest commit 5f2ee1c
🔍 Latest deploy log https://app.netlify.com/projects/nx-dev/deploys/6a88982f7e86360008609cb7
😎 Deploy Preview https://deploy-preview-36701--nx-dev.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@netlify

netlify Bot commented Aug 18, 2026

Copy link
Copy Markdown

Deploy Preview for nx-docs ready!

Name Link
🔨 Latest commit 5f2ee1c
🔍 Latest deploy log https://app.netlify.com/projects/nx-docs/deploys/6a88982f851ede00079fec82
😎 Deploy Preview https://deploy-preview-36701--nx-docs.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@nx-cloud

nx-cloud Bot commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

View your CI Pipeline Execution ↗ for commit 5f2ee1c

Command Status Duration Result
nx affected --targets=lint,oxlint,test,build,e2... ✅ Succeeded 16m 44s View ↗
nx run-many -t check-imports check-lock-files c... ✅ Succeeded 3s View ↗
nx-cloud record -- pnpm nx-cloud conformance:check ✅ Succeeded 1m 1s View ↗
nx build workspace-plugin ✅ Succeeded <1s View ↗
nx-cloud record -- nx sync:check ✅ Succeeded 19s View ↗
nx-cloud record -- nx format:check ✅ Succeeded 4s View ↗

☁️ Nx Cloud last updated this comment at 2026-08-21 19:26:17 UTC

@AgentEnder

Copy link
Copy Markdown
Member Author

The blog post walking through this example is up as a draft at nrwl/nx-blog#65. The post links back to this directory, so that link stays broken until this lands.

@socket-security

socket-security Bot commented Aug 18, 2026

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addednuget/​microsoft.net.test.sdk@​17.14.110010090100100
Addednuget/​xunit.runner.visualstudio@​3.1.49610090100100
Addednuget/​xunit@​2.9.310010090100100

View full report

@socket-security

socket-security Bot commented Aug 18, 2026

Copy link
Copy Markdown

Warning

Review the following alerts detected in dependencies.

According to your organization's Security Policy, it is recommended to resolve "Warn" alerts. Learn more about Socket for GitHub.

Action Severity Alert  (click "▶" to expand/collapse)
Warn High
Obfuscated code: nuget system.reflection.metadata is 90.0% likely obfuscated

Confidence: 0.90

Location: Package overview

From: examples/dotnet/basic/tests/Catalog.Tests/Catalog.Tests.csprojnuget/microsoft.net.test.sdk@17.14.1nuget/system.reflection.metadata@8.0.0

ℹ Read more on: This package | This alert | What is obfuscated code?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Packages should not obfuscate their code. Consider not using packages with obfuscated code.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore nuget/system.reflection.metadata@8.0.0. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

View full report

…a CVE

Socket flagged GHSA-w7jw-789q-3m8p (critical) on shell-quote 1.8.3, pulled in
transitively through @openapitools/openapi-generator-cli 2.24.0 -> concurrently.

2.24.0 was an arbitrary pin on my part; 2.40.1 was already current. It widens
its concurrently range, which resolves shell-quote to 1.9.0, above the 1.8.4
patched version.

The Java generator itself stays pinned at 7.16.0 in openapitools.json, so the
generated client is unchanged. Re-verified after the bump: nx build web green
across all five tasks, breaking the C# record still fails api-client:build with
TS2322, restoring it goes back to green, and pnpm audit reports no known
vulnerabilities.
@AgentEnder

Copy link
Copy Markdown
Member Author

Fixed in 2bc7510.

shell-quote@1.8.3 came in transitively via @openapitools/openapi-generator-cli@2.24.0 -> concurrently. The 2.24.0 pin was arbitrary on my part; 2.40.1 was already current and widens its concurrently range, which resolves shell-quote to 1.9.0, above the 1.8.4 patched version.

The Java generator stays pinned at 7.16.0 in openapitools.json, so the generated client output is unchanged by this.

Re-verified after the bump rather than assuming:

  • nx build web green across all five tasks
  • breaking the C# record still fails api-client:build with TS2322
  • restoring it returns to green
  • pnpm audit --audit-level=high: no known vulnerabilities

No @SocketSecurity ignore needed.

…ame clash

CI failed on nx sync:check and nx format:check.

sync:check was the real one. The example's apps/web/project.json declared a
project named "web", which collides with packages/web in this repo, and the
duplicate name made both workspace plugins fail to create project
configurations. The error surfaced as an AggregateError naming
copy-assets-plugin and examples-plugin, with the underlying cause not shown,
so it read like an unrelated plugin problem.

Renamed the project and its directory to "storefront", which is unique.
Verified: without the example, sync:check passed; with it, it failed; after the
rename, it passes again.

format:check wanted prettier over the README and the two new project.json
files. Ran nx format:write.

The example still behaves the same: nx build storefront runs all five tasks and
the chain is unchanged at
storefront:build -> api-client:build -> Api:codegen -> Api:build -> Api:restore.
main-linux failed with:

  Sandbox violation detected for task api-client:oxlint: 1 unexpected read(s).

Root cause is structural rather than that one task. Every other example in this
repo is a single root-level project: nx.json and package.json at its root and
nothing nested. This one has interior project.json files so it can demonstrate a
cross-project graph edge, and those interior projects land in the ROOT graph,
where root plugins attach targets to them and the root affected sweep runs them.
They cannot work there, because this workspace's node_modules only exists after
its own pnpm install.

Two changes:

- nx.json excludes examples/**/* from @nx/dotnet and @nx/oxlint, matching what
  @nx/js/typescript and @nx/playwright/plugin already do. Without it the root
  graph gave the example's csproj a full set of dotnet targets and gave the TS
  projects an oxlint target.

- The example's interior targets are named `compile` rather than `build`. The
  root CI sweep covers lint, oxlint, test, build, e2e, run and others; `compile`
  is in none of them. Verified beforehand that `nx run api-client:build` from
  the repo root does fail, so this was a live problem and not a theoretical one.

Verified after: root format:check and sync:check pass, no interior project
exposes a swept target name, and inside the example
`nx compile storefront` still runs the full chain
storefront:compile -> api-client:compile -> Api:codegen -> Api:build -> Api:restore.
@AgentEnder

Copy link
Copy Markdown
Member Author

Pushed 564ee4b347 for the main-linux failure. This one touches shared nx.json, so it wants a maintainer opinion.

The failure was Sandbox violation detected for task api-client:oxlint, but the cause is structural.

Every other example here is a single root-level projectnx.json + package.json at its root, nothing nested. angular-rspack has thirteen variants and none has an interior project.json. This example has interior projects on purpose, so it can show a cross-project graph edge (storefront imports @example/api-client, and Nx derives the edge from the import). Those interior projects land in the root graph, where root plugins attach targets and the root affected sweep runs them. They cannot succeed there, since this workspace's node_modules only exists after its own pnpm install.

Two changes:

  1. nx.json now excludes examples/**/* from @nx/dotnet and @nx/oxlint, matching what @nx/js/typescript and @nx/playwright/plugin already do. Before this, the root graph handed the example's .csproj a full set of dotnet targets and gave the TS projects an oxlint target.
  2. The example's interior targets are named compile, not build. The CI sweep covers lint,oxlint,test,build,e2e,e2e-ci,format-native,lint-native,gradle:build-ci,vale,run,validate-example; compile is in none of them. I confirmed nx run api-client:build from the repo root genuinely fails, so this was live rather than theoretical.

Verified locally: format:check and sync:check pass, no interior project exposes a swept target name, and inside the example nx compile storefront runs the full chain.

Worth pushing back on if you'd rather. The alternatives I considered and rejected: .nxignore on the interior would hide those files from validate-example's inputs, so example changes would stop invalidating it — silent staleness, worse than the bug. Dropping the interior projects entirely would match convention but removes the graph edge this example exists to demonstrate. If you'd prefer a different shape, say so and I'll restructure.

nx-cloud[bot]

This comment was marked as outdated.

nx-cloud Bot and others added 3 commits August 18, 2026 20:54
Co-authored-by: AgentEnder <AgentEnder@users.noreply.github.com>
Replaces the hand-written `compile` targets with the `typecheck` target
@nx/js/typescript infers from each tsconfig. A single target default,
`typecheck: { dependsOn: ["^typecheck"] }`, is enough to make it walk the graph,
so the TypeScript side now needs no per-project build config at all.

Two things this surfaced, both worth reading:

- libs/api-client declares `implicitDependencies: ["Api"]`. The generated client
  is gitignored, so there is no import for Nx to derive an edge from. Without
  the declaration, editing a C# record left both TypeScript projects looking
  unaffected.

- Its typecheck also needs `dependentTasksOutputFiles` on `inputs`, for the same
  reason codegen does. This was observed rather than predicted: with only
  dependsOn wired, breaking the C# record left the pipeline green, because the
  gitignored generated client never entered the task hash and the typecheck
  replayed a stale cache entry.

tsconfig.base.json uses `declaration` rather than `composite`. Both satisfy
`tsc --build --emitDeclarationOnly`, but composite makes the ROOT workspace's
typescript-sync add project references into the repo's own tsconfig.json,
pointing at an example that cannot build outside its own workspace. The example
also disables @nx/js:typescript-sync for itself, so the two workspaces' sync
generators do not fight over the same files.

Verified: root format:check and sync:check pass, root tsconfig.json is
untouched, no interior project exposes a CI-swept target name, `pnpm validate`
passes, and breaking then restoring the C# record is caught and cleared
deterministically across repeated cycles with caching enabled.
CI failed with:

  Sandbox violation detected for task
  examples-dotnet-openapi-typescript:validate-example:
  12 unexpected read(s), 60 unexpected write(s).

The examples plugin declared `{projectRoot}/dist` and `{projectRoot}/*/dist`,
which describes a flat example. examples/react/basic writes ./dist and fits;
examples/dotnet/openapi-typescript has apps/ and libs/, so everything it
produces lands two levels down (apps/storefront/dist, libs/api-client/dist,
libs/api-client/src/generated) plus MSBuild's apps/Api/{bin,obj} and the
generated OpenAPI document. None of that matched, and task sandboxing rejects
writes the outputs do not cover.

Widened to the union at any depth: dist, bin, obj, openapi, generated and
tsbuildinfo files. Verified both the flat and the nested example resolve the
same list, format:check and sync:check pass, and workspace-plugin still builds.
@AgentEnder

Copy link
Copy Markdown
Member Author

Pushed 85c299638b. Second shared-tooling change on this PR, so flagging it explicitly.

CI failed with:

Sandbox violation detected for task examples-dotnet-openapi-typescript:validate-example:
12 unexpected read(s), 60 unexpected write(s).

examples-plugin.ts declared outputs: ['{projectRoot}/dist', '{projectRoot}/*/dist'], which describes a flat example. examples/react/basic writes ./dist and fits. This example has apps/ and libs/, so everything it produces lands two levels down:

writes matched before?
apps/storefront/dist, libs/api-client/dist no
libs/api-client/src/generated no
apps/Api/{bin,obj,openapi} no

{projectRoot}/*/dist matches apps/dist, not apps/storefront/dist. Task sandboxing rejects any write the outputs do not cover, hence all 60.

Widened the glob to the union at any depth: dist, bin, obj, openapi, generated, *.tsbuildinfo. Verified the flat and nested examples resolve the same list, format:check and sync:check pass, and workspace-plugin still builds.


Worth saying plainly: this is the fourth time this example has collided with tooling that assumes a flat, single-project example.

  1. Project name web clashed with packages/web
  2. Root @nx/dotnet and @nx/oxlint attached targets to its interior projects
  3. The root affected sweep ran interior build targets that cannot work outside the inner workspace
  4. This one

Each was a real bug and each fix was small, but the pattern is consistent, and it has now cost two changes to shared config (nx.json, examples-plugin.ts). The example is deliberately nested so it can demonstrate a cross-project graph edge, which is the point of it. If you would rather examples stay flat and this be restructured instead, say so and I will — I would rather redo it than have the repo carry accommodations it does not want.

AgentEnder and others added 3 commits August 18, 2026 19:03
Second half of the validate-example sandbox violation; the writes were covered
in the previous commit. Diagnosed from the actual Nx Cloud sandbox reports
rather than guessed:

  npx nx-cloud get sandbox-reports --branch 36701 --since 1d
  npx nx-cloud validate sandbox-violations <index.json> --json

Ten of the twelve unexpected reads were the example's OWN source files
(apps/Api/Program.cs, libs/api-client/src/index.ts, and so on). `default` is
{projectRoot}/**/* and Nx assigns each file to the project that owns it, so an
example whose interior directories are themselves projects in this graph loses
its own sources from that glob. Addressing the tree literally with
{workspaceRoot}/<exampleRoot>/**/* covers them regardless of ownership, and
resolves per example (examples/react/basic gets its own path).

The other two were .editorconfig and Directory.Build.props, read from the repo
root because MSBuild and Roslyn walk ancestor directories up from the project
file. Declared those explicitly, alongside tsconfig.base.json, which the root
tsconfig.json extends and which the existing tsconfig.json input never covered.
The remaining MSBuild and NuGet ancestor files are listed whether or not they
exist today, so adding one later does not quietly start tripping sandboxing.

After the change the task validates clean: 12 reads reconciled, 0 still
unexpected, 0 writes. format:check, sync:check and workspace-plugin:build pass.
Reverts both changes to examples-plugin.ts and puts the same declarations in
examples/dotnet/openapi-typescript/project.json instead, using the "..." spread
so the plugin's inferred inputs and outputs are kept and appended to.

The plugin edits were too broad. Every one of the reads they covered belongs to
this example: the sandbox report attributed .editorconfig and
Directory.Build.props to this task alone, and they are read because MSBuild and
Roslyn walk ancestor directories from a .csproj. No other example is a .NET
workspace, so declaring MSBuild and NuGet ancestor files for all of them was
noise. Same for the deeper outputs globs, which exist only because this example
has apps/ and libs/ while the others are flat.

The plugin's own comment warns against a project.json in an example, on the
grounds that the example's workspace also reads it and implicit dependencies on
projects that exist only in this graph would fail validation there. That
applies to implicit dependencies; this file declares inputs and outputs only.
Verified the inner workspace still resolves its projects and `pnpm validate`
still passes with the file present.

Validated against the real report rather than by inspection: the task reports
ok: true, 12 reads reconciled, 0 still unexpected. Root format:check and
sync:check pass, and tools/workspace-plugin is byte-identical to master again.

This leaves nx.json as the only file this PR changes outside examples/dotnet.

@nx-cloud nx-cloud Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Nx Cloud has identified a flaky task in your failed CI:

🔂 Since the failure was identified as flaky, we triggered a CI rerun by adding an empty commit to this branch.

Nx Cloud View detailed reasoning in Nx Cloud ↗


🎓 Learn more about Self-Healing CI on nx.dev

The full-stack OpenAPI story is moving to nrwl/nx-examples, where it can sit
next to a real front end. What belongs here is a small example of the plugin
itself, so this replaces examples/dotnet/openapi-typescript with
examples/dotnet/basic.

Three projects, no project.json anywhere:

  apps/Api             minimal web API
  libs/Catalog         class library the API references
  tests/Catalog.Tests  xunit project covering the library

Everything is inferred from the .csproj files. Verified rather than described:
Api gets run and publish, Catalog gets pack, Catalog.Tests gets test, and
<ProjectReference> becomes a graph edge, so nx build Api builds Catalog first.
nx.json adds "dependsOn": ["...", "restore"] to build, because the inferred
build runs dotnet build --no-restore and pnpm validate has to work from a clean
checkout.

Dropping the TypeScript half removes most of what the previous shape needed:

- No interior project.json or package.json, so nothing from the example lands
  in this repo's graph, and the sandbox inputs and outputs it needed are gone.
- The @nx/oxlint exclude is gone too; with no TypeScript in the example there
  is nothing for it to attach to. Confirmed by removing it and checking.
- The @nx/dotnet exclude stays. Without it the root graph infers targets for
  the example's .csproj files, including run, which the affected sweep runs.

That leaves one added line in nx.json as this PR's only change outside
examples/, down from three shared files.
@AgentEnder AgentEnder changed the title feat(repo): add .NET + OpenAPI + TypeScript codegen example feat(repo): add a .NET example for @nx/dotnet Aug 21, 2026
`validate-example` failed sandboxing with 2 unexpected reads and 161
unexpected writes.

The reads: MSBuild and Roslyn discover `Directory.Build.props` and
`.editorconfig` by walking parent directories, and neither stops at a
workspace boundary, so the example was climbing out to the Nx repo root and
picking up both. That is a sandbox violation, but more to the point it means
the example did not build the way a copy of it would for a user. Give it its
own `Directory.Build.props` (carrying the same `UseSharedCompilation=false`
the root file was after) and an `.editorconfig` with `root = true`, so the
walk terminates inside the example.

The writes: `dotnet build`/`test` fill bin/ and obj/ next to each .csproj, 164
files locally. Nothing downstream consumes them and obj/ embeds absolute
NuGet paths, so they are excluded in the sandboxing config rather than
declared as outputs of the wrapper. Scoped to this project and target.

Verified by adding a sentinel property to the root Directory.Build.props and
confirming the example no longer sees it, while UseSharedCompilation stays
false; all three projects build with 0 warnings and the test passes.
The writes exclusion landed, but `validate-example` then failed with 131
unexpected reads, all of them the same bin/ and obj/ files the task had just
written: restore writes obj/project.assets.json for the build to consume, and
the test run loads its assemblies out of bin/.

They are this task's own scratch in both directions, so the exclusion applies
to reads as well. Anchored so the two lists cannot drift apart.

Confirmed from the sandbox report: 87 reads under tests/Catalog.Tests/bin, 16
under tests/Catalog.Tests/obj, 14 under libs/Catalog/obj, 11 under
apps/Api/obj and 3 under libs/Catalog/bin. Nothing outside bin/ or obj/.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant